diff --git a/groupchatbot/bot.py b/groupchatbot/bot.py index 8d6eda2..0aac851 100644 --- a/groupchatbot/bot.py +++ b/groupchatbot/bot.py @@ -4,10 +4,13 @@ from time import monotonic from hashlib import sha256 import json from functools import reduce +from typing import Iterable bot = LXMFBot("HSWro Conference Bot") PASS = '34a77e61000b2ba1f56201332ef64d93f9cdc60e63ab48bc255102586ef7e592' -WRONG_LOGIN_BAN_DURATION = 60 +WRONG_LOGIN_BAN_DURATION = 30 +USERS_NAME = "hswroconference:users" +BANNED_NAME = "hswroconference:banned" def check_password(password: str) -> bool: @@ -17,18 +20,18 @@ def check_password(password: str) -> bool: def ban_sender(r, userhash, until): - r.rpush("hswroconference:banned", str(json.dumps({ + r.rpush(BANNED_NAME, str(json.dumps({ "hash": userhash, "until": str(until), }))) def check_banned(r, userhash): - for raw in r.lrange("hswroconference:banned", 0, -1): + for raw in r.lrange(BANNED_NAME, 0, -1): m = json.loads(raw.decode('utf-8')) if m['hash'] == userhash: if monotonic() > float(m['until']): - r.lrem("hswroconference:banned", 0, raw) + r.lrem(BANNED_NAME, 0, raw) print(f"Ban expired for {userhash}, unbanning") return False return True @@ -40,7 +43,7 @@ def check_name(username: str) -> bool: return all(map(lambda c: c.isalnum(), username)) -def handle_login(msg, r, from_known_sender): +def handle_login(msg, r, from_known_sender, known_senders: Iterable): def login_usage(): msg.reply("Usage: `/login [NICK] [PASSWORD]`") parts = msg.content.split() @@ -48,43 +51,79 @@ def handle_login(msg, r, from_known_sender): login_usage() return assert parts[0] == "/login" + if from_known_sender: + msg.reply("You are already logged in.") + return if not check_password(parts[2]): ban_sender(r, msg.sender, monotonic() + WRONG_LOGIN_BAN_DURATION) return if not check_name(parts[1]): msg.reply("Name can only consist of alphanumeric characters.") return - r.rpush("hswroconference:users", str(json.dumps({ + r.rpush(USERS_NAME, str(json.dumps({ "hash": msg.sender, "nick": parts[1], }))) msg.reply(f"Logged in as {parts[1]}.") + send_to(f"{parts[1]} joined the chat.", map( + lambda x: x['hash'], known_senders)) + + +def handle_logout(msg, r, from_known_sender, known_senders: Iterable): + if not from_known_sender: + return handle_help(msg) + for raw in r.lrange(USERS_NAME, 0, -1): + parsed = json.loads(raw.decode('utf-8')) + if msg.sender == parsed['hash']: + r.lrem(USERS_NAME, 0, raw) + msg.reply(f"User {parsed['nick']} logged out.") + send_to(f"{parsed['nick']} left the chat.", map( + lambda x: x['hash'], + filter(lambda x: x['hash'] != parsed['hash'], known_senders))) + return + + +def handle_help(msg): + msg.reply("""Supported commands: +/login [NICK] [PASSWORD] - Logs user in. Partyline messages will be sent. +/logout - Logs user out. Partyline messages will stop. +/help - Displays this message.""") + + +def send_to(text: str, receiver_hashes: Iterable[str]): + for receiver in receiver_hashes: + bot.send(receiver, text) @bot.received -def echo_msg(msg): +def handle_msg(msg): r = Redis(host='localhost', port=6379, db=0) if check_banned(r, msg.sender): print(f"Message from banned user: {msg.sender}") return known_senders = list(map(lambda x: json.loads(x.decode('utf-8')), - r.lrange("hswroconference:users", 0, -1))) + r.lrange(USERS_NAME, 0, -1))) sender_info = reduce(lambda x, y: y if (y["hash"] == msg.sender) else x, known_senders, None) from_known_sender = sender_info is not None assert isinstance(msg.content, str) if msg.content.startswith("/login"): - handle_login(msg, r, from_known_sender) - return + return handle_login(msg, r, from_known_sender, known_senders) + if msg.content.startswith("/logout"): + return handle_logout(msg, r, from_known_sender, known_senders) + if msg.content.startswith("/help"): + return handle_help(msg) + if msg.content.startswith("/"): + return handle_help(msg) if not from_known_sender: msg.reply("Please identify with `/login [NICK] [PASSWORD]`") + print(f"Received a message from unknown user. Ignoring.") else: print(f"Forwarding message from {sender_info['nick']}") - for r in known_senders: - if r["hash"] == msg.sender: - continue - bot.send(r["hash"], f"<{sender_info['nick']}> {msg.content}") + receiver_hashes = map(lambda x: x['hash'], filter( + lambda x: x['hash'] != msg.sender, known_senders)) + send_to(f"<{sender_info['nick']}> {msg.content}", receiver_hashes) bot.run() diff --git a/groupchatbot/lxmfbot.py b/groupchatbot/lxmfbot.py index 95b5bc4..61626f2 100644 --- a/groupchatbot/lxmfbot.py +++ b/groupchatbot/lxmfbot.py @@ -14,7 +14,8 @@ from typing import NamedTuple, Optional, Callable class LXMFBot: path_request_timeout = 15 # seconds - def __init__(self, name, announce=360): + def __init__(self, name, announce=360, + outbound_propagation_node="bf294c725196ff3a996bcba1eb9c16fb"): self.name = name self.announce_time = announce # seconds self.delivery_callbacks = [] @@ -38,6 +39,8 @@ class LXMFBot: RNS.log('Loaded identity from file', RNS.LOG_INFO) self.router = LXMRouter( storagepath=os.path.join(self.config_path, "router")) + self.router.set_outbound_propagation_node( + bytes.fromhex(outbound_propagation_node)) self.router.register_delivery_callback(self._message_received) self.source = self.router.register_delivery_identity( self.id, display_name=name) @@ -76,6 +79,27 @@ class LXMFBot: msg = SimpleNamespace(**obj) callback(msg) + def message_notification(self, message): + """ Taken from NomadNet """ + if message.state == LXMessage.FAILED and \ + hasattr(message, "try_propagation_on_fail") and \ + message.try_propagation_on_fail: + if hasattr(message, "stamp_generation_failed") and \ + message.stamp_generation_failed: + RNS.log(f"Could not send {message} due " + + "to a stamp generation failure", RNS.LOG_ERROR) + else: + RNS.log("Direct delivery of "+str(message) + + " failed. Retrying as propagated message.", + RNS.LOG_VERBOSE) + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + if hasattr(message, "next_delivery_attempt"): + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMessage.PROPAGATED + self.router.handle_outbound(message) + def send(self, destination, message, title='Reply', originally_path_requested=monotonic()): recipent_hash = bytes.fromhex(destination) @@ -91,6 +115,8 @@ class LXMFBot: lxm = LXMessage(dest, self.source, message, title=title, desired_method=LXMessage.DIRECT) lxm.try_propagation_on_fail = True + lxm.register_delivery_callback(self.message_notification) + lxm.register_failed_callback(self.message_notification) RNS.log(f"Will send a message to {destination}") self.queue.put(lambda: self.router.handle_outbound(lxm))