2017-10-24 16:56:37 +00:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from getpass import getpass
|
|
|
|
|
from argparse import ArgumentParser
|
2021-04-23 13:21:01 +00:00
|
|
|
|
from configparser import ConfigParser
|
|
|
|
|
from time import mktime, localtime
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
import slixmpp
|
2021-04-23 13:21:01 +00:00
|
|
|
|
import asyncio
|
|
|
|
|
import aiohttp
|
|
|
|
|
import feedparser
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NS = 'https://linkmauve.fr/protocol/feed-state'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FeedStateStorage(slixmpp.xmlstream.ElementBase):
|
|
|
|
|
namespace = NS
|
|
|
|
|
name = 'feed-state'
|
|
|
|
|
plugin_attrib = 'feed_state'
|
|
|
|
|
interfaces = set()
|
|
|
|
|
|
|
|
|
|
def set_state(self, url, latest):
|
|
|
|
|
state = FeedState()
|
|
|
|
|
state['url'] = url
|
|
|
|
|
state['latest'] = str(latest)
|
|
|
|
|
self.append(state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FeedState(slixmpp.xmlstream.ElementBase):
|
|
|
|
|
namespace = NS
|
|
|
|
|
name = 'feed'
|
|
|
|
|
plugin_attrib = 'feed'
|
|
|
|
|
plugin_multi_attrib = 'feeds'
|
|
|
|
|
interfaces = {'url', 'latest'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
slixmpp.xmlstream.register_stanza_plugin(FeedStateStorage, FeedState, iterable=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Feed:
|
|
|
|
|
def __init__(self, bot, url, last_updated):
|
|
|
|
|
self.bot = bot
|
|
|
|
|
self.url = url
|
|
|
|
|
self.rooms = []
|
|
|
|
|
self.last_updated = last_updated
|
|
|
|
|
asyncio.ensure_future(self.http_loop())
|
|
|
|
|
|
|
|
|
|
async def http_loop(self):
|
|
|
|
|
try:
|
|
|
|
|
async with aiohttp.ClientSession() as client:
|
|
|
|
|
while True:
|
|
|
|
|
text = await self.fetch(client)
|
|
|
|
|
await self.handle_http(text)
|
|
|
|
|
await asyncio.sleep(10)
|
|
|
|
|
except slixmpp.xmlstream.xmlstream.NotConnectedError:
|
|
|
|
|
# THIS is a hack.
|
|
|
|
|
import sys
|
|
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
|
|
async def handle_http(self, text):
|
|
|
|
|
data = feedparser.parse(text)
|
|
|
|
|
feed = data['feed']
|
|
|
|
|
# The global updated_parsed field can be buggy, use the most recent entry’s instead.
|
|
|
|
|
updated = max(entry['updated_parsed'] for entry in data['entries'])
|
|
|
|
|
if updated == self.last_updated:
|
|
|
|
|
return
|
|
|
|
|
self.last_updated = updated
|
|
|
|
|
title = feed['title']
|
|
|
|
|
entries = data['entries']
|
|
|
|
|
for entry in reversed(entries):
|
|
|
|
|
updated = entry['updated_parsed']
|
|
|
|
|
if updated < self.last_updated:
|
|
|
|
|
continue
|
|
|
|
|
body = self.bot.text_template.format(feed=title, title=entry['title'], link=entry['link'])
|
|
|
|
|
xhtml_im = self.bot.xhtml_template.format(feed=title, title=entry['title'], link=entry['link'])
|
|
|
|
|
for room in self.rooms:
|
|
|
|
|
self.bot.send_message(room, body, mhtml=xhtml_im, mtype='groupchat')
|
|
|
|
|
# Save the state of this feed.
|
|
|
|
|
timestamp = int(mktime(self.last_updated))
|
|
|
|
|
self.bot.stored_feeds[self.url] = timestamp
|
|
|
|
|
storage = FeedStateStorage()
|
|
|
|
|
for url, time in self.bot.stored_feeds.items():
|
|
|
|
|
storage.set_state(url, time)
|
|
|
|
|
self.bot.plugin['xep_0223'].store(storage, NS, id='current')
|
|
|
|
|
|
|
|
|
|
async def fetch(self, client):
|
|
|
|
|
async with client.get(self.url) as response:
|
|
|
|
|
return await response.text()
|
|
|
|
|
|
|
|
|
|
def add_room(self, jid):
|
|
|
|
|
self.rooms.append(jid)
|
|
|
|
|
logging.info('Adding room %s to feed %s.', jid, self.url)
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Feed(%s)' % self.url
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MUCBot(slixmpp.ClientXMPP):
|
2021-04-23 13:21:01 +00:00
|
|
|
|
def __init__(self, config):
|
|
|
|
|
slixmpp.ClientXMPP.__init__(self, config.jid, config.password)
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
self.rooms = config.rooms
|
|
|
|
|
self.nick = config.nick
|
|
|
|
|
self.feeds = config.feeds
|
|
|
|
|
self.text_template = config.text_template
|
|
|
|
|
self.xhtml_template = config.xhtml_template
|
|
|
|
|
self.stored_feeds = {}
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
self.add_event_handler("session_start", self.start)
|
2021-04-23 13:21:01 +00:00
|
|
|
|
self.add_event_handler("disconnected", self.on_disconnected)
|
2017-10-24 16:56:37 +00:00
|
|
|
|
self.add_event_handler("groupchat_message", self.muc_message)
|
|
|
|
|
for room in self.rooms:
|
|
|
|
|
self.add_event_handler("muc::%s::got_online" % room, self.muc_online)
|
|
|
|
|
self.add_event_handler("muc::%s::got_offline" % room, self.muc_offline)
|
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
async def start(self, event):
|
2017-10-24 16:56:37 +00:00
|
|
|
|
for room in self.rooms:
|
2021-04-23 13:21:01 +00:00
|
|
|
|
self.plugin['xep_0045'].join_muc(room, self.nick)
|
|
|
|
|
await self.retrieve_stored_feed_state()
|
|
|
|
|
feeds = {}
|
|
|
|
|
for muc, url in self.feeds:
|
|
|
|
|
if url not in feeds:
|
|
|
|
|
last_updated = localtime(self.stored_feeds.get(url))
|
|
|
|
|
feeds[url] = Feed(self, url, last_updated)
|
|
|
|
|
feed = feeds[url]
|
|
|
|
|
feed.add_room(muc)
|
|
|
|
|
self.feeds = feeds
|
|
|
|
|
|
|
|
|
|
def on_disconnected(self, event):
|
|
|
|
|
self.xmpp.connect()
|
|
|
|
|
|
|
|
|
|
async def retrieve_stored_feed_state(self):
|
|
|
|
|
try:
|
|
|
|
|
iq = await self.plugin['xep_0223'].retrieve(NS)
|
|
|
|
|
except slixmpp.exceptions.IqError:
|
|
|
|
|
logging.info('No feeds had been stored in PEP yet.')
|
|
|
|
|
else:
|
|
|
|
|
payload = iq['pubsub']['items']['item']['payload']
|
|
|
|
|
storage = FeedStateStorage(payload)
|
|
|
|
|
self.stored_feeds = {}
|
|
|
|
|
for feed in storage['feeds']:
|
|
|
|
|
url = feed['url']
|
|
|
|
|
time = int(feed['latest'])
|
|
|
|
|
self.stored_feeds[url] = time
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
def muc_message(self, message):
|
|
|
|
|
room = message['from'].bare
|
|
|
|
|
nick = message['from'].resource
|
|
|
|
|
self.log(room, 'R', '<%s> %s' % (message['from'].resource, message['body']))
|
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
reponse = "/me se frotte sur les jambes de"
|
|
|
|
|
|
|
|
|
|
if message['mucnick'] != self.nick and self.nick+"?" in message['body']:
|
|
|
|
|
self.send_message(mto=message['from'].bare,
|
|
|
|
|
mbody="%s %s." % (reponse, message['mucnick']),
|
|
|
|
|
mtype='groupchat')
|
|
|
|
|
|
2017-10-24 16:56:37 +00:00
|
|
|
|
def muc_online(self, presence):
|
|
|
|
|
room = presence['muc']['room']
|
|
|
|
|
nick = presence['muc']['nick']
|
|
|
|
|
status = ' (%s)' % presence['status'] if presence['status'] else ''
|
|
|
|
|
self.log(room, 'I', '---> %s joined the room%s' % (nick, status))
|
|
|
|
|
|
|
|
|
|
def muc_offline(self, presence):
|
|
|
|
|
room = presence['muc']['room']
|
|
|
|
|
nick = presence['muc']['nick']
|
|
|
|
|
status = ' (%s)' % presence['status'] if presence['status'] else ''
|
|
|
|
|
self.log(room, 'I', '<--- %s left the room%s' % (nick, status))
|
|
|
|
|
|
|
|
|
|
def log(self, room, typ, message):
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
day = now.strftime('%Y-%m-%d')
|
|
|
|
|
filename = '%s/%s.log' % (room, day)
|
|
|
|
|
os.makedirs(room, exist_ok=True)
|
|
|
|
|
with open(filename, 'a') as out:
|
|
|
|
|
timestamp = now.isoformat() + 'Z'
|
|
|
|
|
split_message = message.split('\n')
|
|
|
|
|
nb_lines = len(split_message) - 1
|
|
|
|
|
final_message = '\n '.join(split_message)
|
|
|
|
|
lines = 'M%s %s %03d %s\n' % (typ, timestamp, nb_lines, final_message)
|
|
|
|
|
out.write(lines)
|
|
|
|
|
|
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
class Config:
|
|
|
|
|
__slots__ = ['_parser', '_filename', 'jid', 'password', 'rooms', 'nick', 'feeds', 'text_template', 'xhtml_template']
|
|
|
|
|
|
|
|
|
|
def __init__(self, filename):
|
|
|
|
|
if filename is not None:
|
|
|
|
|
self._filename = filename
|
|
|
|
|
else:
|
|
|
|
|
xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
|
|
|
|
|
if xdg_config_home is None or xdg_config_home[0] != '/':
|
|
|
|
|
xdg_config_home = os.path.join(os.environ.get('HOME'), '.config')
|
|
|
|
|
self._filename = os.path.join(xdg_config_home, 'botlogmauve', 'bot.cfg')
|
|
|
|
|
self.restart()
|
|
|
|
|
|
|
|
|
|
def restart(self):
|
|
|
|
|
logging.info('Reading configuration from “%s”.', self._filename)
|
|
|
|
|
self._parser = ConfigParser()
|
|
|
|
|
with open(self._filename) as fp:
|
|
|
|
|
self._parser.read_file(fp, self._filename)
|
|
|
|
|
|
|
|
|
|
default_section = self._parser['DEFAULT']
|
|
|
|
|
self.jid = default_section['jid']
|
|
|
|
|
self.password = default_section['password']
|
|
|
|
|
self.nick = default_section['nick']
|
|
|
|
|
self.text_template = default_section['text_template']
|
|
|
|
|
self.xhtml_template = default_section['xhtml_template']
|
|
|
|
|
|
|
|
|
|
self.rooms = []
|
|
|
|
|
self.feeds = []
|
|
|
|
|
for room in self._parser.sections():
|
|
|
|
|
self.rooms.append(room)
|
|
|
|
|
section = self._parser[room]
|
|
|
|
|
try:
|
|
|
|
|
feeds = section['feeds']
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
for feed in feeds.split():
|
|
|
|
|
self.feeds.append((room, feed))
|
|
|
|
|
|
|
|
|
|
def main():
|
2017-10-24 16:56:37 +00:00
|
|
|
|
# Setup the command line arguments.
|
|
|
|
|
parser = ArgumentParser()
|
|
|
|
|
|
|
|
|
|
# Output verbosity options.
|
|
|
|
|
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
|
|
|
|
|
action="store_const", dest="loglevel",
|
|
|
|
|
const=logging.ERROR, default=logging.INFO)
|
|
|
|
|
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
|
|
|
|
|
action="store_const", dest="loglevel",
|
|
|
|
|
const=logging.DEBUG, default=logging.INFO)
|
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
# Configuration file override.
|
|
|
|
|
parser.add_argument("CONFIG", nargs='?', default='bot.cfg', help="overrides the configuration file")
|
2017-10-24 16:56:37 +00:00
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
# Setup logging.
|
|
|
|
|
logging.basicConfig(level=args.loglevel,
|
|
|
|
|
format='%(levelname)-8s %(message)s')
|
|
|
|
|
|
2021-04-23 13:21:01 +00:00
|
|
|
|
# Load the configuration file.
|
|
|
|
|
try:
|
|
|
|
|
config = Config(args.CONFIG)
|
|
|
|
|
except IOError:
|
|
|
|
|
logging.exception('Failed to read config file “%s”:', args.CONFIG)
|
|
|
|
|
return
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
# Setup the MUCBot and register plugins. Note that while plugins may
|
|
|
|
|
# have interdependencies, the order in which you register them does
|
|
|
|
|
# not matter.
|
2021-04-23 13:21:01 +00:00
|
|
|
|
xmpp = MUCBot(config)
|
2017-10-24 16:56:37 +00:00
|
|
|
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
|
|
|
|
xmpp.register_plugin('xep_0045') # Multi-User Chat
|
2021-04-23 13:21:01 +00:00
|
|
|
|
xmpp.register_plugin('xep_0071') # XHTML-IM
|
|
|
|
|
xmpp.register_plugin('xep_0223') # Persistent Storage of Private Data via PubSub
|
|
|
|
|
#xmpp.register_plugin('xep_0198') # Stream Management
|
|
|
|
|
xmpp.register_plugin('xep_0199', {'keepalive': True, 'interval': 60}) # XMPP Ping
|
2017-10-24 16:56:37 +00:00
|
|
|
|
|
|
|
|
|
# Connect to the XMPP server and start processing XMPP stanzas.
|
|
|
|
|
xmpp.connect()
|
|
|
|
|
xmpp.process()
|
2021-04-23 13:21:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|