mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 16:43:49 +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>
89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The mute state: a deadline per group, and the one question asked of it."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from signal_bot.mute import (
|
|
MUTED_UNTIL_KEY,
|
|
UNMUTE_NAME,
|
|
allows,
|
|
is_muted,
|
|
mute,
|
|
muted_until,
|
|
unmute,
|
|
)
|
|
from signal_bot.registry import CommandRegistry
|
|
from tests.conftest import SECOND_GROUP, WATCHED_GROUP
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.context import CommandContext
|
|
from signal_bot.store import Store
|
|
|
|
NOW = 1_000.0
|
|
|
|
|
|
async def _noop(ctx: CommandContext) -> None:
|
|
"""A handler that does nothing."""
|
|
|
|
|
|
def test_a_group_that_was_never_muted_has_no_deadline(store: Store) -> None:
|
|
assert muted_until(store, WATCHED_GROUP) is None
|
|
assert not is_muted(store, WATCHED_GROUP, NOW)
|
|
|
|
|
|
def test_a_mute_stands_until_its_deadline_and_not_a_moment_longer(store: Store) -> None:
|
|
mute(store, WATCHED_GROUP, NOW + 60)
|
|
assert muted_until(store, WATCHED_GROUP) == NOW + 60
|
|
assert is_muted(store, WATCHED_GROUP, NOW)
|
|
assert is_muted(store, WATCHED_GROUP, NOW + 59.9)
|
|
# The deadline itself is over: "until" is exclusive.
|
|
assert not is_muted(store, WATCHED_GROUP, NOW + 60)
|
|
|
|
|
|
def test_a_mute_is_per_group(store: Store) -> None:
|
|
"""Silence asked for in one group must not reach into another."""
|
|
mute(store, WATCHED_GROUP, NOW + 60)
|
|
assert is_muted(store, WATCHED_GROUP, NOW)
|
|
assert not is_muted(store, SECOND_GROUP, NOW)
|
|
|
|
|
|
def test_a_later_mute_replaces_the_deadline_rather_than_extending_it(store: Store) -> None:
|
|
"""The cap is a cap: ten minutes asked twice is still ten minutes from now."""
|
|
mute(store, WATCHED_GROUP, NOW + 600)
|
|
mute(store, WATCHED_GROUP, NOW + 60)
|
|
assert muted_until(store, WATCHED_GROUP) == NOW + 60
|
|
|
|
|
|
def test_unmute_ends_the_silence_now_and_is_idempotent(store: Store) -> None:
|
|
mute(store, WATCHED_GROUP, NOW + 600)
|
|
unmute(store, WATCHED_GROUP)
|
|
assert not is_muted(store, WATCHED_GROUP, NOW)
|
|
unmute(store, WATCHED_GROUP)
|
|
assert not is_muted(store, WATCHED_GROUP, NOW)
|
|
|
|
|
|
def test_the_deadline_lives_in_group_meta_as_epoch_seconds(store: Store) -> None:
|
|
"""Pinned because forget_group clears group_meta, which is the intended cleanup."""
|
|
mute(store, WATCHED_GROUP, NOW + 60)
|
|
assert store.meta(WATCHED_GROUP, MUTED_UNTIL_KEY) == repr(NOW + 60)
|
|
|
|
|
|
def test_everything_is_allowed_while_nobody_asked_for_silence(store: Store) -> None:
|
|
registry = CommandRegistry()
|
|
registry.register("cat", ["!kot"], _noop, "kot")
|
|
assert allows(store, WATCHED_GROUP, NOW, registry.resolve("!kot"))
|
|
assert allows(store, WATCHED_GROUP, NOW, None)
|
|
|
|
|
|
def test_only_the_unmute_command_is_allowed_through_a_mute(store: Store) -> None:
|
|
"""A command, an unknown word and the unmute: one of the three passes."""
|
|
registry = CommandRegistry()
|
|
registry.register("cat", ["!kot"], _noop, "kot")
|
|
registry.register(UNMUTE_NAME, ["!odcisz"], _noop, "odcisz")
|
|
mute(store, WATCHED_GROUP, NOW + 60)
|
|
assert not allows(store, WATCHED_GROUP, NOW, registry.resolve("!kot"))
|
|
assert not allows(store, WATCHED_GROUP, NOW, None)
|
|
assert allows(store, WATCHED_GROUP, NOW, registry.resolve("!odcisz"))
|