user can send LXMF message to create a new LXST channel
This commit is contained in:
+417
-37
@@ -1,13 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
lxst-relay - Group voice channel bridge for Reticulum Telephony.
|
lxst-relay - Group voice channel bridge for Reticulum Telephony
|
||||||
|
with NomadNet LXMF bot for channel management.
|
||||||
|
|
||||||
Allows multiple lxst users to call into a shared voice channel
|
Allows multiple users to call into a shared voice channel.
|
||||||
and talk together in a conference bridge style. Audio from each
|
Users can create new channels by messaging the bot via LXMF
|
||||||
participant is mixed and redistributed to all others.
|
with ``/create_channel <name>``. Each channel is its own
|
||||||
|
Reticulum node with a unique identity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import RNS
|
import RNS
|
||||||
|
import LXMF
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import signal as signal_module
|
import signal as signal_module
|
||||||
@@ -30,6 +33,10 @@ FIELD_SIGNALLING = 0x00
|
|||||||
FIELD_FRAMES = 0x01
|
FIELD_FRAMES = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Audio pipeline components (unchanged)
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
class FrameCaptureSink(LocalSink):
|
class FrameCaptureSink(LocalSink):
|
||||||
"""Stores the latest decoded float32 audio frame from a participant."""
|
"""Stores the latest decoded float32 audio frame from a participant."""
|
||||||
|
|
||||||
@@ -106,7 +113,7 @@ class ConferenceSource(LocalSource):
|
|||||||
|
|
||||||
|
|
||||||
class Participant:
|
class Participant:
|
||||||
"""A remote caller connected to the relay."""
|
"""A remote caller connected to a channel."""
|
||||||
|
|
||||||
def __init__(self, link, identity):
|
def __init__(self, link, identity):
|
||||||
self.link = link
|
self.link = link
|
||||||
@@ -224,12 +231,234 @@ class VoiceChannel:
|
|||||||
return mixed
|
return mixed
|
||||||
|
|
||||||
|
|
||||||
class LXSTRelay(SignallingReceiver):
|
# ======================================================================
|
||||||
"""Conference bridge for Reticulum Telephony.
|
# Dynamic channel node – each is its own Reticulum identity
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
Registers as a standard ``lxst.telephony`` destination so any
|
class ChannelNode(SignallingReceiver):
|
||||||
lxst client can call in. Callers are placed into a shared
|
"""A voice channel that operates as its own Reticulum node.
|
||||||
``VoiceChannel`` and their audio is mixed together.
|
|
||||||
|
Has an independent ``RNS.Identity``, announces its own
|
||||||
|
``lxst.telephony`` destination, and manages its own
|
||||||
|
``VoiceChannel`` instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, identity, name, admins, frame_time_ms=60):
|
||||||
|
super().__init__()
|
||||||
|
self.identity = identity
|
||||||
|
self.name = name
|
||||||
|
self.admins = admins # set[bytes] – identity hashes
|
||||||
|
self.frame_time_ms = frame_time_ms
|
||||||
|
self.voice_channel = VoiceChannel(name=name, frame_time_ms=frame_time_ms)
|
||||||
|
|
||||||
|
self.destination = RNS.Destination(
|
||||||
|
identity,
|
||||||
|
RNS.Destination.IN,
|
||||||
|
RNS.Destination.SINGLE,
|
||||||
|
APP_NAME,
|
||||||
|
PRIMITIVE_NAME,
|
||||||
|
)
|
||||||
|
self.destination.set_proof_strategy(RNS.Destination.PROVE_NONE)
|
||||||
|
self.destination.set_link_established_callback(
|
||||||
|
self._incoming_link_established
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Link lifecycle ------------------------------------------------
|
||||||
|
|
||||||
|
def _incoming_link_established(self, link):
|
||||||
|
link.is_incoming = True
|
||||||
|
link.is_outgoing = False
|
||||||
|
link.answered = False
|
||||||
|
link.is_terminating = False
|
||||||
|
|
||||||
|
link.set_remote_identified_callback(self._caller_identified)
|
||||||
|
link.set_link_closed_callback(self._link_closed)
|
||||||
|
|
||||||
|
self.signal(Signalling.STATUS_AVAILABLE, link)
|
||||||
|
|
||||||
|
def _caller_identified(self, link, identity):
|
||||||
|
participant = Participant(link, identity)
|
||||||
|
|
||||||
|
self.signal(Signalling.STATUS_RINGING, link)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self.signal(Signalling.STATUS_CONNECTING, link)
|
||||||
|
self.voice_channel.add_participant(participant, relay=self)
|
||||||
|
self.signal(Signalling.STATUS_ESTABLISHED, link)
|
||||||
|
|
||||||
|
RNS.log(
|
||||||
|
f"Call established on channel '{self.name}' with "
|
||||||
|
f"{RNS.prettyhexrep(identity.hash)}, "
|
||||||
|
f"{self.voice_channel.get_participant_count()} participant(s)",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _link_closed(self, link):
|
||||||
|
self.voice_channel.remove_participant(link.link_id)
|
||||||
|
|
||||||
|
# -- SignallingReceiver -------------------------------------------
|
||||||
|
|
||||||
|
def signalling_received(self, signals, source):
|
||||||
|
for signal in signals:
|
||||||
|
if signal >= Signalling.PREFERRED_PROFILE:
|
||||||
|
profile = signal - Signalling.PREFERRED_PROFILE
|
||||||
|
RNS.log(
|
||||||
|
f"Channel '{self.name}' got preferred profile "
|
||||||
|
f"0x{profile:02x}",
|
||||||
|
RNS.LOG_DEBUG,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Public API ---------------------------------------------------
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
self.destination.announce()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
for p in list(self.voice_channel.participants.values()):
|
||||||
|
p.ended = True
|
||||||
|
if p.conf_source:
|
||||||
|
p.conf_source.stop()
|
||||||
|
if p.link_source:
|
||||||
|
p.link_source.stop()
|
||||||
|
if p.link and p.link.status == RNS.Link.ACTIVE:
|
||||||
|
p.link.teardown()
|
||||||
|
self.voice_channel.participants.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Channel manager – persists channels to disk
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
class ChannelManager:
|
||||||
|
"""Creates, loads, and tracks ``ChannelNode`` instances."""
|
||||||
|
|
||||||
|
def __init__(self, storage_dir):
|
||||||
|
self.channels_dir = os.path.join(storage_dir, "channels")
|
||||||
|
os.makedirs(self.channels_dir, exist_ok=True)
|
||||||
|
self.channels = {} # name -> ChannelNode
|
||||||
|
|
||||||
|
# -- Creation ------------------------------------------------------
|
||||||
|
|
||||||
|
def create_channel(self, name, creator_identity_hash):
|
||||||
|
"""Create a new channel with its own identity.
|
||||||
|
|
||||||
|
Returns ``(ChannelNode, None)`` on success or
|
||||||
|
``(None, error_message)`` on failure.
|
||||||
|
"""
|
||||||
|
if name in self.channels:
|
||||||
|
return None, f"Channel '{name}' already exists"
|
||||||
|
|
||||||
|
if not name or "/" in name or ".." in name:
|
||||||
|
return None, "Invalid channel name"
|
||||||
|
|
||||||
|
chan_dir = os.path.join(self.channels_dir, name)
|
||||||
|
try:
|
||||||
|
os.makedirs(chan_dir, exist_ok=False)
|
||||||
|
except FileExistsError:
|
||||||
|
return None, f"Channel '{name}' already exists"
|
||||||
|
|
||||||
|
identity = RNS.Identity()
|
||||||
|
identity.to_file(os.path.join(chan_dir, "identity"))
|
||||||
|
|
||||||
|
admins = {creator_identity_hash}
|
||||||
|
with open(os.path.join(chan_dir, "admins"), "w") as f:
|
||||||
|
f.write(RNS.hexrep(creator_identity_hash, delimit=False) + "\n")
|
||||||
|
|
||||||
|
node = ChannelNode(identity, name, admins)
|
||||||
|
self.channels[name] = node
|
||||||
|
node.announce()
|
||||||
|
|
||||||
|
RNS.log(
|
||||||
|
f"Created channel '{name}' with identity "
|
||||||
|
f"{RNS.hexrep(identity.hash, delimit=False)}",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
return node, None
|
||||||
|
|
||||||
|
# -- Persistence ---------------------------------------------------
|
||||||
|
|
||||||
|
def load_all(self):
|
||||||
|
"""Restore channels from disk (called at startup)."""
|
||||||
|
if not os.path.isdir(self.channels_dir):
|
||||||
|
return
|
||||||
|
for entry in sorted(os.listdir(self.channels_dir)):
|
||||||
|
chan_dir = os.path.join(self.channels_dir, entry)
|
||||||
|
if not os.path.isdir(chan_dir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
identity_path = os.path.join(chan_dir, "identity")
|
||||||
|
admin_path = os.path.join(chan_dir, "admins")
|
||||||
|
if not os.path.isfile(identity_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
identity = RNS.Identity.from_file(identity_path)
|
||||||
|
if identity is None:
|
||||||
|
RNS.log(
|
||||||
|
f"Could not load identity for channel '{entry}', "
|
||||||
|
f"skipping",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
admins = set()
|
||||||
|
if os.path.isfile(admin_path):
|
||||||
|
with open(admin_path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
admins.add(bytes.fromhex(line))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
node = ChannelNode(identity, entry, admins)
|
||||||
|
self.channels[entry] = node
|
||||||
|
RNS.log(
|
||||||
|
f"Restored channel '{entry}' "
|
||||||
|
f"({RNS.hexrep(identity.hash, delimit=False)})",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Queries -------------------------------------------------------
|
||||||
|
|
||||||
|
def is_admin(self, channel_name, identity_hash):
|
||||||
|
node = self.channels.get(channel_name)
|
||||||
|
if node is None:
|
||||||
|
return False
|
||||||
|
return identity_hash in node.admins
|
||||||
|
|
||||||
|
def is_admin_of_any(self, identity_hash):
|
||||||
|
for node in self.channels.values():
|
||||||
|
if identity_hash in node.admins:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# -- Cleanup -------------------------------------------------------
|
||||||
|
|
||||||
|
def cleanup_all(self):
|
||||||
|
for node in self.channels.values():
|
||||||
|
node.cleanup()
|
||||||
|
self.channels.clear()
|
||||||
|
|
||||||
|
def announce_all(self):
|
||||||
|
for node in self.channels.values():
|
||||||
|
node.announce()
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Main relay – telephony + LXMF bot
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
class LXSTRelay(SignallingReceiver):
|
||||||
|
"""Conference bridge with LXMF bot for channel management.
|
||||||
|
|
||||||
|
Registers two destinations on the Reticulum network:
|
||||||
|
|
||||||
|
* ``lxst.telephony`` – the default / lobby channel that anyone
|
||||||
|
can call directly.
|
||||||
|
* ``lxmf.delivery`` – an LXMF bot that responds to
|
||||||
|
``/create_channel <name>``, creating a fresh Reticulum node for
|
||||||
|
each channel and assigning the creator as admin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -253,6 +482,7 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
|
|
||||||
os.makedirs(self.configdir, exist_ok=True)
|
os.makedirs(self.configdir, exist_ok=True)
|
||||||
|
|
||||||
|
# ---- Identity ----
|
||||||
identity_path = os.path.join(self.configdir, "identity")
|
identity_path = os.path.join(self.configdir, "identity")
|
||||||
if os.path.isfile(identity_path):
|
if os.path.isfile(identity_path):
|
||||||
self.identity = RNS.Identity.from_file(identity_path)
|
self.identity = RNS.Identity.from_file(identity_path)
|
||||||
@@ -277,6 +507,7 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
configdir=rnsconfigdir, loglevel=3 + verbosity
|
configdir=rnsconfigdir, loglevel=3 + verbosity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---- Default telephony destination (lobby) ----
|
||||||
self.destination = RNS.Destination(
|
self.destination = RNS.Destination(
|
||||||
self.identity,
|
self.identity,
|
||||||
RNS.Destination.IN,
|
RNS.Destination.IN,
|
||||||
@@ -291,15 +522,49 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
|
|
||||||
self.channel = VoiceChannel(name=channel_name)
|
self.channel = VoiceChannel(name=channel_name)
|
||||||
|
|
||||||
|
# ---- LXMF bot ----
|
||||||
|
self.lxmf_storage = os.path.join(self.configdir, "lxmf")
|
||||||
|
os.makedirs(self.lxmf_storage, exist_ok=True)
|
||||||
|
|
||||||
|
self.lxmf_router = LXMF.LXMRouter(
|
||||||
|
identity=self.identity,
|
||||||
|
storagepath=self.lxmf_storage,
|
||||||
|
)
|
||||||
|
self.bot_destination = self.lxmf_router.register_delivery_identity(
|
||||||
|
self.identity,
|
||||||
|
display_name="LXST Channel Bot",
|
||||||
|
)
|
||||||
|
self.lxmf_router.register_delivery_callback(
|
||||||
|
self._lxmf_delivery_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Channel manager (dynamic nodes) ----
|
||||||
|
self.channel_manager = ChannelManager(self.configdir)
|
||||||
|
self.channel_manager.load_all()
|
||||||
|
|
||||||
|
# ---- Startup announce ----
|
||||||
|
self.destination.announce()
|
||||||
|
self.channel_manager.announce_all()
|
||||||
|
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Relay listening on "
|
f"Relay lobby: {RNS.prettyhexrep(self.destination.hash)}",
|
||||||
f"{RNS.prettyhexrep(self.destination.hash)}",
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
RNS.log(
|
||||||
|
f"Bot (LXMF): {RNS.prettyhexrep(self.bot_destination.hash)}",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
if self.channel_manager.channels:
|
||||||
|
for name, node in self.channel_manager.channels.items():
|
||||||
|
RNS.log(
|
||||||
|
f"Channel '{name}': "
|
||||||
|
f"{RNS.prettyhexrep(node.destination.hash)}",
|
||||||
RNS.LOG_NOTICE,
|
RNS.LOG_NOTICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
# Link lifecycle
|
# Telephony – lobby channel
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
|
|
||||||
def _incoming_link_established(self, link):
|
def _incoming_link_established(self, link):
|
||||||
link.is_incoming = True
|
link.is_incoming = True
|
||||||
@@ -312,11 +577,12 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
|
|
||||||
self.signal(Signalling.STATUS_AVAILABLE, link)
|
self.signal(Signalling.STATUS_AVAILABLE, link)
|
||||||
|
|
||||||
RNS.log("Incoming link, awaiting identification", RNS.LOG_DEBUG)
|
RNS.log("Incoming link (lobby), awaiting identification", RNS.LOG_DEBUG)
|
||||||
|
|
||||||
def _caller_identified(self, link, identity):
|
def _caller_identified(self, link, identity):
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Caller identified: {RNS.prettyhexrep(identity.hash)}",
|
f"Caller identified in lobby: "
|
||||||
|
f"{RNS.prettyhexrep(identity.hash)}",
|
||||||
RNS.LOG_NOTICE,
|
RNS.LOG_NOTICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,41 +596,142 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
self.signal(Signalling.STATUS_ESTABLISHED, link)
|
self.signal(Signalling.STATUS_ESTABLISHED, link)
|
||||||
|
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Call established with "
|
f"Call established in lobby with "
|
||||||
f"{RNS.prettyhexrep(identity.hash)}, "
|
f"{RNS.prettyhexrep(identity.hash)}, "
|
||||||
f"{self.channel.get_participant_count()} participant(s) in "
|
f"{self.channel.get_participant_count()} participant(s)",
|
||||||
f"'{self.channel_name}'",
|
|
||||||
RNS.LOG_NOTICE,
|
RNS.LOG_NOTICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _link_closed(self, link):
|
def _link_closed(self, link):
|
||||||
RNS.log(f"Link closed: {link}", RNS.LOG_DEBUG)
|
RNS.log(f"Link closed (lobby): {link}", RNS.LOG_DEBUG)
|
||||||
self.channel.remove_participant(link.link_id)
|
self.channel.remove_participant(link.link_id)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
# SignallingReceiver
|
# LXMF bot
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
|
|
||||||
|
def _lxmf_delivery_callback(self, message):
|
||||||
|
"""Called on every incoming LXMF message addressed to the bot."""
|
||||||
|
content = message.content_as_string().strip()
|
||||||
|
sender_hash = message.source_hash
|
||||||
|
|
||||||
|
RNS.log(
|
||||||
|
f"Bot received from "
|
||||||
|
f"{RNS.hexrep(sender_hash, delimit=False)}: {content}",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
|
||||||
|
if content.startswith("/create_channel"):
|
||||||
|
self._cmd_create_channel(message, content, sender_hash)
|
||||||
|
elif content.startswith("/channels"):
|
||||||
|
self._cmd_list_channels(message)
|
||||||
|
else:
|
||||||
|
self._bot_reply(
|
||||||
|
message,
|
||||||
|
"Unknown command.\n"
|
||||||
|
"Available: /create_channel <name>, /channels",
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Commands ------------------------------------------------------
|
||||||
|
|
||||||
|
def _cmd_create_channel(self, message, content, sender_hash):
|
||||||
|
parts = content.split(maxsplit=1)
|
||||||
|
if len(parts) < 2 or not parts[1].strip():
|
||||||
|
self._bot_reply(
|
||||||
|
message,
|
||||||
|
"Usage: /create_channel <name>",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_name = parts[1].strip()
|
||||||
|
|
||||||
|
node, error = self.channel_manager.create_channel(
|
||||||
|
channel_name, sender_hash
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
self._bot_reply(message, error)
|
||||||
|
return
|
||||||
|
|
||||||
|
hash_hex = RNS.hexrep(node.destination.hash, delimit=False)
|
||||||
|
self._bot_reply(
|
||||||
|
message,
|
||||||
|
f"Channel '{channel_name}' created!\n\n"
|
||||||
|
f"Call identity hash:\n{hash_hex}\n\n"
|
||||||
|
f"Share this hash for others to join.\n"
|
||||||
|
f"You are the admin of this channel.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cmd_list_channels(self, message):
|
||||||
|
if not self.channel_manager.channels:
|
||||||
|
self._bot_reply(message, "No channels created yet.")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = ["Active channels:"]
|
||||||
|
for name, node in self.channel_manager.channels.items():
|
||||||
|
hash_hex = RNS.hexrep(node.destination.hash, delimit=False)
|
||||||
|
lines.append(f" {name} → {hash_hex}")
|
||||||
|
self._bot_reply(message, "\n".join(lines))
|
||||||
|
|
||||||
|
# -- Send reply ----------------------------------------------------
|
||||||
|
|
||||||
|
def _bot_reply(self, incoming_msg, text):
|
||||||
|
"""Send a reply to the author of *incoming_msg*."""
|
||||||
|
try:
|
||||||
|
sender_identity = RNS.Identity.recall(incoming_msg.source_hash)
|
||||||
|
except Exception:
|
||||||
|
sender_identity = None
|
||||||
|
|
||||||
|
if sender_identity is None:
|
||||||
|
RNS.log(
|
||||||
|
"Cannot reply – sender identity not cached",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
dest = RNS.Destination(
|
||||||
|
sender_identity,
|
||||||
|
RNS.Destination.OUT,
|
||||||
|
RNS.Destination.SINGLE,
|
||||||
|
"lxmf",
|
||||||
|
"delivery",
|
||||||
|
)
|
||||||
|
|
||||||
|
reply = LXMF.LXMessage(
|
||||||
|
dest,
|
||||||
|
self.bot_destination,
|
||||||
|
text,
|
||||||
|
desired_method=LXMF.LXMessage.DIRECT,
|
||||||
|
)
|
||||||
|
self.lxmf_router.handle_outbound(reply)
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# SignallingReceiver (lobby)
|
||||||
|
# ==================================================================
|
||||||
|
|
||||||
def signalling_received(self, signals, source):
|
def signalling_received(self, signals, source):
|
||||||
for signal in signals:
|
for signal in signals:
|
||||||
if signal >= Signalling.PREFERRED_PROFILE:
|
if signal >= Signalling.PREFERRED_PROFILE:
|
||||||
profile = signal - Signalling.PREFERRED_PROFILE
|
profile = signal - Signalling.PREFERRED_PROFILE
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Received preferred profile: 0x{profile:02x}",
|
f"Lobby got preferred profile: 0x{profile:02x}",
|
||||||
RNS.LOG_DEBUG,
|
RNS.LOG_DEBUG,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Received signal: 0x{signal:02x}",
|
f"Lobby got signal: 0x{signal:02x}",
|
||||||
RNS.LOG_DEBUG,
|
RNS.LOG_DEBUG,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
# Public API
|
# Public API
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
|
|
||||||
def announce(self):
|
def announce(self):
|
||||||
self.destination.announce()
|
self.destination.announce()
|
||||||
|
self.channel_manager.announce_all()
|
||||||
RNS.log("Relay announced on network", RNS.LOG_NOTICE)
|
RNS.log("Relay announced on network", RNS.LOG_NOTICE)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -374,7 +741,6 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
signal_module.signal(signal_module.SIGTERM, self._sigterm_handler)
|
signal_module.signal(signal_module.SIGTERM, self._sigterm_handler)
|
||||||
|
|
||||||
self.should_run = True
|
self.should_run = True
|
||||||
self.announce()
|
|
||||||
|
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"LXST Relay ready on channel '{self.channel_name}'",
|
f"LXST Relay ready on channel '{self.channel_name}'",
|
||||||
@@ -384,18 +750,28 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
f"Identity: {RNS.prettyhexrep(self.identity.hash)}",
|
f"Identity: {RNS.prettyhexrep(self.identity.hash)}",
|
||||||
RNS.LOG_NOTICE,
|
RNS.LOG_NOTICE,
|
||||||
)
|
)
|
||||||
RNS.log("Waiting for incoming calls...", RNS.LOG_NOTICE)
|
RNS.log(
|
||||||
|
f"Bot at: {RNS.prettyhexrep(self.bot_destination.hash)}",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
RNS.log(
|
||||||
|
"Message the bot (via NomadNet / LXMF) with "
|
||||||
|
"/create_channel <name> to create a new channel",
|
||||||
|
RNS.LOG_NOTICE,
|
||||||
|
)
|
||||||
|
RNS.log("Waiting for calls and messages...", RNS.LOG_NOTICE)
|
||||||
|
|
||||||
while self.should_run:
|
while self.should_run:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.should_run = False
|
self.should_run = False
|
||||||
RNS.log("Relay stopping...", RNS.LOG_NOTICE)
|
self.channel_manager.cleanup_all()
|
||||||
|
RNS.log("Relay stopped.", RNS.LOG_NOTICE)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
# Signal handlers
|
# Signal handlers
|
||||||
# ------------------------------------------------------------------
|
# ==================================================================
|
||||||
|
|
||||||
def _sigint_handler(self, sig, frame):
|
def _sigint_handler(self, sig, frame):
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -404,11 +780,15 @@ class LXSTRelay(SignallingReceiver):
|
|||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Entry point
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=(
|
description=(
|
||||||
"LXST Relay - Group voice channel bridge for "
|
"LXST Relay – Group voice channel bridge for Reticulum "
|
||||||
"Reticulum Telephony"
|
"Telephony with NomadNet LXMF bot"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -429,13 +809,13 @@ def main():
|
|||||||
"--channel",
|
"--channel",
|
||||||
"-n",
|
"-n",
|
||||||
default="default",
|
default="default",
|
||||||
help="voice channel name (default: 'default')",
|
help="lobby channel name (default: 'default')",
|
||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--version",
|
"--version",
|
||||||
action="version",
|
action="version",
|
||||||
version="lxst-relay 0.1.0",
|
version="lxst-relay 0.2.0",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v", "--verbose", action="count", default=0
|
"-v", "--verbose", action="count", default=0
|
||||||
|
|||||||
Reference in New Issue
Block a user