From cf6e27539bfb5299eca3c14758a418c708641d0c Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 23 Mar 2026 07:59:53 +0000 Subject: [PATCH] code refactor --- bot.go | 584 +++++++++++++++++++++++++++++++++++++-------------------- go.mod | 28 +-- go.sum | 56 +++--- 3 files changed, 423 insertions(+), 245 deletions(-) diff --git a/bot.go b/bot.go index 33e0107..a98a62c 100644 --- a/bot.go +++ b/bot.go @@ -11,6 +11,7 @@ import ( "os" "regexp" "strings" + "sync" "github.com/gotd/td/session" "github.com/gotd/td/telegram" @@ -39,6 +40,7 @@ type Config struct { NtfyTopic string `yaml:"ntfy_topic"` SessionPath string `yaml:"session_path"` YAMLPatterns []YAMLScamPattern `yaml:"scam_patterns"` + BadWords []string `yaml:"bad_words"` Patterns []scamPattern `yaml:"-"` // Compiled patterns, not from YAML } @@ -56,6 +58,7 @@ ntfy_token: "" ntfy_topic: "" session_path: "antiscam.session" scam_patterns: [] +bad_words: [] ` if err := os.WriteFile(path, []byte(defaultConfig), 0644); err != nil { 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 [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 ") + 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 ") + 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 [word]") + } +} + func keywordMatchScore(input string, patterns []scamPattern) matchResult { matched := make(map[string][]string) totalScore := 0.0 @@ -195,33 +340,23 @@ func escapeMarkdown(text string) string { return result.String() } -func escapeHTML(text string) string { - var result strings.Builder - 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 fullName(first, last string) string { + return strings.TrimSpace(first + " " + last) } -func notify(message, host, topic, title string, priority int, token string) { - if !strings.HasSuffix(host, "/") { - host = host + "/" +func formatUsername(username string) string { + if username == "" { + return "" } - url := host + topic - req, err := http.NewRequest("POST", url, bytes.NewBufferString(message)) + return " (@" + username + ")" +} + +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 { log.Printf("notify: creating request: %v", err) return @@ -229,11 +364,10 @@ func notify(message, host, topic, title string, priority int, token string) { if title != "" { req.Header.Set("Title", title) } - req.Header.Set("Priority", fmt.Sprintf("%d", priority)) + req.Header.Set("Priority", "5") req.Header.Set("Markdown", "yes") - if token != "" { - // ntfy uses basic auth with empty username and token as password - encoded := base64.StdEncoding.EncodeToString([]byte(":" + token)) + if cfg.NtfyToken != "" { + encoded := base64.StdEncoding.EncodeToString([]byte(":" + cfg.NtfyToken)) req.Header.Set("Authorization", "Basic "+encoded) } 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() } -func resolveAlertPeer(ctx context.Context, api *tg.Client, cfg *Config) (*tg.InputPeerChannel, error) { - result, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{ +func resolveChannel(ctx context.Context, api *tg.Client, channelID int64) (*tg.InputPeerChannel, *tg.Channel, error) { + req := &tg.MessagesGetDialogsRequest{ OffsetPeer: &tg.InputPeerEmpty{}, 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) { - case *tg.MessagesDialogs: - chats = v.Chats - case *tg.MessagesDialogsSlice: - chats = v.Chats - case *tg.MessagesDialogsNotModified: - return nil, fmt.Errorf("dialogs not modified") - default: - return nil, fmt.Errorf("unexpected dialogs type: %T", result) - } + var ( + chats []tg.ChatClass + dialogs []tg.DialogClass + messages []tg.MessageClass + done bool + ) - for _, chat := range chats { - if channel, ok := chat.(*tg.Channel); ok { - if channel.ID == cfg.AlertChat { - return &tg.InputPeerChannel{ - ChannelID: channel.ID, - AccessHash: channel.AccessHash, - }, nil + switch v := result.(type) { + case *tg.MessagesDialogs: + chats, dialogs, messages = v.Chats, v.Dialogs, v.Messages + done = true + case *tg.MessagesDialogsSlice: + chats, dialogs, messages = v.Chats, v.Dialogs, v.Messages + 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 { - // Extract message from either UpdateNewMessage or UpdateNewChannelMessage - var msg *tg.Message +func toPeerInput(peer tg.PeerClass, chats []tg.ChatClass) (tg.InputPeerClass, error) { + switch p := peer.(type) { + 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) { case *tg.UpdateNewMessage: - m, ok := u.Message.(*tg.Message) - if !ok { - return nil - } - msg = m + m, _ := u.Message.(*tg.Message) + return m case *tg.UpdateNewChannelMessage: - m, ok := u.Message.(*tg.Message) - if !ok { - return nil - } - msg = m + m, _ := u.Message.(*tg.Message) + return m default: log.Printf("DEBUG: unknown update type %T", update) 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 { + handleOwnerCommand(ctx, api, state, alertPeer, msg.Message) return nil } // Check if from monitored supergroup (channels in gotd terminology) peerChannel, ok := msg.PeerID.(*tg.PeerChannel) - if !ok || int64(peerChannel.ChannelID) != cfg.MonitoredChat { + if !ok || int64(peerChannel.ChannelID) != state.cfg.MonitoredChat { return nil } - log.Printf("✓ Processing message in chat %v: %s", msg.PeerID, msg.Message) chatID := int64(peerChannel.ChannelID) - // Use YAML patterns if available, otherwise use defaults - patterns := cfg.Patterns - if len(patterns) == 0 { - patterns = scamPatterns - log.Printf("Using hardcoded scam patterns (%d patterns)", len(patterns)) + // Check bad words first — any match sets score to 1.0 immediately. + state.mu.RLock() + badWordRe := state.badWordRe + state.mu.RUnlock() + + 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 { - 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 { - for range extended_latin { - result.score += 0.1 - if result.score >= 1.0 { - result.score = 1.0 - break + if extended_latin, ok := result.matchedKeywords["extended_latin"]; ok { + for range extended_latin { + result.score += 0.1 + if result.score >= 1.0 { + result.score = 1.0 + break + } } } } @@ -340,113 +542,90 @@ func handleNewMessage(ctx context.Context, api *tg.Client, alertPeer *tg.InputPe return nil } - if result.score == 1.0 || - (result.score == 1.0 && - result.matchedKeywords["extended_latin"] != nil && - len(result.matchedKeywords["links"]) > 0) { - - 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) - } + 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 - channel, ok := entities.Channels[chatID] - if !ok { - return fmt.Errorf("channel %d not found in entities", chatID) - } - _, err := api.ChannelsDeleteMessages(ctx, &tg.ChannelsDeleteMessagesRequest{ - Channel: &tg.InputChannel{ - ChannelID: chatID, - AccessHash: channel.AccessHash, - }, - ID: []int{msg.ID}, - }) - if err != nil { - log.Printf("Failed to delete message: %v", err) - } + // Delete the message from supergroup + channel, ok := entities.Channels[chatID] + if !ok { + return fmt.Errorf("channel %d not found in entities", chatID) + } + _, err := api.ChannelsDeleteMessages(ctx, &tg.ChannelsDeleteMessagesRequest{ + Channel: &tg.InputChannel{ + ChannelID: chatID, + AccessHash: channel.AccessHash, + }, + ID: []int{msg.ID}, + }) + if err != nil { + log.Printf("Failed to delete message: %v", err) + } - // Get sender info - var senderID int64 - if fromID, ok := msg.FromID.(*tg.PeerUser); ok { - senderID = int64(fromID.UserID) - } else { - return fmt.Errorf("could not determine sender") - } + // Get sender info + fromID, ok := msg.FromID.(*tg.PeerUser) + if !ok { + return fmt.Errorf("could not determine sender") + } + senderID := int64(fromID.UserID) - user, ok := entities.Users[senderID] - if !ok { - return fmt.Errorf("user %d not found in entities", senderID) - } + user, ok := entities.Users[senderID] + if !ok { + return fmt.Errorf("user %d not found in entities", senderID) + } - displayName := user.FirstName - if user.LastName != "" { - displayName += " " + user.LastName - } - displayName = strings.TrimSpace(displayName) + displayName := fullName(user.FirstName, user.LastName) + userDisplay := displayName + if user.Username != "" { + userDisplay += " (@" + user.Username + ")" + } else { + userDisplay += " (no username)" + } - username := "no username" - if user.Username != "" { - username = "@" + user.Username - } + // Restrict sender in supergroup + _, err = api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + 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 - _, err = api.ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ - 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) - } + chatName := channel.Title + chatDisplay := escapeMarkdown(chatName) + escapeMarkdown(formatUsername(channel.Username)) - // Get supergroup name and username - chatName := channel.Title - chatDisplay := escapeMarkdown(chatName) - if channel.Username != "" { - chatDisplay += " (@" + escapeMarkdown(channel.Username) + ")" - } + matchMessageHTML := fmt.Sprintf("🚨 Matched\nScore: %.2f\nChat: %s (ID: %d)\nUser: %s (ID: %d)\n", + result.score, chatDisplay, chatID, userDisplay, senderID) - // Build alert message with HTML formatting for markdown v2 - matchMessageHTML := fmt.Sprintf("🚨 Matched\nScore: %.2f\nChat: %s (ID: %d)\nUser: %s (ID: %d)\n", - result.score, chatDisplay, chatID, displayName+" ("+username+")", 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) + notify(plainMessage, state.cfg, fmt.Sprintf("Scam Alert: %s", chatName)) + } - // Send ntfy notification if config set (use plain text for ntfy) - if cfg.NtfyToken != "" || cfg.NtfyTopic != "" { - 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)) - notify(plainMessage, cfg.NtfyHost, cfg.NtfyTopic, fmt.Sprintf("Scam Alert: %s", chatName), 5, cfg.NtfyToken) - } - - // 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) - } + userResolver := func(id int64) (tg.InputUserClass, error) { + return &tg.InputUserFromMessage{Peer: alertPeer, MsgID: 0, UserID: id}, nil + } + 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 @@ -513,38 +692,37 @@ func main() { api := client.API() // Resolve alert channel peer at startup - alertPeer, err := resolveAlertPeer(ctx, api, cfg) + alertPeer, alertChannel, err := resolveChannel(ctx, api, cfg.AlertChat) 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 - 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 + // Get self ID for state and updates manager status, err := client.Auth().Status(ctx) if err != nil { 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 - username := "" - if user.Username != "" { - username = " (@" + user.Username + ")" - } - 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) + log.Printf("✓ Logged in as: %s%s (ID: %d)", fullName(user.FirstName, user.LastName), formatUsername(user.Username), user.ID) + log.Printf("✓ Monitoring chat: %s%s (ID: %d)", monitoredChannel.Title, formatUsername(monitoredChannel.Username), monitoredChannel.ID) + log.Printf("✓ Sending alerts to: %s%s (ID: %d)", alertChannel.Title, formatUsername(alertChannel.Username), alertChannel.ID) return gapManager.Run(ctx, api, status.User.ID, updates.AuthOptions{}) }) diff --git a/go.mod b/go.mod index 98c163d..bcd4c4a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module git.zio.sh/astra/telegram-antiscam go 1.25.3 require ( - github.com/gotd/td v0.139.0 - golang.org/x/term v0.40.0 + github.com/gotd/td v0.142.0 + golang.org/x/term v0.41.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -22,26 +22,26 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gotd/ige v0.2.2 // 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-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/shopspring/decimal v1.4.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // 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/mod v0.32.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 225ab57..bd18e06 100644 --- a/go.sum +++ b/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/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= 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.139.0/go.mod h1:nBietiOYxaXEo6PmRp73LL64upWlk9rcFEZSJu6VieY= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/gotd/td v0.142.0 h1:hsH8zM7Pv98CkSMvrAEzVJurhntUziqKgf4VEofv5Zg= +github.com/gotd/td v0.142.0/go.mod h1:UHO5Gpwce9mH4zplp2qWo6AdzDjFVg7gK+ANMCztsi8= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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-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/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw= -github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI= +github.com/ogen-go/ogen v1.19.0 h1:YvdNpeQJ8A8dLLpS6Vs4WxXL53BT6tBPxH0VSjfALhA= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 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/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 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.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +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/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +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 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=