""" Twitter - access to Twitter through bot commands Copyright (C) 2010 Brian S. Stephan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import oauth2 as oauth import re import thread import time import urlparse import MySQLdb as mdb from extlib import twitter from Module import Module class Twitter(Module): """Access Twitter via the bot as an authenticated client.""" def __init__(self, irc, config): """Prepare for oauth stuff (but don't execute it yet).""" Module.__init__(self, irc, config) # setup regexes getstatuspattern = "^!twitter\s+getstatus(\s+nosource)?(\s+noid)?\s+(\S+)$" getuserstatuspattern = "^!twitter\s+getuserstatus(\s+nosource)?(\s+noid)?\s+(\S+)(\s+.*|$)" tweetpattern = "^!twitter\s+tweet\s+(.*)" gettokenpattern = "^!twitter\s+gettoken$" authpattern = "^!twitter\s+auth\s+(\S+)$" replytopattern = "^!twitter\s+replyto\s+(\S+)\s+(.*)" self.getstatusre = re.compile(getstatuspattern) self.getuserstatusre = re.compile(getuserstatuspattern) self.tweetre = re.compile(tweetpattern) self.gettokenre = re.compile(gettokenpattern) self.authre = re.compile(authpattern) self.replytore = re.compile(replytopattern) # prep oauth magic self.consumer_key = 'N2aSGxBP8t3cCgWyF1B2Aw' self.consumer_secret = '0aQPEV4K3MMpicfi2lDtCP5pvjsKaqIpfuWtsPzx8' self.request_token_url = "https://api.twitter.com/oauth/request_token" self.access_token_url = "https://api.twitter.com/oauth/access_token" self.authorize_url = "https://api.twitter.com/oauth/authorize" self.consumer = oauth.Consumer(self.consumer_key, self.consumer_secret) self.client = oauth.Client(self.consumer) # settings # force timeline check to wait 5 minutes (for channel joins and antispam) self.next_timeline_check = time.time() + 300 # try getting the stored auth tokens and logging in (oauth_token, oauth_token_secret) = self._retrieve_stored_auth_tokens() if oauth_token is not None and oauth_token_secret is not None: self._log_in_to_twitter(oauth_token, oauth_token_secret) if self._verify_credentials(): # self.twit is set # ugly, but the best place to track it. store the connection for later use self.connection = irc.server # print timeline stuff. this will set up the appropriate timer self._check_self_timeline() self.log.debug("Logged in to Twitter with saved token.") else: self.log.error("Could not log in to Twitter with saved token.") self.twit = twitter.Api() else: # create a default twitter API account, in case we never auth self.twit = twitter.Api() thread.start_new_thread(self.thread_do, ()) def db_init(self): """Set up the settings table.""" # init the table if it doesn't exist version = self.db_module_registered(self.__class__.__name__) if version == None or version < 1: db = self.get_db() # create tables try: version = 1 cur = db.cursor(mdb.cursors.DictCursor) cur.execute(""" CREATE TABLE twitter_settings ( since_id BIGINT(20) UNSIGNED NOT NULL, output_channel VARCHAR(64) NOT NULL, oauth_token VARCHAR(256) DEFAULT NULL, oauth_token_secret VARCHAR(256) DEFAULT NULL ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin """) cur.execute("""INSERT INTO twitter_settings (since_id, output_channel) VALUES (0, '#dr.botzo')""") db.commit() self.db_register_module_version(self.__class__.__name__, version) except mdb.Error as e: db.rollback() self.log.error("database error trying to create tables") self.log.exception(e) raise finally: cur.close() def shutdown(self): """Deauth, and make the twitter API item inoperable.""" Module.shutdown(self) self.twit.ClearCredentials() def do(self, connection, event, nick, userhost, what, admin_unlocked): """Attempt to do twitter things.""" if self.getstatusre.search(what): return self.irc.reply(event, self.twitter_getstatus(connection, event, nick, userhost, what, admin_unlocked)) elif self.getuserstatusre.search(what): return self.irc.reply(event, self.twitter_getuserstatus(connection, event, nick, userhost, what, admin_unlocked)) elif self.tweetre.search(what): return self.irc.reply(event, self.twitter_tweet(connection, event, nick, userhost, what, admin_unlocked)) elif self.replytore.search(what): return self.irc.reply(event, self.twitter_replyto(connection, event, nick, userhost, what, admin_unlocked)) elif self.gettokenre.search(what): return self.irc.reply(event, self.twitter_gettoken(connection, event, nick, userhost, what, admin_unlocked)) elif self.authre.search(what): return self.irc.reply(event, self.twitter_auth(connection, event, nick, userhost, what, admin_unlocked)) def twitter_getstatus(self, connection, event, nick, userhost, what, admin_unlocked): """Get a status by tweet ID.""" match = self.getstatusre.search(what) if match: print_source = True print_id = True if match.group(1): print_source = False if match.group(2): print_id = False status = match.group(3) try: tweet = self.twit.GetStatus(status) return self._return_tweet_or_retweet_text(tweet=tweet, print_source=print_source, print_id=print_id) except twitter.TwitterError as e: return "Couldn't obtain status: " + str(e) def twitter_getuserstatus(self, connection, event, nick, userhost, what, admin_unlocked): """Get a status for a user. Allows for getting one other than the most recent.""" match = self.getuserstatusre.search(what) if match: print_source = True print_id = True if match.group(1): print_source = False if match.group(2): print_id = False user = match.group(3) index = match.group(4) try: if index: index = int(index) if index > 0: index = 0 else: index = 0 except ValueError as e: self.log.error("Couldn't convert index: " + str(e)) index = 0 count = (-1*index) + 1 try: tweets = self.twit.GetUserTimeline(screen_name=user, count=count, include_rts=True) if tweets: tweet = tweets[-1*index] return self._return_tweet_or_retweet_text(tweet=tweet, print_source=print_source, print_id=print_id) except twitter.TwitterError as e: return "Couldn't obtain status: " + str(e) except ValueError as e: return "Couldn't obtain status: " + str(e) def twitter_tweet(self, connection, event, nick, userhost, what, admin_unlocked): """Tweet. Needs authentication.""" match = self.tweetre.search(what) if match: tweet = match.group(1) if self._verify_credentials() is None: return "You must be authenticated to tweet." if admin_unlocked is False: return "Only admins can tweet." try: if self.twit.PostUpdates(tweet, continuation='\xe2\x80\xa6') is not None: return "'{0:s}' tweeted.".format(tweet) else: return "Unknown error sending tweet(s)." except twitter.TwitterError as e: return "Couldn't tweet: " + str(e) def twitter_replyto(self, connection, event, nick, userhost, what, admin_unlocked): """Reply to a tweet, in the twitter in_reply_to_status_id sense. Needs authentication.""" match = self.replytore.search(what) if match: status_id = match.group(1) tweet = match.group(2) if self._verify_credentials() is None: return "You must be authenticated to tweet." if admin_unlocked is False: return "Only admins can tweet." replyee_tweet = self.twit.GetStatus(status_id) target = replyee_tweet.user.screen_name.encode('utf-8', 'ignore') try: reptweet = "@{0:s}: {1:s}".format(target, tweet) if self.twit.PostUpdate(reptweet, in_reply_to_status_id=status_id) is not None: return "'{0:s}' tweeted.".format(tweet) else: return "Unknown error sending tweet." except twitter.TwitterError as e: return "Couldn't tweet: " + str(e) def twitter_gettoken(self, connection, event, nick, userhost, what, admin_unlocked): """Get an oauth token, so that the user may authenticate the bot.""" match = self.gettokenre.search(what) if match: # get request token resp, content = self.client.request(self.request_token_url, "GET") if resp['status'] != '200': raise Exception("Invalid response %s." % resp['status']) self.request_token = dict(urlparse.parse_qsl(content)) # have the user auth return "Go to the following link in your browser: %s?oauth_token=%s and then send me the pin." % (self.authorize_url, self.request_token['oauth_token']) def twitter_auth(self, connection, event, nick, userhost, what, admin_unlocked): """Authenticate, given a PIN (following gettoken).""" match = self.authre.search(what) if match: if self._verify_credentials() is not None: return "The bot is already logged in!" authtoken = match.group(1) oauth_verifier = authtoken # request access token token = oauth.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret']) token.set_verifier(oauth_verifier) client = oauth.Client(self.consumer, token) resp, content = client.request(self.access_token_url, "POST") access_token = dict(urlparse.parse_qsl(content)) self._log_in_to_twitter(access_token['oauth_token'], access_token['oauth_token_secret']) if self._verify_credentials() is not None: # ugly, but the best place to track it. store the connection for later use self.connection = connection # print timeline stuff. this will set up the appropriate timer self._check_self_timeline() return "The bot is now logged in." else: return "Could not log in with supplied credentials." def _verify_credentials(self): """Wrap the exceptions in the twitter client VerifyExceptions().""" try: return self.twit.VerifyCredentials() except Exception: return None def _log_in_to_twitter(self, oauth_token, oauth_token_secret): """Do the actual authentication against Twitter.""" # create the twitter API object self.twit = twitter.Api(self.consumer_key, self.consumer_secret, oauth_token, oauth_token_secret) if self._verify_credentials() is not None: # save the auth token for later reuse self._persist_auth_tokens(oauth_token, oauth_token_secret) def thread_do(self): """Check the timeline.""" while not self.is_shutdown: self._check_self_timeline() time.sleep(1) def _check_self_timeline(self): """Check my timeline, and if there are entries, print them to the channel.""" if self.next_timeline_check < time.time(): self.next_timeline_check = time.time() + 300 if self._verify_credentials() is not None: # get the id of the last check we made since_id = self._get_last_since_id() output_channel = self._get_output_channel() if since_id is not None and output_channel != '': # TODO: make this an option in the settings, just # disabled in code for now #tweets = self.twit.GetFriendsTimeline(since_id=since_id) #tweets.reverse() #for tweet in tweets: # tweet_text = self._return_tweet_or_retweet_text(tweet=tweet, print_source=True) # self.sendmsg(self.connection, output_channel.encode('utf-8', 'ignore'), tweet_text) # ## friends timeline printed, find the latest id #new_since_id = self._get_latest_tweet_id(tweets, since_id) tweets = self.twit.GetMentions(since_id=since_id) tweets.reverse() for tweet in tweets: tweet_text = self._return_tweet_or_retweet_text(tweet=tweet, print_source=True) self.sendmsg(self.connection, output_channel.encode('utf-8', 'ignore'), tweet_text) # mentions printed, find the latest id new_since_id = self._get_latest_tweet_id(tweets, new_since_id) # set since_id self._set_last_since_id(new_since_id) def _return_tweet_or_retweet_text(self, tweet, print_source=False, print_id=True): """ Return a string of the author and text body of a status, accounting for whether or not the fetched status is a retweet. """ reply = "" if tweet.retweeted_status: retweet = tweet.retweeted_status if print_source: reply = "@%s (RT @%s): %s" % (tweet.user.screen_name.encode('utf-8', 'ignore'), retweet.user.screen_name.encode('utf-8', 'ignore'), super(Twitter, self)._unencode_xml(retweet.text.encode('utf-8', 'ignore'))) else: reply = "(RT @%s): %s" % (retweet.user.screen_name.encode('utf-8', 'ignore'), super(Twitter, self)._unencode_xml(retweet.text.encode('utf-8', 'ignore'))) else: if print_source: reply = "@%s: %s" % (tweet.user.screen_name.encode('utf-8', 'ignore'), super(Twitter, self)._unencode_xml(tweet.text.encode('utf-8', 'ignore'))) else: reply = "%s" % (super(Twitter, self)._unencode_xml(tweet.text.encode('utf-8', 'ignore'))) if print_id: reply = reply + " [{0:d}]".format(tweet.id) return reply def _get_last_since_id(self): """Get the since_id out of the database.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = "SELECT since_id FROM twitter_settings" cur.execute(query) result = cur.fetchone() if result: return result['since_id'] except mdb.Error as e: self.log.error("database error getting last since ID") self.log.exception(e) raise finally: cur.close() def _get_output_channel(self): """Get the output_channel out of the database.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = "SELECT output_channel FROM twitter_settings" cur.execute(query) result = cur.fetchone() if result: return result['output_channel'] except mdb.Error as e: self.log.error("database error getting output channel") self.log.exception(e) raise finally: cur.close() def _set_last_since_id(self, since_id): """Set the since_id.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = "UPDATE twitter_settings SET since_id = %s" cur.execute(statement, (since_id,)) db.commit() except mdb.Error as e: self.log.error("database error saving last since ID") self.log.exception(e) raise finally: cur.close() def _get_latest_tweet_id(self, tweets, since_id): """Find the latest tweet id in the provided list, or the given since_id.""" latest = since_id for tweet in tweets: if tweet.id > latest: latest = tweet.id return latest def _persist_auth_tokens(self, oauth_token, oauth_token_secret): """Save the auth tokens to the database, with the intent of reusing them.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) statement = "UPDATE twitter_settings SET oauth_token = %s, oauth_token_secret = %s" cur.execute(statement, (oauth_token, oauth_token_secret)) db.commit() except mdb.Error as e: self.log.error("database error saving auth tokens") self.log.exception(e) raise finally: cur.close() def _retrieve_stored_auth_tokens(self): """Check the database for existing auth tokens, try reusing them.""" db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = "SELECT oauth_token, oauth_token_secret FROM twitter_settings" cur.execute(query) result = cur.fetchone() if result: return (result['oauth_token'], result['oauth_token_secret']) except mdb.Error as e: self.log.error("database error retrieving auth tokens") self.log.exception(e) raise finally: cur.close() # vi:tabstop=4:expandtab:autoindent # kate: indent-mode python;indent-width 4;replace-tabs on;