]> jfr.im git - z_archive/twitter.git/blame_incremental - 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
... / ...
CommitLineData
1"""
2twitterbot
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
8USAGE
9
10 twitterbot [config_file]
11
12CONFIG_FILE
13
14 The config file is an ini-style file that must contain the following:
15
16[irc]
17server: <irc_server>
18port: <irc_port>
19nick: <irc_nickname>
20channel: <irc_channels_to_join>
21prefixes: <prefix_type>
22
23[twitter]
24oauth_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
37from __future__ import print_function
38
39BOT_VERSION = "TwitterBot 1.6.1 (http://mike.verdone.ca/twitter)"
40
41CONSUMER_KEY = "XryIxN3J2ACaJs50EizfLQ"
42CONSUMER_SECRET = "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
43
44IRC_BOLD = chr(0x02)
45IRC_ITALIC = chr(0x16)
46IRC_UNDERLINE = chr(0x1f)
47IRC_REGULAR = chr(0x0f)
48
49import sys
50import time
51from datetime import datetime, timedelta
52from email.utils import parsedate
53try:
54 from configparser import ConfigParser
55except ImportError:
56 from ConfigParser import ConfigParser
57from heapq import heappop, heappush
58import traceback
59import os
60import os.path
61
62from .api import Twitter, TwitterError
63from .oauth import OAuth, read_token_file
64from .oauth_dance import oauth_dance
65from .util import htmlentitydecode
66
67PREFIXES = dict(
68 cats=dict(
69 new_tweet="=^_^= ",
70 error="=O_o= ",
71 inform="=o_o= "
72 ),
73 none=dict(
74 new_tweet=""
75 ),
76 )
77ACTIVE_PREFIXES=dict()
78
79def get_prefix(prefix_typ=None):
80 return ACTIVE_PREFIXES.get(prefix_typ, ACTIVE_PREFIXES.get('new_tweet', ''))
81
82
83try:
84 import irclib
85except 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
90OAUTH_FILE = os.environ.get('HOME', '') + os.sep + '.twitterbot_oauth'
91
92def debug(msg):
93 # uncomment this for debug text stuff
94 # print(msg, file=sys.stdout)
95 pass
96
97class 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
113class 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
135class 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
317def 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
349def 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()