1
### This file is part of KoFooBot and is licensed under BSD-license according to the   ###
2
### LICENSE file in the base directory.                                                ###
3
### Code in this file is contributed by:                                               ###
4
###    Krister Svanlund <krister.svanlund gmail.com>                                   ###
5
6
import os, sys, getopt
7
import re
8
import irclib
9
import traceback
10
11
class KoFooGlobals:
12
    ### Bot version / module interface version. ###
13
    bot_version = "0.0.3/3"
14
15
    ### For all commands above this level the user has to be identified by nickserv. ###
16
    bot_id_req_level = 80
17
    
18
    bot_command_alias = { }   # Ex. {'tu': 'twitter update'} this will cause the command 'tu update this' to expand to 'twitter update update this'
19
    
20
    bot_commands = { "quit":
21
                         { 'description': "Disconnect and kill the bot",
22
                           'long help':   """\
23
Send a notice to all users with level > 100 and then disconnect and kill the bot. If no message specified the default quit message is used.""",
24
                           'arguments': "[<message>]",
25
                           'public': False,
26
                           'level': 100 },
27
                     "reconnect":
28
                         { 'description': "Disconnect and reconnect the bot.",
29
                           'long help':   """\
30
Send a notice to all users with level > 100 and then disconnect and reconnect the bot.""",
31
                           'arguments': "[<message>]",
32
                           'public': False,
33
                           'level': 100 },
34
                     "load":
35
                         { 'description': "Load a module with commands",
36
                           'long help': """\
37
Load (or reload, if already loaded) a module from the file kofoo_<modulename>.py. If auto is specified the module will be automatically loaded upon startup.""",
38
                           'arguments': "[auto] <modulename>",
39
                           'public': False,
40
                           'level': 100 },
41
                     "unload":
42
                         { 'description': "Unload a module currently loaded",
43
                           'long help': """\
44
Unload a module and remove automatically update functions.""",
45
                           'arguments': "<modulename>",
46
                           'public': False,
47
                           'level': 100 },
48
                     "save":
49
                         { 'description': "Save settings",
50
                           'long help': """\
51
Save all settings to disk.""",
52
                           'public': False,
53
                           'level': 100 },
54
                     "join":
55
                         { 'description': "Join a channel",
56
                           'long help': """\
57
Join a channel and try to identify with nickserv if password is given.""",
58
                           'public': False,
59
                           'level': 100},
60
                     "part":
61
                         { 'description': "Part from a channel",
62
                           'long help': """\
63
Part (leave) a channel.""",
64
                           'public': False,
65
                           'level': 100},
66
                     }
67
    
68
69
class KoFooSettings:
70
    _unsaved_settings = ["_unsaved_settings",
71
                         "load_settings",
72
                         "save_settings",
73
                         "comments"]
74
    bot_irc_server    = None   # string
75
    bot_irc_port      = 0      # int
76
    bot_nick_name     = None   # string
77
    bot_nick_password = None   # string
78
    bot_nick_email    = None   # string
79
    bot_password      = None   # string
80
                                
81
    bot_quit_message  = ""     # string
82
                                
83
    bot_channel_list  = { }    # dict { "channel": "chanserv_password" } or list ["channel"]
84
                                
85
    bot_user_list     = { }    # dict { "nickname": access_level<int> }
86
87
    bot_auto_load_modules = [] # list of string
88
89
    bot_public_prefix = '^'    # string
90
91
    comments = ""
92
93
    def save_settings(self, config_file):
94
        """Save attributes in settings object to the specified config file."""
95
        print "Save settings to '%s'." % config_file
96
        f = open(config_file, 'w')
97
        ### Write all comments at the top. ###
98
        f.write(self.comments)
99
        for key in dir(self):
100
            config_string = ""
101
            ### Check if the value is not supposed to be saved.                       ###
102
            ### Attributes in _unsaved_settings and that starts with _ are not saved. ###
103
            if key not in self._unsaved_settings and not key[0] == '_':
104
                value = getattr(self, key)
105
                ### If the value is a string add ''. ###
106
                if type(value) == str:
107
                    config_string = "%s = \"%s\"" % (key, value)
108
                else:
109
                    config_string = "%s = %s" % (key, str(value))
110
                print " <", config_string
111
            if config_string:
112
                f.write(config_string+"\n")
113
        f.close()
114
    
115
    def load_settings(self, config_file):
116
        """Load settings from specified config file into settings object."""
117
        print "Load settings from '%s'." % config_file
118
        try:
119
            config = open(config_file)
120
        except Exception, e:
121
            print " + Could not load '%s'." % config_file
122
            return False
123
        ### Read the config file and split the lines for parsing. ###
124
        config_lines = config.read().splitlines()
125
        loadable_config = ""
126
        self.comments = ""
127
        for line in config_lines:
128
            ### Store a version of the string that has no whitespace in beginning or end. ###
129
            stripped_line = line.strip()
130
            ### Split line at # and only use the first part and ignore the commented part. ###
131
            stripped_line = stripped_line.split(' #', 1)[0]
132
            if not stripped_line:
133
                continue
134
            ### If the string start with # just add it to self.comments so that it can ###
135
            ### be written back to the config file later on.                           ###
136
            if stripped_line[0] == "#":
137
                self.comments += line + "\n"
138
                continue
139
            ### if the line is indented it belongs to the previous line. ###
140
            m = re.match(r'\s+(\S.*)', line)
141
            if m:
142
                loadable_config += m.group(1)
143
            else:
144
                loadable_config += "\n" + stripped_line
145
        ### Iterate through the prepeared list and eval each value and assign the result ###
146
        ### to the correct attribute of the settings object.                             ###
147
        for line in loadable_config.splitlines():
148
            ### Split the line into variable and value. ###
149
            m = re.match(r'(\S+)\s*=\s*(\S.*)', line)
150
            if m:
151
                variable = m.group(1)
152
                value = m.group(2)
153
                try:
154
                    setattr(self, variable, eval(value))
155
                    print " > %s = %s" % (variable, value)
156
                except Exception, e:
157
                    print " - Could not load string '%s'. Try to load with right-side as string." % line
158
                    try:
159
                        setattr(self, variable, eval("'%s'" % value))
160
                    except Exception, e:
161
                        print "   - Could not load with right-side as string."
162
        print " + Finished loading settings."
163
164
class KoFoo(irclib.IRC):
165
    ### A dictionary of nickname and function to do when a user is recognised by nickserv. ###
166
    # on_user_id_do = { "UserNickname": (callback, arg_tuple) }
167
    on_user_id_do = {}
168
    
169
    ### All on_*_do lists should be a list of callable functions. ###
170
    # on_event_do    = [ func1, func2, func3 ]
171
    on_update_do     = [] ### A list of functions to do on update iterations.           ###
172
    on_pubmsg_do     = [] ### A list of functions to do on public messages in channels. ###
173
    on_privmsg_do    = [] ### A list of functions to do on private messages to bot.     ###
174
    on_privnotice_do = [] ### A list of functions to do on private notice to bot.       ###
175
    on_pubnotice_do  = [] ### A list of functions to do on public notice to channel.    ###
176
    on_join_part_do  = [] ### A list of functions to do on join/part event.             ###
177
    on_ctcp_do       = [] ### A list of functions to do on recieved ctcp event.         ###
178
179
180
    ### Currently joined channels. ###
181
    channel_list = []
182
183
    ### List of currently loaded modules. ###
184
    module_list = []
185
    ### List of modules not loaded. ###
186
    found_modules = []
187
    
188
    def __init__(self):
189
        """Initiate the IRC client, load the settings and check if all required settings
190
           are set. Also add handlers for IRC events."""
191
        irclib.IRC.__init__(self, config_file)
192
        self.want_to_quit = False
193
        self.reconnect_timeout = 0
194
195
        try:
196
            cwd = os.getcwd()
197
            file_list = os.listdir(cwd)
198
            for item in file_list:
199
                print item
200
                res = re.match("^kofoo_([\S]+).py$", item)
201
                if res:
202
                    module_name = res.group(1)
203
                    self.found_modules.append(module_name)
204
        except Exception, e:
205
            print " - Failed to get file list:", str(e)
206
207
        self.config_file = config_file
208
209
        self.settings = KoFooSettings()
210
        self.settings.load_settings(self.config_file)
211
212
        ### Specify a list of error messages for each reqired non-None setting. ###
213
        neccessary_settings = [("No irc server set", self.settings.bot_irc_server), 
214
                               ("No irc port set", self.settings.bot_irc_port), 
215
                               ("No nickname set for bot", self.settings.bot_nick_name), 
216
                               ("No password for bot nickname set", self.settings.bot_nick_password),
217
                               ("No password for bot set", self.settings.bot_password)]
218
        for desc, opt in neccessary_settings:
219
            if not opt:
220
                print "A neccessary setting is not set."
221
                raise ValueError(desc)
222
223
        self.globals = KoFooGlobals()
224
225
        self.add_global_handler('welcome', self.on_connected)
226
        self.add_global_handler('disconnect', self.on_disconnected)
227
        self.add_global_handler('privmsg', self.on_privmsg)
228
        self.add_global_handler('privnotice', self.on_privnotice)
229
        self.add_global_handler('pubmsg', self.on_pubmsg)
230
        self.add_global_handler('pubnotice', self.on_pubnotice)
231
        self.add_global_handler('join', self.on_join)
232
        self.add_global_handler('part', self.on_part)
233
        self.add_global_handler('kick', self.on_part)
234
        self.add_global_handler('action', self.on_action)
235
        self.add_global_handler('ctcp', self.on_ctcp)
236
237
    def update(self, server, time_since_last):
238
        """Iterate through all the update funcs and run them if their timeout limit
239
           has been reached. Otherwise just adjust their timeout based on time_since_last.
240
           """
241
        for update_func in self.on_update_do:
242
            ### If the update_func somehow has changed into something not callable since ###
243
            ### it was added, remove it from the list and continue with the next one.    ###
244
            if not callable(update_func):
245
                self.on_update_do.remove(update_func)
246
                print " - Remove update function. Not callable."
247
                continue
248
            ### If the update function has a delay variable then check if there is time  ###
249
            ### left otherwise just execute the function and set the time_left to delay. ###
250
            if hasattr(update_func, "delay"):
251
                delay = getattr(update_func, "delay")
252
                if hasattr(update_func, "time_left"):
253
                    time_left = getattr(update_func, "time_left")
254
                    time_left = time_left - time_since_last
255
                    if time_left <= 0:
256
                        try:
257
                            update_func(self, server)
258
                        except Exception, e:
259
                            print " - Failed to run update function: %s" % str(e)
260
                        time_left = delay
261
                    setattr(update_func, "time_left", time_left)
262
                else:
263
                    try:
264
                        update_func(self, server)
265
                    except Exception, e:
266
                        print " - Failed to run update function: %s" % str(e)
267
                    setattr(update_func, "time_left", delay)
268
            else:
269
                try:
270
                    update_func(self, server)
271
                except Exception, e:
272
                    print " - Failed to run update function: %s" % str(e)
273
274
275
    def on_connected(self, server, event):
276
        """On established connection to the IRC server get the real nickname (it could
277
           have been changed on connection if the configured one is occupied."""
278
        print "Connected to %s" % server.get_server_name()
279
        self.settings.bot_nick_name = server.get_nickname()
280
        print " + Using nickname '%s'." % self.settings.bot_nick_name
281
        self.join_channels(server)
282
283
        ### Load all auto-loaded modules when connection is established. ###
284
        if self.settings.bot_auto_load_modules:
285
            for module in self.settings.bot_auto_load_modules:
286
                self.load_module(server, module, None)
287
        else:
288
            self.settings.bot_auto_load_modules = []
289
290
    def on_disconnected(self, server, event):
291
        """On disconnect."""
292
        print " !! Disconnected."
293
        self.reconnect_timeout = 60.0
294
295
    def on_privnotice(self, server, event):
296
        """On a private notice, often used by nickserv, chanserv and other bots."""
297
        sender = event.source()
298
        target = event.target()
299
        if sender:
300
            sender_nick = irclib.nm_to_n(sender).lower()
301
        else:
302
            sender_nick = ""
303
        arg = event.arguments()[0]
304
        if sender_nick == "nickserv":
305
            res = re.match("STATUS (\S*) (\d)", arg)
306
            if res is not None:
307
                user = res.group(1)
308
                status = int(res.group(2), 0)
309
                if status > 1:
310
                    print " + User %s has identified with nickserv." % user
311
                    self.user_confirmed(user)
312
                else:
313
                    print " + User %s is not identified with nickserv." %user
314
                    server.notice(user, "Command failed because you are not identified with nickserv.")
315
                    if user in self.on_user_id_do:
316
                        del self.on_user_id_do[user]
317
                return
318
        if sender_nick and self.on_privnotice_do:
319
            for func in self.on_privnotice_do:
320
                if callable(func):
321
                    func(self, server, sender, arg)
322
                else:
323
                    ### If the function somehow has become a non-callable type. Remove it. ###
324
                    del self.on_privnotice_do[func]
325
                    print " - Delete private notice handler."
326
327
    def on_pubnotice(self, server, event):
328
        """On public notice, often from another bot in the channel."""
329
        sender = event.source()
330
        target = event.target()
331
        sender_nick = irclib.nm_to_n(sender).lower()
332
        arg = event.arguments()[0]
333
        if self.on_pubnotice_do:
334
            for func in self.on_pubnotice_do:
335
                if callable(func):
336
                    func(self, server, sender, target, arg)
337
                else:
338
                    ### If the function somehow has become a non-callable type. Remove it. ###
339
                    del self.on_pubnotice_do[func]
340
                    print " - Delete public notice handler."
341
342
    def on_privmsg(self, server, event):
343
        """On a private message, often used by chat users by /query or /msg."""
344
        sender = event.source()
345
        target = event.target()
346
        arg = event.arguments()[0]
347
        if not self.handle_command(server, sender, target, arg, public=False):
348
            #print " + Private message from '%s': %s" % (irclib.nm_to_n(sender), arg)
349
            if self.on_privmsg_do:
350
                for func in self.on_privmsg_do:
351
                    if callable(func):
352
                        func(self, server, sender, arg)
353
                    else:
354
                        ### If the function somehow has become a non-callable type. Remove it. ###
355
                        del self.on_privmsg_do[func]
356
                        print " - Delete private notice handler."
357
358
    def on_pubmsg(self, server, event):
359
        """On a message in a channel handle commands or send to modules."""
360
        sender = event.source()
361
        target = event.target()
362
        arg = event.arguments()[0]
363
        command_handled = False
364
        if not target in self.channel_list:
365
            print " - Got message from unknown channel %s." % target
366
        else:
367
            ### Check if the message starts with bot_public_prefix which is used as a ###
368
            ### public message prefix. If it is, handle it as a command issued in     ###
369
            ### public.                                                               ###
370
            res = re.match('\%s(.*)' % self.settings.bot_public_prefix, arg)
371
            ### Also check if the string is directed to the bot, this could also be a ###
372
            ### command. Check for bot_nick_name followed by :, ; or whitespace.      ###
373
            res2 = re.match('%s[:;,\s]+(.*)' % self.settings.bot_nick_name, arg)
374
            msg = None
375
            if res is not None:
376
                msg = res.group(1)
377
            elif res2 is not None:
378
                msg = res2.group(1)
379
            ### If the command is handled then just return otherwise handle it as a   ###
380
            ### public string.                                                        ###
381
            if msg:
382
                if self.handle_command(server, sender, target, msg, public=True):
383
                    command_handled = True
384
            if not command_handled and self.on_pubmsg_do:
385
                for func in self.on_pubmsg_do:
386
                    if callable(func):
387
                        func(self, server, sender, target, arg, action = False)
388
                    else:
389
                        ### If the function somehow has become a non-callable type. Remove it. ###
390
                        del self.on_pubmsg_do[func]
391
                        print " - Delete public message handler."
392
393
    def on_action(self, server, event):
394
        """On /me or similar, both public and privat."""
395
        sender = event.source()
396
        target = event.target()
397
        arg = event.arguments()[0]
398
        if target in self.channel_list:
399
            self.on_pubaction(server, sender, target, arg)
400
        else:
401
            self.on_privaction(server, sender, target, arg)
402
403
    def on_pubaction(self, server, sender, target, arg):
404
        for func in self.on_pubmsg_do:
405
            if callable(func):
406
                func(self, server, sender, target, arg, True)
407
            else:
408
                ### If the function somehow has become a non-callable type. Remove it. ###
409
                del self.on_pubmsg_do[func]
410
                print " - Delete public message handler."
411
412
    def on_privaction(self, server, sender, target, arg):
413
        for func in self.on_privmsg_do:
414
            if callable(func):
415
                func(self, server, sender, arg, True)
416
            else:
417
                ### If the function somehow has become a non-callable type. Remove it. ###
418
                del self.on_privmsg_do[func]
419
                print " - Delete public message handler."
420
421
    def on_ctcp(self, server, event):
422
        """On ctcp from other clients."""
423
        sender = event.source()
424
        target = event.target()
425
        arg = event.arguments()[0]
426
        for func in self.on_ctcp_do:
427
            if callable(func):
428
                func(self, server, sender, arg)
429
            else:
430
                ### If the function somehow has become a non-callable type. Remove it. ###
431
                del self.on_ctcp_do[func]
432
                print " - Delete public message handler."
433
                
434
    def user_confirmed(self, user):
435
        """On returned confirmation from nickserv that a user is identified execute the
436
           call the user requested."""
437
        print " + %s has been confirmed by nickserv." % user
438
        if user in self.on_user_id_do:
439
            callback, args = self.on_user_id_do[user]
440
            if callable(callback):
441
                try:
442
                    callback(*args)
443
                except TypeError, e:
444
                    print " - Callback is not a native function:", str(e)
445
                    try:
446
                        callback(self, *args)
447
                    except Exception, e2:
448
                        print " - Failed!", str(e2)
449
            else:
450
                print " - Callback for %s is not callable." % user
451
        else:
452
            print " + Nothing to call for user %s." % user
453
454
    def join_channels(self, server):
455
        """Join channels in bot_channel_list in settings. If a password is specified
456
           try to claim foundersrights for it otherwise just join and let OPs take care
457
           of it."""
458
        if type(self.settings.bot_channel_list) is dict:
459
            self.settings.bot_channel_list = self.settings.bot_channel_list.keys()
460
        if type(self.settings.bot_channel_list) is list:
461
            for channel in self.settings.bot_channel_list:
462
                server.join(channel)
463
        else:
464
            print " - Channel list is not a list."
465
466
    def on_join(self, server, event):
467
        channel = event.target()
468
        self.channel_list.append(channel)
469
        if self.on_join_part_do:
470
            for func in self.on_join_part_do:
471
                if callable(func):
472
                    func(self, server, channel, join=True)
473
                else:
474
                    ### If the function somehow has become a non-callable type. Remove it. ###
475
                    del self.on_join_part_do[func]
476
                    print " - Delete join/part handler."
477
478
    def on_part(self, server, event):
479
        channel = event.target()
480
        if len(event.arguments()) > 1:
481
            reason = event.arguments()[1]
482
        else:
483
            reason = ""
484
        sender = irclib.nm_to_n(event.source())
485
        if reason or sender is self.settings.bot_nick_name:
486
            print " + Got kicked from %s by %s with reason '%s'." % (channel, sender, reason)
487
        self.channel_list.remove(channel)
488
        if self.on_join_part_do:
489
            for func in self.on_join_part_do:
490
                if callable(func):
491
                    func(self, server, channel, join=False)
492
                else:
493
                    ### If the function somehow has become a non-callable type. Remove it. ###
494
                    del self.on_join_part_do[func]
495
                    print " - Delete join/part handler."
496
497
    def respond(self, server, sender, target, arg):
498
        """If the command was issued in private send respons in private otherwise respond
499
           in target channel."""
500
        send_to = irclib.nm_to_n(sender)
501
        prefix = ""
502
        if target != self.settings.bot_nick_name:
503
            prefix = "%s: " % send_to
504
            send_to = target
505
        for line in arg.splitlines():
506
            server.privmsg(send_to, prefix+line)
507
508
    def tell_users(self, server, message, level=100, notice=False):
509
        """Tell all users with a certain level. Optionally a notice to all user of that
510
           level or higher."""
511
        if self.settings.bot_user_list:
512
            for user, user_level in self.settings.bot_user_list.iteritems():
513
                if user_level >= level:
514
                    for line in message.splitlines():
515
                        if notice:
516
                            server.notice(user, line)
517
                        else:
518
                            server.privmsg(user, line)
519
        else:
520
            self.settings.bot_user_list = { }
521
522
    def get_level(self, user):
523
        """Get level of a user or the default 0."""
524
        user_nick = irclib.nm_to_n(user)
525
        user_level = self.settings.bot_user_list.get(user_nick, 0)
526
        print " + %s has level %d." % (user_nick, user_level)
527
        return user_level
528
529
    def add_id_action(self, sender, func, args):
530
        """Add a function to be called on status respons from nickserv."""
531
        sender_nick = irclib.nm_to_n(sender)
532
        self.on_user_id_do[sender_nick] = (func, args)
533
        server.privmsg("nickserv", "STATUS %s" % sender_nick)
534
    
535
    def handle_command(self, server, sender, target, arg, public=True):
536
        """Handle commands either in public or private."""
537
        try:
538
            ### If \xc3 is found in the string the string is most likely encoded     ###
539
            ### UTF-8 otherwise assume it's Latin-1. This is not a really good idea  ###
540
            ### the bot is supposed to be used by channel using different languages  ###
541
            ### than english or swedish (maybe other european languages.             ###
542
            if '\xc3' in arg:
543
                print " + Convert utf-8 to unicode."
544
                arg = unicode(arg, "UTF-8")
545
            else:
546
                print " + Convert latin-1 to unicode."
547
                arg = unicode(arg, "Latin-1")
548
        except:
549
            print " - Failed to recode string."
550
        if not arg.strip():
551
            print " - No arguments."
552
            return True
553
        arg_list = arg.split()
554
        command_name = arg_list[0]
555
        arg_list = arg_list[1:]
556
        sender_level = self.get_level(sender)
557
558
        ### Strip away everything but nick from sender. ###
559
        sender = irclib.nm_to_n(sender)
560
561
        ### Check if the command is in the global bot alias list. ###
562
        if command_name in self.globals.bot_command_alias:
563
            ### If it is, then expand the alias and insert it into the list. ###
564
            alias_expand = self.globals.bot_command_alias[command_name].split()
565
            if alias_expand:
566
                print " + Expanded '%s' to '%s'." % (command_name, ' '.join(alias_expand))
567
                command_name = alias_expand[0]
568
                if len(alias_expand) > 1:
569
                    arg_list = alias_expand[1:] + arg_list
570
571
        ### Check if the command is in bot_commands. If it is try to execute it if the ###
572
        ### user has the rights to do so.                                              ###
573
        if command_name in self.globals.bot_commands:
574
            print "Command '%s' has been issued by %s." % (command_name, irclib.nm_to_n(sender))
575
            command = self.globals.bot_commands[command_name]
576
            if public and not command['public']:
577
                self.respond(server, sender, target, "You can't issue this command here.")
578
                return False
579
            if sender_level >= command['level']:
580
                try:
581
                    print " + Get command function '%s'." % ('do_'+command_name)
582
                    func = getattr(self, 'do_'+command_name)
583
                    if command['level'] >= self.globals.bot_id_req_level:
584
                        self.add_id_action(sender, func, (server, sender, target, arg_list))
585
                        return True
586
                    else:
587
                        try:
588
                            func(server, sender, target, arg_list)
589
                            return True
590
                        except TypeError, e:
591
                            print " - Callback is not a native function."
592
                            try:
593
                                func(self, server, sender, target, arg_list)
594
                                return True
595
                            except Exception, e2:
596
                                print " - Failed!", str(e2)
597
                                return False
598
                except AttributeError:
599
                    self.respond(server, sender, target, "This command is not yet implemented.")
600
                    print " - Has no method called do_%s." % command_name
601
                    return False
602
            else:
603
                self.respond(server, sender, target, "You are not allowed to issue this command.")
604
                return False
605
        else:
606
            return False
607
608
    def reload_module(self, server, module, sender = None):
609
        """Reload a loaded module."""
610
        real_module_name = 'kofoo_'+module
611
        if sender:
612
            server.privmsg(sender, "Reloading the module '%s'." % module)
613
        else:
614
            print "Reloading the module '%s'." % module
615
        try:
616
            self.unload_module(server, module, sender)
617
        except Exception, e:
618
            print " - Failed to unload module '%s': %s" % (module, str(e))
619
            return
620
        try:
621
            self.load_module(server, module, False, sender)
622
        except Exception, e:
623
            print " - Failed to load module '%s': %s" % (module, str(e))
624
            return
625
626
    def unload_module(self, server, module, sender = None):
627
        """Unload a module in memory and clean up all definitions."""
628
        real_module_name = 'kofoo_'+module
629
        print "Unload module '%s'." % real_module_name
630
        if real_module_name in sys.modules:
631
            module_object = sys.modules[real_module_name]
632
            module_attributes = dir(module_object)
633
            update_module = getattr(module_object, "update_"+module, None)
634
            module_alias = getattr(module_object, module+"_alias", None)
635
            module_pubmsg_handler = getattr(module_object, module+"_public_msg_handler", None)
636
            module_privmsg_handler = getattr(module_object, module+"_private_msg_handler", None)
637
            module_pubnotice_handler = getattr(module_object, module+"_public_notice_handler", None)
638
            module_privnotice_handler = getattr(module_object, module+"_private_notice_handler", None)
639
            module_join_part_handler = getattr(module_object, module+"_join_part_handler", None)
640
            module_ctcp_handler = getattr(module_object, module+"_ctcp_handler", None)
641
            module_helpers = getattr(module_object, module+"_helpers", None)
642
            unload_module = getattr(module_object, "unload_"+module, None)
643
            module_actions = {}
644
            if unload_module:
645
                if not unload_module(self, server, sender):
646
                    print " - Failed to unload module."
647
                    if sender:
648
                        server.privmsg(sender, "Failed to unload module '%s'." % module)
649
                        raise Exception("Unload module returns False.")
650
            for module_attribute in module_attributes:
651
                if module_attribute[:3] == "do_":
652
                    action_name = module_attribute[3:]
653
                    if action_name in self.globals.bot_commands:
654
                        try:
655
                            del self.globals.bot_commands[action_name]
656
                            print " + Removed command description for '%s'." % action_name
657
                        except Exception, e:
658
                            print " - Failed to remove command description for '%s'." % action_name
659
                            continue
660
                    try:
661
                        if hasattr(self, module_attribute):
662
                            delattr(self, module_attribute)
663
                    except Exception, e:
664
                        print " - Failed to unload '%s'." % module_attribute
665
                        continue
666
            if update_module:
667
                if update_module in self.on_update_do:
668
                    try:
669
                        self.on_update_do.remove(update_module)
670
                        print " + Removed update function."
671
                    except Exception, e:
672
                        print " - Failed to remove update function."
673
            if module_alias:
674
                for alias in module_alias:
675
                    try:
676
                        del self.globals.bot_command_alias[alias]
677
                        print " + Removed alias '%s'." % alias
678
                    except Exception, e:
679
                        print " - Failed to remove alias '%s'." % alias
680
            if module_pubmsg_handler:
681
                try:
682
                    self.on_pubmsg_do.remove(module_pubmsg_handler)
683
                    print " + Removed public message handler."
684
                except Exception, e:
685
                    print " - Failed to remove public message handler: %s" % str(e)
686
            if module_privmsg_handler:
687
                try:
688
                    self.on_privmsg_do.remove(module_privmsg_handler)
689
                    print " + Removed private message handler."
690
                except Exception, e:
691
                    print " - Failed to remove private message handler: %s" % str(e)
692
            if module_pubnotice_handler:
693
                try:
694
                    self.on_pubnotice_do.remove(module_pubnotice_handler)
695
                    print " + Removed public notice handler."
696
                except Exception, e:
697
                    print " - Failed to remove public notice handler: %s" % str(e)
698
            if module_privnotice_handler:
699
                try:
700
                    self.on_privnotice_do.remove(module_privnotice_handler)
701
                    print " + Removed private notice handler."
702
                except Exception, e:
703
                    print " - Failed to remove private notice handler: %s" % str(e)
704
            if module_join_part_handler:
705
                try:
706
                    self.on_join_part_do.remove(module_join_part_handler)
707
                    print " + Removed join/part handler."
708
                except Exception, e:
709
                    print " - Failed to remove join/part handler: %s" % str(e)
710
            if module_ctcp_handler:
711
                try:
712
                    self.on_ctcp_do.remove(module_ctcp_handler)
713
                    print " + Removed ctcp handler."
714
                except Exception, e:
715
                    print " - Failed to remove ctcp handler: %s" % str(e)
716
            if module_helpers:
717
                for helper in module_helpers:
718
                    try:
719
                        delattr(self, helper)
720
                        print " + Removed helper '%s'." % helper
721
                    except Exception, e:
722
                        print " - Failed to remove helper '%s': %s" % (helper, str(e))
723
            del module_pubmsg_handler
724
            del module_privmsg_handler
725
            del module_pubnotice_handler
726
            del module_privnotice_handler
727
            del module_join_part_handler
728
            del module_ctcp_handler
729
            del module_object
730
            del update_module
731
            del module_alias
732
            del module_helpers
733
            del sys.modules[real_module_name]
734
            self.module_list.remove(module)
735
        else:
736
            print " - No such module loaded."
737
            if sender:
738
                server.privmsg(sender, "The '%s' module is not loaded." % module)
739
                
740
    def load_module(self, server, module, auto = False, sender = None):
741
        """Try to load a module."""
742
        real_module_name = 'kofoo_'+module
743
        print "Load module '%s'." % real_module_name
744
        if real_module_name in sys.modules:
745
            self.reload_module(server, module, sender)
746
            return
747
        try:
748
            module_object = __import__(real_module_name, globals(), locals())
749
        except Exception, e:
750
            tb = traceback.format_exc()
751
            print " - Failed to load the module '%s' because %s.\n%s" % (module, str(e), tb)
752
            if sender:
753
                server.privmsg(sender, "Failed to load module '%s'.\n%s" % (module, tb))
754
            return
755
        module_requires = getattr(module_object, module+"_requires", None)
756
        if module_requires:
757
            missing_modules = []
758
            for requirement in module_requires:
759
                if not requirement in self.module_list:
760
                    missing_modules.append(requirement)
761
            if missing_modules:
762
                fail_string = "Failed to load module, required module%s not loaded: %s" % (('', 's')[missing_modules==[]], ', '.join(missing_modules))
763
                print " -", fail_string
764
                if sender:
765
                    server.privmsg(sender, fail_string)
766
                try:
767
                    del module_requires
768
                    del sys.modules[real_module_name]
769
                    del module_object
770
                    return
771
                except Exception, e:
772
                    print " - Could not unload module '%s'." % module
773
                    return
774
            else:
775
                print " + All requirments met."
776
        init_module = getattr(module_object, "init_"+module, None)
777
        update_module = getattr(module_object, "update_"+module, None)
778
        module_settings = getattr(module_object, module+"_settings", None)
779
        module_alias = getattr(module_object, module+"_alias", None)
780
        module_pubmsg_handler = getattr(module_object, module+"_public_msg_handler", None)
781
        module_privmsg_handler = getattr(module_object, module+"_private_msg_handler", None)
782
        module_pubnotice_handler = getattr(module_object, module+"_public_notice_handler", None)
783
        module_privnotice_handler = getattr(module_object, module+"_private_notice_handler", None)
784
        module_join_part_handler = getattr(module_object, module+"_join_part_handler", None)
785
        module_ctcp_handler = getattr(module_object, module+"_ctcp_handler", None)
786
        module_helpers = getattr(module_object, module+"_helpers", None)
787
        module_actions = {}
788
        module_attributes = dir(module_object)
789
        for module_attribute in module_attributes:
790
            if module_attribute[:3] == "do_":
791
                module_attribute_object = getattr(module_object, module_attribute)
792
                if callable(module_attribute_object):
793
                    module_action_description = getattr(module_attribute_object, 'command', None)
794
                    if not module_action_description:
795
                        module_action_description = getattr(module_object, module_attribute+'_command', None)
796
                    if module_action_description:
797
                        if type(module_action_description) is dict:
798
                            print " + Added action '%s'." % module_attribute[3:]
799
                            module_actions[module_attribute[3:]] = module_action_description
800
                        else:
801
                            print " - '%s' is of wrong type." % module_attribute
802
                    else:
803
                        print " + '%s' has no action description." % module_attribute
804
                else:
805
                    print " - '%s' is not callable." % module_attribute
806
        if module_settings:
807
            print " + Add settings."
808
            if type(module_settings) is dict:
809
                for setting, default in module_settings.iteritems():
810
                    setting_name = module+"_"+setting
811
                    if hasattr(self.settings, setting_name):
812
                        continue
813
                    else:
814
                        setattr(self.settings, setting_name, default)
815
                        print " + Added setting '%s' with default value '%s'." % (setting_name, str(default))
816
            elif type(module_settings) is list:
817
                for setting in module_settings:
818
                    setting_name = module+"_"+setting
819
                    setattr(self.settings, setting_name, None)
820
                    print " + Added setting '%s'." % setting_name
821
            else:
822
                print " - Module settings are of wrong type."
823
        if init_module:
824
            if init_module(self, server, sender):
825
                print " + Successfully initiated module '%s'." % module
826
                if sender:
827
                    server.privmsg(sender, "Successfully loaded module '%s'." % module)
828
            else:
829
                print " - Failed to initiate module: '%s'." % module
830
                if sender:
831
                    server.privmsg(sender, "Failed to initiate module '%s'." % module)
832
                try:
833
                    del module_object
834
                    del sys.modules[real_module_name]
835
                    print " - Module '%s' unloaded." % module
836
                    return
837
                except Exception, e:
838
                    print " - Failed to unload unfinished module."
839
                    return
840
        if update_module:
841
            print " + Adding update func for module '%s'." % module
842
            if not hasattr(update_module, 'delay'):
843
                delay = getattr(module_object, 'update_%s_delay' % module, None)
844
                setattr(update_module, 'delay', delay)
845
            if not hasattr(update_module, 'time_left'):
846
                time_left = getattr(module_object, 'update_%s_time_left' % module, None)
847
                setattr(update_module, 'time_left', time_left)
848
            self.on_update_do.append(update_module)
849
        if module_actions:
850
            for command_name, description in module_actions.iteritems():
851
                print " + Activate action '%s'." % command_name
852
                command_func_name = "do_"+command_name
853
                command_func = getattr(module_object, command_func_name, None)
854
                if command_func:
855
                    setattr(self, command_func_name, command_func)
856
                    if type(description) is tuple:
857
                        description = description[0]
858
                    self.globals.bot_commands[command_name] = description
859
                else:
860
                    print " - Failed to find function '%s'." % command_func_name
861
        if module_pubmsg_handler:
862
            if callable(module_pubmsg_handler):
863
                print " + Added public message handler."
864
                self.on_pubmsg_do.append(module_pubmsg_handler)
865
            else:
866
                print " - Failed to add public message handler. Not callable."
867
        if module_privmsg_handler:
868
            if callable(module_privmsg_handler):
869
                print " + Added private message handler."
870
                self.on_privmsg_do.append(module_privmsg_handler)
871
            else:
872
                print " - Failed to add private message handler. Not callable."
873
        if module_pubnotice_handler:
874
            if callable(module_pubnotice_handler):
875
                print " + Added public notice handler."
876
                self.on_pubnotice_do.append(module_pubnotice_handler)
877
            else:
878
                print " - Failed to add public notice handler. Not callable."
879
        if module_privnotice_handler:
880
            if callable(module_privnotice_handler):
881
                print " + Added private notice handler."
882
                self.on_privnotice_do.append(module_privnotice_handler)
883
            else:
884
                print " - Failed to add private notice handler. Not callable."
885
        if module_join_part_handler:
886
            if callable(module_join_part_handler):
887
                print " + Added join/part handler."
888
                self.on_join_part_do.append(module_join_part_handler)
889
            else:
890
                print " - Failed to add join/part handler. Not callable."
891
        if module_ctcp_handler:
892
            if callable(module_ctcp_handler):
893
                print " + Added ctcp handler."
894
                self.on_ctcp_do.append(module_ctcp_handler)
895
            else:
896
                print " - Failed to add ctcp handler. Not callable."
897
        if module_alias:
898
            try:
899
                self.globals.bot_command_alias.update(module_alias)
900
            except Exception, e:
901
                if sender:
902
                    server.privmsg(sender, "Failed to add alias from '%s'." % module)
903
        if module_helpers:
904
            for helper in module_helpers:
905
                try:
906
                    if hasattr(self, helper):
907
                        print " - Bot already has the attribute '%s'." % helper
908
                    else:
909
                        helper_object = getattr(module_object, helper)
910
                        if helper_object:
911
                            setattr(self, helper, helper_object)
912
                            print " + Added helper '%s' to bot." % helper
913
                        else:
914
                            print " - Module has no such helper '%s'." % helper
915
                except Exception, e:
916
                    print " - Failed to add helper '%s': %s" % (helper, str(e))
917
        if auto:
918
            if not module in self.settings.bot_auto_load_modules:
919
                self.settings.bot_auto_load_modules.append(module)
920
                print " + Added module to autoload."
921
        self.module_list.append(module)
922
923
#
924
# This is where command declaration begins!
925
#
926
927
    def do_quit(self, server, sender, target, args):
928
        """ [Native Command]"""
929
        print "Unload all modules."
930
        for module in self.module_list:
931
            self.unload_module(server, module, sender)
932
        self.want_to_quit = True
933
        alt_quit_msg = ' '.join(args)
934
        self.tell_users(server, "'Quit' issued by %s." % irclib.nm_to_n(sender), level=100, notice=True)
935
        if alt_quit_msg:
936
            self.disconnect_all("Quit '%s'." % alt_quit_msg)
937
        else:
938
            self.disconnect_all(self.settings.bot_quit_message)
939
        print "Disconnect and kill."
940
941
    def do_reconnect(self, server, sender, target, args):
942
        """ [Native Command]"""
943
        self.want_to_quit = False
944
        self.reconnect_timeout = 10.0
945
        self.tell_users(server, "'Reconnect' issued by %s." % irclib.nm_to_n(sender), level=100, notice=True)
946
        self.disconnect_all("Reconnecting '%s'." % ' '.join(args))
947
        print "Disconnect and reconnect."
948
949
    def do_load(self, server, sender, target, args):
950
        """ [Native Command]"""
951
        auto_load = False
952
        if args and args[0] == "auto":
953
            auto_load = True
954
            args = args[1:]
955
        for module in args:
956
            self.load_module(server, module, auto_load, irclib.nm_to_n(sender))
957
958
    def do_unload(self, server, sender, target, args):
959
        """ [Native Command]"""
960
        for module in args:
961
            self.unload_module(server, module, irclib.nm_to_n(sender))
962
            self.respond(server, sender, target, "Unloaded '%s'." % module)
963
964
    def do_save(self, server, sender, target, args):
965
        """ [Native Command]"""
966
        self.settings.save_settings(self.config_file)
967
        self.respond(server, sender, target, "Saved current configuration.")
968
969
    def do_join(self, server, sender, target, args):
970
        """ [Native Command]"""
971
        if args:
972
            for channel in args:
973
                server.join(channel)
974
975
    def do_part(self, server, sender, target, args):
976
        """ [Native Command]"""
977
        if args:
978
            server.send_raw("PART %s" % ", ".join(args))
979
980
#
981
# This is where command declaration ends!
982
#
983
984
def usage():
985
    print """\
986
KoFooBot usage:
987
  python kofoo.py [--config=<config-file>] [--help]
988
989
  Short  Long      Argument    Description
990
   -c    --config  <filename>  Choose what file to load configuration from.
991
   -h    --help                Show this help."""
992
993
if __name__ == '__main__':
994
    config_file = "config.cfg"
995
    if sys.argv:
996
        try:
997
            opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['help', 'config='])
998
        except getopt.GetoptError, e:
999
            print str(e)
1000
            usage()
1001
            sys.exit(2)
1002
        for opt, arg in opts:
1003
            if opt in ('--help', '-h'):
1004
                usage()
1005
                sys.exit(0)
1006
            elif opt in ('--config', '-c'):
1007
                config_file = arg
1008
        
1009
    irc = KoFoo()
1010
    reconnected = 1
1011
    while not irc.want_to_quit:
1012
        import time
1013
        server = irc.server()
1014
        server.connect(irc.settings.bot_irc_server, irc.settings.bot_irc_port, irc.settings.bot_nick_name)
1015
        if not server.is_connected():
1016
            reconnected += 1
1017
        try:
1018
            now = time.time()
1019
            last = now
1020
            last_update = time.time()
1021
            while server.is_connected():
1022
                if (time.time() - last_update) > 10.0:
1023
                    last_update = time.time()
1024
                now = time.time()
1025
                irc.update(server, now-last)
1026
                irc.process_once(0.2)
1027
                last = now
1028
        except KeyboardInterrupt:
1029
            irc.want_to_quit = True
1030
            irc.disconnect_all("Interrupted")
1031
            print "Keyboard interruption captured."
1032
        if irc.reconnect_timeout > 0 and not irc.want_to_quit:
1033
            print "Wait for reconnection '%.2f'." % (irc.reconnect_timeout*reconnected)
1034
            time.sleep(irc.reconnect_timeout*reconnected)
1035
            reconnected -= 1
1036
            if reconnected < 0:
1037
                reconnected = 0
1038
    irc.settings.save_settings(irc.config_file)
1039
    print "Connection closed."
1040