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