mirror of
https://github.com/BEARlogin/max-telegram-bridge-bot.git
synced 2026-04-28 03:39:46 +00:00
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>
110 lines
4.6 KiB
Go
110 lines
4.6 KiB
Go
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
|
||
}
|