Sending via propagatio nodes, new commands

also fixes #1
This commit is contained in:
Michał Rudowicz 2025-10-14 23:15:39 +02:00
parent d48db4e659
commit 1d365359ee
2 changed files with 80 additions and 15 deletions

View File

@ -4,10 +4,13 @@ from time import monotonic
from hashlib import sha256 from hashlib import sha256
import json import json
from functools import reduce from functools import reduce
from typing import Iterable
bot = LXMFBot("HSWro Conference Bot") bot = LXMFBot("HSWro Conference Bot")
PASS = '34a77e61000b2ba1f56201332ef64d93f9cdc60e63ab48bc255102586ef7e592' 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: def check_password(password: str) -> bool:
@ -17,18 +20,18 @@ def check_password(password: str) -> bool:
def ban_sender(r, userhash, until): def ban_sender(r, userhash, until):
r.rpush("hswroconference:banned", str(json.dumps({ r.rpush(BANNED_NAME, str(json.dumps({
"hash": userhash, "hash": userhash,
"until": str(until), "until": str(until),
}))) })))
def check_banned(r, userhash): 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')) m = json.loads(raw.decode('utf-8'))
if m['hash'] == userhash: if m['hash'] == userhash:
if monotonic() > float(m['until']): if monotonic() > float(m['until']):
r.lrem("hswroconference:banned", 0, raw) r.lrem(BANNED_NAME, 0, raw)
print(f"Ban expired for {userhash}, unbanning") print(f"Ban expired for {userhash}, unbanning")
return False return False
return True return True
@ -40,7 +43,7 @@ def check_name(username: str) -> bool:
return all(map(lambda c: c.isalnum(), username)) 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(): def login_usage():
msg.reply("Usage: `/login [NICK] [PASSWORD]`") msg.reply("Usage: `/login [NICK] [PASSWORD]`")
parts = msg.content.split() parts = msg.content.split()
@ -48,43 +51,79 @@ def handle_login(msg, r, from_known_sender):
login_usage() login_usage()
return return
assert parts[0] == "/login" assert parts[0] == "/login"
if from_known_sender:
msg.reply("You are already logged in.")
return
if not check_password(parts[2]): if not check_password(parts[2]):
ban_sender(r, msg.sender, monotonic() + WRONG_LOGIN_BAN_DURATION) ban_sender(r, msg.sender, monotonic() + WRONG_LOGIN_BAN_DURATION)
return return
if not check_name(parts[1]): if not check_name(parts[1]):
msg.reply("Name can only consist of alphanumeric characters.") msg.reply("Name can only consist of alphanumeric characters.")
return return
r.rpush("hswroconference:users", str(json.dumps({ r.rpush(USERS_NAME, str(json.dumps({
"hash": msg.sender, "hash": msg.sender,
"nick": parts[1], "nick": parts[1],
}))) })))
msg.reply(f"Logged in as {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 @bot.received
def echo_msg(msg): def handle_msg(msg):
r = Redis(host='localhost', port=6379, db=0) r = Redis(host='localhost', port=6379, db=0)
if check_banned(r, msg.sender): if check_banned(r, msg.sender):
print(f"Message from banned user: {msg.sender}") print(f"Message from banned user: {msg.sender}")
return return
known_senders = list(map(lambda x: json.loads(x.decode('utf-8')), 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, sender_info = reduce(lambda x, y: y if (y["hash"] == msg.sender) else x,
known_senders, None) known_senders, None)
from_known_sender = sender_info is not None from_known_sender = sender_info is not None
assert isinstance(msg.content, str) assert isinstance(msg.content, str)
if msg.content.startswith("/login"): if msg.content.startswith("/login"):
handle_login(msg, r, from_known_sender) return handle_login(msg, r, from_known_sender, known_senders)
return 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: if not from_known_sender:
msg.reply("Please identify with `/login [NICK] [PASSWORD]`") msg.reply("Please identify with `/login [NICK] [PASSWORD]`")
print(f"Received a message from unknown user. Ignoring.")
else: else:
print(f"Forwarding message from {sender_info['nick']}") print(f"Forwarding message from {sender_info['nick']}")
for r in known_senders: receiver_hashes = map(lambda x: x['hash'], filter(
if r["hash"] == msg.sender: lambda x: x['hash'] != msg.sender, known_senders))
continue send_to(f"<{sender_info['nick']}> {msg.content}", receiver_hashes)
bot.send(r["hash"], f"<{sender_info['nick']}> {msg.content}")
bot.run() bot.run()

View File

@ -14,7 +14,8 @@ from typing import NamedTuple, Optional, Callable
class LXMFBot: class LXMFBot:
path_request_timeout = 15 # seconds 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.name = name
self.announce_time = announce # seconds self.announce_time = announce # seconds
self.delivery_callbacks = [] self.delivery_callbacks = []
@ -38,6 +39,8 @@ class LXMFBot:
RNS.log('Loaded identity from file', RNS.LOG_INFO) RNS.log('Loaded identity from file', RNS.LOG_INFO)
self.router = LXMRouter( self.router = LXMRouter(
storagepath=os.path.join(self.config_path, "router")) 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.router.register_delivery_callback(self._message_received)
self.source = self.router.register_delivery_identity( self.source = self.router.register_delivery_identity(
self.id, display_name=name) self.id, display_name=name)
@ -76,6 +79,27 @@ class LXMFBot:
msg = SimpleNamespace(**obj) msg = SimpleNamespace(**obj)
callback(msg) 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', def send(self, destination, message, title='Reply',
originally_path_requested=monotonic()): originally_path_requested=monotonic()):
recipent_hash = bytes.fromhex(destination) recipent_hash = bytes.fromhex(destination)
@ -91,6 +115,8 @@ class LXMFBot:
lxm = LXMessage(dest, self.source, message, lxm = LXMessage(dest, self.source, message,
title=title, desired_method=LXMessage.DIRECT) title=title, desired_method=LXMessage.DIRECT)
lxm.try_propagation_on_fail = True 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}") RNS.log(f"Will send a message to {destination}")
self.queue.put(lambda: self.router.handle_outbound(lxm)) self.queue.put(lambda: self.router.handle_outbound(lxm))