]> kaliko git repositories - sid.git/blob - sid/sid.py
Better use of logging
[sid.git] / sid / sid.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2007-2012 Thomas Perl <thp.io/about>
4 # Copyright (C) 2010, 2011 AnaĆ«l Verrier <elghinn@free.fr>
5 # Copyright (C) 2014-2015 kaliko <kaliko@azylum.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, version 3 only.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19
20 import inspect
21 import logging
22 import traceback
23
24 import sleekxmpp
25
26
27 def botcmd(*args, **kwargs):
28     """Decorator for bot command functions"""
29
30     def decorate(func, hidden=False, name=None):
31         setattr(func, '_bot_command', True)
32         setattr(func, '_bot_command_hidden', hidden)
33         setattr(func, '_bot_command_name', name or func.__name__)
34         if func.__doc__ is None:
35             func.__doc__ = ''
36         return func
37
38     if len(args):
39         return decorate(args[0], **kwargs)
40     else:
41         return lambda func: decorate(func, **kwargs)
42
43
44 class MUCBot(sleekxmpp.ClientXMPP):
45
46     prefix = '!'
47
48     def __init__(self, jid, password, room, nick, log_file=None,
49             log_level=logging.INFO):
50         super(MUCBot, self).__init__(jid, password)
51
52         self.log = logging.getLogger(__package__)
53         self.plugins = list()
54         self.commands = dict()
55         self.room = room
56         self.nick = nick
57         self.__set_logger(log_file, log_level)
58         self.__seen = dict()
59         self.register_plugin('xep_0030') # Service Discovery
60         self.register_plugin('xep_0045') # Multi-User Chat
61         self.register_plugin('xep_0199') # self Ping
62
63         # The session_start event will be triggered when
64         # the bot establishes its connection with the server
65         # and the XML streams are ready for use. We want to
66         # listen for this event so that we we can initialize
67         # our roster.
68         self.add_event_handler('session_start', self.start)
69
70         # Handles MUC message and dispatch
71         self.add_event_handler('message', self.message)
72         self.add_event_handler('got_online', self._view)
73
74         # Discover bot internal command (ie. help)
75         for name, value in inspect.getmembers(self):
76             if inspect.ismethod(value) and getattr(value, '_bot_command', False):
77                 name = getattr(value, '_bot_command_name')
78                 self.log.debug('Registered command: %s', name)
79                 self.commands[name] = value
80
81     def __set_logger(self, log_file=None, log_level=logging.INFO):
82         """Create console/file handler"""
83         log_fd = open(log_file, 'w') if log_file else None
84         chandler = logging.StreamHandler(log_fd)
85         formatter = logging.Formatter(
86             '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
87         chandler.setFormatter(formatter)
88         self.log.addHandler(chandler)
89         self.log.setLevel(log_level)
90         self.log.debug('set logger, log level : %s', log_level)
91
92     def message(self, msg):
93         if msg['type'] not in ('groupchat', 'chat'):
94             self.log.warning('Unhandled message')
95             return
96         if msg['mucnick'] == self.nick:
97             return
98         body = msg['body'].strip()
99         if not body.startswith(MUCBot.prefix):
100             return
101         if msg['from'] not in self.__seen:
102             self.log.warning('Will not handle message from unseen jid: %s', msg['from'])
103             #return
104         args = body[1:].split()
105         cmd = args.pop(0)
106         if cmd not in self.commands:
107             return
108         self.log.debug('cmd: %s', cmd)
109         if args:
110             self.log.debug('arg: %s', args)
111         try:
112             self.commands[cmd](msg, args)
113         except Exception as err:
114             reply = ''.join(traceback.format_exc())
115             self.log.exception('An error occurred processing: %s: %s', body, reply)
116             if self.log.level < 10 and reply:
117                 self.send_message(mto=msg['from'].bare, mbody=reply, mtype='groupchat')
118
119     def _view(self, pres):
120         nick = pres['from']
121         status = (pres['type'], pres['status'])
122         self.__seen.update({nick: status})
123
124     def start(self, event):
125         """
126         Process the session_start event.
127
128         Typical actions for the session_start event are
129         requesting the roster and broadcasting an initial
130         presence stanza.
131
132         Arguments:
133             event -- An empty dictionary. The session_start
134                      event does not provide any additional
135                      data.
136         """
137         self.get_roster()
138         self.send_presence()
139         self.plugin['xep_0045'].joinMUC(self.room,
140                                         self.nick,
141                                         # If a room password is needed, use:
142                                         # password=the_room_password,
143                                         wait=True)
144
145     def register_bot_plugin(self, plugin_class):
146         self.plugins.append(plugin_class(self))
147         for name, value in inspect.getmembers(self.plugins[-1]):
148             if inspect.ismethod(value) and getattr(value, '_bot_command',
149                                                    False):
150                 name = getattr(value, '_bot_command_name')
151                 self.log.debug('Registered command: %s', name)
152                 self.commands[name] = value
153
154     def foreach_plugin(self, method, *args, **kwds):
155         for plugin in self.plugins:
156             self.log.debug('shuting down %s', plugin.__str__)
157             getattr(plugin, method)(*args, **kwds)
158
159     def shutdown_plugins(self):
160         # TODO: why can't use event session_end|disconnected?
161         self.log.info('shuting down')
162         for plugin in self.plugins:
163             self.log.debug('shuting down %s', plugin)
164             getattr(plugin, 'shutdown')()
165
166     @botcmd
167     def help(self, message, args):
168         """Returns a help string listing available options.
169
170         Automatically assigned to the "help" command."""
171         help_cmd = ('Type {}help <command name>'.format(self.prefix) +
172                     ' to get more info about that specific command.')
173         if not args:
174             if self.__doc__:
175                 description = self.__doc__.strip()
176             else:
177                 description = 'Available commands:'
178
179             cmd_list = list()
180             for name, cmd in self.commands.items():
181                 if name == 'help' or cmd._bot_command_hidden:
182                     continue
183                 doc = (cmd.__doc__.strip() or 'undocumented').split('\n', 1)[0]
184                 cmd_list.append('{0}: {1}'.format(name, doc))
185
186             usage = '\n'.join(cmd_list)
187             usage = usage + '\n\n' + help_cmd
188             text = '{}\n\n{}'.format(description, usage)
189         else:
190             if args[0] in self.commands.keys():
191                 text = self.commands[args[0]].__doc__.strip() or 'undocumented'
192             else:
193                 text = 'That command is not defined.'
194         if message['type'] == 'groupchat':
195             to = message['from'].bare
196         else:
197             to = message['from']
198         self.send_message(mto=to, mbody=text, mtype=message['type'])