mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 11:29:50 +00:00
* feat: add simplex-support-bot-light * feat: package simplex-support-bot-light for docker * docs: document simplex-support-bot-light * fix: make a missing config actionable and bounded * docs: note the attached compose Ctrl+C behaviour * refactor: use the library for startup and custom data * refactor: drop the display-name workaround * style: shorten the startup comment * build: build the core and the library into the image * docs: correct where the image type rule comes from * fix: log what the core said a command got wrong * build: run the container as the operator's uid * docs: keep the container uid in .env * docs: tighten the README
225 lines
8.6 KiB
Python
225 lines
8.6 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
from support_bot_light import commands, setup
|
|
from support_bot_light.config import Config
|
|
from tests.conftest import ROSTER_GROUP_ID, USER_ID, make_group
|
|
|
|
CONFIG = Config("Support", "./x", "hi", "Invite roster", "owner")
|
|
MARKER = {"supportBotLight": {"group": "roster"}}
|
|
|
|
|
|
async def test_creates_group_when_none_marked(api, caplog):
|
|
caplog.set_level(logging.INFO)
|
|
group_id = await setup.ensure_roster_group(api, USER_ID, CONFIG)
|
|
assert group_id == ROSTER_GROUP_ID
|
|
profile = api.new_groups[0]
|
|
assert profile["displayName"] == "Invite roster"
|
|
assert profile["groupPreferences"]["directMessages"] == {"enable": "on"}
|
|
assert profile["groupPreferences"]["commands"] == commands.to_wire(commands.COMMANDS)
|
|
assert api.group_custom_data == [(ROSTER_GROUP_ID, MARKER)]
|
|
assert api.links == [ROSTER_GROUP_ID] # created exactly once
|
|
assert api.group_links[ROSTER_GROUP_ID] in caplog.text
|
|
|
|
|
|
async def test_finds_existing_group_by_marker(api):
|
|
profile = {
|
|
"displayName": "renamed by a human",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "on"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
api.group_links[77] = "https://simplex.chat/contact#/?v=2&group=77"
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == 77
|
|
assert api.new_groups == [] # not recreated
|
|
assert api.profile_updates == [] # commands already match — no broadcast
|
|
|
|
|
|
async def test_found_group_with_link_logs_it_without_recreating(api, caplog):
|
|
caplog.set_level(logging.INFO)
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "on"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
api.group_links[77] = "https://simplex.chat/contact#/?v=2&group=77"
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == 77
|
|
assert api.links == [] # fetched, not recreated
|
|
assert "https://simplex.chat/contact#/?v=2&group=77" in caplog.text
|
|
|
|
|
|
async def test_found_group_without_link_recreates_and_logs_it(api, caplog):
|
|
# Crash window between marking the group and creating its link.
|
|
caplog.set_level(logging.INFO)
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "on"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == 77
|
|
assert api.links == [77] # recovered by creating a new link
|
|
assert api.group_links[77] in caplog.text
|
|
|
|
|
|
async def test_group_link_get_failure_with_link_present_does_not_block_startup(api, caplog):
|
|
# The create fallback hits the unique link index. A missing link must not
|
|
# stop the bot starting.
|
|
caplog.set_level(logging.WARNING)
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "on"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
api.group_links[77] = "https://simplex.chat/contact#/?v=2&group=77"
|
|
api.fail_on.add("api_get_group_link_str")
|
|
api.fail_on.add("api_create_group_link")
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == 77
|
|
|
|
|
|
async def test_pushes_commands_when_they_differ(api):
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {"directMessages": {"enable": "on"}, "commands": []},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
await setup.ensure_roster_group(api, USER_ID, CONFIG)
|
|
assert len(api.profile_updates) == 1
|
|
group_id, sent = api.profile_updates[0]
|
|
assert group_id == 77
|
|
assert sent["groupPreferences"]["commands"] == commands.to_wire(commands.COMMANDS)
|
|
|
|
|
|
async def test_pushed_profile_keeps_existing_display_name(api):
|
|
profile = {
|
|
"displayName": "renamed by a human",
|
|
"fullName": "",
|
|
"groupPreferences": {"directMessages": {"enable": "on"}, "commands": []},
|
|
}
|
|
api.groups.append(make_group(77, profile, custom_data=MARKER))
|
|
await setup.ensure_roster_group(api, USER_ID, CONFIG)
|
|
_, sent = api.profile_updates[0]
|
|
# Syncing commands must not silently rename a group the operator renamed.
|
|
assert sent["displayName"] == "renamed by a human"
|
|
|
|
|
|
async def test_ignores_groups_without_the_marker(api):
|
|
api.groups.append(make_group(88, {"displayName": "Invite roster", "fullName": ""}))
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == ROSTER_GROUP_ID
|
|
assert api.new_groups != [] # name match alone must not be trusted
|
|
|
|
|
|
async def test_ignores_groups_with_a_foreign_marker(api):
|
|
api.groups.append(
|
|
make_group(
|
|
88, {"displayName": "x", "fullName": ""}, custom_data={"otherBot": {"group": "roster"}}
|
|
)
|
|
)
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == ROSTER_GROUP_ID
|
|
assert api.new_groups != []
|
|
|
|
|
|
async def test_ignores_groups_with_the_wrong_marker_value(api):
|
|
# Right namespace, wrong marker.
|
|
api.groups.append(
|
|
make_group(
|
|
88,
|
|
{"displayName": "x", "fullName": ""},
|
|
custom_data={"supportBotLight": {"group": "archive"}},
|
|
)
|
|
)
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == ROSTER_GROUP_ID
|
|
assert api.new_groups != []
|
|
|
|
|
|
async def test_ignores_groups_with_a_non_dict_marker(api):
|
|
api.groups.append(
|
|
make_group(
|
|
88, {"displayName": "x", "fullName": ""}, custom_data={"supportBotLight": "roster"}
|
|
)
|
|
)
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == ROSTER_GROUP_ID
|
|
|
|
|
|
async def test_warns_and_picks_one_when_two_groups_are_marked(api, caplog):
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "on"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups += [
|
|
make_group(20, profile, custom_data=MARKER),
|
|
make_group(21, profile, custom_data=MARKER),
|
|
]
|
|
api.group_links[20] = "https://simplex.chat/#g20"
|
|
with caplog.at_level("WARNING"):
|
|
assert await setup.ensure_roster_group(api, USER_ID, CONFIG) == 20
|
|
assert "2 groups carry the roster marker" in caplog.text
|
|
assert api.new_groups == []
|
|
|
|
|
|
async def test_a_group_the_bot_has_left_is_not_reused(api):
|
|
# Nothing would ever be delivered there, and the marker would keep a
|
|
# replacement from being created.
|
|
api.groups.append(
|
|
make_group(
|
|
77,
|
|
{"displayName": "old roster", "fullName": ""},
|
|
{"supportBotLight": {"group": "roster"}},
|
|
membership_status="removed",
|
|
)
|
|
)
|
|
group_id = await setup.ensure_roster_group(api, USER_ID, CONFIG)
|
|
assert group_id != 77
|
|
assert api.new_groups # a live roster group was created instead
|
|
|
|
|
|
async def test_direct_messages_is_restored_when_an_owner_switches_it_off(api):
|
|
# api_create_member_contact fails without it, so /dm would fail forever with
|
|
# nothing to explain it.
|
|
profile = {
|
|
"displayName": "Invite roster",
|
|
"fullName": "",
|
|
"groupPreferences": {
|
|
"directMessages": {"enable": "off"},
|
|
"commands": commands.to_wire(commands.COMMANDS),
|
|
},
|
|
}
|
|
api.groups.append(make_group(77, profile, {"supportBotLight": {"group": "roster"}}))
|
|
await setup.ensure_roster_group(api, USER_ID, CONFIG)
|
|
pushed = api.profile_updates[-1][1]["groupPreferences"]
|
|
assert pushed["directMessages"] == {"enable": "on"}
|
|
assert pushed["commands"] == commands.to_wire(commands.COMMANDS)
|
|
|
|
|
|
async def test_a_profile_push_that_cannot_return_does_not_hang_startup(api, monkeypatch):
|
|
# The core's view queue is bounded and nothing drains it until the bot
|
|
# serves, so this write can block until it does.
|
|
monkeypatch.setattr(setup, "PROFILE_PUSH_TIMEOUT", 0.05)
|
|
|
|
async def never_returns(group_id, profile):
|
|
await asyncio.sleep(10)
|
|
|
|
monkeypatch.setattr(api, "api_update_group_profile", never_returns)
|
|
api.groups.append(make_group(77, {"displayName": "r", "fullName": ""}, MARKER))
|
|
group_id = await asyncio.wait_for(setup.ensure_roster_group(api, USER_ID, CONFIG), 2)
|
|
assert group_id == 77
|