Completed
Push — master ( 3fb799...a93322 )
by Matt
01:03
created

QDB   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 363
Duplicated Lines 13.77 %
Metric Value
dl 50
loc 363
rs 1.5789
wmc 86

12 Methods

Rating   Name   Duplication   Size   Complexity  
A add_recently_submitted() 0 9 2
A __init__() 0 21 4
A strip_formatting() 0 3 1
F format_line() 0 37 11
F handle() 0 60 12
B recently_submitted() 0 21 6
B _imgurify() 0 23 6
F add_buffer() 50 50 14
B delete() 0 23 6
A _detect_url() 0 14 2
B submit() 0 36 5
F get_qdb_submission() 0 54 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like QDB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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