max-telegram-bridge-bot/repository.go
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

110 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import "errors"
// errThreadMaxBusy — попытка thread-bridge на MAX-чат, уже участвующий в какой-то связке.
var errThreadMaxBusy = errors.New("max chat is already linked (bridge or thread-bridge)")
// Replacement — одно правило замены текста.
// Target: "" или "all" — весь текст, "links" — только ссылки.
type Replacement struct {
From string `json:"from"`
To string `json:"to"`
Regex bool `json:"regex"`
Target string `json:"target,omitempty"`
}
// CrosspostReplacements — замены по направлениям.
type CrosspostReplacements struct {
TgToMax []Replacement `json:"tg>max,omitempty"`
MaxToTg []Replacement `json:"max>tg,omitempty"`
}
// CrosspostLink — одна связка кросспостинга.
type CrosspostLink struct {
TgChatID int64
MaxChatID int64
Direction string
}
// Repository — абстракция хранилища для bridge.
type Repository interface {
// Register обрабатывает /bridge команду.
// Без ключа — создаёт pending запись и возвращает сгенерированный ключ.
// С ключом — ищет пару и создаёт связку.
Register(key, platform string, chatID int64) (paired bool, generatedKey string, err error)
GetMaxChat(tgChatID int64) (int64, bool)
GetTgChat(maxChatID int64) (int64, bool)
MigrateTgChat(oldID, newID int64) error
SaveMsg(tgChatID int64, tgMsgID int, maxChatID int64, maxMsgID string, tgThreadID int)
LookupMaxMsgID(tgChatID int64, tgMsgID int) (string, bool)
LookupTgMsgID(maxMsgID string) (tgChatID int64, tgMsgID int, tgThreadID int, ok bool)
CleanOldMessages()
HasPrefix(platform string, chatID int64) bool
SetPrefix(platform string, chatID int64, on bool) bool
Unpair(platform string, chatID int64) bool
GetTgThreadID(tgChatID int64) int
SetTgThreadID(tgChatID int64, threadID int) error
// Thread-bridge: связка отдельного TG-треда с отдельным MAX-чатом.
// Ключ выдаётся в TG-треде (StartThreadBridge), принимается в MAX (CompleteThreadBridge).
StartThreadBridge(tgChatID int64, threadID int) (key string, err error)
CompleteThreadBridge(key string, maxChatID int64) (tgChatID int64, threadID int, ok bool, err error)
GetThreadMaxChat(tgChatID int64, threadID int) (maxChatID int64, ok bool)
GetThreadTgPair(maxChatID int64) (tgChatID int64, threadID int, ok bool)
UnpairThread(tgChatID int64, threadID int) bool
UnpairThreadByMax(maxChatID int64) bool
// Crosspost methods
PairCrosspost(tgChatID, maxChatID, ownerID, tgOwnerID int64) error
GetCrosspostOwner(maxChatID int64) (maxOwner, tgOwner int64)
GetCrosspostMaxChat(tgChatID int64) (maxChatID int64, direction string, ok bool)
GetCrosspostTgChat(maxChatID int64) (tgChatID int64, direction string, ok bool)
ListCrossposts(ownerID int64) []CrosspostLink
SetCrosspostDirection(maxChatID int64, direction string) bool
UnpairCrosspost(maxChatID, deletedBy int64) bool
GetCrosspostReplacements(maxChatID int64) CrosspostReplacements
SetCrosspostReplacements(maxChatID int64, repl CrosspostReplacements) error
GetCrosspostSyncEdits(maxChatID int64) bool
SetCrosspostSyncEdits(maxChatID int64, on bool) error
// Users
TouchUser(userID int64, platform, username, firstName string)
ListUsers(platform string) ([]int64, error)
// Send queue (retry при недоступности MAX/TG API)
EnqueueSend(item *QueueItem) error
PeekQueue(limit int) ([]QueueItem, error)
DeleteFromQueue(id int64) error
IncrementAttempt(id int64, nextRetry int64) error
// HasPendingQueue возвращает true если для данного dst-чата есть незавершённые элементы.
// Используется для сохранения порядка: новые сообщения тоже идут через очередь,
// пока предыдущие не доставлены.
HasPendingQueue(direction string, dstChatID int64) bool
Close() error
}
// QueueItem — сообщение в очереди на повторную отправку.
type QueueItem struct {
ID int64
Direction string // "tg2max" or "max2tg"
SrcChatID int64
DstChatID int64
SrcMsgID string // TG msg ID (as string) or MAX mid
Text string
AttType string // "video", "file", "audio", ""
AttToken string
ReplyTo string
Format string
AttURL string // URL медиа (для MAX→TG)
ParseMode string // "HTML" или ""
Attempts int
CreatedAt int64
NextRetry int64
}