Add Linkki broadcast image
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

This commit is contained in:
ajp_anton
2026-08-19 20:51:26 +00:00
parent 7e6dea8b24
commit 8ec3cde4f2
56 changed files with 13441 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
# 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.