Commit graph

80 commits

Author SHA1 Message Date
Andrey Lugovskoy
8b80bb8ff1 Route crosspost error notifications to owner DM, not to the channel
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>
2026-04-24 16:29:52 +04:00
Andrey Lugovskoy
63b95e4c8c Preserve links in TG→MAX crossposts; deliver text when video fails
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>
2026-04-24 16:26:24 +04:00
Andrey Lugovskoy
9b7e3cd165 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>
2026-04-23 21:22:45 +04:00
Andrey Lugovskoy
825bfee473 Surface TG→MAX upload errors in chat (not only in log)
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>
2026-04-23 19:01:18 +04:00
Andrey Lugovskoy
1290a23ad7 Add /thread_bridge: link a single TG forum thread to a separate MAX chat
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>
2026-04-23 17:39:00 +04:00
Andrey Lugovskoy
46d2f4f748 Route MAX replies to source TG thread + crosspost/UX fixes
- 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>
2026-04-23 15:26:44 +04:00
Andrey Lugovskoy
38e5777798 Default prefix off, clarify admin requirement for TG forum groups
Some checks failed
Build / build (push) Has been cancelled
- 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>
2026-04-20 17:54:28 +04:00
Andrey Lugovskoy
adb5727c2a Treat anonymous admins as admins for /bridge, /thread etc.
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>
2026-04-20 17:31:37 +04:00
Andrey Lugovskoy
111e4a8c0d Apply replacements in crosspost forwardTgToMax
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>
2026-04-20 17:06:57 +04:00
artemws
bfce1c66d2
Очередь отправки (#34)
* 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
2026-04-16 21:14:42 +04:00
Andrey Lugovskoy
3bd42a5a5e Fix formatting in both TG→MAX and MAX→TG directions
Some checks are pending
Build / build (push) Waiting to run
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>
2026-04-08 18:40:37 +03:00
Andrey Lugovskoy
ec166754c3 Edit messages with media in MAX instead of sending new ones (TG→MAX)
Some checks failed
Build / build (push) Has been cancelled
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>
2026-04-03 14:29:16 +03:00
Andrey Lugovskoy
2fc7e903cf Allow other bots' messages through bridge, filter only self
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>
2026-04-03 11:45:29 +03:00
Andrey Lugovskoy
0eb19f66bb Add sync_edits toggle for crosspost edit/delete sync
Some checks are pending
Build / build (push) Waiting to run
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>
2026-04-02 22:52:55 +03:00
Andrey Lugovskoy
01e0c83bdc Disable edit sync for crosspost channels
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>
2026-04-02 22:47:26 +03:00
Andrey Lugovskoy
9e0a72ea35 Fix caption formatting lost on photo/album crosspost TG→MAX
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>
2026-04-02 21:31:50 +03:00
Andrey Lugovskoy
c8408a2608 Add /thread command to set default topic for MAX→TG messages
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>
2026-04-02 18:10:56 +03:00
Andrey Lugovskoy
19e7ca62e8 Handle forum topics disabled: reset thread_id and retry
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>
2026-04-02 17:56:53 +03:00
Andrey Lugovskoy
856a01e5ee Fix TG webhook: start library workers for update dispatch
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>
2026-04-02 17:52:11 +03:00
Andrey Lugovskoy
f8e4ff0ebd Migrate TG library to go-telegram/bot with forum topic support
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>
2026-04-02 17:48:33 +03:00
Andrey Lugovskoy
0a7e43b708 Strip @botname from commands in groups
/bridge@MaxTelegramBridgeBot now correctly parsed as /bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:54:11 +03:00
Andrey Lugovskoy
164ecddc37 Auto-migrate TG chat ID when group upgrades to supergroup
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>
2026-04-02 12:49:18 +03:00
Andrey Lugovskoy
8b8956af49 Revert "Add forum topic (thread_id) support for TG groups"
This reverts commit 7f5044a7fa.
2026-04-02 12:38:17 +03:00
Andrey Lugovskoy
7f5044a7fa Add forum topic (thread_id) support for TG groups
- 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>
2026-04-02 12:34:57 +03:00
Andrey Lugovskoy
6a98c424dc Add text replacements info to /help in both TG and MAX bots
Some checks are pending
Build / build (push) Waiting to run
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:19:22 +03:00
artemws
4fe0f71736
Update telegram.go 2026-03-28 20:21:44 +02:00
artemws
ef983b0815
Update telegram.go 2026-03-28 07:55:31 +02:00
artemws
dbdb5bdd5c
Update telegram.go 2026-03-28 07:36:39 +02:00
artemws
83349cc00e
Update telegram.go 2026-03-27 15:41:55 +02:00
artemws
a51efa2418
Merge branch 'master' into notify_tlg_name 2026-03-27 14:46:35 +02:00
artemws
56c569a0b2
Update telegram.go 2026-03-27 14:42:06 +02:00
Andrey Lugovskoy
2d6d10e60b Fix: ALLOWED_USERS sends denial message, solo media gets caption
Some checks are pending
Build / build (push) Waiting to run
- 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>
2026-03-27 14:21:11 +03:00
artemws
cf6398403f
Update telegram.go 2026-03-27 07:48:31 +02:00
artemws
364aab9730
Update telegram.go 2026-03-26 18:25:43 +02:00
Andrey Lugovskoy
9b207ba7d8 Add text replacements for crosspost channels
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>
2026-03-26 12:18:53 +03:00
Andrey Lugovskoy
ba42a34e2c Fix crosspost channel albums: buffer media groups instead of sending individually
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>
2026-03-25 15:01:06 +03:00
Andrey Lugovskoy
b0f2b79f53 Fix photo upload: use UploadPhotoFromReader for local TG API
Some checks are pending
Build / build (push) Waiting to run
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>
2026-03-25 12:17:59 +03:00
Andrey Lugovskoy
0e4550dcce Fix photo upload for local TG Bot API: download and upload ourselves
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>
2026-03-25 12:15:11 +03:00
Andrey Lugovskoy
2aea852b18 Fix nil pointer crashes, add video albums, harden ALLOWED_USERS
Some checks are pending
Build / build (push) Waiting to run
Critical fixes (nil dereference prevention):
- Guard msg.From.ID in debug log with tgUserID()
- Guard tgName() for channel posts (msg.From == nil)
- Guard handleTgCallback for nil query.Message/From

Security:
- Add ALLOWED_USERS check to /unbridge command

Media groups:
- Support video in TG→MAX albums (was photos-only)
- Fix race condition in bufferMediaGroup (append before timer)
- Remove unused struct fields (fired, chatID)

Other:
- Case-insensitive LOG_LEVEL parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:03:09 +03:00
BEARlogin
939a4e5bd4
Merge pull request #5 from mikhailnov/set-log-level
Add ability to configure log level and users whitelist
2026-03-24 21:20:45 +03:00
Mikhail Novosyolov
ab4f7556d2 Fix multi-image forwarding: send all attachments and group albums
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
2026-03-24 17:36:54 +03:00
Mikhail Novosyolov
7dd2abe6c0 Add ALLOWED_USERS whitelist to restrict bridge and crosspost setup
Co-authored-by: Z.AI GLM
2026-03-24 15:01:58 +03:00
Andrey Lugovskoy
e57ec17af8 Clarify crosspost instruction: forward to MAX bot DM
Some checks failed
Build / build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:25:07 +03:00
Andrey Lugovskoy
88bcc20ef5 Use HTML parse mode for copyable code blocks in bot messages
Markdown broke on negative channel IDs. HTML <code> works correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:24:05 +03:00
Andrey Lugovskoy
f3e3886ec5 Fix broken crosspost: remove Markdown parsing from bot messages
Markdown with negative channel IDs caused TG API errors,
silently breaking crosspost setup flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:23:11 +03:00
Andrey Lugovskoy
9b047a856c Add copyable code blocks and bot links to bridge instructions
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>
2026-03-23 12:19:32 +03:00
Andrey Lugovskoy
0d4ece7a5f Handle edited messages with media: forward as new message
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>
2026-03-21 16:35:41 +03:00
Andrey Lugovskoy
8bfcd96838 Support custom TG Bot API server (TG_API_URL env)
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>
2026-03-20 11:26:41 +03:00
Andrey Lugovskoy
3e24939dd1 Don't retry permanent errors (403/404) in send queue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:53:04 +03:00
Andrey Lugovskoy
7800882950 Fix nil pointer crash: channel posts have no From field
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>
2026-03-20 10:42:58 +03:00