7.3 KiB
Linkki Handoff Brief
Purpose
This directory is the extracted Linkki Telegram broadcast editor. The active product is a FastAPI web app in a Docker container. Users draft Finnish, Swedish, and English channel broadcasts, preview Telegram formatting, schedule publishing, update published text, map channel messages to linked discussion-group messages, manage pinning, and delete or unlink Telegram posts.
The source is intended to be absorbed into a larger project. Read README.md for setup and operation; this document is implementation context for the next coding agent.
Important entry points
| Path | Role |
|---|---|
webapp_main.py |
Container and local web-server entry point. |
webapp/app.py |
FastAPI routes, async scheduler, publish/update/delete orchestration. |
webapp/storage.py |
On-disk post schema, optimistic field revisions, state transitions, title/directory renames. |
webapp/static/app.js |
Stateful editor client. Large single IIFE; also handles editor-only sidebar navigation without a full page load. |
webapp/static/app.css |
Existing desktop-first editor/sidebar layout. |
webapp/templates/ |
Base shell, editor, login, and instructions markup. |
webapp/translations/catalog.json |
All visible UI strings in Finnish, Swedish, and English. |
linkki_poster/ |
Telegram API wrapper, formatting parser, directory scanning, publishing, updates, deletes, and pinning. |
tests/ |
Pytest suite. |
Data model
Runtime data is outside the source tree under LINKKI_DATA_DIR (/data in Docker):
config/
app_config.json
users.json
telegram_config.json
posts/
slug/
post.json
Suomi.md
Svenska.md
English.md
images named Kuva*, Suomi*, Svenska*, English*
post.json is canonical metadata. webapp.storage.load_metadata() merges it with current defaults and normalizes fields. Current schema is METADATA_SCHEMA_VERSION = 3. Scheduled publishes retain a retry count and next-attempt timestamp; they retry after 1, 5, 15, and then 30 minutes until success. A one-shot migration previously normalized production posts, and old binding-format fallback support was deliberately removed afterward. Do not casually reintroduce old schema handling; if a new schema change is needed, use a deliberate migration.
Every editable persistent field has a revision in metadata.field_revisions. Browser saves include a base revision; storage returns a conflict if another editor changed the field. When adding a metadata field, update all of:
default_metadata()default_field_revisions()when the field is independently editablemerge_metadata_with_defaults()- the browser editable snapshot and save payload in
app.js - storage tests
The title is also the directory slug. apply_title_change() renames the directory after checking the target path does not exist.
Publish state and Telegram bindings
The central state values are draft, scheduled, published, modified, and failed.
- A channel broadcast stores target selection as
telegram_bot_idandtelegram_chat_id. telegram_bindingstores channel message IDs and corresponding discussion-group message IDs.- Linked broadcasts lock target/language/image setup, but title and text remain editable.
- Editing a linked text marks
dirty_text_languages; editing pinning marksdirty_discussion_pinning.perform_publish_action()sends the required Telegram update. - A successful scheduled unpin clears its schedule and sets pin mode to
none.
Keep channel-broadcast bindings (PublishBinding, LanguageBinding, TextSegmentBinding) separate from any future direct-message or group-management feature. The current binding model assumes channel posts with optional discussion messages.
Discussion mapping is a Telegram limitation, not merely a UI concern. The bot must receive fresh automatic-forward updates from the linked discussion group. Privacy mode/admin visibility and competing getUpdates consumers matter. The automatic historical repair path was removed because it could only help while relevant updates were still arriving. Manual channel/group ID mapping is the intended recovery path.
Delete from Telegram deletes discussion messages first, retains partial bindings after failures, then deletes channel messages. Telegram may reject bot deletions after about 48 hours. Pin-service-message deletion is separate from pinning and defaults to enabled for new broadcasts.
Frontend notes
app.js owns a state object containing the current and last-saved broadcast, permissions, operation polling, sidebar data, and active tab. Re-rendering rebuilds language panels and Telegram mapping cells. Existing helpers keep textarea and preview heights synchronized.
Sidebar navigation between editor pages is deliberately partial:
- Sidebar announcement clicks are intercepted.
- The client fetches
GET /api/announcements/{directory}. - It replaces editor state, re-renders, and calls
history.pushState. - Browser back/forward uses
popstateand refetches editor state.
Do not turn ordinary sidebar forms (creation, reordering, sign-out, language change) into partial navigation without designing their return/state behavior. The base template still owns sidebar scroll persistence.
The Telegram link tab uses an aligned table. A group link cannot exist without its channel link. The Pinning tab is disabled when the chosen channel target lacks a discussion group. Preserve those constraints in both UI and backend routes.
Text formatting is conservative by design. Inline styles are parsed/re-serialized to avoid crossing tags. The parser in linkki_poster/formatting.py is authoritative. Text-node HTML escaping intentionally uses quote=False: escaping apostrophes as ' caused Telegram heading rendering bugs.
Permissions and security
Permissions are in webapp/auth.py and normalized into users.json. New actions need both backend enforcement and corresponding browser disabled state.
Never commit real Telegram tokens, user data, password hashes, or /data contents.
Operations and testing
The FastAPI scheduler runs in-process and checks persisted schedules every scheduler_poll_seconds. Its first pass posts every overdue scheduled broadcast immediately after startup, then follows the persisted retry delay. In-memory operation progress (app.state.operations) disappears on container restart; the persisted logs and schedules remain.
Run:
python -m pip install -r requirements-dev.txt
python -m pytest -q
python -m compileall webapp linkki_poster tests
node --check webapp/static/app.js
Use a virtual environment if the host Python is externally managed.
Non-goals and recent decisions
- The application only supports channel broadcasts today.
- A proposed direct/mass-message feature was not implemented because Telegram Bot API bots cannot reliably resolve arbitrary private-user
@usernamesto chat IDs or initiate chats with them. Treat any future direct-message feature as a separate design with numeric chat IDs or a local known-contact store. - A proposed group-creation feature was also deferred; Bot API does not provide general create-group/add-arbitrary-user-by-username behavior.
- The root standalone CLI scripts (
post_broadcast.py,main.py, andmanage_broadcast.py) remain as legacy/manual tools. They are not part of the container workflow.