]> jfr.im git - z_archive/twitter.git/blobdiff - twitter/ircbot.py
Added the ability in IRC bot to operate on multiple channels. Config file now accepts...
[z_archive/twitter.git] / twitter / ircbot.py
index af0cb38f997a447652de415347d648eac31ad3f4..afde87c710e106ede490ff62de637397df692d77 100644 (file)
@@ -1,12 +1,48 @@
+"""
+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 ConfigParser
+from ConfigParser import SafeConfigParser
 from heapq import heappop, heappush
 import traceback
+import os.path
 
-from api import Twitter
+from api import Twitter, TwitterError
+from util import htmlentitydecode
 
 try:
     import irclib
@@ -17,7 +53,7 @@ except:
 
 def debug(msg):
     # uncomment this for debug text stuff
-    print >> sys.stderr, msg
+    print >> sys.stderr, msg
     pass
 
 class SchedTask(object):
@@ -46,19 +82,17 @@ class Scheduler(object):
         now = time.time()
         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.next = now + task.delta
-        heappush(self.task_heap, 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, configFilename):
@@ -66,13 +100,14 @@ class TwitterBot(object):
         self.config = load_config(self.configFilename)
         self.irc = irclib.IRC()
         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.process_events, 1),
-             SchedTask(self.check_statuses, 60)))
+             SchedTask(self.check_statuses, 120)))
         self.lastUpdate = time.gmtime()
 
     def check_statuses(self):
@@ -84,17 +119,28 @@ class TwitterBot(object):
             traceback.print_exc(file=sys.stderr)
             return
         
+        nextLastUpdate = self.lastUpdate
         for update in updates:
             crt = parse(update['created_at']).utctimetuple()
             if (crt > self.lastUpdate):
-                self.privmsg_channel(
-                    "=^_^= %s %s" %(
-                        update['user']['screen_name'],
-                        update['text']))
-                self.lastUpdate = crt
+                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_channels(
+                        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()
@@ -117,10 +163,26 @@ class TwitterBot(object):
                     + "(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)
+            self.config.get('irc', 'channel'), msg.encode('utf-8'))
+            
+    def privmsg_channels(self, msg):
+        return_response=True
+        channels=self.config.get('irc','channel').split(',')
+        return self.ircServer.privmsg_many(channels, msg.encode('utf-8'))
             
     def follow(self, conn, evt, name):
         userNick = evt.source().split('!')[0]
@@ -131,11 +193,17 @@ class TwitterBot(object):
                 userNick,
                 "=O_o= I'm already following %s." %(name))
         else:
-            self.twitter.friendships.create(id=name)
+            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(
+            self.privmsg_channels(
                 "=o_o= %s has asked me to start following %s" %(
                     userNick, name))
     
@@ -152,7 +220,7 @@ class TwitterBot(object):
             conn.privmsg(
                 userNick,
                 "=^_^= Okay! I've stopped following %s." %(name))
-            self.privmsg_channel(
+            self.privmsg_channels(
                 "=o_o= %s has asked me to stop following %s" %(
                     userNick, name))
     
@@ -161,21 +229,59 @@ class TwitterBot(object):
             self.config.get('irc', 'server'), 
             self.config.getint('irc', 'port'),
             self.config.get('irc', 'nick'))
-        self.ircServer.join(self.config.get('irc', 'channel'))
-        try:
-            self.sched.run_forever()
-        except KeyboardInterrupt:
-            pass
+        channels=self.config.get('irc', 'channel').split(',')
+        for channel in channels:
+            self.ircServer.join(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 = ConfigParser(defaults)
+    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
 
+# So there was a joke here about the twitter business model
+# but I got rid of it. Not because I want this codebase to
+# be "professional" in any way, but because someone forked
+# this and deleted the comment because they couldn't take
+# a joke. Hi guy!
+#
+# Fact: The number one use of Google Code is to look for that
+# comment in the Linux kernel that goes "FUCK me gently with
+# a chainsaw." Pretty sure Linus himself wrote it.
+
 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()