mise à jour des derniers changement de LinkMauve

This commit is contained in:
linkmauve 2021-04-23 15:21:01 +02:00
parent 8e5f61c112
commit 54606ff5a1
2 changed files with 215 additions and 32 deletions

24
bot.cfg.example Normal file
View file

@ -0,0 +1,24 @@
[DEFAULT]
# The Jabber identifier of your bot, of the form <user>@<domain>.
jid = bot@example.org
# The password associated with the JID you specified above.
password = bot_password
# A list of admins who are allowed to do things to your bot, separated with a
# space (" ").
admins = you@example.com
# The nick you want your bot to use when it will join the rooms.
nick = bot
# The text and XHTML-IM templates your bot will use to link to new feed
# entries.
text_template = News from {feed}: {title} <{link}>
xhtml_template = News from <strong>{feed}</strong>: <a href="{link}">{title}</a>
# Each room you want to join must be listed between square brackets ("[…]").
[room@chat.example.net]
# Each room can get one or more feeds associated, in which case your bot will
# talk for every new entry. You must separate the feeds with a space (" ").
feeds = https://example.net/feed.atom

223
log.py
View file

@ -5,37 +5,160 @@ import os
from datetime import datetime from datetime import datetime
from getpass import getpass from getpass import getpass
from argparse import ArgumentParser from argparse import ArgumentParser
from configparser import ConfigParser
from time import mktime, localtime
import slixmpp import slixmpp
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 entrys 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
class MUCBot(slixmpp.ClientXMPP): class MUCBot(slixmpp.ClientXMPP):
def __init__(self, jid, password, rooms, nick): def __init__(self, config):
slixmpp.ClientXMPP.__init__(self, jid, password) slixmpp.ClientXMPP.__init__(self, config.jid, config.password)
self.rooms = rooms self.rooms = config.rooms
self.nick = nick self.nick = config.nick
self.feeds = config.feeds
self.text_template = config.text_template
self.xhtml_template = config.xhtml_template
self.stored_feeds = {}
self.add_event_handler("session_start", self.start) self.add_event_handler("session_start", self.start)
self.add_event_handler("disconnected", self.on_disconnected)
self.add_event_handler("groupchat_message", self.muc_message) self.add_event_handler("groupchat_message", self.muc_message)
for room in self.rooms: for room in self.rooms:
self.add_event_handler("muc::%s::got_online" % room, self.muc_online) self.add_event_handler("muc::%s::got_online" % room, self.muc_online)
self.add_event_handler("muc::%s::got_offline" % room, self.muc_offline) self.add_event_handler("muc::%s::got_offline" % room, self.muc_offline)
async def start(self, event):
def start(self, event):
for room in self.rooms: for room in self.rooms:
self.plugin['xep_0045'].join_muc(room, self.plugin['xep_0045'].join_muc(room, self.nick)
self.nick, await self.retrieve_stored_feed_state()
# If a room password is needed, use: feeds = {}
# password=the_room_password, for muc, url in self.feeds:
wait=True) 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
def muc_message(self, message): def muc_message(self, message):
room = message['from'].bare room = message['from'].bare
nick = message['from'].resource nick = message['from'].resource
self.log(room, 'R', '<%s> %s' % (message['from'].resource, message['body'])) self.log(room, 'R', '<%s> %s' % (message['from'].resource, message['body']))
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')
def muc_online(self, presence): def muc_online(self, presence):
room = presence['muc']['room'] room = presence['muc']['room']
nick = presence['muc']['nick'] nick = presence['muc']['nick']
@ -62,7 +185,46 @@ class MUCBot(slixmpp.ClientXMPP):
out.write(lines) out.write(lines)
if __name__ == '__main__': 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():
# Setup the command line arguments. # Setup the command line arguments.
parser = ArgumentParser() parser = ArgumentParser()
@ -74,39 +236,36 @@ if __name__ == '__main__':
action="store_const", dest="loglevel", action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.INFO) const=logging.DEBUG, default=logging.INFO)
# JID and password options. # Configuration file override.
parser.add_argument("-j", "--jid", dest="jid", parser.add_argument("CONFIG", nargs='?', default='bot.cfg', help="overrides the configuration file")
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use")
parser.add_argument("-r", "--rooms", dest="rooms", nargs='+',
help="MUC rooms to join")
parser.add_argument("-n", "--nick", dest="nick",
help="MUC nickname")
args = parser.parse_args() args = parser.parse_args()
# Setup logging. # Setup logging.
logging.basicConfig(level=args.loglevel, logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s') format='%(levelname)-8s %(message)s')
if args.jid is None: # Load the configuration file.
args.jid = input("Username: ") try:
if args.password is None: config = Config(args.CONFIG)
args.password = getpass("Password: ") except IOError:
if args.rooms is None: logging.exception('Failed to read config file “%s”:', args.CONFIG)
args.rooms = input("MUC rooms: ") return
if args.nick is None:
args.nick = input("MUC nickname: ")
# Setup the MUCBot and register plugins. Note that while plugins may # Setup the MUCBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does # have interdependencies, the order in which you register them does
# not matter. # not matter.
xmpp = MUCBot(args.jid, args.password, args.rooms, args.nick) xmpp = MUCBot(config)
xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.register_plugin('xep_0199') # XMPP Ping 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
# Connect to the XMPP server and start processing XMPP stanzas. # Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect() xmpp.connect()
xmpp.process() xmpp.process()
if __name__ == '__main__':
main()