]> jfr.im git - z_archive/twitter.git/blob - twitter/ircbot.py
Add a umode handler to ircbot to work with quakenet. see: http://www.reddit.com/r...
[z_archive/twitter.git] / twitter / ircbot.py
1 """
2 twitterbot
3
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
6 friends.
7
8 USAGE
9
10 twitterbot [config_file]
11
12 CONFIG_FILE
13
14 The config file is an ini-style file that must contain the following:
15
16 [irc]
17 server: <irc_server>
18 port: <irc_port>
19 nick: <irc_nickname>
20 channel: <irc_channels_to_join>
21 prefixes: <prefix_type>
22
23 [twitter]
24 oauth_token_file: <oauth_token_filename>
25
26
27 If no config file is given "twitterbot.ini" will be used by default.
28
29 The channel argument can accept multiple channels separated by commas.
30
31 The default token file is ~/.twitterbot_oauth.
32
33 The default prefix type is 'cats'. You can also use 'none'.
34
35 """
36
37 from __future__ import print_function
38
39 BOT_VERSION = "TwitterBot 1.6.1 (http://mike.verdone.ca/twitter)"
40
41 CONSUMER_KEY = "XryIxN3J2ACaJs50EizfLQ"
42 CONSUMER_SECRET = "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
43
44 IRC_BOLD = chr(0x02)
45 IRC_ITALIC = chr(0x16)
46 IRC_UNDERLINE = chr(0x1f)
47 IRC_REGULAR = chr(0x0f)
48
49 import sys
50 import time
51 from datetime import datetime, timedelta
52 from email.utils import parsedate
53 try:
54 from configparser import ConfigParser
55 except ImportError:
56 from ConfigParser import ConfigParser
57 from heapq import heappop, heappush
58 import traceback
59 import os
60 import os.path
61
62 from .api import Twitter, TwitterError
63 from .oauth import OAuth, read_token_file
64 from .oauth_dance import oauth_dance
65 from .util import htmlentitydecode
66
67 PREFIXES = dict(
68 cats=dict(
69 new_tweet="=^_^= ",
70 error="=O_o= ",
71 inform="=o_o= "
72 ),
73 none=dict(
74 new_tweet=""
75 ),
76 )
77 ACTIVE_PREFIXES=dict()
78
79 def get_prefix(prefix_typ=None):
80 return ACTIVE_PREFIXES.get(prefix_typ, ACTIVE_PREFIXES.get('new_tweet', ''))
81
82
83 try:
84 import irclib
85 except ImportError:
86 raise ImportError(
87 "This module requires python irclib available from "
88 + "https://github.com/sixohsix/python-irclib/zipball/python-irclib3-0.4.8")
89
90 OAUTH_FILE = os.environ.get('HOME', '') + os.sep + '.twitterbot_oauth'
91
92 def debug(msg):
93 # uncomment this for debug text stuff
94 # print(msg, file=sys.stdout)
95 pass
96
97 class SchedTask(object):
98 def __init__(self, task, delta):
99 self.task = task
100 self.delta = delta
101 self.next = time.time()
102
103 def __repr__(self):
104 return "<SchedTask %s next:%i delta:%i>" %(
105 self.task.__name__, self.__next__, self.delta)
106
107 def __lt__(self, other):
108 return self.next < other.next
109
110 def __call__(self):
111 return self.task()
112
113 class Scheduler(object):
114 def __init__(self, tasks):
115 self.task_heap = []
116 for task in tasks:
117 heappush(self.task_heap, task)
118
119 def next_task(self):
120 now = time.time()
121 task = heappop(self.task_heap)
122 wait = task.next - now
123 task.next = now + task.delta
124 heappush(self.task_heap, task)
125 if (wait > 0):
126 time.sleep(wait)
127 task()
128 #debug("tasks: " + str(self.task_heap))
129
130 def run_forever(self):
131 while True:
132 self.next_task()
133
134
135 class TwitterBot(object):
136 def __init__(self, configFilename):
137 self.configFilename = configFilename
138 self.config = load_config(self.configFilename)
139
140 global ACTIVE_PREFIXES
141 ACTIVE_PREFIXES = PREFIXES[self.config.get('irc', 'prefixes')]
142
143 oauth_file = self.config.get('twitter', 'oauth_token_file')
144 if not os.path.exists(oauth_file):
145 oauth_dance("IRC Bot", CONSUMER_KEY, CONSUMER_SECRET, oauth_file)
146 oauth_token, oauth_secret = read_token_file(oauth_file)
147
148 self.twitter = Twitter(
149 auth=OAuth(
150 oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET),
151 api_version='1',
152 domain='api.twitter.com')
153
154 self.irc = irclib.IRC()
155 self.irc.add_global_handler('privmsg', self.handle_privmsg)
156 self.irc.add_global_handler('ctcp', self.handle_ctcp)
157 self.irc.add_global_handler('umode', self.handle_umode)
158 self.ircServer = self.irc.server()
159
160 self.sched = Scheduler(
161 (SchedTask(self.process_events, 1),
162 SchedTask(self.check_statuses, 120)))
163 self.lastUpdate = (datetime.utcnow() - timedelta(minutes=10)).utctimetuple()
164
165 def check_statuses(self):
166 debug("In check_statuses")
167 try:
168 updates = reversed(self.twitter.statuses.friends_timeline())
169 except Exception as e:
170 print("Exception while querying twitter:", file=sys.stderr)
171 traceback.print_exc(file=sys.stderr)
172 return
173
174 nextLastUpdate = self.lastUpdate
175 for update in updates:
176 crt = parsedate(update['created_at'])
177 if (crt > nextLastUpdate):
178 text = (htmlentitydecode(
179 update['text'].replace('\n', ' '))
180 .encode('utf8', 'replace'))
181
182 # Skip updates beginning with @
183 # TODO This would be better if we only ignored messages
184 # to people who are not on our following list.
185 if not text.startswith(b"@"):
186 msg = "%s %s%s%s %s" %(
187 get_prefix(),
188 IRC_BOLD, update['user']['screen_name'],
189 IRC_BOLD, text.decode('utf8'))
190 self.privmsg_channels(msg)
191
192 nextLastUpdate = crt
193
194 self.lastUpdate = nextLastUpdate
195
196 def process_events(self):
197 self.irc.process_once()
198
199 def handle_privmsg(self, conn, evt):
200 debug('got privmsg')
201 args = evt.arguments()[0].split(' ')
202 try:
203 if (not args):
204 return
205 if (args[0] == 'follow' and args[1:]):
206 self.follow(conn, evt, args[1])
207 elif (args[0] == 'unfollow' and args[1:]):
208 self.unfollow(conn, evt, args[1])
209 else:
210 conn.privmsg(
211 evt.source().split('!')[0],
212 "%sHi! I'm Twitterbot! you can (follow "
213 "<twitter_name>) to make me follow a user or "
214 "(unfollow <twitter_name>) to make me stop." %
215 get_prefix())
216 except Exception:
217 traceback.print_exc(file=sys.stderr)
218
219 def handle_ctcp(self, conn, evt):
220 args = evt.arguments()
221 source = evt.source().split('!')[0]
222 if (args):
223 if args[0] == 'VERSION':
224 conn.ctcp_reply(source, "VERSION " + BOT_VERSION)
225 elif args[0] == 'PING':
226 conn.ctcp_reply(source, "PING")
227 elif args[0] == 'CLIENTINFO':
228 conn.ctcp_reply(source, "CLIENTINFO PING VERSION CLIENTINFO")
229
230 def handle_umode(self, conn, evt):
231 """
232 QuakeNet ignores all your commands until after the MOTD. This
233 handler defers joining until after it sees a magic line. It
234 also tries to join right after connect, but this will just
235 make it join again which should be safe.
236 """
237 args = evt.arguments()
238 if (args and args[0] == '+i'):
239 channels = self.config.get('irc', 'channel').split(',')
240 for channel in channels:
241 self.ircServer.join(channel)
242
243 def privmsg_channels(self, msg):
244 return_response=True
245 channels=self.config.get('irc','channel').split(',')
246 return self.ircServer.privmsg_many(channels, msg.encode('utf8'))
247
248 def follow(self, conn, evt, name):
249 userNick = evt.source().split('!')[0]
250 friends = [x['name'] for x in self.twitter.statuses.friends()]
251 debug("Current friends: %s" %(friends))
252 if (name in friends):
253 conn.privmsg(
254 userNick,
255 "%sI'm already following %s." %(get_prefix('error'), name))
256 else:
257 try:
258 self.twitter.friendships.create(id=name)
259 except TwitterError:
260 conn.privmsg(
261 userNick,
262 "%sI can't follow that user. Are you sure the name is correct?" %(
263 get_prefix('error')
264 ))
265 return
266 conn.privmsg(
267 userNick,
268 "%sOkay! I'm now following %s." %(get_prefix('followed'), name))
269 self.privmsg_channels(
270 "%s%s has asked me to start following %s" %(
271 get_prefix('inform'), userNick, name))
272
273 def unfollow(self, conn, evt, name):
274 userNick = evt.source().split('!')[0]
275 friends = [x['name'] for x in self.twitter.statuses.friends()]
276 debug("Current friends: %s" %(friends))
277 if (name not in friends):
278 conn.privmsg(
279 userNick,
280 "%sI'm not following %s." %(get_prefix('error'), name))
281 else:
282 self.twitter.friendships.destroy(id=name)
283 conn.privmsg(
284 userNick,
285 "%sOkay! I've stopped following %s." %(
286 get_prefix('stop_follow'), name))
287 self.privmsg_channels(
288 "%s%s has asked me to stop following %s" %(
289 get_prefix('inform'), userNick, name))
290
291 def _irc_connect(self):
292 self.ircServer.connect(
293 self.config.get('irc', 'server'),
294 self.config.getint('irc', 'port'),
295 self.config.get('irc', 'nick'))
296 channels=self.config.get('irc', 'channel').split(',')
297 for channel in channels:
298 self.ircServer.join(channel)
299
300 def run(self):
301 self._irc_connect()
302
303 while True:
304 try:
305 self.sched.run_forever()
306 except KeyboardInterrupt:
307 break
308 except TwitterError:
309 # twitter.com is probably down because it
310 # sucks. ignore the fault and keep going
311 pass
312 except irclib.ServerNotConnectedError:
313 # Try and reconnect to IRC.
314 self._irc_connect()
315
316
317 def load_config(filename):
318 # Note: Python ConfigParser module has the worst interface in the
319 # world. Mega gross.
320 cp = ConfigParser()
321 cp.add_section('irc')
322 cp.set('irc', 'port', '6667')
323 cp.set('irc', 'nick', 'twitterbot')
324 cp.set('irc', 'prefixes', 'cats')
325 cp.add_section('twitter')
326 cp.set('twitter', 'oauth_token_file', OAUTH_FILE)
327
328 cp.read((filename,))
329
330 # attempt to read these properties-- they are required
331 cp.get('twitter', 'oauth_token_file'),
332 cp.get('irc', 'server')
333 cp.getint('irc', 'port')
334 cp.get('irc', 'nick')
335 cp.get('irc', 'channel')
336
337 return cp
338
339 # So there was a joke here about the twitter business model
340 # but I got rid of it. Not because I want this codebase to
341 # be "professional" in any way, but because someone forked
342 # this and deleted the comment because they couldn't take
343 # a joke. Hi guy!
344 #
345 # Fact: The number one use of Google Code is to look for that
346 # comment in the Linux kernel that goes "FUCK me gently with
347 # a chainsaw." Pretty sure Linus himself wrote it.
348
349 def main():
350 configFilename = "twitterbot.ini"
351 if (sys.argv[1:]):
352 configFilename = sys.argv[1]
353
354 try:
355 if not os.path.exists(configFilename):
356 raise Exception()
357 load_config(configFilename)
358 except Exception as e:
359 print("Error while loading ini file %s" %(
360 configFilename), file=sys.stderr)
361 print(e, file=sys.stderr)
362 print(__doc__, file=sys.stderr)
363 sys.exit(1)
364
365 bot = TwitterBot(configFilename)
366 return bot.run()