diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | telegabber.go | 2 | ||||
-rw-r--r-- | telegram/commands.go | 579 | ||||
-rw-r--r-- | telegram/utils.go | 177 | ||||
-rw-r--r-- | telegram/utils_test.go | 6 | ||||
-rw-r--r-- | xmpp/handlers.go | 272 |
8 files changed, 733 insertions, 309 deletions
@@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.4" +VERSION := "v1.10.0-dev" MAKEOPTS := "-j4" all: @@ -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 @@ -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/telegabber.go b/telegabber.go index 9e71887..d39820d 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.4" +var version string = "1.10.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 4730a3f..397ba91 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -3,6 +3,7 @@ package telegram import ( "fmt" "github.com/pkg/errors" + "sort" "strconv" "strings" "time" @@ -14,6 +15,7 @@ import ( "github.com/zelenin/go-tdlib/client" ) +const unknownCommand string = "Unknown command" const notEnoughArguments string = "Not enough arguments" const TelegramNotInitialized string = "Telegram connection is not initialized yet" const TelegramAuthDone string = "Authorization is done already" @@ -48,54 +50,61 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ - "login": command{"phone", "sign in"}, - "logout": command{"", "sign out"}, - "cancelauth": command{"", "quit the signin wizard"}, - "code": command{"", "check one-time code"}, - "password": command{"", "check 2fa password"}, - "setusername": command{"", "update @username"}, - "setname": command{"first last", "update name"}, - "setbio": command{"", "update about"}, - "setpassword": command{"[old] [new]", "set or remove password"}, - "config": command{"[param] [value]", "view or update configuration options"}, - "report": command{"[chat] [comment]", "report a chat by id or @username"}, - "add": command{"@username", "add @username to your chat list"}, - "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, - "supergroup": command{"title description", "create new supergroup «title» with «description»"}, - "channel": command{"title description", "create new channel «title» with «description»"}, + "help": command{0, []string{}, "help", nil}, + "login": command{1, []string{"phone"}, "sign in", nil}, + "logout": command{0, []string{}, "sign out", nil}, + "cancelauth": command{0, []string{}, "quit the signin wizard", nil}, + "code": command{1, []string{"xxxxx"}, "check one-time code", nil}, + "password": command{1, []string{"********"}, "check 2fa password", nil}, + "setusername": command{0, []string{"@username"}, "update @username", nil}, + "setname": command{1, []string{"first", "last"}, "update name", nil}, + "setbio": command{0, []string{"Lorem ipsum"}, "update about", nil}, + "setpassword": command{0, []string{"old", "new"}, "set or remove password", nil}, + "config": command{0, []string{"param", "value"}, "view or update configuration options", nil}, + "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, } +var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} +var notForPM = []ChatType{ChatTypePrivate, ChatTypeSecret} +var notForPMAndBasic = []ChatType{ChatTypePrivate, ChatTypeSecret, ChatTypeBasicGroup} +var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} + var chatCommands = map[string]command{ - "d": command{"[n]", "delete your last message(s)"}, - "s": command{"edited message", "edit your last message"}, - "silent": command{"message", "send a message without sound"}, - "schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"}, - "forward": command{"message_id target_chat", "forwards a message"}, - "vcard": command{"", "print vCard as text"}, - "add": command{"@username", "add @username to your chat list"}, - "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, - "group": command{"title", "create groupchat «title» with current user"}, - "supergroup": command{"title description", "create new supergroup «title» with «description»"}, - "channel": command{"title description", "create new channel «title» with «description»"}, - "secret": command{"", "create secretchat with current user"}, - "search": command{"string [limit]", "search <string> in current chat"}, - "history": command{"[limit]", "get last [limit] messages from current chat"}, - "block": command{"", "blacklist current user"}, - "unblock": command{"", "unblacklist current user"}, - "invite": command{"id or @username", "add user to current chat"}, - "link": command{"", "get invite link for current chat"}, - "kick": command{"id or @username", "remove user to current chat"}, - "mute": command{"[id or @username] [hours]", "mute the whole chat or a user in current chat"}, - "unmute": command{"[id or @username]", "unmute the whole chat or a user in the current chat"}, - "ban": command{"id or @username [hours]", "restrict @username from current chat for [hours] or forever"}, - "unban": command{"id or @username", "unbans @username in current chat (and devotes from admins)"}, - "promote": command{"id or @username [title]", "promote user to admin in current chat"}, - "leave": command{"", "leave current chat"}, - "leave!": command{"", "leave current chat (for owners)"}, - "ttl": command{"", "set secret chat messages TTL before self-destroying (in seconds)"}, - "close": command{"", "close current secret chat"}, - "delete": command{"", "delete current chat from chat list"}, - "members": command{"[query]", "search members [by optional query] in current chat (requires admin rights)"}, + "help": command{0, []string{}, "help", nil}, + "d": command{0, []string{"n"}, "delete your last message(s)", nil}, + "s": command{1, []string{"edited message"}, "edit your last message", nil}, + "silent": command{1, []string{"message"}, "send a message without sound", nil}, + "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", nil}, + "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", nil}, + "vcard": command{0, []string{}, "print vCard as text", nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, + "group": command{1, []string{"title"}, "create groupchat «title» with current user", ¬ForGroups}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, + "secret": command{0, []string{}, "create secretchat with current user", ¬ForGroups}, + "search": command{0, []string{"string", "[limit]"}, "search <string> in current chat", nil}, + "history": command{0, []string{"limit"}, "get last [limit] messages from current chat", nil}, + "block": command{0, []string{}, "blacklist current user", ¬ForGroups}, + "unblock": command{0, []string{}, "unblacklist current user", ¬ForGroups}, + "invite": command{1, []string{"id or @username"}, "add user to current chat", ¬ForPM}, + "link": command{0, []string{}, "get invite link for current chat", ¬ForPM}, + "kick": command{1, []string{"id or @username"}, "remove user from current chat", ¬ForPM}, + "mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", ¬ForPMAndBasic}, + "unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", ¬ForPMAndBasic}, + "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", ¬ForPM}, + "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", ¬ForPM}, + "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", ¬ForPM}, + "leave": command{0, []string{}, "leave current chat", ¬ForPM}, + "leave!": command{0, []string{}, "leave current chat (for owners)", ¬ForPM}, + "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", &onlyForSecret}, + "close": command{0, []string{}, "close current secret chat", &onlyForSecret}, + "delete": command{0, []string{}, "delete current chat from chat list", nil}, + "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", nil}, } var transportConfigurationOptions = map[string]configurationOption{ @@ -105,43 +114,113 @@ var transportConfigurationOptions = map[string]configurationOption{ } type command struct { + RequiredArgs int + Arguments []string + Description string + NotFor *[]ChatType +} +type configurationOption struct { arguments string description string } -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 +} + +// GetCommand obtains one command +func GetCommand(typ CommandType, cmd string) (command, bool) { + commands := GetCommands(typ) + command, ok := commands[cmd] + return command, ok +} + +// SortedCommandKeys sorts a slice with command keys +func SortedCommandKeys(commandMap map[string]command) []string { + keys := make([]string, len(commandMap)) + + i := 0 + for k := range commandMap { + keys[i] = k + i++ + } + + sort.Strings(keys) + + return keys +} + +// CommandToHelpString builds a text description of a command +func CommandToHelpString(name string, cmd command) string { + var str strings.Builder + + str.WriteString("/") + str.WriteString(name) + for i, arg := range cmd.Arguments { + optional := i >= cmd.RequiredArgs + str.WriteString(" ") + if optional { + str.WriteString("[") + } + str.WriteString(arg) + if optional { + str.WriteString("]") + } + } + str.WriteString(" — ") + str.WriteString(cmd.Description) + + return str.String() +} + +// IsCommandFor checks the suitability of a command for a chat type +func IsCommandForChatType(cmd command, chatType ChatType) bool { + if cmd.NotFor != nil { + for _, typ := range *cmd.NotFor { + if chatType == typ { + return false + } + } + } + + return true +} + +func (c *Client) helpString(typ CommandType, chatId int64) string { + var str strings.Builder + + commandMap := GetCommands(typ) + chatType, chatTypeErr := c.GetChatType(chatId) + str.WriteString("Available commands:\n") - for name, command := range commandMap { - str.WriteString("/") - str.WriteString(name) - if command.arguments != "" { - str.WriteString(" ") - str.WriteString(command.arguments) + for _, name := range SortedCommandKeys(commandMap) { + command := commandMap[name] + if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { + continue } - 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) @@ -229,23 +308,27 @@ func (c *Client) usernameOrIDToID(username string) (int64, error) { } // ProcessTransportCommand executes a command sent directly to the component -// and returns a response -func (c *Client) ProcessTransportCommand(cmdline string, resource string) string { +// and returns a response and execution success result +func (c *Client) ProcessTransportCommand(cmdline string, resource string) (string, bool) { cmd, args := parseCommand(cmdline) + command, ok := transportCommands[cmd] + if !ok { + return unknownCommand, false + } + if len(args) < command.RequiredArgs { + return notEnoughArguments, false + } + switch cmd { case "login", "code", "password": if cmd == "login" && c.Session.Login != "" { - return "Phone number already provided, use /cancelauth to start over" - } - - if len(args) < 1 { - return notEnoughArguments + return "Phone number already provided, use /cancelauth to start over", false } if cmd == "login" { err := c.TryLogin(resource, args[0]) if err != nil { - return err.Error() + return err.Error(), false } c.locks.authorizerWriteLock.Lock() @@ -257,11 +340,11 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string defer c.locks.authorizerWriteLock.Unlock() if c.authorizer == nil { - return TelegramNotInitialized + return TelegramNotInitialized, false } if c.authorizer.isClosed { - return TelegramAuthDone + return TelegramAuthDone, false } switch cmd { @@ -276,12 +359,12 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string // sign out case "logout": if !c.Online() { - return notOnline + return notOnline, false } _, err := c.client.LogOut() if err != nil { - return errors.Wrap(err, "Logout error").Error() + return errors.Wrap(err, "Logout error").Error(), false } for _, id := range c.cache.ChatsKeys() { @@ -292,14 +375,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string // cancel auth case "cancelauth": if c.Online() { - return "Not allowed when online, use /logout instead" + return "Not allowed when online, use /logout instead", false } c.cancelAuth() - return "Cancelled" + return "Cancelled", true // set @username case "setusername": if !c.Online() { - return notOnline + return notOnline, false } var username string @@ -311,17 +394,15 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string Username: username, }) if err != nil { - return errors.Wrap(err, "Couldn't set username").Error() + return errors.Wrap(err, "Couldn't set username").Error(), false } // set My Name case "setname": - var firstname string + firstname := args[0] var lastname string - if len(args) > 0 { - firstname = args[0] - } + if firstname == "" { - return "The name should contain at least one character" + return "The name should contain at least one character", false } if len(args) > 1 { lastname = rawCmdArguments(cmdline, 1) @@ -335,7 +416,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } else { c.locks.authorizerWriteLock.Unlock() if !c.Online() { - return notOnline + return notOnline, false } _, err := c.client.SetName(&client.SetNameRequest{ @@ -343,25 +424,25 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string LastName: lastname, }) if err != nil { - return errors.Wrap(err, "Couldn't set name").Error() + return errors.Wrap(err, "Couldn't set name").Error(), false } } // set About case "setbio": if !c.Online() { - return notOnline + return notOnline, false } _, err := c.client.SetBio(&client.SetBioRequest{ Bio: rawCmdArguments(cmdline, 0), }) if err != nil { - return errors.Wrap(err, "Couldn't set bio").Error() + return errors.Wrap(err, "Couldn't set bio").Error(), false } // set password case "setpassword": if !c.Online() { - return notOnline + return notOnline, false } var oldPassword string @@ -376,29 +457,29 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string NewPassword: newPassword, }) if err != nil { - return errors.Wrap(err, "Couldn't set password").Error() + return errors.Wrap(err, "Couldn't set password").Error(), false } case "config": if len(args) > 1 { var msg string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { - return "The server did not allow to enable carbons" + return "The server did not allow to enable carbons", false } value, err := c.Session.Set(args[0], args[1]) if err != nil { - return err.Error() + return err.Error(), false } gateway.DirtySessions = true - return fmt.Sprintf("%s%s set to %s", msg, args[0], value) + return fmt.Sprintf("%s%s set to %s", msg, args[0], value), true } else if len(args) > 0 { value, err := c.Session.Get(args[0]) if err != nil { - return err.Error() + return err.Error(), false } - return fmt.Sprintf("%s is set to %s", args[0], value) + return fmt.Sprintf("%s is set to %s", args[0], value), true } var entries []string @@ -406,15 +487,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string entries = append(entries, fmt.Sprintf("%s is set to %s", key, value)) } - return strings.Join(entries, "\n") + return strings.Join(entries, "\n"), true case "report": - if len(args) < 2 { - return "Not enough arguments" - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error() + return err.Error(), false + } + if contact == nil { + return "Contact not found", false } text := rawCmdArguments(cmdline, 1) @@ -424,9 +504,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string Text: text, }) if err != nil { - return err.Error() + return err.Error(), false } else { - return "Reported" + return "Reported", true } case "add": return c.cmdAdd(args) @@ -437,32 +517,45 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string case "channel": return c.cmdChannel(args, cmdline) case "help": - return helpString(helpTypeTransport) + return c.helpString(CommandTypeTransport, 0), true } - return "" + return "", true } // ProcessChatCommand executes a command sent in a mapped chat -// and returns a response and the status of command support -func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) { +// and returns a response, the status of command support and the execution success result +func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, bool) { if !c.Online() { - return notOnline, true + return notOnline, true, false } cmd, args := parseCommand(cmdline) + command, ok := chatCommands[cmd] + if !ok { + return unknownCommand, false, false + } + if len(args) < command.RequiredArgs { + return notEnoughArguments, true, false + } + + chatType, chatTypeErr := c.GetChatType(chatID) + if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { + return "Not applicable for this chat type", true, false + } + switch cmd { // delete message case "d": if c.me == nil { - return "@me is not initialized", true + return "@me is not initialized", true, false } var limit int32 if len(args) > 0 { limit64, err := strconv.ParseInt(args[0], 10, 32) if err != nil { - return err.Error(), true + return err.Error(), true, false } limit = int32(limit64) } else { @@ -471,7 +564,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) messages, err := c.getLastMessages(chatID, "", c.me.Id, limit) if err != nil { - return err.Error(), true + return err.Error(), true, false } log.Debugf("pre-deletion query: %#v %#v", messages, messages.Messages) @@ -488,28 +581,25 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Revoke: true, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // edit message case "s": if c.me == nil { - return "@me is not initialized", true - } - if len(args) < 1 { - return "Not enough arguments", true + return "@me is not initialized", true, false } messages, err := c.getLastMessages(chatID, "", c.me.Id, 1) if err != nil { - return err.Error(), true + return err.Error(), true, false } if len(messages.Messages) == 0 { - return "No last message", true + return "No last message", true, false } message := messages.Messages[0] if message == nil { - return "Last message is empty", true + return "Last message is empty", true, false } content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) @@ -521,17 +611,13 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) InputMessageContent: content, }) if err != nil { - return "Message editing error", true + return "Message editing error", true, false } } else { - return "Message processing error", true + return "Message processing error", true, false } // send without sound case "silent": - if len(args) < 1 { - return "Not enough arguments", true - } - content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { @@ -543,17 +629,13 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } } else { - return "Message processing error", true + return "Message processing error", true, false } // schedule a message to timestamp or to going online case "schedule": - if len(args) < 2 { - return "Not enough arguments", true - } - var state client.MessageSchedulingState var result string due := args[0] @@ -607,7 +689,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) break } - return "Invalid schedule time specifier", true + return "Invalid schedule time specifier", true, false } } @@ -622,27 +704,23 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } - return "Scheduled to " + result, true + return "Scheduled to " + result, true, true } else { - return "Message processing error", true + return "Message processing error", true, false } // forward a message to chat case "forward": - if len(args) < 2 { - return notEnoughArguments, true - } - messageId, err := strconv.ParseInt(args[0], 10, 64) if err != nil { - return "Cannot parse message ID", true + return "Cannot parse message ID", true, false } targetChatParts := strings.Split(args[1], "@") // full JIDs are supported too targetChatId, err := strconv.ParseInt(targetChatParts[0], 10, 64) if err != nil { - return "Cannot parse target chat ID", true + return "Cannot parse target chat ID", true, false } messages, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{ @@ -651,7 +729,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) MessageIds: []int64{messageId}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } if messages != nil && messages.Messages != nil { for _, message := range messages.Messages { @@ -662,7 +740,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) case "vcard": info, err := c.GetVcardInfo(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } _, link := c.PermastoreFile(info.Photo, true) entries := []string{ @@ -672,39 +750,39 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) keyValueString("Full name", info.Given+" "+info.Family), keyValueString("Phone number", info.Tel), } - return strings.Join(entries, "\n"), true + return strings.Join(entries, "\n"), true, true // add @contact case "add": - return c.cmdAdd(args), true + response, success := c.cmdAdd(args) + return response, true, success // join https://t.me/publichat or @publicchat case "join": - return c.cmdJoin(args), true + response, success := c.cmdJoin(args) + return response, true, success // create new supergroup case "supergroup": - return c.cmdSupergroup(args, cmdline), true + response, success := c.cmdSupergroup(args, cmdline) + return response, true, success // create new channel case "channel": - return c.cmdChannel(args, cmdline), true + response, success := c.cmdChannel(args, cmdline) + return response, true, success // create new secret chat with current user case "secret": _, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{ UserId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // create group chat with current user case "group": - if len(args) < 1 { - return notEnoughArguments, true - } - _, err := c.client.CreateNewBasicGroupChat(&client.CreateNewBasicGroupChatRequest{ UserIds: []int64{chatID}, Title: args[0], }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // blacklists current user case "block": @@ -713,7 +791,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) BlockList: &client.BlockListMain{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // unblacklists current user case "unblock": @@ -722,17 +800,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) BlockList: nil, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // invite @username to current groupchat case "invite": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } _, err = c.client.AddChatMember(&client.AddChatMemberRequest{ @@ -741,7 +818,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ForwardLimit: 100, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // get link to current chat case "link": @@ -749,18 +826,17 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } - return link.InviteLink, true + return link.InviteLink, true, true // kick @username from current group chat case "kick": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -769,21 +845,24 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &client.ChatMemberStatusLeft{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // mute [@username [n hours]] case "mute": if len(args) > 0 { contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } var hours int64 if len(args) > 1 { hours, err = strconv.ParseInt(args[1], 10, 32) if err != nil { - return "Invalid number of hours", true + return "Invalid number of hours", true, false } } @@ -797,11 +876,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } } else { if !c.Session.IgnoreChat(chatID) { - return "Chat is already ignored", true + return "Chat is already ignored", true, false } gateway.DirtySessions = true } @@ -810,7 +889,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if len(args) > 0 { contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -823,30 +905,29 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } } else { if !c.Session.UnignoreChat(chatID) { - return "Chat wasn't ignored", true + return "Chat wasn't ignored", true, false } gateway.DirtySessions = true } // ban @username from current chat [for N hours] case "ban": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } var hours int64 if len(args) > 1 { hours, err = strconv.ParseInt(args[1], 10, 32) if err != nil { - return "Invalid number of hours", true + return "Invalid number of hours", true, false } } @@ -858,17 +939,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // unban @username case "unban": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -877,17 +957,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &client.ChatMemberStatusMember{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // promote @username to admin case "promote": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if contact == nil { + return "Contact not found", true, false } // clone the permissions @@ -906,7 +985,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &status, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // leave current chat case "leave": @@ -914,12 +993,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // leave current chat (for owners) case "leave!": @@ -927,12 +1006,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // set TTL case "ttl": @@ -941,7 +1020,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if len(args) > 0 { ttl, err = strconv.ParseInt(args[0], 10, 32) if err != nil { - return "Invalid TTL", true + return "Invalid TTL", true, false } } _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{ @@ -950,13 +1029,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // close secret chat case "close": chat, _, err := c.GetContactByID(chatID, nil) if err != nil { - return err.Error(), true + return err.Error(), true, false + } + if chat == nil { + return "Chat not found", true, false } chatType := chat.Type.ChatTypeType() @@ -966,12 +1048,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) SecretChatId: chatTypeSecret.SecretChatId, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } } // delete current chat @@ -982,12 +1064,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Revoke: true, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // message search case "search": @@ -1006,7 +1088,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) messages, err := c.getLastMessages(chatID, query, 0, limit) if err != nil { - return err.Error(), true + return err.Error(), true, false } c.sendMessagesReverse(chatID, messages.Messages) @@ -1035,7 +1117,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Limit: limit, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } messages = append(messages, newMessages.Messages...) @@ -1053,126 +1135,97 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) query = args[0] } - members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ - ChatId: chatID, - Limit: 9999, - Query: query, - Filter: &client.ChatMembersFilterMembers{}, - }) + members, err := c.GetChatMembers(chatID, false, query, MembersListMembers) if err != nil { - return err.Error(), true + return err.Error(), true, false } var entries []string - for _, member := range members.Members { - var senderId int64 - switch member.MemberId.MessageSenderType() { - case client.TypeMessageSenderUser: - memberUser, _ := member.MemberId.(*client.MessageSenderUser) - senderId = memberUser.UserId - case client.TypeMessageSenderChat: - memberChat, _ := member.MemberId.(*client.MessageSenderChat) - senderId = memberChat.ChatId - } + for _, member := range members { + senderId := c.GetSenderId(member.MemberId) entries = append(entries, fmt.Sprintf( "%v | role: %v", - c.formatContact(senderId), + c.FormatContact(senderId), member.Status.ChatMemberStatusType(), )) } - return strings.Join(entries, "\n"), true + return strings.Join(entries, "\n"), true, true case "help": - return helpString(helpTypeChat), true + return c.helpString(CommandTypeChat, chatID), true, true default: - return "", false + return "", false, false } - return "", true + return "", true, true } -func (c *Client) cmdAdd(args []string) string { - if len(args) < 1 { - return notEnoughArguments - } - +func (c *Client) cmdAdd(args []string) (string, bool) { chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } if chat == nil { - return "No error, but chat is nil" + return "No error, but chat is nil", false } c.subscribeToID(chat.Id, chat) - return "" + return "", true } -func (c *Client) cmdJoin(args []string) string { - if len(args) < 1 { - return notEnoughArguments - } - +func (c *Client) cmdJoin(args []string) (string, bool) { if strings.HasPrefix(args[0], "@") { chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } if chat == nil { - return "No error, but chat is nil" + return "No error, but chat is nil", false } _, err = c.client.JoinChat(&client.JoinChatRequest{ ChatId: chat.Id, }) if err != nil { - return err.Error() + return err.Error(), false } } else { _, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{ InviteLink: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } } - return "" + return "", true } -func (c *Client) cmdSupergroup(args []string, cmdline string) string { - if len(args) < 1 { - return notEnoughArguments - } - +func (c *Client) cmdSupergroup(args []string, cmdline string) (string, bool) { _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), }) if err != nil { - return err.Error() + return err.Error(), false } - return "" + return "", true } -func (c *Client) cmdChannel(args []string, cmdline string) string { - if len(args) < 1 { - return notEnoughArguments - } - +func (c *Client) cmdChannel(args []string, cmdline string) (string, bool) { _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), IsChannel: true, }) if err != nil { - return err.Error() + return err.Error(), false } - return "" + return "", true } diff --git a/telegram/utils.go b/telegram/utils.go index 4509d1a..d656885 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -46,6 +46,7 @@ type messageStub struct { } var errOffline = errors.New("TDlib instance is offline") +var errOverLimit = errors.New("Over limit") var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") @@ -53,6 +54,28 @@ var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" const messageHeaderSeparator string = " | " // no hrunicode allowed here yet +// ChatType is an enum of chat types, roughly corresponding to TDLib's one but better +type ChatType int + +const ( + ChatTypeUnknown ChatType = iota + ChatTypePrivate + ChatTypeBasicGroup + ChatTypeSupergroup + ChatTypeSecret + ChatTypeChannel +) + +// MembersList is an enum of member list filters +type MembersList int + +const ( + MembersListMembers MembersList = iota + MembersListRestricted + MembersListBanned + MembersListBannedAndAdministrators +) + // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { if !c.Online() { @@ -130,10 +153,10 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli return chat, user, nil } -// IsPM checks if a chat is PM -func (c *Client) IsPM(id int64) (bool, error) { +// GetChatType obtains chat type from its information +func (c *Client) GetChatType(id int64) (ChatType, error) { if !c.Online() || id == 0 { - return false, errOffline + return ChatTypeUnknown, errOffline } var err error @@ -144,14 +167,38 @@ func (c *Client) IsPM(id int64) (bool, error) { ChatId: id, }) if err != nil { - return false, err + return ChatTypeUnknown, err } c.cache.SetChat(id, chat) } chatType := chat.Type.ChatTypeType() - if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { + if chatType == client.TypeChatTypePrivate { + return ChatTypePrivate, nil + } else if chatType == client.TypeChatTypeBasicGroup { + return ChatTypeBasicGroup, nil + } else if chatType == client.TypeChatTypeSupergroup { + supergroup, _ := chat.Type.(*client.ChatTypeSupergroup) + if supergroup.IsChannel { + return ChatTypeChannel, nil + } + return ChatTypeSupergroup, nil + } else if chatType == client.TypeChatTypeSecret { + return ChatTypeSecret, nil + } + + return ChatTypeUnknown, errors.New("Unknown chat type") +} + +// IsPM checks if a chat is PM +func (c *Client) IsPM(id int64) (bool, error) { + typ, err := c.GetChatType(id) + if err != nil { + return false, err + } + + if typ == ChatTypePrivate || typ == ChatTypeSecret { return true, nil } return false, nil @@ -294,7 +341,8 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o return c.sendPresence(newArgs...) } -func (c *Client) formatContact(chatID int64) string { +// FormatContact retrieves a complete "full name (@usernames)" string for display +func (c *Client) FormatContact(chatID int64) string { if chatID == 0 { return "" } @@ -326,23 +374,27 @@ func (c *Client) formatContact(chatID int64) string { return str } -func (c *Client) getSenderId(message *client.Message) (senderId int64) { - if message.SenderId != nil { - switch message.SenderId.MessageSenderType() { - case client.TypeMessageSenderUser: - senderUser, _ := message.SenderId.(*client.MessageSenderUser) - senderId = senderUser.UserId - case client.TypeMessageSenderChat: - senderChat, _ := message.SenderId.(*client.MessageSenderChat) - senderId = senderChat.ChatId - } +func (c *Client) GetSenderId(sender client.MessageSender) (senderId int64) { + switch sender.MessageSenderType() { + case client.TypeMessageSenderUser: + senderUser, _ := sender.(*client.MessageSenderUser) + senderId = senderUser.UserId + case client.TypeMessageSenderChat: + senderChat, _ := sender.(*client.MessageSenderChat) + senderId = senderChat.ChatId } + return +} +func (c *Client) getMessageSenderId(message *client.Message) (senderId int64) { + if message.SenderId != nil { + senderId = c.GetSenderId(message.SenderId) + } return } func (c *Client) formatSender(message *client.Message) string { - return c.formatContact(c.getSenderId(message)) + return c.FormatContact(c.getMessageSenderId(message)) } func (c *Client) messageToStub(message *client.Message, preview bool, text string) *messageStub { @@ -392,7 +444,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten } gatewayReply = &gateway.Reply{ - Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), + Author: fmt.Sprintf("%v@%s", c.getMessageSenderId(replyMsg), gateway.Jid.Full()), Id: replyId, } } else if !noContent { @@ -409,7 +461,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten } tgReply = &messageStub{ - Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.formatContact(replyTo.ChatId), + Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.FormatContact(replyTo.ChatId), Date: replyTo.OriginSendDate, Text: text, } @@ -479,14 +531,14 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string { switch origin.MessageOriginType() { case client.TypeMessageOriginUser: originUser := origin.(*client.MessageOriginUser) - return c.formatContact(originUser.SenderUserId) + return c.FormatContact(originUser.SenderUserId) case client.TypeMessageOriginChat: originChat := origin.(*client.MessageOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } - return c.formatContact(originChat.SenderChatId) + signature + return c.FormatContact(originChat.SenderChatId) + signature case client.TypeMessageOriginHiddenUser: originUser := origin.(*client.MessageOriginHiddenUser) return originUser.SenderName @@ -496,7 +548,7 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string { if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } - return c.formatContact(channel.ChatId) + signature + return c.FormatContact(channel.ChatId) + signature } return "Unknown origin type" } @@ -665,13 +717,13 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 text := "invited " if len(addMembers.MemberUserIds) > 0 { - text += c.formatContact(addMembers.MemberUserIds[0]) + text += c.FormatContact(addMembers.MemberUserIds[0]) } return text case client.TypeMessageChatDeleteMember: deleteMember, _ := content.(*client.MessageChatDeleteMember) - return "kicked " + c.formatContact(deleteMember.UserId) + return "kicked " + c.FormatContact(deleteMember.UserId) case client.TypeMessagePinMessage: pinMessage, _ := content.(*client.MessagePinMessage) return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil) @@ -821,7 +873,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 } case client.TypeMessageChatSetMessageAutoDeleteTime: ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime) - name := c.formatContact(ttl.FromUserId) + name := c.FormatContact(ttl.FromUserId) if name == "" { if ttl.MessageAutoDeleteTime == 0 { return "The self-destruct timer was disabled" @@ -1127,7 +1179,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands - response, isCommand := c.ProcessChatCommand(chatID, text) + response, isCommand, _ := c.ProcessChatCommand(chatID, text) if response != "" { c.returnMessage(returnJid, chatID, response) } @@ -1628,3 +1680,76 @@ func (c *Client) usernamesToString(usernames []string) string { } return strings.Join(atUsernames, ", ") } + +// GetChatMembers retrieves a list of chat members. "Limited" mode works only if there are no more than 20 members at all +func (c *Client) GetChatMembers(chatID int64, limited bool, query string, membersList MembersList) ([]*client.ChatMember, error) { + var filters []client.ChatMembersFilter + switch membersList { + case MembersListMembers: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterMembers{}} + case MembersListRestricted: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterRestricted{}} + case MembersListBanned: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}} + case MembersListBannedAndAdministrators: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}, &client.ChatMembersFilterAdministrators{}} + } + + limit := int32(9999) + if limited { + limit = 20 + + chat, _, err := c.GetContactByID(chatID, nil) + if err != nil { + return nil, err + } else if chat == nil { + return nil, errors.New("Chat not found") + } + + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypeBasicGroup { + basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) + fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ + BasicGroupId: basicGroupType.BasicGroupId, + }) + if err != nil { + return nil, err + } + + if len(fullInfo.Members) > int(limit) { + return nil, errOverLimit + } + + return fullInfo.Members, nil + } else if chatType == client.TypeChatTypeSupergroup { + supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) + fullInfo, err := c.client.GetSupergroupFullInfo(&client.GetSupergroupFullInfoRequest{ + SupergroupId: supergroupType.SupergroupId, + }) + if err != nil { + return nil, err + } + + if fullInfo.MemberCount > limit { + return nil, errOverLimit + } + } else { + return nil, errors.New("Inapplicable chat type") + } + } + + var members []*client.ChatMember + for _, filter := range filters { + chatMembers, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ + ChatId: chatID, + Limit: limit, + Query: query, + Filter: filter, + }) + if err != nil { + return nil, err + } + members = append(members, chatMembers.Members...) + } + return members, nil +} diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 005d17b..e89077d 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -593,7 +593,7 @@ func TestMessageToPrefix8(t *testing.T) { func GetSenderIdEmpty(t *testing.T) { message := client.Message{} - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != 0 { t.Errorf("Wrong sender id: %v", senderId) } @@ -605,7 +605,7 @@ func GetSenderIdUser(t *testing.T) { UserId: 42, }, } - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != 42 { t.Errorf("Wrong sender id: %v", senderId) } @@ -617,7 +617,7 @@ func GetSenderIdChat(t *testing.T) { ChatId: -42, }, } - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != -42 { t.Errorf("Wrong sender id: %v", senderId) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 811cef6..1d77bc4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/pkg/errors" "io" + "sort" "strconv" "strings" @@ -26,6 +27,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 +55,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 +76,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 + } } } @@ -223,7 +230,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } else { toJid, err := stanza.NewJid(msg.To) if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) { - response := session.ProcessTransportCommand(msg.Body, resource) + response, _ := session.ProcessTransportCommand(msg.Body, resource) if response != "" { gateway.SendServiceMessage(msg.From, response, component) } @@ -468,7 +475,22 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } -func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { +func getTelegramChatType(from string, to string) (telegram.ChatType, error) { + toId, ok := toToID(to) + if ok { + bare, _, ok := gateway.SplitJID(from) + if ok { + session, ok := sessions[bare] + if ok { + return session.GetChatType(toId) + } + } + } + + return telegram.ChatTypeUnknown, errors.New("Unknown chat type") +} + +func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -483,13 +505,37 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { disco := answer.DiscoInfo() _, ok := toToID(iq.To) - if ok { - disco.AddIdentity("", "account", "registered") - disco.AddFeatures(stanza.NSMsgChatMarkers) - disco.AddFeatures(stanza.NSMsgReceipts) + if di.Node == "" { + if ok { + disco.AddIdentity("", "account", "registered") + disco.AddFeatures(stanza.NSMsgChatMarkers) + disco.AddFeatures(stanza.NSMsgReceipts) + } else { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + disco.AddFeatures("jabber:iq:register") + } + disco.AddFeatures(NSCommand) } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") - disco.AddFeatures("jabber:iq:register") + chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) + + var cmdType telegram.CommandType + if ok { + cmdType = telegram.CommandTypeChat + } else { + cmdType = telegram.CommandTypeTransport + } + + for name, command := range telegram.GetCommands(cmdType) { + if di.Node == name { + if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { + break + } + answer.Payload = di + di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") + di.AddFeatures(NSCommand, "jabber:x:data") + break + } + } } answer.Payload = disco @@ -504,7 +550,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 +563,32 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { return } - answer.Payload = answer.DiscoItems() + log.Debugf("discoItems: %#v", di) + + _, ok := toToID(iq.To) + if di.Node == NSCommand { + answer.Payload = di + + chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) + + var cmdType telegram.CommandType + if ok { + cmdType = telegram.CommandTypeChat + } else { + cmdType = telegram.CommandTypeTransport + } + + commands := telegram.GetCommands(cmdType) + for _, name := range telegram.SortedCommandKeys(commands) { + command := commands[name] + if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { + continue + } + di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) + } + } else { + answer.Payload = answer.DiscoItems() + } component, ok := s.(*xmpp.Component) if !ok { @@ -647,6 +718,179 @@ 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) + + bare, resource, ok := gateway.SplitJID(iq.From) + if !ok { + return + } + toId, toOk := toToID(iq.To) + + var cmdString string + var cmdType telegram.CommandType + form, formOk := command.CommandElement.(*stanza.Form) + if toOk { + cmdType = telegram.CommandTypeChat + } else { + cmdType = telegram.CommandTypeTransport + } + if formOk { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji + } + return false + }) + + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) + } + } + + cmdString = cmd.String() + } else { + if command.Action == "" || command.Action == stanza.CommandActionExecute { + cmd, ok := telegram.GetCommand(cmdType, command.Node) + if ok && len(cmd.Arguments) > 0 { + var fields []*stanza.Field + for i, arg := range cmd.Arguments { + var required *string + if i < cmd.RequiredArgs { + dummyString := "" + required = &dummyString + } + + var fieldType string + var options []stanza.Option + if toOk && i == 0 { + switch command.Node { + case "mute", "kick", "ban", "promote", "unmute", "unban": + session, ok := sessions[bare] + if ok { + var membersList telegram.MembersList + switch command.Node { + case "unmute": + membersList = telegram.MembersListRestricted + case "unban": + membersList = telegram.MembersListBannedAndAdministrators + } + members, err := session.GetChatMembers(toId, true, "", membersList) + if err == nil { + fieldType = stanza.FieldTypeListSingle + for _, member := range members { + senderId := session.GetSenderId(member.MemberId) + options = append(options, stanza.Option{ + Label: session.FormatContact(senderId), + ValuesList: []string{strconv.FormatInt(senderId, 10)}, + }) + } + } + } + } + } + + field := stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + Required: required, + Type: fieldType, + Options: options, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) + } + form := stanza.Form{ + Type: stanza.FormTypeForm, + Title: command.Node, + Instructions: []string{cmd.Description}, + Fields: fields, + } + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusExecuting, + CommandElement: &form, + } + log.Debugf("form: %#v", form) + } else { + cmdString = "/" + command.Node + } + } else if command.Action == stanza.CommandActionCancel { + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCancelled, + } + } + } + + if cmdString != "" { + session, ok := sessions[bare] + if !ok { + return + } + + var response string + var success bool + if toOk { + response, _, success = session.ProcessChatCommand(toId, cmdString) + } else { + response, success = session.ProcessTransportCommand(cmdString, resource) + } + + var noteType string + if success { + noteType = stanza.CommandNoteTypeInfo + } else { + noteType = stanza.CommandNoteTypeErr + } + + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElement: &stanza.Note{ + Text: response, + Type: noteType, + }, + } + + } + + log.Debugf("command response: %#v", answer.Payload) +} + func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Type = stanza.IQTypeError answer.Payload = *payload |