]>
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>
21 prefixes: <prefix_type>
24 oauth_token_file: <oauth_token_filename>
27 If no config file is given "twitterbot.ini" will be used by default.
29 The channel argument can accept multiple channels separated by commas.
31 The default token file is ~/.twitterbot_oauth.
33 The default prefix type is 'cats'. You can also use 'none'.
37 BOT_VERSION
= "TwitterBot 1.4 (http://mike.verdone.ca/twitter)"
39 CONSUMER_KEY
= "XryIxN3J2ACaJs50EizfLQ"
40 CONSUMER_SECRET
= "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
43 IRC_ITALIC
= chr(0x16)
44 IRC_UNDERLINE
= chr(0x1f)
45 IRC_REGULAR
= chr(0x0f)
49 from datetime
import datetime
, timedelta
50 from email
.utils
import parsedate
51 from ConfigParser
import SafeConfigParser
52 from heapq
import heappop
, heappush
57 from api
import Twitter
, TwitterError
58 from oauth
import OAuth
, read_token_file
59 from oauth_dance
import oauth_dance
60 from util
import htmlentitydecode
72 ACTIVE_PREFIXES
=dict()
74 def get_prefix(prefix_typ
=None):
75 return ACTIVE_PREFIXES
.get(prefix_typ
, ACTIVE_PREFIXES
.get('new_tweet', ''))
82 "This module requires python irclib available from "
83 + "http://python-irclib.sourceforge.net/")
85 OAUTH_FILE
= os
.environ
.get('HOME', '') + os
.sep
+ '.twitterbot_oauth'
88 # uncomment this for debug text stuff
89 # print >> sys.stderr, msg
92 class SchedTask(object):
93 def __init__(self
, task
, delta
):
96 self
.next
= time
.time()
99 return "<SchedTask %s next:%i delta:%i>" %(
100 self
.task
.__name
__, self
.next
, self
.delta
)
102 def __cmp__(self
, other
):
103 return cmp(self
.next
, other
.next
)
108 class Scheduler(object):
109 def __init__(self
, tasks
):
112 heappush(self
.task_heap
, task
)
116 task
= heappop(self
.task_heap
)
117 wait
= task
.next
- now
118 task
.next
= now
+ task
.delta
119 heappush(self
.task_heap
, task
)
123 #debug("tasks: " + str(self.task_heap))
125 def run_forever(self
):
130 class TwitterBot(object):
131 def __init__(self
, configFilename
):
132 self
.configFilename
= configFilename
133 self
.config
= load_config(self
.configFilename
)
135 global ACTIVE_PREFIXES
136 ACTIVE_PREFIXES
= PREFIXES
[self
.config
.get('irc', 'prefixes')]
138 oauth_file
= self
.config
.get('twitter', 'oauth_token_file')
139 if not os
.path
.exists(oauth_file
):
140 oauth_dance("IRC Bot", CONSUMER_KEY
, CONSUMER_SECRET
, oauth_file
)
141 oauth_token
, oauth_secret
= read_token_file(oauth_file
)
143 self
.twitter
= Twitter(
145 oauth_token
, oauth_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
147 domain
='api.twitter.com')
149 self
.irc
= irclib
.IRC()
150 self
.irc
.add_global_handler('privmsg', self
.handle_privmsg
)
151 self
.irc
.add_global_handler('ctcp', self
.handle_ctcp
)
152 self
.ircServer
= self
.irc
.server()
154 self
.sched
= Scheduler(
155 (SchedTask(self
.process_events
, 1),
156 SchedTask(self
.check_statuses
, 120)))
157 self
.lastUpdate
= (datetime
.utcnow() - timedelta(minutes
=10)).utctimetuple()
159 def check_statuses(self
):
160 debug("In check_statuses")
162 updates
= reversed(self
.twitter
.statuses
.friends_timeline())
164 print >> sys
.stderr
, "Exception while querying twitter:"
165 traceback
.print_exc(file=sys
.stderr
)
168 nextLastUpdate
= self
.lastUpdate
169 debug("self.lastUpdate is %s" % self
.lastUpdate
)
170 for update
in updates
:
171 crt
= parsedate(update
['created_at'])
172 if (crt
> nextLastUpdate
):
173 text
= (htmlentitydecode(
174 update
['text'].replace('\n', ' '))
175 .encode('utf-8', 'replace'))
177 # Skip updates beginning with @
178 # TODO This would be better if we only ignored messages
179 # to people who are not on our following list.
180 if not text
.startswith("@"):
181 self
.privmsg_channels(
184 IRC_BOLD
, update
['user']['screen_name'],
185 IRC_BOLD
, text
.decode('utf-8')))
189 debug("setting self.lastUpdate to %s" % nextLastUpdate
)
190 self
.lastUpdate
= nextLastUpdate
192 def process_events(self
):
193 self
.irc
.process_once()
195 def handle_privmsg(self
, conn
, evt
):
197 args
= evt
.arguments()[0].split(' ')
201 if (args
[0] == 'follow' and args
[1:]):
202 self
.follow(conn
, evt
, args
[1])
203 elif (args
[0] == 'unfollow' and args
[1:]):
204 self
.unfollow(conn
, evt
, args
[1])
207 evt
.source().split('!')[0],
208 "%sHi! I'm Twitterbot! you can (follow "
209 "<twitter_name>) to make me follow a user or "
210 "(unfollow <twitter_name>) to make me stop." %
213 traceback
.print_exc(file=sys
.stderr
)
215 def handle_ctcp(self
, conn
, evt
):
216 args
= evt
.arguments()
217 source
= evt
.source().split('!')[0]
219 if args
[0] == 'VERSION':
220 conn
.ctcp_reply(source
, "VERSION " + BOT_VERSION
)
221 elif args
[0] == 'PING':
222 conn
.ctcp_reply(source
, "PING")
223 elif args
[0] == 'CLIENTINFO':
224 conn
.ctcp_reply(source
, "CLIENTINFO PING VERSION CLIENTINFO")
226 def privmsg_channel(self
, msg
):
227 return self
.ircServer
.privmsg(
228 self
.config
.get('irc', 'channel'), msg
.encode('utf-8'))
230 def privmsg_channels(self
, msg
):
232 channels
=self
.config
.get('irc','channel').split(',')
233 return self
.ircServer
.privmsg_many(channels
, msg
.encode('utf-8'))
235 def follow(self
, conn
, evt
, name
):
236 userNick
= evt
.source().split('!')[0]
237 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
238 debug("Current friends: %s" %(friends))
239 if (name
in friends
):
242 "%sI'm already following %s." %(get_prefix('error'), name
))
245 self
.twitter
.friendships
.create(id=name
)
249 "%sI can't follow that user. Are you sure the name is correct?" %(
255 "%sOkay! I'm now following %s." %(get_prefix('followed'), name
))
256 self
.privmsg_channels(
257 "%s%s has asked me to start following %s" %(
258 get_prefix('inform'), userNick
, name
))
260 def unfollow(self
, conn
, evt
, name
):
261 userNick
= evt
.source().split('!')[0]
262 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
263 debug("Current friends: %s" %(friends))
264 if (name
not in friends
):
267 "%sI'm not following %s." %(get_prefix('error'), name
))
269 self
.twitter
.friendships
.destroy(id=name
)
272 "%sOkay! I've stopped following %s." %(
273 get_prefix('stop_follow'), name
))
274 self
.privmsg_channels(
275 "%s%s has asked me to stop following %s" %(
276 get_prefix('inform'), userNick
, name
))
279 self
.ircServer
.connect(
280 self
.config
.get('irc', 'server'),
281 self
.config
.getint('irc', 'port'),
282 self
.config
.get('irc', 'nick'))
283 channels
=self
.config
.get('irc', 'channel').split(',')
284 for channel
in channels
:
285 self
.ircServer
.join(channel
)
289 self
.sched
.run_forever()
290 except KeyboardInterrupt:
293 # twitter.com is probably down because it
294 # sucks. ignore the fault and keep going
297 def load_config(filename
):
298 # Note: Python ConfigParser module has the worst interface in the
300 cp
= SafeConfigParser()
301 cp
.add_section('irc')
302 cp
.set('irc', 'port', '6667')
303 cp
.set('irc', 'nick', 'twitterbot')
304 cp
.set('irc', 'prefixes', 'cats')
305 cp
.add_section('twitter')
306 cp
.set('twitter', 'oauth_token_file', OAUTH_FILE
)
310 # attempt to read these properties-- they are required
311 cp
.get('twitter', 'oauth_token_file'),
312 cp
.get('irc', 'server')
313 cp
.getint('irc', 'port')
314 cp
.get('irc', 'nick')
315 cp
.get('irc', 'channel')
319 # So there was a joke here about the twitter business model
320 # but I got rid of it. Not because I want this codebase to
321 # be "professional" in any way, but because someone forked
322 # this and deleted the comment because they couldn't take
325 # Fact: The number one use of Google Code is to look for that
326 # comment in the Linux kernel that goes "FUCK me gently with
327 # a chainsaw." Pretty sure Linus himself wrote it.
330 configFilename
= "twitterbot.ini"
332 configFilename
= sys
.argv
[1]
335 if not os
.path
.exists(configFilename
):
337 load_config(configFilename
)
339 print >> sys
.stderr
, "Error while loading ini file %s" %(
341 print >> sys
.stderr
, e
342 print >> sys
.stderr
, __doc__
345 bot
= TwitterBot(configFilename
)