signal-bot/signal_bot/good_bot_command.py
Krzysztof kuhy Rudnicki e3c7bbfe23 !good bot: thank the asker with an emoji on their message, not a reply
A compliment gets a reaction drawn from a small warm set (❤️ 🥰 😊 👍 🎉 🙏
😸) put on the compliment itself, and nothing in the group -- the one
answer that adds no line. Same cooldown as every other command.

The transport learns reactions: send_reaction on the Protocol, POST
/v1/reactions/<number> on the REST adapter with the four fields
api.SendReactionRequest requires (204 back), a recorded list on the fake.
CommandContext.react() names the target the way a quote does, and falls
back to the emoji as text for an envelope with no timestamp, so a shape
this bot has never seen is visible rather than swallowed.

The fake container gains the endpoint, strict about the schema, and
records reactions in order with the sends; a scenario can now expect
{kind: reaction, quotes: true}. First two-word trigger: fold() keeps the
inner space and the registry matches whole text first, so "!good bot"
resolves and "!good" does not; one-word spellings cover a double space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 21:48:03 +02:00

70 lines
2.6 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""!good bot: the bot says thank you the one way that adds no noise.
A reaction rather than a reply. "good bot" is a compliment, and answering a
compliment with a line of text is the kind of thing that gets a bot muted;
an emoji on the compliment itself is seen by the whole group without taking
up a line. Which emoji is a draw from a small warm set, so the second
thank-you of the evening does not look like a canned copy of the first.
Rate-limited like every other command: a heart costs a POST to the
container, and somebody typing it in a loop is the same problem as somebody
typing !cat in a loop.
The trigger is two words, which no other command has. That works because
``folding.fold`` keeps inner spaces and the registry matches the whole text
before it looks at the first word -- so "!good bot" resolves, "!good" alone
resolves to nothing, and "!good bot" typed with two spaces resolves to
nothing either. The one-word spellings in aliases.json are for that last
case.
"""
from __future__ import annotations
import secrets
from typing import TYPE_CHECKING, Final
from signal_bot.animals import load_aliases
if TYPE_CHECKING:
from collections.abc import Sequence
from signal_bot.context import CommandContext
from signal_bot.decorators import Decorator
from signal_bot.registry import CommandRegistry, Handler
COMMAND_NAME: Final = "good_bot"
DESCRIPTION: Final = "bot dziękuje za komplement reakcją, bez gadania"
# Warm and unambiguous, nothing that reads as sarcasm in a group chat.
EMOJI: Final = ("❤️", "🥰", "😊", "👍", "🎉", "🙏", "😸")
def pick(choices: Sequence[str] = EMOJI) -> str:
"""One emoji, uniformly at random.
secrets rather than random for the reason animals.paged_url gives: the
choice is not security-sensitive, but ruff's ALL rule set flags `random`
and this repository forbids the noqa that would silence it.
"""
return choices[secrets.randbelow(len(choices))]
async def good_bot_handler(ctx: CommandContext) -> None:
"""Thank the asker on their own message; say nothing in the group."""
await ctx.react(pick())
def register_good_bot_command(
registry: CommandRegistry,
*,
decorate: Decorator | None = None,
aliases: dict[str, list[str]] | None = None,
) -> None:
"""Register !good bot under every spelling aliases.json gives it."""
table = load_aliases() if aliases is None else aliases
handler: Handler = good_bot_handler
if decorate is not None:
handler = decorate(handler)
registry.register(COMMAND_NAME, table[COMMAND_NAME], handler, DESCRIPTION)