package main
import (
"encoding/json"
"fmt"
"log/slog"
"regexp"
"strings"
"sync"
maxbot "github.com/max-messenger/max-bot-api-client-go"
maxschemes "github.com/max-messenger/max-bot-api-client-go/schemes"
)
// parseCrosspostReplacements парсит JSON из БД в структуру.
func parseCrosspostReplacements(raw string) CrosspostReplacements {
if raw == "" {
return CrosspostReplacements{}
}
var r CrosspostReplacements
if err := json.Unmarshal([]byte(raw), &r); err != nil {
slog.Warn("failed to parse replacements", "err", err)
return CrosspostReplacements{}
}
return r
}
// marshalCrosspostReplacements сериализует структуру в JSON.
func marshalCrosspostReplacements(r CrosspostReplacements) string {
if len(r.TgToMax) == 0 && len(r.MaxToTg) == 0 {
return ""
}
data, _ := json.Marshal(r)
return string(data)
}
// urlRegex матчит URL в тексте.
var urlRegex = regexp.MustCompile(`https?://[^\s<>"]+`)
// applyReplacements применяет список замен к тексту.
func applyReplacements(text string, rules []Replacement) string {
for _, r := range rules {
if r.From == "" {
continue
}
if r.Target == "links" {
text = applyToLinks(text, r)
} else {
text = applyToAll(text, r)
}
}
return text
}
func applyToAll(text string, r Replacement) string {
if r.Regex {
re, err := regexp.Compile(r.From)
if err != nil {
slog.Warn("invalid replacement regex", "pattern", r.From, "err", err)
return text
}
return re.ReplaceAllString(text, r.To)
}
return strings.ReplaceAll(text, r.From, r.To)
}
func applyToLinks(text string, r Replacement) string {
return urlRegex.ReplaceAllStringFunc(text, func(url string) string {
if r.Regex {
re, err := regexp.Compile(r.From)
if err != nil {
return url
}
return re.ReplaceAllString(url, r.To)
}
return strings.ReplaceAll(url, r.From, r.To)
})
}
// formatReplacementItem форматирует одну замену для отдельного сообщения.
func formatReplacementItem(r Replacement, dir string) string {
dirLabel := "TG → MAX"
if dir == "max>tg" {
dirLabel = "MAX → TG"
}
targetLabel := "весь текст"
if r.Target == "links" {
targetLabel = "только ссылки"
}
return fmt.Sprintf("%s %s\n%s → %s\nТип: %s", dirLabel, replacementTags(r), r.From, r.To, targetLabel)
}
// formatReplacementsHeader формирует заголовок для списка замен.
func formatReplacementsHeader(repl CrosspostReplacements) string {
total := len(repl.TgToMax) + len(repl.MaxToTg)
if total == 0 {
return "🔄 Замен нет.\n\nДобавьте замену — текст в пересылаемых постах будет автоматически заменяться."
}
return fmt.Sprintf("🔄 Замены (%d):", total)
}
// replacementTags возвращает теги для отображения замены.
func replacementTags(r Replacement) string {
var tags []string
if r.Regex {
tags = append(tags, "regex")
}
if r.Target == "links" {
tags = append(tags, "ссылки")
}
if len(tags) == 0 {
return ""
}
return "[" + strings.Join(tags, ", ") + "] "
}
// tgReplacementsKeyboard строит inline-клавиатуру для управления заменами.
func tgReplacementsKeyboard(maxChatID int64) *InlineKeyboardMarkup {
id := fmt.Sprintf("%d", maxChatID)
return NewInlineKeyboard(
NewInlineRow(
NewInlineButton("+ TG→MAX", "cpra:tg>max:"+id),
NewInlineButton("+ MAX→TG", "cpra:max>tg:"+id),
),
NewInlineRow(
NewInlineButton("🗑 Очистить всё", "cprc:"+id),
NewInlineButton("◀ Назад", "cprb:"+id),
),
)
}
// tgReplItemKeyboard — кнопки для одной замены в TG.
func tgReplItemKeyboard(dir string, idx int, maxChatID string, currentTarget string) *InlineKeyboardMarkup {
toggleLabel := "🔗 Только ссылки"
toggleTarget := "links"
if currentTarget == "links" {
toggleLabel = "📝 Весь текст"
toggleTarget = "all"
}
return NewInlineKeyboard(
NewInlineRow(
NewInlineButton(toggleLabel, fmt.Sprintf("cprt:%s:%d:%s:%s", dir, idx, toggleTarget, maxChatID)),
NewInlineButton("❌ Удалить", fmt.Sprintf("cprd:%s:%d:%s", dir, idx, maxChatID)),
),
)
}
// maxReplacementsKeyboard строит inline-клавиатуру для управления заменами в MAX.
func maxReplacementsKeyboard(api *maxbot.Api, maxChatID int64) *maxbot.Keyboard {
id := fmt.Sprintf("%d", maxChatID)
kb := api.Messages.NewKeyboardBuilder()
kb.AddRow().
AddCallback("+ TG→MAX", maxschemes.DEFAULT, "cpra:tg>max:"+id).
AddCallback("+ MAX→TG", maxschemes.DEFAULT, "cpra:max>tg:"+id)
kb.AddRow().
AddCallback("🗑 Очистить всё", maxschemes.NEGATIVE, "cprc:"+id).
AddCallback("◀ Назад", maxschemes.DEFAULT, "cprb:"+id)
return kb
}
// maxReplItemKeyboard — кнопки для одной замены в MAX.
func maxReplItemKeyboard(api *maxbot.Api, dir string, idx int, maxChatID string, currentTarget string) *maxbot.Keyboard {
toggleLabel := "🔗 Только ссылки"
toggleTarget := "links"
if currentTarget == "links" {
toggleLabel = "📝 Весь текст"
toggleTarget = "all"
}
kb := api.Messages.NewKeyboardBuilder()
kb.AddRow().
AddCallback(toggleLabel, maxschemes.DEFAULT, fmt.Sprintf("cprt:%s:%d:%s:%s", dir, idx, toggleTarget, maxChatID)).
AddCallback("❌ Удалить", maxschemes.NEGATIVE, fmt.Sprintf("cprd:%s:%d:%s", dir, idx, maxChatID))
return kb
}
// replWait хранит состояние ожидания ввода замены.
type replWait struct {
maxChatID int64
direction string // "tg>max" or "max>tg"
target string // "all" or "links"
}
// replWaitMap — глобальное хранилище ожиданий (по userID).
var (
replWaits = make(map[int64]replWait)
replWaitsMu sync.Mutex
)
func (b *Bridge) setReplWait(userID, maxChatID int64, direction, target string) {
replWaitsMu.Lock()
replWaits[userID] = replWait{maxChatID: maxChatID, direction: direction, target: target}
replWaitsMu.Unlock()
}
func (b *Bridge) getReplWait(userID int64) (replWait, bool) {
replWaitsMu.Lock()
w, ok := replWaits[userID]
replWaitsMu.Unlock()
return w, ok
}
func (b *Bridge) clearReplWait(userID int64) {
replWaitsMu.Lock()
delete(replWaits, userID)
replWaitsMu.Unlock()
}
// parseReplacementInput парсит ввод пользователя "from | to" или "/regex/ | to".
func parseReplacementInput(input string) (Replacement, bool) {
idx := strings.Index(input, "|")
if idx < 0 {
return Replacement{}, false
}
from := strings.TrimSpace(input[:idx])
to := strings.TrimSpace(input[idx+1:])
if from == "" {
return Replacement{}, false
}
// Regex: /pattern/
isRegex := false
if len(from) >= 2 && from[0] == '/' && from[len(from)-1] == '/' {
from = from[1 : len(from)-1]
isRegex = true
// Проверяем что regex валидный
if _, err := regexp.Compile(from); err != nil {
return Replacement{}, false
}
}
return Replacement{From: from, To: to, Regex: isRegex}, true
}