diff --git a/telegram/client.go b/telegram/client.go index df983eb..c6acde1 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -31,6 +31,7 @@ type MUCState struct { type MUCMember struct { Nickname string Affiliation string + Role string } func NewMUCState() *MUCState { diff --git a/telegram/commands.go b/telegram/commands.go index f96598e..b43f2f8 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -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 } diff --git a/telegram/utils.go b/telegram/utils.go index dd919ee..f437fc9 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -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 +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 64dfb3b..d8aafd7 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -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! diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 2bddf83..604a1eb 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -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" diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c0cf696..76ed019 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -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,