Files
ajp_anton 8ec3cde4f2
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 48s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 55s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 54s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:linkki-tiedotus fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:linkki-tiedotus oci_labels:… (push) Successful in 39s
Add Linkki broadcast image
2026-08-19 20:51:26 +00:00
..
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00
2026-08-19 20:51:26 +00:00

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 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:

**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.