Add /reload, cache channel access hashes
This commit is contained in:
parent
ddd5eafe4e
commit
064f0e35b1
1 changed files with 108 additions and 125 deletions
211
bot.go
211
bot.go
|
|
@ -12,6 +12,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/session"
|
||||
"github.com/gotd/td/telegram"
|
||||
|
|
@ -42,6 +43,8 @@ type Config struct {
|
|||
YAMLPatterns []YAMLScamPattern `yaml:"scam_patterns"`
|
||||
BadWords []string `yaml:"bad_words"`
|
||||
Patterns []scamPattern `yaml:"-"` // Compiled patterns, not from YAML
|
||||
MonitoredChatAccessHash int64 `yaml:"-"` // Runtime: access hash for monitored channel
|
||||
AlertChatAccessHash int64 `yaml:"-"` // Runtime: access hash for alert channel
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
|
|
@ -183,11 +186,7 @@ func (s *BotState) compileBadWords() {
|
|||
s.badWordRe = nil
|
||||
return
|
||||
}
|
||||
escaped := make([]string, len(s.cfg.BadWords))
|
||||
for i, w := range s.cfg.BadWords {
|
||||
escaped[i] = regexp.QuoteMeta(w)
|
||||
}
|
||||
s.badWordRe = regexp.MustCompile(`(?i)(` + strings.Join(escaped, "|") + `)`)
|
||||
s.badWordRe = regexp.MustCompile(`(?i)(` + strings.Join(s.cfg.BadWords, "|") + `)`)
|
||||
log.Printf("✓ Bad word pattern: %s", s.badWordRe)
|
||||
}
|
||||
|
||||
|
|
@ -199,110 +198,6 @@ func (s *BotState) saveConfig() error {
|
|||
return os.WriteFile(s.configPath, data, 0644)
|
||||
}
|
||||
|
||||
func sendAdminReply(ctx context.Context, api *tg.Client, alertPeer *tg.InputPeerChannel, text string) {
|
||||
sender := message.NewSender(api)
|
||||
_, err := sender.To(alertPeer).Text(ctx, text)
|
||||
if err != nil {
|
||||
log.Printf("Failed to send admin reply: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOwnerCommand(ctx context.Context, api *tg.Client, state *BotState, alertPeer *tg.InputPeerChannel, text string) {
|
||||
text = strings.TrimSpace(text)
|
||||
if !strings.HasPrefix(text, "/badword") {
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(text, " ", 3)
|
||||
if len(parts) < 2 {
|
||||
sendAdminReply(ctx, api, alertPeer, "Usage: /badword <list|add|rem> [word]")
|
||||
return
|
||||
}
|
||||
|
||||
subCmd := strings.ToLower(parts[1])
|
||||
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
|
||||
switch subCmd {
|
||||
case "list":
|
||||
if len(state.cfg.BadWords) == 0 {
|
||||
sendAdminReply(ctx, api, alertPeer, "No bad words configured.")
|
||||
return
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Bad words:\n")
|
||||
for _, w := range state.cfg.BadWords {
|
||||
sb.WriteString(" • ")
|
||||
sb.WriteString(w)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
reply := sb.String()
|
||||
if state.badWordRe != nil {
|
||||
reply += "\nPattern: " + state.badWordRe.String()
|
||||
}
|
||||
sendAdminReply(ctx, api, alertPeer, strings.TrimRight(reply, "\n"))
|
||||
|
||||
case "add":
|
||||
if len(parts) < 3 {
|
||||
sendAdminReply(ctx, api, alertPeer, "Usage: /badword add <word>")
|
||||
return
|
||||
}
|
||||
word := strings.TrimSpace(parts[2])
|
||||
for _, w := range state.cfg.BadWords {
|
||||
if strings.EqualFold(w, word) {
|
||||
sendAdminReply(ctx, api, alertPeer, fmt.Sprintf("Word %q already exists.", word))
|
||||
return
|
||||
}
|
||||
}
|
||||
state.cfg.BadWords = append(state.cfg.BadWords, word)
|
||||
state.compileBadWords()
|
||||
if err := state.saveConfig(); err != nil {
|
||||
log.Printf("Failed to save config: %v", err)
|
||||
sendAdminReply(ctx, api, alertPeer, fmt.Sprintf("Added %q but failed to save config: %v", word, err))
|
||||
return
|
||||
}
|
||||
reply := fmt.Sprintf("Added bad word: %q", word)
|
||||
if state.badWordRe != nil {
|
||||
reply += "\nPattern: " + state.badWordRe.String()
|
||||
}
|
||||
sendAdminReply(ctx, api, alertPeer, reply)
|
||||
|
||||
case "rem":
|
||||
if len(parts) < 3 {
|
||||
sendAdminReply(ctx, api, alertPeer, "Usage: /badword rem <word>")
|
||||
return
|
||||
}
|
||||
word := strings.TrimSpace(parts[2])
|
||||
newWords := make([]string, 0, len(state.cfg.BadWords))
|
||||
found := false
|
||||
for _, w := range state.cfg.BadWords {
|
||||
if strings.EqualFold(w, word) {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newWords = append(newWords, w)
|
||||
}
|
||||
if !found {
|
||||
sendAdminReply(ctx, api, alertPeer, fmt.Sprintf("Word %q not found.", word))
|
||||
return
|
||||
}
|
||||
state.cfg.BadWords = newWords
|
||||
state.compileBadWords()
|
||||
if err := state.saveConfig(); err != nil {
|
||||
log.Printf("Failed to save config: %v", err)
|
||||
}
|
||||
patternStr := "none"
|
||||
if state.badWordRe != nil {
|
||||
patternStr = state.badWordRe.String()
|
||||
}
|
||||
sendAdminReply(ctx, api, alertPeer, fmt.Sprintf("Removed bad word: %q\nPattern: %s", word, patternStr))
|
||||
|
||||
default:
|
||||
sendAdminReply(ctx, api, alertPeer, "Unknown subcommand. Usage: /badword <list|add|rem> [word]")
|
||||
}
|
||||
}
|
||||
|
||||
func keywordMatchScore(input string, patterns []scamPattern) matchResult {
|
||||
matched := make(map[string][]string)
|
||||
totalScore := 0.0
|
||||
|
|
@ -470,6 +365,92 @@ func toPeerInput(peer tg.PeerClass, chats []tg.ChatClass) (tg.InputPeerClass, er
|
|||
}
|
||||
}
|
||||
|
||||
func getUserJoinDate(ctx context.Context, api *tg.Client, channelID, accessHash, userID int64) (time.Time, error) {
|
||||
result, err := api.ChannelsGetParticipant(ctx, &tg.ChannelsGetParticipantRequest{
|
||||
Channel: &tg.InputChannel{
|
||||
ChannelID: channelID,
|
||||
AccessHash: accessHash,
|
||||
},
|
||||
Participant: &tg.InputPeerUser{
|
||||
UserID: userID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
// Get the date from the participant (works for most participant types)
|
||||
switch p := result.Participant.(type) {
|
||||
case interface{ GetDate() int }:
|
||||
return time.Unix(int64(p.GetDate()), 0), nil
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("cannot determine join date for participant type: %T", result.Participant)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOwnerMessage(ctx context.Context, api *tg.Client, state *BotState, msg *tg.Message, entities tg.Entities) error {
|
||||
text := strings.TrimSpace(msg.Message)
|
||||
|
||||
switch text {
|
||||
case "/reload":
|
||||
cfg, err := loadConfig(state.configPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to reload config: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
state.mu.Lock()
|
||||
state.cfg = cfg
|
||||
state.compileBadWords()
|
||||
state.mu.Unlock()
|
||||
|
||||
var inputPeer tg.InputPeerClass
|
||||
|
||||
// Handle channel peer
|
||||
if peerChan, ok := msg.PeerID.(*tg.PeerChannel); ok {
|
||||
if channel, ok := entities.Channels[int64(peerChan.ChannelID)]; ok {
|
||||
inputPeer = &tg.InputPeerChannel{
|
||||
ChannelID: int64(peerChan.ChannelID),
|
||||
AccessHash: channel.AccessHash,
|
||||
}
|
||||
} else {
|
||||
log.Printf("Channel %d not found in entities", peerChan.ChannelID)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Fallback for other peer types
|
||||
var chats []tg.ChatClass
|
||||
for _, chat := range entities.Chats {
|
||||
chats = append(chats, chat)
|
||||
}
|
||||
inputPeer, err = toPeerInput(msg.PeerID, chats)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert peer: %v", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err = api.MessagesSendReaction(ctx, &tg.MessagesSendReactionRequest{
|
||||
Peer: inputPeer,
|
||||
MsgID: msg.ID,
|
||||
Reaction: []tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "👍"}},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to set reaction: %v", err)
|
||||
}
|
||||
|
||||
case "/test":
|
||||
joinDate, err := getUserJoinDate(ctx, api, state.cfg.MonitoredChat, state.cfg.MonitoredChatAccessHash, state.selfID)
|
||||
if err != nil {
|
||||
log.Printf("getUserJoinDate error: %v", err)
|
||||
} else {
|
||||
log.Printf("✓ Join date: %v", joinDate)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractMessage(update any) *tg.Message {
|
||||
switch u := update.(type) {
|
||||
case *tg.UpdateNewMessage:
|
||||
|
|
@ -490,10 +471,9 @@ func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPe
|
|||
return nil
|
||||
}
|
||||
|
||||
// Handle commands from the bot owner (messages sent by the connected account).
|
||||
// Handle owner commands.
|
||||
if msg.Out {
|
||||
handleOwnerCommand(ctx, api, state, alertPeer, msg.Message)
|
||||
return nil
|
||||
return handleOwnerMessage(ctx, api, state, msg, entities)
|
||||
}
|
||||
|
||||
// Check if from monitored supergroup (channels in gotd terminology)
|
||||
|
|
@ -608,14 +588,14 @@ func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPe
|
|||
}
|
||||
|
||||
chatName := channel.Title
|
||||
chatDisplay := escapeMarkdown(chatName) + escapeMarkdown(formatUsername(channel.Username))
|
||||
chatDisplay := chatName + formatUsername(channel.Username)
|
||||
|
||||
matchMessageHTML := fmt.Sprintf("🚨 Matched\n<b>Score</b>: %.2f\n<b>Chat</b>: %s (ID: %d)\n<b>User</b>: %s (ID: %d)\n",
|
||||
result.score, chatDisplay, chatID, userDisplay, senderID)
|
||||
|
||||
if state.cfg.NtfyToken != "" || state.cfg.NtfyTopic != "" {
|
||||
plainMessage := fmt.Sprintf("🚨 Matched\nScore: %.2f\nChat: %s (ID: %d)\nUser: %s (ID: %d)\n\n%s",
|
||||
result.score, chatName+formatUsername(channel.Username), chatID, userDisplay, senderID, msg.Message)
|
||||
result.score, chatDisplay, chatID, userDisplay, senderID, msg.Message)
|
||||
notify(plainMessage, state.cfg, fmt.Sprintf("Scam Alert: %s", chatName))
|
||||
}
|
||||
|
||||
|
|
@ -696,10 +676,13 @@ func main() {
|
|||
if err != nil {
|
||||
return fmt.Errorf("resolving alert channel: %w", err)
|
||||
}
|
||||
cfg.AlertChatAccessHash = alertChannel.AccessHash
|
||||
|
||||
_, monitoredChannel, err := resolveChannel(ctx, api, cfg.MonitoredChat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving monitored channel: %w", err)
|
||||
}
|
||||
cfg.MonitoredChatAccessHash = monitoredChannel.AccessHash
|
||||
|
||||
// Get self ID for state and updates manager
|
||||
status, err := client.Auth().Status(ctx)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue