Initial
This commit is contained in:
commit
42e446cea0
13 changed files with 1131 additions and 0 deletions
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
### VisualStudioCode template
|
||||
.vscode/
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### GoLand+all template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
|
||||
### Go template
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
.gitlab-ci-local
|
||||
local/
|
||||
log_file.yaml
|
||||
session.json
|
||||
bin/
|
||||
.env
|
||||
.cursor/
|
||||
129
.golangci.yaml
Normal file
129
.golangci.yaml
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
run:
|
||||
concurrency: 4
|
||||
timeout: 10m
|
||||
issues-exit-code: 2
|
||||
tests: true
|
||||
build-tags: []
|
||||
allow-parallel-runners: true
|
||||
allow-serial-runners: true
|
||||
go: '1.24'
|
||||
|
||||
output:
|
||||
formats:
|
||||
- format: colored-line-number
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
path-prefix: ""
|
||||
sort-results: true
|
||||
sort-order:
|
||||
- linter
|
||||
- severity
|
||||
- file
|
||||
show-stats: true
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- depguard
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- copyloopvar
|
||||
- dupl
|
||||
- errorlint
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gosec
|
||||
- grouper
|
||||
- inamedparam
|
||||
- lll
|
||||
- makezero
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- revive
|
||||
- testifylint
|
||||
- whitespace
|
||||
- importas
|
||||
- wrapcheck
|
||||
- nolintlint
|
||||
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 150
|
||||
|
||||
goimports:
|
||||
local-prefixes: "gitlab.com/v8s/trating"
|
||||
|
||||
depguard:
|
||||
rules:
|
||||
configuration:
|
||||
files:
|
||||
- $all
|
||||
- "!**/internal/config/*.go"
|
||||
deny:
|
||||
- pkg: "github.com/spf13/viper"
|
||||
desc: Should be used only in config package, to avoid boiler plate
|
||||
|
||||
replace-std:
|
||||
list-mode: lax
|
||||
files:
|
||||
- "**/internal/**/*.go"
|
||||
deny:
|
||||
- pkg: "errors"
|
||||
desc: Use github.com/pkg/errors for proper callstack logging (check README.md)
|
||||
- pkg: "log"
|
||||
desc: Use github.com/rs/zerolog/log as replacement
|
||||
|
||||
importas:
|
||||
no-unaliased: true
|
||||
alias:
|
||||
# enforce easycfg like usage
|
||||
- pkg: github.com/spf13/pflag
|
||||
alias: cfg
|
||||
|
||||
wrapcheck:
|
||||
ignoreSigs:
|
||||
- github.com/pkg/errors.Wrap(
|
||||
- github.com/pkg/errors.Wrapf(
|
||||
- github.com/pkg/errors.New(
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
dupl:
|
||||
threshold: 100
|
||||
|
||||
issues:
|
||||
exclude-dirs-use-default: true
|
||||
exclude-files: []
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- lll
|
||||
- path: internal/config/
|
||||
linters:
|
||||
- importas
|
||||
- path: cmd/.*\.go
|
||||
text: "exitAfterDefer"
|
||||
linters:
|
||||
- gocritic
|
||||
- text: "should be written without leading space as"
|
||||
linters: [nolintlint]
|
||||
40
Taskfile.yml
Normal file
40
Taskfile.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
dotenv: [".env"]
|
||||
|
||||
tasks:
|
||||
mod:
|
||||
desc: Sync go.mod
|
||||
cmds:
|
||||
- go mod download
|
||||
- go mod tidy
|
||||
|
||||
run:test:
|
||||
desc: Run test command
|
||||
cmds:
|
||||
- go run ./cmd/test/...
|
||||
|
||||
run:
|
||||
desc: Run CLI app
|
||||
cmds:
|
||||
- go run ./. {{.CLI_ARGS}}
|
||||
|
||||
build:
|
||||
cmds:
|
||||
- mkdir -p ./bin
|
||||
- go build -o ./bin/telegram-mcp ./.
|
||||
|
||||
install:
|
||||
desc: Install MCP server to user's local bin
|
||||
deps: [build]
|
||||
cmds:
|
||||
- cp ./bin/telegram-mcp ~/.local/bin/
|
||||
- chmod +x ~/.local/bin/telegram-mcp
|
||||
|
||||
lint:
|
||||
desc: Run linter
|
||||
cmd: golangci-lint run ./...
|
||||
|
||||
|
||||
62
auth.go
Normal file
62
auth.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"telegram-mcp/internal/tg"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func authCommand(_ context.Context, cmd *cli.Command) error {
|
||||
phone := cmd.String("phone")
|
||||
appID := cmd.Root().Int("app-id")
|
||||
apiHash := cmd.Root().String("api-hash")
|
||||
sessionPath := cmd.Root().String("session")
|
||||
|
||||
log.Info().
|
||||
Str("phone", phone).
|
||||
Str("api-hash", apiHash).
|
||||
Str("session", sessionPath).
|
||||
Int64("app-id", appID).
|
||||
Msg("Authenticate with Telegram")
|
||||
|
||||
err := tg.Auth(phone, appID, apiHash, sessionPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to authenticate with Telegram")
|
||||
}
|
||||
|
||||
c := struct {
|
||||
Telegram struct {
|
||||
Command string `json:"command"`
|
||||
Env struct {
|
||||
AppID string `json:"TG_APP_ID"`
|
||||
ApiHash string `json:"TG_API_HASH"`
|
||||
} `json:"env"`
|
||||
} `json:"telegram"`
|
||||
}{
|
||||
Telegram: struct {
|
||||
Command string `json:"command"`
|
||||
Env struct {
|
||||
AppID string `json:"TG_APP_ID"`
|
||||
ApiHash string `json:"TG_API_HASH"`
|
||||
} `json:"env"`
|
||||
}{
|
||||
Command: "telegram-mcp",
|
||||
Env: struct {
|
||||
AppID string `json:"TG_APP_ID"`
|
||||
ApiHash string `json:"TG_API_HASH"`
|
||||
}{
|
||||
AppID: fmt.Sprintf("%d", appID),
|
||||
ApiHash: apiHash,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent(c, "", "\t")
|
||||
log.Info().RawJSON("config", data).Msg("Successfully authenticated with Telegram")
|
||||
|
||||
return nil
|
||||
}
|
||||
79
cmd/test/main.go
Normal file
79
cmd/test/main.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
|
||||
"telegram-mcp/internal/tg"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: zerolog.TimeFormatUnix,
|
||||
})
|
||||
|
||||
appIDStr, apiHash, sessionPath := os.Getenv("TG_APP_ID"), os.Getenv("TG_API_HASH"), os.Getenv("TG_SESSION_PATH")
|
||||
if appIDStr == "" {
|
||||
log.Fatal().Msg("TG_APP_ID is required")
|
||||
}
|
||||
if apiHash == "" {
|
||||
log.Fatal().Msg("TG_API_HASH is required")
|
||||
}
|
||||
if sessionPath == "" {
|
||||
log.Fatal().Msg("TG_SESSION_PATH is required")
|
||||
}
|
||||
|
||||
appID, err := strconv.Atoi(appIDStr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("TG_APP_ID app id")
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
client := tg.New(appID, apiHash, sessionPath).T
|
||||
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
self, err := client.Self(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get self")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("first_name", self.FirstName).
|
||||
Str("last_name", self.LastName).
|
||||
Str("username", self.Username).
|
||||
Int64("id", self.ID).
|
||||
Msg("Logged in as")
|
||||
|
||||
messages, err := getUnreadMessages(ctx, client)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get unread messages")
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
log.Info().
|
||||
Int("id", msg.ID).
|
||||
Str("text", msg.Text).
|
||||
Time("date", msg.Date).
|
||||
Int64("from_id", msg.FromID).
|
||||
Str("from_name", msg.FromName).
|
||||
Str("chat_type", msg.ChatType).
|
||||
Str("chat_title", msg.ChatTitle).
|
||||
Msg("Unread message")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatal().Err(err).Msg("client error")
|
||||
}
|
||||
}
|
||||
280
cmd/test/unread.go
Normal file
280
cmd/test/unread.go
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
cfg "github.com/spf13/pflag"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMessageLimit = 10
|
||||
maxDialogsLimit = 100
|
||||
rateLimitPerSec = 5
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // CLI flags must be global
|
||||
var (
|
||||
messageLimit = cfg.Int("limit", defaultMessageLimit, "limit of unread messages to fetch")
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // Rate limiter should be global for consistent rate limiting across all functions
|
||||
var telegramLimiter = rate.NewLimiter(rate.Limit(rateLimitPerSec), 1)
|
||||
|
||||
// UnreadMessage represents a simplified message structure
|
||||
type UnreadMessage struct {
|
||||
ID int
|
||||
Text string
|
||||
Date time.Time
|
||||
FromID int64
|
||||
FromName string
|
||||
ChatType string
|
||||
ChatTitle string
|
||||
}
|
||||
|
||||
// DialogWithUnread represents a dialog with its unread count and latest message ID
|
||||
type DialogWithUnread struct {
|
||||
Dialog *tg.Dialog
|
||||
UnreadCount int
|
||||
TopMessage int
|
||||
}
|
||||
|
||||
// getUnreadMessages fetches unread messages from different users
|
||||
//
|
||||
//nolint:gocognit,gocyclo // complexity is inherent to handling different types of Telegram messages and users
|
||||
func getUnreadMessages(ctx context.Context, client *telegram.Client) ([]UnreadMessage, error) {
|
||||
if err := telegramLimiter.Wait(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "rate limiter wait")
|
||||
}
|
||||
|
||||
api := client.API()
|
||||
dialogsClass, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
|
||||
OffsetPeer: &tg.InputPeerEmpty{},
|
||||
OffsetDate: 0,
|
||||
OffsetID: 0,
|
||||
Limit: maxDialogsLimit,
|
||||
Hash: 0,
|
||||
Flags: 0,
|
||||
ExcludePinned: false,
|
||||
FolderID: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get dialogs")
|
||||
}
|
||||
|
||||
var dialogs *tg.MessagesDialogs
|
||||
switch d := dialogsClass.(type) {
|
||||
case *tg.MessagesDialogs:
|
||||
dialogs = d
|
||||
case *tg.MessagesDialogsSlice:
|
||||
dialogs = &tg.MessagesDialogs{
|
||||
Dialogs: d.Dialogs,
|
||||
Messages: d.Messages,
|
||||
Chats: d.Chats,
|
||||
Users: d.Users,
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unexpected dialogs response type")
|
||||
}
|
||||
|
||||
// Create a slice of dialogs with unread count
|
||||
dialogsWithUnread := make([]DialogWithUnread, 0, len(dialogs.Dialogs))
|
||||
for _, dialog := range dialogs.Dialogs {
|
||||
dialogItem, ok := dialog.(*tg.Dialog)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if dialogItem.UnreadCount > 0 {
|
||||
dialogsWithUnread = append(dialogsWithUnread, DialogWithUnread{
|
||||
Dialog: dialogItem,
|
||||
UnreadCount: dialogItem.UnreadCount,
|
||||
TopMessage: dialogItem.TopMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort dialogs by TopMessage in descending order (newest first)
|
||||
sort.Slice(dialogsWithUnread, func(i, j int) bool {
|
||||
return dialogsWithUnread[i].TopMessage > dialogsWithUnread[j].TopMessage
|
||||
})
|
||||
|
||||
// Map to store the latest message from each user
|
||||
userMessages := make(map[int64]UnreadMessage)
|
||||
processedCount := 0
|
||||
|
||||
for _, dialogWithUnread := range dialogsWithUnread {
|
||||
dialogItem := dialogWithUnread.Dialog
|
||||
|
||||
var inputPeer tg.InputPeerClass
|
||||
var chatType, chatTitle string
|
||||
var fromID int64
|
||||
var fromName string
|
||||
|
||||
switch peer := dialogItem.Peer.(type) {
|
||||
case *tg.PeerUser:
|
||||
for _, userItem := range dialogs.Users {
|
||||
user, ok := userItem.(*tg.User)
|
||||
if !ok || user.ID != peer.UserID {
|
||||
continue
|
||||
}
|
||||
|
||||
inputPeer = &tg.InputPeerUser{
|
||||
UserID: user.ID,
|
||||
AccessHash: user.AccessHash,
|
||||
}
|
||||
chatType = "user"
|
||||
chatTitle = user.FirstName + " " + user.LastName
|
||||
fromID = user.ID
|
||||
fromName = chatTitle
|
||||
|
||||
break
|
||||
}
|
||||
case *tg.PeerChat:
|
||||
inputPeer = &tg.InputPeerChat{
|
||||
ChatID: peer.ChatID,
|
||||
}
|
||||
chatType = "chat"
|
||||
for _, chatItem := range dialogs.Chats {
|
||||
chat, ok := chatItem.(*tg.Chat)
|
||||
if !ok || chat.ID != peer.ChatID {
|
||||
continue
|
||||
}
|
||||
|
||||
chatTitle = chat.Title
|
||||
|
||||
break
|
||||
}
|
||||
case *tg.PeerChannel:
|
||||
for _, channelItem := range dialogs.Chats {
|
||||
channel, ok := channelItem.(*tg.Channel)
|
||||
if !ok || channel.ID != peer.ChannelID {
|
||||
continue
|
||||
}
|
||||
|
||||
inputPeer = &tg.InputPeerChannel{
|
||||
ChannelID: channel.ID,
|
||||
AccessHash: channel.AccessHash,
|
||||
}
|
||||
chatType = "channel"
|
||||
chatTitle = channel.Title
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if inputPeer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := telegramLimiter.Wait(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "rate limiter wait")
|
||||
}
|
||||
|
||||
messagesClass, err := api.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||
Peer: inputPeer,
|
||||
OffsetID: 0,
|
||||
OffsetDate: 0,
|
||||
AddOffset: 0,
|
||||
Limit: 1, // We only need the latest message
|
||||
MaxID: 0,
|
||||
MinID: 0,
|
||||
Hash: 0,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get messages")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var messages *tg.MessagesMessages
|
||||
switch m := messagesClass.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
messages = m
|
||||
case *tg.MessagesMessagesSlice:
|
||||
messages = &tg.MessagesMessages{
|
||||
Messages: m.Messages,
|
||||
Chats: m.Chats,
|
||||
Users: m.Users,
|
||||
}
|
||||
case *tg.MessagesChannelMessages:
|
||||
messages = &tg.MessagesMessages{
|
||||
Messages: m.Messages,
|
||||
Chats: m.Chats,
|
||||
Users: m.Users,
|
||||
}
|
||||
default:
|
||||
log.Error().Msg("unexpected messages response type")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, msg := range messages.Messages {
|
||||
message, ok := msg.(*tg.Message)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if message.Out {
|
||||
continue
|
||||
}
|
||||
|
||||
if message.FromID != nil {
|
||||
if from, ok := message.FromID.(*tg.PeerUser); ok {
|
||||
for _, userItem := range messages.Users {
|
||||
user, ok := userItem.(*tg.User)
|
||||
if !ok || user.ID != from.UserID {
|
||||
continue
|
||||
}
|
||||
|
||||
fromID = user.ID
|
||||
fromName = user.FirstName + " " + user.LastName
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unreadMsg := UnreadMessage{
|
||||
ID: message.ID,
|
||||
Text: message.Message,
|
||||
Date: time.Unix(int64(message.Date), 0),
|
||||
FromID: fromID,
|
||||
FromName: fromName,
|
||||
ChatType: chatType,
|
||||
ChatTitle: chatTitle,
|
||||
}
|
||||
|
||||
// Only store if we haven't seen this user yet or if this message is newer
|
||||
if existingMsg, exists := userMessages[fromID]; !exists || unreadMsg.Date.After(existingMsg.Date) {
|
||||
userMessages[fromID] = unreadMsg
|
||||
processedCount++
|
||||
}
|
||||
|
||||
break // We only need the latest message
|
||||
}
|
||||
|
||||
if len(userMessages) >= *messageLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice and sort by date
|
||||
messages := make([]UnreadMessage, 0, len(userMessages))
|
||||
for _, msg := range userMessages {
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
// Sort messages by date in descending order
|
||||
sort.Slice(messages, func(i, j int) bool {
|
||||
return messages[i].Date.After(messages[j].Date)
|
||||
})
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
59
go.mod
Normal file
59
go.mod
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
module telegram-mcp
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/gotd/td v0.121.0
|
||||
github.com/metoro-io/mcp-golang v0.8.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/urfave/cli/v3 v3.1.0
|
||||
golang.org/x/time v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.13 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-faster/jx v1.1.0 // indirect
|
||||
github.com/go-faster/xor v1.0.0 // indirect
|
||||
github.com/go-faster/yaml v0.4.6 // indirect
|
||||
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/invopop/jsonschema v0.12.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // 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.10.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
139
go.sum
Normal file
139
go.sum
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
||||
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
||||
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
||||
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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.121.0 h1:U+KvqpJ5g/SOXIzSLMKLXW4i/ncGfnZEibS9SLtv44E=
|
||||
github.com/gotd/td v0.121.0/go.mod h1:e1bdmxa/tV1pnvDmBNRh8bhF0iqw7de8F2pN4MmbJ4g=
|
||||
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
|
||||
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/metoro-io/mcp-golang v0.8.0 h1:DkigHa3w7WwMFomcEz5wiMDX94DsvVm/3mCV3d1obnc=
|
||||
github.com/metoro-io/mcp-golang v0.8.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
||||
github.com/ogen-go/ogen v1.10.1 h1:oeSN8AF9mhTVfapbMuL8pQTF2ToqyW9xXaStmOhHKTA=
|
||||
github.com/ogen-go/ogen v1.10.1/go.mod h1:fXCg9PsNYEzJ8ABdmZ2A7j4hMi9EDHP53jzsNtIM3d0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/urfave/cli/v3 v3.1.0 h1:kQR+oiqpJkBAONxBjM4RWivD4AfPHL/f4vqe/gjYU8M=
|
||||
github.com/urfave/cli/v3 v3.1.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
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=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
|
||||
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
63
internal/tg/auth.go
Normal file
63
internal/tg/auth.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/telegram/auth"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Auth(phone string, appID int64, appHash string, sessionPath string) error {
|
||||
client := telegram.NewClient(int(appID), appHash, telegram.Options{
|
||||
SessionStorage: &telegram.FileSessionStorage{
|
||||
Path: sessionPath,
|
||||
},
|
||||
})
|
||||
|
||||
sessionDir := filepath.Dir(sessionPath)
|
||||
if err := os.MkdirAll(sessionDir, 0700); err != nil {
|
||||
return fmt.Errorf("mkdir(%s): %w", sessionDir, err)
|
||||
}
|
||||
|
||||
if err := client.Run(context.Background(), func(ctx context.Context) error {
|
||||
// Authenticate if needed
|
||||
flow := auth.NewFlow(auth.Constant(phone, "", auth.CodeAuthenticatorFunc(func(ctx context.Context, _ *tg.AuthSentCode) (string, error) {
|
||||
fmt.Print("Enter code: ")
|
||||
code, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(code), nil
|
||||
})), auth.SendCodeOptions{})
|
||||
|
||||
if err := client.Auth().IfNecessary(ctx, flow); err != nil {
|
||||
return fmt.Errorf("auth: %w", err)
|
||||
}
|
||||
|
||||
// Get current user info
|
||||
self, err := client.Self(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get self info: %w", err)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("first_name", self.FirstName).
|
||||
Str("last_name", self.LastName).
|
||||
Str("username", self.Username).
|
||||
Int64("id", self.ID).
|
||||
Msg("Logged in as")
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("client error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
20
internal/tg/client.go
Normal file
20
internal/tg/client.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package tg
|
||||
|
||||
import "github.com/gotd/td/telegram"
|
||||
|
||||
type Client struct {
|
||||
T *telegram.Client
|
||||
}
|
||||
|
||||
func New(appID int, appHash, sessionPath string) *Client {
|
||||
client := telegram.NewClient(appID, appHash, telegram.Options{
|
||||
SessionStorage: &telegram.FileSessionStorage{
|
||||
Path: sessionPath,
|
||||
},
|
||||
NoUpdates: true,
|
||||
})
|
||||
|
||||
return &Client{
|
||||
T: client,
|
||||
}
|
||||
}
|
||||
51
internal/tg/me.go
Normal file
51
internal/tg/me.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type MeResponse struct {
|
||||
ID int64 `json:"id" jsonschema:"required,description=User ID"`
|
||||
FirstName string `json:"first_name" jsonschema:"required,description=User's first name"`
|
||||
LastName string `json:"last_name" jsonschema:"description=User's last name"`
|
||||
Username string `json:"username" jsonschema:"description=User's username"`
|
||||
}
|
||||
|
||||
type EmptyArguments struct{}
|
||||
|
||||
func (c *Client) GetMe(_ EmptyArguments) (*mcp.ToolResponse, error) {
|
||||
var toolResponse *mcp.ToolResponse
|
||||
|
||||
if err := c.T.Run(context.Background(), func(ctx context.Context) error {
|
||||
self, err := c.T.Self(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get self info")
|
||||
}
|
||||
|
||||
// Create response
|
||||
response := MeResponse{
|
||||
ID: self.ID,
|
||||
FirstName: self.FirstName,
|
||||
LastName: self.LastName,
|
||||
Username: self.Username,
|
||||
}
|
||||
|
||||
// Convert response to JSON
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal response")
|
||||
}
|
||||
|
||||
toolResponse = mcp.NewToolResponse(mcp.NewTextContent(string(jsonData)))
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid session")
|
||||
}
|
||||
|
||||
return toolResponse, nil
|
||||
}
|
||||
79
main.go
Normal file
79
main.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
dir = ".telegram-mcp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to get home dir")
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, dir)
|
||||
sesionPath := filepath.Join(configDir, "session.json")
|
||||
|
||||
app := &cli.Command{
|
||||
Name: "telegram-mcp",
|
||||
Usage: "Telegram MCP server",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "app-id",
|
||||
Usage: "Telegram App ID",
|
||||
Required: true,
|
||||
Sources: cli.EnvVars("TG_APP_ID"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "api-hash",
|
||||
Usage: "Telegram API Hash",
|
||||
Required: true,
|
||||
Sources: cli.EnvVars("TG_API_HASH"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "session",
|
||||
Usage: "Path to session file",
|
||||
Value: sesionPath,
|
||||
Sources: cli.EnvVars("TG_SESSION_PATH"),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dry",
|
||||
Usage: "Test configuration",
|
||||
Local: true,
|
||||
HideDefault: true,
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "auth",
|
||||
Usage: "Authenticate with Telegram",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "phone",
|
||||
Usage: "Phone number to authenticate with",
|
||||
Required: true,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
},
|
||||
Action: authCommand,
|
||||
},
|
||||
},
|
||||
Action: serve,
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
58
serve.go
Normal file
58
serve.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"telegram-mcp/internal/tg"
|
||||
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/metoro-io/mcp-golang/transport/stdio"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func serve(ctx context.Context, cmd *cli.Command) error {
|
||||
appID := cmd.Int("app-id")
|
||||
appHash := cmd.String("api-hash")
|
||||
sessionPath := cmd.String("session")
|
||||
dryRun := cmd.Bool("dry")
|
||||
|
||||
_, err := os.Stat(sessionPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("session file not found(%s): %w", sessionPath, err)
|
||||
}
|
||||
|
||||
server := mcp.NewServer(stdio.NewStdioServerTransport())
|
||||
client := tg.New(int(appID), appHash, sessionPath)
|
||||
|
||||
if dryRun {
|
||||
answer, err := client.GetMe(tg.EmptyArguments{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(answer, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
log.Info().RawJSON("answer", data).Msg("Check GetMe: OK")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = server.RegisterTool("me", "Get current Telegram account info", client.GetMe)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register tool: %w", err)
|
||||
}
|
||||
|
||||
if err := server.Serve(); err != nil {
|
||||
return fmt.Errorf("serve: %w", err)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in a new issue