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