Revamp parsing and handling of messages
This commit is contained in:
parent
27f202de37
commit
7d71fd2ba4
125
xbotlib.py
125
xbotlib.py
@ -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 = """
|
||||||
|
Reference in New Issue
Block a user