package tg
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/gotd/td/tg"
mcp "github.com/metoro-io/mcp-golang"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// DialogType represents the type of dialog for filtering
type DialogType string
const (
DialogTypeUnknown DialogType = "unknown"
DialogTypeAll DialogType = ""
DialogTypeUser DialogType = "user"
DialogTypeBot DialogType = "bot"
DialogTypeChat DialogType = "chat"
DialogTypeChannel DialogType = "channel"
DefaultDialogsLimit = 100
)
// nolint:lll
type DialogsArguments struct {
Offset string `json:"offset,omitempty" jsonschema:"description=Offset for continuation"`
OnlyUnread bool `json:"only_unread,omitempty" jsonschema:"description=Include only dialogs with unread mark"`
}
type MessageInfo struct {
Who string `json:"who,omitempty"`
When string `json:"when"`
Text string `json:"text,omitempty"`
IsUnread bool `json:"is_unread,omitempty"`
ts int
}
type DialogInfo struct {
Name string `json:"name,omitempty"`
Type string `json:"type"`
Title string `json:"title"`
LastMessage *MessageInfo `json:"last_message,omitempty"`
Empty bool `json:"empty,omitempty"`
}
type DialogsResponse struct {
Dialogs []DialogInfo `json:"dialogs"`
Offset DialogsOffset `json:"offset"`
}
// GetDialogs returns a list of dialogs (chats, channels, groups)
func (c *Client) GetDialogs(args DialogsArguments) (*mcp.ToolResponse, error) {
var offset DialogsOffset
if args.Offset != "" {
if err := offset.UnmarshalJSON([]byte(args.Offset)); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal offset")
}
}
if offset.Peer == nil {
offset.Peer = &tg.InputPeerEmpty{}
}
var dc tg.MessagesDialogsClass
client := c.T()
if err := client.Run(context.Background(), func(ctx context.Context) (err error) {
api := client.API()
dc, err = api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
OffsetPeer: offset.Peer,
OffsetID: offset.MsgID,
OffsetDate: offset.Date,
})
if err != nil {
return fmt.Errorf("failed to get dialogs: %w", err)
}
// Debug
//jsonData, _ := json.Marshal(dc)
//log.Info().RawJSON("dialogs", cleanJSON(jsonData)).Msg("dialogs")
return nil
}); err != nil {
return nil, errors.Wrap(err, "failed to get dialogs")
}
d, err := newDialogs(dc, args.OnlyUnread)
if err != nil {
return nil, errors.Wrap(err, "failed to get dialogs")
}
rsp := DialogsResponse{
Dialogs: d.Info(),
Offset: d.Offset(),
}
jsonData, err := json.Marshal(rsp)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal response")
}
return mcp.NewToolResponse(mcp.NewTextContent(string(jsonData))), nil
}
type dialogs struct {
tg.MessagesDialogs
// chat id key
messages map[int64]*tg.Message
users map[int64]*tg.User
//dialogs map[string]*tg.Dialog
chats map[int64]*tg.Chat
channels map[int64]*tg.Channel
//opts
onlyUnread bool
}
func newDialogs(rawD tg.MessagesDialogsClass, onlyUnread bool) (*dialogs, error) {
var d dialogs
switch dT := rawD.(type) {
case *tg.MessagesDialogs:
d = dialogs{MessagesDialogs: *dT}
case *tg.MessagesDialogsSlice:
d = dialogs{MessagesDialogs: tg.MessagesDialogs{
Dialogs: dT.Dialogs,
Messages: dT.Messages,
Chats: dT.Chats,
Users: dT.Users,
}}
case *tg.MessagesDialogsNotModified:
default:
}
d.messages = make(map[int64]*tg.Message)
for _, m := range d.Messages {
switch mT := m.(type) {
case *tg.Message:
d.messages[getPeerID(mT.PeerID)] = mT
case *tg.MessageService, *tg.MessageEmpty:
default:
}
}
delete(d.messages, 0)
d.users = make(map[int64]*tg.User)
for _, uc := range d.Users {
u, ok := uc.(*tg.User)
if !ok {
log.Debug().Msgf("newDialogs(%+v): invalid message user", uc)
continue
}
d.users[u.GetID()] = u
}
d.chats = make(map[int64]*tg.Chat)
d.channels = make(map[int64]*tg.Channel)
for _, c := range d.Chats {
switch cT := c.(type) {
case *tg.Chat:
d.chats[cT.ID] = cT
case *tg.Channel:
d.channels[cT.ID] = cT
case *tg.ChatForbidden, *tg.ChannelForbidden, *tg.ChatEmpty:
default:
}
}
d.onlyUnread = onlyUnread
return &d, nil
}
func (d *dialogs) Info() []DialogInfo {
ds := make([]DialogInfo, 0, len(d.Dialogs))
for _, dItem := range d.Dialogs {
dialogItem, ok := dItem.(*tg.Dialog)
if !ok {
continue
}
if d.onlyUnread && dialogItem.UnreadCount == 0 {
continue
}
info, err := d.processDialog(dialogItem)
if err != nil {
log.Debug().Err(err).Str("dialog", dItem.String()).Msg("failed process dialog")
continue
}
if info.Title == "" {
continue
}
ds = append(ds, info)
}
return ds
}
func (d *dialogs) Offset() DialogsOffset {
for i := len(d.Dialogs) - 1; i >= 0; i-- {
dialogItem, ok := d.Dialogs[i].(*tg.Dialog)
if !ok {
continue
}
if dialogItem.Peer == nil {
continue
}
msg, ok := d.messages[getPeerID(dialogItem.Peer)]
if !ok {
continue
}
return DialogsOffset{
MsgID: msg.ID,
Date: msg.Date,
Peer: getInputPeerID(dialogItem.Peer),
}
}
return DialogsOffset{}
}
func (d *dialogs) processDialog(dialogItem *tg.Dialog) (DialogInfo, error) {
var info DialogInfo
if msg, ok := d.messages[getPeerID(dialogItem.Peer)]; ok {
var who string
if msg.FromID != nil {
name, _, err := d.getNameID(msg.FromID)
if err != nil {
return DialogInfo{}, errors.Wrap(err, "failed to get dialog name")
}
who = name
}
// Limit message to 20 words
text := msg.Message
words := strings.Fields(text)
if len(words) > 20 {
text = strings.Join(words[:20], " ") + "..."
}
info.LastMessage = &MessageInfo{
Who: who,
When: time.Unix(int64(msg.Date), 0).Format(time.DateTime),
ts: msg.Date,
Text: text,
IsUnread: dialogItem.UnreadCount > 0,
}
}
if dialogItem.Peer == nil {
return DialogInfo{}, fmt.Errorf("no peer: %s", dialogItem.String())
}
var err error
info.Title, info.Name, err = d.getNameID(dialogItem.Peer)
if err != nil {
return DialogInfo{}, err
}
info.Type = string(d.getType(dialogItem))
if info.LastMessage == nil {
info.Empty = true
}
return info, nil
}
func (d *dialogs) getNameID(pC tg.PeerClass) (string, string, error) {
var name, username string
switch p := pC.(type) {
case *tg.PeerUser:
u, ok := d.users[p.GetUserID()]
if !ok {
return "", "", errors.Errorf("peerid(%d): invalid message user", p.GetUserID())
}
name = getTitle(u)
username = getUsername(u)
case *tg.PeerChannel:
channel, ok := d.channels[p.GetChannelID()]
if !ok {
return "", "", errors.Errorf("peerid(%d): invalid message channel", p.GetChannelID())
}
name = getTitle(channel)
username = getUsername(channel)
case *tg.PeerChat:
chat, ok := d.chats[p.GetChatID()]
if !ok {
return "", "", errors.Errorf("peerid(%d): invalid message chat", p.GetChatID())
}
name = getTitle(chat)
username = getUsername(chat)
default:
return "", "", fmt.Errorf("chose author(%T): invalid dialog peer", p)
}
return name, username, nil
}
func (d *dialogs) getType(rawD *tg.Dialog) DialogType {
switch v := rawD.Peer.(type) {
case *tg.PeerChannel:
return DialogTypeChannel
case *tg.PeerChat:
return DialogTypeChat
case *tg.PeerUser:
u, ok := d.users[getPeerID(rawD.Peer)]
if !ok {
log.Debug().Msgf("getType(%+v): user not found", v)
return DialogTypeUser
}
if u.Bot {
return DialogTypeBot
}
return DialogTypeUser
default:
log.Debug().Msgf("getType(%+v): unknown dialog type", v)
return DialogTypeUnknown
}
}
func getPeerID(p tg.PeerClass) int64 {
if p == nil {
return 0
}
switch v := p.(type) {
case *tg.PeerChannel:
return v.ChannelID
case *tg.PeerChat:
return v.ChatID
case *tg.PeerUser:
return v.UserID
default:
return 0
}
}