Compare commits

...

19 commits

Author SHA1 Message Date
Bohdan Horbeshko bd5f41a76b Fix missing go.sum entry errors in staging.Dockerfile 2024-05-12 11:05:18 -04:00
Bohdan Horbeshko e94a646e19 Upgrade to go-xmpp version with multiple command elements support 2024-05-12 11:03:48 -04:00
Bohdan Horbeshko 249c942fc2 Allow empty form for mute/unmute commands 2024-05-10 19:53:16 -04:00
Bohdan Horbeshko 9aec929e71 Merge branch 'master' into adhoc 2024-05-10 19:24:15 -04:00
Bohdan Horbeshko 4eae44b9a2 Merge branch 'master' into adhoc 2024-05-09 19:32:57 -04:00
Bohdan Horbeshko 43f9603b88 Merge branch 'master' into adhoc 2024-04-28 07:04:42 -04:00
Bohdan Horbeshko 154b59de44 Show command execution success status 2024-02-18 04:36:23 -05:00
Bohdan Horbeshko 5dd60450c2 Fix crashes in commands due to not found contacts 2024-02-18 02:48:57 -05:00
Bohdan Horbeshko 0b1cbda1cc Show member dropdowns in chat administration forms 2024-02-18 02:48:02 -05:00
Bohdan Horbeshko 9b5fee8826 Filter available commands by chat type 2024-02-15 04:40:57 -05:00
Bohdan Horbeshko dc6f99dc3c Stable command order in help and Ad-Hoc list 2024-02-10 16:27:08 -05:00
Bohdan Horbeshko 772246ee4b Mark required fields in forms 2024-02-10 15:22:24 -05:00
Bohdan Horbeshko b0c5302c82 Ad-Hoc support for chat commands 2024-02-10 13:46:02 -05:00
Bohdan Horbeshko a0180eff75 Handle command cancelling 2024-02-03 10:38:00 -05:00
Bohdan Horbeshko e7d5a2a266 Accept forms with arbitrary action 2024-02-03 10:33:37 -05:00
Bohdan Horbeshko 21dc5fa6c6 Form support for transport Ad-Hoc commands with arguments 2024-02-03 04:24:22 -05:00
Bohdan Horbeshko e3a5191905 Declaratively specify optional and required command arguments 2024-02-01 12:14:06 -05:00
Bohdan Horbeshko eace19eef7 Merge branch 'master' into adhoc 2024-01-31 09:29:45 -05:00
Bohdan Horbeshko fd0d7411c2 Basic Ad-Hoc support for transport commands 2024-01-30 21:38:46 -05:00
9 changed files with 756 additions and 313 deletions

View file

@ -2,7 +2,7 @@
COMMIT := $(shell git rev-parse --short HEAD) COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551"
VERSION := "v1.9.5" VERSION := "v1.10.0-dev"
MAKEOPTS := "-j4" MAKEOPTS := "-j4"
all: all:

5
go.mod
View file

@ -4,6 +4,7 @@ go 1.19
require ( require (
github.com/dgraph-io/badger/v4 v4.1.0 github.com/dgraph-io/badger/v4 v4.1.0
github.com/google/uuid v1.1.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
@ -23,7 +24,6 @@ require (
github.com/golang/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.3 // indirect github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/klauspost/compress v1.12.3 // indirect github.com/klauspost/compress v1.12.3 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
go.opencensus.io v0.22.5 // indirect go.opencensus.io v0.22.5 // indirect
@ -33,5 +33,6 @@ require (
nhooyr.io/websocket v1.6.5 // indirect 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-20240512132113-6725c3862314
replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061

4
go.sum
View file

@ -7,6 +7,10 @@ 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-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 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-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=
dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 h1:29/NjOGOUDceO73Hk4Nj4uVa1je8MULJlsDSvKxSN/k=
dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

View file

@ -26,8 +26,8 @@ WORKDIR /src
RUN go env -w GOCACHE=/go-cache RUN go env -w GOCACHE=/go-cache
RUN go env -w GOMODCACHE=/gomod-cache RUN go env -w GOMODCACHE=/gomod-cache
RUN --mount=type=cache,target=/gomod-cache \ RUN --mount=type=cache,target=/gomod-cache \
--mount=type=bind,source=./,target=/src \ --mount=type=bind,source=./,target=/src,rw \
go mod download /bin/bash -c 'go mod tidy; go get -t'
FROM cache AS build FROM cache AS build
ARG MAKEOPTS ARG MAKEOPTS

View file

@ -16,7 +16,7 @@ import (
goxmpp "gosrc.io/xmpp" goxmpp "gosrc.io/xmpp"
) )
var version string = "1.9.5" var version string = "1.10.0-dev"
var commit string var commit string
var sm *goxmpp.StreamManager var sm *goxmpp.StreamManager

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,7 @@ type messageStub struct {
} }
var errOffline = errors.New("TDlib instance is offline") var errOffline = errors.New("TDlib instance is offline")
var errOverLimit = errors.New("Over limit")
var spaceRegex = regexp.MustCompile(`\s+`) var spaceRegex = regexp.MustCompile(`\s+`)
var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") 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 newlineChar string = "\n"
const messageHeaderSeparator string = " | " // no hrunicode allowed here yet 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 // GetContactByUsername resolves username to user id retrieves user and chat information
func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) {
if !c.Online() { if !c.Online() {
@ -130,10 +153,10 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli
return chat, user, nil return chat, user, nil
} }
// IsPM checks if a chat is PM // GetChatType obtains chat type from its information
func (c *Client) IsPM(id int64) (bool, error) { func (c *Client) GetChatType(id int64) (ChatType, error) {
if !c.Online() || id == 0 { if !c.Online() || id == 0 {
return false, errOffline return ChatTypeUnknown, errOffline
} }
var err error var err error
@ -144,14 +167,38 @@ func (c *Client) IsPM(id int64) (bool, error) {
ChatId: id, ChatId: id,
}) })
if err != nil { if err != nil {
return false, err return ChatTypeUnknown, err
} }
c.cache.SetChat(id, chat) c.cache.SetChat(id, chat)
} }
chatType := chat.Type.ChatTypeType() 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 true, nil
} }
return false, nil return false, nil
@ -294,7 +341,8 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
return c.sendPresence(newArgs...) 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 { if chatID == 0 {
return "" return ""
} }
@ -326,23 +374,27 @@ func (c *Client) formatContact(chatID int64) string {
return str return str
} }
func (c *Client) getSenderId(message *client.Message) (senderId int64) { func (c *Client) GetSenderId(sender client.MessageSender) (senderId int64) {
if message.SenderId != nil { switch sender.MessageSenderType() {
switch message.SenderId.MessageSenderType() { case client.TypeMessageSenderUser:
case client.TypeMessageSenderUser: senderUser, _ := sender.(*client.MessageSenderUser)
senderUser, _ := message.SenderId.(*client.MessageSenderUser) senderId = senderUser.UserId
senderId = senderUser.UserId case client.TypeMessageSenderChat:
case client.TypeMessageSenderChat: senderChat, _ := sender.(*client.MessageSenderChat)
senderChat, _ := message.SenderId.(*client.MessageSenderChat) senderId = senderChat.ChatId
senderId = senderChat.ChatId
}
} }
return
}
func (c *Client) getMessageSenderId(message *client.Message) (senderId int64) {
if message.SenderId != nil {
senderId = c.GetSenderId(message.SenderId)
}
return return
} }
func (c *Client) formatSender(message *client.Message) string { 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 { 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{ 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, Id: replyId,
} }
} else if !noContent { } else if !noContent {
@ -409,7 +461,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten
} }
tgReply = &messageStub{ tgReply = &messageStub{
Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.formatContact(replyTo.ChatId), Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.FormatContact(replyTo.ChatId),
Date: replyTo.OriginSendDate, Date: replyTo.OriginSendDate,
Text: text, Text: text,
} }
@ -479,14 +531,14 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string {
switch origin.MessageOriginType() { switch origin.MessageOriginType() {
case client.TypeMessageOriginUser: case client.TypeMessageOriginUser:
originUser := origin.(*client.MessageOriginUser) originUser := origin.(*client.MessageOriginUser)
return c.formatContact(originUser.SenderUserId) return c.FormatContact(originUser.SenderUserId)
case client.TypeMessageOriginChat: case client.TypeMessageOriginChat:
originChat := origin.(*client.MessageOriginChat) originChat := origin.(*client.MessageOriginChat)
var signature string var signature string
if originChat.AuthorSignature != "" { if originChat.AuthorSignature != "" {
signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature)
} }
return c.formatContact(originChat.SenderChatId) + signature return c.FormatContact(originChat.SenderChatId) + signature
case client.TypeMessageOriginHiddenUser: case client.TypeMessageOriginHiddenUser:
originUser := origin.(*client.MessageOriginHiddenUser) originUser := origin.(*client.MessageOriginHiddenUser)
return originUser.SenderName return originUser.SenderName
@ -496,7 +548,7 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string {
if channel.AuthorSignature != "" { if channel.AuthorSignature != "" {
signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) signature = fmt.Sprintf(" (%s)", channel.AuthorSignature)
} }
return c.formatContact(channel.ChatId) + signature return c.FormatContact(channel.ChatId) + signature
} }
return "Unknown origin type" return "Unknown origin type"
} }
@ -665,13 +717,13 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
text := "invited " text := "invited "
if len(addMembers.MemberUserIds) > 0 { if len(addMembers.MemberUserIds) > 0 {
text += c.formatContact(addMembers.MemberUserIds[0]) text += c.FormatContact(addMembers.MemberUserIds[0])
} }
return text return text
case client.TypeMessageChatDeleteMember: case client.TypeMessageChatDeleteMember:
deleteMember, _ := content.(*client.MessageChatDeleteMember) deleteMember, _ := content.(*client.MessageChatDeleteMember)
return "kicked " + c.formatContact(deleteMember.UserId) return "kicked " + c.FormatContact(deleteMember.UserId)
case client.TypeMessagePinMessage: case client.TypeMessagePinMessage:
pinMessage, _ := content.(*client.MessagePinMessage) pinMessage, _ := content.(*client.MessagePinMessage)
return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil) 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: case client.TypeMessageChatSetMessageAutoDeleteTime:
ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime) ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime)
name := c.formatContact(ttl.FromUserId) name := c.FormatContact(ttl.FromUserId)
if name == "" { if name == "" {
if ttl.MessageAutoDeleteTime == 0 { if ttl.MessageAutoDeleteTime == 0 {
return "The self-destruct timer was disabled" 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, "!")) { if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) {
// try to execute commands // try to execute commands
response, isCommand := c.ProcessChatCommand(chatID, text) response, isCommand, _ := c.ProcessChatCommand(chatID, text)
if response != "" { if response != "" {
c.returnMessage(returnJid, chatID, response) c.returnMessage(returnJid, chatID, response)
} }
@ -1628,3 +1680,76 @@ func (c *Client) usernamesToString(usernames []string) string {
} }
return strings.Join(atUsernames, ", ") 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
}

View file

@ -593,7 +593,7 @@ func TestMessageToPrefix8(t *testing.T) {
func GetSenderIdEmpty(t *testing.T) { func GetSenderIdEmpty(t *testing.T) {
message := client.Message{} message := client.Message{}
senderId := (&Client{}).getSenderId(&message) senderId := (&Client{}).getMessageSenderId(&message)
if senderId != 0 { if senderId != 0 {
t.Errorf("Wrong sender id: %v", senderId) t.Errorf("Wrong sender id: %v", senderId)
} }
@ -605,7 +605,7 @@ func GetSenderIdUser(t *testing.T) {
UserId: 42, UserId: 42,
}, },
} }
senderId := (&Client{}).getSenderId(&message) senderId := (&Client{}).getMessageSenderId(&message)
if senderId != 42 { if senderId != 42 {
t.Errorf("Wrong sender id: %v", senderId) t.Errorf("Wrong sender id: %v", senderId)
} }
@ -617,7 +617,7 @@ func GetSenderIdChat(t *testing.T) {
ChatId: -42, ChatId: -42,
}, },
} }
senderId := (&Client{}).getSenderId(&message) senderId := (&Client{}).getMessageSenderId(&message)
if senderId != -42 { if senderId != -42 {
t.Errorf("Wrong sender id: %v", senderId) t.Errorf("Wrong sender id: %v", senderId)
} }

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"io" "io"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -26,6 +27,7 @@ const (
TypeVCard4 TypeVCard4
) )
const NodeVCard4 string = "urn:xmpp:vcard4" const NodeVCard4 string = "urn:xmpp:vcard4"
const NSCommand string = "http://jabber.org/protocol/commands"
func logPacketType(p stanza.Packet) { func logPacketType(p stanza.Packet) {
log.Warnf("Ignoring packet: %T\n", p) log.Warnf("Ignoring packet: %T\n", p)
@ -53,14 +55,14 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
return return
} }
} }
_, ok = iq.Payload.(*stanza.DiscoInfo) discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
if ok { if ok {
go handleGetDiscoInfo(s, iq) go handleGetDiscoInfo(s, iq, discoInfo)
return return
} }
_, ok = iq.Payload.(*stanza.DiscoItems) discoItems, ok := iq.Payload.(*stanza.DiscoItems)
if ok { if ok {
go handleGetDiscoItems(s, iq) go handleGetDiscoItems(s, iq, discoItems)
return return
} }
_, ok = iq.Payload.(*extensions.QueryRegister) _, ok = iq.Payload.(*extensions.QueryRegister)
@ -74,6 +76,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleSetQueryRegister(s, iq, query) go handleSetQueryRegister(s, iq, query)
return 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 { } else {
toJid, err := stanza.NewJid(msg.To) toJid, err := stanza.NewJid(msg.To)
if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) { 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 != "" { if response != "" {
gateway.SendServiceMessage(msg.From, response, component) gateway.SendServiceMessage(msg.From, response, component)
} }
@ -468,7 +475,22 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
_ = gateway.ResumableSend(component, &answer) _ = 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{ answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult, Type: stanza.IQTypeResult,
From: iq.To, From: iq.To,
@ -483,13 +505,37 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
disco := answer.DiscoInfo() disco := answer.DiscoInfo()
_, ok := toToID(iq.To) _, ok := toToID(iq.To)
if ok { if di.Node == "" {
disco.AddIdentity("", "account", "registered") if ok {
disco.AddFeatures(stanza.NSMsgChatMarkers) disco.AddIdentity("", "account", "registered")
disco.AddFeatures(stanza.NSMsgReceipts) disco.AddFeatures(stanza.NSMsgChatMarkers)
disco.AddFeatures(stanza.NSMsgReceipts)
} else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
}
disco.AddFeatures(NSCommand)
} else { } else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram") chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
disco.AddFeatures("jabber:iq:register")
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 answer.Payload = disco
@ -504,7 +550,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
_ = gateway.ResumableSend(component, answer) _ = 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{ answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult, Type: stanza.IQTypeResult,
From: iq.To, From: iq.To,
@ -517,7 +563,32 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) {
return 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) component, ok := s.(*xmpp.Component)
if !ok { if !ok {
@ -647,6 +718,195 @@ 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
var form *stanza.Form
for _, ce := range command.CommandElements {
fo, formOk := ce.(*stanza.Form)
if formOk {
form = fo
break
}
}
if toOk {
cmdType = telegram.CommandTypeChat
} else {
cmdType = telegram.CommandTypeTransport
}
if form != nil {
// 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
switch command.Node {
// allow empty form
case "mute", "unmute":
options = append(options, stanza.Option{
ValuesList: []string{""},
})
}
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,
CommandElements: []stanza.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,
CommandElements: []stanza.CommandElement{
&stanza.Note{
Text: response,
Type: noteType,
},
},
}
}
log.Debugf("command response: %#v", answer.Payload)
}
func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) {
answer.Type = stanza.IQTypeError answer.Type = stanza.IQTypeError
answer.Payload = *payload answer.Payload = *payload