fix(sticker): never adopt an existing pack

Two ordinary /newpack commands could take over a stranger's pack. The
first probe returns an inconclusive error, which correctly keeps the
reservation so the user can retry - but that turned a fresh claim into a
resumed one and defeated the guard that made adoption conditional.
resolveStaleIntent had a second adopt path that never consulted the
guard at all. Both are reproduced by tests added here.

This is the fourth failure of the same mechanism, and it is structural.
Adoption must prove "this set is mine to finish" from local state, and
local state is what a restart on the in-memory backend erases while the
packs at Telegram survive. With the proof gone, a genuine interrupted
attempt and a stranger naming a public share link are indistinguishable.

Remove adoption entirely. /newpack refuses any name a set already
occupies, and leaves no intent or reservation behind when it does.

A pending record is not evidence of ownership either: anyone can make one
naming any set, and DeleteStickerSet is keyed by set name, which Telegram
authorises for every set this bot created. /delpack therefore clears a
pending record locally and contacts Telegram only for a confirmed one.

The cost is that a crash between creating a set and recording it strands
that set. That is documented rather than mitigated - every mitigation
available is the mechanism that just failed.

Also drop a test whose name claimed to pin the resumed-reservation
distinction but bailed past the code that implements it, rename a delpack
test after the guard that actually stops a foreign presser, and pin
releaseSlug's ownership check and detached read - the latter needed a
context-honouring store, since the in-memory one ignores cancellation and
made the first version of that test vacuous.
This commit is contained in:
tiennm99 committed 2026-08-25 16:28:11 +07:00
1 parent a077b75d04
commit 1810c1ee47
9 files changed
+854 -144

No files matched your search

+26 -13
View File
@@ -97,9 +97,8 @@ never says *who* holds a name. Refusals about *managing* a pack are deliberately
uniform for the opposite reason — see below.
A name is claimed *before* the bot calls Telegram, not after the pack exists.
That ordering is the point: the claim is what proves, on a later re-run, that an
existing set under that name is yours to finish rather than someone else's to
take.
That ordering is what keeps two users from racing for the same name: the first
claimant wins it and everyone else is refused before any set is created.
The claim is given up again whenever the bot has positive evidence that no pack
stands behind it — Telegram refusing the creation outright, `/delpack`, or a
@@ -107,6 +106,9 @@ later command finding the set already gone. A `/newpack` that never got as far
as claiming, or that is refused before Telegram is contacted, leaves nothing
behind.
The claim is deliberately **not** treated as proof of ownership over a set that
already exists. See "The bot never takes over an existing pack" below.
Telegram may keep a deleted short name reserved on its own side, so a freed name
is not guaranteed to be usable again by anyone, including its previous owner.
@@ -114,17 +116,28 @@ is not guaranteed to be usable again by anyone, including its previous owner.
"that sticker isn't from your pack" produce the exact same reply. Distinct
wording would let anyone probe which sets exist under this bot.
**An interrupted `/newpack` can be finished.** The bot records your intent
before calling Telegram, so if a deploy or crash lands mid-creation, re-running
the same `/newpack` command completes it instead of reporting the name taken.
`/mypack` marks an unfinished attempt so it is visible rather than mysterious.
**The bot never takes over an existing pack.** If a set already exists under the
name you ask for, `/newpack` refuses — always, for everyone, whatever the bot's
records say about it.
This depends on the bot still holding your claim to the name. If its storage has
been wiped since — which is what a restart does when no database is configured —
the claim is gone while the pack at Telegram is not, and `/newpack` reports the
name as taken rather than adopting a set it can no longer prove is yours.
Recovering a pack in that state needs operator help. Run this module against a
real database, not the in-memory backend.
Earlier versions adopted such a set when local records suggested it came from
your own interrupted attempt. That was wrong in a way no amount of checking
fixes: every fact the bot could use to prove "this set is yours" lives in the
same storage a restart erases, while the packs at Telegram survive. Once the
proof is gone, a genuine interrupted attempt and a stranger naming your pack's
public link present the bot with identical evidence. The feature was the hole,
so the feature is gone.
The cost is real and worth stating plainly: if the bot crashes between creating
your set at Telegram and recording it, the set is stranded. It exists, it is
linkable, and no command in this bot can manage or delete it. `/mypack` marks
the unfinished attempt, and `/delpack` clears the leftover record so you can
create a pack under a different name — but it will not delete anything at
Telegram, because an unfinished record is not evidence that the bot made that
set for you. Anyone can produce such a record for any name.
Run this module against a real database. On the in-memory backend every restart
strands every pack.
**Deleting the last sticker may delete the pack.** Telegram's behaviour here is
undocumented, so the bot does not guess: it will not remove your pack record on