Compare commits

...

25 Commits
master ... muc

Author SHA1 Message Date
Bohdan Horbeshko 4972cb6d5e Reject MUC nickname change attempts
7 months ago
Bohdan Horbeshko 1e7e761c6c Reflect name change of Telegram user in all MUCs
7 months ago
Bohdan Horbeshko b8a57c06b6 Handle MUC PM attempts
7 months ago
Bohdan Horbeshko 02578440cd Detect the "Have no write access to the chat" error from Telegram
7 months ago
Bohdan Horbeshko 47fa7bca49 Return outgoing message errors as message error stanzas (only in groupchats yet)
7 months ago
Bohdan Horbeshko a0803123b2 Advertise muc#stable_id feature
7 months ago
Bohdan Horbeshko b70bb53c6d Display outgoing MUC messages
7 months ago
Bohdan Horbeshko 41503c7fd4 Return registration-required instead of not-authorized
7 months ago
Bohdan Horbeshko cdaaa75c96 Send last pinned message as subject on MUC join
7 months ago
Bohdan Horbeshko b68c07025d Add MUC history limit (maxstanzas only)
7 months ago
Bohdan Horbeshko e8bde73164 Original sender JID in MUCs (why?)
7 months ago
Bohdan Horbeshko e77caf2c42 Send recent history on MUC join
7 months ago
Bohdan Horbeshko c1887e5a1e Fix returning MUC join errors
7 months ago
Bohdan Horbeshko 93abbe834e Send real JID for room occupants
7 months ago
Bohdan Horbeshko 6c65ef9988 Send the own MUC member the last with status codes 110/210 according to the spec
7 months ago
Bohdan Horbeshko 4249a8bf41 Suppress nickname presences for MUCs better
7 months ago
Bohdan Horbeshko f99f4f6acc Send memberlist on MUC join, suppress PM statuses for MUC JIDs
7 months ago
Bohdan Horbeshko 776993894a Merge hotfix: remove redundant "registered" identity
7 months ago
Bohdan Horbeshko 9dbd487dae Merge branch 'master' into muc
7 months ago
Bohdan Horbeshko 7eaf28ad7c Advertise gateway first, MUC next
2 years ago
Bohdan Horbeshko 63521b8f90 Extended room disco info
2 years ago
Bohdan Horbeshko 7ef32096af Basic room disco info
2 years ago
Bohdan Horbeshko 63f12202d0 Refactoring: merge handleGetDiscoInfo/handleGetDiscoItems back into one function
2 years ago
Bohdan Horbeshko 6abb7ff9c2 Respond to disco with conference identity and groups list
2 years ago
Bohdan Horbeshko afa21e10be Add a muc option (useless yet)
2 years ago

@ -2,7 +2,7 @@
COMMIT := $(shell git rev-parse --short HEAD) COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1"
VERSION := "v1.8.2" VERSION := "v2.0.0-dev"
MAKEOPTS := "-j4" MAKEOPTS := "-j4"
all: all:

@ -33,5 +33,5 @@ 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-20220708184440-35d9cd68e55f
replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615

@ -3,6 +3,10 @@ dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 h1:RRUZJSro
dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/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-20220708184440-35d9cd68e55f h1:aT50UsPH1dLje9CCAquRRhr7I9ZvL3kQU6WIWTe8PZ0=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk=
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=

@ -39,6 +39,7 @@ type Session struct {
KeepOnline bool `yaml:":keeponline"` KeepOnline bool `yaml:":keeponline"`
RawMessages bool `yaml:":rawmessages"` RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"` AsciiArrows bool `yaml:":asciiarrows"`
MUC bool `yaml:":muc"`
OOBMode bool `yaml:":oobmode"` OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"` Carbons bool `yaml:":carbons"`
HideIds bool `yaml:":hideids"` HideIds bool `yaml:":hideids"`
@ -49,6 +50,7 @@ var configKeys = []string{
"keeponline", "keeponline",
"rawmessages", "rawmessages",
"asciiarrows", "asciiarrows",
"muc",
"oobmode", "oobmode",
"carbons", "carbons",
"hideids", "hideids",
@ -124,6 +126,8 @@ func (s *Session) Get(key string) (string, error) {
return fromBool(s.RawMessages), nil return fromBool(s.RawMessages), nil
case "asciiarrows": case "asciiarrows":
return fromBool(s.AsciiArrows), nil return fromBool(s.AsciiArrows), nil
case "muc":
return fromBool(s.MUC), nil
case "oobmode": case "oobmode":
return fromBool(s.OOBMode), nil return fromBool(s.OOBMode), nil
case "carbons": case "carbons":
@ -173,6 +177,13 @@ func (s *Session) Set(key string, value string) (string, error) {
} }
s.AsciiArrows = b s.AsciiArrows = b
return value, nil return value, nil
case "muc":
b, err := toBool(value)
if err != nil {
return "", err
}
s.MUC = b
return value, nil
case "oobmode": case "oobmode":
b, err := toBool(value) b, err := toBool(value)
if err != nil { if err != nil {

@ -47,12 +47,14 @@ func TestSessionToMap(t *testing.T) {
session := Session{ session := Session{
Timezone: "klsf", Timezone: "klsf",
RawMessages: true, RawMessages: true,
MUC: true,
OOBMode: true, OOBMode: true,
} }
m := session.ToMap() m := session.ToMap()
sample := map[string]string{ sample := map[string]string{
"timezone": "klsf", "timezone": "klsf",
"keeponline": "false", "keeponline": "false",
"muc": "true",
"rawmessages": "true", "rawmessages": "true",
"asciiarrows": "false", "asciiarrows": "false",
"oobmode": "true", "oobmode": "true",

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

@ -41,6 +41,25 @@ type DelayedStatus struct {
TimestampExpired int64 TimestampExpired int64
} }
// MUCState holds MUC metadata
type MUCState struct {
Resources map[string]bool
Members map[int64]*MUCMember
}
// MUCMember represents a MUC member
type MUCMember struct {
Nickname string
Affiliation string
}
func NewMUCState() *MUCState {
return &MUCState{
Resources: make(map[string]bool),
Members: make(map[int64]*MUCMember),
}
}
// Client stores the metadata for lazily invoked TDlib instance // Client stores the metadata for lazily invoked TDlib instance
type Client struct { type Client struct {
client *client.Client client *client.Client
@ -64,6 +83,8 @@ type Client struct {
lastMsgHashes map[int64]uint64 lastMsgHashes map[int64]uint64
msgHashSeed maphash.Seed msgHashSeed maphash.Seed
mucCache map[int64]*MUCState
locks clientLocks locks clientLocks
SendMessageLock sync.Mutex SendMessageLock sync.Mutex
} }
@ -73,6 +94,7 @@ type clientLocks struct {
chatMessageLocks map[int64]*sync.Mutex chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex resourcesLock sync.Mutex
outboxLock sync.Mutex outboxLock sync.Mutex
mucCacheLock sync.Mutex
lastMsgHashesLock sync.Mutex lastMsgHashesLock sync.Mutex
authorizerReadLock sync.Mutex authorizerReadLock sync.Mutex
@ -133,6 +155,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
Session: session, Session: session,
resources: make(map[string]bool), resources: make(map[string]bool),
outbox: make(map[string]string), outbox: make(map[string]string),
mucCache: make(map[int64]*MUCState),
content: &conf.Content, content: &conf.Content,
cache: cache.NewCache(), cache: cache.NewCache(),
options: options, options: options,

@ -193,23 +193,6 @@ func (c *Client) unsubscribe(chatID int64) error {
) )
} }
func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
reply, _ := c.getMessageReply(message)
gateway.SendMessage(
c.jid,
strconv.FormatInt(chatID, 10),
c.formatMessage(0, 0, false, message),
strconv.FormatInt(message.Id, 10),
c.xmpp,
reply,
false,
)
}
}
func (c *Client) usernameOrIDToID(username string) (int64, error) { func (c *Client) usernameOrIDToID(username string) (int64, error) {
userID, err := strconv.ParseInt(username, 10, 64) userID, err := strconv.ParseInt(username, 10, 64)
// couldn't parse the id, try to lookup as a username // couldn't parse the id, try to lookup as a username
@ -1005,7 +988,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return err.Error(), true return err.Error(), true
} }
c.sendMessagesReverse(chatID, messages.Messages) c.sendMessagesReverse(chatID, messages.Messages, true, "")
// get latest entries from history // get latest entries from history
case "history": case "history":
var limit int32 = 10 var limit int32 = 10
@ -1016,32 +999,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
} }
} }
var newMessages *client.Messages messages, err := c.getNLastMessages(chatID, limit)
var messages []*client.Message if err != nil {
var err error return err.Error(), true
var fromId int64
for _ = range make([]struct{}, limit) { // safety limit
if len(messages) > 0 {
fromId = messages[len(messages)-1].Id
}
newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: fromId,
Limit: limit,
})
if err != nil {
return err.Error(), true
}
messages = append(messages, newMessages.Messages...)
if len(newMessages.Messages) == 0 || len(messages) >= int(limit) {
break
}
} }
c.sendMessagesReverse(chatID, messages, true, "")
c.sendMessagesReverse(chatID, messages)
// chat members // chat members
case "members": case "members":
var query string var query string

@ -153,6 +153,13 @@ func (c *Client) updateHandler() {
// new user discovered // new user discovered
func (c *Client) updateUser(update *client.UpdateUser) { func (c *Client) updateUser(update *client.UpdateUser) {
// check if MUC nicknames should be updated
cacheUser, ok := c.cache.GetUser(update.User.Id)
if ok && (cacheUser.FirstName != update.User.FirstName || cacheUser.LastName != update.User.LastName) {
newNickname := c.GetMUCNickname(update.User.Id)
c.updateMUCsNickname(update.User.Id, newNickname)
}
c.cache.SetUser(update.User.Id, update.User) c.cache.SetUser(update.User.Id, update.User)
show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id) show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id)
go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType)) go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType))
@ -265,7 +272,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
markupFunction, markupFunction,
)) ))
for _, jid := range jids { for _, jid := range jids {
gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false) gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, 0, false, false, "")
} }
} }
} }
@ -315,10 +322,14 @@ func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed)
// chat title changed // chat title changed
func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if c.Session.MUC && c.IsGroup(chat) {
return
}
gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp)
// set also the status (for group chats only) // set also the status (for group chats only)
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if user == nil { if user == nil {
c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true))
} }

@ -44,6 +44,12 @@ var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n")
const newlineChar string = "\n" const newlineChar string = "\n"
const messageHeaderSeparator string = " | " const messageHeaderSeparator string = " | "
const (
ChatTypeOther byte = iota
ChatTypePM
ChatTypeGroup
)
// 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() {
@ -121,10 +127,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 checks if a chat is PM or group
func (c *Client) IsPM(id int64) (bool, error) { func (c *Client) GetChatType(id int64) (byte, error) {
if !c.Online() || id == 0 { if !c.Online() || id == 0 {
return false, errOffline return ChatTypeOther, errOffline
} }
var err error var err error
@ -135,7 +141,7 @@ func (c *Client) IsPM(id int64) (bool, error) {
ChatId: id, ChatId: id,
}) })
if err != nil { if err != nil {
return false, err return ChatTypeOther, err
} }
c.cache.SetChat(id, chat) c.cache.SetChat(id, chat)
@ -143,9 +149,12 @@ func (c *Client) IsPM(id int64) (bool, error) {
chatType := chat.Type.ChatTypeType() chatType := chat.Type.ChatTypeType()
if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret {
return true, nil return ChatTypePM, nil
}
if c.IsGroup(chat) {
return ChatTypeGroup, nil
} }
return false, nil return ChatTypeOther, nil
} }
func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) {
@ -217,6 +226,10 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
return err return err
} }
if chat != nil && c.Session.MUC && c.IsGroup(chat) {
return nil
}
var photo string var photo string
if chat != nil && chat.Photo != nil { if chat != nil && chat.Photo != nil {
file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) file, path, err := c.ForceOpenFile(chat.Photo.Small, 1)
@ -290,6 +303,214 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
) )
} }
// JoinMUC saves MUC join fact and sends initialization data
func (c *Client) JoinMUC(chatId int64, resource string, limit int32) {
// save the nickname in this MUC, also as a marker of join
c.locks.mucCacheLock.Lock()
mucState, ok := c.mucCache[chatId]
if !ok || mucState == nil {
mucState = NewMUCState()
c.mucCache[chatId] = mucState
}
_, ok = mucState.Resources[resource]
if ok {
// already joined, initializing anyway
} else {
mucState.Resources[resource] = true
}
c.locks.mucCacheLock.Unlock()
c.sendMUCStatuses(chatId)
messages, err := c.getNLastMessages(chatId, limit)
if err == nil {
c.sendMessagesReverse(chatId, messages, false, c.jid+"/"+resource)
}
c.sendMUCSubject(chatId, resource)
}
func (c *Client) getFullName(user *client.User) string {
fullName := user.FirstName
if user.LastName != "" {
fullName = fullName + " " + user.LastName
}
return fullName
}
func (c *Client) sendMUCStatuses(chatID int64) {
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
mucState, ok := c.mucCache[chatID]
if !ok || mucState == nil {
mucState = NewMUCState()
c.mucCache[chatID] = mucState
}
sChatId := strconv.FormatInt(chatID, 10)
myNickname := "me"
if c.me != nil {
myNickname = c.getFullName(c.me)
}
myAffiliation := "member"
members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{
ChatId: chatID,
Limit: 200,
Filter: &client.ChatMembersFilterMembers{},
})
if err == nil {
gatewayJidSuffix := "@" + gateway.Jid.Full()
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
}
nickname := c.GetMUCNickname(senderId)
affiliation := c.memberStatusToAffiliation(member.Status)
mucState.Members[senderId] = &MUCMember{
Nickname: nickname,
Affiliation: affiliation,
}
if c.me != nil && senderId == c.me.Id {
myNickname = nickname
myAffiliation = affiliation
continue
}
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(sChatId),
gateway.SPResource(nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCJid(strconv.FormatInt(senderId, 10) + gatewayJidSuffix),
)
}
}
// according to the spec, own member entry should be sent the last
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(sChatId),
gateway.SPResource(myNickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(myAffiliation),
gateway.SPMUCStatusCodes([]uint16{100, 110, 210}),
)
}
func (c *Client) sendMUCSubject(chatID int64, resource string) {
pin, err := c.client.GetChatPinnedMessage(&client.GetChatPinnedMessageRequest{
ChatId: chatID,
})
mucJid := strconv.FormatInt(chatID, 10) + "@" + gateway.Jid.Bare()
toJid := c.jid + "/" + resource
if err == nil {
gateway.SendSubjectMessage(
toJid,
mucJid + "/" + c.GetMUCNickname(c.GetSenderId(pin)),
c.messageToText(pin, false),
strconv.FormatInt(pin.Id, 10),
c.xmpp,
int64(pin.Date),
)
} else {
gateway.SendSubjectMessage(toJid, mucJid, "", "", c.xmpp, 0)
}
}
// GetMUCNickname generates a unique nickname for a MUC member
func (c *Client) GetMUCNickname(chatID int64) string {
return c.formatContact(chatID)
}
func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
for mucId, state := range c.mucCache {
oldMember, ok := state.Members[memberID]
if ok {
state.Members[memberID] = &MUCMember{
Nickname: newNickname,
Affiliation: oldMember.Affiliation,
}
sMucId := strconv.FormatInt(mucId, 10)
unavailableStatusCodes := []uint16{303, 210}
availableStatusCodes := []uint16{100, 210}
if c.me != nil && memberID == c.me.Id {
unavailableStatusCodes = append(unavailableStatusCodes, 110)
availableStatusCodes = append(availableStatusCodes, 110)
}
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPType("unavailable"),
gateway.SPFrom(sMucId),
gateway.SPResource(oldMember.Nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(oldMember.Affiliation),
gateway.SPMUCNick(newNickname),
gateway.SPMUCStatusCodes(unavailableStatusCodes),
)
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(sMucId),
gateway.SPResource(newNickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(oldMember.Affiliation),
gateway.SPMUCStatusCodes(availableStatusCodes),
)
}
}
}
// MUCHasResource checks if a MUC was joined from a given resource
func (c *Client) MUCHasResource(chatID int64, resource string) bool {
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
mucState, ok := c.mucCache[chatID]
if !ok || mucState == nil {
return false
}
_, ok = mucState.Resources[resource]
return ok
}
// GetMyMUCNickname obtains this account's nickname in a given MUC
func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) {
if c.me == nil {
return "", false
}
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
mucState, ok := c.mucCache[chatID]
if !ok || mucState == nil {
return "", false
}
member, ok := mucState.Members[c.me.Id]
if !ok {
return "", false
}
return member.Nickname, true
}
func (c *Client) formatContact(chatID int64) string { func (c *Client) formatContact(chatID int64) string {
if chatID == 0 { if chatID == 0 {
return "" return ""
@ -322,7 +543,8 @@ func (c *Client) formatContact(chatID int64) string {
return str return str
} }
func (c *Client) getSenderId(message *client.Message) (senderId int64) { // GetSenderId extracts a sender id from a message
func (c *Client) GetSenderId(message *client.Message) (senderId int64) {
if message.SenderId != nil { if message.SenderId != nil {
switch message.SenderId.MessageSenderType() { switch message.SenderId.MessageSenderType() {
case client.TypeMessageSenderUser: case client.TypeMessageSenderUser:
@ -338,7 +560,7 @@ func (c *Client) getSenderId(message *client.Message) (senderId int64) {
} }
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.GetSenderId(message))
} }
func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) { func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) {
@ -358,7 +580,7 @@ func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply,
replyId = strconv.FormatInt(message.ReplyToMessageId, 10) replyId = strconv.FormatInt(message.ReplyToMessageId, 10)
} }
reply = &gateway.Reply{ reply = &gateway.Reply{
Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), Author: fmt.Sprintf("%v@%s", c.GetSenderId(replyMsg), gateway.Jid.Full()),
Id: replyId, Id: replyId,
} }
} }
@ -851,13 +1073,13 @@ func (c *Client) countCharsInLines(lines *[]string) (count int) {
} }
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) { func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) {
isPM, err := c.IsPM(message.ChatId) chatType, err := c.GetChatType(message.ChatId)
if err != nil { if err != nil {
log.Errorf("Could not determine if chat is PM: %v", err) log.Errorf("Could not determine chat type: %v", err)
} }
isCarbonsEnabled := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons isCarbonsEnabled := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons
// with carbons, hide for all messages in PM and only for outgoing in group chats // with carbons, hide for all messages in PM and only for outgoing in group chats
hideSender := isCarbonsEnabled && (message.IsOutgoing || isPM) hideSender := (isCarbonsEnabled && (message.IsOutgoing || chatType == ChatTypePM)) || (c.Session.MUC && chatType == ChatTypeGroup)
var replyStart, replyEnd int var replyStart, replyEnd int
prefix := []string{} prefix := []string{}
@ -878,7 +1100,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string,
} }
} }
} }
if !isPM || !c.Session.HideIds { if (chatType != ChatTypePM && !c.Session.MUC) || !c.Session.HideIds {
prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10))
} }
// show sender in group chats // show sender in group chats
@ -931,10 +1153,29 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File {
return file return file
} }
// ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side // ProcessIncomingMessage is a legacy wrapper for SendMessageToGateway aiming only PM messages
func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing c.SendMessageToGateway(chatId, message, "", false, "", []string{})
jids := c.getCarbonFullJids(isCarbon, "") }
// SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side
func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string) {
var isCarbon bool
var jids []string
var isGroupchat bool
var originalFrom string
if len(groupChatTos) == 0 {
isCarbon = gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing
jids = c.getCarbonFullJids(isCarbon, "")
} else {
isGroupchat = true
jids = groupChatTos
senderId := c.GetSenderId(message)
if senderId != 0 {
originalFrom = strconv.FormatInt(senderId, 10) + "@" + gateway.Jid.Full()
}
}
var text, oob, auxText string var text, oob, auxText string
@ -1006,13 +1247,29 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
}) })
// forward message to XMPP // forward message to XMPP
sId := strconv.FormatInt(message.Id, 10) var sId string
sChatId := strconv.FormatInt(chatId, 10) if id == "" {
sId = strconv.FormatInt(message.Id, 10)
} else {
sId = id
}
var from string
if groupChatFrom == "" {
from = strconv.FormatInt(chatId, 10)
} else {
from = groupChatFrom
}
var timestamp int64
if delay {
timestamp = int64(message.Date)
}
for _, jid := range jids { for _, jid := range jids {
gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon) gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, isCarbon, isGroupchat, originalFrom)
if auxText != "" { if auxText != "" {
gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon) gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, isCarbon, isGroupchat, originalFrom)
} }
} }
} }
@ -1023,21 +1280,21 @@ func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageC
} }
// ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id // ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id
func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64) int64 { func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, isGroupchat bool) *client.Message {
if !c.Online() { if !c.Online() {
// we're offline // we're offline
return 0 return nil
} }
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, 0, isGroupchat)
} }
// do not send on success // do not send on success
if isCommand { if isCommand {
return 0 return nil
} }
} }
@ -1059,27 +1316,31 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) {
response, err := http.Get(text) response, err := http.Get(text)
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err) c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err, 500, isGroupchat)
} }
if response != nil && response.Body != nil { if response != nil && response.Body != nil {
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != 200 { if response.StatusCode != 200 {
c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode)) c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode), response.StatusCode, isGroupchat)
return nil
} }
tempDir, err := ioutil.TempDir("", "telegabber-*") tempDir, err := ioutil.TempDir("", "telegabber-*")
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Failed to create a temporary directory", err) c.returnError(returnJid, chatID, "Failed to create a temporary directory", err, 500, isGroupchat)
return nil
} }
tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text)))
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Failed to create a temporary file", err) c.returnError(returnJid, chatID, "Failed to create a temporary file", err, 500, isGroupchat)
return nil
} }
_, err = io.Copy(tempFile, response.Body) _, err = io.Copy(tempFile, response.Body)
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Failed to write a temporary file", err) c.returnError(returnJid, chatID, "Failed to write a temporary file", err, 500, isGroupchat)
return nil
} }
file = &client.InputFileLocal{ file = &client.InputFileLocal{
@ -1107,10 +1368,10 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
InputMessageContent: content, InputMessageContent: content,
}) })
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Not edited", err) c.returnError(returnJid, chatID, "Not edited", err, 400, isGroupchat)
return 0 return nil
} }
return tgMessage.Id return tgMessage
} }
tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{
@ -1119,18 +1380,30 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
InputMessageContent: content, InputMessageContent: content,
}) })
if err != nil { if err != nil {
c.returnError(returnJid, chatID, "Not sent", err) c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat)
return 0 return nil
} }
return tgMessage.Id return tgMessage
} }
func (c *Client) returnMessage(returnJid string, chatID int64, text string) { func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) {
gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp) sChatId := strconv.FormatInt(chatID, 10)
if isGroupchat {
gateway.SendErrorMessage(returnJid, sChatId + "@" + gateway.Jid.Bare(), text, code, isGroupchat, c.xmpp)
} else {
gateway.SendTextMessage(returnJid, sChatId, text, c.xmpp)
}
} }
func (c *Client) returnError(returnJid string, chatID int64, msg string, err error) { func (c *Client) returnError(returnJid string, chatID int64, msg string, err error, code int, isGroupchat bool) {
c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error())) responseError, ok := err.(client.ResponseError)
log.Debugf("responseError: %#v", responseError)
if ok && responseError.Err != nil {
if responseError.Err.Message == "Have no write access to the chat" {
code = 403
}
}
c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()), code, isGroupchat)
} }
func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent { func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent {
@ -1228,6 +1501,36 @@ func (c *Client) getLastMessages(id int64, query string, from int64, count int32
}) })
} }
func (c *Client) getNLastMessages(chatID int64, limit int32) ([]*client.Message, error) {
var newMessages *client.Messages
var messages []*client.Message
var err error
var fromId int64
for _ = range make([]struct{}, limit) { // safety limit
if len(messages) > 0 {
fromId = messages[len(messages)-1].Id
}
newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: fromId,
Limit: limit,
})
if err != nil {
return nil, err
}
messages = append(messages, newMessages.Messages...)
if len(newMessages.Messages) == 0 || len(messages) >= int(limit) {
break
}
}
return messages, nil
}
// DownloadFile actually obtains a file by id given by TDlib // DownloadFile actually obtains a file by id given by TDlib
func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) { func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) {
return c.client.DownloadFile(&client.DownloadFileRequest{ return c.client.DownloadFile(&client.DownloadFileRequest{
@ -1285,7 +1588,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
} }
} }
} else { } else {
log.Warnf("Coudln't retrieve private chat info: %v", err.Error()) log.Warnf("Couldn't retrieve private chat info: %v", err.Error())
} }
} else if chatType == client.TypeChatTypeBasicGroup { } else if chatType == client.TypeChatTypeBasicGroup {
basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup)
@ -1295,7 +1598,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
if err == nil { if err == nil {
return fullInfo.Description return fullInfo.Description
} else { } else {
log.Warnf("Coudln't retrieve basic group info: %v", err.Error()) log.Warnf("Couldn't retrieve basic group info: %v", err.Error())
} }
} else if chatType == client.TypeChatTypeSupergroup { } else if chatType == client.TypeChatTypeSupergroup {
supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup)
@ -1305,12 +1608,68 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
if err == nil { if err == nil {
return fullInfo.Description return fullInfo.Description
} else { } else {
log.Warnf("Coudln't retrieve supergroup info: %v", err.Error()) log.Warnf("Couldn't retrieve supergroup info: %v", err.Error())
} }
} }
return "" return ""
} }
// GetChatMemberCount obtains the member count depending on the chat type
func (c *Client) GetChatMemberCount(chat *client.Chat) int32 {
chatType := chat.Type.ChatTypeType()
if chatType == client.TypeChatTypePrivate {
return 2
} else if chatType == client.TypeChatTypeBasicGroup {
basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup)
basicGroup, err := c.client.GetBasicGroup(&client.GetBasicGroupRequest{
BasicGroupId: basicGroupType.BasicGroupId,
})
if err == nil {
return basicGroup.MemberCount
} else {
log.Warnf("Couldn't retrieve basic group: %v", err.Error())
}
} else if chatType == client.TypeChatTypeSupergroup {
supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup)
supergroup, err := c.client.GetSupergroup(&client.GetSupergroupRequest{
SupergroupId: supergroupType.SupergroupId,
})
if err == nil {
return supergroup.MemberCount
} else {
log.Warnf("Couldn't retrieve supergroup: %v", err.Error())
}
}
return 0
}
// GetGroupChats obtains all group chats
func (c *Client) GetGroupChats() []*client.Chat {
var groupChats []*client.Chat
chats, err := c.client.GetChats(&client.GetChatsRequest{
Limit: chatsLimit,
})
if err == nil {
for _, id := range chats.ChatIds {
chat, _, _ := c.GetContactByID(id, nil)
if chat != nil && c.IsGroup(chat) {
groupChats = append(groupChats, chat)
}
}
} else {
log.Errorf("Could not retrieve chats: %v", err)
}
return groupChats
}
// IsGroup determines if a chat is eligible to be represented as MUC
func (c *Client) IsGroup(chat *client.Chat) bool {
typ := chat.Type.ChatTypeType()
return typ == client.TypeChatTypeBasicGroup
}
// subscribe to a Telegram ID // subscribe to a Telegram ID
func (c *Client) subscribeToID(id int64, chat *client.Chat) { func (c *Client) subscribeToID(id int64, chat *client.Chat) {
var args []args.V var args []args.V
@ -1321,6 +1680,10 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) {
chat, _, _ = c.GetContactByID(id, nil) chat, _, _ = c.GetContactByID(id, nil)
} }
if chat != nil { if chat != nil {
if c.Session.MUC && c.IsGroup(chat) {
return
}
args = append(args, gateway.SPNickname(chat.Title)) args = append(args, gateway.SPNickname(chat.Title))
gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp)
@ -1378,6 +1741,10 @@ func (c *Client) UpdateChatNicknames() {
for _, id := range c.cache.ChatsKeys() { for _, id := range c.cache.ChatsKeys() {
chat, ok := c.cache.GetChat(id) chat, ok := c.cache.GetChat(id)
if ok { if ok {
if c.Session.MUC && c.IsGroup(chat) {
continue
}
newArgs := []args.V{ newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(id, 10)), gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPNickname(chat.Title), gateway.SPNickname(chat.Title),
@ -1504,3 +1871,59 @@ func (c *Client) usernamesToString(usernames []string) string {
} }
return strings.Join(atUsernames, ", ") return strings.Join(atUsernames, ", ")
} }
func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) string {
switch memberStatus.ChatMemberStatusType() {
case client.TypeChatMemberStatusCreator:
return "owner"
case client.TypeChatMemberStatusAdministrator:
return "admin"
case client.TypeChatMemberStatusMember:
return "member"
case client.TypeChatMemberStatusRestricted:
return "outcast"
case client.TypeChatMemberStatusLeft:
return "none"
case client.TypeChatMemberStatusBanned:
return "outcast"
}
return "member"
}
func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) {
sChatId := strconv.FormatInt(chatID, 10)
var mucJid string
if toJid != "" {
mucJid = sChatId + "@" + gateway.Jid.Bare()
}
for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
if plain {
reply, _ := c.getMessageReply(message)
gateway.SendMessage(
c.jid,
sChatId,
c.formatMessage(0, 0, false, message),
strconv.FormatInt(message.Id, 10),
c.xmpp,
reply,
0,
false,
false,
"",
)
} else {
c.SendMessageToGateway(
chatID,
message,
"",
true,
mucJid + "/" + c.GetMUCNickname(c.GetSenderId(message)),
[]string{toJid},
)
}
}
}

@ -213,6 +213,60 @@ type QueryRegisterRemove struct {
XMLName xml.Name `xml:"remove"` XMLName xml.Name `xml:"remove"`
} }
// PresenceXMucUserExtension is from XEP-0045
type PresenceXMucUserExtension struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"`
Item PresenceXMucUserItem
Statuses []PresenceXMucUserStatus
}
// PresenceXMucUserItem is from XEP-0045
type PresenceXMucUserItem struct {
XMLName xml.Name `xml:"item"`
Affiliation string `xml:"affiliation,attr"`
Jid string `xml:"jid,attr"`
Nick string `xml:"nick,attr,omitempty"`
Role string `xml:"role,attr"`
}
// PresenceXMucUserStatus is from XEP-0045
type PresenceXMucUserStatus struct {
XMLName xml.Name `xml:"status"`
Code uint16 `xml:"code,attr"`
}
// MessageDelay is from XEP-0203
type MessageDelay struct {
XMLName xml.Name `xml:"urn:xmpp:delay delay"`
From string `xml:"from,attr"`
Stamp string `xml:"stamp,attr"`
}
// MessageDelayLegacy is from XEP-0203
type MessageDelayLegacy struct {
XMLName xml.Name `xml:"jabber:x:delay x"`
From string `xml:"from,attr"`
Stamp string `xml:"stamp,attr"`
}
// MessageAddresses is from XEP-0033
type MessageAddresses struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"`
Addresses []MessageAddress
}
// MessageAddress is from XEP-0033
type MessageAddress struct {
XMLName xml.Name `xml:"address"`
Type string `xml:"type,attr"`
Jid string `xml:"jid,attr"`
}
// EmptySubject is a dummy for MUCs to circumvent omitempty. Not registered as it would conflict with Subject field
type EmptySubject struct {
XMLName xml.Name `xml:"subject"`
}
// Namespace is a namespace! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -278,6 +332,21 @@ func (c QueryRegister) GetSet() *stanza.ResultSet {
return c.ResultSet return c.ResultSet
} }
// Namespace is a namespace!
func (c PresenceXMucUserExtension) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c MessageDelay) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c MessageDelayLegacy) Namespace() string {
return c.XMLName.Space
}
// Name is a packet name // Name is a packet name
func (ClientMessage) Name() string { func (ClientMessage) Name() string {
return "message" return "message"
@ -362,4 +431,28 @@ func init() {
"jabber:iq:register", "jabber:iq:register",
"query", "query",
}, QueryRegister{}) }, QueryRegister{})
// presence muc user
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{
"http://jabber.org/protocol/muc#user",
"x",
}, PresenceXMucUserExtension{})
// message delay
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:delay",
"delay",
}, MessageDelay{})
// legacy message delay
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"jabber:x:delay",
"x",
}, MessageDelayLegacy{})
// message addresses
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"http://jabber.org/protocol/address",
"addresses",
}, MessageAddresses{})
} }

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"strings" "strings"
"sync" "sync"
"time"
"dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/extensions"
@ -42,26 +43,41 @@ var DirtySessions = false
var MessageOutgoingPermissionVersion = 0 var MessageOutgoingPermissionVersion = 0
// SendMessage creates and sends a message stanza // SendMessage creates and sends a message stanza
func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) { func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) {
sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon) sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0)
} }
// SendServiceMessage creates and sends a simple message stanza from transport // SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) { func SendServiceMessage(to, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", component, nil, "", false) sendMessageWrapper(to, "", body, "", "", "", component, nil, 0, "", false, false, false, "", 0)
} }
// SendTextMessage creates and sends a simple message stanza // SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to string, from string, body string, component *xmpp.Component) { func SendTextMessage(to, from, body string, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", component, nil, "", false) sendMessageWrapper(to, from, body, "", "", "", component, nil, 0, "", false, false, false, "", 0)
}
// SendErrorMessage creates and sends an error message stanza
func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, "", "", text, "", component, nil, 0, "", false, isGroupchat, false, "", code)
}
// SendErrorMessageWithBody creates and sends an error message stanza with body payload
func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", errorText, id, component, nil, 0, "", false, isGroupchat, false, "", code)
} }
// SendMessageWithOOB creates and sends a message stanza with OOB URL // SendMessageWithOOB creates and sends a message stanza with OOB URL
func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) {
sendMessageWrapper(to, from, body, id, component, reply, oob, isCarbon) sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom, 0)
}
// SendSubjectMessage creates and sends a MUC subject
func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) {
sendMessageWrapper(to, from, "", subject, "", id, component, nil, timestamp, "", false, true, true, "", 0)
} }
func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) {
toJid, err := stanza.NewJid(to) toJid, err := stanza.NewJid(to)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -76,12 +92,17 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
var logFrom string var logFrom string
var messageFrom string var messageFrom string
var messageTo string var messageTo string
if from == "" { if isGroupchat {
logFrom = componentJid
messageFrom = componentJid
} else {
logFrom = from logFrom = from
messageFrom = from + "@" + componentJid messageFrom = from
} else {
if from == "" {
logFrom = componentJid
messageFrom = componentJid
} else {
logFrom = from
messageFrom = from + "@" + componentJid
}
} }
if isCarbon { if isCarbon {
messageTo = messageFrom messageTo = messageFrom
@ -95,14 +116,51 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
"to": to, "to": to,
}).Warn("Got message") }).Warn("Got message")
var messageType stanza.StanzaType
if errorCode != 0 {
messageType = stanza.MessageTypeError
} else if isGroupchat {
messageType = stanza.MessageTypeGroupchat
} else {
messageType = stanza.MessageTypeChat
}
message := stanza.Message{ message := stanza.Message{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: messageFrom, From: messageFrom,
To: messageTo, To: messageTo,
Type: "chat", Type: messageType,
Id: id, Id: id,
}, },
Body: body, Subject: subject,
Body: body,
}
if errorCode != 0 {
message.Error = stanza.Err{
Code: errorCode,
Text: errorText,
}
switch errorCode {
case 400:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "bad-request"
case 403:
message.Error.Type = stanza.ErrorTypeAuth
message.Error.Reason = "forbidden"
case 404:
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "item-not-found"
case 406:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "not-acceptable"
case 500:
message.Error.Type = stanza.ErrorTypeWait
message.Error.Reason = "internal-server-error"
default:
log.Error("Unknown error code, falling back with empty reason")
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "undefined-condition"
}
} }
if oob != "" { if oob != "" {
@ -119,16 +177,43 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End))
} }
} }
if !isCarbon && toJid.Resource != "" { if !isGroupchat && !isCarbon && toJid.Resource != "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
} }
if timestamp != 0 {
var delayFrom string
if isGroupchat {
delayFrom, _, _ = SplitJID(from)
}
message.Extensions = append(message.Extensions, extensions.MessageDelay{
From: delayFrom,
Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339),
})
message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{
From: delayFrom,
Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"),
})
}
if originalFrom != "" {
message.Extensions = append(message.Extensions, extensions.MessageAddresses{
Addresses: []extensions.MessageAddress{
extensions.MessageAddress{
Type: "ofrom",
Jid: originalFrom,
},
},
})
}
if subject == "" && forceSubject {
message.Extensions = append(message.Extensions, extensions.EmptySubject{})
}
if isCarbon { if isCarbon {
carbonMessage := extensions.ClientMessage{ carbonMessage := extensions.ClientMessage{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: bareTo, From: bareTo,
To: to, To: to,
Type: "chat", Type: messageType,
}, },
} }
carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{ carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{
@ -240,6 +325,18 @@ var SPResource = args.NewString()
// SPImmed skips queueing // SPImmed skips queueing
var SPImmed = args.NewBool(args.Default(true)) var SPImmed = args.NewBool(args.Default(true))
// SPMUCAffiliation is a XEP-0045 MUC affiliation
var SPMUCAffiliation = args.NewString()
// SPMUCNick is a XEP-0045 MUC user nick
var SPMUCNick = args.NewString()
// SPMUCJid is a real jid of a MUC member
var SPMUCJid = args.NewString()
// SPMUCStatusCodes is a set of XEP-0045 MUC status codes
var SPMUCStatusCodes = args.New()
func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
var presenceFrom string var presenceFrom string
if SPFrom.IsSet(args) { if SPFrom.IsSet(args) {
@ -295,6 +392,32 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
}) })
} }
} }
if SPMUCAffiliation.IsSet(args) {
affiliation := SPMUCAffiliation.Get(args)
if affiliation != "" {
userExt := extensions.PresenceXMucUserExtension{
Item: extensions.PresenceXMucUserItem{
Affiliation: affiliation,
Role: affilationToRole(affiliation),
},
}
if SPMUCNick.IsSet(args) {
userExt.Item.Nick = SPMUCNick.Get(args)
}
if SPMUCJid.IsSet(args) {
userExt.Item.Jid = SPMUCJid.Get(args)
}
if SPMUCStatusCodes.IsSet(args) {
statusCodes := SPMUCStatusCodes.Get(args).([]uint16)
for _, statusCode := range statusCodes {
userExt.Statuses = append(userExt.Statuses, extensions.PresenceXMucUserStatus{
Code: statusCode,
})
}
}
presence.Extensions = append(presence.Extensions, userExt)
}
}
return presence return presence
} }
@ -377,3 +500,13 @@ func SplitJID(from string) (string, string, bool) {
} }
return fromJid.Bare(), fromJid.Resource, true return fromJid.Bare(), fromJid.Resource, true
} }
func affilationToRole(affilation string) string {
switch affilation {
case "owner", "admin":
return "moderator"
case "member":
return "participant"
}
return "none"
}

@ -27,6 +27,12 @@ const (
) )
const NodeVCard4 string = "urn:xmpp:vcard4" const NodeVCard4 string = "urn:xmpp:vcard4"
type discoType int
const (
discoTypeInfo discoType = iota
discoTypeItems
)
func logPacketType(p stanza.Packet) { func logPacketType(p stanza.Packet) {
log.Warnf("Ignoring packet: %T\n", p) log.Warnf("Ignoring packet: %T\n", p)
} }
@ -55,12 +61,12 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
} }
_, ok = iq.Payload.(*stanza.DiscoInfo) _, ok = iq.Payload.(*stanza.DiscoInfo)
if ok { if ok {
go handleGetDiscoInfo(s, iq) go handleGetDisco(discoTypeInfo, s, iq)
return return
} }
_, ok = iq.Payload.(*stanza.DiscoItems) _, ok = iq.Payload.(*stanza.DiscoItems)
if ok { if ok {
go handleGetDiscoItems(s, iq) go handleGetDisco(discoTypeItems, s, iq)
return return
} }
_, ok = iq.Payload.(*extensions.QueryRegister) _, ok = iq.Payload.(*extensions.QueryRegister)
@ -117,6 +123,26 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
toID, ok := toToID(msg.To) toID, ok := toToID(msg.To)
if ok { if ok {
toJid, err := stanza.NewJid(msg.To)
if err != nil {
log.Error("Invalid to JID!")
return
}
isGroupchat := msg.Type == "groupchat"
if session.Session.MUC && toJid.Resource != "" {
chat, _, err := session.GetContactByID(toID, nil)
if err == nil && session.IsGroup(chat) {
if isGroupchat {
gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component)
} else {
gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component)
}
return
}
}
var reply extensions.Reply var reply extensions.Reply
var fallback extensions.Fallback var fallback extensions.Fallback
var replace extensions.Replace var replace extensions.Replace
@ -128,7 +154,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
log.Debugf("replace: %#v", replace) log.Debugf("replace: %#v", replace)
var replyId int64 var replyId int64
var err error
text := msg.Body text := msg.Body
if len(reply.Id) > 0 { if len(reply.Id) > 0 {
chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id) chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id)
@ -191,8 +216,8 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
session.SendMessageLock.Lock() session.SendMessageLock.Lock()
defer session.SendMessageLock.Unlock() defer session.SendMessageLock.Unlock()
tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat)
if tgMessageId != 0 { if tgMessage != nil {
if replaceId != 0 { if replaceId != 0 {
// not needed (is it persistent among clients though?) // not needed (is it persistent among clients though?)
/* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId) /* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId)
@ -201,11 +226,23 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
} */ } */
session.AddToOutbox(replace.Id, resource) session.AddToOutbox(replace.Id, resource)
} else { } else {
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessage.Id, msg.Id)
if err != nil { if err != nil {
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) log.Errorf("Failed to save ids %v/%v %v", toID, tgMessage.Id, msg.Id)
} }
} }
// pong groupchat messages back
if isGroupchat && toJid.Resource == "" {
session.SendMessageToGateway(
toID,
tgMessage,
msg.Id,
false,
msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage)),
[]string{msg.From},
)
}
} else { } else {
/* /*
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
@ -281,7 +318,15 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) {
} }
if prs.To == gateway.Jid.Bare() { if prs.To == gateway.Jid.Bare() {
handlePresence(s, prs) handlePresence(s, prs)
return
} }
var mucExt stanza.MucPresence
prs.Get(&mucExt)
if mucExt.XMLName.Space != "" {
handleMUCPresence(s, prs, mucExt)
return
}
tryHandleMUCNicknameChange(s, prs)
} }
func handleSubscription(s xmpp.Sender, p stanza.Presence) { func handleSubscription(s xmpp.Sender, p stanza.Presence) {
@ -391,6 +436,141 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) {
} }
} }
func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresence) {
log.WithFields(log.Fields{
"type": p.Type,
"from": p.From,
"to": p.To,
}).Warn("MUC presence")
log.Debugf("%#v", p)
if p.Type == "" {
toBare, nickname, ok := gateway.SplitJID(p.To)
if ok {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
// separate declaration is crucial for passing as pointer to defer
var reply *stanza.Presence
reply = &stanza.Presence{Attrs: stanza.Attrs{
From: toBare,
To: p.From,
Id: p.Id,
}}
defer gateway.ResumableSend(component, reply)
if nickname == "" {
presenceReplySetError(reply, 400)
return
}
chatId, ok := toToID(toBare)
if !ok {
presenceReplySetError(reply, 404)
return
}
fromBare, fromResource, ok := gateway.SplitJID(p.From)
if !ok {
presenceReplySetError(reply, 400)
return
}
session, ok := sessions[fromBare]
if !ok || !session.Session.MUC {
presenceReplySetError(reply, 407)
return
}
chat, _, err := session.GetContactByID(chatId, nil)
if err != nil || !session.IsGroup(chat) {
presenceReplySetError(reply, 404)
return
}
limit, ok := mucExt.History.MaxStanzas.Get()
if !ok {
limit = 20
}
session.JoinMUC(chatId, fromResource, int32(limit))
}
}
}
func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) {
log.WithFields(log.Fields{
"type": p.Type,
"from": p.From,
"to": p.To,
}).Warn("Nickname change presence?")
log.Debugf("%#v", p)
if p.Type != "" {
return
}
toBare, nickname, ok := gateway.SplitJID(p.To)
if !ok || nickname == "" {
return
}
fromBare, fromResource, ok := gateway.SplitJID(p.From)
if !ok {
return
}
session, ok := sessions[fromBare]
if !ok || !session.Session.MUC {
return
}
chatId, ok := toToID(toBare)
if !ok {
return
}
chat, _, err := session.GetContactByID(chatId, nil)
if err != nil || !session.IsGroup(chat) {
return
}
if !session.MUCHasResource(chatId, fromResource) {
return
}
log.Warn("🗿 Yes")
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
from := toBare
nickname, ok = session.GetMyMUCNickname(chatId)
if ok {
from = from+"/"+nickname
}
reply := &stanza.Presence{
Attrs: stanza.Attrs{
From: from,
To: p.From,
Id: p.Id,
Type: stanza.PresenceTypeError,
},
Error: stanza.Err{
Code: 406,
Type: stanza.ErrorTypeModify,
Reason: "not-acceptable",
Text: "Telegram does not support changing nicknames per-chat. Issue a /setname command to the transport if you wish to change the global name",
},
}
gateway.ResumableSend(component, reply)
}
func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"from": iq.From, "from": iq.From,
@ -441,7 +621,7 @@ 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 handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
answer, err := stanza.NewIQ(stanza.Attrs{ answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult, Type: stanza.IQTypeResult,
From: iq.To, From: iq.To,
@ -454,41 +634,92 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
return return
} }
disco := answer.DiscoInfo() if dt == discoTypeInfo {
_, ok := toToID(iq.To) disco := answer.DiscoInfo()
if ok { toID, toOk := toToID(iq.To)
disco.AddIdentity("", "account", "registered") if !toOk {
} else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register")
disco.AddFeatures("jabber:iq:register") }
}
answer.Payload = disco
log.Debugf("%#v", answer) var isMuc bool
bare, _, fromOk := gateway.SplitJID(iq.From)
if fromOk {
session, sessionOk := sessions[bare]
if sessionOk && session.Session.MUC {
if toOk {
chat, _, err := session.GetContactByID(toID, nil)
if err == nil && session.IsGroup(chat) {
isMuc = true
disco.AddIdentity(chat.Title, "conference", "text")
disco.AddFeatures(
"http://jabber.org/protocol/muc",
"muc_persistent",
"muc_hidden",
"muc_membersonly",
"muc_unmoderated",
"muc_nonanonymous",
"muc_unsecured",
"http://jabber.org/protocol/muc#stable_id",
)
fields := []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: "hidden",
ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"},
},
&stanza.Field{
Var: "muc#roominfo_description",
Label: "Description",
ValuesList: []string{session.GetChatDescription(chat)},
},
&stanza.Field{
Var: "muc#roominfo_occupants",
Label: "Number of occupants",
ValuesList: []string{strconv.FormatInt(int64(session.GetChatMemberCount(chat)), 10)},
},
}
component, ok := s.(*xmpp.Component) disco.Form = stanza.NewForm(fields, "result")
if !ok { }
log.Error("Not a component") } else {
return disco.AddFeatures(
} stanza.NSDiscoItems,
"http://jabber.org/protocol/muc#stable_id",
)
disco.AddIdentity("Telegram group chats", "conference", "text")
}
}
}
if toOk && !isMuc {
disco.AddIdentity("", "account", "registered")
}
answer.Payload = disco
} else if dt == discoTypeItems {
disco := answer.DiscoItems()
_ = gateway.ResumableSend(component, answer) _, ok := toToID(iq.To)
} if !ok {
bare, _, ok := gateway.SplitJID(iq.From)
if ok {
// raw access, no need to create a new instance if not connected
session, ok := sessions[bare]
if ok && session.Session.MUC {
bareJid := gateway.Jid.Bare()
disco.AddItem(bareJid, "", "Telegram group chats")
for _, chat := range session.GetGroupChats() {
jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid
disco.AddItem(jid, "", chat.Title)
}
}
}
}
func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { answer.Payload = disco
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
} }
answer.Payload = answer.DiscoItems() log.Debugf("%#v", answer)
component, ok := s.(*xmpp.Component) component, ok := s.(*xmpp.Component)
if !ok { if !ok {
@ -658,6 +889,28 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code
} }
} }
func presenceReplySetError(reply *stanza.Presence, code int) {
reply.Type = stanza.PresenceTypeError
reply.Error = stanza.Err{
Code: code,
}
switch code {
case 400:
reply.Error.Type = stanza.ErrorTypeModify
reply.Error.Reason = "jid-malformed"
case 407:
reply.Error.Type = stanza.ErrorTypeAuth
reply.Error.Reason = "registration-required"
case 404:
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "item-not-found"
default:
log.Error("Unknown error code, falling back with empty reason")
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "undefined-condition"
}
}
func toToID(to string) (int64, bool) { func toToID(to string) (int64, bool) {
toParts := strings.Split(to, "@") toParts := strings.Split(to, "@")
if len(toParts) < 2 { if len(toParts) < 2 {

Loading…
Cancel
Save