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