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