140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const telegramDefaultTopic = "General"
|
|
|
|
type activeTelegramTopic struct {
|
|
Conversation string
|
|
Label string
|
|
}
|
|
|
|
func normalizeTelegramTopicLabel(raw string) (string, error) {
|
|
label := strings.Join(strings.Fields(strings.TrimSpace(raw)), " ")
|
|
if label == "" {
|
|
return "", errors.New("topic name is empty")
|
|
}
|
|
if utf8.RuneCountInString(label) > 48 {
|
|
return "", errors.New("topic name is too long")
|
|
}
|
|
for _, character := range label {
|
|
if unicode.IsControl(character) {
|
|
return "", errors.New("topic name contains control characters")
|
|
}
|
|
}
|
|
return label, nil
|
|
}
|
|
|
|
func telegramTopicID(label string) string {
|
|
if strings.EqualFold(strings.TrimSpace(label), telegramDefaultTopic) {
|
|
return "general"
|
|
}
|
|
digest := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(label))))
|
|
return hex.EncodeToString(digest[:8])
|
|
}
|
|
|
|
func telegramTopicConversation(topicID string) string {
|
|
if topicID == "" || topicID == "general" {
|
|
// Preserve the original named conversation so existing Telegram context
|
|
// survives rollout into topic-aware routing.
|
|
return "telegram"
|
|
}
|
|
return "telegram-topic-" + topicID
|
|
}
|
|
|
|
func (router *tenantRouter) activeTelegramTopic(userID string) activeTelegramTopic {
|
|
identity := router.identityHash("telegram", userID)
|
|
router.mu.Lock()
|
|
defer router.mu.Unlock()
|
|
state := router.state.TelegramTopics[identity]
|
|
topicID := state.Active
|
|
if topicID == "" {
|
|
topicID = "general"
|
|
}
|
|
label := telegramDefaultTopic
|
|
if topic, found := state.Topics[topicID]; found && strings.TrimSpace(topic.Label) != "" {
|
|
label = topic.Label
|
|
}
|
|
return activeTelegramTopic{Conversation: telegramTopicConversation(topicID), Label: label}
|
|
}
|
|
|
|
func (router *tenantRouter) selectTelegramTopic(userID, rawLabel string) (activeTelegramTopic, error) {
|
|
label, err := normalizeTelegramTopicLabel(rawLabel)
|
|
if err != nil {
|
|
return activeTelegramTopic{}, err
|
|
}
|
|
identity := router.identityHash("telegram", userID)
|
|
topicID := telegramTopicID(label)
|
|
if topicID == "general" {
|
|
label = telegramDefaultTopic
|
|
}
|
|
router.mu.Lock()
|
|
defer router.mu.Unlock()
|
|
state := router.state.TelegramTopics[identity]
|
|
if state.Topics == nil {
|
|
state.Topics = map[string]telegramTopic{}
|
|
}
|
|
state.Active = topicID
|
|
state.Topics[topicID] = telegramTopic{Label: label, LastUsed: router.now().Unix()}
|
|
router.state.TelegramTopics[identity] = state
|
|
if err := router.saveLocked(); err != nil {
|
|
return activeTelegramTopic{}, err
|
|
}
|
|
return activeTelegramTopic{Conversation: telegramTopicConversation(topicID), Label: label}, nil
|
|
}
|
|
|
|
func (router *tenantRouter) touchTelegramTopic(userID string) (activeTelegramTopic, error) {
|
|
identity := router.identityHash("telegram", userID)
|
|
router.mu.Lock()
|
|
defer router.mu.Unlock()
|
|
state := router.state.TelegramTopics[identity]
|
|
if state.Topics == nil {
|
|
state.Topics = map[string]telegramTopic{}
|
|
}
|
|
topicID := state.Active
|
|
if topicID == "" {
|
|
topicID = "general"
|
|
state.Active = topicID
|
|
}
|
|
topic := state.Topics[topicID]
|
|
if strings.TrimSpace(topic.Label) == "" {
|
|
topic.Label = telegramDefaultTopic
|
|
}
|
|
topic.LastUsed = router.now().Unix()
|
|
state.Topics[topicID] = topic
|
|
router.state.TelegramTopics[identity] = state
|
|
if err := router.saveLocked(); err != nil {
|
|
return activeTelegramTopic{}, err
|
|
}
|
|
return activeTelegramTopic{Conversation: telegramTopicConversation(topicID), Label: topic.Label}, nil
|
|
}
|
|
|
|
func (router *tenantRouter) telegramTopics(userID string) []telegramTopic {
|
|
identity := router.identityHash("telegram", userID)
|
|
router.mu.Lock()
|
|
defer router.mu.Unlock()
|
|
state := router.state.TelegramTopics[identity]
|
|
topics := make([]telegramTopic, 0, len(state.Topics)+1)
|
|
for _, topic := range state.Topics {
|
|
topics = append(topics, topic)
|
|
}
|
|
if _, found := state.Topics["general"]; !found {
|
|
topics = append(topics, telegramTopic{Label: telegramDefaultTopic})
|
|
}
|
|
sort.SliceStable(topics, func(left, right int) bool {
|
|
if topics[left].LastUsed == topics[right].LastUsed {
|
|
return strings.ToLower(topics[left].Label) < strings.ToLower(topics[right].Label)
|
|
}
|
|
return topics[left].LastUsed > topics[right].LastUsed
|
|
})
|
|
return topics
|
|
}
|