]> jfr.im git - z_archive/twitter.git/blob - twitter/ircbot.py
last minute 0.4 stuff
[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
30 BOT_VERSION = "TwitterBot 0.4 (mike.verdone.ca/twitter)"
31
32 IRC_BOLD = chr(0x02)
33 IRC_ITALIC = chr(0x16)
34 IRC_UNDERLINE = chr(0x1f)
35 IRC_REGULAR = chr(0x0f)
36
37 import sys
38 import time
39 from dateutil.parser import parse
40 from ConfigParser import SafeConfigParser
41 from heapq import heappop, heappush
42 import traceback
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 return cp
243
244 def main():
245 configFilename = "twitterbot.ini"
246 if (sys.argv[1:]):
247 configFilename = sys.argv[1]
248 try:
249 load_config(configFilename)
250 except:
251 print >> sys.stderr, "Error loading ini file %s" %(
252 configFilename)
253 print __doc__
254 sys.exit(1)
255 bot = TwitterBot(configFilename)
256 return bot.run()