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