]> jfr.im git - z_archive/twitter.git/blame - twitter/ircbot.py
Merge branch 'shell' into master_shell_merge
[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.
7
8USAGE
9
10 twitterbot [config_file]
11
12CONFIG_FILE
13
14 The config file is an ini-style file that must contain the following:
15
16[irc]
17server: <irc_server>
18port: <irc_port>
19nick: <irc_nickname>
20channel: <irc_channel_to_join>
21
22[twitter]
23email: <twitter_account_email>
24password: <twitter_account_password>
25
26 If no config file is given "twitterbot.ini" will be used by default.
5b7080ef 27"""
d8ac8b72 28
6793b9c6 29BOT_VERSION = "TwitterBot 1.0 (http://mike.verdone.ca/twitter)"
d8ac8b72 30
31IRC_BOLD = chr(0x02)
32IRC_ITALIC = chr(0x16)
33IRC_UNDERLINE = chr(0x1f)
34IRC_REGULAR = chr(0x0f)
35
7d7c8094 36import sys
772fbdd1 37import time
38from dateutil.parser import parse
65ec2606 39from ConfigParser import SafeConfigParser
7d7c8094 40from heapq import heappop, heappush
41import traceback
9d1c2940 42import os.path
7d7c8094 43
c93672d4 44from api import Twitter, TwitterError
8ad2cf0b 45from util import htmlentitydecode
772fbdd1 46
47try:
48 import irclib
49except:
7d7c8094 50 raise ImportError(
51 "This module requires python irclib available from "
52 + "http://python-irclib.sourceforge.net/")
53
54def debug(msg):
55 # uncomment this for debug text stuff
5b7080ef 56 # print >> sys.stderr, msg
7d7c8094 57 pass
772fbdd1 58
59class SchedTask(object):
60 def __init__(self, task, delta):
61 self.task = task
62 self.delta = delta
7d7c8094 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
772fbdd1 75class Scheduler(object):
76 def __init__(self, tasks):
7d7c8094 77 self.task_heap = []
78 for task in tasks:
79 heappush(self.task_heap, task)
772fbdd1 80
81 def next_task(self):
82 now = time.time()
7d7c8094 83 task = heappop(self.task_heap)
772fbdd1 84 wait = task.next - now
65ec2606 85 task.next = now + task.delta
86 heappush(self.task_heap, task)
772fbdd1 87 if (wait > 0):
88 time.sleep(wait)
7d7c8094 89 task()
7d7c8094 90 debug("tasks: " + str(self.task_heap))
772fbdd1 91
92 def run_forever(self):
65ec2606 93 while True:
94 self.next_task()
95
772fbdd1 96
97class TwitterBot(object):
7d7c8094 98 def __init__(self, configFilename):
99 self.configFilename = configFilename
100 self.config = load_config(self.configFilename)
772fbdd1 101 self.irc = irclib.IRC()
7d7c8094 102 self.irc.add_global_handler('privmsg', self.handle_privmsg)
d8ac8b72 103 self.irc.add_global_handler('ctcp', self.handle_ctcp)
7d7c8094 104 self.ircServer = self.irc.server()
105 self.twitter = Twitter(
106 self.config.get('twitter', 'email'),
107 self.config.get('twitter', 'password'))
772fbdd1 108 self.sched = Scheduler(
7d7c8094 109 (SchedTask(self.process_events, 1),
d8ac8b72 110 SchedTask(self.check_statuses, 120)))
7d7c8094 111 self.lastUpdate = time.gmtime()
772fbdd1 112
113 def check_statuses(self):
7d7c8094 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
6da3627e 122 nextLastUpdate = self.lastUpdate
7d7c8094 123 for update in updates:
124 crt = parse(update['created_at']).utctimetuple()
125 if (crt > self.lastUpdate):
8ad2cf0b 126 text = (htmlentitydecode(
127 update['text'].replace('\n', ' '))
128 .encode('utf-8', 'replace'))
7f6c5ae3 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(
65ec2606 135 u"=^_^= %s%s%s %s" %(
7f6c5ae3 136 IRC_BOLD, update['user']['screen_name'],
65ec2606 137 IRC_BOLD, text.decode('utf-8')))
7f6c5ae3 138
6da3627e 139 nextLastUpdate = crt
7d7c8094 140 else:
141 break
6da3627e 142 self.lastUpdate = nextLastUpdate
143
7d7c8094 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)
d8ac8b72 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")
7d7c8094 177
178 def privmsg_channel(self, msg):
179 return self.ircServer.privmsg(
65ec2606 180 self.config.get('irc', 'channel'), msg.encode('utf-8'))
7d7c8094 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:
c93672d4 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
7d7c8094 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
772fbdd1 222 def run(self):
7d7c8094 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'))
65ec2606 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
7d7c8094 237
238def load_config(filename):
239 defaults = dict(server=dict(port=6667, nick="twitterbot"))
65ec2606 240 cp = SafeConfigParser(defaults)
7d7c8094 241 cp.read((filename,))
9d1c2940
MV
242
243 # attempt to read these properties-- they are required
625bc8f0
MV
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')
9d1c2940 250
7d7c8094 251 return cp
772fbdd1 252
44bcaa4f
MV
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!
9d1c2940 258#
44bcaa4f
MV
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.
9d1c2940 262
7d7c8094 263def main():
264 configFilename = "twitterbot.ini"
265 if (sys.argv[1:]):
266 configFilename = sys.argv[1]
9d1c2940 267
5b7080ef 268 try:
9d1c2940
MV
269 if not os.path.exists(configFilename):
270 raise Exception()
5b7080ef 271 load_config(configFilename)
625bc8f0 272 except Exception, e:
9d1c2940 273 print >> sys.stderr, "Error while loading ini file %s" %(
5b7080ef 274 configFilename)
625bc8f0
MV
275 print >> sys.stderr, e
276 print >> sys.stderr, __doc__
5b7080ef 277 sys.exit(1)
9d1c2940 278
7d7c8094 279 bot = TwitterBot(configFilename)
280 return bot.run()