Bot Menu via Ad-Hoc

This commit is contained in:
Bohdan Horbeshko 2025-04-28 20:46:21 -04:00
parent 3cac57e0f3
commit 4414c147d8
5 changed files with 162 additions and 16 deletions

View file

@ -58,6 +58,8 @@ type Client struct {
lastMsgIds map[int64]string
msgHashSeed maphash.Seed
LastBotCmdString string
XmppClientFeatures map[string]*[]string
XmppClientFeaturesLock sync.Mutex

View file

@ -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
}

View file

@ -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 != "" {

View file

@ -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, &note)
}
func probeClientFeatures(jid string, component *xmpp.Component) {
id, err := uuid.NewRandom()
if err != nil {

View file

@ -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, &note)
}
func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) {
payload = &stanza.Command{
SessionId: "loginwizard",