]>
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_channel_to_join>
23 email: <twitter_account_email>
24 password: <twitter_account_password>
26 If no config file is given "twitterbot.ini" will be used by default.
29 BOT_VERSION
= "TwitterBot 1.0 (http://mike.verdone.ca/twitter)"
32 IRC_ITALIC
= chr(0x16)
33 IRC_UNDERLINE
= chr(0x1f)
34 IRC_REGULAR
= chr(0x0f)
38 from dateutil
.parser
import parse
39 from ConfigParser
import SafeConfigParser
40 from heapq
import heappop
, heappush
44 from api
import Twitter
, TwitterError
45 from util
import htmlentitydecode
51 "This module requires python irclib available from "
52 + "http://python-irclib.sourceforge.net/")
55 # uncomment this for debug text stuff
56 # print >> sys.stderr, msg
59 class SchedTask(object):
60 def __init__(self
, task
, delta
):
63 self
.next
= time
.time()
66 return "<SchedTask %s next:%i delta:%i>" %(
67 self
.task
.__name
__, self
.next
, self
.delta
)
69 def __cmp__(self
, other
):
70 return cmp(self
.next
, other
.next
)
75 class Scheduler(object):
76 def __init__(self
, tasks
):
79 heappush(self
.task_heap
, task
)
83 task
= heappop(self
.task_heap
)
84 wait
= task
.next
- now
85 task
.next
= now
+ task
.delta
86 heappush(self
.task_heap
, task
)
90 debug("tasks: " + str(self
.task_heap
))
92 def run_forever(self
):
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()
113 def check_statuses(self
):
114 debug("In check_statuses")
116 updates
= self
.twitter
.statuses
.friends_timeline()
118 print >> sys
.stderr
, "Exception while querying twitter:"
119 traceback
.print_exc(file=sys
.stderr
)
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'))
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')))
142 self
.lastUpdate
= nextLastUpdate
144 def process_events(self
):
145 debug("In process_events")
146 self
.irc
.process_once()
148 def handle_privmsg(self
, conn
, evt
):
150 args
= evt
.arguments()[0].split(' ')
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])
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.")
165 traceback
.print_exc(file=sys
.stderr
)
167 def handle_ctcp(self
, conn
, evt
):
168 args
= evt
.arguments()
169 source
= evt
.source().split('!')[0]
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")
178 def privmsg_channel(self
, msg
):
179 return self
.ircServer
.privmsg(
180 self
.config
.get('irc', 'channel'), msg
.encode('utf-8'))
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
):
189 "=O_o= I'm already following %s." %(name))
192 self
.twitter
.friendships
.create(id=name
)
196 "=O_o= I can't follow that user. Are you sure the name is correct?")
200 "=^_^= Okay! I'm now following %s." %(name))
201 self
.privmsg_channel(
202 "=o_o= %s has asked me to start following %s" %(
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
):
212 "=O_o= I'm not following %s." %(name))
214 self
.twitter
.friendships
.destroy(id=name
)
217 "=^_^= Okay! I've stopped following %s." %(name))
218 self
.privmsg_channel(
219 "=o_o= %s has asked me to stop following %s" %(
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'))
231 self
.sched
.run_forever()
232 except KeyboardInterrupt:
235 # twitter.com is probably down because it sucks. ignore the fault and keep going
238 def load_config(filename
):
239 defaults
= dict(server
=dict(port
=6667, nick
="twitterbot"))
240 cp
= SafeConfigParser(defaults
)
243 # attempt to read these properties-- they are required
244 cp
.get('twitter', 'email'),
245 cp
.get('twitter', 'password')
246 cp
.get('irc', 'server')
247 cp
.getint('irc', 'port')
248 cp
.get('irc', 'nick')
249 cp
.get('irc', 'channel')
253 # So there was a joke here about the twitter business model
254 # but I got rid of it. Not because I want this codebase to
255 # be "professional" in any way, but because someone forked
256 # this and deleted the comment because they couldn't take
259 # Fact: The number one use of Google Code is to look for that
260 # comment in the Linux kernel that goes "FUCK me gently with
261 # a chainsaw." Pretty sure Linus himself wrote it.
264 configFilename
= "twitterbot.ini"
266 configFilename
= sys
.argv
[1]
269 if not os
.path
.exists(configFilename
):
271 load_config(configFilename
)
273 print >> sys
.stderr
, "Error while loading ini file %s" %(
275 print >> sys
.stderr
, e
276 print >> sys
.stderr
, __doc__
279 bot
= TwitterBot(configFilename
)