X-Git-Url: https://jfr.im/git/z_archive/twitter.git/blobdiff_plain/c8d451e84b52bff57d38c9c6bcfcb4ab21281db6..17b9ff10207340026b876eb623660f2c79bfe85d:/twitter/ircbot.py diff --git a/twitter/ircbot.py b/twitter/ircbot.py index fb14c11..6091417 100644 --- a/twitter/ircbot.py +++ b/twitter/ircbot.py @@ -4,7 +4,7 @@ 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] @@ -12,21 +12,34 @@ USAGE CONFIG_FILE The config file is an ini-style file that must contain the following: - + [irc] server: port: nick: -channel: +channel: +prefixes: [twitter] -email: -password: +oauth_token_file: + If no config file is given "twitterbot.ini" will be used by default. + + The channel argument can accept multiple channels separated by commas. + + The default token file is ~/.twitterbot_oauth. + + The default prefix type is 'cats'. You can also use 'none'. + """ -BOT_VERSION = "TwitterBot 0.5.1 (http://mike.verdone.ca/twitter)" +from __future__ import print_function + +BOT_VERSION = "TwitterBot 1.9.1 (http://mike.verdone.ca/twitter)" + +CONSUMER_KEY = "XryIxN3J2ACaJs50EizfLQ" +CONSUMER_SECRET = "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE" IRC_BOLD = chr(0x02) IRC_ITALIC = chr(0x16) @@ -35,25 +48,50 @@ IRC_REGULAR = chr(0x0f) import sys import time -from dateutil.parser import parse -from ConfigParser import SafeConfigParser +from datetime import datetime, timedelta +from email.utils import parsedate +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser from heapq import heappop, heappush import traceback +import os import os.path -from api import Twitter, TwitterError -from util import htmlentitydecode +from .api import Twitter, TwitterError +from .oauth import OAuth, read_token_file +from .oauth_dance import oauth_dance +from .util import htmlentitydecode + +PREFIXES = dict( + cats=dict( + new_tweet="=^_^= ", + error="=O_o= ", + inform="=o_o= " + ), + none=dict( + new_tweet="" + ), + ) +ACTIVE_PREFIXES=dict() + +def get_prefix(prefix_typ=None): + return ACTIVE_PREFIXES.get(prefix_typ, ACTIVE_PREFIXES.get('new_tweet', '')) + try: import irclib -except: +except ImportError: raise ImportError( "This module requires python irclib available from " - + "http://python-irclib.sourceforge.net/") + + "https://github.com/sixohsix/python-irclib/zipball/python-irclib3-0.4.8") + +OAUTH_FILE = os.environ.get('HOME', os.environ.get('USERPROFILE', '')) + os.sep + '.twitterbot_oauth' def debug(msg): # uncomment this for debug text stuff - # print >> sys.stderr, msg + # print(msg, file=sys.stdout) pass class SchedTask(object): @@ -64,11 +102,11 @@ class SchedTask(object): def __repr__(self): return "" %( - self.task.__name__, self.next, self.delta) - - def __cmp__(self, other): - return cmp(self.next, other.next) - + self.task.__name__, self.__next__, self.delta) + + def __lt__(self, other): + return self.next < other.next + def __call__(self): return self.task() @@ -77,7 +115,7 @@ class Scheduler(object): self.task_heap = [] for task in tasks: heappush(self.task_heap, task) - + def next_task(self): now = time.time() task = heappop(self.task_heap) @@ -87,64 +125,76 @@ class Scheduler(object): if (wait > 0): time.sleep(wait) task() - debug("tasks: " + str(self.task_heap)) - + #debug("tasks: " + str(self.task_heap)) + def run_forever(self): while True: self.next_task() - + class TwitterBot(object): def __init__(self, configFilename): self.configFilename = configFilename self.config = load_config(self.configFilename) + + global ACTIVE_PREFIXES + ACTIVE_PREFIXES = PREFIXES[self.config.get('irc', 'prefixes')] + + oauth_file = self.config.get('twitter', 'oauth_token_file') + if not os.path.exists(oauth_file): + oauth_dance("IRC Bot", CONSUMER_KEY, CONSUMER_SECRET, oauth_file) + oauth_token, oauth_secret = read_token_file(oauth_file) + + self.twitter = Twitter( + auth=OAuth( + oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET), + domain='api.twitter.com') + self.irc = irclib.IRC() self.irc.add_global_handler('privmsg', self.handle_privmsg) self.irc.add_global_handler('ctcp', self.handle_ctcp) + self.irc.add_global_handler('umode', self.handle_umode) 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, 120))) - self.lastUpdate = time.gmtime() + self.lastUpdate = (datetime.utcnow() - timedelta(minutes=10)).utctimetuple() def check_statuses(self): debug("In check_statuses") try: - updates = self.twitter.statuses.friends_timeline() - except Exception, e: - print >> sys.stderr, "Exception while querying twitter:" + updates = reversed(self.twitter.statuses.home_timeline()) + except Exception as e: + print("Exception while querying twitter:", file=sys.stderr) traceback.print_exc(file=sys.stderr) return - + nextLastUpdate = self.lastUpdate for update in updates: - crt = parse(update['created_at']).utctimetuple() - if (crt > self.lastUpdate): + crt = parsedate(update['created_at']) + if (crt > nextLastUpdate): text = (htmlentitydecode( update['text'].replace('\n', ' ')) - .encode('utf-8', 'replace')) + .encode('utf8', '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'))) - + if not text.startswith(b"@"): + msg = "%s %s%s%s %s" %( + get_prefix(), + IRC_BOLD, update['user']['screen_name'], + IRC_BOLD, text.decode('utf8')) + self.privmsg_channels(msg) + 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(' ') @@ -157,13 +207,14 @@ class TwitterBot(object): 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.") + evt.source().split('!')[0], + "%sHi! I'm Twitterbot! you can (follow " + ") to make me follow a user or " + "(unfollow ) to make me stop." % + get_prefix()) except Exception: traceback.print_exc(file=sys.stderr) - + def handle_ctcp(self, conn, evt): args = evt.arguments() source = evt.source().split('!')[0] @@ -175,10 +226,24 @@ class TwitterBot(object): 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 handle_umode(self, conn, evt): + """ + QuakeNet ignores all your commands until after the MOTD. This + handler defers joining until after it sees a magic line. It + also tries to join right after connect, but this will just + make it join again which should be safe. + """ + args = evt.arguments() + if (args and args[0] == '+i'): + channels = self.config.get('irc', 'channel').split(',') + for channel in channels: + self.ircServer.join(channel) + + def privmsg_channels(self, msg): + return_response=True + channels=self.config.get('irc','channel').split(',') + return self.ircServer.privmsg_many(channels, msg.encode('utf8')) + def follow(self, conn, evt, name): userNick = evt.source().split('!')[0] friends = [x['name'] for x in self.twitter.statuses.friends()] @@ -186,22 +251,24 @@ class TwitterBot(object): if (name in friends): conn.privmsg( userNick, - "=O_o= I'm already following %s." %(name)) + "%sI'm already following %s." %(get_prefix('error'), name)) else: try: - self.twitter.friendships.create(id=name) + self.twitter.friendships.create(screen_name=name) except TwitterError: conn.privmsg( userNick, - "=O_o= I can't follow that user. Are you sure the name is correct?") + "%sI can't follow that user. Are you sure the name is correct?" %( + get_prefix('error') + )) 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)) - + "%sOkay! I'm now following %s." %(get_prefix('followed'), name)) + self.privmsg_channels( + "%s%s has asked me to start following %s" %( + get_prefix('inform'), userNick, name)) + def unfollow(self, conn, evt, name): userNick = evt.source().split('!')[0] friends = [x['name'] for x in self.twitter.statuses.friends()] @@ -209,22 +276,28 @@ class TwitterBot(object): if (name not in friends): conn.privmsg( userNick, - "=O_o= I'm not following %s." %(name)) + "%sI'm not following %s." %(get_prefix('error'), name)) else: - self.twitter.friendships.destroy(id=name) + self.twitter.friendships.destroy(screen_name=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): + "%sOkay! I've stopped following %s." %( + get_prefix('stop_follow'), name)) + self.privmsg_channels( + "%s%s has asked me to stop following %s" %( + get_prefix('inform'), userNick, name)) + + def _irc_connect(self): self.ircServer.connect( - self.config.get('irc', 'server'), + self.config.get('irc', 'server'), self.config.getint('irc', 'port'), self.config.get('irc', 'nick')) - self.ircServer.join(self.config.get('irc', 'channel')) + channels=self.config.get('irc', 'channel').split(',') + for channel in channels: + self.ircServer.join(channel) + + def run(self): + self._irc_connect() while True: try: @@ -232,47 +305,60 @@ class TwitterBot(object): except KeyboardInterrupt: break except TwitterError: - # twitter.com is probably down because it sucks. ignore the fault and keep going + # twitter.com is probably down because it + # sucks. ignore the fault and keep going pass + except irclib.ServerNotConnectedError: + # Try and reconnect to IRC. + self._irc_connect() + def load_config(filename): - defaults = dict(server=dict(port=6667, nick="twitterbot")) - cp = SafeConfigParser(defaults) + # Note: Python ConfigParser module has the worst interface in the + # world. Mega gross. + cp = ConfigParser() + cp.add_section('irc') + cp.set('irc', 'port', '6667') + cp.set('irc', 'nick', 'twitterbot') + cp.set('irc', 'prefixes', 'cats') + cp.add_section('twitter') + cp.set('twitter', 'oauth_token_file', OAUTH_FILE) + cp.read((filename,)) - + # attempt to read these properties-- they are required - self.config.get('twitter', 'email'), - self.config.get('twitter', 'password') - self.config.get('irc', 'server') - self.config.getint('irc', 'port') - self.config.get('irc', 'nick') + cp.get('twitter', 'oauth_token_file'), + 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!! +# 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! # -# 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 - +# 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: - print >> sys.stderr, "Error while loading ini file %s" %( - configFilename) - print __doc__ + except Exception as e: + print("Error while loading ini file %s" %( + configFilename), file=sys.stderr) + print(e, file=sys.stderr) + print(__doc__, file=sys.stderr) sys.exit(1) bot = TwitterBot(configFilename)