diff --git a/Taskfile.yml b/Taskfile.yml index 4a347df..0330c70 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -46,7 +46,10 @@ tasks: desc: Run linter cmd: golangci-lint run --fix ./... -# git tag -a v0.1.1 -m "Added releases" -# git push origin v0.1.0 + tag: + desc: Create a new tag + cmds: + - git tag -a v0.1.6 -m "Dialog fix" + - git push origin v0.1.6 diff --git a/go.mod b/go.mod index 3478591..c48f7a6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 github.com/spf13/pflag v1.0.6 + github.com/tidwall/gjson v1.18.0 github.com/urfave/cli/v3 v3.1.0 golang.org/x/time v0.11.0 ) @@ -34,7 +35,6 @@ require ( 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 diff --git a/internal/tg/dialogs.go b/internal/tg/dialogs.go index f42fbe6..e555430 100644 --- a/internal/tg/dialogs.go +++ b/internal/tg/dialogs.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "time" "github.com/gotd/td/tg" @@ -70,11 +71,16 @@ func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) { api := client.API() dialogsClass, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{ OffsetPeer: &tg.InputPeerEmpty{}, + Limit: 20, }) if err != nil { return fmt.Errorf("failed to get dialogs: %w", err) } + // Debug + // jsonData, _ := json.Marshal(dialogsClass) + // log.Info().RawJSON("dialogs", cleanJSON(jsonData)).Msg("dialogs") + var dialogs *tg.MessagesDialogs switch d := dialogsClass.(type) { case *tg.MessagesDialogs: @@ -90,6 +96,25 @@ func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) { return errors.New("unexpected dialogs response type") } + messageMap := make(map[string][]*tg.Message) + for _, m := range dialogs.Messages { + msg, ok := m.(*tg.Message) + if !ok { + continue + } + + if msg.PeerID == nil { + continue + } + + messageMap[msg.PeerID.String()] = append(messageMap[msg.PeerID.String()], msg) + } + + usersMap := make(map[string]tg.UserClass) + for _, u := range dialogs.Users { + usersMap["Peer"+u.String()] = u + } + result = make([]DialogInfo, 0, len(dialogs.Dialogs)) for _, dialog := range dialogs.Dialogs { @@ -103,22 +128,27 @@ func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) { info.LastMessageID = dialogItem.TopMessage if args.WithLastMessages { - for _, msg := range dialogs.Messages { - message, ok := msg.(*tg.Message) - if !ok { - continue + msgs := messageMap[dialogItem.Peer.String()] + for _, msg := range msgs { + var who string + if msg.FromID != nil { + if u, ok := usersMap[msg.FromID.String()]; ok { + who = u.String() + } } - var who string - if message.FromID != nil { - who = message.FromID.String() + // Limit message to 20 words + text := msg.Message + words := strings.Fields(text) + if len(words) > 20 { + text = strings.Join(words[:20], " ") + "..." } info.LastMessages = append(info.LastMessages, MessageInfo{ Who: who, - When: time.Unix(int64(message.Date), 0).Format(time.DateTime), - Text: message.Message, - IsUnread: !message.Out, + When: time.Unix(int64(msg.Date), 0).Format(time.DateTime), + Text: text, + IsUnread: dialogItem.UnreadCount > 0, }) } } @@ -206,15 +236,6 @@ func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) { return nil, errors.Wrap(err, "failed to marshal response") } - 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 + cleanedData := cleanJSON(jsonData) + return mcp.NewToolResponse(mcp.NewTextContent(string(cleanedData))), nil } diff --git a/internal/tg/helpers.go b/internal/tg/helpers.go new file mode 100644 index 0000000..f918ab0 --- /dev/null +++ b/internal/tg/helpers.go @@ -0,0 +1,88 @@ +package tg + +import ( + "encoding/json" + + "github.com/gotd/td/tg" + "github.com/tidwall/gjson" +) + +func getUserName(user *tg.User) string { + if username, ok := user.GetUsername(); ok && username != "" { + return "@" + username + } + + name := user.FirstName + if user.LastName != "" { + name += " " + user.LastName + } + + return name +} + +// cleanJSON removes empty/default fields from JSON +func cleanJSON(data []byte) []byte { + result := gjson.ParseBytes(data) + cleaned := cleanValue(result) + if cleaned == nil { + return data // Return original if cleaning failed + } + + cleanedJSON, err := json.Marshal(cleaned) + if err != nil { + return data // Return original if marshaling failed + } + + return cleanedJSON +} + +func cleanValue(v gjson.Result) interface{} { + switch v.Type { + case gjson.String: + if v.String() == "" { + return nil + } + return v.String() + case gjson.Number: + // return nil + if v.Int() == 0 && v.Float() == 0 { + return nil + } + return v.Value() + case gjson.True: + return nil + // return true + case gjson.False: + return nil + case gjson.Null: + return nil + case gjson.JSON: + if v.IsArray() { + arr := make([]interface{}, 0) + v.ForEach(func(_, item gjson.Result) bool { + if cleaned := cleanValue(item); cleaned != nil { + arr = append(arr, cleaned) + } + return true + }) + if len(arr) == 0 { + return nil + } + return arr + } + if v.IsObject() { + obj := make(map[string]interface{}) + v.ForEach(func(key, val gjson.Result) bool { + if cleaned := cleanValue(val); cleaned != nil { + obj[key.String()] = cleaned + } + return true + }) + if len(obj) == 0 { + return nil + } + return obj + } + } + return nil +} diff --git a/resourse.go b/resourse.go new file mode 100644 index 0000000..2c858c8 --- /dev/null +++ b/resourse.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/json" + "fmt" + + mcp "github.com/metoro-io/mcp-golang" +) + +func sampleResource() (*mcp.ResourceResponse, error) { + type Chat struct { + ID int64 `json:"id,omitempty"` + Type string `json:"type"` + Title string `json:"title"` + UnreadCount int `json:"unread_count"` + } + + chats := []Chat{ + { + ID: 123456789, + Type: "channel", + Title: "Sample Channel", + UnreadCount: 5, + }, + { + ID: 987654321, + Type: "group", + Title: "Test Group", + UnreadCount: 2, + }, + } + + rss := make([]*mcp.EmbeddedResource, 0, len(chats)) + for _, chat := range chats { + chat.ID = 0 + uri := fmt.Sprintf("telegram://chats/%d", chat.ID) + + content, err := json.Marshal(chat) + if err != nil { + return nil, err + } + + rss = append(rss, mcp.NewTextEmbeddedResource(uri, string(content), "application/json")) + } + + return mcp.NewResourceResponse(rss...), nil +} diff --git a/serve.go b/serve.go index 485c923..ea0e2d7 100644 --- a/serve.go +++ b/serve.go @@ -56,16 +56,21 @@ func serve(ctx context.Context, cmd *cli.Command) error { return nil } - err = server.RegisterTool("me", "Get current Telegram account info", client.GetMe) + err = server.RegisterTool("tg_me", "Get current account info", client.GetMe) if err != nil { return fmt.Errorf("register tool: %w", err) } - err = server.RegisterTool("dialogs", "Get list of dialogs (chats, channels, groups)", client.GetDialogs) + err = server.RegisterTool("tg_dialogs", "Get list of dialogs (chats, channels, groups)", client.GetDialogs) if err != nil { return fmt.Errorf("register dialogs tool: %w", err) } + err = server.RegisterResource("telegram://chats", "tg_chats", "List of telegram chats", "application/json", sampleResource) + if err != nil { + return fmt.Errorf("register chats resource: %w", err) + } + if err := server.Serve(); err != nil { return fmt.Errorf("serve: %w", err) }