Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,8 @@ with the same `{placeholders}`; every Spanish string containing `?` also contain
- `<data>/state/sct.sqlite` is git-ignored. One table `kv(key TEXT PRIMARY KEY, value TEXT)` holding
exactly: `offset`, `last_ask`, `conv:<chat>`, `pending:<chat>`, `said:<chat>`, `options:<chat>`,
`remind:<chat>` (the reminder and plan offers waiting for a tap), `ctx:<chat>` (the last group line
a kept turn took), `calplan`, `skip:<date>`, `migrated_chat`; any other key is refused. Two more
a kept turn took), `calplan`, `skip:<date>`, `migrated_chat`, `announced_version`; any other key
is refused. Two more
tables: `chat`, the chat log (5.2), and `reminder`, the reminders set from the chat (5.6).
Everything else (`/estado` facts, what was already said, undo) is read from git, the log or the
`Change`. The connection is opened with `check_same_thread=False` behind one lock, since the worker
Expand All @@ -1103,6 +1104,10 @@ with the same `{placeholders}`; every Spanish string containing `?` also contain
- At start the bot reads `getMe.can_read_all_group_messages`, then `getChatMember(chat_id=<group>,
user_id=<bot id>)`; either one (privacy mode off, or status `administrator`/`creator`) turns on the
privacy-off rule (4.3), and the log line says which.
- After the menu, a version the KV's `announced_version` does not hold yet is announced in private to
each admin of the profile (the group when it names none): «Nueva versión en marcha: 0.6.0 (antes
0.5.0).». The version is stored once Telegram took at least one of those messages, so a failed send
is tried again at the next start and a restart on the same version says nothing.
- `download(file_path, limit)` fetches a file `getFile` returned from
`https://api.telegram.org/file/bot<token>/<file_path>`. Like `call`, its errors are
`TelegramError("download", code, description)` with no URL in them; more than `limit` bytes raises
Expand Down
2 changes: 1 addition & 1 deletion supercycletracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""SuperCycleTracker: a menstrual cycle tracker that lives in a Telegram group."""

__version__ = "0.5.0"
__version__ = "0.6.0"
21 changes: 20 additions & 1 deletion supercycletracker/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

import yaml

from supercycletracker import assistant, cycle, records
from supercycletracker import __version__, assistant, cycle, records
from supercycletracker.i18n import Translator
from supercycletracker.records import SYMPTOMS, BlockError, Change, Day, Edit, Log
from supercycletracker.repo import DATA_LOCK, KV, STATE_DB, GitError, Repo
Expand Down Expand Up @@ -640,6 +640,24 @@ def group_admin(self) -> bool:
return False
return isinstance(member, Mapping) and member.get("status") in ADMIN_STATUSES

def announce_version(self, version: str = __version__) -> None:
"""Tell the admins in private, or the group when the profile names none, that a new
version runs. The version is kept in the KV only once Telegram took at least one message,
so a failed send is tried again at the next start and a restart on the same version
says nothing."""
previous = self.kv.get("announced_version")
if previous == version:
return
if previous:
text = self.t("version_new_from", version=version, previous=previous)
else:
text = self.t("version_new", version=version)
chats = list(self.profile.admins) or [self.chat_id]
sent = [c for c in chats if c is not None and self.post(c, text) is not None]
if sent:
self.kv.set("announced_version", version)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
log.info("version %s announced to %d chat(s)", version, len(sent))

def set_commands(self) -> None:
"""The menu: one `setMyCommands` in the default scope with the five she uses. The admin
commands are listed by /ayuda when an admin asks."""
Expand Down Expand Up @@ -1831,6 +1849,7 @@ def run(self, stop: threading.Event) -> None:
delay = min(delay * 2, BACKOFF_MAX_S)
self.repo.check_remote()
self.set_commands()
self.announce_version()
self.resume()
try:
while not stop.is_set():
Expand Down
2 changes: 2 additions & 0 deletions supercycletracker/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,5 @@ plan_yes: "Yes"
plan_no: "No"
plan_moved: "The forecast has changed since; I will offer you the new one."
plan_calendar_turn: "[asked with the «In the calendar» button of the forecast] Put these forecast entries in my calendar:\n{entries}"
version_new: "New version running: {version}."
version_new_from: "New version running: {version} (before {previous})."
2 changes: 2 additions & 0 deletions supercycletracker/locales/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,5 @@ plan_yes: "Sí"
plan_no: "No"
plan_moved: "La previsión ha cambiado desde entonces; te ofrezco la nueva."
plan_calendar_turn: "[pedido con el botón «En el calendario» de la previsión] Pon en mi calendario estas entradas de la previsión:\n{entries}"
version_new: "Nueva versión en marcha: {version}."
version_new_from: "Nueva versión en marcha: {version} (antes {previous})."
7 changes: 4 additions & 3 deletions supercycletracker/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

# --- SQLite kv -----------------------------------------------------------------------------

KV_KEYS = frozenset({"offset", "last_ask", "migrated_chat", "calplan"})
KV_KEYS = frozenset({"offset", "last_ask", "migrated_chat", "calplan", "announced_version"})
KV_PREFIXES = ("conv:", "pending:", "skip:", "said:", "options:", "ctx:", "remind:")
CHAT_SCHEMA = """
CREATE TABLE IF NOT EXISTS chat (
Expand All @@ -66,8 +66,9 @@ class KV:
`pending:<chat>`, `said:<chat>` (the last answered messages, so an edit can be compared),
`options:<chat>` (the choices under recent answers), `remind:<chat>` (reminders and calendar
entries offered with buttons), `ctx:<chat>` (the last chat log row a turn took as context),
`calplan` (the calendar entries last offered), `skip:<date>` and `migrated_chat`; any other
key is refused, so nothing else about the cycle can creep into a second store.
`calplan` (the calendar entries last offered), `skip:<date>`, `migrated_chat` and
`announced_version` (the last version the admins were told of); any other key is refused,
so nothing else about the cycle can creep into a second store.

Two more tables: `chat`, the chat log (every message of the chats the bot serves that it
keeps, and its answers, for the assistant's recap), and `reminder`, the one-off reminders set
Expand Down
56 changes: 55 additions & 1 deletion tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
seed_log,
)

from supercycletracker import assistant, cycle, records
from supercycletracker import __version__, assistant, cycle, records
from supercycletracker.bot import (
TEXT_MAX,
Bot,
Expand Down Expand Up @@ -1385,6 +1385,60 @@ def test_run_stops_on_a_bad_token(make_bot):
bot.run(threading.Event())


def test_the_first_start_announces_the_version_to_each_admin_in_private(make_bot):
bot = make_bot()
bot.announce_version("1.2.3")
assert [(m["chat_id"], m["text"]) for m in bot.tg.of("sendMessage")] == [
(ADMIN, "Nueva versión en marcha: 1.2.3.")
]
assert bot.kv.get("announced_version") == "1.2.3"


def test_a_restart_on_the_same_version_says_nothing(make_bot):
bot = make_bot()
bot.announce_version("1.2.3")
bot.announce_version("1.2.3")
assert len(bot.tg.of("sendMessage")) == 1


def test_a_new_version_names_the_one_before(make_bot):
bot = make_bot()
bot.announce_version("1.2.3")
bot.announce_version("1.3.0")
assert bot.tg.texts()[-1] == "Nueva versión en marcha: 1.3.0 (antes 1.2.3)."
assert bot.kv.get("announced_version") == "1.3.0"


def test_a_failed_announcement_is_not_stored_and_goes_out_at_the_next_start(make_bot):
bot = make_bot()
bot.tg.fail_next("sendMessage", 403, "Forbidden: bot can't initiate conversation with a user")
bot.announce_version("1.2.3")
assert bot.kv.get("announced_version") is None
bot.announce_version("1.2.3")
assert bot.tg.texts() == ["Nueva versión en marcha: 1.2.3."] * 2 # refused, then taken
assert bot.kv.get("announced_version") == "1.2.3"


def test_with_no_admins_the_announcement_goes_to_the_group(make_bot):
bot = make_bot(profile=PROFILE.replace(" admins: {222: Marta}\n", ""))
bot.announce_version("1.2.3")
assert [m["chat_id"] for m in bot.tg.of("sendMessage")] == [GROUP]


def test_run_announces_the_running_version_once(make_bot):
bot = make_bot()
bot.repo.flush = lambda force=False: "idle"
stop = threading.Event()

def interrupted():
stop.set()
raise Interrupted

bot.tg.updates = [interrupted]
bot.run(stop)
assert bot.tg.texts() == [f"Nueva versión en marcha: {__version__}."]


# --- voice notes ---------------------------------------------------------------------------


Expand Down
Loading