From 4414c147d8deab24a893d452090f7502246b8434 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Apr 2025 20:46:21 -0400 Subject: [PATCH] Bot Menu via Ad-Hoc --- telegram/client.go | 2 + telegram/handlers.go | 15 ++++++- telegram/utils.go | 48 +++++++++++++++++++- xmpp/handlers.go | 104 +++++++++++++++++++++++++++++++++++++++++-- xmpp/loginwizard.go | 9 ---- 5 files changed, 162 insertions(+), 16 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index bd24adb..daaf627 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -58,6 +58,8 @@ type Client struct { lastMsgIds map[int64]string msgHashSeed maphash.Seed + LastBotCmdString string + XmppClientFeatures map[string]*[]string XmppClientFeaturesLock sync.Mutex diff --git a/telegram/handlers.go b/telegram/handlers.go index 8d1412d..d1e4dce 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -247,10 +247,23 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) + var forceCmd bool + if c.LastBotCmdString != "" && update.Message.IsOutgoing { + if update.Message.Content.MessageContentType() == client.TypeMessageText { + textMessage, _ := update.Message.Content.(*client.MessageText) + + if textMessage.Text != nil && textMessage.Text.Text == c.LastBotCmdString { + forceCmd = true + c.LastBotCmdString = "" + } + } + } + // ignore self outgoing messages if update.Message.IsOutgoing && update.Message.SendingState != nil && - update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending { + update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending && + !forceCmd { return } diff --git a/telegram/utils.go b/telegram/utils.go index 98d9d11..b0d1dd1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -47,6 +47,16 @@ type messageStub struct { Text string } +type BotCommand struct { + Command string + Description string +} + +type BotLink struct { + Description string + Link string +} + const ( typeFileDataSha1 byte = iota typeFileDataBase64 @@ -211,6 +221,40 @@ func (c *Client) IsPM(id int64) (bool, error) { return false, nil } +// IsBot checks if a chat is a bot +func (c *Client) IsBot(id int64) (bool, error) { + _, user, err := c.GetContactByID(id, nil) + if err != nil { + return false, err + } + if user == nil || user.Type == nil { + return false, nil + } + + return user.Type.UserTypeType() == client.TypeUserTypeBot, nil +} + +// GetBotMenu retrieves the bot's attachment menu +func (c *Client) GetBotMenu(id int64) (*BotLink, []*BotCommand, error) { + fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ + UserId: id, + }) + if err == nil && fullInfo.BotInfo != nil { + if fullInfo.BotInfo.MenuButton != nil { + menuButton := fullInfo.BotInfo.MenuButton + return &BotLink{menuButton.Text, menuButton.Url}, nil, nil + } else { + var commands []*BotCommand + for _, command := range fullInfo.BotInfo.Commands { + commands = append(commands, &BotCommand{command.Command, command.Description}) + } + return nil, commands, nil + } + } + + return nil, nil, err +} + func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { var show, textStatus, presenceType string @@ -1275,13 +1319,13 @@ func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageC } // ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64) int64 { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, raw bool) int64 { if !c.Online() { // we're offline return 0 } - if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { + if replaceId == 0 && !raw && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand, _ := c.ProcessChatCommand(chatID, text) if response != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1857787..49c41fb 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -204,7 +204,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() - tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) + tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, false) if tgMessageId != 0 { if replaceId != 0 { // not needed (is it persistent among clients though?) @@ -679,7 +679,7 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) - _, toOk := toToID(iq.To) + toID, toOk := toToID(iq.To) if di.Node == gateway.NSCommand { answer.Payload = di @@ -698,6 +698,13 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { session, ok := sessions[bare] if ok { isOnline = session.Online() + + if toOk { + isBot, err := session.IsBot(toID) + if err == nil && isBot { + di.AddItem(iq.To, "botmenu", "Bot Menu") + } + } } } @@ -966,7 +973,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command CommandElements: elements, } } - } else if command.Node == "loginwizard" { + } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource) @@ -974,6 +981,31 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if cancelSend { go sendLoginWizardResponse(component, answer, session) } + } else if toOk && command.Node == "botmenu" { + payload := &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + } + answer.Payload = payload + + if len(form.Fields) == 1 && form.Fields[0] != nil && + form.Fields[0].Var == "command" && len(form.Fields[0].ValuesList) == 1 { + session, ok := sessions[bare] + if ok { + msgText := "/"+form.Fields[0].ValuesList[0] + session.LastBotCmdString = msgText + tgMessageId := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, true) + if tgMessageId != 0 { + payload.Status = stanza.CommandStatusCompleted + } else { + setCommandPayloadError(payload, "Failed to send a bot command") + } + } else { + setCommandPayloadError(payload, "Session is lost") + } + } else { + setCommandPayloadError(payload, "Broken form") + } } else { // just for the case the client messed the order somehow sort.Slice(form.Fields, func(i int, j int) bool { @@ -1102,7 +1134,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command CommandElements: []stanza.CommandElement{&form}, } log.Debugf("form: %#v", form) - } else if command.Node == "loginwizard" { + } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource) @@ -1110,6 +1142,61 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if cancelSend { go sendLoginWizardResponse(component, answer, session) } + } else if toOk && command.Node == "botmenu" { + session, ok := sessions[bare] + + var link *telegram.BotLink + var commands []*telegram.BotCommand + var err error + if ok { + link, commands, err = session.GetBotMenu(toId) + } + + payload := &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + } + answer.Payload = payload + + if !ok || err != nil { + setCommandPayloadError(payload, "Cannot retrieve commands") + } else { + if link != nil { + payload.Status = stanza.CommandStatusCompleted + payload.CommandElements = []stanza.CommandElement{ + &stanza.Note{ + Text: fmt.Sprintf("%v: %v", link.Description, link.Link), + Type: stanza.CommandNoteTypeInfo, + }, + } + } else { + var options []stanza.Option + for _, cmd := range commands { + options = append(options, stanza.Option{ + Label: fmt.Sprintf("/%v — %v", cmd.Command, cmd.Description), + ValuesList: []string{cmd.Command}, + }) + } + + dummyString := "" + field := stanza.Field{ + Var: "command", + Type: stanza.FieldTypeListSingle, + Required: &dummyString, + Options: options, + } + log.Debugf("field: %#v", field) + + form := stanza.Form{ + Type: stanza.FormTypeForm, + Fields: []*stanza.Field{&field}, + } + log.Debugf("form: %#v", form) + + payload.Status = stanza.CommandStatusExecuting + payload.CommandElements = []stanza.CommandElement{&form} + } + } } else { cmdString = "/" + command.Node } @@ -1206,6 +1293,15 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code } } +func setCommandPayloadError(payload *stanza.Command, err string) { + note := stanza.Note{ + Text: err, + Type: stanza.CommandNoteTypeErr, + } + payload.Status = stanza.CommandStatusCompleted + payload.CommandElements = append(payload.CommandElements, ¬e) +} + func probeClientFeatures(jid string, component *xmpp.Component) { id, err := uuid.NewRandom() if err != nil { diff --git a/xmpp/loginwizard.go b/xmpp/loginwizard.go index 28f6ac1..d1b161a 100644 --- a/xmpp/loginwizard.go +++ b/xmpp/loginwizard.go @@ -11,15 +11,6 @@ import ( "gosrc.io/xmpp/stanza" ) -func setCommandPayloadError(payload *stanza.Command, err string) { - note := stanza.Note{ - Text: err, - Type: stanza.CommandNoteTypeErr, - } - payload.Status = stanza.CommandStatusCompleted - payload.CommandElements = append(payload.CommandElements, ¬e) -} - func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) { payload = &stanza.Command{ SessionId: "loginwizard",