Issues (12)

modules/qdb.py (1 issue)

1
from event import Event
2
import re
3
import difflib
4
5
try:
6
  import imgurpython
7
except ImportError:
8
  print "Warning: QDB module requires imgurpython."
9
  imgurpython = object
10
11
try:
12
  import requests
13
except ImportError:
14
  print "Warning: QDB module requires requests."
15
  requests = object
16
17
18
class QDB:
19
    def __init__(self, events=None, printer_handle=None, bot=None, say=None):
20
        self.events = events
21
        self.printer = printer_handle
22
        self.interests = ['__.qdb__', '1__all_lines__']  # should be first event in the listing.. so lines being added is a priority
23
        self.bot = bot
24
        self.say = say
25
        try:
26
          from imgur_credentials import ImgurCredentials as ic
27
        except ImportError:
28
            print "Warning: imgur module requires credentials in modules/imgur_credentials.py"
29
            class PhonyIc:
30
                imgur_client_id = "None"
31
                imgur_client_secret = "None"
32
            ic = PhonyIc()
33
        self.imgur_client_id = ic.imgur_client_id
34
        self.imgur_client_secret = ic.imgur_client_secret
35
36
        # prevent unecessarily clearing our mem_store['qdb'] dict
37
        if not "qdb" in self.bot.mem_store:
38
          self.bot.mem_store['qdb'] = {}
39
        #define a key for _recent since that will not be a potential channel name
40
        self.bot.mem_store['qdb']['_recent'] = []
41
42
        for event in events:
43
          if event._type in self.interests:
44
            event.subscribe(self)
45
46
        self.help = ".qdb <search string of first line> | <search string of last line>"
47
        self.MAX_BUFFER_SIZE = 500
48
        self.MAX_HISTORY_SIZE = 10
49
50
    def _imgurify(self, url):
51
        client = imgurpython.ImgurClient(self.imgur_client_id, self.imgur_client_secret)
52
53
        replacement_values = list()
54
55
        if type(url) is list:
56
            for u in url:
57
                resp = client.upload_from_url(u)
58
                replacement_values.append(resp)
59
        else:
60
            try:
61
                resp = client.upload_from_url(url)
62
                replacement_values.append(resp)
63
            except imgurpython.helpers.error.ImgurClientError, e:
64
                self.bot.debug_print("ImgurClientError: ") 
65
                self.bot.debug_print(str(e))
66
            except UnboundLocalError, e:
67
                self.bot.debug_print("UnboundLocalError: ")
68
                self.bot.debug_print(str(e))
69
            except requests.ConnectionError, e:
70
                self.bot.debug_print("ConnectionError: ")
71
                self.bot.debug_print(str(e))
72
        return replacement_values
73
      
74
    def _detect_url(self, quote):
75
        """
76
        for tsd printouts and imgflip (.meme)
77
        follows this format:
78
        http://irc.teamschoolyd.org/printouts/8xnK5DmfMz
79
        http://i.imgflip.com/zs1e6.jpg
80
        """
81
        tsd_regex = "(?P<url>http://irc\.teamschoolyd\.org/printouts/\w+)"
82
        if_regex = "(?P<url>http://i\.imgflip\.com/\w+.jpg)"
83
84
        # to allow us to check both variables at the end
85
        tsd_url, if_url = None, None
86
87
        try:
88
            tsd_url = re.search(tsd_regex, quote).group("url")
89
        except AttributeError:
90
            pass
91
        try:
92
            if_url = re.search(if_regex, quote).group("url")
93
        except AttributeError:
94
            pass
95
96
        if not tsd_url and not if_url:
97
            return quote
98
99
        if tsd_url:
100
          url = tsd_url
101
        if if_url:
102
          url = if_url
103
104
        repl = self._imgurify(url)
105
106
        if tsd_url:
107
          new_quote = re.sub(tsd_regex, repl[0]['link'], quote)
108
        if if_url:
109
          new_quote = re.sub(if_regex, repl[0]['link'], quote)
110
        return new_quote
111
    
112
    def strip_formatting(self, msg):
113
        """Uses regex to replace any special formatting in IRC (bold, colors) with nothing"""
114
        return re.sub('([\x02\x1D\x1F\x16\x0F]|\x03([0-9]{2})?)', '', msg)
115
116 View Code Duplication
    def add_buffer(self, event=None, debug=False): 
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
117
        """Takes a channel name and line passed to it and stores them in the bot's mem_store dict
118
        for future access. The dict will have channel as key. The value to that key will be a list
119
        of formatted lines of activity. 
120
        If the buffer size is not yet exceeded, lines are just added. If the buffer
121
        is maxed out, the oldest line is removed and newest one inserted at the beginning.
122
        """
123
        if debug:
124
            print "Line: " + event.line
125
            print "Verb: " + event.verb
126
            print "Channel: " + event.channel
127
            print ""
128
        if not event:
129
            return
130
        #there are certain things we want to record in history, like nick changes and quits
131
        #these often add to the humor of a quote. however, these are not specific to a channel
132
        #in IRC and our bot does not maintain a userlist per channel. Therefore, when nick
133
        #changes and quits occur, we will add them to every buffer. This is not technically
134
        #correct behavior and could very well lead to quits/nick changes that are not visible
135
        #showing up in a quote, but it's the best we can do at the moment
136
        if not event.channel:
137
            #discard events with unwanted verbs 
138
            if event.verb not in ["QUIT", "NICK"]:
139
                return
140
            try:
141
                for chan in self.bot.mem_store['qdb'].keys():
142
                    if chan != '_recent':
143
                        if len(self.bot.mem_store['qdb'][chan]) >= self.MAX_BUFFER_SIZE:
144
                            self.bot.mem_store['qdb'][chan].pop()
145
                        line = self.format_line(event)
146
                        if line:
147
                            self.bot.mem_store['qdb'][chan].insert(0, line)
148
            except (KeyError, IndexError):
149
                print "QDB add_buffer() error when no event channel"
150
        #now we continue with normal, per channel line addition
151
        #create a dictionary associating the channel with an empty list if it doesn't exist yet
152
        else:
153
            if event.channel not in self.bot.mem_store['qdb']:
154
                self.bot.mem_store['qdb'][event.channel] = []
155
            try:
156
            #check for the length of the buffer. if it's too long, pop the last item
157
                if len(self.bot.mem_store['qdb'][event.channel]) >= self.MAX_BUFFER_SIZE:
158
                    self.bot.mem_store['qdb'][event.channel].pop()
159
                #get a line by passing event to format_line
160
                #insert the line into the first position in the list
161
                line = self.format_line(event) 
162
                if line:
163
                    self.bot.mem_store['qdb'][event.channel].insert(0, line)
164
            except IndexError:
165
                print "QDB add_buffer() error. Couldn't access the list index."
166
167
    def format_line(self, event):
168
        """Takes an event and formats a string appropriate for quotation from it"""
169
170
        # first strip out printout urls and replace them with imgur mirrors
171
        # commenting out for now to avoid uploading to imgur so often
172
        #event.msg = self._detect_url(event.msg)
173
174
        #format all strings based on the verb
175
        if event.verb == "":
176
            return ''
177
        elif event.verb == "PRIVMSG":
178
            #special formatting for ACTION strings
179
            if event.msg.startswith('\001ACTION'): 
180
                #strip out the word ACTION from the msg
181
                return ' * %s %s\n' % (event.user, event.msg[7:])
182
            else:
183
                return '<%s> %s\n' % (event.user, self.strip_formatting(event.msg))
184
        elif event.verb == "JOIN":
185
            return ' --> %s has joined channel %s\n' % (event.user, event.channel)
186
        elif event.verb == "PART":
187
            return ' <-- %s has left channel %s\n' % (event.user, event.channel)
188
        elif event.verb == "NICK":
189
            return ' -- %s has changed their nick to %s\n' % (event.user, event.msg)
190
        elif event.verb == "TOPIC":
191
            return ' -- %s has changed the topic for %s to "%s"\n' % (event.user, event.channel, event.msg)
192
        elif event.verb == "QUIT":
193
            return ' <-- %s has quit (%s)\n' % (event.user, event.msg)
194
        elif event.verb == "KICK":
195
            #this little bit of code finds the kick target by getting the last
196
            #thing before the event message begins
197
            target = event.line.split(":", 2)[1].split()[-1]
198
            return ' <--- %s has kicked %s from %s (%s)\n' % (event.user, target, event.channel, event.msg)
199
        elif event.verb == "NOTICE": 
200
            return ' --NOTICE from %s: %s\n' % (event.user, event.msg)
201
        else:
202
            #no matching verbs found. just ignore the line
203
            return ''
204
205
    def get_qdb_submission(self, channel=None, start_msg='', end_msg='', strict=False):
206
        """Given two strings, start_msg and end_msg, this function will assemble a submission for the QDB.
207
        start_msg is a substring to search for and identify a starting line. end_msg similarly is used
208
        to search for the last desired line in the submission. This function returns a string ready
209
        for submission to the QDB if it finds the desired selection. If not, it returns None.
210
        """
211
        if not channel:
212
            return None
213
        #must have at least one msg to search for and channel to look it up in
214
        if len(start_msg) == 0 or not channel:
215
            return None
216
        #first, check to see if we are doing a single string submission.
217
        if end_msg == '':
218
            for line in self.bot.mem_store['qdb'][channel]:
219
                if start_msg.lower() in line.lower():
220
                    return self._detect_url(line) #removing temporary printout urls and replacing with imgur
221
            #making sure we get out of the function if no matching strings were found
222
            #don't want to search for a nonexistent second string later
223
            return None
224
        #search for a matching start and end string and get the buffer index for the start and end message
225
        start_index = -1
226
        end_index = -1
227
        """Finds matching string for beginning line. Buffer is traversed in reverse-chronological order
228
        .qdb -> strict = False -> earliest occurence
229
        .qdbs -> strict = True -> latest occurence
230
        """
231
        for index, line in enumerate(self.bot.mem_store['qdb'][channel]):
232
            #print "evaluating line for beginning: {}".format(line)
233
            if start_msg.encode('utf-8','ignore').lower() in line.encode('utf-8','ignore').lower():
234
                #print "found match, start_index={}".format(index)
235
                start_index = index
236
                if strict:
237
                    break
238
        #finds newest matching string for ending line
239
        for index, line in enumerate(self.bot.mem_store['qdb'][channel]):
240
            #print "evaluating line for end: {}".format(line)
241
            if end_msg.lower() in line.lower():
242
                #print "found match, end_index={}".format(index)
243
                end_index = index
244
                break
245
        #check to see if index values are positive. if not, string was not found and we're done
246
        if start_index == -1 or end_index == -1 or start_index < end_index:
247
            return None
248
        #now we generate the string to be returned for submission
249
        submission = ''
250
        try:
251
            for i in reversed(range(end_index, start_index + 1)):
252
                #print 'Index number is ' + str(i) + ' and current submission is ' + submission
253
                submission += self._detect_url(self.bot.mem_store['qdb'][channel][i]) #detect temporary printout urls and replace with imgur
254
        except IndexError:
255
            print "QDB get_qdb_submission() error when accessing list index"
256
257
258
        return submission
259
260
    def submit(self, qdb_submission, debug=False):
261
        """Given a string, qdb_submission, this function will upload the string to hlmtre's qdb
262
        server. Returns a string with status of submission. If it worked, includes a link to new quote. 
263
        """ 
264
        if debug:
265
            print "Submission is:"
266
            print qdb_submission
267
            print "Current buffer is:"
268
            print self.bot.mem_store['qdb']
269
            print ""
270
            return ''
271
        #accessing hlmtre's qdb api
272
        url = 'https://qdb.zero9f9.com/api.php'
273
        payload = {'q':'new', 'quote': qdb_submission.rstrip('\n')}
274
        try:
275
          qdb = requests.post(url, payload)
276
        except ConnectionError, e:
277
          self.bot.debug_print("ConnectionError: ")
278
          self.bot.debug_print(str(e))
279
        #check for any HTTP errors and return False if there were any
280
        try:
281
            qdb.raise_for_status()
282
        except requests.exceptions.HTTPError, e:
283
            self.bot.debug_print('HTTPError: ')
284
            self.bot.debug_print(str(e))
285
            self.bot.debug_print("Perhaps informative:")
286
            self.bot.debug_print(url)
287
            self.bot.debug_print(str(payload))
288
            return "HTTPError encountered when submitting to QDB"
289
        try:
290
            q_url = qdb.json()
291
            self.add_recently_submitted(q_url['id'], qdb_submission)
292
            return "QDB submission successful! https://qdb.zero9f9.com/quote.php?id=" + str(q_url['id'])
293
        except (KeyError, UnicodeDecodeError):
294
            return "Error getting status of quote submission." 
295
        return "That was probably successful since no errors came up, but no status available."
296
297
    def delete(self, user, post_id='', passcode=''):
298
        """A special function that allows certain users to delete posts"""
299
        #accessing hlmtre's qdb api
300
        url = 'http://qdb.zero9f9.com/api.php'
301
        payload = {'q':'delete', 'user':user, 'id':post_id, 'code':passcode}
302
        deletion = requests.get(url, params=payload)
303
        #check for any HTTP errors and return False if there were any
304
        try:
305
            deletion.raise_for_status()
306
        except requests.exceptions.HTTPError, e:
307
            self.bot.debug_print('HTTPError: ')
308
            self.bot.debug_print(str(e))
309
            return "HTTPError encountered when accessing QDB"
310
        try:
311
            del_status = deletion.json()
312
            if del_status['success'] == "true":
313
              for quote in self.bot.mem_store['qdb']['_recent']: # they're a list of dicts
314
                if int(post_id) in quote:
315
                  self.bot.mem_store['qdb']['_recent'].remove(quote)
316
              return "QDB deletion succeeded."
317
            return "QDB deletion failed."
318
        except (KeyError, UnicodeDecodeError):
319
            return "Error getting status of quote deletion." 
320
321
    def recently_submitted(self, submission):
322
        """Checks to see if the given submission is string is at least 75% similar to the strings
323
        in the list of recently submitted quotes.
324
        Returns the id of the quote if it was recently submitted. If not, returns -1.
325
        """
326
        #set up a difflib SequenceMatcher with the first string to test
327
        comparer = difflib.SequenceMatcher()
328
        comparer.set_seq1(submission)
329
        #if we find that it has 75% similarity or greater to a recent submission, return True
330
        try:
331
            for recent_quote in self.bot.mem_store['qdb']['_recent']:
332
                comparer.set_seq2(recent_quote.values()[0])
333
                if comparer.ratio() >= .75:
334
                    return recent_quote.keys()[0]
335
        except TypeError:
336
            return -1
337
        except KeyError:
338
            return -1
339
        except IndexError:
340
            return -1
341
        return -1 
342
343
    def add_recently_submitted(self, q_id, submission):
344
        """Takes a string, submission, and adds it to the list of recent submissions.
345
        Also we do length checking, only keep record of the previous MAX_HISTORY_SIZE quotes.
346
        """
347
        #first, see if we have reached the maximum history size. if so, remove last item
348
        if len(self.bot.mem_store['qdb']['_recent']) >= self.MAX_HISTORY_SIZE:
349
            self.bot.mem_store['qdb']['_recent'].pop()
350
        #inserting a dict with the qdb id of the submission and the submission content
351
        self.bot.mem_store['qdb']['_recent'].insert(0, {q_id:submission})
352
353
    def handle(self, event):
354
        #first check to see if there is a special deletion going on
355
        if event.msg.startswith(".qdbdelete") and event.is_pm:
356
            deletion = event.msg.split(' ', 2)
357
            try:
358
                #requires the format ".qdbdelete <post_id> <password>"
359
                self.say(event.user, self.delete(event.user, deletion[1], deletion[2]))
360
            except IndexError:
361
                self.say(event.user, "Not enough parameters provided for deletion.")
362
            return
363
        """
364
        See if we're going to generate a qdb submission, or just add the line to the buffer.
365
        .qdb is the standard, generous implementation selected after hours of testing and ideal for a significant number of situations where lines are repeated. Use specific search strings. the start_index of the submission will be the EARLIEST occurrence of the substring in the buffer.
366
        .qdbs is the strict implementation. The start_index will be the LATEST occurrence of the substring.
367
        """
368
369
        if event.msg.startswith(".qdb ") or event.msg.startswith(".qdbs "):
370
            #split the msg with '.qdb[s] ' stripped off beginning and divide into 1 or 2 search strings
371
            #e.g. ".qdb string1|string2" -> [".qdb", "string1|string2"] 
372
            cmd_parts = event.msg.split(None,1)
373
            if len(cmd_parts) < 2:
374
                #do something here to handle '.qdb[s]'
375
                return
376
            #determine if using strict mode
377
            strict_mode = cmd_parts[0] == ".qdbs"
378
            #split the search parameter(s)
379
            #e.g. "string1|string2" -> ["string1", "string2"]
380
            string_token = cmd_parts[1].split('|', 1)
381
            start_msg = string_token[0].rstrip()
382
            #see if we only have a one line submission
383
            if len(string_token) == 1:
384
                #s is the string to submit
385
                s = self.get_qdb_submission(event.channel, start_msg)
386
                recent = self.recently_submitted(s)
387
                if recent > 0:
388
                    q_url = "http://qdb.zero9f9.com/quote.php?id=" + str(recent)
389
                    self.printer("PRIVMSG " + event.channel + " :QDB Error: A quote of >75% similarity has already been posted here: " + q_url + "\n")
390
                    return
391
                if not s:
392
                    self.printer("PRIVMSG " + event.channel + ' :QDB Error: Could not find requested string.\n')
393
                    return
394
                #Print the link to the newly submitted quote
395
                self.printer("PRIVMSG " + event.channel + ' :' + self.submit(s) + '\n')
396
                return
397
            #We should only get here if there are two items in string_token
398
            end_msg = string_token[1].lstrip()
399
            s = self.get_qdb_submission(event.channel, start_msg, end_msg, strict_mode)
400
            recent = self.recently_submitted(s)
401
            if recent > 0:
402
                q_url = "http://qdb.zero9f9.com/quote.php?id=" + str(recent)
403
                self.printer("PRIVMSG " + event.channel + " :QDB Error: A quote of >75% similarity has already been posted here: " + q_url + "\n")
404
                return
405
            #if there's nothing found for the submission, then we alert the channel and gtfo
406
            if not s: 
407
                self.printer("PRIVMSG " + event.channel + ' :QDB Error: Could not find requested quotes or parameters were not specific enough.\n')
408
                return
409
            #print the link to the new submission
410
            self.printer("PRIVMSG " + event.channel + ' :' + self.submit(s) + '\n')
411
            return
412
        self.add_buffer(event)
413