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