- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
178 lines
6.0 KiB
Plaintext
Executable File
178 lines
6.0 KiB
Plaintext
Executable File
# chatcli — uTun headless chat CLI client
|
|
#
|
|
# Usage:
|
|
# chatcli [--host HOST] [--port PORT] <command> [args...]
|
|
#
|
|
# Environment:
|
|
# CHATCLI_HOST — default host (default: 127.0.0.1)
|
|
# CHATCLI_PORT — default port (default: 9999)
|
|
#
|
|
# Commands (see chatcli_commands.txt for full description):
|
|
# ping alive check
|
|
# status node info, connections, NTP
|
|
# channels list all channels
|
|
# members <ch_id> list members with full state
|
|
# messages <ch_id> [count] read last messages
|
|
# send <ch_id> <text> send text message
|
|
# invite <ch_id> create invite link
|
|
# connect <utun://...> join channel via invite link
|
|
# create <name> create new channel
|
|
# listen interactive event listener
|
|
|
|
import sys, os, json, socket, struct
|
|
|
|
DEFAULT_HOST = os.environ.get("CHATCLI_HOST", "127.0.0.1")
|
|
DEFAULT_PORT = int(os.environ.get("CHATCLI_PORT", "9999"))
|
|
TIMEOUT = 3
|
|
|
|
def _connect():
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(TIMEOUT)
|
|
try:
|
|
s.connect((DEFAULT_HOST, DEFAULT_PORT))
|
|
except (socket.timeout, ConnectionRefusedError, OSError) as e:
|
|
print(f"ERROR: cannot connect to {DEFAULT_HOST}:{DEFAULT_PORT} — {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
return s
|
|
|
|
def _readline(s):
|
|
buf = b""
|
|
while True:
|
|
try:
|
|
ch = s.recv(1)
|
|
except socket.timeout:
|
|
break
|
|
if not ch:
|
|
break
|
|
if ch == b"\n":
|
|
break
|
|
buf += ch
|
|
return buf.decode("utf-8", errors="replace")
|
|
|
|
def _send(s, js):
|
|
s.sendall(js.encode())
|
|
|
|
def _req(cmd, **params):
|
|
s = _connect()
|
|
js = {"id": 1, "cmd": cmd}
|
|
js.update(params)
|
|
_send(s, json.dumps(js, ensure_ascii=True) + "\n")
|
|
resp = _readline(s)
|
|
s.close()
|
|
try:
|
|
obj = json.loads(resp)
|
|
except json.JSONDecodeError:
|
|
print(f"RAW: {resp}")
|
|
return
|
|
if obj.get("ok"):
|
|
data = obj.get("data", {})
|
|
if isinstance(data, str):
|
|
print(data)
|
|
else:
|
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
else:
|
|
print(f"ERROR: {obj.get('error', 'unknown')}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def cmd_ping():
|
|
_req("ping")
|
|
|
|
def cmd_status():
|
|
_req("status")
|
|
|
|
def cmd_channels():
|
|
_req("channels")
|
|
|
|
def cmd_members(ch_id):
|
|
_req("members", ch=ch_id)
|
|
|
|
def cmd_messages(ch_id, count=None):
|
|
params = {"ch": ch_id}
|
|
if count:
|
|
params["count"] = int(count)
|
|
_req("messages", **params)
|
|
|
|
def cmd_send(ch_id, text):
|
|
_req("send", ch=ch_id, content_type="text", data=text)
|
|
|
|
def cmd_invite(ch_id):
|
|
_req("invite", ch=ch_id)
|
|
|
|
def cmd_connect(link):
|
|
_req("connect", link=link)
|
|
|
|
def cmd_create(name):
|
|
_req("create_channel", name=name)
|
|
|
|
def cmd_listen():
|
|
print(f"Listening on {DEFAULT_HOST}:{DEFAULT_PORT} (Ctrl+C to quit)")
|
|
s = _connect()
|
|
_send(s, json.dumps({"id": 1, "cmd": "subscribe", "enable": 1}) + "\n")
|
|
try:
|
|
while True:
|
|
line = _readline(s)
|
|
if not line:
|
|
break
|
|
try:
|
|
obj = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
print(line)
|
|
continue
|
|
evt = obj.get("event", "")
|
|
if evt == "msg":
|
|
print(f"[{obj.get('ch','?')}] {obj.get('author_id','?')}: new message")
|
|
elif evt == "members_changed":
|
|
print(f"[{obj.get('ch','?')}] members changed")
|
|
elif evt == "channel_updated":
|
|
print(f"[{obj.get('ch','?')}] channel updated")
|
|
elif evt == "invite_received":
|
|
print(f"INVITE: ch={obj.get('ch','?')} name={obj.get('ch_name','?')} from={obj.get('from_id','?')}")
|
|
else:
|
|
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
|
except KeyboardInterrupt:
|
|
print("\nDisconnected.")
|
|
finally:
|
|
s.close()
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
i = 0
|
|
while i < len(args):
|
|
if args[i] == "--host" and i + 1 < len(args):
|
|
global DEFAULT_HOST; DEFAULT_HOST = args[i + 1]; i += 2
|
|
elif args[i] == "--port" and i + 1 < len(args):
|
|
global DEFAULT_PORT; DEFAULT_PORT = int(args[i + 1]); i += 2
|
|
else:
|
|
break
|
|
args = args[i:]
|
|
|
|
if not args or args[0] in ("help", "--help", "-h"):
|
|
print(__doc__)
|
|
sys.exit(0)
|
|
|
|
cmd = args[0]
|
|
try:
|
|
if cmd == "ping": cmd_ping()
|
|
elif cmd == "status": cmd_status()
|
|
elif cmd == "channels": cmd_channels()
|
|
elif cmd == "members": cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members <ch_id>"),))
|
|
elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages <ch_id> [count]"),))
|
|
elif cmd == "send": cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send <ch_id> <text>"),))
|
|
elif cmd == "invite": cmd_invite(*args[1:2] if len(args) > 1 else (_die("usage: invite <ch_id>"),))
|
|
elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
|
|
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
|
|
elif cmd == "listen": cmd_listen()
|
|
else: print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
|
|
except socket.timeout:
|
|
print(f"ERROR: timeout ({TIMEOUT}s) connecting to {DEFAULT_HOST}:{DEFAULT_PORT}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except ConnectionRefusedError:
|
|
print(f"ERROR: connection refused at {DEFAULT_HOST}:{DEFAULT_PORT} — is uTun running with headless_control_bind?", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def _die(msg):
|
|
print(msg, file=sys.stderr); sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|