]> kaliko git repositories - sid.git/blobdiff - sid/sid.py
sphinx: Add sphinx docstring
[sid.git] / sid / sid.py
index dd768c17c1f929664020c19564d1347b124f0c06..fadb79e172ec77de117f9b2696f13d437f5fa6ee 100644 (file)
@@ -2,7 +2,7 @@
 
 # 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>
+# Copyright (C) 2014, 2015, 2020 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
@@ -21,11 +21,15 @@ import inspect
 import logging
 import traceback
 
-import sleekxmpp
+import slixmpp
 
 
 def botcmd(*args, **kwargs):
-    """Decorator for bot command functions"""
+    """Decorator for bot command functions
+
+    :param bool hidden: is the command hidden in global help
+    :param str name: command name, default to decorated function name
+    """
 
     def decorate(func, hidden=False, name=None):
         setattr(func, '_bot_command', True)
@@ -41,39 +45,51 @@ def botcmd(*args, **kwargs):
         return lambda func: decorate(func, **kwargs)
 
 
-class MUCBot(sleekxmpp.ClientXMPP):
+class MUCBot(slixmpp.ClientXMPP):
+    """
+    :param str jid: jid to log with
+    :param str password: jid password
+    :param str room: conference room to join
+    :param str nick: Nickname to use in the room
+    """
 
+    #: Class attribute to define bot's command prefix
+    #:
+    #: Defaults to "!"
     prefix = '!'
 
     def __init__(self, jid, password, room, nick, log_file=None,
-            log_level=logging.INFO):
+                 log_level=logging.INFO):
         super(MUCBot, self).__init__(jid, password)
 
-        self.log = logging.getLogger(__name__)
+        self.log = logging.getLogger(__package__)
         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
+        self.__seen = dict()
+        self.register_plugin('xep_0030')  # Service Discovery
+        self.register_plugin('xep_0045')  # Multi-User Chat
+        self.register_plugin('xep_0071')  # xhtml-im
+        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)
+        self.add_event_handler('session_start', self.start)
 
         # Handles MUC message and dispatch
-        self.add_event_handler("groupchat_message", self.muc_message)
+        self.add_event_handler('message', self.message)
+        self.add_event_handler('got_online', self._view)
 
         # 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.log.debug('Registered command: %s', name)
                 self.commands[name] = value
 
     def __set_logger(self, log_file=None, log_level=logging.INFO):
@@ -81,33 +97,49 @@ class MUCBot(sleekxmpp.ClientXMPP):
         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')
+            '%(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)
+        self.log.debug('set logger, log level : %s', log_level)
+
+    def message(self, msg):
+        """Messages handler
 
-    def muc_message(self, msg):
-        # ignore message from self
-        body = msg['body']
-        mucfrom = msg['mucnic']
+        Parses message received to detect :py:obj:`prefix`
+        """
+        if msg['type'] not in ('groupchat', 'chat'):
+            self.log.warning('Unhandled message')
+            return
         if msg['mucnick'] == self.nick:
             return
+        body = msg['body'].strip()
         if not body.startswith(MUCBot.prefix):
             return
+        if msg['from'] not in self.__seen:
+            self.log.warning('Will not handle message from unseen jid: %s', msg['from'])
+            #return
         args = body[1:].split()
         cmd = args.pop(0)
         if cmd not in self.commands:
             return
-        self.log.debug('cmd: {0}'.format(cmd))
+        self.log.debug('cmd: %s', cmd)
         if args:
-            self.log.debug('arg: {0}'.format(args))
+            self.log.debug('arg: %s', args)
         try:
-            reply = self.commands[cmd](msg, args)
+            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.send_message(mto=msg['from'].bare, mbody=reply, mtype='groupchat')
+            reply = ''.join(traceback.format_exc())
+            self.log.exception('An error occurred processing: %s: %s', body, reply)
+            if self.log.level < 10 and reply:
+                self.send_message(mto=msg['from'].bare, mbody=reply, mtype='groupchat')
+
+    def _view(self, pres):
+        """Track known nick"""
+        nick = pres['from']
+        status = (pres['type'], pres['status'])
+        self.__seen.update({nick: status})
 
     def start(self, event):
         """
@@ -117,38 +149,37 @@ class MUCBot(sleekxmpp.ClientXMPP):
         requesting the roster and broadcasting an initial
         presence stanza.
 
-        Arguments:
-            event -- An empty dictionary. The session_start
-                     event does not provide any additional
-                     data.
+        :param dict 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.plugin['xep_0045'].join_muc(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))
+        :param `sid.plugin.Plugin` plugin_cls: A :py:obj:`sid.plugin.Plugin` class
+        """
+        self.plugins.append(plugin_cls(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.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__)
+            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)
+            self.log.debug('shuting down %s', plugin)
             getattr(plugin, 'shutdown')()
 
     @botcmd
@@ -157,7 +188,8 @@ class MUCBot(sleekxmpp.ClientXMPP):
 
         Automatically assigned to the "help" command."""
         help_cmd = ('Type {}help <command name>'.format(self.prefix) +
-                    ' to get more info about that specific command.')
+                    ' to get more info about that specific command.\n\n'+
+                    'SRC: http://git.kaliko.me/sid.git')
         if not args:
             if self.__doc__:
                 description = self.__doc__.strip()
@@ -176,7 +208,12 @@ class MUCBot(sleekxmpp.ClientXMPP):
             text = '{}\n\n{}'.format(description, usage)
         else:
             if args[0] in self.commands.keys():
-                text = self.commands[args[0]].__doc__.strip() or 'undocumented'
+                text = self.commands[args[0]].__doc__ or 'undocumented'
+                text = inspect.cleandoc(text)
             else:
                 text = 'That command is not defined.'
-        return text
+        if message['type'] == 'groupchat':
+            to = message['from'].bare
+        else:
+            to = message['from']
+        self.send_message(mto=to, mbody=text, mtype=message['type'])