]> jfr.im git - z_archive/twitter.git/blame - twitter/ircbot.py
Another removal of term encoding (py3_only)
[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>
5b7080ef 21
22[twitter]
52792ab4 23oauth_token_file: <oauth_token_filename>
5b7080ef 24
b313a2b4 25
5b7080ef 26 If no config file is given "twitterbot.ini" will be used by default.
77a8613a
M
27
28 The channel argument can accept multiple channels separated by commas.
52792ab4
MV
29
30 The default token file is ~/.twitterbot_oauth.
31
5b7080ef 32"""
d8ac8b72 33
52792ab4
MV
34BOT_VERSION = "TwitterBot 1.4 (http://mike.verdone.ca/twitter)"
35
36CONSUMER_KEY = "XryIxN3J2ACaJs50EizfLQ"
37CONSUMER_SECRET = "j7IuDCNjftVY8DBauRdqXs4jDl5Fgk1IJRag8iE"
d8ac8b72 38
39IRC_BOLD = chr(0x02)
40IRC_ITALIC = chr(0x16)
41IRC_UNDERLINE = chr(0x1f)
42IRC_REGULAR = chr(0x0f)
43
7d7c8094 44import sys
772fbdd1 45import time
46from dateutil.parser import parse
f7e63802 47from configparser import SafeConfigParser
7d7c8094 48from heapq import heappop, heappush
49import traceback
52792ab4 50import os
9d1c2940 51import os.path
7d7c8094 52
f7e63802
MV
53from .api import Twitter, TwitterError
54from .oauth import OAuth, read_token_file
55from .oauth_dance import oauth_dance
56from .util import htmlentitydecode
772fbdd1 57
58try:
59 import irclib
60except:
7d7c8094 61 raise ImportError(
62 "This module requires python irclib available from "
63 + "http://python-irclib.sourceforge.net/")
64
52792ab4
MV
65OAUTH_FILE = os.environ.get('HOME', '') + os.sep + '.twitterbot_oauth'
66
7d7c8094 67def debug(msg):
68 # uncomment this for debug text stuff
5b7080ef 69 # print >> sys.stderr, msg
7d7c8094 70 pass
772fbdd1 71
72class SchedTask(object):
73 def __init__(self, task, delta):
74 self.task = task
75 self.delta = delta
7d7c8094 76 self.next = time.time()
77
78 def __repr__(self):
79 return "<SchedTask %s next:%i delta:%i>" %(
f7e63802 80 self.task.__name__, self.__next__, self.delta)
d828f28d 81
7d7c8094 82 def __cmp__(self, other):
f7e63802 83 return cmp(self.__next__, other.__next__)
d828f28d 84
7d7c8094 85 def __call__(self):
86 return self.task()
87
772fbdd1 88class Scheduler(object):
89 def __init__(self, tasks):
7d7c8094 90 self.task_heap = []
91 for task in tasks:
92 heappush(self.task_heap, task)
d828f28d 93
772fbdd1 94 def next_task(self):
95 now = time.time()
7d7c8094 96 task = heappop(self.task_heap)
f7e63802 97 wait = task.__next__ - now
65ec2606 98 task.next = now + task.delta
99 heappush(self.task_heap, task)
772fbdd1 100 if (wait > 0):
101 time.sleep(wait)
7d7c8094 102 task()
7d7c8094 103 debug("tasks: " + str(self.task_heap))
d828f28d 104
772fbdd1 105 def run_forever(self):
65ec2606 106 while True:
107 self.next_task()
108
d828f28d 109
772fbdd1 110class TwitterBot(object):
7d7c8094 111 def __init__(self, configFilename):
112 self.configFilename = configFilename
113 self.config = load_config(self.configFilename)
52792ab4
MV
114
115 oauth_file = self.config.get('twitter', 'oauth_token_file')
116 if not os.path.exists(oauth_file):
117 oauth_dance("IRC Bot", CONSUMER_KEY, CONSUMER_SECRET, oauth_file)
118 oauth_token, oauth_secret = read_token_file(oauth_file)
119
120 self.twitter = Twitter(
121 auth=OAuth(
122 oauth_token, oauth_secret, CONSUMER_KEY, CONSUMER_SECRET),
d828f28d
MV
123 api_version='1',
124 domain='api.twitter.com')
52792ab4 125
772fbdd1 126 self.irc = irclib.IRC()
7d7c8094 127 self.irc.add_global_handler('privmsg', self.handle_privmsg)
d8ac8b72 128 self.irc.add_global_handler('ctcp', self.handle_ctcp)
7d7c8094 129 self.ircServer = self.irc.server()
52792ab4 130
772fbdd1 131 self.sched = Scheduler(
7d7c8094 132 (SchedTask(self.process_events, 1),
d8ac8b72 133 SchedTask(self.check_statuses, 120)))
7d7c8094 134 self.lastUpdate = time.gmtime()
772fbdd1 135
136 def check_statuses(self):
7d7c8094 137 debug("In check_statuses")
138 try:
139 updates = self.twitter.statuses.friends_timeline()
f7e63802
MV
140 except Exception as e:
141 print("Exception while querying twitter:", file=sys.stderr)
7d7c8094 142 traceback.print_exc(file=sys.stderr)
143 return
d828f28d 144
6da3627e 145 nextLastUpdate = self.lastUpdate
7d7c8094 146 for update in updates:
147 crt = parse(update['created_at']).utctimetuple()
148 if (crt > self.lastUpdate):
8ad2cf0b 149 text = (htmlentitydecode(
150 update['text'].replace('\n', ' '))
151 .encode('utf-8', 'replace'))
7f6c5ae3 152
153 # Skip updates beginning with @
154 # TODO This would be better if we only ignored messages
155 # to people who are not on our following list.
156 if not text.startswith("@"):
c1b9acea 157 self.privmsg_channels(
f7e63802 158 "=^_^= %s%s%s %s" %(
7f6c5ae3 159 IRC_BOLD, update['user']['screen_name'],
65ec2606 160 IRC_BOLD, text.decode('utf-8')))
d828f28d 161
6da3627e 162 nextLastUpdate = crt
7d7c8094 163 else:
164 break
6da3627e 165 self.lastUpdate = nextLastUpdate
d828f28d 166
7d7c8094 167 def process_events(self):
168 debug("In process_events")
169 self.irc.process_once()
d828f28d 170
7d7c8094 171 def handle_privmsg(self, conn, evt):
172 debug('got privmsg')
173 args = evt.arguments()[0].split(' ')
174 try:
175 if (not args):
176 return
177 if (args[0] == 'follow' and args[1:]):
178 self.follow(conn, evt, args[1])
179 elif (args[0] == 'unfollow' and args[1:]):
180 self.unfollow(conn, evt, args[1])
181 else:
182 conn.privmsg(
d828f28d 183 evt.source().split('!')[0],
7d7c8094 184 "=^_^= Hi! I'm Twitterbot! you can (follow "
185 + "<twitter_name>) to make me follow a user or "
186 + "(unfollow <twitter_name>) to make me stop.")
187 except Exception:
188 traceback.print_exc(file=sys.stderr)
d828f28d 189
d8ac8b72 190 def handle_ctcp(self, conn, evt):
191 args = evt.arguments()
192 source = evt.source().split('!')[0]
193 if (args):
194 if args[0] == 'VERSION':
195 conn.ctcp_reply(source, "VERSION " + BOT_VERSION)
196 elif args[0] == 'PING':
197 conn.ctcp_reply(source, "PING")
198 elif args[0] == 'CLIENTINFO':
199 conn.ctcp_reply(source, "CLIENTINFO PING VERSION CLIENTINFO")
7d7c8094 200
201 def privmsg_channel(self, msg):
202 return self.ircServer.privmsg(
65ec2606 203 self.config.get('irc', 'channel'), msg.encode('utf-8'))
d828f28d 204
c1b9acea
PP
205 def privmsg_channels(self, msg):
206 return_response=True
207 channels=self.config.get('irc','channel').split(',')
208 return self.ircServer.privmsg_many(channels, msg.encode('utf-8'))
d828f28d 209
7d7c8094 210 def follow(self, conn, evt, name):
211 userNick = evt.source().split('!')[0]
212 friends = [x['name'] for x in self.twitter.statuses.friends()]
213 debug("Current friends: %s" %(friends))
214 if (name in friends):
215 conn.privmsg(
216 userNick,
217 "=O_o= I'm already following %s." %(name))
218 else:
c93672d4 219 try:
220 self.twitter.friendships.create(id=name)
221 except TwitterError:
222 conn.privmsg(
223 userNick,
224 "=O_o= I can't follow that user. Are you sure the name is correct?")
225 return
7d7c8094 226 conn.privmsg(
227 userNick,
228 "=^_^= Okay! I'm now following %s." %(name))
c1b9acea 229 self.privmsg_channels(
7d7c8094 230 "=o_o= %s has asked me to start following %s" %(
231 userNick, name))
d828f28d 232
7d7c8094 233 def unfollow(self, conn, evt, name):
234 userNick = evt.source().split('!')[0]
235 friends = [x['name'] for x in self.twitter.statuses.friends()]
236 debug("Current friends: %s" %(friends))
237 if (name not in friends):
238 conn.privmsg(
239 userNick,
240 "=O_o= I'm not following %s." %(name))
241 else:
242 self.twitter.friendships.destroy(id=name)
243 conn.privmsg(
244 userNick,
245 "=^_^= Okay! I've stopped following %s." %(name))
c1b9acea 246 self.privmsg_channels(
7d7c8094 247 "=o_o= %s has asked me to stop following %s" %(
248 userNick, name))
d828f28d 249
772fbdd1 250 def run(self):
7d7c8094 251 self.ircServer.connect(
d828f28d 252 self.config.get('irc', 'server'),
7d7c8094 253 self.config.getint('irc', 'port'),
254 self.config.get('irc', 'nick'))
c1b9acea
PP
255 channels=self.config.get('irc', 'channel').split(',')
256 for channel in channels:
257 self.ircServer.join(channel)
65ec2606 258
259 while True:
260 try:
261 self.sched.run_forever()
262 except KeyboardInterrupt:
263 break
264 except TwitterError:
265 # twitter.com is probably down because it sucks. ignore the fault and keep going
266 pass
7d7c8094 267
268def load_config(filename):
ca242389
MV
269 # Note: Python ConfigParser module has the worst interface in the
270 # world. Mega gross.
271 cp = SafeConfigParser()
272 cp.add_section('irc')
273 cp.set('irc', 'port', '6667')
274 cp.set('irc', 'nick', 'twitterbot')
275 cp.add_section('twitter')
276 cp.set('twitter', 'oauth_token_file', OAUTH_FILE)
7d7c8094 277 cp.read((filename,))
d828f28d 278
9d1c2940 279 # attempt to read these properties-- they are required
52792ab4 280 cp.get('twitter', 'oauth_token_file'),
625bc8f0
MV
281 cp.get('irc', 'server')
282 cp.getint('irc', 'port')
283 cp.get('irc', 'nick')
284 cp.get('irc', 'channel')
9d1c2940 285
7d7c8094 286 return cp
772fbdd1 287
44bcaa4f
MV
288# So there was a joke here about the twitter business model
289# but I got rid of it. Not because I want this codebase to
290# be "professional" in any way, but because someone forked
291# this and deleted the comment because they couldn't take
292# a joke. Hi guy!
9d1c2940 293#
44bcaa4f
MV
294# Fact: The number one use of Google Code is to look for that
295# comment in the Linux kernel that goes "FUCK me gently with
296# a chainsaw." Pretty sure Linus himself wrote it.
9d1c2940 297
7d7c8094 298def main():
299 configFilename = "twitterbot.ini"
300 if (sys.argv[1:]):
301 configFilename = sys.argv[1]
d828f28d 302
5b7080ef 303 try:
9d1c2940
MV
304 if not os.path.exists(configFilename):
305 raise Exception()
5b7080ef 306 load_config(configFilename)
f7e63802
MV
307 except Exception as e:
308 print("Error while loading ini file %s" %(
309 configFilename), file=sys.stderr)
310 print(e, file=sys.stderr)
311 print(__doc__, file=sys.stderr)
5b7080ef 312 sys.exit(1)
9d1c2940 313
7d7c8094 314 bot = TwitterBot(configFilename)
315 return bot.run()