Revamp parsing and handling of messages

This commit is contained in:
Luke Murphy 2021-01-16 16:38:12 +01:00
parent 27f202de37
commit 7d71fd2ba4
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC

View File

@ -1,5 +1,6 @@
"""XMPP bots for humans.""" """XMPP bots for humans."""
import re
from argparse import ArgumentParser from argparse import ArgumentParser
from configparser import ConfigParser from configparser import ConfigParser
from datetime import datetime as dt from datetime import datetime as dt
@ -21,59 +22,102 @@ from slixmpp import ClientXMPP
class SimpleMessage: class SimpleMessage:
"""A simple message interface.""" """A simple message interface."""
def __init__(self, message): def __init__(self, message, bot):
"""Initialise the object."""
self.message = message self.message = message
self.bot = bot
@property @property
def body(self): def text(self):
"""The entire message text."""
return self.message["body"] return self.message["body"]
@property
def content(self):
"""The content of the message received.
This implementation aims to match and extract the content of the
messages directed at bots in group chats. So, for example, when sending
messages like so.
echobot: hi
echobot, hi
echobot hi
The result produced by `message.content` will always be "hi". This
makes it easier to work with various commands and avoid messy parsing
logic in end-user implementations.
"""
body = self.message["body"]
try:
match = f"^{self.bot.nick}.?(\s)"
split = re.split(match, body)
filtered = list(filter(None, split))
return filtered[-1].strip()
except Exception as exception:
self.bot.log.error(f"Couldn't parse {body}: {exception}")
return None
@property @property
def sender(self): def sender(self):
"""The sender of the message."""
return self.message["from"] return self.message["from"]
@property @property
def room(self): def room(self):
"""The room from which the message originated."""
return self.message["from"].bare return self.message["from"].bare
@property @property
def receiver(self): def receiver(self):
"""The receiver of the message."""
return self.message["to"] return self.message["to"]
@property @property
def nickname(self): def type(self):
return self.message["mucnick"] """The type of the message."""
return self.message["type"]
@property @property
def type(self): def nick(self):
return self.message["type"] """The nick of the message."""
return self.message["mucnick"]
class Config: class Config:
"""Bot file configuration.""" """Bot file configuration."""
def __init__(self, name, config): def __init__(self, name, config):
"""Initialise the object."""
self.name = name self.name = name
self.config = config self.config = config
self.section = config[self.name] if self.name in config else {} self.section = config[self.name] if self.name in config else {}
@property @property
def account(self): def account(self):
"""The account of the bot."""
return self.section.get("account", None) return self.section.get("account", None)
@property @property
def password(self): def password(self):
"""The password of the bot account."""
return self.section.get("password", None) return self.section.get("password", None)
@property @property
def nick(self): def nick(self):
"""The nickname of the bot."""
return self.section.get("nick", None) return self.section.get("nick", None)
class Bot(ClientXMPP): class Bot(ClientXMPP):
"""XMPP bots for humans.""" """XMPP bots for humans."""
DIRECT_MESSAGE_TYPES = ("chat", "normal")
GROUP_MESSAGE_TYPES = ("groupchat", "normal")
def __init__(self): def __init__(self):
"""Initialise the object."""
self.name = type(self).__name__.lower() self.name = type(self).__name__.lower()
self.start = dt.now() self.start = dt.now()
@ -209,21 +253,21 @@ class Bot(ClientXMPP):
self.add_event_handler("message_error", self.error_message) self.add_event_handler("message_error", self.error_message)
def error_message(self, message): def error_message(self, message):
_message = SimpleMessage(message) message = SimpleMessage(message, self)
self.log.error(f"Received error message: {_message.body}") self.log.error(f"Received error message: {message.text}")
def direct_message(self, message): def direct_message(self, message):
"""Handle message event.""" """Handle direct message events."""
if message["type"] not in ("chat", "normal"): message = SimpleMessage(message, self)
if message.type not in self.DIRECT_MESSAGE_TYPES:
return return
_message = SimpleMessage(message) if message.text.startswith("@"):
self.command(message, to=message.sender)
if "@" in _message.body:
self.command(_message, to=_message.sender)
try: try:
self.direct(_message) self.direct(message)
except AttributeError: except AttributeError:
self.log.info(f"Bot.direct not implemented for {self.nick}") self.log.info(f"Bot.direct not implemented for {self.nick}")
@ -258,23 +302,24 @@ class Bot(ClientXMPP):
self.plugin["xep_0045"].join_muc(message["from"], self.config.nick) self.plugin["xep_0045"].join_muc(message["from"], self.config.nick)
def group_message(self, message): def group_message(self, message):
"""Handle groupchat_message event.""" """Handle group chat message events."""
if message["type"] not in ("groupchat", "normal"): message = SimpleMessage(message, self)
if message.text.startswith("@"):
return self.meta(message, room=message.room)
miss = message.type not in self.GROUP_MESSAGE_TYPES
loop = message.nick == self.nick
other = self.nick not in message.text
if miss or loop or other:
return return
if message["mucnick"] == self.config.nick: if message.content.startswith("@"):
return self.command(message, room=message.room)
if self.nick not in message["body"]:
return
_message = SimpleMessage(message)
if self.nick in _message.body and "@" in _message.body:
self.command(_message, room=_message.room)
try: try:
self.group(_message) self.group(message)
except AttributeError: except AttributeError:
self.log.info(f"Bot.group not implemented for {self.nick}") self.log.info(f"Bot.group not implemented for {self.nick}")
@ -305,7 +350,7 @@ class Bot(ClientXMPP):
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
def reply(self, body, to=None, room=None): def reply(self, text, to=None, room=None):
"""Send back a reply.""" """Send back a reply."""
if to is None and room is None: if to is None and room is None:
self.log.error("`to` or `room` arguments required for `reply`") self.log.error("`to` or `room` arguments required for `reply`")
@ -315,7 +360,7 @@ class Bot(ClientXMPP):
self.log.error("Cannot send to both `to` and `room` for `reply`") self.log.error("Cannot send to both `to` and `room` for `reply`")
exit(1) exit(1)
kwargs = {"mbody": body} kwargs = {"mbody": text}
if to is not None: if to is not None:
kwargs["mto"] = to kwargs["mto"] = to
kwargs["mtype"] = "chat" kwargs["mtype"] = "chat"
@ -330,17 +375,22 @@ class Bot(ClientXMPP):
"""Time since the bot came up.""" """Time since the bot came up."""
return naturaldelta(self.start - dt.now()) return naturaldelta(self.start - dt.now())
def meta(self, message, **kwargs):
"""Handle meta command invocations."""
if message.text.startswith("@bots"):
self.reply("🖐️", **kwargs)
def command(self, message, **kwargs): def command(self, message, **kwargs):
"""Handle command invocations.""" """Handle command invocations."""
if "@uptime" in message.body: if message.content.startswith("@uptime"):
self.reply(self.uptime, **kwargs) self.reply(self.uptime, **kwargs)
elif "@help" in message.body: elif message.content.startswith("@help"):
try: try:
self.reply(cleandoc(self.help), **kwargs) self.reply(cleandoc(self.help), **kwargs)
except AttributeError: except AttributeError:
self.reply("No help found 🤔️", **kwargs) self.reply("No help found 🤔️", **kwargs)
else: else:
self.log.info(f"'{message.body}' not handled") self.log.info(f"'{message.content}' not handled")
class EchoBot(Bot): class EchoBot(Bot):
@ -349,18 +399,17 @@ class EchoBot(Bot):
Simply direct message the bot and see if you get back what you sent. It Simply direct message the bot and see if you get back what you sent. It
also works in group chats but in this case you need to summon the bot using also works in group chats but in this case you need to summon the bot using
its nickname. its nickname.
""" """
help = "I echo messages back 🖖️" help = "I echo messages back 🖖️"
def direct(self, message): def direct(self, message):
"""Send back whatever we receive.""" """Send back whatever we receive."""
self.reply(message.body, to=message.sender) self.reply(message.text, to=message.sender)
def group(self, message): def group(self, message):
"""Send back whatever receive in group chats.""" """Send back whatever receive in group chats."""
self.reply(message.body.split(":")[-1], room=message.room) self.reply(message.text.split(":")[-1], room=message.room)
class WhisperBot(Bot): class WhisperBot(Bot):
@ -371,14 +420,13 @@ class WhisperBot(Bot):
to whisper your message into the group chat. The bot will then do this on to whisper your message into the group chat. The bot will then do this on
your behalf and not reveal your identity. This is nice when you want to your behalf and not reveal your identity. This is nice when you want to
communicate with the group anonymously. communicate with the group anonymously.
""" """
help = "I whisper private messages into group chats 😌️" help = "I whisper private messages into group chats 😌️"
def direct(self, message): def direct(self, message):
"""Receive private messages and whisper them into group chats.""" """Receive private messages and whisper them into group chats."""
self.reply(f"*pssttt...* {message.body}", room=message.room) self.reply(f"*pssttt...* {message.content}", room=message.room)
class GlossBot(Bot): class GlossBot(Bot):
@ -391,7 +439,6 @@ class GlossBot(Bot):
shared glossary when summoned in a group chat. This bot makes use of shared glossary when summoned in a group chat. This bot makes use of
persistent storage so the glossary is always there even if the bot goes persistent storage so the glossary is always there even if the bot goes
away. away.
""" """
help = """ help = """