From 4ceac515f0e2d64b6c7bded4e8618cc5b7715a7f Mon Sep 17 00:00:00 2001 From: Gabby Morgan Date: Fri, 29 May 2026 20:32:52 -0500 Subject: [PATCH] init --- __pycache__/client.cpython-314.pyc | Bin 0 -> 20270 bytes bridge_whitelist.json | 3 + client.py | 399 +++++++++++++++++++++++++++++ lxst-client | 2 + 4 files changed, 404 insertions(+) create mode 100644 __pycache__/client.cpython-314.pyc create mode 100644 bridge_whitelist.json create mode 100644 client.py create mode 100755 lxst-client diff --git a/__pycache__/client.cpython-314.pyc b/__pycache__/client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72995cca94e350f8fb0163194b01bf2481d0cfa6 GIT binary patch literal 20270 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 len(self.channels): + print("Invalid channel index") + return + + channel = self.channels[index] + dest_hash_hex = channel["hash"] + dest_hash = bytes.fromhex(dest_hash_hex) + + print( + f"\nConnecting to channel {index + 1}: " + f"{dest_hash_hex[:16]}..." + ) + + if not RNS.Transport.has_path(dest_hash): + RNS.Transport.request_path(dest_hash) + print(" Requesting path...", end="", flush=True) + for _ in range(100): + if RNS.Transport.has_path(dest_hash): + break + time.sleep(0.1) + print() + + if not RNS.Transport.has_path(dest_hash): + print(" Could not find path to channel") + return + + hops = RNS.Transport.hops_to(dest_hash) + print(f" Path found ({hops} hop{'s' if hops != 1 else ''})") + + identity = RNS.Identity.recall(dest_hash) + if identity is None: + print(" Could not discover channel identity") + return + + self._init_telephone() + self.state = self.STATE_CALLING + self.telephone.call(identity) + + def show_channels(self): + if not self.channels: + print("No channels discovered. Run 'discover' first.") + return + + print( + f"\n{Terminal.BOLD}" + f"Available channels ({len(self.channels)}):" + f"{Terminal.END}" + ) + print(f" {'#':<4} {'Destination Hash':<48} {'Relay':<16}") + print(f" {'-'*4} {'-'*48} {'-'*16}") + for i, ch in enumerate(self.channels): + relay_short = ( + ch["relay"][:14] + ".." + if len(ch["relay"]) > 16 + else ch["relay"] + ) + print(f" {i + 1:<4} {ch['hash']:<48} {relay_short}") + + def show_help(self): + print("") + print(f"{Terminal.BOLD}Commands:{Terminal.END}") + print(f" Call the channel at that number") + print(f" d Discover channels (query relays)") + print(f" l List channels") + print(f" h / ? Show this help") + print(f" q Quit") + + def run(self): + signal.signal(signal.SIGINT, self._sigint_handler) + signal.signal(signal.SIGTERM, self._sigterm_handler) + self.should_run = True + + print( + f"\n{Terminal.BOLD}" + f"LXST Channel Client" + f"{Terminal.END}" + ) + print(f" Identity: {RNS.prettyhexrep(self.identity.hash)}") + print(f" Whitelist: {self.whitelist_path}") + print() + + self.discover(timeout=15) + self.show_channels() + self.show_help() + + while self.should_run: + if self.state == self.STATE_IN_CALL: + try: + input(" In call, press Enter to hang up > ") + self.telephone.hangup() + self.state = self.STATE_READY + except (EOFError, KeyboardInterrupt): + break + continue + + if self.state == self.STATE_CALLING: + time.sleep(0.5) + continue + + if self.state == self.STATE_DISCOVERING: + time.sleep(0.5) + continue + + try: + inp = input("> ").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + break + + if not inp: + continue + + if inp == "q": + break + elif inp in ("h", "?"): + self.show_help() + elif inp == "d": + self.discover() + self.show_channels() + elif inp == "l": + self.show_channels() + else: + try: + idx = int(inp) - 1 + if 0 <= idx < len(self.channels): + self.call_channel(idx) + else: + print( + f"Invalid index. " + f"Enter a number between 1 and {len(self.channels)}" + ) + except ValueError: + print(f"Unknown command: {inp}. Type 'h' for help.") + + def cleanup(self): + if self.telephone and self.state == self.STATE_IN_CALL: + self.telephone.hangup() + self.should_run = False + + def _sigint_handler(self, sig, frame): + self.cleanup() + sys.exit(0) + + def _sigterm_handler(self, sig, frame): + self.cleanup() + sys.exit(0) + + +def main(): + parser = argparse.ArgumentParser(description="LXST Channel Client") + parser.add_argument( + "--config", "-c", default=None, help="config directory", + ) + parser.add_argument( + "--rnsconfig", default=None, + help="path to alternative Reticulum config directory", + ) + parser.add_argument( + "--whitelist", "-w", default=None, + help="path to bridge_whitelist.json", + ) + parser.add_argument( + "--version", + action="version", + version="lxst-client 0.1.0", + ) + parser.add_argument("-v", "--verbose", action="count", default=0) + + args = parser.parse_args() + + client = ChannelClient( + configdir=args.config, + rnsconfigdir=args.rnsconfig, + whitelist_path=args.whitelist, + verbosity=args.verbose, + ) + + try: + client.run() + except KeyboardInterrupt: + client.cleanup() + print() + + +if __name__ == "__main__": + main() diff --git a/lxst-client b/lxst-client new file mode 100755 index 0000000..bd9327a --- /dev/null +++ b/lxst-client @@ -0,0 +1,2 @@ +#!/bin/sh +exec python3 "$(dirname "$0")/client.py" "$@"