Compare commits

..

26 Commits

Author SHA1 Message Date
developer a41281c495 Merge pull request 'release: v1.17.0 — implicit offline model + two-tier client-version gate + unified lobby' (#250) from development into master 2026-07-13 01:06:35 +00:00
developer 516ffbe5f0 Merge pull request 'release: v1.16.0 — offer live pricing, bot health telemetry, edge blocklist' (#247) from development into master 2026-07-11 11:56:09 +00:00
developer 6badc20078 Merge pull request 'Release v1.15.0 — in-game UX + wallet redesign + WAL-alert fix' (#243) from development into master 2026-07-10 16:15:20 +00:00
developer 0ca01133b5 Merge pull request 'Release v1.14.1 — ansible certs-dir fix + Robokassa go-live' (#239) from development into master 2026-07-10 10:58:38 +00:00
developer 45f0b34881 Merge pull request 'Release v1.14.0 — monetization launch (E5-E8)' (#237) from development into master 2026-07-10 10:11:02 +00:00
developer 18785efc8c Merge pull request 'release v1.13.0: payments wallet mechanics + database point-in-time recovery' (#220) from development into master 2026-07-08 23:42:15 +00:00
developer 780ff68ec2 Merge pull request 'release: offline mode + local pass-and-play (hotseat) — proposed v1.12.0' (#212) from development into master 2026-07-07 14:40:43 +00:00
developer 57ff2d03f8 Merge pull request 'Release v1.11.0: PWA install + code-only PWA login + email/metrics fixes' (#187) from development into master 2026-07-05 21:02:08 +00:00
developer a9d0986e74 Merge pull request 'Release v1.10.0: banner colours + urgent, and 3 UI fixes' (#183) from development into master 2026-07-05 14:10:25 +00:00
developer 45957bdcd6 Merge pull request 'Release: promote development → master (old Android WebView support + unsupported-engine telemetry)' (#178) from development into master 2026-07-04 21:23:29 +00:00
developer 829e29a726 Merge pull request 'Release v1.8.0: prod build fix (re-promote)' (#175) from development into master 2026-07-03 21:48:13 +00:00
developer 399508f2f0 Merge pull request 'Release v1.8.0: promote development → master' (#173) from development into master 2026-07-03 21:31:25 +00:00
developer 3a18e683ca Merge pull request 'release: VK Mini App + landscape UI + dict v1.3.1 seed (development→master)' (#144) from development into master 2026-06-30 05:37:43 +00:00
developer 93d086a8a3 Merge pull request 'release: v1.7.0 — Telegram Mini App embedding enhancements' (#138) from development into master 2026-06-24 11:49:44 +00:00
developer 8fe1bdba6b Merge pull request 'release: v1.6.0 — promo deep-link seeds EN variant (+ UI nits)' (#135) from development into master 2026-06-23 21:02:19 +00:00
developer 7923b3cc09 Merge pull request 'release v1.5.1: support-relay card + topic-reopen fixes' (#133) from development into master 2026-06-23 16:54:01 +00:00
developer 4891216749 Merge pull request 'release v1.5.0: Telegram bot support relay' (#131) from development into master 2026-06-23 16:16:04 +00:00
developer f1b8769c89 Merge pull request 'release: v1.4.1 — Telegram nav (windowed, own back button, debug panel)' (#129) from development into master 2026-06-23 13:27:31 +00:00
developer b6f28a2423 Merge pull request 'release: v1.4.0 — Telegram launch diagnostic + dynamic SDK load' (#127) from development into master 2026-06-23 08:40:09 +00:00
developer e32ee9ce68 Merge pull request 'Release: development → master' (#125) from development into master 2026-06-22 22:36:42 +00:00
developer dc946a1faf Merge pull request 'release v1.2.2: edge HTTP/3 stall fix + db-size dashboard threshold' (#121) from development into master 2026-06-22 19:50:58 +00:00
developer 384bd143d0 Merge pull request 'Promote development → master: banner tip set + banner/push language fix' (#114) from development into master 2026-06-22 18:28:00 +00:00
developer c5d22fceca Merge pull request 'Promote development → master: Erudit blank star + dictionary v1.3.0 pin' (#111) from development into master 2026-06-22 13:12:01 +00:00
developer deaa7a29c5 Merge pull request 'Promote development → master (docs finalize + UI tweaks + Telegram name fallback)' (#108) from development into master 2026-06-22 07:27:40 +00:00
developer 24017bcb7f Merge pull request 'Promote development → master (deploy v2: versioning + visible jobs + rollback)' (#106) from development into master 2026-06-22 06:01:03 +00:00
developer 2c4f4b10dc Merge pull request 'Promote development → master (initial production release: pre-release line + Stage 18)' (#104) from development into master 2026-06-22 05:05:48 +00:00
6 changed files with 11 additions and 155 deletions
+3 -5
View File
@@ -430,11 +430,9 @@ unanswered reply. Guests cannot send feedback (the entry is hidden). A player th
barred from feedback (a role, not a full account block) sees the send control disabled.
### Telegram support chat
A user can also reach the operators straight from the Telegram bot. The `/support` command replies
with the support desk's working hours and what to include (a description and, when possible,
screenshots). Anything else they send the bot other than `/start` — text, a photo, a voice message,
a file — is forwarded into the operators' private support group, grouped into a per-user thread. The
user sees no automatic reply to a forwarded message; an
A user can also reach the operators straight from the Telegram bot: anything they send the bot
other than `/start` — text, a photo, a voice message, a file — is forwarded into the operators'
private support group, grouped into a per-user thread. The user sees no automatic reply; an
operator answers from that thread and the bot delivers the answer back as an ordinary bot message,
so to the user it is a quiet one-to-one conversation. Operators can block a user (the bot then
silently ignores their messages) or clear a thread's messages. This is independent of the in-app
+3 -5
View File
@@ -441,11 +441,9 @@ Telegram. На сенсорном устройстве в настройках
обратную связь (роль, а не полная блокировка аккаунта), видит кнопку отправки недоступной.
### Чат поддержки в Telegram
С операторами можно поговорить и прямо из Telegram-бота. Команда `/support` отвечает графиком работы
поддержки и напоминанием, что приложить (подробное описание и, по возможности, скриншоты). Всё
остальное, что пользователь присылает боту, кроме `/start` — текст, фото, голосовое, файл, —
пересылается в закрытую группу поддержки операторов и собирается в отдельную ветку на пользователя.
Пользователь не получает автоответа на пересланное сообщение; оператор
С операторами можно поговорить и прямо из Telegram-бота: всё, что пользователь присылает боту,
кроме `/start` — текст, фото, голосовое, файл, — пересылается в закрытую группу поддержки операторов
и собирается в отдельную ветку на пользователя. Пользователь не получает автоответа; оператор
отвечает из этой ветки, и бот доставляет ответ обратно обычным сообщением — для пользователя это
тихий диалог один на один. Операторы могут заблокировать пользователя (тогда бот молча игнорирует
его сообщения) или очистить сообщения ветки. Это независимо от встроенной «Обратной связи» выше —
+3 -12
View File
@@ -9,9 +9,8 @@ it. See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md) §1/§3/§10/§12/
validation gRPC API the gateway calls during Telegram auth, on the trusted internal
network with no VPN. Because it needs no Telegram reachability, **game login stays up
even when the bot or the bot-link is down**.
- **`cmd/bot`** (remote) — runs the Bot API long-poll (Mini App launch, `/start`
deep-links, the `/support` info reply) and `sendMessage`, the only component reaching
the Telegram Bot API. It
- **`cmd/bot`** (remote) — runs the Bot API long-poll (Mini App launch + `/start`
deep-links) and `sendMessage`, the only component reaching the Telegram Bot API. It
holds **no inbound port**: it dials the gateway over a reverse **mTLS bot-link** and
executes the send commands the gateway pushes, so its egress can run on a host with
native Telegram access (a VPN sidecar in the test contour, a separate host in prod).
@@ -50,13 +49,6 @@ Telegram identity to an account from a browser. Both map a rejection to gRPC
chat" — rather than a dangling "@"). This is otherwise **self-contained**
— the bot never calls back into the game, so `/start` onboarding works even when the game
is down.
- **Support command.** `/support` replies with a fixed support-desk info message — the
operators' working hours and what to include (a description and, when possible,
screenshots). Like `/start` it answers only in a private chat and is **Russian or
English** by the sender's reported language, and it is listed in the bot's command menu
(localized, Russian/English). It is a dedicated command handler, so it intercepts
`/support` before the support relay below — the command line itself is not forwarded to
operators, while the user's following description still is.
- **Moderated-chat gating.** When `TELEGRAM_CHAT_ID` names a channel's linked discussion
group, the bot gates who may write there. The group **allows sending by default** (a
human setting) and the bot only **restricts** — Telegram intersects the chat default with
@@ -84,8 +76,7 @@ Telegram identity to an account from a browser. Both map a rejection to gRPC
message. State (user→topic map, block list, relayed ids) is a JSON file under
`TELEGRAM_SUPPORT_STATE_DIR` on a persistent volume — the bot host has no database. The bot must
be an **administrator** in the group with the **manage-topics** and **delete-messages** rights.
The relay is bot-local (no backend, no bot-link) and a forwarded message gets no automatic
reply (the `/support` command above aside).
The relay is bot-local (no backend, no bot-link) and the user gets no automatic reply.
- **Promo bot (optional).** When `TELEGRAM_PROMO_BOT_TOKEN` is set, the container also
runs a **second, standalone** bot whose only job is to answer `/start` with a localized
message and a button that opens the **main** bot's Mini App. The button is a **URL** to
+2 -19
View File
@@ -1,7 +1,6 @@
// Package bot wraps the Telegram Bot API client (github.com/go-telegram/bot): it
// runs the long-poll update loop — replying to /start (with an optional deep-link
// payload), to /support with the support-desk info message, and to any other message
// with a Mini App launch button — and sends the
// payload) and any other message with a Mini App launch button — and sends the
// notification and admin messages the connector requests. The bot token lives only
// in this process.
package bot
@@ -137,7 +136,6 @@ func New(cfg Config, log *zap.Logger) (*Bot, error) {
opts := []tgbot.Option{
tgbot.WithDefaultHandler(t.handleUpdate),
tgbot.WithMessageTextHandler("/start", tgbot.MatchTypePrefix, t.handleStart),
tgbot.WithMessageTextHandler("/support", tgbot.MatchTypePrefix, t.handleSupport),
}
if t.supportEnabled() {
t.supportLocks = newKeyedMutex()
@@ -188,26 +186,11 @@ func New(cfg Config, log *zap.Logger) (*Bot, error) {
// Run sets the bot commands and the Mini App menu button, then blocks on the
// long-poll update loop until ctx is cancelled.
func (t *Bot) Run(ctx context.Context) {
// Command menu: the default (fallback) list is English; a Russian-scoped list
// localises the labels for ru clients. A language scope replaces the whole list, so
// it repeats /start with its own Russian label.
if _, err := t.api.SetMyCommands(ctx, &tgbot.SetMyCommandsParams{
Commands: []models.BotCommand{
{Command: "start", Description: "Play Scrabble"},
{Command: "support", Description: "Contact the administration"},
},
Commands: []models.BotCommand{{Command: "start", Description: "Open Scrabble"}},
}); err != nil {
t.log.Warn("set commands failed", zap.Error(err))
}
if _, err := t.api.SetMyCommands(ctx, &tgbot.SetMyCommandsParams{
LanguageCode: "ru",
Commands: []models.BotCommand{
{Command: "start", Description: "Играть в «Эрудита»"},
{Command: "support", Description: "Связаться с администрацией"},
},
}); err != nil {
t.log.Warn("set ru commands failed", zap.Error(err))
}
if _, err := t.api.SetChatMenuButton(ctx, &tgbot.SetChatMenuButtonParams{
MenuButton: models.MenuButtonWebApp{
Type: models.MenuButtonTypeWebApp,
@@ -1,63 +0,0 @@
package bot
import (
"context"
"strings"
tgbot "github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"go.uber.org/zap"
)
// The /support command replies with a fixed support-desk info message — the operators'
// working hours and what to include (a description and, when possible, screenshots). It
// is a dedicated command handler (registered like /start), so it intercepts "/support"
// before the support relay: the command line itself is not forwarded into an operator
// topic, while the user's following description still is.
// handleSupport replies to /support with the localized support-desk info message. Like
// handleStart it answers only in a private chat — in the moderated group the bot never
// chats — and it sends a plain text message with no launch button.
func (t *Bot) handleSupport(ctx context.Context, api *tgbot.Bot, update *models.Update) {
if update.Message == nil {
return
}
if update.Message.Chat.Type != models.ChatTypePrivate {
return
}
// The sender's Telegram language rides on the message itself (Message.from.language_code);
// fall back to English when it is absent, matching startText.
lang := ""
if update.Message.From != nil {
lang = update.Message.From.LanguageCode
}
if _, err := api.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: supportText(lang),
}); err != nil {
t.log.Warn("reply to support failed", zap.Error(err))
}
}
// supportText returns the localized /support reply. Russian is used when lang (the IETF
// language tag the Telegram client reports on the message's sender) starts with "ru",
// English otherwise and when it is absent — mirroring startText's language choice.
func supportText(lang string) string {
if strings.HasPrefix(strings.ToLower(lang), "ru") {
return ruSupport
}
return enSupport
}
// ruSupport and enSupport are the Russian and English /support replies; the English one
// is the fallback for any non-Russian or missing sender language.
const (
ruSupport = "Поддержка «Эрудита» на связи с 08:00 до 18:00 (по времени UTC). " +
"Если Вы столкнулись с проблемой, подробно опишите её и добавьте, по возможности, скриншоты. " +
"Также будем рады выслушать Ваши пожелания и предложения по функционалу игры. " +
"Ответим в самое ближайшее время."
enSupport = "“Erudite” support is available from 08:00 to 18:00 (UTC). " +
"If you have run into a problem, please describe it in detail and attach screenshots if you can. " +
"We would also be glad to hear your wishes and suggestions about the game's features. " +
"We will reply as soon as possible."
)
@@ -1,51 +0,0 @@
package bot
import (
"context"
"strings"
"testing"
"github.com/go-telegram/bot/models"
)
func TestHandleSupportRepliesPrivateOnly(t *testing.T) {
t.Run("english by default", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleSupport(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate}, Text: "/support",
}})
if api.chatID != "42" {
t.Errorf("chat_id = %q, want 42", api.chatID)
}
// No reported language -> English support reply.
if !strings.Contains(api.text, "support is available") {
t.Errorf("text = %q, want the English support reply", api.text)
}
// A plain info message carries no launch button.
if api.replyMarkup != "" {
t.Errorf("reply_markup = %q, want none", api.replyMarkup)
}
})
t.Run("russian for a ru sender", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleSupport(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate}, Text: "/support",
From: &models.User{ID: 7, LanguageCode: "ru"},
}})
if !strings.Contains(api.text, "Поддержка «Эрудита»") {
t.Errorf("text = %q, want the Russian support reply", api.text)
}
})
t.Run("group ignored", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleSupport(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: -100, Type: models.ChatTypeSupergroup}, Text: "/support",
}})
if api.chatID != "" {
t.Errorf("group /support got a reply (chat=%q); the bot never chats in the group", api.chatID)
}
})
}