1 # -*- coding: utf-8 -*-
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 kaliko <kaliko@azylum.org>
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.
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.
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/>.
27 def botcmd(*args, **kwargs):
28 """Decorator for bot command functions"""
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:
39 return decorate(args[0], **kwargs)
41 return lambda func: decorate(func, **kwargs)
44 class MUCBot(sleekxmpp.ClientXMPP):
48 def __init__(self, jid, password, room, nick, log_file=None,
49 log_level=logging.INFO):
50 super(MUCBot, self).__init__(jid, password)
52 self.log = logging.getLogger(__name__)
54 self.commands = dict()
57 self.__set_logger(log_file, log_level)
58 self.register_plugin('xep_0030') # Service Discovery
59 self.register_plugin('xep_0045') # Multi-User Chat
60 self.register_plugin('xep_0199') # self Ping
62 # The session_start event will be triggered when
63 # the bot establishes its connection with the server
64 # and the XML streams are ready for use. We want to
65 # listen for this event so that we we can initialize
67 self.add_event_handler("session_start", self.start)
69 # Handles MUC message and dispatch
70 self.add_event_handler("groupchat_message", self.muc_message)
72 # Discover bot internal command (ie. help)
73 for name, value in inspect.getmembers(self):
74 if inspect.ismethod(value) and getattr(value, '_bot_command', False):
75 name = getattr(value, '_bot_command_name')
76 self.log.debug('Registered command: %s' % name)
77 self.commands[name] = value
79 def __set_logger(self, log_file=None, log_level=logging.INFO):
80 """Create console/file handler"""
81 log_fd = open(log_file, 'w') if log_file else None
82 chandler = logging.StreamHandler(log_fd)
83 formatter = logging.Formatter(
84 '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
85 chandler.setFormatter(formatter)
86 self.log.addHandler(chandler)
87 self.log.setLevel(log_level)
88 self.log.debug('set logger, log level : %s' % log_level)
90 def muc_message(self, msg):
91 # ignore message from self
92 body = msg['body'].strip()
93 mucfrom = msg['mucnic']
94 if msg['mucnick'] == self.nick:
96 if not body.startswith(MUCBot.prefix):
98 args = body[1:].split()
100 if cmd not in self.commands:
102 self.log.debug('cmd: {0}'.format(cmd))
104 self.log.debug('arg: {0}'.format(args))
106 self.commands[cmd](msg, args)
107 except Exception as err:
108 reply = ''.join(traceback.format_exc())
109 self.log.exception('An error occurred processing: {0}: {1}'.format(body, reply))
110 if self.log.level < 10 and reply:
111 self.send_message(mto=msg['from'].bare, mbody=reply, mtype='groupchat')
113 def start(self, event):
115 Process the session_start event.
117 Typical actions for the session_start event are
118 requesting the roster and broadcasting an initial
122 event -- An empty dictionary. The session_start
123 event does not provide any additional
128 self.plugin['xep_0045'].joinMUC(self.room,
130 # If a room password is needed, use:
131 # password=the_room_password,
134 def register_bot_plugin(self, plugin_class):
135 self.plugins.append(plugin_class(self))
136 for name, value in inspect.getmembers(self.plugins[-1]):
137 if inspect.ismethod(value) and getattr(value, '_bot_command',
139 name = getattr(value, '_bot_command_name')
140 self.log.debug('Registered command: %s' % name)
141 self.commands[name] = value
143 def foreach_plugin(self, method, *args, **kwds):
144 for plugin in self.plugins:
145 self.log.debug('shuting down %s' % plugin.__str__)
146 getattr(plugin, method)(*args, **kwds)
148 def shutdown_plugins(self):
149 # TODO: why can't use event session_end|disconnected?
150 self.log.info('shuting down')
151 for plugin in self.plugins:
152 self.log.debug('shuting down %s' % plugin)
153 getattr(plugin, 'shutdown')()
156 def help(self, message, args):
157 """Returns a help string listing available options.
159 Automatically assigned to the "help" command."""
160 help_cmd = ('Type {}help <command name>'.format(self.prefix) +
161 ' to get more info about that specific command.')
164 description = self.__doc__.strip()
166 description = 'Available commands:'
169 for name, cmd in self.commands.items():
170 if name == 'help' or cmd._bot_command_hidden:
172 doc = (cmd.__doc__.strip() or 'undocumented').split('\n', 1)[0]
173 cmd_list.append('{0}: {1}'.format(name, doc))
175 usage = '\n'.join(cmd_list)
176 usage = usage + '\n\n' + help_cmd
177 text = '{}\n\n{}'.format(description, usage)
179 if args[0] in self.commands.keys():
180 text = self.commands[args[0]].__doc__.strip() or 'undocumented'
182 text = 'That command is not defined.'
183 self.send_message(mto=message['from'].bare, mbody=text, mtype='groupchat')