Completed
Push — master ( a25d8f...57824c )
by Matt
01:23
created

QDB.__init__()   B

Complexity

Conditions 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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