aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--telegabber.go2
-rw-r--r--telegram/commands.go579
-rw-r--r--telegram/utils.go177
-rw-r--r--telegram/utils_test.go6
-rw-r--r--xmpp/handlers.go272
8 files changed, 733 insertions, 309 deletions
diff --git a/Makefile b/Makefile
index f8d5b73..07d25e1 100644
--- a/Makefile
+++ b/Makefile
@@ -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:
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/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", &notForGroups},
+ "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", &notForGroups},
+ "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", &notForGroups},
+ "unblock": command{0, []string{}, "unblacklist current user", &notForGroups},
+ "invite": command{1, []string{"id or @username"}, "add user to current chat", &notForPM},
+ "link": command{0, []string{}, "get invite link for current chat", &notForPM},
+ "kick": command{1, []string{"id or @username"}, "remove user from current chat", &notForPM},
+ "mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", &notForPMAndBasic},
+ "unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", &notForPMAndBasic},
+ "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", &notForPM},
+ "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", &notForPM},
+ "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", &notForPM},
+ "leave": command{0, []string{}, "leave current chat", &notForPM},
+ "leave!": command{0, []string{}, "leave current chat (for owners)", &notForPM},
+ "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