code refactor

This commit is contained in:
Astra 2026-03-23 07:59:53 +00:00
parent 20048736eb
commit cf6e27539b
3 changed files with 423 additions and 245 deletions

584
bot.go
View file

@ -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 <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 {
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("&amp;")
case '<':
result.WriteString("&lt;")
case '>':
result.WriteString("&gt;")
case '"':
result.WriteString("&quot;")
case '\'':
result.WriteString("&#39;")
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\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)
// Build alert message with HTML formatting for markdown v2
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, 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{})
})

28
go.mod
View file

@ -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
)

56
go.sum
View file

@ -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=