Skip to content

Commit a7138e2

Browse files
committed
release: merge v0.0.1-beta.2
2 parents b7a49ec + ae3179a commit a7138e2

28 files changed

Lines changed: 536 additions & 1502 deletions

‎.env.example‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ DEEPGRAM_API_KEY=
99
# TELEGRAM_API_BASE=http://telegram-bot-api:8081
1010
TELEGRAM_API_BASE=https://api.telegram.org
1111

12+
# Required when TELEGRAM_API_BASE is self-hosted. Such a server runs with
13+
# --local: getFile answers with an absolute path and nothing is served over
14+
# HTTP, so its data directory must be mounted. Mount only this bot's own
15+
# subdirectory — the others are named after other bots' tokens.
16+
# BOT_API_FILES_DIR=/var/lib/telegram-bot-api
17+
TELEGRAM_READY_WAIT=30s
18+
1219
# Local compose overrides this. For shared core-postgres use the voicy_core role.
1320
DATABASE_URL=postgres://voicy:voicy@postgres:5432/voicy?sslmode=disable
1421

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,10 @@ jobs:
8888
cp .env.example .env
8989
docker compose config >/dev/null
9090
cp .env.example deploy/ws04/.env
91-
VOICY_IMAGE=ghcr.io/freshlabdev/voicy:test docker compose -f deploy/ws04/compose.yaml config >/dev/null
91+
# The production stack mounts this bot's own directory on the shared
92+
# Bot API server, and that path is the token, so validation needs a
93+
# value to interpolate.
94+
VOICY_IMAGE=ghcr.io/freshlabdev/voicy:test \
95+
TELEGRAM_BOT_TOKEN=000:ci-validation-only \
96+
BOT_API_HOST_DIR=/opt/stacks/telegram-bot-api/files \
97+
docker compose -f deploy/ws04/compose.yaml config >/dev/null

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ jobs:
4343
cp .env.example .env
4444
docker compose config >/dev/null
4545
cp .env.example deploy/ws04/.env
46-
VOICY_IMAGE=ghcr.io/freshlabdev/voicy:test docker compose -f deploy/ws04/compose.yaml config >/dev/null
46+
VOICY_IMAGE=ghcr.io/freshlabdev/voicy:test \
47+
TELEGRAM_BOT_TOKEN=000:ci-validation-only \
48+
BOT_API_HOST_DIR=/opt/stacks/telegram-bot-api/files \
49+
docker compose -f deploy/ws04/compose.yaml config >/dev/null
4750
4851
database:
4952
name: Verify database behavior

‎AGENTS.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ Keep Voicy minimal, private by default, and production-minded.
88
- PostgreSQL is the only durable store. Domain tables live in a `voicy`
99
schema. Telegram identity and presence are delegated to shared `core`
1010
(`core.touch('voicy', …)`).
11-
- Telegram uses a first-party HTTP client in `internal/telegram`. Long polling
12-
only.
11+
- Telegram goes through `github.com/FreshLabDev/tg`, the client shared by the
12+
bot family. Voicy keeps no private HTTP client: transport, retries, token
13+
redaction, rich and ephemeral messages, and local Bot API files all live
14+
there. Long polling only.
15+
- `Preflight` runs before the bot starts and names the methods Voicy cannot
16+
work without. A server that lacks them is a startup failure, never a bot
17+
that polls and answers nothing.
1318

1419
## Product Boundaries
1520

‎CHANGELOG.md‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,63 @@ GitHub Releases.
88

99
## Unreleased
1010

11+
## v0.0.1-beta.2 - 2026-09-08
12+
13+
Voicy's Telegram client is no longer its own, and it will not start against a
14+
server that cannot serve it.
15+
16+
### Changed
17+
18+
- Telegram now goes through `github.com/FreshLabDev/tg`, the client shared by
19+
the bot family, and `internal/telegram` is gone. The transport, retries,
20+
token redaction, rich messages and the 10.3 ephemeral contract were the same
21+
code in three bots and had already drifted apart; they now have one home.
22+
Nothing about Voicy's behaviour changes with them.
23+
- `internal/media.Of` replaces `telegram.Message.Media()`. Go cannot put a
24+
method on another package's type, and the order in which attachments are
25+
preferred — speech first — was always a Voicy decision rather than a
26+
Telegram one.
27+
- `voicy_telegram_errors_total` now also counts transport failures (DNS,
28+
connect, reset). They were invisible before, because only a parsed API error
29+
reached the counter.
30+
31+
### Added
32+
33+
- A preflight at startup. Voicy names the methods it cannot work without —
34+
`sendRichMessage` and `editEphemeralMessageText` — and refuses to start when
35+
the Bot API server does not implement them. A server behind the bot answers
36+
`404 method not found` to every rich message, which used to mean a bot that
37+
polled happily and answered nothing at all.
38+
- `BOT_API_FILES_DIR`, required when `TELEGRAM_API_BASE` is self-hosted, and
39+
`TELEGRAM_READY_WAIT` (default 30s) for a server that is still booting.
40+
41+
### Fixed
42+
43+
- Media on a self-hosted Bot API server is read from disk again. Such a server
44+
runs with `--local`, and a `--local` server serves no files over HTTP at all:
45+
its `/file/bot<token>/…` route answers 404 in every version. Fetching over
46+
the network, introduced in v0.0.1-alpha.10, could therefore never work. Only
47+
this bot's own directory is mounted — the parent holds one directory per bot
48+
named after that bot's token — and the mount is verified at startup instead
49+
of failing on the first voice message.
50+
51+
### Operations
52+
53+
- The WS04 stack mounts the bot's own media directory on the Bot API server
54+
and runs as uid 101, the user that server writes as. `BOT_API_HOST_DIR` and
55+
`BOT_API_FILES_DIR` name that directory on the host and in the container.
56+
Files are removed after transcription, which a local server never does on
57+
its own.
58+
- The mount needs Compose's long volume syntax *with* a `bind` option. Both
59+
paths contain the bot token, a token contains a colon, and Compose flattens
60+
an option-less long mount back into `source:target:rw`, which the daemon
61+
then splits in the wrong places.
62+
- Production runs against `telegram-bot-api-next`, the FreshLab-built Bot API
63+
10.3 server, while the older shared server keeps the bots that have not
64+
moved. Moving a bot between servers is: stop it, `logOut` on the server it
65+
leaves, then start it on the new one. A running bot re-registers itself
66+
within a second, so the stop is not optional.
67+
1168
## v0.0.1-beta.1 - 2026-09-06
1269

1370
Voicy speaks the sixteen languages the bot family shares, transcribes audio

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ getFile -> bounded download
149149
|:--|:--:|:--|:--|
150150
| `TELEGRAM_BOT_TOKEN` | yes | — | Bot token from BotFather |
151151
| `TELEGRAM_API_BASE` | no | `https://api.telegram.org` | Bot API server; a self-hosted one lifts the 20 MB media ceiling |
152+
| `BOT_API_FILES_DIR` | with a self-hosted server | — | That server's media directory as Voicy sees it (its `--files-dir`, or its `--dir` on a server without one). It runs with `--local`, so it hands over files by absolute path and serves none over HTTP; mount only `<dir>/<token>`, and create that directory owned by uid 101 first |
153+
| `TELEGRAM_READY_WAIT` | no | `30s` | How long startup waits for a self-hosted server that is still booting |
152154
| `DEEPGRAM_API_KEY` | yes | — | Deepgram project API key |
153155
| `DATABASE_URL` | yes | — | PostgreSQL URL for `voicy_core`; the service enforces `search_path=voicy` |
154156
| `HTTP_ADDR` | no | `:8080` | HTTP listen address |

‎cmd/voicy/main.go‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import (
1111
"syscall"
1212
"time"
1313

14+
"github.com/FreshLabDev/tg"
1415
"github.com/FreshLabDev/voicy/internal/bot"
1516
"github.com/FreshLabDev/voicy/internal/config"
1617
"github.com/FreshLabDev/voicy/internal/db"
1718
"github.com/FreshLabDev/voicy/internal/deepgram"
1819
"github.com/FreshLabDev/voicy/internal/health"
1920
"github.com/FreshLabDev/voicy/internal/media"
2021
"github.com/FreshLabDev/voicy/internal/metrics"
21-
"github.com/FreshLabDev/voicy/internal/telegram"
2222
)
2323

2424
var (
@@ -54,16 +54,45 @@ func run(log *slog.Logger) error {
5454
}
5555
}
5656

57-
tg := telegram.NewClient(cfg.TelegramBotToken)
58-
tg.SetAPIBase(cfg.TelegramAPIBase)
59-
if cfg.TelegramAPIBase != config.DefaultTelegramAPIBase {
60-
// A local server lifts the cloud API's 20 MB getFile ceiling and returns
61-
// files by absolute path on a shared volume.
57+
client := tg.New(cfg.TelegramBotToken,
58+
tg.WithAPIBase(cfg.TelegramAPIBase),
59+
// my_chat_member is what tells shared Core where Voicy lives; Telegram
60+
// leaves it out of the default set, so it has to be asked for.
61+
tg.WithAllowedUpdates("message", "callback_query", "my_chat_member"),
62+
tg.WithLocalFiles(cfg.BotAPIFilesDir),
63+
tg.WithLogger(log),
64+
tg.WithObserver(func(e tg.Event) {
65+
switch {
66+
case e.Err == nil:
67+
case e.Status == http.StatusTooManyRequests:
68+
metrics.TelegramLimited.Inc(e.Method)
69+
default:
70+
metrics.TelegramErrors.Inc(e.Method)
71+
}
72+
}),
73+
)
74+
selfHosted := cfg.TelegramAPIBase != config.DefaultTelegramAPIBase
75+
if selfHosted {
76+
// A local server lifts the cloud API's 20 MB getFile ceiling and hands
77+
// over files by absolute path on its own disk.
6278
log.Info("using a self-hosted bot api server", "base", cfg.TelegramAPIBase)
6379
}
80+
// Voicy without rich messages is a bot that receives a voice message and
81+
// answers nothing: every transcript, placeholder and ephemeral reply goes
82+
// through them. A server that lacks them is a startup failure, not a
83+
// surprise on the first circle.
84+
me, err := client.Preflight(ctx, tg.Needs{
85+
Methods: []string{"sendRichMessage", "editEphemeralMessageText"},
86+
Files: selfHosted,
87+
Wait: cfg.TelegramReadyWait,
88+
})
89+
if err != nil {
90+
return err
91+
}
92+
log.Info("telegram preflight passed", "username", me.Username, "bot_api", tg.BotAPI)
6493
stt := deepgram.New(cfg.DeepgramAPIKey)
6594
store.SetStatsTimezone(cfg.StatsTimezone)
66-
b := bot.New(store, tg, stt, log)
95+
b := bot.New(store, client, stt, log)
6796
b.SetMediaLimits(cfg.MaxMediaBytes, cfg.MaxMediaDuration)
6897
b.SetWorkers(cfg.MaxConcurrentJobs)
6998
b.SetStatsTTL(cfg.StatsCacheTTL)

‎deploy/ws04/compose.yaml‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,35 @@ services:
1414
MIGRATIONS_DIR: /app/migrations
1515
HTTP_ADDR: :8080
1616
AUTO_MIGRATE: "true"
17+
# Where this bot sees the Bot API server's media. On a server started
18+
# with --files-dir (Bot API 10.3+) that is not the working directory.
19+
BOT_API_FILES_DIR: ${BOT_API_FILES_DIR:-/var/lib/telegram-bot-api}
20+
# The shared Bot API server runs with --local, which means it answers
21+
# getFile with an absolute path on its own disk and serves nothing over
22+
# HTTP: its /file/bot<token>/... route returns 404 by design. So the
23+
# directory has to be mounted — but only this bot's own subdirectory. The
24+
# parent holds one subdirectory per bot, each named after that bot's full
25+
# token, and mounting it would hand Voicy every other bot's credentials.
26+
# Long syntax, not "source:target": a bot token contains a colon and both
27+
# paths contain the token, so the short form splits in the wrong places.
28+
# The bind option is not decoration either -- without one, Compose flattens
29+
# long syntax back into a "source:target:rw" string and the daemon's parser
30+
# fails the same way ("invalid volume specification: too many colons").
31+
volumes:
32+
- type: bind
33+
# The directory must already exist and be owned by uid 101, the user
34+
# the Bot API server writes as. Docker would otherwise create it as
35+
# root, and then neither the server nor this bot could write in it.
36+
source: ${BOT_API_HOST_DIR:?BOT_API_HOST_DIR must point at the Bot API server media directory on the host}/${TELEGRAM_BOT_TOKEN:?TELEGRAM_BOT_TOKEN must be set for the Bot API files mount}
37+
target: ${BOT_API_FILES_DIR:-/var/lib/telegram-bot-api}/${TELEGRAM_BOT_TOKEN}
38+
bind:
39+
propagation: rprivate
40+
# The Bot API server writes as uid 101 with mode 0750. Voicy has to read
41+
# those files and delete them afterwards, because a local server never
42+
# reclaims what it produced.
43+
user: "101:101"
1744
# telegram_bot_api_net reaches the shared self-hosted Bot API server, the
18-
# same one vido uses. Its data directory is deliberately NOT mounted: it
19-
# holds one subdirectory per bot named after that bot's token, so mounting
20-
# it would hand Voicy every other bot's credentials. Files are fetched from
21-
# the server over this network instead, which still lifts the cloud API's
22-
# 20 MB ceiling.
45+
# same one vido uses.
2346
networks: [core_net, telegram_bot_api_net]
2447
logging:
2548
driver: json-file

‎docs/architecture.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ surface. PostgreSQL is its only durable store.
1414
deleted when the job ends. It is never logged or stored in PostgreSQL.
1515
- ffmpeg reduces video and oversized media to a mono Opus track before the
1616
Deepgram request. Without ffmpeg the original is sent instead.
17-
- Telegram is accessed through the first-party HTTP client in
18-
`internal/telegram`. There is no SDK and no webhook mode.
17+
- Telegram is accessed through `github.com/FreshLabDev/tg`, the client shared
18+
by the bot family. There is no SDK and no webhook mode.
1919
- `TELEGRAM_API_BASE` selects the Bot API server. The shared self-hosted one on
2020
`telegram_bot_api_net` runs with `TELEGRAM_LOCAL`, so `getFile` answers with an
2121
absolute path in its data directory. That directory is not mounted into Voicy:
@@ -100,5 +100,4 @@ Markdown when possible, then DM, then an owner-bound deep link as recovery.
100100
- `internal/deepgram`: prerecorded Listen client and response extraction.
101101
- `internal/health`: readiness and liveness contract.
102102
- `internal/settings`: settings registry and cache variants.
103-
- `internal/telegram`: Bot API HTTP transport.
104103
- `internal/transcript`: localized panels, rich HTML, splitting, stats.

‎go.mod‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ module github.com/FreshLabDev/voicy
44

55
go 1.26.6
66

7-
require github.com/jackc/pgx/v5 v5.9.2
7+
require (
8+
github.com/FreshLabDev/tg v0.0.1-alpha.1
9+
github.com/jackc/pgx/v5 v5.9.2
10+
)
811

912
require (
1013
github.com/jackc/pgpassfile v1.0.0 // indirect

0 commit comments

Comments
 (0)