mostr-zulip-bot/mostr.py

131 lines
4.9 KiB
Python

from typing import Dict
from pynostr.event import Event
from pynostr.relay_manager import RelayManager
from pynostr.key import PrivateKey
from pynostr.key import PublicKey
from zulip_bots.lib import AbstractBotHandler
def parse_pubkey(key_str: str) -> PublicKey:
try:
return PublicKey.from_hex(key_str)
except (ValueError, TypeError):
pass # try to load as npub if hex fails
try:
return PublicKey.from_npub(key_str)
except (ValueError, TypeError):
return None
class MostrHandler:
"""A Zulip bot to create mostr tasks on a nostr relay."""
def usage(self) -> str:
return """
Send me a message to create a task!
Use '#' and '-' to add resp. remove tags from the current context.
"""
def initialize(self, bot_handler: AbstractBotHandler) -> None:
self.config_info = bot_handler.get_config_info('mostr')
self.relay = self.config_info['relay']
self.relays = RelayManager()
self.relays.add_relay(self.relay)
self.key = PrivateKey.from_nsec(self.config_info['key']).hex()
print('Connecting to relay', self.relay)
# TODO React to all group messages
def handle_message(self, message: Dict[str, str], bot_handler: AbstractBotHandler) -> None:
username = message.get('sender_full_name')
print('Received', message, 'from', username)
# Tags
if message.get('type') == 'private' and len(message.get('display_recipient')) < 3:
context = f"s{message.get('sender_id')}"
else:
context = f"r{message.get('recipient_id')}"
try:
hashtags = list(filter(None, bot_handler.storage.get(context).split("#")))
except KeyError:
hashtags = []
print('Context', context, 'contains', hashtags)
if message['content'].startswith("#"):
hashtags.extend(filter(None, message['content'].split('#')))
bot_handler.send_reply(message, f"Active Hashtags: {hashtags}")
if hashtags:
result = bot_handler.storage.put(context, '#'.join(set(hashtags)))
return
if message['content'].startswith("-"):
to_remove = message['content'].replace('-', '#').split('#')
hashtags = [t for t in hashtags if t not in to_remove]
bot_handler.send_reply(message, f"Active Hashtags: {hashtags}")
bot_handler.storage.put(context, '#'.join(hashtags))
return
# Initial Feedback
if len(message['content']) < 4:
if message.get('type') == 'private':
bot_handler.send_reply(message, "That task name seems a bit short!")
else:
bot_handler.react(message, 'question')
return
bot_handler.react(message, 'working_on_it')
# Assignee Public Key
pubkey = None
user = message.get('sender_id') or message.get('sender_email')
if user:
# TODO Need proper verification, which is not done by pynostr
# A public key is generally also a valid private key, apparently
# try:
# privkey = PrivateKey.from_hex(message['content'])
# bot_handler.send_reply(message, f"Whoa that looks like a private key! Please delete it swiftly. This is the corresponding private key: {privkey.public_key}")
# return
# except:
# pass
# Parse pubkey from storage or message
key = f"pubkey_{user}"
if message['content']:
pubkey = parse_pubkey(message['content'])
if pubkey:
bot_handler.storage.put(key, pubkey.npub)
bot_handler.send_reply(message, "New pubkey set!")
return
try:
pubkey = PublicKey.from_npub(bot_handler.storage.get(key))
except (KeyError, IndexError):
if message.get('type') == 'private':
bot_handler.send_reply(message, "Please send me your public key for proper attribution")
# auto-assign via username matching, extract hashtags
tags = list(map(lambda tag: ['t', tag], hashtags or ['zulip']))
if pubkey:
tags.append(['p', pubkey.hex()])
event = Event(content=message['content'], kind=1621, tags=tags)
event.sign(self.key)
print('Publishing', event)
self.relays.publish_event(event)
self.relays.run_sync()
if message.get('type') == 'private':
bot_handler.send_reply(message, f"Sent `{event}` to {self.relay}")
else:
if hasattr(bot_handler, '_client'):
bot_handler._client.remove_reaction({'message_id': message['id'], 'emoji_name': 'working_on_it'})
bot_handler.react(message, 'check')
def __del__(self):
if hasattr(self, 'relays'):
self.relays.close_connections()
print('Terminated Mostr Bot')
handler_class = MostrHandler