+"""
+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: <irc_server>
+port: <irc_port>
+nick: <irc_nickname>
+channel: <irc_channel_to_join>
+
+[twitter]
+email: <twitter_account_email>
+password: <twitter_account_password>
+
+ If no config file is given "twitterbot.ini" will be used by default.
+"""
+
+BOT_VERSION = "TwitterBot 1.0 (http://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
+import os.path
+
+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 "<SchedTask %s next:%i delta:%i>" %(
+ 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 "
+ + "<twitter_name>) to make me follow a user or "
+ + "(unfollow <twitter_name>) 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,))
+
+ # attempt to read these properties-- they are required
+ cp.get('twitter', 'email'),
+ cp.get('twitter', 'password')
+ cp.get('irc', 'server')
+ cp.getint('irc', 'port')
+ cp.get('irc', 'nick')
+ cp.get('irc', 'channel')
+
+ return cp
+
+
+# Howdy, hacker!! You've found the secret Twitter business model!!
+#
+# 1. provide awesome status-update service
+# 2. buy a lot of new hardware to keep it running
+# 3. ???
+# 4. profit!
+#
+# I'm just kidding... :3
+
+
+def main():
+ configFilename = "twitterbot.ini"
+ if (sys.argv[1:]):
+ configFilename = sys.argv[1]
+
+ try:
+ if not os.path.exists(configFilename):
+ raise Exception()
+ load_config(configFilename)
+ except Exception, e:
+ print >> sys.stderr, "Error while loading ini file %s" %(
+ configFilename)
+ print >> sys.stderr, e
+ print >> sys.stderr, __doc__
+ sys.exit(1)
+ bot = TwitterBot(configFilename)
+ return bot.run()