]>
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>
22 modes: <modes to set (i.e. +ix)>
23 autocmd: <command to send upon connect>
26 oauth_token_file: <oauth_token_filename>
29 If no config file is given "twitterbot.ini" will be used by default.
31 The channel argument can accept multiple channels separated by commas.
33 The default token file is ~/.twitterbot_oauth.
35 The default prefix type is 'cats'. You can also use 'none'.
37 autocmd and modes may be omitted.
41 from __future__
import print_function
43 BOT_VERSION
= "TwitterBot 1.9.1 (http://mike.verdone.ca/twitter)"
45 CONSUMER_KEY
= "XryIxN3J2ACaJs50EizfLQ"
46 CONSUMER_SECRET
= "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
49 IRC_ITALIC
= chr(0x16)
50 IRC_UNDERLINE
= chr(0x1f)
51 IRC_REGULAR
= chr(0x0f)
55 from datetime
import datetime
, timedelta
56 from email
.utils
import parsedate
58 from configparser
import ConfigParser
60 from ConfigParser
import ConfigParser
61 from heapq
import heappop
, heappush
66 from .api
import Twitter
, TwitterError
67 from .oauth
import OAuth
, read_token_file
68 from .oauth_dance
import oauth_dance
69 from .util
import htmlentitydecode
81 ACTIVE_PREFIXES
=dict()
83 def get_prefix(prefix_typ
=None):
84 return ACTIVE_PREFIXES
.get(prefix_typ
, ACTIVE_PREFIXES
.get('new_tweet', ''))
91 "This module requires python irclib available from "
92 + "https://github.com/sixohsix/python-irclib/zipball/python-irclib3-0.4.8")
94 OAUTH_FILE
= os
.environ
.get('HOME', os
.environ
.get('USERPROFILE', '')) + os
.sep
+ '.twitterbot_oauth'
97 # uncomment this for debug text stuff
98 # print(msg, file=sys.stdout)
101 class SchedTask(object):
102 def __init__(self
, task
, delta
):
105 self
.next
= time
.time()
108 return "<SchedTask %s next:%i delta:%i>" %(
109 self
.task
.__name
__, self
.__next
__, self
.delta
)
111 def __lt__(self
, other
):
112 return self
.next
< other
.next
117 class Scheduler(object):
118 def __init__(self
, tasks
):
121 heappush(self
.task_heap
, task
)
125 task
= heappop(self
.task_heap
)
126 wait
= task
.next
- now
127 task
.next
= now
+ task
.delta
128 heappush(self
.task_heap
, task
)
132 #debug("tasks: " + str(self.task_heap))
134 def run_forever(self
):
139 class TwitterBot(object):
140 def __init__(self
, configFilename
):
141 self
.configFilename
= configFilename
142 self
.config
= load_config(self
.configFilename
)
144 global ACTIVE_PREFIXES
145 ACTIVE_PREFIXES
= PREFIXES
[self
.config
.get('irc', 'prefixes')]
147 oauth_file
= self
.config
.get('twitter', 'oauth_token_file')
148 if not os
.path
.exists(oauth_file
):
149 oauth_dance("IRC Bot", CONSUMER_KEY
, CONSUMER_SECRET
, oauth_file
)
150 oauth_token
, oauth_secret
= read_token_file(oauth_file
)
152 self
.twitter
= Twitter(
154 oauth_token
, oauth_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
155 domain
='api.twitter.com')
157 self
.irc
= irclib
.IRC()
158 self
.irc
.add_global_handler('privmsg', self
.handle_privmsg
)
159 self
.irc
.add_global_handler('ctcp', self
.handle_ctcp
)
160 self
.irc
.add_global_handler('umode', self
.handle_umode
)
161 self
.ircServer
= self
.irc
.server()
163 self
.sched
= Scheduler(
164 (SchedTask(self
.process_events
, 1),
165 SchedTask(self
.check_statuses
, 120)))
166 self
.lastUpdate
= (datetime
.utcnow() - timedelta(minutes
=10)).utctimetuple()
168 def check_statuses(self
):
169 debug("In check_statuses")
171 updates
= reversed(self
.twitter
.statuses
.home_timeline())
172 except Exception as e
:
173 print("Exception while querying twitter:", file=sys
.stderr
)
174 traceback
.print_exc(file=sys
.stderr
)
177 nextLastUpdate
= self
.lastUpdate
178 for update
in updates
:
180 # This part raises lots of exceptions which kill the bot
181 # (Unicode errors, etc.)
182 # Ignore any exceptions, as a band-aid.
183 crt
= parsedate(update
['created_at'])
184 if (crt
> nextLastUpdate
):
185 text
= (htmlentitydecode(
186 update
['text'].replace('\n', ' '))
187 .encode('utf8', 'replace'))
189 # Skip updates beginning with @
190 # TODO This would be better if we only ignored messages
191 # to people who are not on our following list.
192 if not text
.startswith(b
"@"):
193 msg
= "%s %s%s%s %s" %(
195 IRC_BOLD
, update
['user']['screen_name'],
196 IRC_BOLD
, text
.decode('utf8'))
197 self
.privmsg_channels(msg
)
200 except Exception as e
:
201 print("Exception while sending updates:", file=sys
.stderr
)
202 traceback
.print_exc(file=sys
.stderr
)
203 pass # don't return as this one is likely to keep happening
205 crt
= parsedate(update
['created_at'])
206 if (crt
> nextLastUpdate
):
207 text
= (htmlentitydecode(
208 update
['text'].replace('\n', ' '))
209 .encode('utf8', 'replace'))
211 # Skip updates beginning with @
212 # TODO This would be better if we only ignored messages
213 # to people who are not on our following list.
214 if not text
.startswith(b
"@"):
215 msg
= "%s %s%s%s %s" %(
217 IRC_BOLD
, update
['user']['screen_name'],
218 IRC_BOLD
, text
.decode('utf8'))
219 self
.privmsg_channels(msg
)
223 self
.lastUpdate
= nextLastUpdate
225 def process_events(self
):
226 self
.irc
.process_once()
228 def handle_privmsg(self
, conn
, evt
):
230 args
= evt
.arguments()[0].split(' ')
234 if (args
[0] == 'follow' and args
[1:]):
235 self
.follow(conn
, evt
, args
[1])
236 elif (args
[0] == 'unfollow' and args
[1:]):
237 self
.unfollow(conn
, evt
, args
[1])
240 evt
.source().split('!')[0],
241 "%sHi! I'm Twitterbot! you can (follow "
242 "<twitter_name>) to make me follow a user or "
243 "(unfollow <twitter_name>) to make me stop." %
246 traceback
.print_exc(file=sys
.stderr
)
248 def handle_ctcp(self
, conn
, evt
):
249 args
= evt
.arguments()
250 source
= evt
.source().split('!')[0]
252 if args
[0] == 'VERSION':
253 conn
.ctcp_reply(source
, "VERSION " + BOT_VERSION
)
254 elif args
[0] == 'PING':
255 conn
.ctcp_reply(source
, "PING")
256 elif args
[0] == 'CLIENTINFO':
257 conn
.ctcp_reply(source
, "CLIENTINFO PING VERSION CLIENTINFO")
259 def handle_umode(self
, conn
, evt
):
261 QuakeNet ignores all your commands until after the MOTD. This
262 handler defers joining until after it sees a magic line. It
263 also tries to join right after connect, but this will just
264 make it join again which should be safe.
266 args
= evt
.arguments()
267 if (args
and args
[0] == '+i'):
269 self
.ircServer
.send_raw(self
.config
.get('irc', 'autocmd'))
271 pass # ignore errors, it's probably just not defined
273 self
.ircServer
.send_raw("MODE %s %s" % (self
.ircServer
.get_nickname(), self
.config
.get('irc', 'modes')))
275 pass # ignore errors again
276 channels
= self
.config
.get('irc', 'channel').split(',')
277 for channel
in channels
:
278 self
.ircServer
.join(channel
)
280 def privmsg_channels(self
, msg
):
282 channels
=self
.config
.get('irc','channel').split(',')
283 return self
.ircServer
.privmsg_many(channels
, msg
.encode('utf8'))
285 def follow(self
, conn
, evt
, name
):
286 userNick
= evt
.source().split('!')[0]
287 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
288 debug("Current friends: %s" %(friends))
289 if (name
in friends
):
292 "%sI'm already following %s." %(get_prefix('error'), name
))
295 self
.twitter
.friendships
.create(screen_name
=name
)
299 "%sI can't follow that user. Are you sure the name is correct?" %(
305 "%sOkay! I'm now following %s." %(get_prefix('followed'), name
))
306 self
.privmsg_channels(
307 "%s%s has asked me to start following %s" %(
308 get_prefix('inform'), userNick
, name
))
310 def unfollow(self
, conn
, evt
, name
):
311 userNick
= evt
.source().split('!')[0]
312 friends
= [x
['name'] for x
in self
.twitter
.statuses
.friends()]
313 debug("Current friends: %s" %(friends))
314 if (name
not in friends
):
317 "%sI'm not following %s." %(get_prefix('error'), name
))
319 self
.twitter
.friendships
.destroy(screen_name
=name
)
322 "%sOkay! I've stopped following %s." %(
323 get_prefix('stop_follow'), name
))
324 self
.privmsg_channels(
325 "%s%s has asked me to stop following %s" %(
326 get_prefix('inform'), userNick
, name
))
328 def _irc_connect(self
):
329 self
.ircServer
.connect(
330 self
.config
.get('irc', 'server'),
331 self
.config
.getint('irc', 'port'),
332 self
.config
.get('irc', 'nick'))
333 channels
=self
.config
.get('irc', 'channel').split(',')
335 self
.ircServer
.send_raw(self
.config
.get('irc', 'autocmd'))
336 except Exception as e
:
337 pass # ignore errors, it's probably just not defined
339 self
.ircServer
.send_raw("MODE %s %s" % (self
.ircServer
.get_nickname(), self
.config
.get('irc', 'modes')))
340 except Exception as e
:
341 pass # ignore errors again
342 for channel
in channels
:
343 self
.ircServer
.join(channel
)
350 self
.sched
.run_forever()
351 except KeyboardInterrupt:
354 # twitter.com is probably down because it
355 # sucks. ignore the fault and keep going
357 except irclib
.ServerNotConnectedError
:
358 # Try and reconnect to IRC.
362 def load_config(filename
):
363 # Note: Python ConfigParser module has the worst interface in the
366 cp
.add_section('irc')
367 cp
.set('irc', 'port', '6667')
368 cp
.set('irc', 'nick', 'twitterbot')
369 cp
.set('irc', 'prefixes', 'cats')
370 cp
.add_section('twitter')
371 cp
.set('twitter', 'oauth_token_file', OAUTH_FILE
)
375 # attempt to read these properties-- they are required
376 cp
.get('twitter', 'oauth_token_file'),
377 cp
.get('irc', 'server')
378 cp
.getint('irc', 'port')
379 cp
.get('irc', 'nick')
380 cp
.get('irc', 'channel')
384 # So there was a joke here about the twitter business model
385 # but I got rid of it. Not because I want this codebase to
386 # be "professional" in any way, but because someone forked
387 # this and deleted the comment because they couldn't take
390 # Fact: The number one use of Google Code is to look for that
391 # comment in the Linux kernel that goes "FUCK me gently with
392 # a chainsaw." Pretty sure Linus himself wrote it.
395 configFilename
= "twitterbot.ini"
397 configFilename
= sys
.argv
[1]
400 if not os
.path
.exists(configFilename
):
402 load_config(configFilename
)
403 except Exception as e
:
404 print("Error while loading ini file %s" %(
405 configFilename
), file=sys
.stderr
)
406 print(e
, file=sys
.stderr
)
407 print(__doc__
, file=sys
.stderr
)
410 bot
= TwitterBot(configFilename
)