]>
jfr.im git - z_archive/twitter.git/blob - twitter/ircbot.py
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
10 twitterbot [config_file]
14 The config file is an ini-style file that must contain the following:
20 channel: <irc_channels_to_join>
23 oauth_token_file: <oauth_token_filename>
26 If no config file is given "twitterbot.ini" will be used by default.
28 The channel argument can accept multiple channels separated by commas.
30 The default token file is ~/.twitterbot_oauth.
34 BOT_VERSION
= "TwitterBot 1.4 (http://mike.verdone.ca/twitter)"
36 CONSUMER_KEY
= "XryIxN3J2ACaJs50EizfLQ"
37 CONSUMER_SECRET
= "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
40 IRC_ITALIC
= chr(0x16)
41 IRC_UNDERLINE
= chr(0x1f)
42 IRC_REGULAR
= chr(0x0f)
46 from dateutil
.parser
import parse
47 from ConfigParser
import SafeConfigParser
48 from heapq
import heappop
, heappush
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
62 "This module requires python irclib available from "
63 + "http://python-irclib.sourceforge.net/")
65 OAUTH_FILE
= os
.environ
.get('HOME', '') + os
.sep
+ '.twitterbot_oauth'
68 # uncomment this for debug text stuff
69 # print >> sys.stderr, msg
72 class SchedTask(object):
73 def __init__(self
, task
, delta
):
76 self
.next
= time
.time()
79 return "<SchedTask %s next:%i delta:%i>" %(
80 self
.task
.__name
__, self
.next
, self
.delta
)
82 def __cmp__(self
, other
):
83 return cmp(self
.next
, other
.next
)
88 class Scheduler(object):
89 def __init__(self
, tasks
):
92 heappush(self
.task_heap
, task
)
96 task
= heappop(self
.task_heap
)
97 wait
= task
.next
- now
98 task
.next
= now
+ task
.delta
99 heappush(self
.task_heap
, task
)
103 debug("tasks: " + str(self
.task_heap
))
105 def run_forever(self
):
110 class TwitterBot(object):
111 def __init__(self
, configFilename
):
112 self
.configFilename
= configFilename
113 self
.config
= load_config(self
.configFilename
)
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
)
120 self
.twitter
= Twitter(
122 oauth_token
, oauth_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
124 domain
='api.twitter.com')
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()
131 self
.sched
= Scheduler(
132 (SchedTask(self
.process_events
, 1),
133 SchedTask(self
.check_statuses
, 120)))
134 self
.lastUpdate
= time
.gmtime()
136 def check_statuses(self
):
137 debug("In check_statuses")
139 updates
= self
.twitter
.statuses
.friends_timeline()
141 print >> sys
.stderr
, "Exception while querying twitter:"
142 traceback
.print_exc(file=sys
.stderr
)
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'))
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')))
165 self
.lastUpdate
= nextLastUpdate
167 def process_events(self
):
168 debug("In process_events")
169 self
.irc
.process_once()
171 def handle_privmsg(self
, conn
, evt
):
173 args
= evt
.arguments()[0].split(' ')
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])
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.")
188 traceback
.print_exc(file=sys
.stderr
)
190 def handle_ctcp(self
, conn
, evt
):
191 args
= evt
.arguments()
192 source
= evt
.source().split('!')[0]
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")
201 def privmsg_channel(self
, msg
):
202 return self
.ircServer
.privmsg(
203 self
.config
.get('irc', 'channel'), msg
.encode('utf-8'))
205 def privmsg_channels(self
, msg
):
207 channels
=self
.config
.get('irc','channel').split(',')
208 return self
.ircServer
.privmsg_many(channels
, msg
.encode('utf-8'))
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
):
217 "=O_o= I'm already following %s." %(name))
220 self
.twitter
.friendships
.create(id=name
)
224 "=O_o= I can't follow that user. Are you sure the name is correct?")
228 "=^_^= Okay! I'm now following %s." %(name))
229 self
.privmsg_channels(
230 "=o_o= %s has asked me to start following %s" %(
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
):
240 "=O_o= I'm not following %s." %(name))
242 self
.twitter
.friendships
.destroy(id=name
)
245 "=^_^= Okay! I've stopped following %s." %(name))
246 self
.privmsg_channels(
247 "=o_o= %s has asked me to stop following %s" %(
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
)
261 self
.sched
.run_forever()
262 except KeyboardInterrupt:
265 # twitter.com is probably down because it sucks. ignore the fault and keep going
268 def load_config(filename
):
269 # Note: Python ConfigParser module has the worst interface in the
271 cp
= SafeConfigParser()
272 cp
.add_section('irc')
273 cp
.set('irc', 'port', '6667')
274 cp
.set('irc', 'nick', 'twitterbot')
275 cp
.add_section('twitter')
276 cp
.set('twitter', 'oauth_token_file', OAUTH_FILE
)
279 # attempt to read these properties-- they are required
280 cp
.get('twitter', 'oauth_token_file'),
281 cp
.get('irc', 'server')
282 cp
.getint('irc', 'port')
283 cp
.get('irc', 'nick')
284 cp
.get('irc', 'channel')
288 # So there was a joke here about the twitter business model
289 # but I got rid of it. Not because I want this codebase to
290 # be "professional" in any way, but because someone forked
291 # this and deleted the comment because they couldn't take
294 # Fact: The number one use of Google Code is to look for that
295 # comment in the Linux kernel that goes "FUCK me gently with
296 # a chainsaw." Pretty sure Linus himself wrote it.
299 configFilename
= "twitterbot.ini"
301 configFilename
= sys
.argv
[1]
304 if not os
.path
.exists(configFilename
):
306 load_config(configFilename
)
308 print >> sys
.stderr
, "Error while loading ini file %s" %(
310 print >> sys
.stderr
, e
311 print >> sys
.stderr
, __doc__
314 bot
= TwitterBot(configFilename
)