mise à jour des derniers changement de LinkMauve
This commit is contained in:
parent
8e5f61c112
commit
54606ff5a1
2 changed files with 215 additions and 32 deletions
24
bot.cfg.example
Normal file
24
bot.cfg.example
Normal 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
223
log.py
|
@ -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 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
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
Loading…
Reference in a new issue