]> jfr.im git - z_archive/twitter.git/blob - twitter/ircbot.py
Version 1.17.0
[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.9.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.environ.get('USERPROFILE', '')) + 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 domain='api.twitter.com')
152
153 self.irc = irclib.IRC()
154 self.irc.add_global_handler('privmsg', self.handle_privmsg)
155 self.irc.add_global_handler('ctcp', self.handle_ctcp)
156 self.irc.add_global_handler('umode', self.handle_umode)
157 self.ircServer = self.irc.server()
158
159 self.sched = Scheduler(
160 (SchedTask(self.process_events, 1),
161 SchedTask(self.check_statuses, 120)))
162 self.lastUpdate = (datetime.utcnow() - timedelta(minutes=10)).utctimetuple()
163
164 def check_statuses(self):
165 debug("In check_statuses")
166 try:
167 updates = reversed(self.twitter.statuses.home_timeline())
168 except Exception as e:
169 print("Exception while querying twitter:", file=sys.stderr)
170 traceback.print_exc(file=sys.stderr)
171 return
172
173 nextLastUpdate = self.lastUpdate
174 for update in updates:
175 crt = parsedate(update['created_at'])
176 if (crt > nextLastUpdate):
177 text = (htmlentitydecode(
178 update['text'].replace('\n', ' '))
179 .encode('utf8', 'replace'))
180
181 # Skip updates beginning with @
182 # TODO This would be better if we only ignored messages
183 # to people who are not on our following list.
184 if not text.startswith(b"@"):
185 msg = "%s %s%s%s %s" %(
186 get_prefix(),
187 IRC_BOLD, update['user']['screen_name'],
188 IRC_BOLD, text.decode('utf8'))
189 self.privmsg_channels(msg)
190
191 nextLastUpdate = crt
192
193 self.lastUpdate = nextLastUpdate
194
195 def process_events(self):
196 self.irc.process_once()
197
198 def handle_privmsg(self, conn, evt):
199 debug('got privmsg')
200 args = evt.arguments()[0].split(' ')
201 try:
202 if (not args):
203 return
204 if (args[0] == 'follow' and args[1:]):
205 self.follow(conn, evt, args[1])
206 elif (args[0] == 'unfollow' and args[1:]):
207 self.unfollow(conn, evt, args[1])
208 else:
209 conn.privmsg(
210 evt.source().split('!')[0],
211 "%sHi! I'm Twitterbot! you can (follow "
212 "<twitter_name>) to make me follow a user or "
213 "(unfollow <twitter_name>) to make me stop." %
214 get_prefix())
215 except Exception:
216 traceback.print_exc(file=sys.stderr)
217
218 def handle_ctcp(self, conn, evt):
219 args = evt.arguments()
220 source = evt.source().split('!')[0]
221 if (args):
222 if args[0] == 'VERSION':
223 conn.ctcp_reply(source, "VERSION " + BOT_VERSION)
224 elif args[0] == 'PING':
225 conn.ctcp_reply(source, "PING")
226 elif args[0] == 'CLIENTINFO':
227 conn.ctcp_reply(source, "CLIENTINFO PING VERSION CLIENTINFO")
228
229 def handle_umode(self, conn, evt):
230 """
231 QuakeNet ignores all your commands until after the MOTD. This
232 handler defers joining until after it sees a magic line. It
233 also tries to join right after connect, but this will just
234 make it join again which should be safe.
235 """
236 args = evt.arguments()
237 if (args and args[0] == '+i'):
238 channels = self.config.get('irc', 'channel').split(',')
239 for channel in channels:
240 self.ircServer.join(channel)
241
242 def privmsg_channels(self, msg):
243 return_response=True
244 channels=self.config.get('irc','channel').split(',')
245 return self.ircServer.privmsg_many(channels, msg.encode('utf8'))
246
247 def follow(self, conn, evt, name):
248 userNick = evt.source().split('!')[0]
249 friends = [x['name'] for x in self.twitter.statuses.friends()]
250 debug("Current friends: %s" %(friends))
251 if (name in friends):
252 conn.privmsg(
253 userNick,
254 "%sI'm already following %s." %(get_prefix('error'), name))
255 else:
256 try:
257 self.twitter.friendships.create(screen_name=name)
258 except TwitterError:
259 conn.privmsg(
260 userNick,
261 "%sI can't follow that user. Are you sure the name is correct?" %(
262 get_prefix('error')
263 ))
264 return
265 conn.privmsg(
266 userNick,
267 "%sOkay! I'm now following %s." %(get_prefix('followed'), name))
268 self.privmsg_channels(
269 "%s%s has asked me to start following %s" %(
270 get_prefix('inform'), userNick, name))
271
272 def unfollow(self, conn, evt, name):
273 userNick = evt.source().split('!')[0]
274 friends = [x['name'] for x in self.twitter.statuses.friends()]
275 debug("Current friends: %s" %(friends))
276 if (name not in friends):
277 conn.privmsg(
278 userNick,
279 "%sI'm not following %s." %(get_prefix('error'), name))
280 else:
281 self.twitter.friendships.destroy(screen_name=name)
282 conn.privmsg(
283 userNick,
284 "%sOkay! I've stopped following %s." %(
285 get_prefix('stop_follow'), name))
286 self.privmsg_channels(
287 "%s%s has asked me to stop following %s" %(
288 get_prefix('inform'), userNick, name))
289
290 def _irc_connect(self):
291 self.ircServer.connect(
292 self.config.get('irc', 'server'),
293 self.config.getint('irc', 'port'),
294 self.config.get('irc', 'nick'))
295 channels=self.config.get('irc', 'channel').split(',')
296 for channel in channels:
297 self.ircServer.join(channel)
298
299 def run(self):
300 self._irc_connect()
301
302 while True:
303 try:
304 self.sched.run_forever()
305 except KeyboardInterrupt:
306 break
307 except TwitterError:
308 # twitter.com is probably down because it
309 # sucks. ignore the fault and keep going
310 pass
311 except irclib.ServerNotConnectedError:
312 # Try and reconnect to IRC.
313 self._irc_connect()
314
315
316 def load_config(filename):
317 # Note: Python ConfigParser module has the worst interface in the
318 # world. Mega gross.
319 cp = ConfigParser()
320 cp.add_section('irc')
321 cp.set('irc', 'port', '6667')
322 cp.set('irc', 'nick', 'twitterbot')
323 cp.set('irc', 'prefixes', 'cats')
324 cp.add_section('twitter')
325 cp.set('twitter', 'oauth_token_file', OAUTH_FILE)
326
327 cp.read((filename,))
328
329 # attempt to read these properties-- they are required
330 cp.get('twitter', 'oauth_token_file'),
331 cp.get('irc', 'server')
332 cp.getint('irc', 'port')
333 cp.get('irc', 'nick')
334 cp.get('irc', 'channel')
335
336 return cp
337
338 # So there was a joke here about the twitter business model
339 # but I got rid of it. Not because I want this codebase to
340 # be "professional" in any way, but because someone forked
341 # this and deleted the comment because they couldn't take
342 # a joke. Hi guy!
343 #
344 # Fact: The number one use of Google Code is to look for that
345 # comment in the Linux kernel that goes "FUCK me gently with
346 # a chainsaw." Pretty sure Linus himself wrote it.
347
348 def main():
349 configFilename = "twitterbot.ini"
350 if (sys.argv[1:]):
351 configFilename = sys.argv[1]
352
353 try:
354 if not os.path.exists(configFilename):
355 raise Exception()
356 load_config(configFilename)
357 except Exception as e:
358 print("Error while loading ini file %s" %(
359 configFilename), file=sys.stderr)
360 print(e, file=sys.stderr)
361 print(__doc__, file=sys.stderr)
362 sys.exit(1)
363
364 bot = TwitterBot(configFilename)
365 return bot.run()