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
|
|
|
|