# Linkki Telegram Broadcast Editor Linkki is a Docker-hosted web application for drafting, scheduling, publishing, updating, and deleting multilingual Telegram channel broadcasts. It supports Finnish, Swedish, and English, using a Telegram-oriented Markdown dialect with a live preview. The app stores its configuration and broadcasts outside the source tree, so container rebuilds do not remove published-message mappings, images, or drafts. For codebase and maintenance context, see [CODEX_HANDOFF.md](CODEX_HANDOFF.md). ## Run locally Requires Python 3.12 or newer. ```text python -m pip install -r requirements.txt python webapp_main.py ``` Open `http://localhost:8080`. The data directory comes from `LINKKI_DATA_DIR`; without it, the app uses `data/` beside the source tree. ```text data/ config/ app_config.json users.json telegram_config.json posts/ my-broadcast/ post.json Suomi.md Svenska.md English.md Kuva1.jpg Suomi1.jpg Svenska1.jpg English1.jpg ``` Copy and adapt the files under `config-examples/`. Do not commit real tokens, password hashes, or production data. ## Configuration `data/config/app_config.json`: ```json { "timezone": "Europe/Helsinki", "session_secret": "replace-with-a-long-random-secret", "scheduler_poll_seconds": 60, "users_file": "users.json" } ``` `session_secret` signs login session cookies. Changing it logs out every active user. `data/config/telegram_config.json` supports multiple bots and channel targets: ```json { "bots": [ { "display_name": "Main bot", "token": "123456:ABCDEF...", "default": true, "channels": [ { "display_name": "Main channel", "channel_id": "-1001234567890", "discussion_group_id": "-1001234567891", "channel_username": "my_public_channel", "default": true } ] } ] } ``` `discussion_group_id` is optional. It enables discussion-message mapping and pinning. `channel_username` is optional; it is used for public Telegram message URLs, while private channels use `t.me/c/...` URLs derived from `channel_id`. `data/config/users.json` contains password hashes, UI language preference, and permissions. Generate a password hash with: ```text python -m webapp.auth ``` Permissions default to `false` when omitted. The available keys are: ```text edit_content edit_targets save_announcements open_telegram post_now schedule unschedule unlink_telegram delete_telegram delete_storage ``` The app normalizes missing permission keys and saved UI-language preferences back into `users.json`. ## Using the editor New broadcasts appear first in the sidebar. The editor has four settings tabs: - `Main`: title, selected bot/channel, language order, scheduling, images, and text editors. - `Telegram link`: channel and discussion-group message mappings. Missing links can be entered manually as a message ID or a Telegram message URL. - `Pinning`: discussion-group pin mode, service-message cleanup, and scheduled unpinning. - `Logs`: accumulated publish, update, delete, and unpin output. The sidebar markers convey state: green is published, blue is modified after publish, yellow is scheduled, and red is failed. Incomplete Telegram links make the status marker red. A pin marker means linked discussion messages are pinned; it becomes yellow when an unpin is scheduled. The broadcast lifecycle is deliberately restrictive: - Drafts can change targets, language setup, images, and text. - Scheduled broadcasts keep their selected bot/channel locked until unscheduled. - Linked broadcasts keep targets, language setup, and images locked. The internal title, text, link-preview settings, and pinning settings remain editable. - Published text changes become `modified` and are sent with `Post modifications now`. - `Delete from Telegram` tries discussion-group messages before channel messages. It retains links for messages that could not be deleted. - Telegram bots cannot delete messages after roughly 48 hours. The UI disables that action once the stored deadline has passed. Discussion-message mappings require the bot to see the discussion group's forwarded messages. In practice, the bot needs privacy mode disabled or sufficient admin visibility. Historic links missed at posting time cannot be recovered automatically; use the manual mapping fields. `Delete service messages` is enabled by default for new broadcasts. It requires Telegram deletion rights in the discussion group and may fail independently of pinning. ## Text syntax The text editor supports Telegram-oriented Markdown and preview: ~~~text **bold** _italic_ ++underline++ ~~strikethrough~~ `inline code` ```code block``` [label](https://example.com) ||spoiler|| > quote - bullet 1. numbered item # heading ~~~ Cross-language links use `[label](fi)`, `[label](sv)`, or `[label](en)`. A backslash escapes formatting markers. The editor toolbar and `Ctrl/Cmd+B`, `I`, and `U` apply common inline styles. ## Container image This repository publishes `git.ajpanton.se/ajp_anton/linkki-tiedotus:1` and the mutable `latest` tag. The image serves port `8080`; put an authenticated reverse proxy in front of it when needed. `compose.example.yaml` is a complete generic service definition. The application data directory must be persistent and must not be committed to source control. ```yaml services: linkki-tiedotus: image: git.ajpanton.se/ajp_anton/linkki-tiedotus:1 ports: - "8008:8080" environment: LINKKI_DATA_DIR: /data volumes: - /host/linkki-data:/data restart: unless-stopped ``` Scheduled broadcasts are persisted in `/data`. If the container is offline when one becomes due, it is posted on the scheduler's first pass after startup. Failed scheduled posts retry after 1, 5, 15, and then every 30 minutes until they succeed or are unscheduled. A manually started post is retried once immediately before it is marked failed. ## Legacy standalone CLI The web app is the maintained workflow. The repository also keeps the original one-directory poster for manual use and as a lower-level reference: ```text python post_broadcast.py path/to/broadcast ``` It expects a separate, single-bot `telegram_config.json` beside the script with `bot_token`, `chat_id`, and optional `channel_username`. `main.py` is a compatibility alias. `manage_broadcast.py` is an advanced helper for updating or deleting a previously linked broadcast. These scripts are not used by the container.