Implement occupant IDs (XEP-0421) and shorten visible occupant nicknames

This commit is contained in:
Bohdan Horbeshko 2026-08-22 17:56:31 -04:00
parent 4566297817
commit 22cb5b3d4c
7 changed files with 311 additions and 97 deletions

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@ -215,9 +216,17 @@ func (c *Client) updateUser(update *client.UpdateUser) {
// check if MUC nicknames should be updated
oldCacheUser, ok := c.cache.GetUser(update.User.Id)
c.cache.SetUser(update.User.Id, update.User)
if ok && (oldCacheUser.FirstName != update.User.FirstName || oldCacheUser.LastName != update.User.LastName) {
newNickname := c.GetMUCNickname(update.User.Id)
c.updateMUCsNickname(update.User.Id, newNickname)
if ok {
var oldUsernames, newUsernames []string
if oldCacheUser.Usernames != nil {
oldUsernames = oldCacheUser.Usernames.ActiveUsernames
}
if update.User.Usernames != nil {
newUsernames = update.User.Usernames.ActiveUsernames
}
if oldCacheUser.FirstName != update.User.FirstName || oldCacheUser.LastName != update.User.LastName || !slices.Equal(oldUsernames, newUsernames) {
c.updateMUCsNickname(update.User.Id)
}
}
show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id)
@ -425,10 +434,11 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
var from string
var originalFrom string
var nickname string
var senderId int64
if isMUC {
if messageErr == nil {
senderId := c.getMessageSenderId(message)
nickname = c.GetMUCNickname(senderId)
senderId = c.getMessageSenderId(message)
nickname = c.GetMUCNickname(update.ChatId, senderId)
originalFrom = gateway.CHATJID(senderId, true)
safeToSend = c.assureMUCOccupant(update.ChatId, senderId, message.SenderId, chat)
@ -461,7 +471,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
textContent.Text.Text,
textContent.Text.Entities,
markupFunction,
c,
mucMentionRetriever{c, update.ChatId},
))
id := "e"+sId
@ -510,10 +520,14 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
for _, jid := range jids {
if safeToSend {
var occupantId string
if isMUC && senderId != 0 {
occupantId = gateway.CHATNODE(senderId)
}
gateway.SendMessage(jid, from, c.xmpp,
gateway.SMBody(body), gateway.SMId(id), gateway.SMReplaceId(replaceId),
gateway.SMIsCarbon(isCarbon), gateway.SMIsGroupchat(isMUC), gateway.SMOriginalFrom(originalFrom),
gateway.SMOMEMOEnvelope(envelope),
gateway.SMOMEMOEnvelope(envelope), gateway.SMOccupantId(occupantId),
)
} else {
gateway.SendMUCAnnouncement(jid, from, body, nickname, id, c.xmpp)
@ -713,6 +727,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) {
gateway.SPResource(occupant.Nickname),
gateway.SPImmed(true),
gateway.SPMUCJid(gateway.CHATJID(occupant.id, true)),
gateway.SPOccupantId(gateway.CHATNODE(occupant.id)),
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role),
gateway.SPToJids(toJids),

View file

@ -114,8 +114,8 @@ func (lru *MUCOccupantsLRU) Set(id int64, occupant *MUCOccupant) (deleted *MUCOc
lru.cutOut(oldOccupant)
delete(lru.rev, oldOccupant.Nickname)
lru.rev[occupant.Nickname] = id
}
lru.rev[occupant.Nickname] = id
if (lru.oldest == nil) != (lru.newest == nil) {
log.Fatal("MRD MUDAQ")

View file

@ -210,7 +210,7 @@ func (c *Client) updateMessageInteractionInfo(update *client.UpdateMessageIntera
if jids := c.GetReactionCapableFullJids(); len(jids) > 0 && c.peerReactionsChanged(update.ChatId, update.MessageId, 0, peerEmojis) {
from := gateway.CHATNODE(update.ChatId)
for _, jid := range jids {
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, peerEmojis, false)
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, peerEmojis)
}
}
return
@ -231,9 +231,9 @@ func (c *Client) updateMessageInteractionInfo(update *client.UpdateMessageIntera
if !c.assureMUCOccupant(update.ChatId, senderId, &client.MessageSenderUser{UserId: senderId}, chat) {
continue
}
from := mucJid + "/" + c.GetMUCNickname(senderId)
from := mucJid + "/" + c.GetMUCNickname(update.ChatId, senderId)
for _, jid := range jids {
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, senderEmojis, true)
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, senderEmojis, gateway.SMIsGroupchat(true), gateway.SMOccupantId(gateway.CHATNODE(senderId)))
}
}
@ -245,9 +245,9 @@ func (c *Client) updateMessageInteractionInfo(update *client.UpdateMessageIntera
if c.peerReactionsChanged(update.ChatId, update.MessageId, update.ChatId, anonymousEmojis) {
anonymousSender := &client.MessageSenderChat{ChatId: update.ChatId}
if c.assureMUCOccupant(update.ChatId, update.ChatId, anonymousSender, chat) {
from := mucJid + "/" + c.GetMUCNickname(update.ChatId)
from := mucJid + "/" + c.GetMUCNickname(update.ChatId, update.ChatId)
for _, jid := range jids {
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, anonymousEmojis, true)
gateway.SendReactionMessage(jid, from, c.xmpp, xmppId, anonymousEmojis, gateway.SMIsGroupchat(true), gateway.SMOccupantId(gateway.CHATNODE(update.ChatId)))
}
}
}
@ -306,13 +306,21 @@ func (c *Client) SendDelayedMUCReactions(chatId int64, message *client.Message,
timestamp := int64(message.Date)
for senderId, senderEmojis := range perOccupant {
from := mucJid + "/" + c.GetMUCNickname(senderId)
from := mucJid + "/" + c.GetMUCNickname(chatId, senderId)
entryId := "r" + strId + "-" + strconv.FormatInt(senderId, 10)
gateway.SendMAMReactionMessage(toJid, from, c.xmpp, xmppId, senderEmojis, entryId, timestamp, mucJid, mamQueryId)
gateway.SendReactionMessage(toJid, from, c.xmpp, xmppId, senderEmojis,
gateway.SMId(entryId), gateway.SMStanzaId(entryId), gateway.SMTimestamp(timestamp),
gateway.SMMamQueryId(mamQueryId), gateway.SMMucJID(mucJid), gateway.SMIsGroupchat(true),
gateway.SMOccupantId(gateway.CHATNODE(senderId)),
)
}
if len(anonymousEmojis) > 0 {
from := mucJid + "/" + c.GetMUCNickname(chatId)
gateway.SendMAMReactionMessage(toJid, from, c.xmpp, xmppId, anonymousEmojis, "r"+strId+"-a", timestamp, mucJid, mamQueryId)
from := mucJid + "/" + c.GetMUCNickname(chatId, chatId)
gateway.SendReactionMessage(toJid, from, c.xmpp, xmppId, anonymousEmojis,
gateway.SMId("r"+strId+"-a"), gateway.SMStanzaId("r"+strId+"-a"), gateway.SMTimestamp(timestamp),
gateway.SMMamQueryId(mamQueryId), gateway.SMMucJID(mucJid), gateway.SMIsGroupchat(true),
gateway.SMOccupantId(gateway.CHATNODE(chatId)),
)
}
}
@ -431,7 +439,24 @@ func (c *Client) ownReactions(chatId, messageId int64) (map[string]bool, error)
MessageId: messageId,
})
if err != nil {
return nil, err
// messageId may be a since-superseded provisional id for a message
// we ourselves just sent - TDlib assigns a temporary id
// immediately and reassigns the real one shortly after via
// UpdateMessageSendSucceeded (see updateMessageSendSucceeded's
// MessageIdChanges bookkeeping). Retry with the current id if the
// reassignment has already landed.
c.MessageIdChangesLock.Lock()
newId, newIdOk := c.MessageIdChanges[chatId][messageId]
c.MessageIdChangesLock.Unlock()
if newIdOk && newId.Id != 0 {
message, err = c.client.GetMessage(&client.GetMessageRequest{
ChatId: chatId,
MessageId: newId.Id,
})
}
if err != nil {
return nil, err
}
}
current := make(map[string]bool)

View file

@ -596,6 +596,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
gateway.SPMUCAffiliation(occupant.Affiliation),
gateway.SPMUCRole(occupant.Role),
gateway.SPMUCJid(chatJid),
gateway.SPOccupantId(gateway.CHATNODE(chatID)),
gateway.SPToJids(toJids),
)
err := c.sendPresence(newMucArgs...)
@ -723,10 +724,11 @@ func (c *Client) sendMUCStatuses(chatID int64) {
func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) {
sChatId := gateway.MUCNODE(chatID)
myNickname := "me"
var myJid string
var myJid, myOccupantId string
if c.me != nil {
myNickname = c.GetMUCNickname(c.me.Id)
myNickname = c.getMUCNicknameLocked(c.me.Id, mucState)
myJid = gateway.CHATJID(c.me.Id, true)
myOccupantId = gateway.CHATNODE(c.me.Id)
}
myAffiliation := "member"
myRole := "participant"
@ -736,7 +738,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []
_, toJids := c.getMUCJoinedJIDs(chatID, mucState, false)
for _, member := range members {
senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat)
senderId, nickname, affiliation, role := c.tgMemberToMUCOccupantLocked(member, chat, mucState)
mucState.Occupants.Set(senderId, &MUCOccupant{
Nickname: nickname,
Affiliation: affiliation,
@ -758,6 +760,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role),
gateway.SPMUCJid(gateway.CHATJID(senderId, true)),
gateway.SPOccupantId(gateway.CHATNODE(senderId)),
gateway.SPToJids(toJids),
)
}
@ -774,6 +777,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []
gateway.SPMUCAffiliation(myAffiliation),
gateway.SPMUCRole(myRole),
gateway.SPMUCJid(myJid),
gateway.SPOccupantId(myOccupantId),
gateway.SPMUCStatusCodes(selfStatusCodes),
gateway.SPToJids(toJids),
)
@ -819,7 +823,7 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s
return false
}
nickname := c.GetMUCNickname(memberID)
nickname := c.getMUCNicknameLocked(memberID, mucState)
_, toJids := c.getMUCJoinedJIDs(mucID, mucState, false)
@ -830,6 +834,7 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role),
gateway.SPMUCJid(gateway.CHATJID(memberID, true)),
gateway.SPOccupantId(gateway.CHATNODE(memberID)),
gateway.SPToJids(toJids),
)
@ -857,7 +862,7 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) {
if err == nil {
gateway.SendSubjectMessage(
toJid,
mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(pin)),
mucJid + "/" + c.GetMUCNickname(chatID, c.getMessageSenderId(pin)),
c.messageToText(pin, false),
strconv.FormatInt(pin.Id, 10),
c.xmpp,
@ -868,8 +873,37 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) {
}
}
// GetMUCNickname generates a unique nickname for a MUC occupant
func (c *Client) GetMUCNickname(chatID int64) string {
// MyId returns the bridged Telegram account's own user id, or 0 if the
// account info isn't loaded yet.
func (c *Client) MyId() int64 {
if c.me == nil {
return 0
}
return c.me.Id
}
// resourcePrepBMP runs XMPP resourceprep and additionally rejects non-BMP
// characters, returning ok=false if either check fails.
func resourcePrepBMP(s string) (string, bool) {
rp, err := gateway.ResourcePrep(s)
if err != nil {
return "", false
}
for _, r := range rp {
if r > bmpCeil {
return "", false
}
}
return rp, true
}
// buildMUCNickname formats chatID (a Telegram user or chat id) for display
// as a MUC nickname. Normally a username-less user's nickname is always
// disambiguated with a trailing "(id)" so nicknames can never collide.
// When collides is non-nil (see GetMUCNickname), that disambiguator is
// dropped in favor of the bare name, unless collides reports that the
// bare name is already claimed by a different member.
func (c *Client) buildMUCNickname(chatID int64, collides func(name string) bool) string {
if chatID == 0 {
if c.me != nil {
chatID = c.me.Id
@ -877,22 +911,49 @@ func (c *Client) GetMUCNickname(chatID int64) string {
return "me"
}
}
fc := c.FormatContact(chatID)
rp, err := gateway.ResourcePrep(fc)
if err == nil {
// additionally check for non-BMP characters
for _, r := range rp {
if r > bmpCeil {
err = errors.New("Non-BMP character")
break
chat, user, err := c.GetContactByID(chatID, nil, false)
var fc string
switch {
case err != nil:
fc = "unknown contact: " + err.Error()
// user must be checked before chat: GetContactByID resolves a non-nil
// chat (the private-chat wrapper) for any positive user id too, not
// just actual groups - checking chat first would always shadow user.
case user != nil:
var usernames string
if user.Usernames != nil {
usernames = c.usernamesToString(user.Usernames.ActiveUsernames)
}
name := spaceRegex.ReplaceAllString(fmt.Sprintf("%s %s", user.FirstName, user.LastName), " ")
if collides != nil && usernames == "" {
if rp, ok := resourcePrepBMP(name); ok && !collides(rp) {
return rp
}
}
idOrUsername := usernames
if idOrUsername == "" {
idOrUsername = strconv.FormatInt(chatID, 10)
}
fc = fmt.Sprintf("%s (%v)", name, idOrUsername)
case chat != nil:
title := spaceRegex.ReplaceAllString(chat.Title, " ")
if collides != nil {
if rp, ok := resourcePrepBMP(title); ok && !collides(rp) {
return rp
}
}
fc = fmt.Sprintf("%s (%v)", chat.Title, chat.Id)
default:
fc = strconv.FormatInt(chatID, 10)
}
if err != nil {
fc = spaceRegex.ReplaceAllString(fc, " ")
rp, ok := resourcePrepBMP(fc)
if !ok {
log.Warnf("Resourceprep for %v failed, falling back to chat ID", fc)
var usernames string
_, user, _ := c.GetContactByID(chatID, nil, false)
if user != nil && user.Usernames != nil {
usernames = c.usernamesToString(user.Usernames.ActiveUsernames)
}
@ -911,7 +972,45 @@ func (c *Client) GetMUCNickname(chatID int64) string {
return rp
}
func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
// getMUCNicknameLocked computes the nickname to register for memberId as
// an occupant of mucState's room, dropping the "(id)" disambiguator
// unless the bare name is already taken by someone else. mucState may be
// nil (no room context, e.g. a PM), in which case this degrades to the
// plain always-disambiguated buildMUCNickname formatting.
// achtung: assuming a locked mucState context
func (c *Client) getMUCNicknameLocked(memberId int64, mucState *MUCState) string {
// buildMUCNickname resolves a 0 memberId to c.me.Id internally (the
// "self" convention GetMUCNickname(chatId, 0) also relies on) - the
// collision check below needs that same resolved id, or self's own
// registered nickname would look like a collision with someone else's
// id 0.
resolvedId := memberId
if resolvedId == 0 && c.me != nil {
resolvedId = c.me.Id
}
var collides func(string) bool
if mucState != nil {
collides = func(name string) bool {
id, ok := mucState.Occupants.GetIdByNickname(name)
return ok && id != resolvedId
}
}
return c.buildMUCNickname(memberId, collides)
}
// GetMUCNickname is getMUCNicknameLocked for callers that don't already
// hold c.locks.mucCacheLock. chatId 0 (no room context) falls back to the
// plain always-disambiguated nickname.
func (c *Client) GetMUCNickname(chatId, memberId int64) string {
c.locks.mucCacheLock.Lock()
mucState := c.mucCache[chatId]
nickname := c.getMUCNicknameLocked(memberId, mucState)
c.locks.mucCacheLock.Unlock()
return nickname
}
func (c *Client) updateMUCsNickname(memberID int64) {
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
@ -919,6 +1018,11 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
for mucId, state := range c.mucCache {
oldOccupant, ok := state.Occupants.Get(memberID)
if ok {
newNickname := c.getMUCNicknameLocked(memberID, state)
if newNickname == oldOccupant.Nickname {
continue
}
c.DelayedStatusesLock.Lock()
delete(c.DelayedStatuses, mucId)
c.DelayedStatusesLock.Unlock()
@ -949,6 +1053,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
gateway.SPMUCNick(newNickname),
gateway.SPMUCStatusCodes(unavailableStatusCodes),
gateway.SPMUCJid(realJid),
gateway.SPOccupantId(gateway.CHATNODE(memberID)),
gateway.SPToJids(toJids),
)
c.sendPresence(
@ -959,6 +1064,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
gateway.SPMUCRole(oldOccupant.Role),
gateway.SPMUCStatusCodes(availableStatusCodes),
gateway.SPMUCJid(realJid),
gateway.SPOccupantId(gateway.CHATNODE(memberID)),
gateway.SPToJids(toJids),
)
}
@ -1157,7 +1263,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten
replyTo.Quote.Text.Text,
replyTo.Quote.Text.Entities,
c.getFormatter(),
c,
mucMentionRetriever{c, replyTo.ChatId},
)
// make the whole quote fit one line
text = strings.ReplaceAll(text, "\n", " ")
@ -1187,10 +1293,11 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten
Id: replyId,
}
} else if !noContent {
// it's safe to assume there's no need to pass ChatId here
// as it's needed only for pin messages which are not allowed in replies
// replyTo.ChatId, not message.ChatId: this quotes a message
// from another chat, and its mentions should resolve against
// that chat's own occupants, not the replying message's.
if text == "" && replyTo.Content != nil {
text = c.messageContentToText(replyTo.Content, 0, preview)
text = c.messageContentToText(replyTo.Content, replyTo.ChatId, preview)
}
if text == "" {
@ -1544,7 +1651,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
photo.Caption.Text,
photo.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageAudio:
@ -1556,7 +1663,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
audio.Caption.Text,
audio.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageVideo:
@ -1568,7 +1675,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
video.Caption.Text,
video.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageDocument:
@ -1580,7 +1687,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
document.Caption.Text,
document.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageText:
@ -1592,7 +1699,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
text.Text.Text,
text.Text.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageVoiceNote:
@ -1604,7 +1711,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
voice.Caption.Text,
voice.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageVideoNote:
@ -1618,7 +1725,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6
animation.Caption.Text,
animation.Caption.Entities,
markupMode,
c,
mucMentionRetriever{c, chatId},
)
}
case client.TypeMessageContact:
@ -1909,7 +2016,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
switch message.Content.MessageContentType() {
case client.TypeMessageChatJoinByLink:
c.mucOccupantRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(senderId))
c.mucOccupantRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(chatId, senderId))
case client.TypeMessageChatAddMembers:
addMembers, _ := message.Content.(*client.MessageChatAddMembers)
for _, memberId := range addMembers.MemberUserIds {
@ -1918,11 +2025,11 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
gateway.InviteToMUC(chatId, jid, c.xmpp)
}
}
c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId))
c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(chatId, memberId))
}
case client.TypeMessageChatDeleteMember:
deleteMember, _ := message.Content.(*client.MessageChatDeleteMember)
c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId))
c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(chatId, deleteMember.UserId))
case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate, client.TypeMessageChatUpgradeTo:
inviteChatId := chatId
if message.Content.MessageContentType() == client.TypeMessageChatUpgradeTo {
@ -1938,7 +2045,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
safeToSend = c.assureMUCOccupant(chatId, senderId, message.SenderId, chat)
groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId)
groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(chatId, senderId)
var ok bool
ok, groupChatTos = c.getMUCJoinedJIDs(chatId, nil, true)
if !ok {
@ -1960,6 +2067,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
var jids []string
var isGroupchat bool
var originalFrom string
senderId := c.getMessageSenderId(message)
if len(groupChatTos) == 0 {
isCarbon = c.isCarbonsEnabled() && message.IsOutgoing
jids = c.GetCarbonFullJids(isCarbon, "", true)
@ -1967,7 +2075,6 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
isGroupchat = true
jids = groupChatTos
senderId := c.getMessageSenderId(message)
if senderId != 0 {
originalFrom = gateway.CHATJID(senderId, true)
}
@ -2189,12 +2296,18 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
}
}
var occupantId string
if isGroupchat && senderId != 0 {
occupantId = gateway.CHATNODE(senderId)
}
for _, jid := range jids {
commonArgs := []args.V{
gateway.SMReply(reply), gateway.SMTimestamp(timestamp), gateway.SMIsCarbon(isCarbon),
gateway.SMIsGroupchat(isGroupchat), gateway.SMRequestReceipt(c.Session.Receipts),
gateway.SMOriginalFrom(originalFrom), gateway.SMMamQueryId(mamQueryId),
gateway.SMMucJID(mucJID), gateway.SMMucUserItem(mucUserItem),
gateway.SMOccupantId(occupantId),
}
if omemoFailed {
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
@ -2237,7 +2350,7 @@ func (c *Client) SendDelayedMUCMessage(chatId int64, message *client.Message, to
message,
msgId,
true,
gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(c.getMessageSenderId(message)),
gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(chatId, c.getMessageSenderId(message)),
[]string{toJid},
mamQueryId,
)
@ -2868,7 +2981,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
fullInfo.Bio.Text,
fullInfo.Bio.Entities,
c.getFormatter(),
c,
mucMentionRetriever{c, 0},
)
} else if fullInfo.BotInfo != nil {
if fullInfo.BotInfo.ShortDescription != "" {
@ -3266,8 +3379,21 @@ func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMember
// TgMemberToMUCMember resolves useful data to generate a MUC occupant
func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember, chat *client.Chat) (senderId int64, nickname, affiliation, role string) {
var mucState *MUCState
if chat != nil {
c.locks.mucCacheLock.Lock()
mucState = c.mucCache[chat.Id]
defer c.locks.mucCacheLock.Unlock()
}
return c.tgMemberToMUCOccupantLocked(member, chat, mucState)
}
// tgMemberToMUCOccupantLocked is TgMemberToMUCOccupant for callers that
// already hold c.locks.mucCacheLock (mirrors updateMUCOccupants' locking
// convention).
func (c *Client) tgMemberToMUCOccupantLocked(member *client.ChatMember, chat *client.Chat, mucState *MUCState) (senderId int64, nickname, affiliation, role string) {
senderId = c.GetSenderId(member.MemberId)
nickname = c.GetMUCNickname(senderId)
nickname = c.getMUCNicknameLocked(senderId, mucState)
affiliation, role = c.memberStatusToAffiliationAndRole(member.Status, chat)
return
}
@ -3299,12 +3425,14 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p
var originalFrom string
var from string
var occupantId string
if isMUC {
senderId := c.getMessageSenderId(message)
if senderId != 0 {
originalFrom = gateway.CHATJID(senderId, true)
occupantId = gateway.CHATNODE(senderId)
}
from = mucJid + "/" + c.GetMUCNickname(senderId)
from = mucJid + "/" + c.GetMUCNickname(chatID, senderId)
} else {
from = sChatId
}
@ -3349,7 +3477,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p
gateway.SendMessage(to, from, c.xmpp,
gateway.SMBody(body), gateway.SMId(sId),
gateway.SMReply(reply), gateway.SMIsGroupchat(isMUC), gateway.SMOriginalFrom(originalFrom),
gateway.SMOMEMOEnvelope(envelope),
gateway.SMOMEMOEnvelope(envelope), gateway.SMOccupantId(occupantId),
)
}
} else {
@ -3518,17 +3646,19 @@ func (c *Client) leaveChat(chatID int64) error {
// achtung: assuming a locked mucState context
func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, mucState *MUCState) error {
var myJid string
var myJid, myOccupantId string
if c.me != nil {
myJid = gateway.CHATJID(c.me.Id, true)
myOccupantId = gateway.CHATNODE(c.me.Id)
}
_, toJids := c.getMUCJoinedJIDs(chatID, mucState, mucState == nil)
args := []args.V{
gateway.SPFrom(gateway.MUCNODE(chatID)),
gateway.SPResource(c.GetMUCNickname(0)),
gateway.SPResource(c.getMUCNicknameLocked(0, mucState)),
gateway.SPMUCAffiliation("none"),
gateway.SPMUCRole("none"),
gateway.SPMUCJid(myJid),
gateway.SPOccupantId(myOccupantId),
gateway.SPMUCStatusCodes(statusCodes),
gateway.SPToJids(toJids),
}
@ -3562,6 +3692,7 @@ func (c *Client) kickStaleOccupant(chatID int64, deleted *MUCOccupant, mucState
gateway.SPMUCAffiliation(deleted.Affiliation),
gateway.SPMUCRole(deleted.Role),
gateway.SPMUCStatusCodes([]uint16{307, 333}),
gateway.SPOccupantId(gateway.CHATNODE(deleted.id)),
)
}
@ -3719,6 +3850,7 @@ func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMember
gateway.SPResource(nickname),
gateway.SPImmed(true),
gateway.SPMUCJid(gateway.CHATJID(userID, true)),
gateway.SPOccupantId(gateway.CHATNODE(userID)),
gateway.SPToJids(toJids),
}
@ -3827,16 +3959,29 @@ func (c *Client) getChatMemberStatus(status client.ChatMemberStatus) ChatMemberS
return ChatMemberStatusIllegal
}
// GetMUCNicknameByUsername implement the MentionRetriever interface for message formatters
func (c *Client) GetMUCNicknameByUsername(username string) (string, error) {
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
// mucMentionRetriever implements formatter.MentionRetriever bound to a
// single chat, so an @mention inside one of that chat's messages renders
// with the same nickname shown in its occupant list (see GetMUCNickname).
// chatId 0 (no particular room in scope) falls back to the plain
// always-disambiguated nickname.
type mucMentionRetriever struct {
c *Client
chatId int64
}
func (m mucMentionRetriever) GetMUCNickname(id int64) string {
return m.c.GetMUCNickname(m.chatId, id)
}
func (m mucMentionRetriever) GetMUCNicknameByUsername(username string) (string, error) {
chat, err := m.c.client.SearchPublicChat(&client.SearchPublicChatRequest{
Username: username,
})
if err != nil {
return "", err
}
return c.GetMUCNickname(chat.Id), nil
return m.c.GetMUCNickname(m.chatId, chat.Id), nil
}
// FindMessageByTime retrieves the closest message before the given timestamp

View file

@ -333,6 +333,12 @@ type MessageOriginId struct {
Id string `xml:"id,attr"`
}
// OccupantId is from XEP-0421, carried on both MUC messages and presence
type OccupantId struct {
XMLName xml.Name `xml:"urn:xmpp:occupant-id:0 occupant-id"`
Id string `xml:"id,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"`
@ -889,6 +895,18 @@ func init() {
"origin-id",
}, MessageOriginId{})
// occupant id (message)
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:occupant-id:0",
"occupant-id",
}, OccupantId{})
// occupant id (presence)
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{
"urn:xmpp:occupant-id:0",
"occupant-id",
}, OccupantId{})
// muc admin query
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"http://jabber.org/protocol/muc#admin",

View file

@ -72,6 +72,9 @@ const NSCaps string = "http://jabber.org/protocol/caps"
// NSReactions is the XEP-0444 message reactions namespace
const NSReactions string = "urn:xmpp:reactions:0"
// NSOccupantId is the XEP-0421 occupant id namespace
const NSOccupantId string = "urn:xmpp:occupant-id:0"
// CapsNode is this software's advertised XEP-0115 node URI
const CapsNode string = "https://dev.narayana.im/narayana/telegabber/"
@ -281,28 +284,11 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker
// SendReactionMessage creates and sends a message stanza with a XEP-0444
// reaction set (see Reactions); an empty reactionEmojis clears id's reactions.
func SendReactionMessage(to string, from string, component *xmpp.Component, id string, reactionEmojis []string, isGroupchat bool) {
sendMessageWrapper(to, from, component, SMReactions(&Reactions{
func SendReactionMessage(to string, from string, component *xmpp.Component, id string, reactionEmojis []string, args ...args.V) {
sendMessageWrapper(to, from, component, append(args, SMReactions(&Reactions{
Id: id,
Reactions: reactionEmojis,
}), SMIsGroupchat(isGroupchat))
}
// SendMAMReactionMessage is SendReactionMessage wrapped as a XEP-0313 MAM
// result: entryId is this result's own id (distinct from id, the
// reacted-to message's own id); timestamp stands in for a historical
// reaction timestamp, which doesn't exist (only a reaction's current
// state, not when it was added).
func SendMAMReactionMessage(to string, from string, component *xmpp.Component, id string, reactionEmojis []string, entryId string, timestamp int64, mucJID string, mamQueryId string) {
sendMessageWrapper(to, from, component,
SMReactions(&Reactions{Id: id, Reactions: reactionEmojis}),
SMId(entryId),
SMStanzaId(entryId),
SMTimestamp(timestamp),
SMMamQueryId(mamQueryId),
SMMucJID(mucJID),
SMIsGroupchat(true),
)
}))...)
}
// SendMUCInvite creates and send a MUC invitation message
@ -366,6 +352,9 @@ var SMInviteFrom = args.NewString()
// SMStanzaId is a XEP-0359 stanza id
var SMStanzaId = args.NewString()
// SMOccupantId is a XEP-0421 occupant id (a stable identifier for a MUC occupant)
var SMOccupantId = args.NewString()
// SMStatusCode is a XEP-0045 muc#user status code
var SMStatusCode = args.NewInt64()
@ -404,6 +393,7 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
errorCode := SMErrorCode.Get(args)
inviteFrom := SMInviteFrom.Get(args)
stanzaId := SMStanzaId.Get(args)
occupantId := SMOccupantId.Get(args)
statusCode := SMStatusCode.Get(args)
mamQueryId := SMMamQueryId.Get(args)
mucJID := SMMucJID.Get(args)
@ -619,6 +609,11 @@ func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args
})
}
}
if occupantId != "" {
message.Extensions = append(message.Extensions, extensions.OccupantId{
Id: occupantId,
})
}
if isCarbon {
carbonMessage := extensions.ClientMessage{
@ -807,6 +802,9 @@ var SPMUCNick = args.NewString()
// SPMUCJid is a real jid of a MUC member
var SPMUCJid = args.NewString()
// SPOccupantId is a XEP-0421 occupant id (a stable identifier for a MUC occupant)
var SPOccupantId = args.NewString()
// SPMUCStatusCodes is a set of XEP-0045 MUC status codes
var SPMUCStatusCodes = args.New()
@ -883,6 +881,14 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
})
}
}
if SPOccupantId.IsSet(args) {
occupantId := SPOccupantId.Get(args)
if occupantId != "" {
presence.Extensions = append(presence.Extensions, extensions.OccupantId{
Id: occupantId,
})
}
}
if SPMUCAffiliation.IsSet(args) {
affiliation := SPMUCAffiliation.Get(args)
if affiliation != "" {

View file

@ -298,13 +298,17 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
var replyId int64
text := msg.Body
// OMEMO decrypt hook - personal-chat pseudo-JIDs only (true
// MUC/XEP-0045 groupchat OMEMO is out of scope entirely, not
// deferred - see the plan doc). A successful decrypt is itself
// the auto-upgrade signal for this chat (enablement trigger 2);
// the account-wide config toggle (trigger 1) is a separate,
// not-yet-wired-up piece (M4).
if !isGroupchat {
// OMEMO decrypt hook - personal-chat pseudo-JIDs only, XEP-0045
// groupchat OMEMO is out of scope entirely. A successful
// decrypt marks the chat OMEMO-active. Groupchat OMEMO is
// bounced rather than silently dropped, so the sender gets a
// clear signal instead of treating it as delivered.
if isGroupchat {
if hasOMEMOPayload(msg) {
gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "OMEMO encryption is not supported in this room", msg.Id, 406, true, component)
return
}
} else {
if backend, backendOk := gateway.E2EE.Backend(); backendOk {
env, hasEnv, decErr := decodeOMEMOEnvelope(msg)
if decErr != nil {
@ -444,14 +448,14 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
tgMessage,
msg.Id,
false,
msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)),
msg.To + "/" + session.GetMUCNickname(toID, session.GetSenderId(tgMessage.SenderId)),
[]string{msg.From},
"",
)
}
} else if isCommand && isGroupchat && session.Session.MUC {
// pong outgoing commands back to groupchats
gateway.SendMessage(msg.From, msg.To+"/"+session.GetMUCNickname(0), component, gateway.SMBody(text), gateway.SMIsGroupchat(isGroupchat))
gateway.SendMessage(msg.From, msg.To+"/"+session.GetMUCNickname(toID, 0), component, gateway.SMBody(text), gateway.SMIsGroupchat(isGroupchat), gateway.SMOccupantId(gateway.CHATNODE(session.MyId())))
} else {
/*
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
@ -903,6 +907,7 @@ func handleMUCUnavailable(component *xmpp.Component, p stanza.Presence, session
gateway.SPMUCAffiliation("member"),
gateway.SPMUCRole("none"),
gateway.SPMUCJid(p.From),
gateway.SPOccupantId(gateway.CHATNODE(session.MyId())),
gateway.SPMUCStatusCodes([]uint16{110}),
)
}
@ -1129,7 +1134,6 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
"http://jabber.org/protocol/muc",
"muc_persistent",
"muc_hidden",
"muc_membersonly",
"muc_moderated",
"muc_nonanonymous",
"muc_unsecured",
@ -1142,6 +1146,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
gateway.NSReactions,
"urn:xmpp:sid:0",
"vcard-temp",
gateway.NSOccupantId,
)
fields := []*stanza.Field{
&stanza.Field{
@ -1215,7 +1220,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
chat, _, err := session.GetContactByID(toID, nil, true)
if err == nil && session.IsGroup(chat) {
disco.SetNode(di.Node)
disco.AddIdentity(session.GetMUCNickname(0), "conference", "text")
disco.AddIdentity(session.GetMUCNickname(toID, 0), "conference", "text")
}
}
}
@ -1340,7 +1345,7 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
}
defer gateway.ResumableSend(component, answer)
_, toOk, toIsGroup := toToID(iq.To)
toID, toOk, toIsGroup := toToID(iq.To)
bare, _, ok := gateway.SplitJID(iq.From)
var session *telegram.Client
@ -1353,7 +1358,7 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
if toIsGroup {
nickname := "me"
if sessionOk {
nickname = session.GetMUCNickname(0)
nickname = session.GetMUCNickname(toID, 0)
}
answer.Payload = extensions.QueryRegister{
Instructions: "MUC username is static",
@ -2317,7 +2322,7 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
return
}
nick := session.GetMUCNickname(userID)
nick := session.GetMUCNickname(toID, userID)
var status telegram.ChatMemberStatus