#!/usr/bin/env python3 import asyncio import json import os import uuid import websockets __version__ = '0.1.0' # SimpleX business bot, invite a preset SimpleX user to an auto-created group # AGPL 3.0 or later. Written by Taurix IT https://www.taurix.net WS_URL = os.environ.get("SXC_WS", "ws://127.0.0.1:5225") OPERATOR_ALIAS = os.environ.get("SXC_OPERATOR_ALIAS", "@Operator") OPERATOR_ADDR = os.environ.get("SXC_OPERATOR_ADDR", "").strip() def cmd(c: str) -> str: return json.dumps({"corrId": str(uuid.uuid4()), "cmd": c}, separators=(",", ":")) def q(s: str) -> str: return '"' + s.replace('"', r'\"') + '"' # Cache per-group info groups = {} # gid -> {"name": str, "customerId": str, "invited": bool} async def ensure_operator_contact(ws): if OPERATOR_ADDR: # Idempotent: fine to call on start await ws.send(cmd(f"/c {OPERATOR_ADDR}")) def extract_business_chat(resp: dict): # Primary event in your logs if resp.get("type") == "acceptingBusinessRequest": gi = resp.get("groupInfo", {}) gname = gi.get("localDisplayName") or gi.get("groupProfile", {}).get("displayName") gid = gi.get("groupId") bchat = gi.get("businessChat") or {} cust = bchat.get("customerId") return gid, gname, cust # Fallback from the first items burst if resp.get("type") == "newChatItems": for it in resp.get("chatItems", []): ci = it.get("chatInfo", {}) if ci.get("type") == "group" and ci.get("businessChat"): gi = ci.get("groupInfo", {}) gname = gi.get("localDisplayName") or gi.get("groupProfile", {}).get("displayName") gid = gi.get("groupId") cust = ci.get("businessChat", {}).get("customerId") return gid, gname, cust return None, None, None def member_connected(resp: dict): if resp.get("type") != "newChatItems": return None, None for it in resp.get("chatItems", []): ci = it.get("chatInfo", {}) if ci.get("type") != "group": continue gid = ci.get("groupInfo", {}).get("groupId") cont = it.get("content", {}) if cont.get("type") == "rcvGroupEvent" and cont.get("rcvGroupEvent", {}).get("type") == "memberConnected": gm = it.get("chatItem", {}).get("groupMember") or {} member_id = gm.get("memberId") disp = (gm.get("memberProfile", {}) or {}).get("displayName") or gm.get("localDisplayName") or "" return gid, (member_id, disp) return None, None async def invite_operator(ws, gid: int): info = groups.get(gid) if not info or info.get("invited"): return gname = info["name"] await ws.send(cmd(f"/a {gname} {OPERATOR_ALIAS}")) await ws.send(cmd(f"#{gname} Added {OPERATOR_ALIAS} to assist.")) info["invited"] = True print(f"[invited] {OPERATOR_ALIAS} -> group {gname} (id {gid})") async def run(): while True: try: async with websockets.connect(WS_URL, max_size=None) as ws: print(f"[ok] connected {WS_URL}") await ensure_operator_contact(ws) await ws.send(cmd("/help")) async for raw in ws: # print(f"[frame] {raw}") try: msg = json.loads(raw) except Exception: continue resp = msg.get("resp") or {} rtype = resp.get("type") # 1) See a business chat -> cache it and invite operator gid, gname, cust = extract_business_chat(resp) if isinstance(gid, int) and gname and cust: if gid not in groups: groups[gid] = {"name": gname, "customerId": cust, "invited": False} print(f"[chat] business chat created gid={gid} name='{gname}' customerId={cust}") await invite_operator(ws, gid) # 2) Member joins jgid, member = member_connected(resp) if isinstance(jgid, int) and member: mem_id, disp = member info = groups.get(jgid) if info: if mem_id == info["customerId"]: print(f"[join] customer connected to gid={jgid} as '{disp}'") else: print(f"[join] non-customer joined gid={jgid}: '{disp}'") except Exception as e: print(f"[ws] {e}; retrying in 2s") await asyncio.sleep(2) if __name__ == "__main__": # env: # export SXC_WS=ws://127.0.0.1:5225 # export SXC_OPERATOR_ALIAS="Taurix-Operator" # pick a unique alias # export SXC_OPERATOR_ADDR="simplex://..." # optional asyncio.run(run())