--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from sid.sid import MUCBot
+from sid.echo import Echo
+from sid.feeds import Feeds
+
+JID = 'bot@example.org'
+NICK = 'sid'
+PASS = '53cr34'
+ROOM = 'room@conf.example.org'
+
+def main():
+ Feeds.TEMPO = 60
+ xmpp = MUCBot(JID, PASS, ROOM, NICK)
+ xmpp.register_bot_plugin(Feeds)
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ if xmpp.connect():
+ # If you do not have the dnspython library installed, you will need
+ # to manually specify the name of the server if it does not match
+ # the one in the JID. For example, to use Google Talk you would
+ # need to use:
+ #
+ # if xmpp.connect(('talk.google.com', 5222)):
+ # ...
+ xmpp.process(block=True)
+ xmpp.shutdown_plugins()
+ xmpp.log.info('Done')
+ else:
+ xmpp.log.info('Unable to connect.')
+
+# Script starts here
+if __name__ == '__main__':
+ main()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2007-2012 Thomas Perl <thp.io/about>
+# 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/>.
+
+from .plugin import Plugin, botcmd
+
+class Echo(Plugin):
+
+ def __init__(self, bot):
+ Plugin.__init__(self, bot)
+ bot.add_event_handler("groupchat_message", self.muc_message)
+ # The groupchat_presence event is triggered whenever a
+ # presence stanza is received from any chat room, including
+ # any presences you send yourself. To limit event handling
+ # to a single room, use the events muc::room@server::presence,
+ # muc::room@server::got_online, or muc::room@server::got_offline.
+ #bot.add_event_handler("muc::%s::got_online" % self.bot.room, self.muc_online)
+
+ def muc_message(self, msg):
+ """
+ Process incoming message stanzas from any chat room. Be aware
+ that if you also have any handlers for the 'message' event,
+ message stanzas may be processed by both handlers, so check
+ the 'type' attribute when using a 'message' event handler.
+
+ Whenever the bot's nickname is mentioned, respond to
+ the message.
+
+ IMPORTANT: Always check that a message is not from yourself,
+ otherwise you will create an infinite loop responding
+ to your own messages.
+
+ This handler will reply to messages that mention
+ the bot's nickname.
+
+ Arguments:
+ msg -- The received message stanza. See the documentation
+ for stanza objects and the Message stanza to see
+ how it may be used.
+ """
+ if msg['mucnick'] != self.bot.nick and self.bot.nick in msg['body']:
+ self.bot.send_message(mto=msg['from'].bare,
+ mbody="I heard that, %s." % msg['mucnick'],
+ mtype='groupchat')
+
+ def muc_online(self, presence):
+ """
+ Process a presence stanza from a chat room. In this case,
+ presences from users that have just come online are
+ handled by sending a welcome message that includes
+ the user's nickname and role in the room.
+
+ Arguments:
+ presence -- The received presence stanza. See the
+ documentation for the Presence stanza
+ to see how else it may be used.
+ """
+ if presence['muc']['nick'] != self.bot.nick:
+ self.bot.send_message(mto=presence['from'].bare,
+ mbody="Hello, %s %s" % (presence['muc']['role'],
+ presence['muc']['nick']),
+ mtype='groupchat')
+
+ @botcmd
+ def tell(self, message, args):
+ pass
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2011, 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 datetime
+import threading
+import time
+
+from feedparser import parse as feed_parse
+
+from .plugin import Plugin, botcmd
+
+
+html_escape_table = {
+ "&": "&",
+ '"': """,
+ "'": "'",
+ ">": ">",
+ "<": "<",
+ }
+
+
+def html_escape(text):
+ """Produce entities within text."""
+ return "".join(html_escape_table.get(c, c) for c in text)
+
+
+def strtm_to_dtm(struc_time):
+ return datetime.datetime(*struc_time[:6])
+
+
+class FeedMonitor(threading.Thread):
+ def __init__(self, plugin):
+ threading.Thread.__init__(self)
+ self.feeds_list = plugin.FEEDS
+ self.tempo = plugin.TEMPO
+ self.bot = plugin.bot
+ self.last_check = datetime.datetime.utcnow()
+ self.seen = dict()
+ self.thread_killed = False
+
+ def send(self, message):
+ """simple wrapper around JabberBot().send()"""
+ self.bot.log.debug(self.bot.room)
+ self.bot.send_message(mto=self.bot.room,
+ mbody=message[1],
+ mhtml=message[0],
+ mtype='groupchat')
+
+ def new_posts(self, feed):
+ """Send new posts in feed"""
+ parsed_feed = feed_parse(feed)
+
+ # Cannot resolve address
+ if 'status' not in parsed_feed:
+ self.bot.log.error(u'Error from "%s": %s.' %
+ (feed, parsed_feed.bozo_exception.__repr__()))
+ return
+
+ # unusual return http code
+ if parsed_feed.status != 200:
+ self.bot.log.error(
+ u'Got code %(status)d from "%(href)s" (please update).' %
+ parsed_feed)
+ return
+
+ feed_updated = parsed_feed.feed.get('updated_parsed', None)
+
+ # Avoid looping over all posts if possible
+ if feed_updated and strtm_to_dtm(feed_updated) < self.last_check:
+ self.bot.log.debug('updated : %s' % strtm_to_dtm(feed_updated))
+ self.bot.log.debug('last check: %s' % self.last_check)
+ return
+
+ title = u'"%s":' % parsed_feed.feed.get('title', 'n/a')
+ xtitle = u'<strong>%s</strong>:' % html_escape(
+ parsed_feed.feed.get('title', 'n/a'))
+ text = [title]
+ xhtml = [xtitle]
+ feed_id = parsed_feed.feed.get('id', feed)
+ if not self.seen.setdefault(feed_id):
+ # Fills with post id when first started (prevent from posting all
+ # entries at startup)
+ self.seen[feed_id] = [post.id for post in parsed_feed.entries]
+ return
+
+ for post in parsed_feed.entries:
+ if post.id not in self.seen.get(feed_id):
+ self.seen[feed_id].append(post.id)
+ self.bot.log.info(post.title)
+
+ body = u'%(title)s %(link)s' % post
+ text.append(body)
+
+ xpost = dict(**post)
+ xpost['title'] = html_escape(xpost.get('title', 'n/a'))
+ xbody = u'<a href="%(link)s">%(title)s</a>' % xpost
+ xhtml.append(xbody)
+
+ if len(text) > 1:
+ self.send(('<br/>'.join(xhtml), '\n'.join(text)))
+
+ def run(self):
+ while not self.thread_killed:
+ self.bot.log.info(u'feeds check')
+ for feed in self.feeds_list:
+ try:
+ self.new_posts(feed)
+ except Exception as err:
+ self.bot.log.error(u'feeds thread crashed')
+ self.bot.log.error(err)
+ self.thread_killed = True
+ self.last_check = datetime.datetime.utcnow()
+ for _ in list(range(self.tempo)):
+ time.sleep(1)
+ if self.thread_killed:
+ return
+
+
+class Feeds(Plugin):
+ TEMPO = 60
+ FEEDS = [
+ # not working <http://bugs.debian.org/612274>
+ # 'http://www.debian.org/security/dsa',
+
+ # not working <http://bugs.debian.org/612274>
+ # 'http://www.debian.org/News/news',
+
+ # DPN in french
+ 'http://www.debian.org/News/weekly/dwn.fr.rdf',
+
+ # Misc
+ 'http://rss.gmane.org/topics/excerpts/gmane.linux.debian.devel.announce',
+ 'http://rss.gmane.org/gmane.linux.debian.user.security.announce',
+ 'http://planet-fr.debian.net/users/rss20.xml',
+ 'http://planet.debian.org/atom.xml',
+ ]
+
+ def __init__(self, bot):
+ Plugin.__init__(self, bot)
+ self.last_check = None
+ self.th_mon = FeedMonitor(self)
+ self.th_mon.start()
+
+ def shutdown(self):
+ self.th_mon.thread_killed = True
+
+ @botcmd
+ def feeds(self, message, args):
+ """feeds monitors debian project related feeds.
+ !feeds : registred feeds list
+ !feeds last : last check time"""
+ if 'last' in args:
+ return u'Last feeds check: %s' % self.th_mon.last_check
+ return u'\n'.join(Feeds.FEEDS)
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# 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/>.
+
+from .sid import botcmd
+
+class Plugin(object):
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ def shutdown(self):
+ pass
--- /dev/null
+# -*- 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