Reflect MUC member affiliation/role changes in Telegram

This commit is contained in:
Bohdan Horbeshko 2025-06-14 13:20:02 -04:00
parent 26eafbba3d
commit ebdf180ce5
6 changed files with 234 additions and 102 deletions

View file

@ -31,6 +31,7 @@ type MUCState struct {
type MUCMember struct {
Nickname string
Affiliation string
Role string
}
func NewMUCState() *MUCState {

View file

@ -855,7 +855,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Contact not found", true, false
}
err = c.Kick(chatID, contact.Id, "")
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusKicked, 0, "", "")
if err != nil {
return err.Error(), true, false
}
@ -878,15 +878,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
}
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: c.formatBantime(hours),
Permissions: &permissionsReadonly,
},
})
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusMuted, hours, "", "")
if err != nil {
return err.Error(), true, false
}
@ -907,15 +899,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Contact not found", true, false
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: 0,
Permissions: &permissionsMember,
},
})
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnmuted, 0, "", "")
if err != nil {
return err.Error(), true, false
}
@ -943,13 +927,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
}
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusBanned{
BannedUntilDate: c.formatBantime(hours),
},
})
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusBanned, hours, "", "")
if err != nil {
return err.Error(), true, false
}
@ -963,11 +941,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Contact not found", true, false
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusMember{},
})
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnbanned, 0, "", "")
if err != nil {
return err.Error(), true, false
}
@ -981,21 +955,13 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Contact not found", true, false
}
// clone the permissions
status := client.ChatMemberStatusAdministrator{
CanBeEdited: true,
Rights: &permissionsAdmin,
}
var customTitle string
if len(args) > 1 {
status.CustomTitle = args[1]
customTitle = args[1]
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &status,
})
// clone the permissions
err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusPromoted, 0, customTitle, "")
if err != nil {
return err.Error(), true, false
}

View file

@ -131,6 +131,17 @@ func NewMessageLimitSince(since int64) *MessageLimit {
const AVATAR_SIZE_LIMIT int64 = 128 * 1024
const (
ChatMemberStatusIllegal = iota
ChatMemberStatusKicked
ChatMemberStatusMuted
ChatMemberStatusUnmuted
ChatMemberStatusBanned
ChatMemberStatusUnbanned
ChatMemberStatusPromoted
)
type ChatMemberStatus int
// 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() {
@ -512,6 +523,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
gateway.SPFrom(gateway.MUCNODE(mucId)),
gateway.SPResource(member.Nickname),
gateway.SPMUCAffiliation(member.Affiliation),
gateway.SPMUCRole(member.Role),
gateway.SPMUCJid(chatJid),
)
err := c.sendPresence(newMucArgs...)
@ -597,6 +609,7 @@ func (c *Client) sendMUCStatuses(chatID int64) {
myNickname = c.getFullName(c.me)
}
myAffiliation := "member"
myRole := "participant"
members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{
ChatId: chatID,
@ -616,15 +629,17 @@ func (c *Client) sendMUCStatuses(chatID int64) {
}
nickname := c.GetMUCNickname(senderId)
affiliation := c.memberStatusToAffiliation(member.Status)
affiliation, role := c.memberStatusToAffiliationAndRole(member.Status)
mucState.Members[senderId] = &MUCMember{
Nickname: nickname,
Affiliation: affiliation,
Role: role,
}
if c.me != nil && senderId == c.me.Id {
myNickname = nickname
myAffiliation = affiliation
myRole = role
continue
}
@ -633,6 +648,7 @@ func (c *Client) sendMUCStatuses(chatID int64) {
gateway.SPResource(nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(affiliation),
gateway.SPMUCJid(gateway.CHATJID(senderId, true)),
)
}
@ -644,6 +660,7 @@ func (c *Client) sendMUCStatuses(chatID int64) {
gateway.SPResource(myNickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(myAffiliation),
gateway.SPMUCRole(myRole),
gateway.SPMUCStatusCodes([]uint16{100, 110, 210}),
)
}
@ -660,7 +677,7 @@ func (c *Client) mucCacheHasMember(mucID int64, memberID int64) bool {
return ok
}
func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) bool {
func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation, role string) bool {
c.locks.mucCacheLock.Lock()
defer c.locks.mucCacheLock.Unlock()
mucState, ok := c.mucCache[mucID]
@ -675,6 +692,7 @@ func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) b
gateway.SPResource(nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role),
gateway.SPMUCJid(gateway.CHATJID(memberID, true)),
)
@ -682,6 +700,7 @@ func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) b
mucState.Members[memberID] = &MUCMember{
Nickname: nickname,
Affiliation: affiliation,
Role: role,
}
return true
}
@ -747,6 +766,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
gateway.SPResource(oldMember.Nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(oldMember.Affiliation),
gateway.SPMUCRole(oldMember.Role),
gateway.SPMUCNick(newNickname),
gateway.SPMUCStatusCodes(unavailableStatusCodes),
gateway.SPMUCJid(realJid),
@ -756,6 +776,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
gateway.SPResource(newNickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation(oldMember.Affiliation),
gateway.SPMUCRole(oldMember.Role),
gateway.SPMUCStatusCodes(availableStatusCodes),
gateway.SPMUCJid(realJid),
)
@ -1618,7 +1639,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
if c.Session.MUC && c.IsGroup(chat) {
if message.Content.MessageContentType() == client.TypeMessageChatDeleteMember {
deleteMember, _ := message.Content.(*client.MessageChatDeleteMember)
c.kickMemberFromMUC(chatId, deleteMember.UserId, c.GetMUCNickname(deleteMember.UserId))
c.mucMemberRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId))
}
senderId := c.getMessageSenderId(message)
@ -1636,7 +1657,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
if err == nil {
status = chatMember.Status
}
safeToSend = c.addMUCMember(chatId, senderId, c.memberStatusToAffiliation(status))
affiliation, role := c.memberStatusToAffiliationAndRole(status)
safeToSend = c.addMUCMember(chatId, senderId, affiliation, role)
}
groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId)
@ -2441,24 +2463,28 @@ func (c *Client) usernamesToString(usernames []string) string {
return strings.Join(atUsernames, ", ")
}
func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) string {
func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMemberStatus) (string, string) {
if memberStatus != nil {
switch memberStatus.ChatMemberStatusType() {
case client.TypeChatMemberStatusCreator:
return "owner"
return "owner", "moderator"
case client.TypeChatMemberStatusAdministrator:
return "admin"
return "admin", "moderator"
case client.TypeChatMemberStatusMember:
return "member"
return "member", "participant"
case client.TypeChatMemberStatusRestricted:
return "outcast"
restricted, _ := memberStatus.(*client.ChatMemberStatusRestricted)
if restricted.Permissions != nil && !restricted.Permissions.CanSendBasicMessages {
return "member", "visitor"
}
return "member", "participant"
case client.TypeChatMemberStatusLeft:
return "none"
return "none", "none"
case client.TypeChatMemberStatusBanned:
return "outcast"
return "outcast", "none"
}
}
return "member"
return "member", "participant"
}
func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) {
@ -2598,38 +2624,114 @@ func (c *Client) MigrateToMUCs() {
}
}
// Kick kicks
func (c *Client) Kick(chatID, userID int64, nickname string) error {
// SetChatMemberStatus is a handy wrapper for the following TDLib method
func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStatus, numericPayload int64, stringPayload, nickname string) error {
var chatMemberStatus client.ChatMemberStatus
switch status {
case ChatMemberStatusKicked:
chatMemberStatus = &client.ChatMemberStatusLeft{}
case ChatMemberStatusMuted:
chatMemberStatus = &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: c.formatBantime(numericPayload),
Permissions: &permissionsReadonly,
}
case ChatMemberStatusUnmuted:
chatMemberStatus = &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: 0,
Permissions: &permissionsMember,
}
case ChatMemberStatusBanned:
chatMemberStatus = &client.ChatMemberStatusBanned{
BannedUntilDate: c.formatBantime(numericPayload),
}
case ChatMemberStatusUnbanned:
chatMemberStatus = &client.ChatMemberStatusMember{}
case ChatMemberStatusPromoted:
chatMemberStatus = &client.ChatMemberStatusAdministrator{
CanBeEdited: true,
Rights: &permissionsAdmin,
CustomTitle: stringPayload,
}
}
_, err := c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: userID},
Status: &client.ChatMemberStatusLeft{},
Status: chatMemberStatus,
})
if err != nil && nickname != "" {
c.kickMemberFromMUC(chatID, userID, nickname)
if err == nil && nickname != "" {
c.mucMemberRolePresence(chatID, userID, status, nickname)
}
return err
}
func (c *Client) kickMemberFromMUC(chatID, userID int64, nickname string) {
unavailableStatusCodes := []uint16{307}
if c.me != nil && userID == c.me.Id {
unavailableStatusCodes = append(unavailableStatusCodes, 110)
}
c.sendPresence(
gateway.SPType("unavailable"),
func (c *Client) mucMemberRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) {
args := []args.V{
gateway.SPFrom(gateway.MUCNODE(chatID)),
gateway.SPResource(nickname),
gateway.SPImmed(true),
gateway.SPMUCAffiliation("none"),
gateway.SPMUCStatusCodes(unavailableStatusCodes),
gateway.SPMUCJid(gateway.CHATJID(userID, true)),
}
var statusCodes []uint16
var newAffiliation, newRole string
switch status {
case ChatMemberStatusKicked:
args = append(args, gateway.SPType("unavailable"))
newAffiliation = "none"
newRole = "none"
statusCodes = append(statusCodes, 307)
if c.me != nil && userID == c.me.Id {
statusCodes = append(statusCodes, 110)
}
case ChatMemberStatusMuted:
newAffiliation = "member"
newRole = "visitor"
case ChatMemberStatusUnmuted, ChatMemberStatusUnbanned:
newAffiliation = "member"
newRole = "participant"
case ChatMemberStatusBanned:
args = append(args, gateway.SPType("unavailable"))
newAffiliation = "outcast"
newRole = "none"
statusCodes = append(statusCodes, 301)
case ChatMemberStatusPromoted:
newAffiliation = "admin"
newRole = "moderator"
}
args = append(
args,
gateway.SPMUCAffiliation(newAffiliation),
gateway.SPMUCRole(newRole),
gateway.SPMUCStatusCodes(statusCodes),
)
c.sendPresence(args...)
c.locks.mucCacheLock.Lock()
mucState, ok := c.mucCache[chatID]
if ok && mucState != nil {
delete(mucState.Members, userID)
if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned {
delete(mucState.Members, userID)
} else {
member, ok := mucState.Members[userID]
if ok {
member.Affiliation = newAffiliation
member.Role = newRole
}
}
}
c.locks.mucCacheLock.Unlock()
}
// GetErrorCode obtains an error code from a Telegram response error
func GetErrorCode(err error) (int32, bool) {
responseError, ok := err.(client.ResponseError)
if !ok || responseError.Err == nil {
return 0, false
}
return responseError.Err.Code, true
}

View file

@ -316,10 +316,12 @@ type QueryMucAdmin struct {
// QueryMucAdminItem is a child element from XEP-0045
type QueryMucAdminItem struct {
XMLName xml.Name `xml:"item"`
Nick string `xml:"nick,attr"`
Role string `xml:"role,attr"`
Reason string `xml:"reason,omitempty"`
XMLName xml.Name `xml:"item"`
Jid string `xml:"jid,attr,omitempty"`
Nick string `xml:"nick,attr,omitempty"`
Role string `xml:"role,attr,omitempty"`
Affiliation string `xml:"affiliation,attr,omitempty"`
Reason string `xml:"reason,omitempty"`
}
// Namespace is a namespace!

View file

@ -438,6 +438,9 @@ var SPImmed = args.NewBool(args.Default(true))
// SPMUCAffiliation is a XEP-0045 MUC affiliation
var SPMUCAffiliation = args.NewString()
// SPMUCRole is a XEP-0045 MUC role
var SPMUCRole = args.NewString()
// SPMUCNick is a XEP-0045 MUC user nick
var SPMUCNick = args.NewString()
@ -505,10 +508,16 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
if SPMUCAffiliation.IsSet(args) {
affiliation := SPMUCAffiliation.Get(args)
if affiliation != "" {
var role string
if SPMUCRole.IsSet(args) {
role = SPMUCRole.Get(args)
} else {
role = affiliationToRole(affiliation)
}
userExt := extensions.PresenceXMucUserExtension{
Item: extensions.PresenceXMucUserItem{
Affiliation: affiliation,
Role: affilationToRole(affiliation),
Role: role,
},
}
if SPMUCNick.IsSet(args) {
@ -625,7 +634,7 @@ func SplitJID(from string) (string, string, bool) {
return fromJid.Bare(), fromJid.Resource, true
}
func affilationToRole(affilation string) string {
func affiliationToRole(affilation string) string {
switch affilation {
case "owner", "admin":
return "moderator"

View file

@ -1602,37 +1602,84 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
}
defer gateway.ResumableSend(component, answer)
if query.Item.Role == "none" {
bare, _, fromOk := gateway.SplitJID(iq.From)
if !fromOk {
iqAnswerSetError(answer, 400)
return
}
bare, _, fromOk := gateway.SplitJID(iq.From)
if !fromOk {
iqAnswerSetError(answer, 400)
return
}
session, sessionOk := sessions[bare]
if !sessionOk || !session.Session.MUC {
iqAnswerSetError(answer, 401)
return
}
session, sessionOk := sessions[bare]
if !sessionOk || !session.Session.MUC {
iqAnswerSetError(answer, 403)
return
}
toID, toOk, toIsGroup := toToID(iq.To)
if !toOk || !toIsGroup {
iqAnswerSetError(answer, 406)
return
}
toID, toOk, toIsGroup := toToID(iq.To)
if !toOk || !toIsGroup {
iqAnswerSetError(answer, 406)
return
}
userID := session.GetMUCMemberIdByNickname(toID, query.Item.Nick)
if userID == 0 {
iqAnswerSetError(answer, 404)
return
}
var userID int64
if query.Item.Jid != "" {
userID, _, _ = toToID(query.Item.Jid)
} else if query.Item.Nick != "" {
userID = session.GetMUCMemberIdByNickname(toID, query.Item.Nick)
}
if userID == 0 {
iqAnswerSetError(answer, 404)
return
}
err := session.Kick(toID, userID, query.Item.Nick)
if err != nil {
iqAnswerSetError(answer, 500)
answer.Error.Text = err.Error()
return
nick := query.Item.Nick
if nick == "" {
nick = session.GetMUCNickname(userID)
}
var status telegram.ChatMemberStatus
var integerPayload int64
var stringPayload string
switch query.Item.Role {
case "none":
status = telegram.ChatMemberStatusKicked
case "visitor":
status = telegram.ChatMemberStatusMuted
case "participant":
status = telegram.ChatMemberStatusUnmuted
case "moderator":
status = telegram.ChatMemberStatusPromoted
}
// affiliations have a higher priority over roles
switch query.Item.Affiliation {
case "none":
status = telegram.ChatMemberStatusKicked
case "outcast":
status = telegram.ChatMemberStatusBanned
case "member":
status = telegram.ChatMemberStatusUnmuted
case "admin":
status = telegram.ChatMemberStatusPromoted
case "owner":
iqAnswerSetError(answer, 403)
return
}
// nothing has been detected
if status == telegram.ChatMemberStatusIllegal {
iqAnswerSetError(answer, 400)
return
}
err := session.SetChatMemberStatus(toID, userID, status, integerPayload, stringPayload, nick)
if err != nil {
code, ok := telegram.GetErrorCode(err)
if !ok {
code = 500
}
iqAnswerSetError(answer, int(code))
answer.Error.Text = err.Error()
return
}
}
@ -1658,6 +1705,11 @@ func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) {
Type: stanza.ErrorTypeAuth,
Reason: "not-authorized",
}
case 403:
answer.Error = &stanza.Err{
Type: stanza.ErrorTypeAuth,
Reason: "forbidden",
}
case 404:
answer.Error = &stanza.Err{
Type: stanza.ErrorTypeCancel,