mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 21:54:03 +01:00
Some checks are pending
CI / check (push) Waiting to run
Two things the group hit within minutes of 0.1.0, both reported by real users rather than found by a test. Somebody typed !korona, got "Za mało danych — wróć za kilka dni." and nothing else. That answer tells them neither what the command does nor what it will eventually show, which makes it worse than useless on a release day when almost every command is empty by design. Every command now leads with what it is, and the empty answer says what it will show once there is history and which date it is counting from. And somebody asked whether "Najczęściej piszesz o 12:00" meant the whole hour 12-13. It did, and the label did not say so. It now reads "między 12:00 a 13:00". Same fix in kind for the nightly summary line, which said "średnio 47 znaków" without saying per what -- now "na wiadomość". A one-line wording fix takes the next pope exactly like a feature does, per DOCS-changelog.md. Exercised in testbot at the unbumped version first: !korona, !ja, !dni and !rekordy all ran clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
134 lines
4.9 KiB
Python
134 lines
4.9 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Rendering one day as the block the group reads.
|
|
|
|
Plain text, because that is all Signal renders. No markdown, and no column
|
|
padding: Signal uses a proportional font, so ``ljust`` lines up in a terminal
|
|
and on nobody's phone. A separator that does not depend on glyph widths is the
|
|
only spelling that looks the same in both places.
|
|
|
|
The block is capped at MAX_LINES so the nightly post stays something people
|
|
read rather than scroll past. When it has to give something up it gives up
|
|
speakers, never the highlight -- a leaderboard with the interesting part cut
|
|
off is worse than a shorter leaderboard.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
from signal_bot.stats import text
|
|
from signal_bot.stats.periods import pl_date, pl_day_month
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.stats.queries_day import DayRow, DayTotals
|
|
|
|
MAX_LINES: Final = 12
|
|
CROWN: Final = "\U0001f451"
|
|
CAMERA: Final = "\U0001f4f7"
|
|
CHART: Final = "\U0001f4ca"
|
|
|
|
|
|
def _media(count: int) -> str:
|
|
"""The camera marker, outside any padded column so it cannot misalign."""
|
|
return f" ({count} {CAMERA})" if count else ""
|
|
|
|
|
|
def person_line(row: DayRow, place: int) -> str:
|
|
"""One person's line: a crown for first place, otherwise a rank."""
|
|
lead = CROWN if place == 1 else f"{place}."
|
|
return f"{lead} {row.display_name} — {row.messages}{_media(row.media)}"
|
|
|
|
|
|
def silence_line(rows: list[DayRow], unnamed: int) -> str:
|
|
"""Everyone who said nothing, in one line, or "" when everybody spoke.
|
|
|
|
``unnamed`` are group members who have never written here at all. They are
|
|
counted rather than named on purpose: the listing knows them only as a raw
|
|
uuid or a phone number, and printing either into a group chat is a leak.
|
|
"""
|
|
names = [row.display_name for row in rows if row.is_silent]
|
|
if unnamed:
|
|
names.append(f"i {text.people(unnamed)} bez ani jednej wiadomości tutaj")
|
|
return "Cisza: " + ", ".join(names) if names else ""
|
|
|
|
|
|
def summary_line(totals: DayTotals, *, approximate: bool = False) -> str:
|
|
"""Commands and average length -- the two facts that qualify the count.
|
|
|
|
``approximate`` drops the length claim. It is set for the one day carried
|
|
over from the pre-0.1.0 counter, whose messages have a count but no
|
|
lengths: reporting "średnio 0 znaków" would be a measurement nobody took.
|
|
"""
|
|
if not totals.messages:
|
|
return ""
|
|
parts = (
|
|
[] if approximate else [f"średnio {text.characters(totals.average_length)} na wiadomość"]
|
|
)
|
|
if totals.commands:
|
|
parts.insert(0, f"z tego {text.commands(totals.commands)}")
|
|
if not parts:
|
|
return ""
|
|
return parts[0].capitalize() + (f", {parts[1]}." if len(parts) > 1 else ".")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DayView:
|
|
"""How one day is presented, apart from its numbers.
|
|
|
|
A value object rather than a widening keyword list: this renderer is
|
|
shared by the nightly post and several commands, each wanting a different
|
|
combination of these, and they keep accumulating.
|
|
"""
|
|
|
|
since: str | None = None
|
|
unnamed: int = 0
|
|
highlights: tuple[str, ...] = ()
|
|
heading: str | None = None
|
|
# The day's messages have counts but no lengths -- see stats.backfill.
|
|
approximate: bool = False
|
|
|
|
|
|
def render_day(
|
|
day: str,
|
|
rows: list[DayRow],
|
|
totals: DayTotals,
|
|
view: DayView | None = None,
|
|
) -> str:
|
|
"""Compose the day block, trimmed to MAX_LINES."""
|
|
view = view or DayView()
|
|
header = (
|
|
view.heading
|
|
or f"{CHART} {pl_date(day)} — {text.messages(totals.messages)}{_media(totals.media)}"
|
|
)
|
|
tail = [
|
|
line
|
|
for line in (
|
|
silence_line(rows, view.unnamed),
|
|
summary_line(totals, approximate=view.approximate),
|
|
)
|
|
if line
|
|
]
|
|
picks = list(view.highlights)
|
|
if view.since:
|
|
tail.append(f"(liczymy od {pl_day_month(view.since)})")
|
|
|
|
# Everything except the speakers is mandatory, and at least one highlight
|
|
# slot is reserved before any speaker is allowed a line.
|
|
reserved = len(tail) + min(len(picks), 1)
|
|
spoke = [row for row in rows if not row.is_silent]
|
|
# At least one, so the truncation line below always has a line to occupy.
|
|
# The tail cannot realistically reach eleven lines, but a floor here is
|
|
# cheaper than a defensive branch nothing can ever exercise.
|
|
room = max(1, MAX_LINES - 1 - reserved)
|
|
people = [person_line(row, i + 1) for i, row in enumerate(spoke[:room])]
|
|
if room < len(spoke):
|
|
hidden = spoke[room - 1 :]
|
|
total = sum(r.messages for r in hidden)
|
|
people[-1] = f"…i {text.people(len(hidden))} więcej (razem {text.messages(total)})"
|
|
|
|
lines = [header, *people]
|
|
lines.extend(picks[: max(0, MAX_LINES - len(lines) - len(tail))])
|
|
lines.extend(tail)
|
|
return "\n".join(lines)
|