mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-15 16:45:46 +00:00
e63c403623
* docs: simplex-chat-python design and implementation plan * bots: Python wire types codegen * simplex-chat-python: package scaffold * simplex-chat-python: native libsimplex loader * simplex-chat-python: async FFI wrappers * simplex-chat-python: ChatApi with 49 api methods * simplex-chat-python: Bot class with decorators and dispatch * simplex-chat-python: install CLI, example bot, README * simplex-chat-python: audit fixes * bots: regenerate API docs and types Catches up the markdown, TypeScript and Python codegen outputs with two upstream schema changes: - APIConnectPlan.connectionLink became optional (from sh/python-lib audit fixes); cmdString and EBNF syntax now reflect optional parameter. - APIAddGroupRelays command and CRGroupRelaysAdded/CRGroupRelaysAddFailed responses added in #6917 (relay management). The TS and markdown outputs were regenerated when #6917 landed but the Python types module only got the new entries with this regeneration. * core: refresh SQLite query plans after relay_inactive_at migration The M20260507_relay_inactive_at migration (#6917 / #6952) shifted the query plans that 'Save query plans' verifies. Regenerated via the test that owns those snapshots; no behavioral change. * bots: keep APIConnectPlan connectionLink as required parameter The prior audit-fixes commit changed the syntax expression to `Optional ...` because the Haskell field is `connectionLink :: Maybe AConnectionLink`. That misrepresents the API contract: the `Maybe` is purely an internal signal for link-parsing failure (the handler returns `CEInvalidConnReq` on `Nothing`), not API-level optionality. Callers MUST always pass a connection link. Revert the syntax expression to `Param "connectionLink"` and add a comment so the intent is preserved next time someone audits. Regenerates COMMANDS.md, commands.ts and _commands.py to match.
176 lines
5.5 KiB
Python
176 lines
5.5 KiB
Python
from simplex_chat import util
|
|
|
|
|
|
def test_chat_info_ref_direct():
|
|
ci = {"type": "direct", "contact": {"contactId": 7}}
|
|
assert util.chat_info_ref(ci) == {"chatType": "direct", "chatId": 7}
|
|
|
|
|
|
def test_chat_info_ref_group():
|
|
ci = {"type": "group", "groupInfo": {"groupId": 42}}
|
|
assert util.chat_info_ref(ci) == {"chatType": "group", "chatId": 42}
|
|
|
|
|
|
def test_chat_info_ref_group_with_member_support_scope():
|
|
ci = {
|
|
"type": "group",
|
|
"groupInfo": {"groupId": 42},
|
|
"groupChatScope": {"type": "memberSupport", "groupMember_": {"groupMemberId": 99}},
|
|
}
|
|
ref = util.chat_info_ref(ci)
|
|
assert ref == {
|
|
"chatType": "group",
|
|
"chatId": 42,
|
|
"chatScope": {"type": "memberSupport", "groupMemberId_": 99},
|
|
}
|
|
|
|
|
|
def test_chat_info_ref_group_with_member_support_scope_no_member():
|
|
ci = {
|
|
"type": "group",
|
|
"groupInfo": {"groupId": 42},
|
|
"groupChatScope": {"type": "memberSupport"},
|
|
}
|
|
ref = util.chat_info_ref(ci)
|
|
# No groupMember_ → no groupMemberId_ in the wire scope.
|
|
assert ref == {
|
|
"chatType": "group",
|
|
"chatId": 42,
|
|
"chatScope": {"type": "memberSupport"},
|
|
}
|
|
|
|
|
|
def test_chat_info_ref_returns_none_for_non_targets():
|
|
assert util.chat_info_ref({"type": "contactRequest"}) is None
|
|
assert util.chat_info_ref({"type": "contactConnection"}) is None
|
|
|
|
|
|
def test_chat_info_name_direct():
|
|
ci = {"type": "direct", "contact": {"profile": {"displayName": "Alice"}}}
|
|
assert util.chat_info_name(ci) == "@Alice"
|
|
|
|
|
|
def test_chat_info_name_group():
|
|
ci = {"type": "group", "groupInfo": {"groupProfile": {"displayName": "MyGroup"}}}
|
|
assert util.chat_info_name(ci) == "#MyGroup"
|
|
|
|
|
|
def test_chat_info_name_group_with_member_support():
|
|
ci = {
|
|
"type": "group",
|
|
"groupInfo": {"groupProfile": {"displayName": "MyGroup"}},
|
|
"groupChatScope": {
|
|
"type": "memberSupport",
|
|
"groupMember_": {"memberProfile": {"displayName": "Carol"}},
|
|
},
|
|
}
|
|
assert util.chat_info_name(ci) == "#MyGroup(support Carol)"
|
|
|
|
|
|
def test_chat_info_name_local():
|
|
assert util.chat_info_name({"type": "local"}) == "private notes"
|
|
|
|
|
|
def test_chat_info_name_contact_request():
|
|
ci = {"type": "contactRequest", "contactRequest": {"profile": {"displayName": "Eve"}}}
|
|
assert util.chat_info_name(ci) == "request from @Eve"
|
|
|
|
|
|
def test_chat_info_name_contact_connection():
|
|
assert util.chat_info_name({"type": "contactConnection", "contactConnection": {}}) == (
|
|
"pending connection"
|
|
)
|
|
assert (
|
|
util.chat_info_name({"type": "contactConnection", "contactConnection": {"localAlias": "X"}})
|
|
== "pending connection (X)"
|
|
)
|
|
|
|
|
|
def test_sender_name_direct_uses_chat_name():
|
|
ci = {"type": "direct", "contact": {"profile": {"displayName": "Alice"}}}
|
|
chat_dir = {"type": "directRcv"}
|
|
assert util.sender_name(ci, chat_dir) == "@Alice"
|
|
|
|
|
|
def test_sender_name_group_appends_member():
|
|
ci = {"type": "group", "groupInfo": {"groupProfile": {"displayName": "MyGroup"}}}
|
|
chat_dir = {"type": "groupRcv", "groupMember": {"memberProfile": {"displayName": "Bob"}}}
|
|
assert util.sender_name(ci, chat_dir) == "#MyGroup @Bob"
|
|
|
|
|
|
def test_contact_address_str_prefers_short():
|
|
assert util.contact_address_str({"connFullLink": "full", "connShortLink": "short"}) == "short"
|
|
|
|
|
|
def test_contact_address_str_falls_back_to_full():
|
|
assert util.contact_address_str({"connFullLink": "full"}) == "full"
|
|
|
|
|
|
def test_from_local_profile_strips_extras_and_undefined():
|
|
local = {
|
|
"displayName": "x",
|
|
"fullName": "X Y",
|
|
"shortDescr": None,
|
|
"image": "data:image/png;base64,...",
|
|
"contactLink": None,
|
|
"preferences": {},
|
|
"peerType": "bot",
|
|
"profileId": 99, # extra LocalProfile field
|
|
"localAlias": "alias", # extra LocalProfile field
|
|
}
|
|
p = util.from_local_profile(local)
|
|
assert p == {
|
|
"displayName": "x",
|
|
"fullName": "X Y",
|
|
"image": "data:image/png;base64,...",
|
|
"preferences": {},
|
|
"peerType": "bot",
|
|
}
|
|
|
|
|
|
def test_ci_content_text_rcv():
|
|
ci = {"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": "hello"}}}
|
|
assert util.ci_content_text(ci) == "hello"
|
|
|
|
|
|
def test_ci_content_text_snd():
|
|
ci = {"content": {"type": "sndMsgContent", "msgContent": {"type": "text", "text": "world"}}}
|
|
assert util.ci_content_text(ci) == "world"
|
|
|
|
|
|
def test_ci_content_text_other():
|
|
ci = {"content": {"type": "rcvGroupEvent"}}
|
|
assert util.ci_content_text(ci) is None
|
|
|
|
|
|
def test_ci_bot_command_match():
|
|
ci = {"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": "/ping"}}}
|
|
assert util.ci_bot_command(ci) == ("ping", "")
|
|
|
|
|
|
def test_ci_bot_command_with_args():
|
|
ci = {
|
|
"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": "/echo hi "}}
|
|
}
|
|
assert util.ci_bot_command(ci) == ("echo", "hi")
|
|
|
|
|
|
def test_ci_bot_command_not_a_command():
|
|
ci = {"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": "hello"}}}
|
|
assert util.ci_bot_command(ci) is None
|
|
|
|
|
|
def test_ci_bot_command_no_text():
|
|
ci = {"content": {"type": "rcvGroupEvent"}}
|
|
assert util.ci_bot_command(ci) is None
|
|
|
|
|
|
def test_reaction_text_emoji():
|
|
r = {"chatReaction": {"reaction": {"type": "emoji", "emoji": "🎉"}}}
|
|
assert util.reaction_text(r) == "🎉"
|
|
|
|
|
|
def test_reaction_text_tag():
|
|
r = {"chatReaction": {"reaction": {"type": "unknown", "tag": "thumbs_up"}}}
|
|
assert util.reaction_text(r) == "thumbs_up"
|