mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:37:32 +01:00
Anybody may ask; ten quiet minutes is the whole downside, so there is no trust gate on the way in and no cooldown. The confirmation is the last thing said: below it the dispatcher drops every reply in that group -- commands, the near-miss suggestion, and the nightly report, which is held back (never marked) rather than lost. Messages are still counted. !odcisz is the one command that runs through a mute, and only for TRUSTED_INVITERS; a stranger is refused without a reply, because a refusal is a reply. Unset, anybody may -- the same reading InviteTrust gives an unset variable, now shared as trust.is_trusted. There is deliberately no !unmute alias: it is one edit from !mute, and tests/test_nearest.py forbids two commands within reach of each other. The state is a deadline in group_meta, so it survives a restart and a reload and is cleared with the group. tests/test_help_command.py hit the 250-line cap and its album half moved to tests/test_help_album.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
4.7 KiB
Python
132 lines
4.7 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The dispatcher while a group is muted: listening, counting, not talking."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from typing import TYPE_CHECKING
|
|
|
|
from signal_bot.dispatcher import Dispatcher
|
|
from signal_bot.mute import mute
|
|
from signal_bot.mute_command import register_mute_commands
|
|
from signal_bot.registry import CommandRegistry
|
|
from signal_bot.registry_holder import RegistryHolder
|
|
from signal_bot.stats.queries_day import day_rows
|
|
from tests.conftest import SECOND_GROUP, WATCHED_GROUP, envelope
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.config import Config
|
|
from signal_bot.context import CommandContext
|
|
from signal_bot.discovery import GroupDirectory
|
|
from signal_bot.store import Store
|
|
from signal_bot.transport_fake import FakeTransport
|
|
|
|
NOW = dt.datetime(2026, 9, 4, 12, 0, tzinfo=dt.UTC)
|
|
ALIASES = {"mute": ["!cisza"], "unmute": ["!odcisz"]}
|
|
|
|
|
|
def _dispatcher(
|
|
config: Config,
|
|
directory: GroupDirectory,
|
|
store: Store,
|
|
transport: FakeTransport,
|
|
runs: list[str],
|
|
clock: list[dt.datetime],
|
|
) -> Dispatcher:
|
|
"""!cat plus the mute pair, in front of the real dispatcher."""
|
|
|
|
async def handler(ctx: CommandContext) -> None:
|
|
runs.append(ctx.command_name)
|
|
|
|
registry = CommandRegistry()
|
|
registry.register("cat", ["!cat"], handler, "kot")
|
|
register_mute_commands(registry, aliases=ALIASES)
|
|
return Dispatcher(
|
|
config=config,
|
|
directory=directory,
|
|
holder=RegistryHolder(lambda: registry),
|
|
store=store,
|
|
transport=transport,
|
|
clock=lambda: clock[0],
|
|
)
|
|
|
|
|
|
async def test_a_muted_group_is_counted_but_not_answered(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
"""A command, an unknown word, chatter: three messages recorded, no reply."""
|
|
runs: list[str] = []
|
|
dispatcher = _dispatcher(config, directory, store, transport, runs, [NOW])
|
|
mute(store, WATCHED_GROUP, NOW.timestamp() + 60)
|
|
|
|
await dispatcher.handle(envelope(text="!cat", timestamp=1))
|
|
await dispatcher.handle(envelope(text="!cta", timestamp=2))
|
|
await dispatcher.handle(envelope(text="hello", timestamp=3))
|
|
|
|
assert runs == []
|
|
assert transport.sent == []
|
|
day = store.events.first_day(WATCHED_GROUP)
|
|
assert day is not None
|
|
(row,) = day_rows(store.events, WATCHED_GROUP, day)
|
|
assert (row.messages, row.commands) == (3, 1)
|
|
# The near miss is not filed either: nothing about the mute is remembered
|
|
# as a suggestion to act on later.
|
|
assert store.suggestions.unknown() == []
|
|
|
|
|
|
async def test_the_mute_is_confirmed_once_and_then_the_bot_is_silent(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
runs: list[str] = []
|
|
dispatcher = _dispatcher(config, directory, store, transport, runs, [NOW])
|
|
|
|
await dispatcher.handle(envelope(text="!cisza 1"))
|
|
await dispatcher.handle(envelope(text="!cisza 1"))
|
|
await dispatcher.handle(envelope(text="!cat"))
|
|
|
|
assert [m.text for m in transport.sent] == ["BEEP BOOP CISZA PRZEZ 1 MINUTĘ."]
|
|
assert runs == []
|
|
|
|
|
|
async def test_unmute_is_the_one_command_that_runs_through_a_mute(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
runs: list[str] = []
|
|
dispatcher = _dispatcher(config, directory, store, transport, runs, [NOW])
|
|
mute(store, WATCHED_GROUP, NOW.timestamp() + 600)
|
|
|
|
await dispatcher.handle(envelope(text="!odcisz"))
|
|
await dispatcher.handle(envelope(text="!cat"))
|
|
|
|
assert transport.sent[0].text == "BEEP BOOP WRACAM."
|
|
assert runs == ["cat"]
|
|
|
|
|
|
async def test_the_silence_ends_on_its_own_at_the_deadline(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
runs: list[str] = []
|
|
clock = [NOW]
|
|
dispatcher = _dispatcher(config, directory, store, transport, runs, clock)
|
|
|
|
await dispatcher.handle(envelope(text="!cisza 1"))
|
|
clock[0] = NOW + dt.timedelta(seconds=59)
|
|
await dispatcher.handle(envelope(text="!cat"))
|
|
assert runs == []
|
|
clock[0] = NOW + dt.timedelta(seconds=60)
|
|
await dispatcher.handle(envelope(text="!cat"))
|
|
assert runs == ["cat"]
|
|
|
|
|
|
async def test_a_mute_in_one_group_leaves_the_other_talking(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
runs: list[str] = []
|
|
dispatcher = _dispatcher(config, directory, store, transport, runs, [NOW])
|
|
|
|
await dispatcher.handle(envelope(text="!cisza", group_id=WATCHED_GROUP))
|
|
await dispatcher.handle(envelope(text="!cat", group_id=WATCHED_GROUP))
|
|
await dispatcher.handle(envelope(text="!cat", group_id=SECOND_GROUP))
|
|
|
|
assert runs == ["cat"]
|