Process MUC kicking

This commit is contained in:
Bohdan Horbeshko 2025-05-31 00:10:07 -04:00
parent dd8267df2c
commit 1d165731a2
4 changed files with 175 additions and 26 deletions

View file

@ -855,11 +855,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
return "Contact not found", true, false return "Contact not found", true, false
} }
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ err = c.Kick(chatID, contact.Id, "")
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusLeft{},
})
if err != nil { if err != nil {
return err.Error(), true, false return err.Error(), true, false
} }

View file

@ -808,6 +808,25 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) {
return member.Nickname, true 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 // NewPinnedMessage sends a text message and pins it right away
func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool {
c.locks.pinOutboxLock.Lock() c.locks.pinOutboxLock.Lock()
@ -1591,6 +1610,11 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
groupChatFrom := "" groupChatFrom := ""
groupChatTos := []string{} groupChatTos := []string{}
if c.Session.MUC && c.IsGroup(chat) { 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) senderId := c.getMessageSenderId(message)
if senderId == 0 { if senderId == 0 {
log.Errorf("Invalid sender id for message %#v", message) 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()
}

View file

@ -307,6 +307,21 @@ type EmptySubject struct {
XMLName xml.Name `xml:"subject"` 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! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -392,6 +407,16 @@ func (ClientMessage) Name() string {
return "message" 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 // NewReplyFallback initializes a fallback range
func NewReplyFallback(start uint64, end uint64) Fallback { func NewReplyFallback(start uint64, end uint64) Fallback {
return Fallback{ return Fallback{
@ -513,4 +538,10 @@ func init() {
"urn:xmpp:sid:0", "urn:xmpp:sid:0",
"origin-id", "origin-id",
}, MessageOriginId{}) }, MessageOriginId{})
// muc admin query
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"http://jabber.org/protocol/muc#admin",
"query",
}, QueryMucAdmin{})
} }

View file

@ -72,9 +72,9 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
return return
} }
} else if iq.Type == stanza.IQTypeSet { } else if iq.Type == stanza.IQTypeSet {
query, ok := iq.Payload.(*extensions.QueryRegister) queryRegister, ok := iq.Payload.(*extensions.QueryRegister)
if ok { if ok {
go handleSetQueryRegister(s, iq, query) go handleSetQueryRegister(s, iq, queryRegister)
return return
} }
command, ok := iq.Payload.(*stanza.Command) command, ok := iq.Payload.(*stanza.Command)
@ -82,6 +82,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleSetQueryCommand(s, iq, command) go handleSetQueryCommand(s, iq, command)
return return
} }
queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin)
if ok {
go handleSetQueryMucAdmin(s, iq, queryMucAdmin)
return
}
} else if iq.Type == stanza.IQTypeResult { } else if iq.Type == stanza.IQTypeResult {
discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
if ok { if ok {
@ -1111,7 +1116,7 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
} }
} else { } else {
query := extensions.QueryRegister{} query := extensions.QueryRegister{}
iqAnswerSetError(answer, &query, 404) iqAnswerRegisterSetError(answer, &query, 404)
return return
} }
} else { } else {
@ -1151,12 +1156,12 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
_, toOk, _ := toToID(iq.To) _, toOk, _ := toToID(iq.To)
if toOk { if toOk {
iqAnswerSetError(answer, query, 400) iqAnswerRegisterSetError(answer, query, 400)
return return
} }
if query.Remove != nil { if query.Remove != nil {
iqAnswerSetError(answer, query, 405) iqAnswerRegisterSetError(answer, query, 405)
return return
} }
@ -1174,7 +1179,7 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
if !ok { if !ok {
session, ok = getTelegramInstance(bare, &persistence.Session{}, component) session, ok = getTelegramInstance(bare, &persistence.Session{}, component)
if !ok { if !ok {
iqAnswerSetError(answer, query, 500) iqAnswerRegisterSetError(answer, query, 500)
return return
} }
} }
@ -1182,23 +1187,23 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer
err := session.TryLogin(resource, query.Username) err := session.TryLogin(resource, query.Username)
if err != nil { if err != nil {
if err.Error() == telegram.TelegramAuthDone { if err.Error() == telegram.TelegramAuthDone {
iqAnswerSetError(answer, query, 406) iqAnswerRegisterSetError(answer, query, 406)
} else { } else {
iqAnswerSetError(answer, query, 500) iqAnswerRegisterSetError(answer, query, 500)
} }
return return
} }
err = session.SetPhoneNumber(query.Username) err = session.SetPhoneNumber(query.Username)
if err != nil { if err != nil {
iqAnswerSetError(answer, query, 500) iqAnswerRegisterSetError(answer, query, 500)
return return
} }
// everything okay, the response should be empty with no payload/error at this point // everything okay, the response should be empty with no payload/error at this point
gateway.SubscribeToTransport(component, iq.From) gateway.SubscribeToTransport(component, iq.From)
} else { } 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) log.Debugf("command response: %#v %v", answer.Payload, cancelSend)
} }
func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) {
answer.Type = stanza.IQTypeError 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 answer.Payload = *payload
iqAnswerSetErrorInternal(answer, code, true)
}
func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) {
answer.Type = stanza.IQTypeError
switch code { switch code {
case 400: case 400:
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify, Type: stanza.ErrorTypeModify,
Reason: "bad-request", Reason: "bad-request",
} }
case 401:
answer.Error = &stanza.Err{
Type: stanza.ErrorTypeAuth,
Reason: "not-authorized",
}
case 404: case 404:
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel, Type: stanza.ErrorTypeCancel,
Reason: "item-not-found", Reason: "item-not-found",
Text: "No such room",
} }
case 405: case 405:
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel, Type: stanza.ErrorTypeCancel,
Reason: "not-allowed", 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: case 406:
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify, Type: stanza.ErrorTypeModify,
Reason: "not-acceptable", Reason: "not-acceptable",
Text: "Phone number already provided, chat with the transport for further instruction",
} }
case 500: case 500:
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeWait, Type: stanza.ErrorTypeWait,
Reason: "internal-server-error", Reason: "internal-server-error",
} }
default: default:
log.Error("Unknown error code, falling back with empty reason") log.Error("Unknown error code, falling back with empty reason")
answer.Error = &stanza.Err{ answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel, Type: stanza.ErrorTypeCancel,
Reason: "undefined-condition", 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) { func presenceReplySetError(reply *stanza.Presence, code int) {