Completes TODO #2. PUBLIC_MODE swaps the ALLOWED_USERS allowlist for a
membership check against REQUIRED_CHATS, with REQUIRE_MODE choosing
between needing every chat or just one.
- Stop is_admin() falling back to "everyone is an admin" when
ADMIN_USERS is unset once PUBLIC_MODE is on. That fallback is the
original single-owner convenience, but under public mode it handed
moderation, /edit and /delete to anyone who found the bot. Public
mode now grants admin to nobody until ADMIN_USERS is set, and says
so at startup. Private deployments are unaffected.
- Cache membership answers: inline search queries on every keystroke,
so an uncached check meant several API calls per typed character.
Grants last five minutes, refusals thirty seconds, so a user who has
just joined is not locked out for the full window.
- Fail closed when the membership check errors, and validate
REQUIRED_CHATS at startup so a chat the bot cannot read is reported
rather than silently refusing every user.
- Refused users get a join prompt with real links instead of silence;
inline shows a tappable bar that opens the bot and explains why.
A private bot stays silent as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes TODO #3. Submissions are gated behind ACCEPT_SUBMISSIONS,
which defaults to off: is_allowed() treats everyone as allowed when
ALLOWED_USERS is empty, so opening submissions implicitly would have
accepted media from anyone who found the bot.
- Schema v3 adds status, reviewed_by and reviewed_at. Existing rows
default to approved, so upgrading changes nothing for a running bot.
- Search, /list and counts only return approved items; callers that
need everything pass status=None.
- /review shows the oldest pending item with approve, reject, fix
keywords and stop. No cursor is stored anywhere: each action just
asks for the next pending item, which is what lets a moderator stop
and resume later, and lets several moderators share one queue.
- set_status only updates rows still pending, so if two moderators open
the same item the first decision wins and the second is told so.
- A failed send no longer wedges the queue; the item's details are
shown anyway so it can still be rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes TODO #6. The bot accepted photo, animation, video, voice and
sticker; music files, documents and round videos were silently dropped.
- Replace the five near-identical media handlers with one dispatch table.
Animations are matched before documents because Telegram populates
both fields for a GIF, so the old ordering would have filed every GIF
as a document once document support existed.
- Store video notes as "video". Telegram has no cached inline result for
video notes, so this is what keeps round videos searchable; it costs
the round presentation on playback.
- Build inline results from a table keyed by media_type, and cover audio
and document. A test asserts every storable type has a builder, so a
new media type cannot be added without a way to return it.
- Reuse one MEDIA_FILTER for both the idle and mid-keyword handlers;
the second list had already drifted from the first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 0 of the roadmap: everything from the moderation queue onward adds
columns, and init_db could only ever run CREATE TABLE IF NOT EXISTS, so
it could not alter a live deployment.
- Track schema state in PRAGMA user_version and run ordered migrations.
Fresh installs create the v1 table and migrate forward, so the upgrade
path runs on every new deployment rather than only on old ones.
- Rebuild media with UNIQUE(owner_id, file_unique_id). The old global
unique constraint stopped two users from each saving the same file,
which blocks public mode.
- Replace INSERT OR REPLACE with an upsert. Replacing deleted and
reinserted the row, giving it a new id and silently invalidating any
id the user had noted from /list.
- Give search_media an optional owner_id scope, defaulting to the
current shared-library behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design-only document covering the moderation queue, public mode,
save-vs-extract vault, and federation. Records the four decisions
taken on the roadmap and the API claims verified against aiogram
3.15.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VERSION had been left at 1.0.0 while the registry already carried 1.0.1, so
push.sh (which defaults to the VERSION file) would have rebuilt an existing
tag. This realigns the file with what is actually published.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Split docker-compose into build (docker-compose.build.yaml) and deploy (docker-compose.yaml pulls from registry)
- Add push.sh for building and pushing versioned images to git.goyban.com/goyban/bot_meme
- Add ADMIN_USERS env var; admins can add/edit/delete, users can search
- Add /edit command with inline keyboard to update meme keywords
- Add sticker support (save and serve via inline)
- Switch /delete to use numeric row ID instead of file_unique_id
- Search is now global (shared pool) instead of per-user
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>