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.
Run locally
Requires Python 3.12 or newer.
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.
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:
{
"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:
{
"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:
python -m webapp.auth
Permissions default to false when omitted. The available keys are:
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
modifiedand are sent withPost modifications now. Delete from Telegramtries 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:
**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.
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:
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.