mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 22:23:48 +01:00
The scheduler fired exactly on time tonight and posted nothing, because publish_report covered the previous full day and the bot's first recorded day was today: "no messages in group.WTI5dVptbG... on 2026-08-29". Correct by code, a day late to everybody reading the group. So the report covers the day it runs on. The count closes at the report time and render_day says so, because a total that quietly omits three hours is worse than one that admits to it. Everything that made a late day recoverable stays: the per-group marker still prevents a double post, and days_to_report still yields every day an outage swallowed. closing_periods keeps taking a day rather than reading a clock, so a week now closes on the Sunday evening it ends -- off a day that is itself still running -- and a catch-up on a week-old Sunday still gets it right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Which days still owe the group a report, and posting them.
|
|
|
|
The event log already survives an outage: a message carries Signal's own send
|
|
timestamp, signal-cli keeps receiving while the bot is down, and the replay on
|
|
reconnect is absorbed by the primary key. So the *counts* come back on their
|
|
own, filed under the day they were actually sent.
|
|
|
|
The *report* did not. A bot that was down at 21:37 simply never posted that
|
|
day, and nothing ever went back for it -- the old counter merged the day into
|
|
the next one instead, which is the bug this release exists to remove. So the
|
|
last day reported is written down per group, and every day since then gets
|
|
posted when the bot comes back.
|
|
|
|
Bounded, because coming back from a fortnight offline should not open with a
|
|
fortnight of back-posts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import logging
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
from signal_bot.stats.periods import Period, month_of, shift, week_of, year_of
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.store import Store
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LAST_REPORTED_KEY: Final = "last_reported_day"
|
|
# At most a week of catch-up in one go. Long enough to cover a weekend
|
|
# outage nobody noticed, short enough that a month offline is not a wall.
|
|
CATCHUP_LIMIT: Final = 7
|
|
|
|
|
|
def days_to_report(store: Store, group_id: str, latest: str) -> list[str]:
|
|
"""Every day up to ``latest`` that has not been reported yet, oldest first.
|
|
|
|
Args:
|
|
store: The open database, which holds the per-group marker.
|
|
group_id: The group being reported on.
|
|
latest: The most recent day eligible for a report, normally today.
|
|
|
|
Returns:
|
|
The days to post, oldest first. A group with no marker gets only
|
|
``latest``: the marker is absent on a brand-new group and on the first
|
|
run after this release, and neither should trigger a backlog.
|
|
"""
|
|
last = store.meta(group_id, LAST_REPORTED_KEY)
|
|
if last is None:
|
|
return [latest]
|
|
if last >= latest:
|
|
return []
|
|
|
|
days = []
|
|
day = shift(last, 1)
|
|
while day <= latest:
|
|
days.append(day)
|
|
day = shift(day, 1)
|
|
return days[-CATCHUP_LIMIT:]
|
|
|
|
|
|
SUNDAY: Final = 6
|
|
LAST_DAY_OF_DECEMBER: Final = "12-31"
|
|
|
|
|
|
def closing_periods(day: str) -> list[Period]:
|
|
"""The longer periods that ended with ``day``, in the order to post them.
|
|
|
|
Keyed off the day being *reported*, never off the calendar. The nightly
|
|
post covers the day it runs on, so a week is complete exactly when that
|
|
day is a Sunday -- and the same reading stays correct when the bot has
|
|
been offline and is catching up on a week-old day, which is why the
|
|
predicate takes the day rather than reading a clock.
|
|
|
|
No separate scheduler for these. Three background tasks firing at the same
|
|
wall-clock time into the same group is three independent ways for the
|
|
evening to go wrong, for no gain over an if-statement.
|
|
"""
|
|
date = dt.date.fromisoformat(day)
|
|
closing = []
|
|
if date.weekday() == SUNDAY:
|
|
closing.append(week_of(day))
|
|
if shift(day, 1)[8:] == "01":
|
|
closing.append(month_of(day))
|
|
if day.endswith(LAST_DAY_OF_DECEMBER):
|
|
closing.append(year_of(day))
|
|
return closing
|
|
|
|
|
|
def mark_reported(store: Store, group_id: str, day: str) -> None:
|
|
"""Record that a day no longer owes this group a report.
|
|
|
|
Called for a day that was posted *and* for a day nobody spoke on -- an
|
|
empty day is settled, not pending, and leaving it unmarked would make the
|
|
bot reconsider it every night forever.
|
|
"""
|
|
store.set_meta(group_id, LAST_REPORTED_KEY, day)
|