+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2007-2012 Thomas Perl <thp.io/about>
+# Copyright (C) 2010, 2011 Anaël Verrier <elghinn@free.fr>
+# Copyright (C) 2014 kaliko <kaliko@azylum.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 only.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import inspect
+import logging
+import traceback
+
+import sleekxmpp
+
+
+def botcmd(*args, **kwargs):
+ """Decorator for bot command functions"""
+
+ def decorate(func, hidden=False, name=None):
+ setattr(func, '_bot_command', True)
+ setattr(func, '_bot_command_hidden', hidden)
+ setattr(func, '_bot_command_name', name or func.__name__)
+ if func.__doc__ is None:
+ func.__doc__ = ''
+ return func
+
+ if len(args):
+ return decorate(args[0], **kwargs)
+ else:
+ return lambda func: decorate(func, **kwargs)
+
+
+class MUCBot(sleekxmpp.ClientXMPP):
+
+ prefix = '!'
+
+ def __init__(self, jid, password, room, nick, log_file=None,
+ log_level=logging.INFO):
+ super(MUCBot, self).__init__(jid, password)
+
+ self.log = logging.getLogger(__name__)
+ self.plugins = list()
+ self.commands = dict()
+ self.room = room
+ self.nick = nick
+ self.__set_logger(log_file, log_level)
+ self.register_plugin('xep_0030') # Service Discovery
+ self.register_plugin('xep_0045') # Multi-User Chat
+ self.register_plugin('xep_0199') # self Ping
+
+ # The session_start event will be triggered when
+ # the bot establishes its connection with the server
+ # and the XML streams are ready for use. We want to
+ # listen for this event so that we we can initialize
+ # our roster.
+ self.add_event_handler("session_start", self.start)
+
+ # Handles MUC message and dispatch
+ self.add_event_handler("groupchat_message", self.muc_message)
+
+ # Discover bot internal command (ie. help)
+ for name, value in inspect.getmembers(self):
+ if inspect.ismethod(value) and getattr(value, '_bot_command', False):
+ name = getattr(value, '_bot_command_name')
+ self.log.debug('Registered command: %s' % name)
+ self.commands[name] = value
+
+ def __set_logger(self, log_file=None, log_level=logging.INFO):
+ """Create console/file handler"""
+ log_fd = open(log_file, 'w') if log_file else None
+ chandler = logging.StreamHandler(log_fd)
+ formatter = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ chandler.setFormatter(formatter)
+ self.log.addHandler(chandler)
+ self.log.setLevel(log_level)
+ self.log.debug('set logger, log level : %s' % log_level)
+
+ def muc_message(self, msg):
+ # ignore message from self
+ body = msg['body']
+ mucfrom = msg['mucnic']
+ if msg['mucnick'] == self.nick:
+ self.log.debug('ignoring message from me')
+ return
+ if not body.startswith(MUCBot.prefix):
+ return
+ args = body[1:].split()
+ cmd= args.pop(0)
+ if cmd not in self.commands:
+ return
+ self.log.debug('cmd: {0}'.format(cmd))
+ if args:
+ self.log.debug('arg: {0}'.format(args))
+ try:
+ reply = self.commands[cmd](msg, args)
+ except Exception as err:
+ reply = traceback.format_exc(err)
+ self.log.exception('An error occurred processing: {0}: {1}'.format(body, reply))
+ self.log.debug(reply)
+ self.send_message(mto=msg['from'].bare, mbody=reply, mtype='groupchat')
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an initial
+ presence stanza.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.get_roster()
+ self.send_presence()
+ self.plugin['xep_0045'].joinMUC(self.room,
+ self.nick,
+ # If a room password is needed, use:
+ # password=the_room_password,
+ wait=True)
+
+ def register_bot_plugin(self, plugin_class):
+ self.plugins.append(plugin_class(self))
+ for name, value in inspect.getmembers(self.plugins[-1]):
+ if inspect.ismethod(value) and getattr(value, '_bot_command',
+ False):
+ name = getattr(value, '_bot_command_name')
+ self.log.debug('Registered command: %s' % name)
+ self.commands[name] = value
+
+ def foreach_plugin(self, method, *args, **kwds):
+ for plugin in self.plugins:
+ self.log.debug('shuting down %s' % plugin.__str__)
+ getattr(plugin, method)(*args, **kwds)
+
+ def shutdown_plugins(self):
+ # TODO: why can't use event session_end|disconnected?
+ self.log.info('shuting down')
+ for plugin in self.plugins:
+ self.log.debug('shuting down %s' % plugin)
+ getattr(plugin, 'shutdown')()
+
+ @botcmd
+ def help(self, message, args):
+ """Returns a help string listing available options.
+
+ Automatically assigned to the "help" command."""
+ self.log.info(args)
+ help_cmd = ('Type {}help <command name>'.format(self.prefix) +
+ ' to get more info about that specific command.')
+ if not args:
+ if self.__doc__:
+ description = self.__doc__.strip()
+ else:
+ description = 'Available commands:'
+
+ cmd_list = list()
+ for name, cmd in self.commands.items():
+ if name == 'help' or cmd._bot_command_hidden:
+ continue
+ doc = (cmd.__doc__.strip() or 'undocumented').split('\n', 1)[0]
+ cmd_list.append('{0}: {1}'.format(name, doc))
+
+ usage = '\n'.join(cmd_list)
+ usage = usage + '\n\n' + help_cmd
+ text = '{}\n\n{}'.format(description, usage)
+ else:
+ self.log.debug(args[0])
+ self.log.debug(args[0] in self.commands.keys())
+ if args[0] in self.commands.keys():
+ text = self.commands[args[0]].__doc__.strip() or 'undocumented'
+ else:
+ text = 'That command is not defined.'
+ return text