From fd0d7411c2f5e1e0368d3494318ce05195d0a56d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 30 Jan 2024 21:38:46 -0500 Subject: [PATCH] Basic Ad-Hoc support for transport commands --- go.mod | 2 +- go.sum | 2 + telegram/commands.go | 58 +++++++++++++++++-------- xmpp/handlers.go | 100 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 133 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 949667e..fe7aeb4 100644 --- a/go.mod +++ b/go.mod @@ -33,5 +33,5 @@ require ( nhooyr.io/websocket v1.6.5 // indirect ) -replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f +replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 diff --git a/go.sum b/go.sum index d44752b..f5e218f 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 h1:CWAQT74L dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd h1:+UW+E7JjI88aH4beDn1cw6D8rs1I061hN91HU4Y4pT8= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/telegram/commands.go b/telegram/commands.go index 9251ebb..1c10d12 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -48,6 +48,7 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ + "help": command{"", "help"}, "login": command{"phone", "sign in"}, "logout": command{"", "sign out"}, "cancelauth": command{"", "quit the signin wizard"}, @@ -66,6 +67,7 @@ var transportCommands = map[string]command{ } var chatCommands = map[string]command{ + "help": command{"", "help"}, "d": command{"[n]", "delete your last message(s)"}, "s": command{"edited message", "edit your last message"}, "silent": command{"message", "send a message without sound"}, @@ -110,38 +112,56 @@ type command struct { } type configurationOption command -type helpType int +// CommandType disinguishes command sets by chat +type CommandType int const ( - helpTypeTransport helpType = iota - helpTypeChat + CommandTypeTransport CommandType = iota + CommandTypeChat ) -func helpString(ht helpType) string { - var str strings.Builder +// GetCommands exposes the set of commands +func GetCommands(typ CommandType) map[string]command { var commandMap map[string]command - switch ht { - case helpTypeTransport: + switch typ { + case CommandTypeTransport: commandMap = transportCommands - case helpTypeChat: + case CommandTypeChat: commandMap = chatCommands } + return commandMap +} + +// CommandToHelpString builds a text description of a command +func CommandToHelpString(name string, cmd command) string { + var str strings.Builder + + str.WriteString("/") + str.WriteString(name) + if cmd.arguments != "" { + str.WriteString(" ") + str.WriteString(cmd.arguments) + } + str.WriteString(" — ") + str.WriteString(cmd.description) + + return str.String() +} + +func helpString(typ CommandType) string { + var str strings.Builder + + commandMap := GetCommands(typ) + str.WriteString("Available commands:\n") for name, command := range commandMap { - str.WriteString("/") - str.WriteString(name) - if command.arguments != "" { - str.WriteString(" ") - str.WriteString(command.arguments) - } - str.WriteString(" — ") - str.WriteString(command.description) + str.WriteString(CommandToHelpString(name, command)) str.WriteString("\n") } - if ht == helpTypeTransport { + if typ == CommandTypeTransport { str.WriteString("Configuration options\n") for name, option := range transportConfigurationOptions { str.WriteString(name) @@ -448,7 +468,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string case "channel": return c.cmdChannel(args, cmdline) case "help": - return helpString(helpTypeTransport) + return helpString(CommandTypeTransport) } return "" @@ -1088,7 +1108,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return strings.Join(entries, "\n"), true case "help": - return helpString(helpTypeChat), true + return helpString(CommandTypeChat), true default: return "", false } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 8c6ba37..a062f0c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -26,6 +26,7 @@ const ( TypeVCard4 ) const NodeVCard4 string = "urn:xmpp:vcard4" +const NSCommand string = "http://jabber.org/protocol/commands" func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) @@ -53,14 +54,14 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { return } } - _, ok = iq.Payload.(*stanza.DiscoInfo) + discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { - go handleGetDiscoInfo(s, iq) + go handleGetDiscoInfo(s, iq, discoInfo) return } - _, ok = iq.Payload.(*stanza.DiscoItems) + discoItems, ok := iq.Payload.(*stanza.DiscoItems) if ok { - go handleGetDiscoItems(s, iq) + go handleGetDiscoItems(s, iq, discoItems) return } _, ok = iq.Payload.(*extensions.QueryRegister) @@ -74,6 +75,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryRegister(s, iq, query) return } + command, ok := iq.Payload.(*stanza.Command) + if ok { + go handleSetQueryCommand(s, iq, command) + return + } } } @@ -468,7 +474,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } -func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { +func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -488,8 +494,20 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { disco.AddFeatures(stanza.NSMsgChatMarkers) disco.AddFeatures(stanza.NSMsgReceipts) } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") - disco.AddFeatures("jabber:iq:register") + if di.Node == "" { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + disco.AddFeatures("jabber:iq:register") + disco.AddFeatures(NSCommand) + } else { + for name, command := range telegram.GetCommands(telegram.CommandTypeTransport) { + if di.Node == name { + answer.Payload = di + di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") + di.AddFeatures(NSCommand, "jabber:x:data") + break + } + } + } } answer.Payload = disco @@ -504,7 +522,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } -func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { +func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -517,7 +535,20 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { return } - answer.Payload = answer.DiscoItems() + log.Debugf("discoItems: %#v", di) + + _, ok := toToID(iq.To) + if !ok { + commands := telegram.GetCommands(telegram.CommandTypeTransport) + if di.Node == NSCommand { + answer.Payload = di + for name, command := range commands { + di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) + } + } else { + answer.Payload = answer.DiscoItems() + } + } component, ok := s.(*xmpp.Component) if !ok { @@ -647,6 +678,57 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } } +func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command) { + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + answer, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeResult, + From: iq.To, + To: iq.From, + Id: iq.Id, + Lang: "en", + }) + if err != nil { + log.Errorf("Failed to create answer IQ: %v", err) + return + } + + defer gateway.ResumableSend(component, answer) + + log.Debugf("command: %#v", command) + + if command.Action == "" || command.Action == stanza.CommandActionExecute { + _, ok := toToID(iq.To) + if !ok { + bare, resource, ok := gateway.SplitJID(iq.From) + if !ok { + return + } + + session, ok := sessions[bare] + if !ok { + return + } + + response := session.ProcessTransportCommand("/" + command.Node, resource) + + answer.Payload = &stanza.Command{ + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElement: &stanza.Note{ + Text: response, + Type: stanza.CommandNoteTypeInfo, + }, + } + log.Debugf("command response: %#v", answer.Payload) + } + } +} + func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Type = stanza.IQTypeError answer.Payload = *payload