]> jfr.im git - z_archive/twitter.git/blame - twitter/ircbot.py
Finally fixed the tweet repeat bug,
[z_archive/twitter.git] / twitter / ircbot.py
CommitLineData
5b7080ef 1"""
2twitterbot
772fbdd1 3
5b7080ef 4 A twitter IRC bot. Twitterbot connected to an IRC server and idles in
5 a channel, polling a twitter account and broadcasting all updates to
6 friends.
d828f28d 7
5b7080ef 8USAGE
9
10 twitterbot [config_file]
11
12CONFIG_FILE
13
14 The config file is an ini-style file that must contain the following:
d828f28d 15
5b7080ef 16[irc]
17server: <irc_server>
18port: <irc_port>
19nick: <irc_nickname>
77a8613a 20channel: <irc_channels_to_join>
0bcc8bb9 21prefixes: <prefix_type>
5b7080ef 22
23[twitter]
52792ab4 24oauth_token_file: <oauth_token_filename>
5b7080ef 25
b313a2b4 26
5b7080ef 27 If no config file is given "twitterbot.ini" will be used by default.
77a8613a
M
28
29 The channel argument can accept multiple channels separated by commas.
52792ab4
MV
30
31 The default token file is ~/.twitterbot_oauth.
32
0bcc8bb9
MV
33 The default prefix type is 'cats'. You can also use 'none'.
34
5b7080ef 35"""
d8ac8b72 36
52792ab4
MV
37BOT_VERSION = "TwitterBot 1.4 (http://mike.verdone.ca/twitter)"
38
39CONSUMER_KEY = "XryIxN3J2ACaJs50EizfLQ"
40CONSUMER_SECRET = "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
d8ac8b72 41
42IRC_BOLD = chr(0x02)
43IRC_ITALIC = chr(0x16)
44IRC_UNDERLINE = chr(0x1f)
45IRC_REGULAR = chr(0x0f)
46
7d7c8094 47import sys
772fbdd1 48import time
49from dateutil.parser import parse
65ec2606 50from ConfigParser import SafeConfigParser
7d7c8094 51from heapq import heappop, heappush
52import traceback
52792ab4 53import os
9d1c2940 54import os.path
7d7c8094 55
c93672d4 56from api import Twitter, TwitterError
52792ab4
MV
57from oauth import OAuth, read_token_file
58from oauth_dance import oauth_dance
8ad2cf0b 59from util import htmlentitydecode
772fbdd1 60
0bcc8bb9
MV
61PREFIXES = dict(
62 cats=dict(
63 new_tweet="=^_^= ",
64 error="=O_o= ",
65 inform="=o_o= "
85681608 66 ),
0bcc8bb9
MV
67 none=dict(
68 new_tweet=""
85681608 69 ),
0bcc8bb9
MV
70 )
71ACTIVE_PREFIXES=dict()
72
73def get_prefix(prefix_typ=None):
74 return ACTIVE_PREFIXES.get(prefix_typ, ACTIVE_PREFIXES.get('new_tweet', ''))
75
76
772fbdd1 77try:
78 import irclib
79except:
7d7c8094 80 raise ImportError(
81 "This module requires python irclib available from "
82 + "http://python-irclib.sourceforge.net/")
83
52792ab4
MV
84OAUTH_FILE = os.environ.get('HOME', '') + os.sep + '.twitterbot_oauth'
85
7d7c8094 86def debug(msg):
87 # uncomment this for debug text stuff
5b7080ef 88 # print >> sys.stderr, msg
7d7c8094 89 pass
772fbdd1 90
91class SchedTask(object):
92 def __init__(self, task, delta):
93 self.task = task
94 self.delta = delta
7d7c8094 95 self.next = time.time()
96
97 def __repr__(self):
98 return "<SchedTask %s next:%i delta:%i>" %(
99 self.task.__name__, self.next, self.delta)
d828f28d 100
7d7c8094 101 def __cmp__(self, other):
102 return cmp(self.next, other.next)
d828f28d 103
7d7c8094 104 def __call__(self):
105 return self.task()
106
772fbdd1 107class Scheduler(object):
108 def __init__(self, tasks):
7d7c8094 109 self.task_heap = []
110 for task in tasks:
111 heappush(self.task_heap, task)
d828f28d 112
772fbdd1 113 def next_task(self):
114 now = time.time()
7d7c8094 115 task = heappop(self.task_heap)
772fbdd1 116 wait = task.next - now
65ec2606 117 task.next = now + task.delta
118 heappush(self.task_heap, task)
772fbdd1 119 if (wait > 0):
120 time.sleep(wait)
7d7c8094 121 task()
85681608 122 #debug("tasks: " + str(self.task_heap))
d828f28d 123
772fbdd1 124 def run_forever(self):
65ec2606 125 while True:
126 self.next_task()
127
d828f28d 128
772fbdd1 129class TwitterBot(object):
7d7c8094 130 def __init__(self, configFilename):
131 self.configFilename = configFilename
132 self.config = load_config(self.configFilename)
52792ab4 133
85681608
MV
134 global ACTIVE_PREFIXES
135 ACTIVE_PREFIXES = PREFIXES[self.config.get('irc', 'prefixes')]
136
52792ab4
MV
137 oauth_file = self.config.get('twitter', 'oauth_token_file')
138 if not os.path.exists(oauth_file):
139 oauth_dance("IRC Bot", CONSUMER_KEY, CONSUMER_SECRET, oauth_file)
140 oauth_token, oauth_secret = read_token_file(oauth_file)
141
142 self.twitter = Twitter(
143 auth=OAuth(
144 oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET),
d828f28d
MV
145 api_version='1',
146 domain='api.twitter.com')
52792ab4 147
772fbdd1 148 self.irc = irclib.IRC()
7d7c8094 149 self.irc.add_global_handler('privmsg', self.handle_privmsg)
d8ac8b72 150 self.irc.add_global_handler('ctcp', self.handle_ctcp)
7d7c8094 151 self.ircServer = self.irc.server()
52792ab4 152
772fbdd1 153 self.sched = Scheduler(
7d7c8094 154 (SchedTask(self.process_events, 1),
d8ac8b72 155 SchedTask(self.check_statuses, 120)))
7d7c8094 156 self.lastUpdate = time.gmtime()
772fbdd1 157
158 def check_statuses(self):
7d7c8094 159 debug("In check_statuses")
160 try:
161 updates = self.twitter.statuses.friends_timeline()
162 except Exception, e:
163 print >> sys.stderr, "Exception while querying twitter:"
164 traceback.print_exc(file=sys.stderr)
165 return
d828f28d 166
6da3627e 167 nextLastUpdate = self.lastUpdate
85681608 168 debug("self.lastUpdate is %s" % self.lastUpdate)
7d7c8094 169 for update in updates:
170 crt = parse(update['created_at']).utctimetuple()
85681608 171 if (crt > nextLastUpdate):
8ad2cf0b 172 text = (htmlentitydecode(
173 update['text'].replace('\n', ' '))
174 .encode('utf-8', 'replace'))
7f6c5ae3 175
176 # Skip updates beginning with @
177 # TODO This would be better if we only ignored messages
178 # to people who are not on our following list.
179 if not text.startswith("@"):
c1b9acea 180 self.privmsg_channels(
85681608 181 u"%s %s%s%s %s" %(
0bcc8bb9 182 get_prefix(),
7f6c5ae3 183 IRC_BOLD, update['user']['screen_name'],
65ec2606 184 IRC_BOLD, text.decode('utf-8')))
d828f28d 185
85681608
MV
186 debug("tweet has crt %s, updating nextLastUpdate (was %s)" %(
187 crt, nextLastUpdate,
188 ))
6da3627e 189 nextLastUpdate = crt
7d7c8094 190 else:
191 break
85681608 192 debug("setting self.lastUpdate to %s" % nextLastUpdate)
6da3627e 193 self.lastUpdate = nextLastUpdate
d828f28d 194
7d7c8094 195 def process_events(self):
7d7c8094 196 self.irc.process_once()
d828f28d 197
7d7c8094 198 def handle_privmsg(self, conn, evt):
199 debug('got privmsg')
200 args = evt.arguments()[0].split(' ')
201 try:
202 if (not args):
203 return
204 if (args[0] == 'follow' and args[1:]):
205 self.follow(conn, evt, args[1])
206 elif (args[0] == 'unfollow' and args[1:]):
207 self.unfollow(conn, evt, args[1])
208 else:
209 conn.privmsg(
d828f28d 210 evt.source().split('!')[0],
0bcc8bb9 211 "%sHi! I'm Twitterbot! you can (follow "
7d7c8094 212 + "<twitter_name>) to make me follow a user or "
0bcc8bb9
MV
213 + "(unfollow <twitter_name>) to make me stop." %
214 get_prefix())
7d7c8094 215 except Exception:
216 traceback.print_exc(file=sys.stderr)
d828f28d 217
d8ac8b72 218 def handle_ctcp(self, conn, evt):
219 args = evt.arguments()
220 source = evt.source().split('!')[0]
221 if (args):
222 if args[0] == 'VERSION':
223 conn.ctcp_reply(source, "VERSION " + BOT_VERSION)
224 elif args[0] == 'PING':
225 conn.ctcp_reply(source, "PING")
226 elif args[0] == 'CLIENTINFO':
227 conn.ctcp_reply(source, "CLIENTINFO PING VERSION CLIENTINFO")
7d7c8094 228
229 def privmsg_channel(self, msg):
230 return self.ircServer.privmsg(
65ec2606 231 self.config.get('irc', 'channel'), msg.encode('utf-8'))
d828f28d 232
c1b9acea
PP
233 def privmsg_channels(self, msg):
234 return_response=True
235 channels=self.config.get('irc','channel').split(',')
236 return self.ircServer.privmsg_many(channels, msg.encode('utf-8'))
d828f28d 237
7d7c8094 238 def follow(self, conn, evt, name):
239 userNick = evt.source().split('!')[0]
240 friends = [x['name'] for x in self.twitter.statuses.friends()]
241 debug("Current friends: %s" %(friends))
242 if (name in friends):
243 conn.privmsg(
244 userNick,
0bcc8bb9 245 "%sI'm already following %s." %(get_prefix('error'), name))
7d7c8094 246 else:
c93672d4 247 try:
248 self.twitter.friendships.create(id=name)
249 except TwitterError:
250 conn.privmsg(
251 userNick,
0bcc8bb9
MV
252 "%sI can't follow that user. Are you sure the name is correct?" %(
253 get_prefix('error')
254 ))
c93672d4 255 return
7d7c8094 256 conn.privmsg(
257 userNick,
0bcc8bb9 258 "%sOkay! I'm now following %s." %(get_prefix('followed'), name))
c1b9acea 259 self.privmsg_channels(
0bcc8bb9
MV
260 "%s%s has asked me to start following %s" %(
261 get_prefix('inform'), userNick, name))
d828f28d 262
7d7c8094 263 def unfollow(self, conn, evt, name):
264 userNick = evt.source().split('!')[0]
265 friends = [x['name'] for x in self.twitter.statuses.friends()]
266 debug("Current friends: %s" %(friends))
267 if (name not in friends):
268 conn.privmsg(
269 userNick,
0bcc8bb9 270 "%sI'm not following %s." %(get_prefix('error'), name))
7d7c8094 271 else:
272 self.twitter.friendships.destroy(id=name)
273 conn.privmsg(
274 userNick,
0bcc8bb9
MV
275 "%sOkay! I've stopped following %s." %(
276 get_prefix('stop_follow'), name))
c1b9acea 277 self.privmsg_channels(
0bcc8bb9
MV
278 "%s%s has asked me to stop following %s" %(
279 get_prefix('inform'), userNick, name))
d828f28d 280
772fbdd1 281 def run(self):
7d7c8094 282 self.ircServer.connect(
d828f28d 283 self.config.get('irc', 'server'),
7d7c8094 284 self.config.getint('irc', 'port'),
285 self.config.get('irc', 'nick'))
c1b9acea
PP
286 channels=self.config.get('irc', 'channel').split(',')
287 for channel in channels:
288 self.ircServer.join(channel)
65ec2606 289
290 while True:
291 try:
292 self.sched.run_forever()
293 except KeyboardInterrupt:
294 break
295 except TwitterError:
0bcc8bb9
MV
296 # twitter.com is probably down because it
297 # sucks. ignore the fault and keep going
65ec2606 298 pass
7d7c8094 299
300def load_config(filename):
ca242389
MV
301 # Note: Python ConfigParser module has the worst interface in the
302 # world. Mega gross.
303 cp = SafeConfigParser()
304 cp.add_section('irc')
305 cp.set('irc', 'port', '6667')
306 cp.set('irc', 'nick', 'twitterbot')
0bcc8bb9 307 cp.set('irc', 'prefixes', 'cats')
ca242389
MV
308 cp.add_section('twitter')
309 cp.set('twitter', 'oauth_token_file', OAUTH_FILE)
0bcc8bb9 310
7d7c8094 311 cp.read((filename,))
d828f28d 312
9d1c2940 313 # attempt to read these properties-- they are required
52792ab4 314 cp.get('twitter', 'oauth_token_file'),
625bc8f0
MV
315 cp.get('irc', 'server')
316 cp.getint('irc', 'port')
317 cp.get('irc', 'nick')
318 cp.get('irc', 'channel')
9d1c2940 319
7d7c8094 320 return cp
772fbdd1 321
44bcaa4f
MV
322# So there was a joke here about the twitter business model
323# but I got rid of it. Not because I want this codebase to
324# be "professional" in any way, but because someone forked
325# this and deleted the comment because they couldn't take
326# a joke. Hi guy!
9d1c2940 327#
44bcaa4f
MV
328# Fact: The number one use of Google Code is to look for that
329# comment in the Linux kernel that goes "FUCK me gently with
330# a chainsaw." Pretty sure Linus himself wrote it.
9d1c2940 331
7d7c8094 332def main():
333 configFilename = "twitterbot.ini"
334 if (sys.argv[1:]):
335 configFilename = sys.argv[1]
d828f28d 336
5b7080ef 337 try:
9d1c2940
MV
338 if not os.path.exists(configFilename):
339 raise Exception()
5b7080ef 340 load_config(configFilename)
625bc8f0 341 except Exception, e:
9d1c2940 342 print >> sys.stderr, "Error while loading ini file %s" %(
5b7080ef 343 configFilename)
625bc8f0
MV
344 print >> sys.stderr, e
345 print >> sys.stderr, __doc__
5b7080ef 346 sys.exit(1)
9d1c2940 347
7d7c8094 348 bot = TwitterBot(configFilename)
349 return bot.run()