|
1
|
|
|
#!/usr/bin/env python |
|
2
|
|
|
# |
|
3
|
|
|
# see version.py for version |
|
4
|
|
|
# works with python 2.6.x and 2.7.x |
|
5
|
|
|
# |
|
6
|
|
|
|
|
7
|
|
|
|
|
8
|
|
|
import sys |
|
9
|
|
|
import socket |
|
10
|
|
|
import string |
|
11
|
|
|
import os |
|
12
|
|
|
import datetime |
|
13
|
|
|
import time |
|
14
|
|
|
import select |
|
15
|
|
|
import traceback |
|
16
|
|
|
import threading |
|
17
|
|
|
import inspect |
|
18
|
|
|
import argparse |
|
19
|
|
|
|
|
20
|
|
|
import botbrain |
|
21
|
|
|
from logger import Logger |
|
22
|
|
|
import confman |
|
23
|
|
|
from event import Event |
|
24
|
|
|
import util |
|
25
|
|
|
|
|
26
|
|
|
DEBUG = False |
|
27
|
|
|
RETRY_COUNTER = 0 |
|
28
|
|
|
|
|
29
|
|
|
class Bot(threading.Thread): |
|
30
|
|
|
""" |
|
31
|
|
|
bot instance. one bot gets instantiated per network, as an entirely distinct, sandboxed thread. |
|
32
|
|
|
handles the core IRC protocol stuff, and passing lines to defined events, which dispatch to their subscribed modules. |
|
33
|
|
|
""" |
|
34
|
|
|
def __init__(self, conf=None, network=None, d=None): |
|
35
|
|
|
threading.Thread.__init__(self) |
|
36
|
|
|
|
|
37
|
|
|
self.HOST = None |
|
38
|
|
|
self.PORT = None |
|
39
|
|
|
self.REALNAME = None |
|
40
|
|
|
self.IDENT = None |
|
41
|
|
|
self.DEBUG = d |
|
42
|
|
|
self.brain = None |
|
43
|
|
|
self.network = network |
|
44
|
|
|
self.OFFLINE = False |
|
45
|
|
|
self.CONNECTED = False |
|
46
|
|
|
self.JOINED = False |
|
47
|
|
|
self.conf = conf |
|
48
|
|
|
self.pid = os.getpid() |
|
49
|
|
|
self.logger = Logger() |
|
50
|
|
|
# to be a dict of dicts |
|
51
|
|
|
self.command_function_map = dict() |
|
52
|
|
|
|
|
53
|
|
|
if self.conf.getDBType() == "sqlite": |
|
54
|
|
|
import lite |
|
55
|
|
|
self.db = lite.SqliteDB(self) |
|
56
|
|
|
else: |
|
57
|
|
|
import db |
|
58
|
|
|
self.db = db.DB(self) |
|
59
|
|
|
|
|
60
|
|
|
|
|
61
|
|
|
self.NICK = self.conf.getNick(self.network) |
|
62
|
|
|
|
|
63
|
|
|
self.logger.write(Logger.INFO, "\n", self.NICK) |
|
64
|
|
|
self.logger.write(Logger.INFO, " initializing bot, pid " + str(os.getpid()), self.NICK) |
|
65
|
|
|
|
|
66
|
|
|
# arbitrary key/value store for modules |
|
67
|
|
|
# they should be 'namespaced' like bot.mem_store.module_name |
|
68
|
|
|
self.mem_store = dict() |
|
69
|
|
|
|
|
70
|
|
|
self.CHANNELINIT = conf.getChannels(self.network) |
|
71
|
|
|
# this will be the socket |
|
72
|
|
|
self.s = None # each bot thread holds its own socket open to the network |
|
73
|
|
|
|
|
74
|
|
|
self.brain = botbrain.BotBrain(self.send, self) |
|
75
|
|
|
|
|
76
|
|
|
self.events_list = list() |
|
77
|
|
|
|
|
78
|
|
|
# define events here and add them to the events_list |
|
79
|
|
|
|
|
80
|
|
|
all_lines = Event("1__all_lines__") |
|
81
|
|
|
all_lines.define(".*") |
|
82
|
|
|
self.events_list.append(all_lines) |
|
83
|
|
|
|
|
84
|
|
|
implying = Event("__implying__") |
|
85
|
|
|
implying.define(">") |
|
86
|
|
|
|
|
87
|
|
|
#command = Event("__command__") |
|
88
|
|
|
# this is an example of passing in a regular expression to the event definition |
|
89
|
|
|
#command.define("fo.bar") |
|
90
|
|
|
|
|
91
|
|
|
lastfm = Event("__.lastfm__") |
|
92
|
|
|
lastfm.define(".lastfm") |
|
93
|
|
|
|
|
94
|
|
|
dance = Event("__.dance__") |
|
95
|
|
|
dance.define("\.dance") |
|
96
|
|
|
|
|
97
|
|
|
#unloads = Event("__module__") |
|
98
|
|
|
#unloads.define("^\.module") |
|
99
|
|
|
|
|
100
|
|
|
pimp = Event("__pimp__") |
|
101
|
|
|
pimp.define("\.pimp") |
|
102
|
|
|
|
|
103
|
|
|
bofh = Event("__.bofh__") |
|
104
|
|
|
bofh.define("\.bofh") |
|
105
|
|
|
|
|
106
|
|
|
#youtube = Event("__youtubes__") |
|
107
|
|
|
#youtube.define("youtube.com[\S]+") |
|
108
|
|
|
|
|
109
|
|
|
weather = Event("__.weather__") |
|
110
|
|
|
weather.define("\.weather") |
|
111
|
|
|
|
|
112
|
|
|
steam = Event("__.steam__") |
|
113
|
|
|
steam.define("\.steam") |
|
114
|
|
|
|
|
115
|
|
|
part = Event("__.part__") |
|
116
|
|
|
part.define("part") |
|
117
|
|
|
|
|
118
|
|
|
tell = Event("__privmsg__") |
|
119
|
|
|
tell.define("PRIVMSG") |
|
120
|
|
|
|
|
121
|
|
|
links = Event("__urls__") |
|
122
|
|
|
links.define("https?://*") |
|
123
|
|
|
|
|
124
|
|
|
# example |
|
125
|
|
|
# test = Event("__test__") |
|
126
|
|
|
# test.define(msg_definition="^\.test") |
|
127
|
|
|
|
|
128
|
|
|
# add your defined events here |
|
129
|
|
|
# tell your friends |
|
130
|
|
|
self.events_list.append(lastfm) |
|
131
|
|
|
self.events_list.append(dance) |
|
132
|
|
|
self.events_list.append(pimp) |
|
133
|
|
|
#self.events_list.append(youtube) |
|
134
|
|
|
self.events_list.append(bofh) |
|
135
|
|
|
self.events_list.append(weather) |
|
136
|
|
|
self.events_list.append(steam) |
|
137
|
|
|
self.events_list.append(part) |
|
138
|
|
|
self.events_list.append(tell) |
|
139
|
|
|
self.events_list.append(links) |
|
140
|
|
|
#self.events_list.append(unloads) |
|
141
|
|
|
# example |
|
142
|
|
|
# self.events_list.append(test) |
|
143
|
|
|
|
|
144
|
|
|
self.load_modules() |
|
145
|
|
|
self.logger.write(Logger.INFO, "bot initialized.", self.NICK) |
|
146
|
|
|
|
|
147
|
|
|
# conditionally subscribe to events list or add event to listing |
|
148
|
|
|
def register_event(self, event, module): |
|
149
|
|
|
""" |
|
150
|
|
|
Allows for dynamic, asynchronous event creation. To be used by modules, mostly, to define their own events in their initialization. |
|
151
|
|
|
Prevents multiple of the same _type of event being registered. |
|
152
|
|
|
|
|
153
|
|
|
Args: |
|
154
|
|
|
event: an event object to be registered with the bot |
|
155
|
|
|
module: calling module; ensures the calling module can be subscribed to the event if it is not already. |
|
156
|
|
|
|
|
157
|
|
|
Returns: |
|
158
|
|
|
nothing. |
|
159
|
|
|
""" |
|
160
|
|
|
for e in self.events_list: |
|
161
|
|
|
if e.definition == event.definition and e._type == event._type: |
|
162
|
|
|
# if our event is already in the listing, don't add it again, just have our module subscribe |
|
163
|
|
|
e.subscribe(module) |
|
164
|
|
|
return |
|
165
|
|
|
|
|
166
|
|
|
self.events_list.append(event) |
|
167
|
|
|
return |
|
168
|
|
|
|
|
169
|
|
|
def load_snippets(self): |
|
170
|
|
|
import imp |
|
171
|
|
|
snippets_path = self.modules_path + '/snippets' |
|
172
|
|
|
self.snippets_list = set() |
|
173
|
|
|
# load up snippets first |
|
174
|
|
|
for filename in os.listdir(snippets_path): |
|
175
|
|
|
name, ext = os.path.splitext(filename) |
|
176
|
|
|
try: |
|
177
|
|
|
if ext == ".py": |
|
178
|
|
|
# snippet is a module |
|
179
|
|
|
snippet = imp.load_source(name, snippets_path + '/' + filename) |
|
180
|
|
|
self.snippets_list.add(snippet) |
|
181
|
|
|
except Exception, e: |
|
182
|
|
|
print e |
|
183
|
|
|
print name, filename |
|
184
|
|
|
|
|
185
|
|
|
def set_snippets(self): |
|
186
|
|
|
""" |
|
187
|
|
|
check each snippet for a function with a list of commands in it |
|
188
|
|
|
create a big ol list of dictionaries, commands mapping to the functions to call if the command is encountered |
|
189
|
|
|
""" |
|
190
|
|
|
for obj in self.snippets_list: |
|
191
|
|
|
for k,v in inspect.getmembers(obj, inspect.isfunction): |
|
192
|
|
|
if inspect.isfunction(v) and hasattr(v, 'commands'): |
|
193
|
|
|
for c in v.commands: |
|
194
|
|
|
if not c in self.command_function_map: |
|
195
|
|
|
self.command_function_map[c] = dict() |
|
196
|
|
|
self.command_function_map[c] = v |
|
197
|
|
|
|
|
198
|
|
|
|
|
199
|
|
|
# utility function for loading modules; can be called by modules themselves |
|
200
|
|
|
def load_modules(self, specific=None): |
|
201
|
|
|
""" |
|
202
|
|
|
Run through the ${bot_dir}/modules directory, dynamically instantiating each module as it goes. |
|
203
|
|
|
|
|
204
|
|
|
Args: |
|
205
|
|
|
specific: string name of module. if it is specified, the function attempts to load the named module. |
|
206
|
|
|
|
|
207
|
|
|
Returns: |
|
208
|
|
|
1 if successful, 0 on failure. In keeping with the perverse reversal of UNIX programs and boolean values. |
|
209
|
|
|
""" |
|
210
|
|
|
nonspecific = False |
|
211
|
|
|
found = False |
|
212
|
|
|
|
|
213
|
|
|
self.loaded_modules = list() |
|
214
|
|
|
|
|
215
|
|
|
modules_dir_list = list() |
|
216
|
|
|
tmp_list = list() |
|
217
|
|
|
|
|
218
|
|
|
self.modules_path = 'modules' |
|
219
|
|
|
modules_path = 'modules' |
|
220
|
|
|
self.autoload_path = 'modules/autoloads' |
|
221
|
|
|
autoload_path = 'modules/autoloads' |
|
222
|
|
|
|
|
223
|
|
|
# this is magic. |
|
224
|
|
|
|
|
225
|
|
|
import os, imp, json |
|
226
|
|
|
|
|
227
|
|
|
|
|
228
|
|
|
self.load_snippets() |
|
229
|
|
|
self.set_snippets() |
|
230
|
|
|
|
|
231
|
|
|
dir_list = os.listdir(modules_path) |
|
232
|
|
|
mods = {} |
|
233
|
|
|
autoloads = {} |
|
234
|
|
|
# load autoloads if it exists |
|
235
|
|
|
if os.path.isfile(autoload_path): |
|
236
|
|
|
self.logger.write(Logger.INFO, "Found autoloads file", self.NICK) |
|
237
|
|
|
try: |
|
238
|
|
|
autoloads = json.load(open(autoload_path)) |
|
239
|
|
|
# logging |
|
240
|
|
|
for k in autoloads.keys(): |
|
241
|
|
|
self.logger.write(Logger.INFO, "Autoloads found for network " + k, self.NICK) |
|
242
|
|
|
except IOError: |
|
243
|
|
|
self.logger.write(Logger.ERROR, "Could not load autoloads file.",self.NICK) |
|
244
|
|
|
# create dictionary of things in the modules directory to load |
|
245
|
|
|
for fname in dir_list: |
|
246
|
|
|
name, ext = os.path.splitext(fname) |
|
247
|
|
|
if specific is None: |
|
248
|
|
|
nonspecific = True |
|
249
|
|
|
# ignore compiled python and __init__ files. |
|
250
|
|
|
# choose to either load all .py files or, available, just ones specified in autoloads |
|
251
|
|
|
if self.network not in autoloads.keys(): # if autoload does not specify for this network |
|
252
|
|
|
if ext == '.py' and not name == '__init__': |
|
253
|
|
|
f, filename, descr = imp.find_module(name, [modules_path]) |
|
254
|
|
|
mods[name] = imp.load_module(name, f, filename, descr) |
|
255
|
|
|
self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK) |
|
256
|
|
|
else: # follow autoload's direction |
|
257
|
|
|
if ext == '.py' and not name == '__init__': |
|
258
|
|
|
if name == 'module': |
|
259
|
|
|
f, filename, descr = imp.find_module(name, [modules_path]) |
|
260
|
|
|
mods[name] = imp.load_module(name, f, filename, descr) |
|
261
|
|
|
self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK) |
|
262
|
|
|
elif ('include' in autoloads[self.network] and name in autoloads[self.network]['include']) or ('exclude' in autoloads[self.network] and name not in autoloads[self.network]['exclude']): |
|
263
|
|
|
f, filename, descr = imp.find_module(name, [modules_path]) |
|
264
|
|
|
mods[name] = imp.load_module(name, f, filename, descr) |
|
265
|
|
|
self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK) |
|
266
|
|
|
else: |
|
267
|
|
|
if name == specific: # we're reloading only one module |
|
268
|
|
|
if ext != '.pyc': # ignore compiled |
|
269
|
|
|
f, filename, descr = imp.find_module(name, [modules_path]) |
|
270
|
|
|
mods[name] = imp.load_module(name, f, filename, descr) |
|
271
|
|
|
found = True |
|
272
|
|
|
|
|
273
|
|
|
for k,v in mods.iteritems(): |
|
274
|
|
|
for name in dir(v): |
|
275
|
|
|
obj = getattr(mods[k], name) # get the object from the namespace of 'mods' |
|
276
|
|
|
try: |
|
277
|
|
|
if inspect.isclass(obj): # it's a class definition, initialize it |
|
278
|
|
|
a = obj(self.events_list, self.send, self, self.say) # now we're passing in a reference to the calling bot |
|
279
|
|
|
if a not in self.loaded_modules: # don't add in multiple copies |
|
280
|
|
|
self.loaded_modules.append(a) |
|
281
|
|
|
except TypeError: |
|
282
|
|
|
pass |
|
283
|
|
|
|
|
284
|
|
|
if nonspecific is True or found is True: |
|
285
|
|
|
return 0 |
|
286
|
|
|
else: |
|
287
|
|
|
return 1 |
|
288
|
|
|
# end magic. |
|
289
|
|
|
|
|
290
|
|
|
def send(self, message): |
|
291
|
|
|
""" |
|
292
|
|
|
Simply sends the specified message to the socket. Which should be our connected server. |
|
293
|
|
|
We parse our own lines, as well, in case we want an event triggered by something we say. |
|
294
|
|
|
If debug is True, we also print out a pretty thing to console. |
|
295
|
|
|
|
|
296
|
|
|
Args: |
|
297
|
|
|
message: string, sent directly and without manipulation (besides UTF-8ing it) to the server. |
|
298
|
|
|
""" |
|
299
|
|
|
if self.OFFLINE: |
|
300
|
|
|
self.debug_print(util.bcolors.YELLOW + " >> " + util.bcolors.ENDC + self.getName() + ": " + message.encode('utf-8', 'ignore')) |
|
301
|
|
|
else: |
|
302
|
|
|
if self.DEBUG is True: |
|
303
|
|
|
self.logger.write(Logger.INFO, "DEBUGGING OUTPUT", self.NICK) |
|
304
|
|
|
self.logger.write(Logger.INFO, self.getName() + " " + message.encode('utf-8', 'ignore'), self.NICK) |
|
305
|
|
|
self.debug_print(util.bcolors.OKGREEN + ">> " + util.bcolors.ENDC + ": " + " " + message.encode('utf-8', 'ignore')) |
|
306
|
|
|
|
|
307
|
|
|
self.s.send(message.encode('utf-8', 'ignore')) |
|
308
|
|
|
target = message.split()[1] |
|
309
|
|
|
if target.startswith("#"): |
|
310
|
|
|
self.processline(':' + self.conf.getNick(self.network) + '!~' + self.conf.getNick(self.network) + '@fakehost.here ' + message.rstrip()) |
|
311
|
|
|
|
|
312
|
|
|
def pong(self, response): |
|
313
|
|
|
""" |
|
314
|
|
|
Keepalive heartbeat for IRC protocol. Until someone changes the IRC spec, don't modify this. |
|
315
|
|
|
""" |
|
316
|
|
|
self.send('PONG ' + response + '\n') |
|
317
|
|
|
|
|
318
|
|
|
def processline(self, line): |
|
319
|
|
|
""" |
|
320
|
|
|
Grab newline-delineated lines sent to us, and determine what to do with them. |
|
321
|
|
|
This function handles our initial low-level IRC stuff, as well; if we haven't joined, it waits for the MOTD message (or message indicating there isn't one) and then issues our own JOIN calls. |
|
322
|
|
|
|
|
323
|
|
|
Also immediately passes off PING messages to PONG. |
|
324
|
|
|
|
|
325
|
|
|
Args: |
|
326
|
|
|
line: string. |
|
327
|
|
|
|
|
328
|
|
|
""" |
|
329
|
|
|
if self.DEBUG: |
|
330
|
|
|
import datetime |
|
331
|
|
|
if os.name == "posix": # because windows doesn't like the color codes. |
|
332
|
|
|
self.debug_print(util.bcolors.OKBLUE + "<< " + util.bcolors.ENDC + line) |
|
333
|
|
|
else: |
|
334
|
|
|
self.debug_print("<< " + ": " + line) |
|
335
|
|
|
|
|
336
|
|
|
message_number = line.split()[1] |
|
337
|
|
|
|
|
338
|
|
|
try: |
|
339
|
|
|
first_word = line.split(":", 2)[2].split()[0] |
|
340
|
|
|
channel = line.split()[2] |
|
341
|
|
|
except IndexError: |
|
342
|
|
|
pass |
|
343
|
|
|
else: |
|
344
|
|
|
if first_word in self.command_function_map: |
|
345
|
|
|
self.command_function_map[first_word](self, line, channel) |
|
346
|
|
|
|
|
347
|
|
|
try: |
|
348
|
|
|
for e in self.events_list: |
|
349
|
|
|
if e.matches(line): |
|
350
|
|
|
e.notifySubscribers(line) |
|
351
|
|
|
# don't bother going any further if it's a PING/PONG request |
|
352
|
|
|
if line.startswith("PING"): |
|
353
|
|
|
ping_response_line = line.split(":", 1) |
|
354
|
|
|
self.pong(ping_response_line[1]) |
|
355
|
|
|
# pings we respond to directly. everything else... |
|
356
|
|
|
else: |
|
357
|
|
|
# patch contributed by github.com/thekanbo |
|
358
|
|
|
if self.JOINED is False and (message_number == "376" or message_number == "422"): |
|
359
|
|
|
# wait until we receive end of MOTD before joining, or until the server tells us the MOTD doesn't exist |
|
360
|
|
|
self.chan_list = self.conf.getChannels(self.network) |
|
361
|
|
|
for c in self.chan_list: |
|
362
|
|
|
self.send('JOIN '+c+' \n') |
|
363
|
|
|
self.JOINED = True |
|
364
|
|
|
|
|
365
|
|
|
line_array = line.split() |
|
366
|
|
|
user_and_mask = line_array[0][1:] |
|
367
|
|
|
usr = user_and_mask.split("!")[0] |
|
368
|
|
|
channel = line_array[2] |
|
369
|
|
|
try: |
|
370
|
|
|
message = line.split(":",2)[2] |
|
371
|
|
|
self.brain.respond(usr, channel, message) |
|
372
|
|
|
except IndexError: |
|
373
|
|
|
try: |
|
374
|
|
|
message = line.split(":",2)[1] |
|
375
|
|
|
self.brain.respond(usr, channel, message) |
|
376
|
|
|
except IndexError: |
|
377
|
|
|
print "index out of range.", line |
|
378
|
|
|
|
|
379
|
|
|
except Exception: |
|
380
|
|
|
print "Unexpected error:", sys.exc_info()[0] |
|
381
|
|
|
traceback.print_exc(file=sys.stdout) |
|
382
|
|
|
|
|
383
|
|
|
|
|
384
|
|
|
def worker(self, mock=False): |
|
385
|
|
|
""" |
|
386
|
|
|
Open the socket, make the first incision^H^H connection and get us on the server. |
|
387
|
|
|
Handles keeping the connection alive; if we disconnect from the server, attempts to reconnect. |
|
388
|
|
|
|
|
389
|
|
|
Args: |
|
390
|
|
|
mock: boolean. If mock is true, don't loop forever -- mock is for testing. |
|
391
|
|
|
""" |
|
392
|
|
|
self.HOST = self.network |
|
393
|
|
|
self.NICK = self.conf.getNick(self.network) |
|
394
|
|
|
|
|
395
|
|
|
# we have to cast it to an int, otherwise the connection fails silently and the entire process dies |
|
396
|
|
|
self.PORT = int(self.conf.getPort(self.network)) |
|
397
|
|
|
self.IDENT = 'mypy' |
|
398
|
|
|
self.REALNAME = 's1ash' |
|
399
|
|
|
self.OWNER = self.conf.getOwner(self.network) |
|
400
|
|
|
|
|
401
|
|
|
# connect to server |
|
402
|
|
|
self.s = socket.socket() |
|
403
|
|
|
while not self.CONNECTED: |
|
404
|
|
|
try: |
|
405
|
|
|
# low level socket TCP/IP connection |
|
406
|
|
|
self.s.connect((self.HOST, self.PORT)) # force them into one argument |
|
407
|
|
|
self.CONNECTED = True |
|
408
|
|
|
self.logger.write(Logger.INFO, "Connected to " + self.network, self.NICK) |
|
409
|
|
|
if self.DEBUG: |
|
410
|
|
|
self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + "connected to " + self.network) |
|
411
|
|
|
except: |
|
412
|
|
|
if self.DEBUG: |
|
413
|
|
|
self.debug_print(util.bcolors.FAIL + ">> " + util.bcolors.ENDC + "Could not connect to " + self.HOST + " at " + str(self.PORT) + "! Retrying... ") |
|
414
|
|
|
self.logger.write(Logger.CRITICAL, "Could not connect to " + self.HOST + " at " + str(self.PORT) + "! Retrying...") |
|
415
|
|
|
time.sleep(1) |
|
416
|
|
|
|
|
417
|
|
|
self.worker() |
|
418
|
|
|
|
|
419
|
|
|
time.sleep(1) |
|
420
|
|
|
# core IRC protocol stuff |
|
421
|
|
|
self.s.send('NICK '+self.NICK+'\n') |
|
422
|
|
|
|
|
423
|
|
|
if self.DEBUG: |
|
424
|
|
|
self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': NICK ' + self.NICK + '\\n') |
|
425
|
|
|
|
|
426
|
|
|
self.s.send('USER '+self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\n') # yeah, don't delete this line |
|
427
|
|
|
|
|
428
|
|
|
if self.DEBUG: |
|
429
|
|
|
self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ": USER " +self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\\n') |
|
430
|
|
|
|
|
431
|
|
|
time.sleep(3) # allow services to catch up |
|
432
|
|
|
|
|
433
|
|
|
self.s.send('PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\n') # we're registered! |
|
434
|
|
|
|
|
435
|
|
|
if self.DEBUG: |
|
436
|
|
|
self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\\n') |
|
437
|
|
|
|
|
438
|
|
|
self.s.setblocking(1) |
|
439
|
|
|
|
|
440
|
|
|
read = "" |
|
441
|
|
|
|
|
442
|
|
|
timeout = 0 |
|
443
|
|
|
|
|
444
|
|
|
# does not require a definition -- it will be invoked specifically when the bot notices it has been disconnected |
|
445
|
|
|
disconnect_event = Event("__.disconnection__") |
|
446
|
|
|
# if we're only running a test of connecting, and don't want to loop forever |
|
447
|
|
|
if mock: |
|
448
|
|
|
return |
|
449
|
|
|
# infinite loop to keep parsing lines |
|
450
|
|
|
while True: |
|
451
|
|
|
try: |
|
452
|
|
|
timeout += 1 |
|
453
|
|
|
# if we haven't received anything for 120 seconds |
|
454
|
|
|
time_since = self.conf.getTimeout(self.network) |
|
455
|
|
|
if timeout > time_since: |
|
456
|
|
|
if self.DEBUG: |
|
457
|
|
|
self.debug_print("Disconnected! Retrying... ") |
|
458
|
|
|
disconnect_event.notifySubscribers("null") |
|
459
|
|
|
self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK) |
|
460
|
|
|
# so that we rejoin all our channels upon reconnecting to the server |
|
461
|
|
|
self.JOINED = False |
|
462
|
|
|
self.CONNECTED = False |
|
463
|
|
|
|
|
464
|
|
|
global RETRY_COUNTER |
|
465
|
|
|
if RETRY_COUNTER > 10: |
|
466
|
|
|
self.debug_print("Failed to reconnect after 10 tries. Giving up...") |
|
467
|
|
|
sys.exit(1) |
|
468
|
|
|
|
|
469
|
|
|
RETRY_COUNTER+=1 |
|
470
|
|
|
self.worker() |
|
471
|
|
|
|
|
472
|
|
|
time.sleep(1) |
|
473
|
|
|
ready = select.select([self.s], [], [], 1) |
|
474
|
|
|
if ready[0]: |
|
475
|
|
|
try: |
|
476
|
|
|
read = read + self.s.recv(1024).decode('utf8', 'ignore') |
|
477
|
|
|
except UnicodeDecodeError, e: |
|
478
|
|
|
self.debug_print("Unicode decode error; " + e.__str__()) |
|
479
|
|
|
self.debug_print("Offending recv: " + self.s.recv) |
|
480
|
|
|
pass |
|
481
|
|
|
except Exception, e: |
|
482
|
|
|
print e |
|
483
|
|
|
if self.DEBUG: |
|
484
|
|
|
self.debug_print("Disconnected! Retrying... ") |
|
485
|
|
|
disconnect_event.notifySubscribers("null") |
|
486
|
|
|
self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK) |
|
487
|
|
|
self.JOINED = False |
|
488
|
|
|
self.CONNECTED = False |
|
489
|
|
|
self.worker() |
|
490
|
|
|
|
|
491
|
|
|
|
|
492
|
|
|
lines = read.split('\n') |
|
493
|
|
|
|
|
494
|
|
|
|
|
495
|
|
|
# Important: all lines from irc are terminated with '\n'. lines.pop() will get you any "to be continued" |
|
496
|
|
|
# line that couldn't fit in the socket buffer. It is stored and tacked on to the start of the next recv. |
|
497
|
|
|
read = lines.pop() |
|
498
|
|
|
|
|
499
|
|
|
if len(lines) > 0: |
|
500
|
|
|
timeout = 0 |
|
501
|
|
|
|
|
502
|
|
|
for line in lines: |
|
503
|
|
|
line = line.rstrip() |
|
504
|
|
|
self.processline(line) |
|
505
|
|
|
except KeyboardInterrupt: |
|
506
|
|
|
print "keyboard interrupt caught; exiting ..." |
|
507
|
|
|
raise |
|
508
|
|
|
# end worker |
|
509
|
|
|
|
|
510
|
|
|
def debug_print(self, line, error=False): |
|
511
|
|
|
""" |
|
512
|
|
|
Prepends incoming lines with the current timestamp and the thread's name, then spits it to stdout. |
|
513
|
|
|
Warning: this is entirely asynchronous between threads. If you connect to multiple networks, they will interrupt each other between lines. |
|
514
|
|
|
|
|
515
|
|
|
Args: |
|
516
|
|
|
line: text. |
|
517
|
|
|
error: boolean, defaults to False. if True, prints out with red >> in the debug line |
|
518
|
|
|
""" |
|
519
|
|
|
|
|
520
|
|
|
if not error: |
|
521
|
|
|
print str(datetime.datetime.now()) + ": " + self.getName() + ": " + line.strip('\n').rstrip().lstrip() |
|
522
|
|
|
else: |
|
523
|
|
|
print str(datetime.datetime.now()) + ": " + self.getName() + ": " + util.bcolors.RED + ">> " + util.bcolors.ENDC + line.strip('\n').rstrip().lstrip() |
|
524
|
|
|
|
|
525
|
|
|
|
|
526
|
|
|
def run(self): |
|
527
|
|
|
""" |
|
528
|
|
|
For implementing the parent threading.Thread class. Allows the thread the be initialized with our code. |
|
529
|
|
|
""" |
|
530
|
|
|
self.worker() |
|
531
|
|
|
|
|
532
|
|
|
def say(self, channel, thing): |
|
533
|
|
|
""" |
|
534
|
|
|
Speak, damn you! |
|
535
|
|
|
""" |
|
536
|
|
|
self.brain.say(channel, thing) |
|
537
|
|
|
# end class Bot |
|
538
|
|
|
|
|
539
|
|
|
## MAIN ## ACTUAL EXECUTION STARTS HERE |
|
540
|
|
|
|
|
541
|
|
|
if __name__ == "__main__": |
|
542
|
|
|
DEBUG = False |
|
543
|
|
|
import bot |
|
544
|
|
|
|
|
545
|
|
|
parser = argparse.ArgumentParser(description="a python irc bot that does stuff") |
|
546
|
|
|
parser.add_argument('config', nargs='?') |
|
547
|
|
|
parser.add_argument('-d', help='debug (foreground) mode', action='store_true') |
|
548
|
|
|
|
|
549
|
|
|
args = parser.parse_args() |
|
550
|
|
|
if args.d: |
|
551
|
|
|
DEBUG = True |
|
552
|
|
|
if args.config: |
|
553
|
|
|
config = args.config |
|
554
|
|
|
else: |
|
555
|
|
|
config = "~/.pybotrc" |
|
556
|
|
|
|
|
557
|
|
|
botslist = list() |
|
558
|
|
|
if not DEBUG and hasattr(os, 'fork'): |
|
559
|
|
|
pid = os.fork() |
|
560
|
|
|
if pid == 0: # child |
|
561
|
|
|
if os.name == "posix": |
|
562
|
|
|
print "starting bot in the background, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC |
|
563
|
|
|
else: |
|
564
|
|
|
print "starting bot in the background, pid " + str(os.getpid()) |
|
565
|
|
|
|
|
566
|
|
|
cm = confman.ConfManager(config) |
|
567
|
|
|
net_list = cm.getNetworks() |
|
568
|
|
|
for c in cm.getNetworks(): |
|
569
|
|
|
b = bot.Bot(cm, c, DEBUG) |
|
570
|
|
|
b.start() |
|
571
|
|
|
|
|
572
|
|
|
elif pid > 0: |
|
573
|
|
|
sys.exit(0) |
|
574
|
|
|
else: # don't background; either we're in debug (foreground) mode, or on windows TODO |
|
575
|
|
|
if os.name == 'nt': |
|
576
|
|
|
print 'in debug mode; backgrounding currently unsupported on windows.' |
|
577
|
|
|
DEBUG = True |
|
578
|
|
|
print "starting bot, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC |
|
579
|
|
|
try: |
|
580
|
|
|
f = open(os.path.expanduser(config)) |
|
581
|
|
|
except IOError: |
|
582
|
|
|
print "Could not open conf file " + config |
|
583
|
|
|
sys.exit(1) |
|
584
|
|
|
|
|
585
|
|
|
cm = confman.ConfManager(config) |
|
586
|
|
|
net_list = cm.getNetworks() |
|
587
|
|
|
for c in cm.getNetworks(): |
|
588
|
|
|
b = bot.Bot(cm, c, DEBUG) |
|
589
|
|
|
b.daemon = True |
|
590
|
|
|
b.start() |
|
591
|
|
|
botslist.append(b) |
|
592
|
|
|
try: |
|
593
|
|
|
while True: |
|
594
|
|
|
time.sleep(5) |
|
595
|
|
|
except (KeyboardInterrupt, SystemExit): |
|
596
|
|
|
l = Logger() |
|
597
|
|
|
l.write(Logger.INFO, "killed by ctrl+c or term signal") |
|
598
|
|
|
for b in botslist: |
|
599
|
|
|
b.s.send("QUIT :because I got killed\n") |
|
600
|
|
|
print |
|
601
|
|
|
print "keyboard interrupt caught; exiting" |
|
602
|
|
|
sys.exit(1) |
|
603
|
|
|
|
|
604
|
|
|
|
|
605
|
|
|
|