X-Git-Url: https://jfr.im/git/z_archive/twitter.git/blobdiff_plain/772fbdd143ceee9a1626672f8abadd3378cdb64a..0469b7aaf9714edca95c03d5609a67a03d7d7103:/twitter/ircbot.py diff --git a/twitter/ircbot.py b/twitter/ircbot.py index 2ff425b..876007d 100644 --- a/twitter/ircbot.py +++ b/twitter/ircbot.py @@ -1,69 +1,256 @@ +""" +twitterbot + A twitter IRC bot. Twitterbot connected to an IRC server and idles in + a channel, polling a twitter account and broadcasting all updates to + friends. + +USAGE + + twitterbot [config_file] + +CONFIG_FILE + + The config file is an ini-style file that must contain the following: + +[irc] +server: +port: +nick: +channel: + +[twitter] +email: +password: + + If no config file is given "twitterbot.ini" will be used by default. + +""" + +BOT_VERSION = "TwitterBot 0.4 (mike.verdone.ca/twitter)" + +IRC_BOLD = chr(0x02) +IRC_ITALIC = chr(0x16) +IRC_UNDERLINE = chr(0x1f) +IRC_REGULAR = chr(0x0f) + +import sys import time from dateutil.parser import parse +from ConfigParser import SafeConfigParser +from heapq import heappop, heappush +import traceback + +from api import Twitter, TwitterError +from util import htmlentitydecode try: import irclib except: - raise Exception("This module requires python_irclib") + raise ImportError( + "This module requires python irclib available from " + + "http://python-irclib.sourceforge.net/") + +def debug(msg): + # uncomment this for debug text stuff + # print >> sys.stderr, msg + pass class SchedTask(object): def __init__(self, task, delta): self.task = task self.delta = delta - self.next = time.time() + delta - + self.next = time.time() + + def __repr__(self): + return "" %( + self.task.__name__, self.next, self.delta) + + def __cmp__(self, other): + return cmp(self.next, other.next) + + def __call__(self): + return self.task() + class Scheduler(object): def __init__(self, tasks): - self.tasks = sorted(tasks, lambda x,y: cmp(x.delta, y.delta)) + self.task_heap = [] + for task in tasks: + heappush(self.task_heap, task) def next_task(self): now = time.time() - task = self.tasks.pop(0) + task = heappop(self.task_heap) wait = task.next - now + task.next = now + task.delta + heappush(self.task_heap, task) if (wait > 0): time.sleep(wait) - task.task() - task.next = now + task.delta - for idx in range(len(self.tasks)): - if self.tasks[idx].next > task.next: - break - self.tasks.insert(idx, task) + task() + debug("tasks: " + str(self.task_heap)) def run_forever(self): - try: - while True: - self.next_task() - except KeyboardInterrupt: - pass + while True: + self.next_task() + class TwitterBot(object): - def __init__(self, twitter, twitter_users, server, port, nick): - self.server = server - self.port = port - self.nick = nick - self.twitter = twitter - self.twitter_user_dict = {} - now = time.gmtime() - for user in twitter_users: - self.twitter_user_dict[user] = now + def __init__(self, configFilename): + self.configFilename = configFilename + self.config = load_config(self.configFilename) self.irc = irclib.IRC() - self.server = self.irc.server() + self.irc.add_global_handler('privmsg', self.handle_privmsg) + self.irc.add_global_handler('ctcp', self.handle_ctcp) + self.ircServer = self.irc.server() + self.twitter = Twitter( + self.config.get('twitter', 'email'), + self.config.get('twitter', 'password')) self.sched = Scheduler( - (SchedTask(self.check_statuses, 60),)) + (SchedTask(self.process_events, 1), + SchedTask(self.check_statuses, 120))) + self.lastUpdate = time.gmtime() def check_statuses(self): - for user, last_update in self.twiter_users.items(): - updates = self.twitter.statuses.user_timeline( - id=user, count=1) - if (updates): - latest = updates[0] - crt = parse(latest['created_at']).utctimetuple() - if (crt > last_update): - self.server. - self.twitter_user_dict[user] = crt + debug("In check_statuses") + try: + updates = self.twitter.statuses.friends_timeline() + except Exception, e: + print >> sys.stderr, "Exception while querying twitter:" + traceback.print_exc(file=sys.stderr) + return + + nextLastUpdate = self.lastUpdate + for update in updates: + crt = parse(update['created_at']).utctimetuple() + if (crt > self.lastUpdate): + text = (htmlentitydecode( + update['text'].replace('\n', ' ')) + .encode('utf-8', 'replace')) + + # Skip updates beginning with @ + # TODO This would be better if we only ignored messages + # to people who are not on our following list. + if not text.startswith("@"): + self.privmsg_channel( + u"=^_^= %s%s%s %s" %( + IRC_BOLD, update['user']['screen_name'], + IRC_BOLD, text.decode('utf-8'))) + + nextLastUpdate = crt + else: + break + self.lastUpdate = nextLastUpdate + + def process_events(self): + debug("In process_events") + self.irc.process_once() + + def handle_privmsg(self, conn, evt): + debug('got privmsg') + args = evt.arguments()[0].split(' ') + try: + if (not args): + return + if (args[0] == 'follow' and args[1:]): + self.follow(conn, evt, args[1]) + elif (args[0] == 'unfollow' and args[1:]): + self.unfollow(conn, evt, args[1]) + else: + conn.privmsg( + evt.source().split('!')[0], + "=^_^= Hi! I'm Twitterbot! you can (follow " + + ") to make me follow a user or " + + "(unfollow ) to make me stop.") + except Exception: + traceback.print_exc(file=sys.stderr) + + def handle_ctcp(self, conn, evt): + args = evt.arguments() + source = evt.source().split('!')[0] + if (args): + if args[0] == 'VERSION': + conn.ctcp_reply(source, "VERSION " + BOT_VERSION) + elif args[0] == 'PING': + conn.ctcp_reply(source, "PING") + elif args[0] == 'CLIENTINFO': + conn.ctcp_reply(source, "CLIENTINFO PING VERSION CLIENTINFO") + + def privmsg_channel(self, msg): + return self.ircServer.privmsg( + self.config.get('irc', 'channel'), msg.encode('utf-8')) + + def follow(self, conn, evt, name): + userNick = evt.source().split('!')[0] + friends = [x['name'] for x in self.twitter.statuses.friends()] + debug("Current friends: %s" %(friends)) + if (name in friends): + conn.privmsg( + userNick, + "=O_o= I'm already following %s." %(name)) + else: + try: + self.twitter.friendships.create(id=name) + except TwitterError: + conn.privmsg( + userNick, + "=O_o= I can't follow that user. Are you sure the name is correct?") + return + conn.privmsg( + userNick, + "=^_^= Okay! I'm now following %s." %(name)) + self.privmsg_channel( + "=o_o= %s has asked me to start following %s" %( + userNick, name)) + + def unfollow(self, conn, evt, name): + userNick = evt.source().split('!')[0] + friends = [x['name'] for x in self.twitter.statuses.friends()] + debug("Current friends: %s" %(friends)) + if (name not in friends): + conn.privmsg( + userNick, + "=O_o= I'm not following %s." %(name)) + else: + self.twitter.friendships.destroy(id=name) + conn.privmsg( + userNick, + "=^_^= Okay! I've stopped following %s." %(name)) + self.privmsg_channel( + "=o_o= %s has asked me to stop following %s" %( + userNick, name)) + def run(self): - self.server.connect(server, port, nick) - self.server.join(self.channel) - self.sched.run_forever() + self.ircServer.connect( + self.config.get('irc', 'server'), + self.config.getint('irc', 'port'), + self.config.get('irc', 'nick')) + self.ircServer.join(self.config.get('irc', 'channel')) + + while True: + try: + self.sched.run_forever() + except KeyboardInterrupt: + break + except TwitterError: + # twitter.com is probably down because it sucks. ignore the fault and keep going + pass + +def load_config(filename): + defaults = dict(server=dict(port=6667, nick="twitterbot")) + cp = SafeConfigParser(defaults) + cp.read((filename,)) + return cp +def main(): + configFilename = "twitterbot.ini" + if (sys.argv[1:]): + configFilename = sys.argv[1] + try: + load_config(configFilename) + except: + print >> sys.stderr, "Error loading ini file %s" %( + configFilename) + print __doc__ + sys.exit(1) + bot = TwitterBot(configFilename) + return bot.run()