mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:17:02 +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>
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Per-group silence: the state, and the one question the dispatcher asks it.
|
|
|
|
A mute is a deadline in ``group_meta``, keyed by the canonical group id, so
|
|
it survives a restart and a reload alike and never leaks into another
|
|
group. While it stands the bot says nothing in that group: no replies, no
|
|
"did you mean", no nightly report. Messages are still counted -- silence is
|
|
about talking, not about listening -- and exactly one command still runs,
|
|
the one that ends it.
|
|
|
|
Anyone may ask for silence, for at most ten minutes, because the worst
|
|
anyone can do with that is buy ten quiet minutes. Ending it early is the
|
|
operator's call: see mute_command.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.registry import Command
|
|
from signal_bot.store import Store
|
|
|
|
# The group_meta key. Epoch seconds, as text, like every other value there.
|
|
MUTED_UNTIL_KEY: Final = "muted_until"
|
|
MAX_MINUTES: Final = 10
|
|
DEFAULT_MINUTES: Final = MAX_MINUTES
|
|
# The one command that runs through a mute. By name rather than by a flag on
|
|
# Command, so that the registry -- which has nothing to do with silence --
|
|
# does not grow a field only one command sets.
|
|
UNMUTE_NAME: Final = "unmute"
|
|
|
|
|
|
def muted_until(store: Store, group_id: str) -> float | None:
|
|
"""When the current mute ends, or None when there has never been one."""
|
|
raw = store.meta(group_id, MUTED_UNTIL_KEY)
|
|
return None if raw is None else float(raw)
|
|
|
|
|
|
def is_muted(store: Store, group_id: str, now: float) -> bool:
|
|
"""Whether the group is silent at this instant.
|
|
|
|
An expired deadline is left in place rather than deleted: reading it
|
|
costs one comparison, and deleting it would make this a write.
|
|
"""
|
|
until = muted_until(store, group_id)
|
|
return until is not None and now < until
|
|
|
|
|
|
def mute(store: Store, group_id: str, until: float) -> None:
|
|
"""Silence the group until the given instant, replacing any earlier deadline."""
|
|
store.set_meta(group_id, MUTED_UNTIL_KEY, repr(until))
|
|
|
|
|
|
def unmute(store: Store, group_id: str) -> None:
|
|
"""End the silence now. Idempotent: a group that is not muted stays not muted."""
|
|
store.set_meta(group_id, MUTED_UNTIL_KEY, "0")
|
|
|
|
|
|
def allows(store: Store, group_id: str, now: float, command: Command | None) -> bool:
|
|
"""Whether the dispatcher may say anything in reply to this message.
|
|
|
|
True whenever the group is not muted; while it is, true only for the
|
|
command that ends the mute. ``None`` is a word nobody registered, whose
|
|
"did you mean" reply is exactly the kind of noise a mute is for.
|
|
"""
|
|
if not is_muted(store, group_id, now):
|
|
return True
|
|
return command is not None and command.name == UNMUTE_NAME
|