Initial version
This commit is contained in:
parent
262aef0cf7
commit
bed40c8f50
0
INSTALL.md
Normal file
0
INSTALL.md
Normal file
123
src/simplex_customerservicebot.py
Normal file
123
src/simplex_customerservicebot.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
#!/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())
|
Loading…
x
Reference in New Issue
Block a user