Create Zulip Mostr Bot
This commit is contained in:
commit
7808b57b0b
|
@ -0,0 +1,5 @@
|
|||
.idea
|
||||
fixtures/
|
||||
test_mostr.py
|
||||
/mostr.conf
|
||||
/zuliprc
|
|
@ -0,0 +1,31 @@
|
|||
This is a zulip bot to create mostr tasks on a nostr relay.
|
||||
|
||||
# Quickstart
|
||||
|
||||
1. Clone https://github.com/zulip/python-zulip-api
|
||||
2. Run `python3 ./tools/provision` and activate the created venv
|
||||
3. Clone this repo into `python-zulip-api/zulip_bots/zulip_bots/bots/mostr`
|
||||
4. Create a `mostr.conf` to specify the private key and relay for the bot to use:
|
||||
```ini
|
||||
[mostr]
|
||||
key = nsec...
|
||||
relay = wss://...
|
||||
```
|
||||
|
||||
Now you can test it with:
|
||||
|
||||
zulip-bot-shell mostr --bot-config-file mostr.conf
|
||||
|
||||
5. Follow the setup steps in https://zulip.com/api/running-bots and put the zuliprc file into the mostr directory
|
||||
|
||||
Now you can properly run the bot with:
|
||||
|
||||
zulip-run-bot mostr --config-file zuliprc --bot-config-file mostr.conf
|
||||
|
||||
It will track context per conversation,
|
||||
so you can DM it, ping it in topics or group chats.
|
||||
|
||||
Note that every message not starting with `#` or `-`
|
||||
(to add or remove tags from current context, respectively)
|
||||
or containing a valid public key for assignment
|
||||
will create a new task.
|
|
@ -0,0 +1,126 @@
|
|||
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 for Mostr"""
|
||||
|
||||
def usage(self) -> str:
|
||||
return "Send me a message to create a task!"
|
||||
|
||||
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):
|
||||
self.relays.close_connections()
|
||||
print('Terminated Mostr Bot')
|
||||
|
||||
handler_class = MostrHandler
|
|
@ -0,0 +1 @@
|
|||
pynostr
|
Loading…
Reference in New Issue