For crosspost TG→MAX (a post in a TG channel), any delivery error
message used to be posted back into the same channel (visible to all
subscribers). The only person who can act on it is the crosspost owner.
New helper notifyTgUser(ctx, srcMsg, maxChatID, text, isCrosspost):
- crosspost: resolves tg_owner_id via GetCrosspostOwner and DMs that
user; drops with a warn if the crosspost has no owner (legacy).
- bridge: keeps the existing behavior (post into the source chat,
preserving forum thread).
All upload/size/circuit-breaker notifications in forwardTgToMax and
flushMediaGroup now go through notifyTgUser. Bridge flows are
unchanged; crosspost flows stop spamming channels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues reported by a user via the bridge:
1. Links vanish on TG channel → MAX crosspost. formatTgCrosspostCaption
returned raw text and forwardTgToMax dropped markdown format for the
crosspost branch, so text_link entities became plain text. Now
formatTgCrosspostCaption runs tgEntitiesToMarkdown, and
forwardTgToMax always uses markdown. Media group flusher also
skips the second conversion for crosspost items (caption is already
markdown) and keeps markdown format.
2. When a channel video is too big for Bot API getFile (e.g. HD > 2 GB
on local server), the whole post was dropped — even the caption
text was lost. Video upload failure no longer returns from
forwardTgToMax; the fallback branch appends a "[Видео]" marker so
subscribers in MAX at least get the text and know a video was
attached but not relayed. The fallback condition now checks
presence of any media instead of only msg.Text == "" (it missed
video-with-caption posts) and also covers Animation and Photo.
Tradeoff: crosspost replacements now operate on markdown text. URL-
and phrase-level replacements keep working; regex rules touching
markdown characters (_, *, [, ]) may need adjustment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Requested by user Victor via bridged group.
- New helper uploadErrHint maps common upload failures to a short
Russian hint ("файл слишком большой", "Telegram не нашёл файл",
"MAX CDN не успел обработать файл", etc) and falls back to a truncated
raw error for the unknown case.
- Every TG→MAX upload failure path now tells the user what happened
(photo, gif, sticker, video, video note, document, voice, audio,
edit-with-media, media group).
- Edit path previously failed silently and media group dropped the
error on the floor — both now notify.
- Media group notifies once with the first failure when no photos
survived upload (avoids N messages per album).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #38 part 2. Allows using multiple MAX group chats as per-thread
mirrors of a TG forum group, since MAX has no native thread concept.
- New table thread_pairs (tg_chat_id, tg_thread_id, max_chat_id) with
unique(max_chat_id) — one MAX chat = at most one TG thread.
- pending gains thread_id column for thread-bridge key exchange.
- Repo methods: StartThreadBridge (TG issues key), CompleteThreadBridge
(MAX consumes key), GetThreadMaxChat, GetThreadTgPair, UnpairThread,
UnpairThreadByMax.
- Commands:
* /thread_bridge in a TG forum thread (admin, non-General) -> key
* /thread_bridge <key> in a MAX chat -> binds
* /thread_unbridge on either side
TG's BOT_COMMAND_INVALID rule forbids hyphens, so the commands use
underscore.
- Routing priority: thread-bridge > regular pair.
TG->MAX: if msg.MessageThreadID has a thread_pair, route there.
MAX->TG: if MAX chat is in thread_pairs, route to (tg_chat, thread).
- For thread-paired MAX chats, the Part 1 reply-to-source-thread
override is disabled: thread-paired chats have a fixed target thread,
replies stay there.
- Safeguard: a MAX chat cannot be in both pairs and thread_pairs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Issue #38 part 1: MAX reply to a bridged TG message now lands in the
same TG forum thread as the original, not in the pair's default
thread. messages table gets tg_thread_id column (migration 000014);
SaveMsg stores the TG thread, LookupTgMsgID returns it, forwardMaxToTg
applies source thread for reply-routing (both body.ReplyTo and
Link.Type=reply paths).
- Fix crosspost attribution heuristic: forwardMaxToTg used
"caption != text" to detect bridge mode, which broke when MaxToTg
replacements or whitespace made caption differ from raw body.Text —
MAX→TG crossposts then got [MAX] prefix and bold name. Now explicit
isCrosspost flag.
- /bridge without key in an already-linked chat no longer generates a
fresh key; instead shows "already linked" hint with pairing guidance
(both TG and MAX sides).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Default prefix to 0 for new pairs (user can enable via /bridge prefix on)
- Anonymous admins (SenderChat == Chat) recognized as admins
- Log getChatMember errors; show "make bot admin" hint on CHAT_ADMIN_REQUIRED
- Clarify /bridge key message: send in the other group, not in bot DM
- Add /start hint that TG forum supergroups require bot admin
- Add /bridge command logging with chat/user IDs for support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a group owner/admin enables "Remain anonymous", their messages
are sent from @GroupAnonymousBot with msg.SenderChat pointing to the
group itself. GetChatMember was then checking the bot's status (not
admin) and rejecting valid owners with "only for admins".
New helper isTgAnonymousAdmin(msg) returns true when SenderChat.ID
equals Chat.ID; in that case we skip the GetChatMember check and
treat the user as admin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forwardTgToMax built the final text from msg.Text/msg.Caption directly,
discarding the caption parameter that already had replacements applied
in handleTgChannelPost. For single posts (not media groups) in crosspost,
replacements were effectively ignored.
Added isCrosspost flag: when true, use the caption as-is (post-replacements,
no attribution, no markdown format since replacement may have invalidated
entity offsets).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update max.go
* Update telegram.go
* Update format.go
* Update telegram.go
* Implement message queuing for existing pending messages
Added logic to queue messages if there are pending messages for the chat to maintain delivery order.
* Refactor message sending logic for Telegram
* Implement hasPendingForChat function
Add hasPendingForChat function to check for pending messages in the queue.
* Add HasPendingQueue method to repository interface
* Add HasPendingQueue method to sqliteRepo
* Add HasPendingQueue method to check for pending jobs
TG→MAX: tgEntitiesToMarkdown was called on caption with "Name: " prefix,
but entity offsets are relative to raw text — formatting markers landed
in wrong positions. Now entities are converted on raw text first, then
attribution is applied.
MAX→TG: condition `caption == text` was always false for bridge chats
(caption has "Name: " prefix), so markups were never converted to HTML.
Now markups are always applied, with proper HTML-escaped attribution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, editing a TG message with photo/media would send a new
message in MAX. Now it properly calls EditMessage with the updated
attachment when a message mapping exists, falling back to new message
only when no mapping is found.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously all bot messages were skipped. Now only our own bot
is filtered (by TG username / MAX user ID), so messages from
other bots in the group are forwarded normally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New setting per crosspost pair — disabled by default:
- Toggle via inline button "Синк правок" in TG and MAX
- When enabled: edits and deletes sync between channels (respecting direction)
- When disabled (default): only new posts are crossposted
- Migration 000013 adds sync_edits column to crossposts table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same rationale as delete sync — edits between crossposted channels
cause confusion. Now edits only sync for regular bridge pairs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Photo sends via SDK (Messages.SendWithResult) didn't convert
TG entities to markdown or set format field. Text-only sends
used sendMaxDirectFormatted which did both — hence the discrepancy.
Now photo and media group captions convert CaptionEntities to
markdown and call SetFormat("markdown") when formatting is present.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Usage: send /thread in a forum topic to route all MAX messages there.
Send /thread in General or non-forum group to reset to default.
Admin-only, requires linked chat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When TG group disables topics, sending with message_thread_id fails.
Now detected via "message thread not found" / "TOPIC_NOT_FOUND" errors:
- forwardMaxToTg: resets thread_id to 0 and retries immediately
- processQueueMax2Tg: same reset + immediate retry
- /bridge: always saves thread_id (0 = no topics), allowing reset
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WebhookHandler() puts updates into internal channel, but without
StartWebhook() no workers read from it — TG updates were silently lost.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace go-telegram-bot-api/v5 (2021, no forum topics) with
go-telegram/bot v1.20.0 (Bot API 9.5) via TGSender adapter pattern.
- New TGSender interface + tgBotSender implementation isolating library
- Native message_thread_id support: saved at /bridge, used in MAX→TG
sends, echoed in all command responses, looked up in queue retries
- All 16 files migrated, zero old library references remain
- Proper error wrapping: MigrateError, Forbidden, BadRequest, NotFound,
TooManyRequests → TGError with codes
- ForwardFromChat → ForwardOriginChat (Bot API MessageOrigin pattern)
- Repository: GetTgThreadID/SetTgThreadID (migration 000012 already applied)
- Tests for convertMsg, convertCallback, wrapErr, toInputFile,
toLibInputMedia, tgEntitiesToMarkdown, maxMarkupsToHTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Handle MigrateToChatID from TG webhook and from send errors.
Automatically update pairs table with new chat ID and retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Store tg_thread_id in pairs table (from /bridge command location)
- tgSendText/tgSendMediaToThread with message_thread_id support
- Text messages in MAX→TG now sent to correct topic
- Raw HTTP for thread_id since go-telegram-bot-api v5 lacks support
Fixes TOPIC_CLOSED errors in TG forum groups.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- checkUserAllowed() sends "no access" message instead of silent ignore
- Solo media (audio, files, stickers) from MAX→TG now get caption and
reply context when no photo/video was sent before them
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-crosspost configurable find/replace rules applied during forwarding.
Supports string and regex replacements, scoped to full text or links only.
Each direction (TG→MAX, MAX→TG) has its own set of rules.
UI: inline buttons for add/delete/toggle target type in both TG and MAX bots.
Closes#14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Channel crosspost (handleTgChannelPost) was sending each photo in an album
as a separate message. Added MediaGroupID buffering for crosspost channels,
same as regular chats. Also improved error message for album send failures.
Fixes#12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PHOTO type has different upload flow in MAX API (returns PhotoTokens,
not UploadedInfo). Use SDK UploadPhotoFromReader instead of
customUploadToMax for photos.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MAX API cannot reach local TG Bot API server to download photos.
When TG_API_URL is set, download photos ourselves and upload to MAX
via customUploadToMax instead of UploadPhotoFromUrl.
Fixes album and single photo forwarding with custom TG API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MAX→TG: remove premature break that caused only the first attachment
to be sent. Photo and video attachments are now grouped and sent via
SendMediaGroup as a Telegram album. Audio, file, and sticker
attachments are sent individually after the album.
TG→MAX: buffer messages with the same MediaGroupID for 1 second,
then upload and send all photos together in a single MAX message with
multiple AddPhoto() calls. Falls back to individual sends on failure.
Co-authored-by: Z.AI GLM
TG: key and commands wrapped in monospace for easy copy.
Both: bot links included so users can quickly navigate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MAX API EditMessage doesn't support adding attachments,
so when a TG message is edited to include media, we forward
it as a new message instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route all TG API calls and file downloads through local Bot API
server to bypass Telegram API throttling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
msg.From is nil for channel posts. Added tgUserID() safe getter.
Was causing panic loop (385 restarts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>