# -*- 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, 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
-# 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/>.
+# SPDX-FileCopyrightText: 2007-2012 Thomas Perl <thp.io/about>
+# SPDX-FileCopyrightText: 2010, 2011 Anaël Verrier <elghinn@free.fr>
+# SPDX-FileCopyrightText: 2014, 2015, 2020, 2023 kaliko <kaliko@azylum.org>
+# SPDX-License-Identifier: GPL-3.0-or-later
import inspect
import slixmpp
+from sid import __url__, __doc__
+
+log = logging.getLogger(__package__)
+
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)
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,
+ def __init__(self, jid, password, room, nick,
log_level=logging.INFO):
super(MUCBot, self).__init__(jid, password)
- self.log = logging.getLogger(__package__)
+ # Clean sphinx autodoc for self documentation
+ # (cf. MUCBot.help)
+ self.__doc__ = None
+ self.log = log
self.plugins = list()
self.commands = dict()
self.room = room
self.nick = nick
- self.__set_logger(log_file, log_level)
+ self.__set_logger(log_level)
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
+ 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
self.add_event_handler('message', self.message)
self.add_event_handler('got_online', self._view)
+ # Handles disconnection
+ self.add_event_handler('disconnected', self.disconn)
+
# Discover bot internal command (ie. help)
for name, value in inspect.getmembers(self):
- if inspect.ismethod(value) and getattr(value, '_bot_command', False):
+ if inspect.ismethod(value) and \
+ getattr(value, '_bot_command', False):
name = getattr(value, '_bot_command_name')
- self.log.debug('Registered command: %s', name)
+ 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 __set_logger(self, log_level):
+ """Set logging level"""
+ log.setLevel(log_level)
+ log.debug('set logger, log level : %s', log_level)
+
+ def disconn(self, event):
+ """disconnected handler"""
+ msg = ": %s" % event if event else "‽"
+ log.info('Disconnected from server%s', msg)
+ self.connect()
def message(self, msg):
- """Messages handler"""
+ """Messages handler
+
+ Parses message received to detect :py:obj:`prefix`
+ """
if msg['type'] not in ('groupchat', 'chat'):
- self.log.warning('Unhandled message')
+ 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
+ log.debug(msg['from'])
+ if msg['from'] not in self.__seen and msg['type'] == 'chat':
+ log.warning('Will not handle direct 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: %s', cmd)
+ log.debug('cmd: %s', cmd)
if args:
- self.log.debug('arg: %s', args)
+ log.debug('arg: %s', args)
try:
self.commands[cmd](msg, args)
except Exception as err:
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')
+ log.exception('An error occurred processing: %s: %s', body, reply)
+ if log.level < 10 and reply:
+ self.send_message(mto=msg['from'].bare, mbody=reply,
+ mtype='groupchat')
def _view(self, pres):
"""Track known nick"""
status = (pres['type'], pres['status'])
self.__seen.update({nick: status})
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
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()
+ await self.get_roster()
self.send_presence()
- 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))
+ await self.plugin['xep_0045'].join_muc_wait(self.room,
+ self.nick,
+ # Do not fetch history
+ maxstanzas=0,
+ # If a room password is needed, use:
+ # password=the_room_password,
+ )
+
+ def register_bot_plugin(self, plugin_cls):
+ """Registers plugin, takes a class, the method instanciates the plugin
+
+ :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):
+ if inspect.ismethod(value) and \
+ getattr(value, '_bot_command', False):
+ name = getattr(value, '_bot_command_name')
+ log.debug('Registered command: %s', name)
+ self.commands[name] = value
+
+ def unregister_bot_plugin(self, plugin):
+ for name, value in inspect.getmembers(plugin):
+ if inspect.ismethod(value) and \
+ getattr(value, '_bot_command', False):
name = getattr(value, '_bot_command_name')
- self.log.debug('Registered command: %s', name)
+ log.debug('Unegistered command: %s', name)
self.commands[name] = value
+ self.plugins.remove(plugin)
def foreach_plugin(self, method, *args, **kwds):
for plugin in self.plugins:
- self.log.debug('shuting down %s', plugin.__str__)
+ log.debug('calling %s for %s', method, plugin)
getattr(plugin, method)(*args, **kwds)
def shutdown_plugins(self):
- # TODO: why can't use event session_end|disconnected?
- self.log.info('shuting down')
+ # TODO: also use event session_end|disconnected?
+ log.info('shuting down')
for plugin in self.plugins:
- self.log.debug('shuting down %s', plugin)
+ log.debug('shuting down %s', plugin)
getattr(plugin, 'shutdown')()
@botcmd
Automatically assigned to the "help" command."""
help_cmd = ('Type {}help <command name>'.format(self.prefix) +
- ' to get more info about that specific command.\n\n'+
- 'SRC: http://git.kaliko.me/sid.git')
+ ' to get more info about that specific command.\n\n' +
+ f'DOC: {__doc__}\n' +
+ f'SRC: {__url__}')
if not args:
if self.__doc__:
description = self.__doc__.strip()