diff --git a/README.md b/README.md index dc941dd..a29c172 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,10 @@ The Model Context Protocol (MCP) is a system that lets AI apps, like Claude Desk ## What does this server do? - [x] Get current user data -- [ ] Get the list of dialogs (chats, channels, groups) +- [x] Get the list of dialogs (chats, channels, groups) - [ ] Get the list of (unread) messages in the given dialog - [ ] Mark chanel as read - [ ] Retrieve messages by date and time -- [ ] Download media files - [ ] Get the list of contacts - [ ] Draft a message diff --git a/cmd/test/main.go b/cmd/test/main.go index 3ed10b2..8475c27 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -39,7 +39,7 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - client := tg.New(appID, apiHash, sessionPath).T + client := tg.New(appID, apiHash, sessionPath).T() if err := client.Run(ctx, func(ctx context.Context) error { self, err := client.Self(ctx) diff --git a/internal/tg/client.go b/internal/tg/client.go index 3ed86b9..cc6b820 100644 --- a/internal/tg/client.go +++ b/internal/tg/client.go @@ -3,18 +3,24 @@ package tg import "github.com/gotd/td/telegram" type Client struct { - T *telegram.Client + appID int + appHash string + sessionPath string } func New(appID int, appHash, sessionPath string) *Client { - client := telegram.NewClient(appID, appHash, telegram.Options{ + return &Client{ + appID: appID, + appHash: appHash, + sessionPath: sessionPath, + } +} + +func (c *Client) T() *telegram.Client { + return telegram.NewClient(c.appID, c.appHash, telegram.Options{ SessionStorage: &telegram.FileSessionStorage{ - Path: sessionPath, + Path: c.sessionPath, }, NoUpdates: true, }) - - return &Client{ - T: client, - } } diff --git a/internal/tg/dialogs.go b/internal/tg/dialogs.go new file mode 100644 index 0000000..3fa6d1c --- /dev/null +++ b/internal/tg/dialogs.go @@ -0,0 +1,187 @@ +package tg + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/gotd/td/tg" + mcp "github.com/metoro-io/mcp-golang" + "github.com/pkg/errors" +) + +// DialogType represents the type of dialog for filtering +type DialogType string + +const ( + // DialogTypeAll represents all types of dialogs + DialogTypeAll DialogType = "" + // DialogTypeUser represents user chats + DialogTypeUser DialogType = "user" + // DialogTypeChat represents group chats + DialogTypeChat DialogType = "chat" + // DialogTypeChannel represents channels + DialogTypeChannel DialogType = "channel" + + // DefaultDialogsLimit is the default limit for dialogs + DefaultDialogsLimit = 100 +) + +// DialogsArguments contains parameters for getting dialogs +type DialogsArguments struct { + Type DialogType `json:"type,omitempty" jsonschema:"description=Filter dialogs by type (user, chat, channel or empty for all),enum=,enum=user,enum=chat,enum=channel"` + Limit int `json:"limit,omitempty" jsonschema:"description=Maximum number of dialogs to return (max: 100),default=100"` +} + +// DialogInfo represents a simplified dialog structure +type DialogInfo struct { + ID int64 `json:"id"` + Type string `json:"type"` + Title string `json:"title"` + UnreadCount int `json:"unread_count"` + LastMessageID int `json:"last_message_id"` + IsVerified bool `json:"is_verified,omitempty"` +} + +// GetDialogs returns a list of dialogs (chats, channels, groups) +func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) { + var result []DialogInfo + + if args.Limit <= 0 || args.Limit > DefaultDialogsLimit { + args.Limit = DefaultDialogsLimit + } + + if args.Type == "" { + args.Type = DialogTypeAll + } + + client := c.T() + if err := client.Run(context.Background(), func(ctx context.Context) error { + api := client.API() + dialogsClass, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{ + OffsetPeer: &tg.InputPeerEmpty{}, + }) + if err != nil { + return fmt.Errorf("failed to get dialogs: %w", err) + } + + 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 fmt.Errorf("unexpected dialogs response type") + } + + result = make([]DialogInfo, 0, len(dialogs.Dialogs)) + + for _, dialog := range dialogs.Dialogs { + dialogItem, ok := dialog.(*tg.Dialog) + if !ok { + continue + } + + var info DialogInfo + info.UnreadCount = dialogItem.UnreadCount + info.LastMessageID = dialogItem.TopMessage + + switch peer := dialogItem.Peer.(type) { + case *tg.PeerUser: + if args.Type != DialogTypeAll && args.Type != DialogTypeUser { + continue + } + + for _, userItem := range dialogs.Users { + user, ok := userItem.(*tg.User) + if !ok || user.ID != peer.UserID { + continue + } + + info.ID = user.ID + info.Type = "user" + info.Title = getUserName(user) + info.IsVerified = user.Verified + + result = append(result, info) + break + } + + case *tg.PeerChat: + if args.Type != DialogTypeAll && args.Type != DialogTypeChat { + continue + } + + for _, chatItem := range dialogs.Chats { + chat, ok := chatItem.(*tg.Chat) + if !ok || chat.ID != peer.ChatID { + continue + } + + info.ID = chat.ID + info.Type = "chat" + info.Title = chat.Title + + result = append(result, info) + break + } + + case *tg.PeerChannel: + if args.Type != DialogTypeAll && args.Type != DialogTypeChannel { + continue + } + + for _, channelItem := range dialogs.Chats { + channel, ok := channelItem.(*tg.Channel) + if !ok || channel.ID != peer.ChannelID { + continue + } + + info.ID = channel.ID + info.Type = "channel" + info.Title = channel.Title + info.IsVerified = channel.Verified + + result = append(result, info) + break + } + } + } + + return nil + }); err != nil { + return nil, errors.Wrap(err, "failed to get dialogs") + } + + // Convert response to JSON + jsonData, err := json.Marshal(result) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal response") + } + + sort.Slice(result, func(i, j int) bool { + return result[i].LastMessageID > result[j].LastMessageID + }) + + if len(result) > args.Limit { + result = result[:args.Limit] + } + + return mcp.NewToolResponse(mcp.NewTextContent(string(jsonData))), nil +} + +// Helper function to get user's name +func getUserName(user *tg.User) string { + name := user.FirstName + if user.LastName != "" { + name += " " + user.LastName + } + return name +} diff --git a/internal/tg/me.go b/internal/tg/me.go index cb05228..8105d9d 100644 --- a/internal/tg/me.go +++ b/internal/tg/me.go @@ -20,8 +20,9 @@ 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) + client := c.T() + if err := client.Run(context.Background(), func(ctx context.Context) error { + self, err := client.Self(ctx) if err != nil { return errors.Wrap(err, "failed to get self info") } diff --git a/serve.go b/serve.go index ae5cae1..0adc8c6 100644 --- a/serve.go +++ b/serve.go @@ -41,6 +41,18 @@ func serve(ctx context.Context, cmd *cli.Command) error { log.Info().RawJSON("answer", data).Msg("Check GetMe: OK") + answer, err = client.GetDialogs(tg.DialogsArguments{}) + if err != nil { + return fmt.Errorf("get dialogs: %w", err) + } + + data, err = json.MarshalIndent(answer, "", " ") + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + + log.Info().RawJSON("answer", data).Msg("Check GetDialogs: OK") + return nil } @@ -49,6 +61,11 @@ func serve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("register tool: %w", err) } + err = server.RegisterTool("dialogs", "Get list of dialogs (chats, channels, groups)", client.GetDialogs) + if err != nil { + return fmt.Errorf("register dialogs tool: %w", err) + } + if err := server.Serve(); err != nil { return fmt.Errorf("serve: %w", err) }