Simplify upload error hints — user-facing, no config details
Some checks are pending
Build / build (push) Waiting to run

Previous hints mentioned bot config (e.g. "20 MB limit without local
server") which is useless to a regular group member who can't
administer the bot. Drop technical detail:

- "файл слишком большой" (no parenthetical)
- "файл не найден"
- "попробуйте ещё раз" (for transient MAX CDN)
- unknown error → no suffix, just the generic "Не удалось отправить X в MAX."

New helper uploadErrMsg(base, err) builds the final string, appending
": hint." only when uploadErrHint returns a non-empty known hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrey Lugovskoy 2026-04-23 21:22:45 +04:00
parent 825bfee473
commit 9b7e3cd165
3 changed files with 32 additions and 30 deletions

View file

@ -204,8 +204,9 @@ func (b *Bridge) isSelfTgBot(from *UserInfo) bool {
return from != nil && from.IsBot && from.UserName == b.tg.BotUsername()
}
// uploadErrHint превращает техническую ошибку загрузки в короткий русский текст
// для отправки в чат. Для неизвестных ошибок отдаёт усечённое сырое сообщение.
// uploadErrHint превращает техническую ошибку загрузки в короткий текст для юзера.
// Возвращает пустую строку для неизвестных ошибок — вызывающий код тогда
// отправит только generic-сообщение без технической мути.
func uploadErrHint(err error) string {
if err == nil {
return ""
@ -213,21 +214,22 @@ func uploadErrHint(err error) string {
s := err.Error()
switch {
case strings.Contains(s, "file is too big"):
return "файл слишком большой для Telegram Bot API (лимит без локального сервера — 20 МБ)"
return "файл слишком большой"
case strings.Contains(s, "file is not found") || strings.Contains(s, "FILE_REFERENCE_EXPIRED"):
return "Telegram не нашёл файл (возможно, он удалён или ссылка протухла)"
case strings.Contains(s, "wrong file id") || strings.Contains(s, "Wrong file"):
return "некорректный file_id"
return "файл не найден"
case strings.Contains(s, "attachment.not.ready"):
return "MAX CDN не успел обработать файл — попробуйте ещё раз"
case strings.Contains(s, "chat.denied"):
return "MAX отклонил отправку: бот не добавлен в чат или не имеет прав"
return "попробуйте ещё раз"
}
const maxLen = 180
if len(s) > maxLen {
s = s[:maxLen] + "…"
return ""
}
// uploadErrMsg собирает user-facing сообщение: base + ": hint" если подсказка известна,
// иначе просто "base." (без технической ошибки).
func uploadErrMsg(base string, err error) string {
if hint := uploadErrHint(err); hint != "" {
return base + ": " + hint + "."
}
return s
return base + "."
}
func (b *Bridge) tgWebhookPath() string {

View file

@ -163,7 +163,7 @@ func (b *Bridge) flushMediaGroup(ctx context.Context, groupID string) {
}
if photoFailErr != nil && photosSent == 0 {
b.tg.SendMessage(ctx, items[0].msg.Chat.ID,
fmt.Sprintf("Не удалось отправить альбом в MAX: %s", uploadErrHint(photoFailErr)), nil)
uploadErrMsg("Не удалось отправить альбом в MAX", photoFailErr), nil)
}
// Загружаем видео из альбома через direct API

View file

@ -620,7 +620,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
m.AddPhoto(uploaded)
} else {
slog.Error("TG→MAX photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить фото в MAX", err), nil)
return
}
} else if fileURL, err := b.tgFileURL(ctx, photo.FileID); err == nil {
@ -628,12 +628,12 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
m.AddPhoto(uploaded)
} else {
slog.Error("TG→MAX photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить фото в MAX", err), nil)
return
}
} else {
slog.Error("TG→MAX photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить фото в MAX", err), nil)
return
}
if msg.ReplyToMessage != nil {
@ -669,7 +669,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
mediaAttType = "video"
} else {
slog.Error("TG→MAX gif upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить GIF \"%s\" в MAX: %s", name, uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg(fmt.Sprintf("Не удалось отправить GIF \"%s\" в MAX", name), err), nil)
return
}
} else if msg.Sticker != nil {
@ -683,7 +683,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
mediaAttType = "video"
} else {
slog.Error("TG→MAX sticker upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить стикер в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить стикер в MAX", err), nil)
return
}
} else {
@ -701,7 +701,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
result, err := b.maxApi.Messages.SendWithResult(ctx, m)
if err != nil {
slog.Error("TG→MAX sticker send failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить стикер в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить стикер в MAX", err), nil)
} else {
slog.Info("TG→MAX sent", "mid", result.Body.Mid)
b.repo.SaveMsg(msg.Chat.ID, msg.MessageID, maxChatID, result.Body.Mid, msg.MessageThreadID)
@ -709,12 +709,12 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
return
} else {
slog.Error("TG→MAX sticker photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить стикер в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить стикер в MAX", err), nil)
return
}
} else {
slog.Error("TG→MAX sticker getFileURL failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить стикер в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить стикер в MAX", err), nil)
return
}
}
@ -731,7 +731,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
mediaAttType = "video"
} else {
slog.Error("TG→MAX video upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить видео \"%s\" в MAX: %s", name, uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg(fmt.Sprintf("Не удалось отправить видео \"%s\" в MAX", name), err), nil)
return
}
} else if msg.VideoNote != nil {
@ -743,7 +743,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
mediaAttType = "video"
} else {
slog.Error("TG→MAX video note upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить кружок в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить кружок в MAX", err), nil)
return
}
} else if msg.Document != nil {
@ -785,7 +785,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
}
slog.Error("TG→MAX file upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID,
fmt.Sprintf("Не удалось отправить файл \"%s\" в MAX: %s", name, uploadErrHint(err)), nil)
uploadErrMsg(fmt.Sprintf("Не удалось отправить файл \"%s\" в MAX", name), err), nil)
return
}
} else if msg.Voice != nil {
@ -803,7 +803,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
return
}
slog.Error("TG→MAX voice upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить голосовое сообщение в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось отправить голосовое сообщение в MAX", err), nil)
return
}
} else if msg.Audio != nil {
@ -834,7 +834,7 @@ func (b *Bridge) forwardTgToMax(ctx context.Context, msg *TGMessage, maxChatID i
return
}
slog.Error("TG→MAX audio upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось отправить аудио \"%s\" в MAX: %s", name, uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg(fmt.Sprintf("Не удалось отправить аудио \"%s\" в MAX", name), err), nil)
return
}
}
@ -965,7 +965,7 @@ func (b *Bridge) editTgMediaInMax(ctx context.Context, msg *TGMessage, maxChatID
m.AddPhoto(uploaded)
} else {
slog.Error("TG→MAX edit photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось обновить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось обновить фото в MAX", err), nil)
return
}
} else if fileURL, err := b.tgFileURL(ctx, photo.FileID); err == nil {
@ -973,12 +973,12 @@ func (b *Bridge) editTgMediaInMax(ctx context.Context, msg *TGMessage, maxChatID
m.AddPhoto(uploaded)
} else {
slog.Error("TG→MAX edit photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось обновить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось обновить фото в MAX", err), nil)
return
}
} else {
slog.Error("TG→MAX edit photo upload failed", "err", err)
b.tg.SendMessage(ctx, msg.Chat.ID, fmt.Sprintf("Не удалось обновить фото в MAX: %s", uploadErrHint(err)), nil)
b.tg.SendMessage(ctx, msg.Chat.ID, uploadErrMsg("Не удалось обновить фото в MAX", err), nil)
return
}
}