]>
jfr.im git - z_archive/twitter.git/blob - twitter/ircbot.py
1c2a6490a56c82284af336fc94653a05888aec78
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 dateutil
.parser
import parse
50 from ConfigParser
import SafeConfigParser
51 from heapq
import heappop
, heappush
56 from api
import Twitter
, TwitterError
57 from oauth
import OAuth
, read_token_file
58 from oauth_dance
import oauth_dance
59 from util
import htmlentitydecode
71 ACTIVE_PREFIXES
=dict()
73 def get_prefix(prefix_typ
=None):
74 return ACTIVE_PREFIXES
.get(prefix_typ
, ACTIVE_PREFIXES
.get('new_tweet', ''))
81 "This module requires python irclib available from "
82 + "http://python-irclib.sourceforge.net/")
84 OAUTH_FILE
= os
.environ
.get('HOME', '') + os
.sep
+ '.twitterbot_oauth'
87 # uncomment this for debug text stuff
88 # print >> sys.stderr, msg
91 class SchedTask(object):
92 def __init__(self
, task
, delta
):
95 self
.next
= time
.time()
98 return "<SchedTask %s next:%i delta:%i>" %(
99 self
.task
.__name
__, self
.next
, self
.delta
)
101 def __cmp__(self
, other
):
102 return cmp(self
.next
, other
.next
)
107 class Scheduler(object):
108 def __init__(self
, tasks
):
111 heappush(self
.task_heap
, task
)
115 task
= heappop(self
.task_heap
)
116 wait
= task
.next
- now
117 task
.next
= now
+ task
.delta
118 heappush(self
.task_heap
, task
)
122 debug("tasks: " + str(self
.task_heap
))
124 def run_forever(self
):
129 class TwitterBot(object):
130 def __init__(self
, configFilename
):
131 self
.configFilename
= configFilename
132 self
.config
= load_config(self
.configFilename
)
134 oauth_file
= self
.config
.get('twitter', 'oauth_token_file')
135 if not os
.path
.exists(oauth_file
):
136 oauth_dance("IRC Bot", CONSUMER_KEY
, CONSUMER_SECRET
, oauth_file
)
137 oauth_token
, oauth_secret
= read_token_file(oauth_file
)
139 self
.twitter
= Twitter(
141 oauth_token
, oauth_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
143 domain
='api.twitter.com')
145 self
.irc
= irclib
.IRC()
146 self
.irc
.add_global_handler('privmsg', self
.handle_privmsg
)
147 self
.irc
.add_global_handler('ctcp', self
.handle_ctcp
)
148 self
.ircServer
= self
.irc
.server()
150 self
.sched
= Scheduler(
151 (SchedTask(self
.process_events
, 1),
152 SchedTask(self
.check_statuses
, 120)))
153 self
.lastUpdate
= time
.gmtime()
155 def check_statuses(self
):
156 debug("In check_statuses")
158 updates
= self
.twitter
.statuses
.friends_timeline()
160 print >> sys
.stderr
, "Exception while querying twitter:"
161 traceback
.print_exc(file=sys
.stderr
)
164 nextLastUpdate
= self
.lastUpdate
165 for update
in updates
:
166 crt
= parse(update
['created_at']).utctimetuple()
167 if (crt
> self
.lastUpdate
):
168 text
= (htmlentitydecode(
169 update
['text'].replace('\n', ' '))
170 .encode('utf-8', 'replace'))
172 # Skip updates beginning with @
173 # TODO This would be better if we only ignored messages
174 # to people who are not on our following list.
175 if not text
.startswith("@"):
176 self
.privmsg_channels(
179 IRC_BOLD
, update
['user']['screen_name'],
180 IRC_BOLD
, text
.decode('utf-8')))
185 self
.lastUpdate
= nextLastUpdate
187 def process_events(self
):
188 debug("In process_events")
189 self
.irc
.process_once()
191 def handle_privmsg(self
, conn
, evt
):
193 args
= evt
.arguments()[0].split(' ')
197 if (args
[0] == 'follow' and args
[1:]):
198 self
.follow(conn
, evt
, args
[1])
199 elif (args
[0] == 'unfollow' and args
[1:]):
200 self
.unfollow(conn
, evt
, args
[1])
203 evt
.source().split('!')[0],
204 "%sHi! I'm Twitterbot! you can (follow "
205 + "<twitter_name>) to make me follow a user or "
206 + "(unfollow <twitter_name>) to make me stop." %
209 traceback
.print_exc(file=sys
.stderr
)
211 def handle_ctcp(self
, conn
, evt
):
212 args
= evt
.arguments()
213 source
= evt
.source().split('!')[0]
215 if args
[0] == 'VERSION':
216 conn
.ctcp_reply(source
, "VERSION " + BOT_VERSION
)
217 elif args
[0] == 'PING':
218 conn
.ctcp_reply(source
, "PING")
219 elif args
[0] == 'CLIENTINFO':
220 conn
.ctcp_reply(source
, "CLIENTINFO PING VERSION CLIENTINFO")
222 def privmsg_channel(self
, msg
):
223 return self
.ircServer
.privmsg(
224 self
.config
.get('irc', 'channel'), msg
.encode('utf-8'))
226 def privmsg_channels(self
, msg
):
228 channels
=self
.config
.get('irc','channel').split(',')
229 return self
.ircServer
.privmsg_many(channels
, msg
.encode('utf-8'))
231 def follow(self
, conn
, evt
, name
):
232 userNick
= evt
.source().split('!')[0]
233 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
234 debug("Current friends: %s" %(friends))
235 if (name
in friends
):
238 "%sI'm already following %s." %(get_prefix('error'), name
))
241 self
.twitter
.friendships
.create(id=name
)
245 "%sI can't follow that user. Are you sure the name is correct?" %(
251 "%sOkay! I'm now following %s." %(get_prefix('followed'), name
))
252 self
.privmsg_channels(
253 "%s%s has asked me to start following %s" %(
254 get_prefix('inform'), userNick
, name
))
256 def unfollow(self
, conn
, evt
, name
):
257 userNick
= evt
.source().split('!')[0]
258 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
259 debug("Current friends: %s" %(friends))
260 if (name
not in friends
):
263 "%sI'm not following %s." %(get_prefix('error'), name
))
265 self
.twitter
.friendships
.destroy(id=name
)
268 "%sOkay! I've stopped following %s." %(
269 get_prefix('stop_follow'), name
))
270 self
.privmsg_channels(
271 "%s%s has asked me to stop following %s" %(
272 get_prefix('inform'), userNick
, name
))
275 self
.ircServer
.connect(
276 self
.config
.get('irc', 'server'),
277 self
.config
.getint('irc', 'port'),
278 self
.config
.get('irc', 'nick'))
279 channels
=self
.config
.get('irc', 'channel').split(',')
280 for channel
in channels
:
281 self
.ircServer
.join(channel
)
285 self
.sched
.run_forever()
286 except KeyboardInterrupt:
289 # twitter.com is probably down because it
290 # sucks. ignore the fault and keep going
293 def load_config(filename
):
294 # Note: Python ConfigParser module has the worst interface in the
296 cp
= SafeConfigParser()
297 cp
.add_section('irc')
298 cp
.set('irc', 'port', '6667')
299 cp
.set('irc', 'nick', 'twitterbot')
300 cp
.set('irc', 'prefixes', 'cats')
301 cp
.add_section('twitter')
302 cp
.set('twitter', 'oauth_token_file', OAUTH_FILE
)
306 # attempt to read these properties-- they are required
307 cp
.get('twitter', 'oauth_token_file'),
308 cp
.get('irc', 'server')
309 cp
.getint('irc', 'port')
310 cp
.get('irc', 'nick')
311 cp
.get('irc', 'channel')
315 # So there was a joke here about the twitter business model
316 # but I got rid of it. Not because I want this codebase to
317 # be "professional" in any way, but because someone forked
318 # this and deleted the comment because they couldn't take
321 # Fact: The number one use of Google Code is to look for that
322 # comment in the Linux kernel that goes "FUCK me gently with
323 # a chainsaw." Pretty sure Linus himself wrote it.
326 configFilename
= "twitterbot.ini"
328 configFilename
= sys
.argv
[1]
331 if not os
.path
.exists(configFilename
):
333 load_config(configFilename
)
335 print >> sys
.stderr
, "Error while loading ini file %s" %(
337 print >> sys
.stderr
, e
338 print >> sys
.stderr
, __doc__
341 global ACTIVE_PREFIXES
342 ACTIVE_PREFIXES
= PREFIXES
[cp
.get('irc', 'prefixes')]
343 bot
= TwitterBot(configFilename
)