angel/angel.py

291 lines
8.5 KiB
Python
Raw Normal View History

2025-04-07 15:57:52 -03:00
import asyncio
from collections import defaultdict
from slixmpp import ClientXMPP
2025-04-09 21:31:37 -03:00
from slixmpp.stanza import Message
2025-04-07 15:57:52 -03:00
2025-06-01 00:44:06 -03:00
2025-04-07 15:57:52 -03:00
class Lifo(list):
"""Limited size LIFO array to store messages and urls."""
def __init__(self, size):
"""Initialize the LIFO array."""
super().__init__()
self.size = size
def add(self, item):
"""Add an item to the LIFO array."""
self.insert(0, item)
if len(self) > self.size:
self.pop()
2025-06-01 00:44:06 -03:00
2025-04-09 21:31:37 -03:00
def create_messages_dict():
return defaultdict(
lambda: {
2025-06-01 00:44:06 -03:00
'messages': Lifo(100),
'links': Lifo(10),
'previews': Lifo(10),
2025-04-09 21:31:37 -03:00
}
)
2025-04-07 15:57:52 -03:00
2025-06-01 00:44:06 -03:00
2025-04-07 15:57:52 -03:00
class RegexCmd:
"""Regex command decorator."""
2025-04-09 21:31:37 -03:00
def __init__(self, bot, pattern, block=False, matcher=None):
2025-04-07 15:57:52 -03:00
"""Initialize the decorator."""
self.pattern = pattern
self.bot = bot
2025-04-09 21:31:37 -03:00
self.block = block
self.matcher = matcher
2025-04-07 15:57:52 -03:00
def __call__(self, func):
"""Call the decorator."""
self.bot.regex_cmds.append(self)
self.func = func
return self
2025-06-01 00:44:06 -03:00
2025-04-07 15:57:52 -03:00
class AngelBot(ClientXMPP):
"""AngelBot class."""
2025-06-01 00:44:06 -03:00
def __init__(self, jid, password, nick='angel', autojoin=None):
2025-04-09 21:31:37 -03:00
"""Initialize the bot."""
super().__init__(jid, password)
self.jid = jid
self.nick = nick
self.autojoin = autojoin or []
self.messages = create_messages_dict()
self.register_plugins()
self.add_handlers()
def reply(self, msg, body):
"""Reply to a message."""
self.save_message_history(msg)
self.raw_reply(msg, body)
def raw_reply(self, msg, body):
"""Reply to a message without saving history."""
self.send_message(
2025-06-01 00:44:06 -03:00
mto=msg['from'].bare,
2025-04-09 21:31:37 -03:00
mbody=body,
2025-06-01 00:44:06 -03:00
mtype=msg['type'],
2025-04-09 21:31:37 -03:00
)
def save_message_history(self, msg):
"""Save the history of messages."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
self.messages[sender]['messages'].add(msg['body'])
2025-04-09 21:31:37 -03:00
def get_message_history(self, msg):
"""Get the messages from the sender."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
return self.messages[sender]['messages']
2025-04-09 21:31:37 -03:00
def save_link_history(self, msg, url):
"""Save the history of links."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
self.messages[sender]['links'].add(url)
2025-04-09 21:31:37 -03:00
def get_link_history(self, msg):
"""Get the links from the sender."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
return self.messages[sender]['links']
2025-04-09 21:31:37 -03:00
def save_preview_history(self, msg, preview):
"""Save the history of previews."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
self.messages[sender]['previews'].add(preview)
2025-04-09 21:31:37 -03:00
def get_preview_history(self, msg):
"""Get the previews from the sender."""
2025-06-01 00:44:06 -03:00
sender = msg['from'].bare
return self.messages[sender]['previews']
2025-04-07 15:57:52 -03:00
regex_cmds = []
2025-04-09 21:31:37 -03:00
async def embed_file(self, sender, mtype, ftype, fname, outfile):
2025-04-07 15:57:52 -03:00
"""Embed a file and send the result to the sender."""
2025-06-01 00:44:06 -03:00
furl = await self.plugin['xep_0363'].upload_file(
2025-04-07 15:57:52 -03:00
fname, content_type=ftype, input_file=outfile
)
2025-06-01 00:44:06 -03:00
self.messages[sender]['links'].add(furl)
2025-04-07 15:57:52 -03:00
message = self.make_message(sender)
2025-06-01 00:44:06 -03:00
message['body'] = furl
message['type'] = mtype
message['oob']['url'] = furl
2025-04-07 15:57:52 -03:00
message.send()
2025-04-09 21:31:37 -03:00
def register_plugins(self):
2025-06-01 00:44:06 -03:00
self.register_plugin('xep_0030')
self.register_plugin('xep_0060')
self.register_plugin('xep_0054')
self.register_plugin('xep_0045')
self.register_plugin('xep_0066')
self.register_plugin('xep_0084')
self.register_plugin('xep_0153')
self.register_plugin('xep_0363')
2025-04-07 15:57:52 -03:00
2025-04-09 21:31:37 -03:00
def add_handlers(self):
2025-06-01 00:44:06 -03:00
self.add_event_handler('session_start', self.session_start)
self.add_event_handler('message', self.message)
self.add_event_handler('groupchat_message', self.muc_message)
2025-04-07 15:57:52 -03:00
# self.add_event_handler("vcard_avatar_update", self.debug_event)
# self.add_event_handler("stream_error", self.debug_event)
2025-06-01 00:44:06 -03:00
self.add_event_handler('disconnected', lambda _: self.connect())
2025-04-07 15:57:52 -03:00
async def session_start(self, event):
"""Start the bot."""
self.send_presence()
await self.get_roster()
await self.update_info()
for channel in self.autojoin:
try:
2025-06-01 00:44:06 -03:00
self.plugin['xep_0045'].join_muc(channel, self.nick)
2025-04-07 15:57:52 -03:00
except Exception as e:
print(e)
async def update_info(self):
"""Update the bot info."""
2025-06-01 00:44:06 -03:00
with open('angel.png', 'rb') as avatar_file:
2025-04-07 15:57:52 -03:00
avatar = avatar_file.read()
2025-06-01 00:44:06 -03:00
avatar_type = 'image/png'
avatar_id = self.plugin['xep_0084'].generate_id(avatar)
2025-04-07 15:57:52 -03:00
avatar_bytes = len(avatar)
2025-06-01 00:44:06 -03:00
asyncio.gather(self.plugin['xep_0084'].publish_avatar(avatar))
2025-04-07 15:57:52 -03:00
asyncio.gather(
2025-06-01 00:44:06 -03:00
self.plugin['xep_0153'].set_avatar(
2025-04-07 15:57:52 -03:00
avatar=avatar,
mtype=avatar_type,
)
)
info = {
2025-06-01 00:44:06 -03:00
'id': avatar_id,
'type': avatar_type,
'bytes': avatar_bytes,
2025-04-07 15:57:52 -03:00
}
2025-06-01 00:44:06 -03:00
asyncio.gather(self.plugin['xep_0084'].publish_avatar_metadata([info]))
2025-04-07 15:57:52 -03:00
2025-06-01 00:44:06 -03:00
vcard = self.plugin['xep_0054'].make_vcard()
2025-04-07 15:57:52 -03:00
2025-06-01 00:44:06 -03:00
vcard['URL'] = 'https://wiki.kalli.st/Angel'
vcard['DESC'] = 'Angel is a bot that can do link previews and embeds.'
vcard['NICKNAME'] = 'Angel'
vcard['FN'] = 'Angel'
2025-04-07 15:57:52 -03:00
2025-06-01 00:44:06 -03:00
asyncio.gather(self.plugin['xep_0054'].publish_vcard(vcard))
2025-04-07 15:57:52 -03:00
async def message(self, msg):
"""Process a message."""
2025-06-01 00:44:06 -03:00
if msg['type'] in ('chat', 'normal'):
edit = 'urn:xmpp:message-correct:0' in str(msg)
2025-04-07 15:57:52 -03:00
if edit:
return
2025-06-01 00:44:06 -03:00
mtype = msg['type']
sender = msg['from'].bare
2025-04-07 15:57:52 -03:00
self.process_commands(msg, sender, mtype)
async def muc_message(self, msg):
"""Process a groupchat message."""
2025-06-01 00:44:06 -03:00
if msg['type'] in ('groupchat', 'normal'):
edit = 'urn:xmpp:message-correct:0' in str(msg)
2025-04-07 15:57:52 -03:00
if edit:
return
2025-06-01 00:44:06 -03:00
if msg['mucnick'] == self.nick:
2025-04-07 15:57:52 -03:00
return
2025-06-01 00:44:06 -03:00
mtype = msg['type']
sender = msg['from'].bare
2025-04-07 15:57:52 -03:00
self.process_commands(msg, sender, mtype)
def process_commands(self, msg, sender, mtype):
"""Process commands."""
for cmd in self.regex_cmds:
2025-06-01 00:44:06 -03:00
if cmd.pattern.search(msg['body']):
2025-04-09 21:31:37 -03:00
ctx = CommandContext(self, msg)
if cmd.matcher and not cmd.matcher(ctx):
continue
cmd.func(ctx)
2025-06-01 00:44:06 -03:00
if cmd.block:
2025-04-09 21:31:37 -03:00
return
2025-06-01 00:44:06 -03:00
self.messages[sender]['messages'].add(msg['body'])
2025-04-09 21:31:37 -03:00
class CommandContext:
"""Command context."""
2025-06-01 00:44:06 -03:00
2025-04-09 21:31:37 -03:00
def __init__(self, bot: AngelBot, msg: Message):
"""Initialize the command context."""
self.bot = bot
self.msg = msg
def reply(self, body):
"""Get the reply function."""
return self.bot.reply(self.msg, body)
2025-06-01 00:44:06 -03:00
def raw_reply(self, body):
"""Get the raw reply function."""
return self.bot.raw_reply(self.msg, body)
2025-04-09 21:31:37 -03:00
@property
def sender(self):
"""Get the sender of the message."""
2025-06-01 00:44:06 -03:00
return self.msg['from'].bare
2025-04-09 21:31:37 -03:00
@property
def mtype(self):
"""Get the message type."""
2025-06-01 00:44:06 -03:00
return self.msg['type']
2025-04-09 21:31:37 -03:00
@property
def body(self):
"""Get the message body."""
2025-06-01 00:44:06 -03:00
return self.msg['body']
2025-04-09 21:31:37 -03:00
@property
def message_history(self):
"""Get the message history."""
return self.bot.get_message_history(self.msg)
@property
def link_history(self):
"""Get the link history."""
return self.bot.get_link_history(self.msg)
@property
def preview_history(self):
"""Get the preview history."""
return self.bot.get_preview_history(self.msg)
def save_link_history(self, url):
"""Save the link history."""
self.bot.save_link_history(self.msg, url)
def save_message_history(self):
"""Save the message history."""
self.bot.save_message_history(self.msg)
def save_preview_history(self, preview):
"""Save the preview history."""
self.bot.save_preview_history(self.msg, preview)
@property
def is_oob(self):
"""Check if the message is OOB."""
2025-06-01 00:44:06 -03:00
return bool(self.msg['oob']['url'])
2025-04-09 21:31:37 -03:00
def embed_file(self, ftype, fname, outfile):
"""Embed a file and send the result to the sender."""
2025-06-01 00:44:06 -03:00
asyncio.gather(
self.bot.embed_file(self.sender, self.mtype, ftype, fname, outfile)
)