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