signal-bot/signal_bot/counting.py
2026-08-29 15:30:08 +02:00

57 lines
2.0 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Which messages count toward the daily tally, and which chats we listen to."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from signal_bot.discovery import GroupDirectory
from signal_bot.models import IncomingMessage
def watched_group(message: IncomingMessage, directory: GroupDirectory) -> str | None:
"""Return the canonical id of the watched group a message came from.
The bot answers in every group it has been added to, and in nothing
else. Direct messages are rejected here by an explicit lookup rather
than a falsy check: a DM carries no group id at all, and one must never
reach a handler no matter how the surrounding code is later rearranged.
It returns the id rather than a bool so that the gate and the reply
router are the same decision. Answering "may this run?" and "where does
the answer go?" separately is how a bot ends up replying to the wrong
group.
Args:
message: The parsed inbound message.
directory: The live set of groups the bot is a member of.
Returns:
The canonical group id, or None for a direct message or any group
the bot is not scoped to.
"""
if message.group_id is None:
return None
return directory.resolve(message.group_id)
def should_count(message: IncomingMessage) -> bool:
"""True when a message should add to its sender's tally.
Reactions and stickers are not messages for tally purposes, and a frame
with no text body carries nothing to count.
"""
if message.is_reaction or message.has_sticker:
return False
return bool(message.text)
def format_tally(rows: list[tuple[str, int]]) -> str:
"""Render the daily report, or a placeholder when nobody spoke."""
if not rows:
return "Brak wiadomosci dzisiaj."
width = max(len(name) for name, _ in rows)
lines = [f"{name.ljust(width)} {count}" for name, count in rows]
return "\n".join(lines)