qdb.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from event import Event
  2. import re
  3. import imgurpython
  4. try:
  5. import requests
  6. except ImportError:
  7. print "Warning: QDB module requires requests."
  8. requests = object
  9. import difflib
  10. class QDB:
  11. def __init__(self, events=None, printer_handle=None, bot=None, say=None):
  12. self.events = events
  13. self.printer = printer_handle
  14. self.interests = ['__.qdb__', '1__all_lines__'] # should be first event in the listing.. so lines being added is a priority
  15. self.bot = bot
  16. self.say = say
  17. self.imgur_client_id = "6f4e468a474bb6e"
  18. self.imgur_client_secret = "22f791df5569e7964a1ca78637125c94cba6f312"
  19. self.bot.mem_store['qdb'] = {}
  20. #define a key for _recent since that will not be a potential channel name
  21. self.bot.mem_store['qdb']['_recent'] = []
  22. for event in events:
  23. if event._type in self.interests:
  24. event.subscribe(self)
  25. self.help = ".qdb <search string of first line> | <search string of last line>"
  26. self.MAX_BUFFER_SIZE = 200
  27. self.MAX_HISTORY_SIZE = 10
  28. def _imgurify(self, url):
  29. client = imgurpython.ImgurClient(self.imgur_client_id, self.imgur_client_secret)
  30. replacement_values = list()
  31. if type(url) is list:
  32. for u in url:
  33. resp = client.upload_from_url(u)
  34. replacement_values.append(resp)
  35. else:
  36. resp = client.upload_from_url(url)
  37. replacement_values.append(resp)
  38. return replacement_values
  39. def _detect_url(self, quote):
  40. """
  41. right now this is strictly for tsdbot's printout functionality
  42. follows this format:
  43. http://irc.teamschoolyd.org/printouts/8xnK5DmfMz.jpg
  44. """
  45. try:
  46. url = re.search("(?P<url>http://irc.teamschoolyd.org/printouts/.+\.jpg)", quote).group("url")
  47. except AttributeError: # we didn't find anything
  48. return quote
  49. repl = self._imgurify(url)
  50. new_quote = re.sub('(?P<url>http://irc.teamschoolyd.org/printouts/.+\.jpg)',repl[0]['link'], quote)
  51. return new_quote
  52. def add_buffer(self, event=None, debug=False):
  53. """Takes a channel name and line passed to it and stores them in the bot's mem_store dict
  54. for future access. The dict will have channel as key. The value to that key will be a list
  55. of formatted lines of activity.
  56. If the buffer size is not yet exceeded, lines are just added. If the buffer
  57. is maxed out, the oldest line is removed and newest one inserted at the beginning.
  58. """
  59. if debug:
  60. print "Line: " + event.line
  61. print "Verb: " + event.verb
  62. print "Channel: " + event.channel
  63. print ""
  64. if not event:
  65. return
  66. #there are certain things we want to record in history, like nick changes and quits
  67. #these often add to the humor of a quote. however, these are not specific to a channel
  68. #in IRC and our bot does not maintain a userlist per channel. Therefore, when nick
  69. #changes and quits occur, we will add them to every buffer. This is not technically
  70. #correct behavior and could very well lead to quits/nick changes that are not visible
  71. #showing up in a quote, but it's the best we can do at the moment
  72. if not event.channel:
  73. #discard events with unwanted verbs
  74. if event.verb not in ["QUIT", "NICK"]:
  75. return
  76. try:
  77. for chan in self.bot.mem_store['qdb'].keys():
  78. if chan != '_recent':
  79. if len(self.bot.mem_store['qdb'][chan]) >= self.MAX_BUFFER_SIZE:
  80. self.bot.mem_store['qdb'][chan].pop()
  81. line = self.format_line(event)
  82. if line:
  83. self.bot.mem_store['qdb'][chan].insert(0, line)
  84. except KeyError, IndexError:
  85. print "QDB add_buffer() error when no event channel"
  86. #now we continue with normal, per channel line addition
  87. #create a dictionary associating the channel with an empty list if it doesn't exist yet
  88. else:
  89. if event.channel not in self.bot.mem_store['qdb']:
  90. self.bot.mem_store['qdb'][event.channel] = []
  91. try:
  92. #check for the length of the buffer. if it's too long, pop the last item
  93. if len(self.bot.mem_store['qdb'][event.channel]) >= self.MAX_BUFFER_SIZE:
  94. self.bot.mem_store['qdb'][event.channel].pop()
  95. #get a line by passing event to format_line
  96. #insert the line into the first position in the list
  97. line = self.format_line(event)
  98. if line:
  99. self.bot.mem_store['qdb'][event.channel].insert(0, line)
  100. except IndexError:
  101. print "QDB add_buffer() error. Couldn't access the list index."
  102. def format_line(self, event):
  103. """Takes an event and formats a string appropriate for quotation from it"""
  104. #format all strings based on the verb
  105. if event.verb == "":
  106. return ''
  107. elif event.verb == "PRIVMSG":
  108. #special formatting for ACTION strings
  109. if event.msg.startswith('\001ACTION'):
  110. #strip out the word ACTION from the msg
  111. return ' * %s %s\n' % (event.user, event.msg[7:])
  112. else:
  113. return '<%s> %s\n' % (event.user, event.msg)
  114. elif event.verb == "JOIN":
  115. return ' --> %s has joined channel %s\n' % (event.user, event.channel)
  116. elif event.verb == "PART":
  117. return ' <-- %s has left channel %s\n' % (event.user, event.channel)
  118. elif event.verb == "NICK":
  119. return ' -- %s has changed their nick to %s\n' % (event.user, event.msg)
  120. elif event.verb == "TOPIC":
  121. return ' -- %s has changed the topic for %s to "%s"\n' % (event.user, event.channel, event.msg)
  122. elif event.verb == "QUIT":
  123. return ' <-- %s has quit (%s)\n' % (event.user, event.msg)
  124. elif event.verb == "KICK":
  125. #this little bit of code finds the kick target by getting the last
  126. #thing before the event message begins
  127. target = event.line.split(":", 2)[1].split()[-1]
  128. return ' <--- %s has kicked %s from %s (%s)\n' % (event.user, target, event.channel, event.msg)
  129. elif event.verb == "NOTICE":
  130. return ' --NOTICE from %s: %s\n' % (event.user, event.msg)
  131. else:
  132. #no matching verbs found. just ignore the line
  133. return ''
  134. def get_qdb_submission(self, channel=None, start_msg='', end_msg=''):
  135. """Given two strings, start_msg and end_msg, this function will assemble a submission for the QDB.
  136. start_msg is a substring to search for and identify a starting line. end_msg similarly is used
  137. to search for the last desired line in the submission. This function returns a string ready
  138. for submission to the QDB if it finds the desired selection. If not, it returns None.
  139. """
  140. if not channel:
  141. return None
  142. #must have at least one msg to search for and channel to look it up in
  143. if len(start_msg) == 0 or not channel:
  144. return None
  145. #first, check to see if we are doing a single string submission.
  146. if end_msg == '':
  147. for line in self.bot.mem_store['qdb'][channel]:
  148. if start_msg.lower() in line.lower():
  149. return line
  150. #making sure we get out of the function if no matching strings were found
  151. #don't want to search for a nonexistent second string later
  152. return None
  153. #search for a matching start and end string and get the buffer index for the start and end message
  154. start_index = -1
  155. end_index = -1
  156. #finds oldest matching string for beginning line
  157. for index, line in enumerate(self.bot.mem_store['qdb'][channel]):
  158. if start_msg.encode('utf-8','ignore').lower() in line.encode('utf-8','ignore').lower():
  159. start_index = index
  160. #finds newest matching string for ending line
  161. for index, line in enumerate(self.bot.mem_store['qdb'][channel]):
  162. if end_msg.lower() in line.lower():
  163. end_index = index
  164. break
  165. #check to see if index values are positive. if not, string was not found and we're done
  166. if start_index == -1 or end_index == -1 or start_index < end_index:
  167. return None
  168. #now we generate the string to be returned for submission
  169. submission = ''
  170. try:
  171. for i in reversed(range(end_index, start_index + 1)):
  172. #print 'Index number is ' + str(i) + ' and current submission is ' + submission
  173. submission += self.bot.mem_store['qdb'][channel][i]
  174. except IndexError:
  175. print "QDB get_qdb_submission() error when accessing list index"
  176. return self._detect_url(submission)
  177. def submit(self, qdb_submission, debug=False):
  178. """Given a string, qdb_submission, this function will upload the string to hlmtre's qdb
  179. server. Returns a string with status of submission. If it worked, includes a link to new quote.
  180. """
  181. if debug:
  182. print "Submission is:"
  183. print qdb_submission
  184. print "Current buffer is:"
  185. print self.bot.mem_store['qdb']
  186. print ""
  187. return ''
  188. #accessing hlmtre's qdb api
  189. url = 'http://qdb.zero9f9.com/api.php'
  190. payload = {'q':'new', 'quote': qdb_submission.rstrip('\n')}
  191. qdb = requests.post(url, payload)
  192. #check for any HTTP errors and return False if there were any
  193. try:
  194. qdb.raise_for_status()
  195. except requests.exceptions.HTTPError:
  196. return "HTTPError encountered when submitting to QDB"
  197. try:
  198. q_url = qdb.json()
  199. self.add_recently_submitted(q_url['id'], qdb_submission)
  200. return "QDB submission successful! http://qdb.zero9f9.com/quote.php?id=" + str(q_url['id'])
  201. except (KeyError, UnicodeDecodeError):
  202. return "Error getting status of quote submission."
  203. return "That was probably successful since no errors came up, but no status available."
  204. def delete(self, user, post_id='', passcode=''):
  205. """A special function that allows certain users to delete posts"""
  206. #accessing hlmtre's qdb api
  207. url = 'http://qdb.zero9f9.com/api.php'
  208. payload = {'q':'delete', 'user':user, 'id':post_id, 'code':passcode}
  209. deletion = requests.get(url, params=payload)
  210. #check for any HTTP errors and return False if there were any
  211. try:
  212. deletion.raise_for_status()
  213. except requests.exceptions.HTTPError:
  214. return "HTTPError encountered when accessing QDB"
  215. try:
  216. del_status = deletion.json()
  217. if del_status['success'] == "true":
  218. return "QDB deletion succeeded."
  219. return "QDB deletion failed."
  220. except (KeyError, UnicodeDecodeError):
  221. return "Error getting status of quote deletion."
  222. def recently_submitted(self, submission):
  223. """Checks to see if the given submission is string is at least 75% similar to the strings
  224. in the list of recently submitted quotes.
  225. Returns the id of the quote if it was recently submitted. If not, returns -1.
  226. """
  227. #set up a difflib SequenceMatcher with the first string to test
  228. comparer = difflib.SequenceMatcher()
  229. comparer.set_seq1(submission)
  230. #if we find that it has 75% similarity or greater to a recent submission, return True
  231. try:
  232. for recent_quote in self.bot.mem_store['qdb']['_recent']:
  233. comparer.set_seq2(recent_quote.values()[0])
  234. if comparer.ratio() >= .75:
  235. return recent_quote.keys()[0]
  236. except TypeError:
  237. return -1
  238. except KeyError:
  239. return -1
  240. except IndexError:
  241. return -1
  242. return -1
  243. def add_recently_submitted(self, q_id, submission):
  244. """Takes a string, submission, and adds it to the list of recent submissions.
  245. Also we do length checking, only keep record of the previous MAX_HISTORY_SIZE quotes.
  246. """
  247. #first, see if we have reached the maximum history size. if so, remove last item
  248. if len(self.bot.mem_store['qdb']['_recent']) >= self.MAX_HISTORY_SIZE:
  249. self.bot.mem_store['qdb']['_recent'].pop()
  250. #inserting a dict with the qdb id of the submission and the submission content
  251. self.bot.mem_store['qdb']['_recent'].insert(0, {q_id:submission})
  252. def handle(self, event):
  253. #first check to see if there is a special deletion going on
  254. if event.msg.startswith(".qdbdelete") and event.is_pm:
  255. deletion = event.msg.split(' ', 2)
  256. try:
  257. #requires the format ".qdbdelete <post_id> <password>"
  258. self.say(event.user, self.delete(event.user, deletion[1], deletion[2]))
  259. except IndexError:
  260. self.say(event.user, "Not enough parameters provided for deletion.")
  261. return
  262. #we see if we're going to generate a qdb submission, or just add the line to the buffer
  263. if event.msg.startswith(".qdb "):
  264. #split the msg with '.qdb ' stripped off beginning and divide into 1 or 2 search strings
  265. string_token = event.msg[5:].split('|', 1)
  266. start_msg = string_token[0].rstrip()
  267. #see if we only have a one line submission
  268. if len(string_token) == 1:
  269. #s is the string to submit
  270. s = self.get_qdb_submission(event.channel, start_msg)
  271. recent = self.recently_submitted(s)
  272. if recent > 0:
  273. q_url = "http://qdb.zero9f9.com/quote.php?id=" + str(recent)
  274. self.printer("PRIVMSG " + event.channel + " :QDB Error: A quote of >75% similarity has already been posted here: " + q_url + "\n")
  275. return
  276. if not s:
  277. self.printer("PRIVMSG " + event.channel + ' :QDB Error: Could not find requested string.\n')
  278. return
  279. #Print the link to the newly submitted quote
  280. self.printer("PRIVMSG " + event.channel + ' :' + self.submit(s) + '\n')
  281. return
  282. #We should only get here if there are two items in string_token
  283. end_msg = string_token[1].lstrip()
  284. s = self.get_qdb_submission(event.channel, start_msg, end_msg)
  285. recent = self.recently_submitted(s)
  286. if recent > 0:
  287. q_url = "http://qdb.zero9f9.com/quote.php?id=" + str(recent)
  288. self.printer("PRIVMSG " + event.channel + " :QDB Error: A quote of >75% similarity has already been posted here: " + q_url + "\n")
  289. return
  290. #if there's nothing found for the submission, then we alert the channel and gtfo
  291. if not s:
  292. self.printer("PRIVMSG " + event.channel + ' :QDB Error: Could not find requested quotes or parameters were not specific enough.\n')
  293. return
  294. #print the link to the new submission
  295. self.printer("PRIVMSG " + event.channel + ' :' + self.submit(s) + '\n')
  296. return
  297. self.add_buffer(event)