class DbAccessYoutubeRefTest(unittest.TestCase): def setUp(self): config = ConfigParser.ConfigParser() results = config.read('gthx.config.local') if not results: raise SystemExit("Failed to read config file 'gthx.config.local'") dbUser = config.get('MYSQL', 'GTHX_MYSQL_USER') dbPassword = config.get('MYSQL', 'GTHX_MYSQL_PASSWORD') dbName = config.get('MYSQL', 'GTHX_MYSQL_DATABASE') self.db = DbAccess(dbUser, dbPassword, dbName) def test_youtube_refs(self): testItem = "I7nVrT00ST4" testTitle = "Pro Riders Laughing" # Verify that referencing an item the first time causes the ref count to # be set to 1 rows = self.db.addYoutubeRef(testItem) self.assertEquals( len(rows), 1, "First youtube ref returned the wrong number of rows.") data = rows[0] self.assertEquals( data[0], 1, "First youtube ref returned wrong number of references.") self.assertIsNone( data[1], "First youtube ref returned a title when it shouldn't have.") rows = self.db.addYoutubeRef(testItem) self.assertEquals( len(rows), 1, "First youtube ref returned the wrong number of rows.") data = rows[0] self.assertEquals( data[0], 2, "Second youtube ref returned wrong number of references.") self.assertIsNone( data[1], "Second youtube ref returned a title when it shouldn't have.") self.db.addYoutubeTitle(testItem, testTitle) rows = self.db.addYoutubeRef(testItem) self.assertEquals( len(rows), 1, "First youtube ref returned the wrong number of rows.") data = rows[0] self.assertEquals( data[0], 3, "Third youtube ref returned wrong number of references.") self.assertEquals( data[1], testTitle, "Third youtube ref returned the wrong title: '%s'" % data[1]) def tearDown(self): self.db.deleteAllYoutubeRefs()
class Gthx(irc.IRCClient): """An IRC bot for #reprap.""" restring = "" def __init__(self, dbUser, dbPassword, dbDatabase, nickservPassword): self.db = DbAccess(dbUser, dbPassword, dbDatabase) # Just setting this variable sets the nickserv login password # (Maybe? We still do our own procesing later) self.password = nickservPassword print "Connected to MySQL server" self.trackedpresent = dict() self.gotwhoischannel = False self.seenQuery = re.compile( "\s*seen\s+([a-zA-Z\*_\\\[\]\{\}^`|\*][a-zA-Z0-9\*_\\\[\]\{\}^`|-]*)[\s\?]*" ) self.tellQuery = re.compile( "\s*tell\s+([a-zA-Z\*_\\\[\]\{\}^`|\*][a-zA-Z0-9\*_\\\[\]\{\}^`|-]*)\s*(.+)" ) self.factoidQuery = re.compile( "(.+)[?!](\s*$|\s*\|\s*([a-zA-Z\*_\\\[\]\{\}^`|\*][a-zA-Z0-9\*_\\\[\]\{\}^`|-]*)$)" ) self.factoidSet = re.compile("(.+?)\s(is|are)(\salso)?\s(.+)") self.googleQuery = re.compile( "\s*google\s+(.*?)\s+for\s+([a-zA-Z\*_\\\[\]\{\}^`|\*][a-zA-Z0-9\*_\\\[\]\{\}^`|-]*)" ) self.thingMention = re.compile( "http(s)?:\/\/www.thingiverse.com\/thing:(\d+)", re.IGNORECASE) self.youtubeMention = re.compile( "http(s)?:\/\/(www\.youtube\.com\/watch\?v=|youtu\.be\/)([\w\-]*)(\S*)", re.IGNORECASE) self.uptimeStart = datetime.now() self.lurkerReplyChannel = "" def connectionMade(self): if self.password: self.log("IRC Connection made -- sending CAP REQ") self.sendLine('CAP REQ :sasl') else: self.log("IRC Connection made - no nickserv password") irc.IRCClient.connectionMade(self) def irc_CAP(self, prefix, params): self.log("Got irc_CAP") if params[1] != 'ACK' or params[2].split() != ['sasl']: print 'sasl not available' self.quit('') sasl = ('{0}\0{0}\0{1}'.format( self.nickname, self.password)).encode('base64').strip() self.sendLine('AUTHENTICATE PLAIN') self.sendLine('AUTHENTICATE ' + sasl) def irc_903(self, prefix, params): self.log("Got SASL connection successful.") self.sendLine('CAP END') def irc_904(self, prefix, params): print 'sasl auth failed', params self.quit('') def connectionLost(self, reason): irc.IRCClient.connectionLost(self, reason) self.log("[disconnected at %s]" % time.asctime(time.localtime(time.time()))) self.emailClient.send( "%s disconnected" % self.nickname, "%s is disconnected from the server.\n\n%s" % (self.nickname, reason)) def log(self, message): """Write a message to the screen.""" timestamp = time.strftime("[%H:%M:%S]", time.localtime(time.time())) print '%s %s' % (timestamp, message) # callbacks for events def signedOn(self): """Called when bot has succesfully signed on to server.""" self.log("Signed on to the IRC server") self.channelList = [ channel for channel in self.factory.channels.split(',') ] for channelm in self.channelList: self.log("Joining channel %s" % channelm) self.join(channelm) if (trackednick == None): self.trackedpresent[channelm] = False self.gotwhoischannel = False # kthx uses: "\s*(${names})[:;,-]?\s*" to match nicks if (trackednick): self.matchNick = "(%s|%s)(:|;|,|-|\s)+(.+)" % (self.nickname, trackednick) print "Querying WHOIS %s at startup" % trackednick self.whois(trackednick) else: self.matchNick = "(%s)(:|;|,|-|\s)+(.+)" % (self.nickname) print "Running in standalone mode." def joined(self, channel): """Called when the bot joins the channel.""" self.log("[I have joined %s as '%s']" % (channel, self.nickname)) message = "I have joined channel %s as '%s'\n" % (channel, self.nickname) self.emailClient.threadsend("%s connected" % self.nickname, message) def userJoined(self, user, channel): """ Called when I see another user joining a channel. """ print "%s joined channel %s" % (user, channel) # TODO: Change this to verify the IP address before setting it if trackednick and (user == trackednick) and (self.trackedpresent[channel] == False): self.trackedpresent[channel] = True print "%s is here!" % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has joined channel %s" % (user, channel)) def userLeft(self, user, channel): """ Called when I see another user leaving a channel. """ print "%s left channel %s" % (user, channel) if trackednick and (user == trackednick) and (self.trackedpresent[channel]): self.trackedpresent[channel] = False print "%s is gone." % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has left channel %s" % (user, channel)) def userQuit(self, user, quitMessage): """ Called when I see another user disconnect from the network. """ safeQuitMessage = quitMessage.decode("utf-8") print "%s disconnected : %s" % (user, safeQuitMessage) if trackednick and (user == trackednick): for channel in self.channelList: self.trackedpresent[channel] = False print "%s is gone." % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has quit: %s" % (user, quitMessage)) def userKicked(self, kickee, channel, kicker, message): """ Called when I observe someone else being kicked from a channel. """ print "In %s, %s kicked %s : %s" % (channel, kicker, kickee, message) if trackednick and (kickee == trackednick) and (self.trackedpresent[channel]): self.trackedpresent[channel] = False print "%s is gone." % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has been kicked from %s by %s: %s" % (kickee, channel, kicker, message)) def userRenamed(self, oldname, newname): """ A user changed their name from oldname to newname. """ print "%s renamed to %s" % (oldname, newname) if (trackednick == None): return if oldname == trackednick: for channel in self.channelList: self.trackedpresent[channel] = False print "%s is gone." % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has been renamed to %s" % (oldname, newname)) if newname == trackednick: self.whois(trackednick) print "%s is here!" % trackednick self.emailClient.threadsend( "%s status" % self.nickname, "%s has been renamed to %s--checking WHOIS" % (oldname, newname)) def irc_unknown(self, prefix, command, params): print "Unknown command '%s' '%s' '%s'" % (prefix, command, params) if (command == 'RPL_NAMREPLY'): if (self.lurkerReplyChannel == ""): return users = params[3].split() for user in users: self.channelCount = self.channelCount + 1 rows = self.db.seen(user) if len(rows) == 0: self.lurkerCount = self.lurkerCount + 1 elif (command == 'RPL_ENDOFNAMES'): if (self.lurkerReplyChannel == ""): return print "Got RPL_ENDOFNAMES" self.msg( self.lurkerReplyChannel, "%d of the %d users in %s right now have never said anything." % (self.lurkerCount, self.channelCount, params[1])) self.lurkerReplyChannel = "" def irc_RPL_WHOISCHANNELS(self, prefix, params): """This method is called when the client recieves a reply for whois. params[0]: requestor params[1]: nick requested params[2]: list of channels in common """ print "Got WHOISCHANNELS with prefix '%s' and params '%s'" % (prefix, params) print "%s is in channels %s" % (params[1], params[2]) self.gotwhoischannel = True trackedchannels = params[2].translate(None, '@').split(" ") for channel in self.channelList: if channel in trackedchannels: self.trackedpresent[channel] = True print "%s is in %s!!" % (params[1], channel) self.emailClient.threadsend( "%s status" % self.nickname, "%s is in channel %s" % (params[1], params[2])) else: self.trackedpresent[channel] = False print "%s is NOT in %s!!" % (params[1], channel) def irc_RPL_WHOISUSER(self, prefix, params): print "Got WHOISUSER with prefix '%s' and params '%s'" % (prefix, params) def irc_RPL_WHOISSERVER(self, prefix, params): print "Got WHOISSERVER with prefix '%s' and params '%s'" % (prefix, params) def irc_RPL_WHOISOPERATOR(self, prefix, params): print "Got WHOISOPERATOR with prefix '%s' and params '%s'" % (prefix, params) def irc_RPL_WHOISIDLE(self, prefix, params): print "Got WHOISIDLE with prefix '%s' and params '%s'" % (prefix, params) def irc_RPL_ENDOFWHOIS(self, prefix, params): print "Got ENDOFWHOIS with prefix '%s' and params '%s'" % (prefix, params) if not self.gotwhoischannel: if (trackednick != None): print "No response from %s. Must not be present." % trackednick for channel in self.channelList: self.trackedpresent[channel] = False self.emailClient.threadsend( "%s status" % self.nickname, "%s is not in channel %s" % (trackednick, channel)) def getFactoidString(self, query): answer = self.db.getFactoid(query) if answer: for i, factoid in enumerate(answer): if i == 0: if factoid[3].startswith( "<reply>") or factoid[3].startswith("<action>"): fstring = factoid[3] break else: fstring = query fstring += "%s" % " are " if factoid[2] else " is " if i > 0: fstring += "also " fstring += factoid[3] if i < len(answer) - 1: fstring += " and" return fstring else: return None def moodToString(self, mood): if mood < -100: return "suicidal!" if mood < -50: return "really depressed." if mood < -10: return "depressed." if mood < 0: return "kinda bummed." if mood == 0: return "meh, okay I guess." if mood < 10: return "alright." if mood < 50: return "pretty good." return "great, Great, GREAT!!" def privmsg(self, user, channel, msg): """Called when the bot receives a message, both public and private.""" user = user.split('!', 1)[0] # By default, don't reply to anything canReply = False private = False replyChannel = channel parseMsg = msg directAddress = False # Debug print ALL messages #print "Message from '%s' on '%s': '%s'" % (user, channel, msg) # Check to see if they're sending me a private message if channel == self.nickname: canReply = True private = True replyChannel = user self.log("Private message from %s: %s" % (user, msg)) if str.lower(user) == "nickserv": self.log("Nickserv says: %s" % msg) if parseMsg.startswith("whois "): whoisnick = parseMsg.split(" ", 1)[1] print "Doing a whois '%s'" % whoisnick self.whois(whoisnick) # Update the seen database, but only if it's not a private message if channel in self.channelList and not private: self.db.updateSeen(user, channel, msg) # If kthx said something, mark him as here and ignore everything he says if user == trackednick and not private: if (self.trackedpresent[channel] == False): self.trackedpresent[channel] = True self.emailClient.threadsend( "%s status" % self.nickname, "%s spoke in %s unexpectedly and got marked as present: %s" % (user, channel, msg)) return # If kthx is gone, then we can always reply if not private and not self.trackedpresent[channel]: canReply = True # Check to see if we have a tell waiting for this user tells = self.db.getTell(user) if tells: for message in tells: print "Found tell for '%s' from '%s'" % (user, message[Tell.author]) author = message[Tell.author] timestring = timesincestring(message[Tell.timestamp]) text = message[Tell.message] inTracked = message[Tell.inTracked] # We have 3 cases: # 1) kthx was around when this tell happened and is still around now. # In this case, we assume kthx will relay the message and just delete it # 2) kthx was around when this tell happened and is not here now. # In this case, we want to send the message and mention that kthx may repeat it # 3) kthx was not around when this tell happened and may or may not be here now # Whether or not kthx is now here, we need to say the message # 4) gthx was specifically addressed for this tell # Whether or not kthx is now here, we need to say the message # # If we can't reply, it means that kthx is present. In that # case, the tell has already been erased, so in both cases, # we're good. if canReply or not inTracked: if inTracked: self.msg( replyChannel, "%s: %s ago <%s> tell %s %s (%s may repeat this)" % (user, timestring, author, user, text, trackednick)) else: self.msg( replyChannel, "%s: %s ago <%s> tell %s %s" % (user, timestring, author, user, text)) # Check for specifically addressed messages m = re.match(self.matchNick, parseMsg) if m: print "Found message addressed to '%s'. My nick is '%s'." % ( m.group(1), self.nickname) parseMsg = m.group(3) # Mark it as a direct address so we can look for a factoid directAddress = True # If it's addressed directly to me, we can reply if m.group(1) == self.nickname: canReply = True # Check for status query if canReply and parseMsg == "status?": if (trackednick): if (private): reply = "%s: OK; Up for %s; " % ( VERSION, timesincestring(self.uptimeStart)) for channel in self.channelList: reply += "%s %s; " % (channel, "PRESENT" if self.trackedpresent[channel] else "GONE") else: reply = "%s: OK; Up for %s; %s is %s" % ( VERSION, timesincestring( self.uptimeStart), trackednick, "PRESENT" if self.trackedpresent[channel] else "GONE") else: reply = "%s: OK; Up for %s; standalone mode" % ( VERSION, timesincestring(self.uptimeStart)) mood = self.db.mood() reply += " mood: %s" % self.moodToString(mood) self.msg(replyChannel, reply) return # Check for lurker query if canReply and parseMsg == "lurkers?": self.msg(replyChannel, "Looking for lurkers...") self.lurkerReplyChannel = replyChannel self.lurkerCount = 0 self.channelCount = 0 print "Sending request 'NAMES %s'" % channel self.sendLine("NAMES %s" % channel) return # Check for tell query m = self.tellQuery.match(parseMsg) if m and directAddress: print "Got tell from '%s' for '%s' message '%s'." % ( user, m.group(1), m.group(2)) # The is in the tracked bot if the tracked bot is present and it was not a message # specifically directed to us. This is a little tricky since the only way to know # that a message was specifically directed to us is to see if it was a direct address # and we can reply success = self.db.addTell( user, m.group(1), m.group(2), not (directAddress and canReply) and self.trackedpresent[channel]) if success and canReply: self.msg( replyChannel, "%s: I'll pass that on when %s is around." % (user, m.group(1))) return # Check for seen query if canReply: m = self.seenQuery.match(parseMsg) if m: queryname = m.group(1) print "%s asked about '%s'" % (user, queryname) rows = self.db.seen(queryname) if len(rows) == 0: reply = "Sorry, I haven't seen %s." % queryname self.msg(replyChannel, reply) for i, row in enumerate(rows): reply = "%s was last seen in %s %s ago saying '%s'." % ( row[Seen.name], row[Seen.channel], timesincestring( row[Seen.timestamp]), row[Seen.message]) self.msg(replyChannel, reply) if i >= 2: # Don't reply more than 3 times to a seen query break return # Check for google query if directAddress: if canReply: m = self.googleQuery.match(parseMsg) if m: queryname = urllib.quote_plus(m.group(1)) foruser = m.group(2) print "%s asked to google '%s' for %s" % (user, queryname, foruser) reply = "%s: http://lmgtfy.com/?q=%s" % (foruser, queryname) self.msg(replyChannel, reply) return # Check for setting a factoid factoid = None if directAddress: factoid = self.factoidSet.match(parseMsg) if factoid: invalidwords = re.match( '(here|how|it|something|that|this|what|when|where|which|who|why|you)', factoid.group(1), re.IGNORECASE) if not invalidwords: safeFactoid = factoid.group(1).decode("utf-8") print "%s tried to set factoid '%s'." % (user, safeFactoid) success = self.db.addFactoid( user, factoid.group(1), True if factoid.group(2) == 'are' else False, factoid.group(4), True if not factoid.group(3) else False) if canReply: if success: self.msg(replyChannel, "%s: Okay." % user) else: self.msg( replyChannel, "I'm sorry, %s. I'm afraid I can't do that." % user) # Check for getting a factoid if canReply: f = self.factoidQuery.match(parseMsg) if f: safeFactoid = f.group(1).decode("utf-8") print "factoid query from %s:%s for '%s'" % (user, channel, safeFactoid) answer = self.getFactoidString(f.group(1)) if answer: # Replace !who and !channel in the reply answer = re.sub("!who", user, answer) answer = re.sub("!channel", channel, answer) if answer.startswith("<reply>"): answer = answer[7:] if answer.startswith("<action>"): self.describe(replyChannel, answer[8:]) else: if (f.group(3)): answer = "%s, %s" % (f.group(3), answer) self.msg(replyChannel, answer) # Check for info request if canReply and parseMsg.startswith("info "): query = parseMsg[5:] if query[-1:] == "?": query = query[:-1] safeFactoid = query.decode("utf-8") print "info request for '%s' ReplyChannel is '%s'" % (safeFactoid, replyChannel) refcount = 0 answer = self.db.infoFactoid(query) if answer: count = answer[0][6] if not count: count = "0" print "Factoid '%s' has been referenced %s times" % ( safeFactoid, count) self.msg( replyChannel, "Factoid '%s' has been referenced %s times" % (query, count)) for factoid in answer: user = factoid[3] value = factoid[2] if not user: user = "******" if value: print "At %s, %s set to: %s" % (factoid[4], user, value) self.msg( replyChannel, "At %s, %s set to: %s" % (factoid[4], user, value)) else: print "At %s, %s deleted this item" % (factoid[4], user) self.msg( replyChannel, "At %s, %s deleted this item" % (factoid[4], user)) else: print "No info for factoid '%s'" % safeFactoid self.msg(replyChannel, "Sorry, I couldn't find an entry for %s" % query) # Check for forget request if directAddress and parseMsg.startswith("forget "): query = parseMsg[7:] print "forget request for '%s'" % query forgotten = self.db.forgetFactoid(query, user) if canReply: if forgotten: self.msg(replyChannel, "%s: I've forgotten about %s" % (user, query)) else: self.msg( replyChannel, "%s: Okay, but %s didn't exist anyway" % (user, query)) # Check for thingiverse mention if canReply: match = self.thingMention.search(parseMsg) if match: thingId = int(match.group(2)) print "Match for thingiverse query item %s" % thingId rows = self.db.addThingiverseRef(thingId) refs = int(rows[0][0]) title = rows[0][1] if title is None: print "Attemping to get title for thingiverse ID %s" % thingId agent = Agent(reactor) titleQuery = agent.request( 'GET', 'https://www.thingiverse.com/thing:%s' % thingId, Headers({'User-Agent': ['gthx IRC bot']}), None) def titleResponse(title): if title: title = unescape(title) self.db.addThingiverseTitle(thingId, title) print "The title for thing %s is: %s " % (thingId, title) reply = 'https://www.thingiverse.com/thing:%s => %s => %s IRC mentions' % ( thingId, title, refs) self.msg(replyChannel, reply) else: print "No title found for thing %s" % (thingId) reply = 'https://www.thingiverse.com/thing:%s => ???? => %s IRC mentions' % ( thingId, refs) self.msg(replyChannel, reply) def queryResponse(response): if response.code == 200: finished = Deferred() finished.addCallback(titleResponse) response.deliverBody(TitleParser(finished)) return finished print "Got error response from thingiverse query: %s" % ( response) titleResponse(None) return None titleQuery.addCallback(queryResponse) else: print "Already have a title for thing %s: %s" % (thingId, title) reply = 'https://www.thingiverse.com/thing:%s => %s => %s IRC mentions' % ( thingId, title, refs) self.msg(replyChannel, reply) # Check for youtube mention if canReply: match = self.youtubeMention.search(parseMsg) if match: youtubeId = match.group(3) fullLink = match.group(0) print "Match for youtube query item %s" % youtubeId rows = self.db.addYoutubeRef(youtubeId) refs = int(rows[0][0]) title = rows[0][1] if title is None: print "Attemping to get title for youtubeId %s" % youtubeId agent = Agent(reactor) titleQuery = agent.request( 'GET', 'https://www.youtube.com/watch?v=%s' % youtubeId, Headers({'User-Agent': ['gthx IRC bot']}), None) def titleResponse(title): if title: title = unescape(title) self.db.addYoutubeTitle(youtubeId, title) print "The title for video %s is: %s " % ( youtubeId, title) reply = '%s => %s => %s IRC mentions' % ( fullLink, title, refs) print "Reply is: %s" % reply self.msg(replyChannel, reply) print "Message sent." else: print "No title found for youtube video %s" % ( youtubeId) reply = '%s => ???? => %s IRC mentions' % ( fullLink, refs) self.msg(replyChannel, reply) def queryResponse(response): if response.code == 200: finished = Deferred() finished.addCallback(titleResponse) response.deliverBody(TitleParser(finished)) return finished print "Got error response from youtube query: %s:%s" % ( response.code, response.phrase) pprint(list(response.headers.getAllRawHeaders())) titleResponse(None) return None titleQuery.addCallback(queryResponse) else: print "Already have a title for item %s: %s" % (youtubeId, title) reply = '%s => %s => %s IRC mentions' % (fullLink, title, refs) self.msg(replyChannel, reply) def action(self, sender, channel, message): m = re.match( "([a-zA-Z\*_\\\[\]\{\}^`|\*][a-zA-Z0-9\*_\\\[\]\{\}^`|-]*)", sender) if m: sender = m.group(1) print "* %s %s" % (sender, message) self.db.updateSeen(sender, channel, "* %s %s" % (sender, message))