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)
TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1"
VERSION := "v1.8.2"
VERSION := "v2.0.0-dev"
MAKEOPTS := "-j4"
all:

@ -33,5 +33,5 @@ require (
nhooyr.io/websocket v1.6.5 // indirect
)
replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f
replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f
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-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-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/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

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

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

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

@ -41,6 +41,25 @@ type DelayedStatus struct {
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
type Client struct {
client *client.Client
@ -64,6 +83,8 @@ type Client struct {
lastMsgHashes map[int64]uint64
msgHashSeed maphash.Seed
mucCache map[int64]*MUCState
locks clientLocks
SendMessageLock sync.Mutex
}
@ -73,6 +94,7 @@ type clientLocks struct {
chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex
outboxLock sync.Mutex
mucCacheLock sync.Mutex
lastMsgHashesLock sync.Mutex
authorizerReadLock sync.Mutex
@ -133,6 +155,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
Session: session,
resources: make(map[string]bool),
outbox: make(map[string]string),
mucCache: make(map[int64]*MUCState),
content: &conf.Content,
cache: cache.NewCache(),
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) {
userID, err := strconv.ParseInt(username, 10, 64)
// 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
}
c.sendMessagesReverse(chatID, messages.Messages)
c.sendMessagesReverse(chatID, messages.Messages, true, "")
// get latest entries from history
case "history":
var limit int32 = 10
@ -1016,32 +999,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
}
}
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 err.Error(), true
}
messages = append(messages, newMessages.Messages...)
if len(newMessages.Messages) == 0 || len(messages) >= int(limit) {
break
}
messages, err := c.getNLastMessages(chatID, limit)
if err != nil {
return err.Error(), true
}
c.sendMessagesReverse(chatID, messages)
c.sendMessagesReverse(chatID, messages, true, "")
// chat members
case "members":
var query string

@ -153,6 +153,13 @@ func (c *Client) updateHandler() {
// new user discovered
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)
show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id)
go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType))
@ -265,7 +272,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
markupFunction,
))
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
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)
// set also the status (for group chats only)
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if user == nil {
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 messageHeaderSeparator string = " | "
const (
ChatTypeOther byte = iota
ChatTypePM
ChatTypeGroup
)
// GetContactByUsername resolves username to user id retrieves user and chat information
func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) {
if !c.Online() {
@ -121,10 +127,10 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli
return chat, user, nil
}
// IsPM checks if a chat is PM
func (c *Client) IsPM(id int64) (bool, error) {
// GetChatType checks if a chat is PM or group
func (c *Client) GetChatType(id int64) (byte, error) {
if !c.Online() || id == 0 {
return false, errOffline
return ChatTypeOther, errOffline
}
var err error
@ -135,7 +141,7 @@ func (c *Client) IsPM(id int64) (bool, error) {
ChatId: id,
})
if err != nil {
return false, err
return ChatTypeOther, err
}
c.cache.SetChat(id, chat)
@ -143,9 +149,12 @@ func (c *Client) IsPM(id int64) (bool, error) {
chatType := chat.Type.ChatTypeType()
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) {
@ -217,6 +226,10 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
return err
}
if chat != nil && c.Session.MUC && c.IsGroup(chat) {
return nil
}
var photo string
if chat != nil && chat.Photo != nil {
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 {
if chatID == 0 {
return ""
@ -322,7 +543,8 @@ func (c *Client) formatContact(chatID int64) string {
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 {
switch message.SenderId.MessageSenderType() {
case client.TypeMessageSenderUser:
@ -338,7 +560,7 @@ func (c *Client) getSenderId(message *client.Message) (senderId int64) {
}
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) {
@ -358,7 +580,7 @@ func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply,
replyId = strconv.FormatInt(message.ReplyToMessageId, 10)
}
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,
}
}
@ -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) {
isPM, err := c.IsPM(message.ChatId)
chatType, err := c.GetChatType(message.ChatId)
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
// 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
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))
}
// show sender in group chats
@ -931,10 +1153,29 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.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) {
isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing
jids := c.getCarbonFullJids(isCarbon, "")
c.SendMessageToGateway(chatId, message, "", false, "", []string{})
}
// 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
@ -1006,13 +1247,29 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
})
// forward message to XMPP
sId := strconv.FormatInt(message.Id, 10)
sChatId := strconv.FormatInt(chatId, 10)
var sId string
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 {
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 != "" {
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
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() {
// we're offline
return 0
return nil
}
if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) {
// try to execute commands
response, isCommand := c.ProcessChatCommand(chatID, text)
if response != "" {
c.returnMessage(returnJid, chatID, response)
c.returnMessage(returnJid, chatID, response, 0, isGroupchat)
}
// do not send on success
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) {
response, err := http.Get(text)
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 {
defer response.Body.Close()
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-*")
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)))
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)
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{
@ -1107,10 +1368,10 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
InputMessageContent: content,
})
if err != nil {
c.returnError(returnJid, chatID, "Not edited", err)
return 0
c.returnError(returnJid, chatID, "Not edited", err, 400, isGroupchat)
return nil
}
return tgMessage.Id
return tgMessage
}
tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{
@ -1119,18 +1380,30 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
InputMessageContent: content,
})
if err != nil {
c.returnError(returnJid, chatID, "Not sent", err)
return 0
c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat)
return nil
}
return tgMessage.Id
return tgMessage
}
func (c *Client) returnMessage(returnJid string, chatID int64, text string) {
gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp)
func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) {
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) {
c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()))
func (c *Client) returnError(returnJid string, chatID int64, msg string, err error, code int, isGroupchat bool) {
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 {
@ -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
func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) {
return c.client.DownloadFile(&client.DownloadFileRequest{
@ -1285,7 +1588,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
}
}
} 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 {
basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup)
@ -1295,7 +1598,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
if err == nil {
return fullInfo.Description
} 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 {
supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup)
@ -1305,12 +1608,68 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
if err == nil {
return fullInfo.Description
} else {
log.Warnf("Coudln't retrieve supergroup info: %v", err.Error())
log.Warnf("Couldn't retrieve supergroup info: %v", err.Error())
}
}
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
func (c *Client) subscribeToID(id int64, chat *client.Chat) {
var args []args.V
@ -1321,6 +1680,10 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) {
chat, _, _ = c.GetContactByID(id, nil)
}
if chat != nil {
if c.Session.MUC && c.IsGroup(chat) {
return
}
args = append(args, gateway.SPNickname(chat.Title))
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() {
chat, ok := c.cache.GetChat(id)
if ok {
if c.Session.MUC && c.IsGroup(chat) {
continue
}
newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPNickname(chat.Title),
@ -1504,3 +1871,59 @@ func (c *Client) usernamesToString(usernames []string) string {
}
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"`
}
// 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!
func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space
@ -278,6 +332,21 @@ func (c QueryRegister) GetSet() *stanza.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
func (ClientMessage) Name() string {
return "message"
@ -362,4 +431,28 @@ func init() {
"jabber:iq:register",
"query",
}, 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"
"strings"
"sync"
"time"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
@ -42,26 +43,41 @@ var DirtySessions = false
var MessageOutgoingPermissionVersion = 0
// SendMessage creates and sends a message stanza
func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) {
sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon)
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, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0)
}
// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", component, nil, "", false)
func SendServiceMessage(to, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", "", "", component, nil, 0, "", false, false, false, "", 0)
}
// SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to string, from string, body string, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", component, nil, "", false)
func SendTextMessage(to, from, body string, component *xmpp.Component) {
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
func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) {
sendMessageWrapper(to, from, body, id, component, reply, oob, isCarbon)
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, 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)
if err != nil {
log.WithFields(log.Fields{
@ -76,12 +92,17 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
var logFrom string
var messageFrom string
var messageTo string
if from == "" {
logFrom = componentJid
messageFrom = componentJid
} else {
if isGroupchat {
logFrom = from
messageFrom = from + "@" + componentJid
messageFrom = from
} else {
if from == "" {
logFrom = componentJid
messageFrom = componentJid
} else {
logFrom = from
messageFrom = from + "@" + componentJid
}
}
if isCarbon {
messageTo = messageFrom
@ -95,14 +116,51 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
"to": to,
}).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{
Attrs: stanza.Attrs{
From: messageFrom,
To: messageTo,
Type: "chat",
Type: messageType,
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 != "" {
@ -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))
}
}
if !isCarbon && toJid.Res