From 1d165731a2ff37a7232557870b697887746d398a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 31 May 2025 00:10:07 -0400 Subject: [PATCH] Process MUC kicking --- telegram/commands.go | 6 +- telegram/utils.go | 60 ++++++++++++++++++++ xmpp/extensions/extensions.go | 31 ++++++++++ xmpp/handlers.go | 104 +++++++++++++++++++++++++++------- 4 files changed, 175 insertions(+), 26 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index f1554eb..f96598e 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -855,11 +855,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.ChatMemberStatusLeft{}, - }) + err = c.Kick(chatID, contact.Id, "") if err != nil { return err.Error(), true, false } diff --git a/telegram/utils.go b/telegram/utils.go index 29d08d8..00b2380 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -808,6 +808,25 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { return member.Nickname, true } +// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet!) +func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatID] + if !ok || mucState == nil { + return 0 + } + + for memberId, member := range mucState.Members { + if member.Nickname == nickname { + return memberId + } + } + + return 0 +} + // NewPinnedMessage sends a text message and pins it right away func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { c.locks.pinOutboxLock.Lock() @@ -1591,6 +1610,11 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { groupChatFrom := "" groupChatTos := []string{} 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)) + } + senderId := c.getMessageSenderId(message) if senderId == 0 { log.Errorf("Invalid sender id for message %#v", message) @@ -2623,3 +2647,39 @@ func (c *Client) MigrateToMUCs() { } } } + +// Kick kicks +func (c *Client) Kick(chatID, userID int64, nickname string) error { + _, err := c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: userID}, + Status: &client.ChatMemberStatusLeft{}, + }) + if err != nil && nickname != "" { + c.kickMemberFromMUC(chatID, userID, 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"), + gateway.SPFrom(gateway.MUCNODE(chatID)), + gateway.SPResource(nickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCStatusCodes(unavailableStatusCodes), + gateway.SPMUCJid(gateway.CHATJID(userID, true)), + ) + + c.locks.mucCacheLock.Lock() + mucState, ok := c.mucCache[chatID] + if ok && mucState != nil { + delete(mucState.Members, userID) + } + c.locks.mucCacheLock.Unlock() +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 2886e23..64dfb3b 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -307,6 +307,21 @@ type EmptySubject struct { XMLName xml.Name `xml:"subject"` } +// QueryMucAdmin is from XEP-0045 +type QueryMucAdmin struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#admin query"` + Item QueryMucAdminItem `xml:"item"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// 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"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -392,6 +407,16 @@ func (ClientMessage) Name() string { return "message" } +// Namespace is a namespace! +func (c QueryMucAdmin) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c QueryMucAdmin) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // NewReplyFallback initializes a fallback range func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -513,4 +538,10 @@ func init() { "urn:xmpp:sid:0", "origin-id", }, MessageOriginId{}) + + // muc admin query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "http://jabber.org/protocol/muc#admin", + "query", + }, QueryMucAdmin{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a1dee50..d33c5d2 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -72,9 +72,9 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { return } } else if iq.Type == stanza.IQTypeSet { - query, ok := iq.Payload.(*extensions.QueryRegister) + queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { - go handleSetQueryRegister(s, iq, query) + go handleSetQueryRegister(s, iq, queryRegister) return } command, ok := iq.Payload.(*stanza.Command) @@ -82,6 +82,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryCommand(s, iq, command) return } + queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin) + if ok { + go handleSetQueryMucAdmin(s, iq, queryMucAdmin) + return + } } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { @@ -1111,7 +1116,7 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { } } else { query := extensions.QueryRegister{} - iqAnswerSetError(answer, &query, 404) + iqAnswerRegisterSetError(answer, &query, 404) return } } else { @@ -1151,12 +1156,12 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer _, toOk, _ := toToID(iq.To) if toOk { - iqAnswerSetError(answer, query, 400) + iqAnswerRegisterSetError(answer, query, 400) return } if query.Remove != nil { - iqAnswerSetError(answer, query, 405) + iqAnswerRegisterSetError(answer, query, 405) return } @@ -1174,7 +1179,7 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer if !ok { session, ok = getTelegramInstance(bare, &persistence.Session{}, component) if !ok { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) return } } @@ -1182,23 +1187,23 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer err := session.TryLogin(resource, query.Username) if err != nil { if err.Error() == telegram.TelegramAuthDone { - iqAnswerSetError(answer, query, 406) + iqAnswerRegisterSetError(answer, query, 406) } else { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) } return } err = session.SetPhoneNumber(query.Username) if err != nil { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) return } // everything okay, the response should be empty with no payload/error at this point gateway.SubscribeToTransport(component, iq.From) } else { - iqAnswerSetError(answer, query, 406) + iqAnswerRegisterSetError(answer, query, 406) } } @@ -1590,51 +1595,108 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command log.Debugf("command response: %#v %v", answer.Payload, cancelSend) } -func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { - answer.Type = stanza.IQTypeError +func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + if query.Item.Role == "none" { + 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 + } + + 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 + } + + err := session.Kick(toID, userID, query.Item.Nick) + if err != nil { + iqAnswerSetError(answer, 500) + answer.Error.Text = err.Error() + return + } + } +} + +func iqAnswerSetError(answer *stanza.IQ, code int) { + iqAnswerSetErrorInternal(answer, code, false) +} + +func iqAnswerRegisterSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Payload = *payload + iqAnswerSetErrorInternal(answer, code, true) +} + +func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) { + answer.Type = stanza.IQTypeError switch code { case 400: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeModify, Reason: "bad-request", } + case 401: + answer.Error = &stanza.Err{ + Type: stanza.ErrorTypeAuth, + Reason: "not-authorized", + } case 404: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "item-not-found", - Text: "No such room", } case 405: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "not-allowed", - Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", } case 406: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeModify, Reason: "not-acceptable", - Text: "Phone number already provided, chat with the transport for further instruction", } case 500: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "undefined-condition", } } + answer.Error.Code = code + + if registerMode { + switch code { + case 404: + answer.Error.Text = "No such room" + case 405: + answer.Error.Text = "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport" + case 406: + answer.Error.Text = "Phone number already provided, chat with the transport for further instruction" + } + } } func presenceReplySetError(reply *stanza.Presence, code int) {