code refactor
This commit is contained in:
parent
20048736eb
commit
cf6e27539b
3 changed files with 423 additions and 245 deletions
584
bot.go
584
bot.go
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gotd/td/session"
|
"github.com/gotd/td/session"
|
||||||
"github.com/gotd/td/telegram"
|
"github.com/gotd/td/telegram"
|
||||||
|
|
@ -39,6 +40,7 @@ type Config struct {
|
||||||
NtfyTopic string `yaml:"ntfy_topic"`
|
NtfyTopic string `yaml:"ntfy_topic"`
|
||||||
SessionPath string `yaml:"session_path"`
|
SessionPath string `yaml:"session_path"`
|
||||||
YAMLPatterns []YAMLScamPattern `yaml:"scam_patterns"`
|
YAMLPatterns []YAMLScamPattern `yaml:"scam_patterns"`
|
||||||
|
BadWords []string `yaml:"bad_words"`
|
||||||
Patterns []scamPattern `yaml:"-"` // Compiled patterns, not from YAML
|
Patterns []scamPattern `yaml:"-"` // Compiled patterns, not from YAML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +58,7 @@ ntfy_token: ""
|
||||||
ntfy_topic: ""
|
ntfy_topic: ""
|
||||||
session_path: "antiscam.session"
|
session_path: "antiscam.session"
|
||||||
scam_patterns: []
|
scam_patterns: []
|
||||||
|
bad_words: []
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(path, []byte(defaultConfig), 0644); err != nil {
|
if err := os.WriteFile(path, []byte(defaultConfig), 0644); err != nil {
|
||||||
return nil, fmt.Errorf("creating default config file: %w", err)
|
return nil, fmt.Errorf("creating default config file: %w", err)
|
||||||
|
|
@ -158,6 +161,148 @@ var scamPatterns = []scamPattern{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BotState holds shared runtime state that may be updated at runtime.
|
||||||
|
type BotState struct {
|
||||||
|
cfg *Config
|
||||||
|
configPath string
|
||||||
|
selfID int64
|
||||||
|
badWordRe *regexp.Regexp
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBotState(cfg *Config, configPath string, selfID int64) *BotState {
|
||||||
|
s := &BotState{cfg: cfg, configPath: configPath, selfID: selfID}
|
||||||
|
s.compileBadWords()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileBadWords rebuilds the bad word regex from cfg.BadWords.
|
||||||
|
// Must be called with mu held (write).
|
||||||
|
func (s *BotState) compileBadWords() {
|
||||||
|
if len(s.cfg.BadWords) == 0 {
|
||||||
|
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, "|") + `)`)
|
||||||
|
log.Printf("✓ Bad word pattern: %s", s.badWordRe)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BotState) saveConfig() error {
|
||||||
|
data, err := yaml.Marshal(s.cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling config: %w", err)
|
||||||
|
}
|
||||||
|
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.Fields(text)
|
||||||
|
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 := 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 := 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 {
|
func keywordMatchScore(input string, patterns []scamPattern) matchResult {
|
||||||
matched := make(map[string][]string)
|
matched := make(map[string][]string)
|
||||||
totalScore := 0.0
|
totalScore := 0.0
|
||||||
|
|
@ -195,33 +340,23 @@ func escapeMarkdown(text string) string {
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeHTML(text string) string {
|
func fullName(first, last string) string {
|
||||||
var result strings.Builder
|
return strings.TrimSpace(first + " " + last)
|
||||||
for _, r := range text {
|
|
||||||
switch r {
|
|
||||||
case '&':
|
|
||||||
result.WriteString("&")
|
|
||||||
case '<':
|
|
||||||
result.WriteString("<")
|
|
||||||
case '>':
|
|
||||||
result.WriteString(">")
|
|
||||||
case '"':
|
|
||||||
result.WriteString(""")
|
|
||||||
case '\'':
|
|
||||||
result.WriteString("'")
|
|
||||||
default:
|
|
||||||
result.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(message, host, topic, title string, priority int, token string) {
|
func formatUsername(username string) string {
|
||||||
if !strings.HasSuffix(host, "/") {
|
if username == "" {
|
||||||
host = host + "/"
|
return ""
|
||||||
}
|
}
|
||||||
url := host + topic
|
return " (@" + username + ")"
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBufferString(message))
|
}
|
||||||
|
|
||||||
|
func notify(body string, cfg *Config, title string) {
|
||||||
|
host := cfg.NtfyHost
|
||||||
|
if !strings.HasSuffix(host, "/") {
|
||||||
|
host += "/"
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", host+cfg.NtfyTopic, bytes.NewBufferString(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("notify: creating request: %v", err)
|
log.Printf("notify: creating request: %v", err)
|
||||||
return
|
return
|
||||||
|
|
@ -229,11 +364,10 @@ func notify(message, host, topic, title string, priority int, token string) {
|
||||||
if title != "" {
|
if title != "" {
|
||||||
req.Header.Set("Title", title)
|
req.Header.Set("Title", title)
|
||||||
}
|
}
|
||||||
req.Header.Set("Priority", fmt.Sprintf("%d", priority))
|
req.Header.Set("Priority", "5")
|
||||||
req.Header.Set("Markdown", "yes")
|
req.Header.Set("Markdown", "yes")
|
||||||
if token != "" {
|
if cfg.NtfyToken != "" {
|
||||||
// ntfy uses basic auth with empty username and token as password
|
encoded := base64.StdEncoding.EncodeToString([]byte(":" + cfg.NtfyToken))
|
||||||
encoded := base64.StdEncoding.EncodeToString([]byte(":" + token))
|
|
||||||
req.Header.Set("Authorization", "Basic "+encoded)
|
req.Header.Set("Authorization", "Basic "+encoded)
|
||||||
}
|
}
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
@ -244,94 +378,162 @@ func notify(message, host, topic, title string, priority int, token string) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAlertPeer(ctx context.Context, api *tg.Client, cfg *Config) (*tg.InputPeerChannel, error) {
|
func resolveChannel(ctx context.Context, api *tg.Client, channelID int64) (*tg.InputPeerChannel, *tg.Channel, error) {
|
||||||
result, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
|
req := &tg.MessagesGetDialogsRequest{
|
||||||
OffsetPeer: &tg.InputPeerEmpty{},
|
OffsetPeer: &tg.InputPeerEmpty{},
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var chats []tg.ChatClass
|
for {
|
||||||
|
result, err := api.MessagesGetDialogs(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
switch v := result.(type) {
|
var (
|
||||||
case *tg.MessagesDialogs:
|
chats []tg.ChatClass
|
||||||
chats = v.Chats
|
dialogs []tg.DialogClass
|
||||||
case *tg.MessagesDialogsSlice:
|
messages []tg.MessageClass
|
||||||
chats = v.Chats
|
done bool
|
||||||
case *tg.MessagesDialogsNotModified:
|
)
|
||||||
return nil, fmt.Errorf("dialogs not modified")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected dialogs type: %T", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, chat := range chats {
|
switch v := result.(type) {
|
||||||
if channel, ok := chat.(*tg.Channel); ok {
|
case *tg.MessagesDialogs:
|
||||||
if channel.ID == cfg.AlertChat {
|
chats, dialogs, messages = v.Chats, v.Dialogs, v.Messages
|
||||||
return &tg.InputPeerChannel{
|
done = true
|
||||||
ChannelID: channel.ID,
|
case *tg.MessagesDialogsSlice:
|
||||||
AccessHash: channel.AccessHash,
|
chats, dialogs, messages = v.Chats, v.Dialogs, v.Messages
|
||||||
}, nil
|
case *tg.MessagesDialogsNotModified:
|
||||||
|
return nil, nil, fmt.Errorf("dialogs not modified")
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unexpected dialogs type: %T", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chat := range chats {
|
||||||
|
if channel, ok := chat.(*tg.Channel); ok {
|
||||||
|
if channel.ID == channelID {
|
||||||
|
return &tg.InputPeerChannel{
|
||||||
|
ChannelID: channel.ID,
|
||||||
|
AccessHash: channel.AccessHash,
|
||||||
|
}, channel, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if done || len(dialogs) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build offset from the last dialog for the next page.
|
||||||
|
last, ok := dialogs[len(dialogs)-1].(*tg.Dialog)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
msgID := last.TopMessage
|
||||||
|
var msgDate int
|
||||||
|
for _, m := range messages {
|
||||||
|
if msg, ok := m.(*tg.Message); ok && msg.ID == msgID {
|
||||||
|
msgDate = msg.Date
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msgDate == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offsetPeer, err := toPeerInput(last.Peer, chats)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
req.OffsetDate = msgDate
|
||||||
|
req.OffsetID = msgID
|
||||||
|
req.OffsetPeer = offsetPeer
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("alert channel %d not found", cfg.AlertChat)
|
return nil, nil, fmt.Errorf("channel %d not found in dialogs", channelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPeerChannel, cfg *Config, entities tg.Entities, update any) error {
|
func toPeerInput(peer tg.PeerClass, chats []tg.ChatClass) (tg.InputPeerClass, error) {
|
||||||
// Extract message from either UpdateNewMessage or UpdateNewChannelMessage
|
switch p := peer.(type) {
|
||||||
var msg *tg.Message
|
case *tg.PeerChannel:
|
||||||
|
for _, c := range chats {
|
||||||
|
if ch, ok := c.(*tg.Channel); ok && ch.ID == p.ChannelID {
|
||||||
|
return &tg.InputPeerChannel{ChannelID: ch.ID, AccessHash: ch.AccessHash}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("channel %d not found in chats", p.ChannelID)
|
||||||
|
case *tg.PeerChat:
|
||||||
|
return &tg.InputPeerChat{ChatID: p.ChatID}, nil
|
||||||
|
case *tg.PeerUser:
|
||||||
|
return &tg.InputPeerUser{UserID: p.UserID}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown peer type %T", peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMessage(update any) *tg.Message {
|
||||||
switch u := update.(type) {
|
switch u := update.(type) {
|
||||||
case *tg.UpdateNewMessage:
|
case *tg.UpdateNewMessage:
|
||||||
m, ok := u.Message.(*tg.Message)
|
m, _ := u.Message.(*tg.Message)
|
||||||
if !ok {
|
return m
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg = m
|
|
||||||
case *tg.UpdateNewChannelMessage:
|
case *tg.UpdateNewChannelMessage:
|
||||||
m, ok := u.Message.(*tg.Message)
|
m, _ := u.Message.(*tg.Message)
|
||||||
if !ok {
|
return m
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg = m
|
|
||||||
default:
|
default:
|
||||||
log.Printf("DEBUG: unknown update type %T", update)
|
log.Printf("DEBUG: unknown update type %T", update)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only process messages sent by the bot (Out=true)
|
func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPeerChannel, state *BotState, entities tg.Entities, update any) error {
|
||||||
|
msg := extractMessage(update)
|
||||||
|
if msg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle commands from the bot owner (messages sent by the connected account).
|
||||||
if msg.Out {
|
if msg.Out {
|
||||||
|
handleOwnerCommand(ctx, api, state, alertPeer, msg.Message)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if from monitored supergroup (channels in gotd terminology)
|
// Check if from monitored supergroup (channels in gotd terminology)
|
||||||
peerChannel, ok := msg.PeerID.(*tg.PeerChannel)
|
peerChannel, ok := msg.PeerID.(*tg.PeerChannel)
|
||||||
if !ok || int64(peerChannel.ChannelID) != cfg.MonitoredChat {
|
if !ok || int64(peerChannel.ChannelID) != state.cfg.MonitoredChat {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("✓ Processing message in chat %v: %s", msg.PeerID, msg.Message)
|
|
||||||
chatID := int64(peerChannel.ChannelID)
|
chatID := int64(peerChannel.ChannelID)
|
||||||
|
|
||||||
// Use YAML patterns if available, otherwise use defaults
|
// Check bad words first — any match sets score to 1.0 immediately.
|
||||||
patterns := cfg.Patterns
|
state.mu.RLock()
|
||||||
if len(patterns) == 0 {
|
badWordRe := state.badWordRe
|
||||||
patterns = scamPatterns
|
state.mu.RUnlock()
|
||||||
log.Printf("Using hardcoded scam patterns (%d patterns)", len(patterns))
|
|
||||||
|
var result matchResult
|
||||||
|
if badWordRe != nil && badWordRe.MatchString(msg.Message) {
|
||||||
|
result = matchResult{
|
||||||
|
score: 1.0,
|
||||||
|
matchedKeywords: map[string][]string{"bad_words": badWordRe.FindAllString(msg.Message, -1)},
|
||||||
|
}
|
||||||
|
log.Printf("Bad word match in message, score set to 1.0")
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Using YAML-loaded scam patterns (%d patterns)", len(patterns))
|
// Use YAML patterns if available, otherwise use defaults.
|
||||||
}
|
patterns := state.cfg.Patterns
|
||||||
|
if len(patterns) == 0 {
|
||||||
|
patterns = scamPatterns
|
||||||
|
log.Printf("Using hardcoded scam patterns (%d patterns)", len(patterns))
|
||||||
|
} else {
|
||||||
|
log.Printf("Using YAML-loaded scam patterns (%d patterns)", len(patterns))
|
||||||
|
}
|
||||||
|
|
||||||
// Score the message
|
result = keywordMatchScore(msg.Message, patterns)
|
||||||
result := keywordMatchScore(msg.Message, patterns)
|
|
||||||
|
|
||||||
if extended_latin, ok := result.matchedKeywords["extended_latin"]; ok {
|
if extended_latin, ok := result.matchedKeywords["extended_latin"]; ok {
|
||||||
for range extended_latin {
|
for range extended_latin {
|
||||||
result.score += 0.1
|
result.score += 0.1
|
||||||
if result.score >= 1.0 {
|
if result.score >= 1.0 {
|
||||||
result.score = 1.0
|
result.score = 1.0
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -340,113 +542,90 @@ func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPe
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.score == 1.0 ||
|
log.Printf("Matched message with score %.2f", result.score)
|
||||||
(result.score == 1.0 &&
|
for key, values := range result.matchedKeywords {
|
||||||
result.matchedKeywords["extended_latin"] != nil &&
|
for _, value := range values {
|
||||||
len(result.matchedKeywords["links"]) > 0) {
|
log.Printf(" %s: %s", key, value)
|
||||||
|
|
||||||
log.Printf("Matched message with score %.2f", result.score)
|
|
||||||
for key, values := range result.matchedKeywords {
|
|
||||||
for _, value := range values {
|
|
||||||
log.Printf(" %s: %s", key, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the message from supergroup
|
// Delete the message from supergroup
|
||||||
channel, ok := entities.Channels[chatID]
|
channel, ok := entities.Channels[chatID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("channel %d not found in entities", chatID)
|
return fmt.Errorf("channel %d not found in entities", chatID)
|
||||||
}
|
}
|
||||||
_, err := api.ChannelsDeleteMessages(ctx, &tg.ChannelsDeleteMessagesRequest{
|
_, err := api.ChannelsDeleteMessages(ctx, &tg.ChannelsDeleteMessagesRequest{
|
||||||
Channel: &tg.InputChannel{
|
Channel: &tg.InputChannel{
|
||||||
ChannelID: chatID,
|
ChannelID: chatID,
|
||||||
AccessHash: channel.AccessHash,
|
AccessHash: channel.AccessHash,
|
||||||
},
|
},
|
||||||
ID: []int{msg.ID},
|
ID: []int{msg.ID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to delete message: %v", err)
|
log.Printf("Failed to delete message: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get sender info
|
// Get sender info
|
||||||
var senderID int64
|
fromID, ok := msg.FromID.(*tg.PeerUser)
|
||||||
if fromID, ok := msg.FromID.(*tg.PeerUser); ok {
|
if !ok {
|
||||||
senderID = int64(fromID.UserID)
|
return fmt.Errorf("could not determine sender")
|
||||||
} else {
|
}
|
||||||
return fmt.Errorf("could not determine sender")
|
senderID := int64(fromID.UserID)
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := entities.Users[senderID]
|
user, ok := entities.Users[senderID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("user %d not found in entities", senderID)
|
return fmt.Errorf("user %d not found in entities", senderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
displayName := user.FirstName
|
displayName := fullName(user.FirstName, user.LastName)
|
||||||
if user.LastName != "" {
|
userDisplay := displayName
|
||||||
displayName += " " + user.LastName
|
if user.Username != "" {
|
||||||
}
|
userDisplay += " (@" + user.Username + ")"
|
||||||
displayName = strings.TrimSpace(displayName)
|
} else {
|
||||||
|
userDisplay += " (no username)"
|
||||||
|
}
|
||||||
|
|
||||||
username := "no username"
|
// Restrict sender in supergroup
|
||||||
if user.Username != "" {
|
_, err = api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
|
||||||
username = "@" + user.Username
|
Channel: &tg.InputChannel{
|
||||||
}
|
ChannelID: chatID,
|
||||||
|
AccessHash: channel.AccessHash,
|
||||||
|
},
|
||||||
|
Participant: &tg.InputPeerUser{
|
||||||
|
UserID: senderID,
|
||||||
|
AccessHash: user.AccessHash,
|
||||||
|
},
|
||||||
|
BannedRights: tg.ChatBannedRights{
|
||||||
|
SendMessages: true,
|
||||||
|
SendMedia: true,
|
||||||
|
SendStickers: true,
|
||||||
|
SendGifs: true,
|
||||||
|
UntilDate: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to restrict user %d: %v", senderID, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Restrict sender in supergroup
|
chatName := channel.Title
|
||||||
_, err = api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{
|
chatDisplay := escapeMarkdown(chatName) + escapeMarkdown(formatUsername(channel.Username))
|
||||||
Channel: &tg.InputChannel{
|
|
||||||
ChannelID: chatID,
|
|
||||||
AccessHash: channel.AccessHash,
|
|
||||||
},
|
|
||||||
Participant: &tg.InputPeerUser{
|
|
||||||
UserID: senderID,
|
|
||||||
AccessHash: user.AccessHash,
|
|
||||||
},
|
|
||||||
BannedRights: tg.ChatBannedRights{
|
|
||||||
SendMessages: true,
|
|
||||||
SendMedia: true,
|
|
||||||
SendStickers: true,
|
|
||||||
SendGifs: true,
|
|
||||||
UntilDate: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to restrict user %d: %v", senderID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get supergroup name and 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",
|
||||||
chatName := channel.Title
|
result.score, chatDisplay, chatID, userDisplay, senderID)
|
||||||
chatDisplay := escapeMarkdown(chatName)
|
|
||||||
if channel.Username != "" {
|
|
||||||
chatDisplay += " (@" + escapeMarkdown(channel.Username) + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build alert message with HTML formatting for markdown v2
|
if state.cfg.NtfyToken != "" || state.cfg.NtfyTopic != "" {
|
||||||
matchMessageHTML := fmt.Sprintf("🚨 Matched\n<b>Score</b>: %.2f\n<b>Chat</b>: %s (ID: %d)\n<b>User</b>: %s (ID: %d)\n",
|
plainMessage := fmt.Sprintf("🚨 Matched\nScore: %.2f\nChat: %s (ID: %d)\nUser: %s (ID: %d)\n\n%s",
|
||||||
result.score, chatDisplay, chatID, displayName+" ("+username+")", senderID)
|
result.score, chatName+formatUsername(channel.Username), chatID, userDisplay, senderID, msg.Message)
|
||||||
|
notify(plainMessage, state.cfg, fmt.Sprintf("Scam Alert: %s", chatName))
|
||||||
|
}
|
||||||
|
|
||||||
// Send ntfy notification if config set (use plain text for ntfy)
|
userResolver := func(id int64) (tg.InputUserClass, error) {
|
||||||
if cfg.NtfyToken != "" || cfg.NtfyTopic != "" {
|
return &tg.InputUserFromMessage{Peer: alertPeer, MsgID: 0, UserID: id}, nil
|
||||||
plainMessage := fmt.Sprintf("🚨 Matched\nScore: %.2f\nChat: %s (ID: %d)\nUser: %s (ID: %d)\n\n%s",
|
}
|
||||||
result.score, escapeMarkdown(chatDisplay), chatID, escapeMarkdown(displayName+" ("+username+")"), senderID, escapeMarkdown(msg.Message))
|
sender := message.NewSender(api)
|
||||||
notify(plainMessage, cfg.NtfyHost, cfg.NtfyTopic, fmt.Sprintf("Scam Alert: %s", chatName), 5, cfg.NtfyToken)
|
_, err = sender.To(alertPeer).StyledText(ctx, html.String(userResolver, matchMessageHTML))
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("Failed to send alert message: %v", err)
|
||||||
// Create a resolver for user mentions (not needed for this message, but required by html.String)
|
|
||||||
userResolver := func(id int64) (tg.InputUserClass, error) {
|
|
||||||
return &tg.InputUserFromMessage{
|
|
||||||
Peer: alertPeer,
|
|
||||||
MsgID: 0,
|
|
||||||
UserID: id,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send alert message to alert chat using StyledText with HTML
|
|
||||||
sender := message.NewSender(api)
|
|
||||||
_, err = sender.To(alertPeer).StyledText(ctx, html.String(userResolver, matchMessageHTML))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to send alert message: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -513,38 +692,37 @@ func main() {
|
||||||
api := client.API()
|
api := client.API()
|
||||||
|
|
||||||
// Resolve alert channel peer at startup
|
// Resolve alert channel peer at startup
|
||||||
alertPeer, err := resolveAlertPeer(ctx, api, cfg)
|
alertPeer, alertChannel, err := resolveChannel(ctx, api, cfg.AlertChat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolving alert peer: %w", err)
|
return fmt.Errorf("resolving alert channel: %w", err)
|
||||||
|
}
|
||||||
|
_, monitoredChannel, err := resolveChannel(ctx, api, cfg.MonitoredChat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving monitored channel: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register message handler for private/group messages
|
// Get self ID for state and updates manager
|
||||||
dispatcher.OnNewMessage(func(ctx context.Context, entities tg.Entities, update *tg.UpdateNewMessage) error {
|
|
||||||
return handleNewMessage(ctx, api, alertPeer, cfg, entities, update)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register handler for channel messages (supergroups)
|
|
||||||
dispatcher.OnNewChannelMessage(func(ctx context.Context, entities tg.Entities, update *tg.UpdateNewChannelMessage) error {
|
|
||||||
return handleNewMessage(ctx, api, alertPeer, cfg, entities, update)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get self ID for updates manager
|
|
||||||
status, err := client.Auth().Status(ctx)
|
status, err := client.Auth().Status(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state := newBotState(cfg, configPath, int64(status.User.ID))
|
||||||
|
|
||||||
|
// Register message handler for private/group messages
|
||||||
|
dispatcher.OnNewMessage(func(ctx context.Context, entities tg.Entities, update *tg.UpdateNewMessage) error {
|
||||||
|
return handleNewMessage(ctx, api, alertPeer, state, entities, update)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register handler for channel messages (supergroups)
|
||||||
|
dispatcher.OnNewChannelMessage(func(ctx context.Context, entities tg.Entities, update *tg.UpdateNewChannelMessage) error {
|
||||||
|
return handleNewMessage(ctx, api, alertPeer, state, entities, update)
|
||||||
|
})
|
||||||
|
|
||||||
user := status.User
|
user := status.User
|
||||||
username := ""
|
log.Printf("✓ Logged in as: %s%s (ID: %d)", fullName(user.FirstName, user.LastName), formatUsername(user.Username), user.ID)
|
||||||
if user.Username != "" {
|
log.Printf("✓ Monitoring chat: %s%s (ID: %d)", monitoredChannel.Title, formatUsername(monitoredChannel.Username), monitoredChannel.ID)
|
||||||
username = " (@" + user.Username + ")"
|
log.Printf("✓ Sending alerts to: %s%s (ID: %d)", alertChannel.Title, formatUsername(alertChannel.Username), alertChannel.ID)
|
||||||
}
|
|
||||||
displayName := user.FirstName
|
|
||||||
if user.LastName != "" {
|
|
||||||
displayName += " " + user.LastName
|
|
||||||
}
|
|
||||||
log.Printf("✓ Logged in as: %s%s (ID: %d)", displayName, username, user.ID)
|
|
||||||
log.Printf("✓ Bot running, monitoring chat %d", cfg.MonitoredChat)
|
|
||||||
return gapManager.Run(ctx, api, status.User.ID, updates.AuthOptions{})
|
return gapManager.Run(ctx, api, status.User.ID, updates.AuthOptions{})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
28
go.mod
28
go.mod
|
|
@ -3,8 +3,8 @@ module git.zio.sh/astra/telegram-antiscam
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gotd/td v0.139.0
|
github.com/gotd/td v0.142.0
|
||||||
golang.org/x/term v0.40.0
|
golang.org/x/term v0.41.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,26 +22,26 @@ require (
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gotd/ige v0.2.2 // indirect
|
github.com/gotd/ige v0.2.2 // indirect
|
||||||
github.com/gotd/neo v0.1.5 // indirect
|
github.com/gotd/neo v0.1.5 // indirect
|
||||||
github.com/klauspost/compress v1.18.3 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ogen-go/ogen v1.16.0 // indirect
|
github.com/ogen-go/ogen v1.19.0 // indirect
|
||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
||||||
golang.org/x/mod v0.32.0 // indirect
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
56
go.sum
56
go.sum
|
|
@ -33,10 +33,10 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
||||||
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
||||||
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||||
github.com/gotd/td v0.139.0 h1:3viuXqNdC0+mmd5GerDFp/rlII/QcZSzh/pjuG56NSU=
|
github.com/gotd/td v0.142.0 h1:hsH8zM7Pv98CkSMvrAEzVJurhntUziqKgf4VEofv5Zg=
|
||||||
github.com/gotd/td v0.139.0/go.mod h1:nBietiOYxaXEo6PmRp73LL64upWlk9rcFEZSJu6VieY=
|
github.com/gotd/td v0.142.0/go.mod h1:UHO5Gpwce9mH4zplp2qWo6AdzDjFVg7gK+ANMCztsi8=
|
||||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|
@ -45,8 +45,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw=
|
github.com/ogen-go/ogen v1.19.0 h1:YvdNpeQJ8A8dLLpS6Vs4WxXL53BT6tBPxH0VSjfALhA=
|
||||||
github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI=
|
github.com/ogen-go/ogen v1.19.0/go.mod h1:DeShwO+TEpLYXNCuZliSAedphphXsJaTGGbmSomWUjE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
|
@ -57,12 +57,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|
@ -71,26 +71,26 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue