Upgrade MUC occupants map to LRU

This commit is contained in:
Bohdan Horbeshko 2025-07-23 17:03:19 -04:00
parent a909b86831
commit f3eeba7273
5 changed files with 618 additions and 39 deletions

View file

@ -21,27 +21,6 @@ type DelayedStatus struct {
TimestampExpired int64 TimestampExpired int64
} }
// MUCState holds MUC metadata
type MUCState struct {
Resources map[string]bool
Occupants map[int64]*MUCOccupant
}
// MUCOccupant represents a MUC occupant
type MUCOccupant struct {
Nickname string
Affiliation string
Role string
Status client.ChatMemberStatus
}
func NewMUCState() *MUCState {
return &MUCState{
Resources: make(map[string]bool),
Occupants: make(map[int64]*MUCOccupant),
}
}
// HashedAvatar stores a SHA-1 hash and a Telegram file ID // HashedAvatar stores a SHA-1 hash and a Telegram file ID
type HashedAvatar struct { type HashedAvatar struct {
Hash string Hash string

View file

@ -506,7 +506,7 @@ func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInf
mucState, ok := c.mucCache[chatID] mucState, ok := c.mucCache[chatID]
if ok && mucState != nil { if ok && mucState != nil {
mucState.Occupants = make(map[int64]*MUCOccupant) mucState.Occupants.Clear()
c.updateMUCOccupants(mucState, chatID, update.BasicGroupFullInfo.Members) c.updateMUCOccupants(mucState, chatID, update.BasicGroupFullInfo.Members)
} }
@ -528,7 +528,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) {
mucState, ok := c.mucCache[update.ChatId] mucState, ok := c.mucCache[update.ChatId]
if ok && mucState != nil { if ok && mucState != nil {
_, toJids := c.getMUCJoinedJIDs(update.ChatId, mucState, false) _, toJids := c.getMUCJoinedJIDs(update.ChatId, mucState, false)
for memberID, occupant := range mucState.Occupants { for occupant := range mucState.Occupants.Range() {
affiliation, role := c.memberStatusToAffiliationAndRole(occupant.Status, chat) affiliation, role := c.memberStatusToAffiliationAndRole(occupant.Status, chat)
if affiliation != occupant.Affiliation || role != occupant.Role { if affiliation != occupant.Affiliation || role != occupant.Role {
occupant.Affiliation = affiliation occupant.Affiliation = affiliation
@ -538,7 +538,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) {
gateway.SPFrom(gateway.MUCNODE(update.ChatId)), gateway.SPFrom(gateway.MUCNODE(update.ChatId)),
gateway.SPResource(occupant.Nickname), gateway.SPResource(occupant.Nickname),
gateway.SPImmed(true), gateway.SPImmed(true),
gateway.SPMUCJid(gateway.CHATJID(memberID, true)), gateway.SPMUCJid(gateway.CHATJID(occupant.key, true)),
gateway.SPMUCAffiliation(affiliation), gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role), gateway.SPMUCRole(role),
gateway.SPToJids(toJids), gateway.SPToJids(toJids),

181
telegram/muc.go Normal file
View file

@ -0,0 +1,181 @@
package telegram
import (
"sync"
"github.com/zelenin/go-tdlib/client"
log "github.com/sirupsen/logrus"
)
const MUCOccupantsLimit int32 = 200
// MUCState holds MUC metadata
type MUCState struct {
Resources map[string]bool
Occupants *MUCOccupantsLRU
}
// MUCOccupant represents a MUC occupant
type MUCOccupant struct {
Nickname string
Affiliation string
Role string
Status client.ChatMemberStatus
prev *MUCOccupant
next *MUCOccupant
key int64
}
func (o *MUCOccupant) cutOut() (prev, next *MUCOccupant) {
prev = o.prev
next = o.next
// -- * --- * -X- * -X- * --- * --
o.prev = nil
o.next = nil
if prev != nil {
prev.next = next
}
if next != nil {
next.prev = prev
}
return
}
func NewMUCState() *MUCState {
return &MUCState{
Resources: make(map[string]bool),
Occupants: NewMUCOccupantsLRU(),
}
}
type MUCOccupantsLRU struct {
m map[int64]*MUCOccupant
oldest *MUCOccupant
newest *MUCOccupant
lock sync.Mutex
}
func NewMUCOccupantsLRU() *MUCOccupantsLRU {
return &MUCOccupantsLRU{
m: make(map[int64]*MUCOccupant),
}
}
func (lru *MUCOccupantsLRU) Get(key int64) (*MUCOccupant, bool) {
lru.lock.Lock()
defer lru.lock.Unlock()
occupant, ok := lru.m[key]
return occupant, ok
}
func (lru *MUCOccupantsLRU) cutOut(oldOccupant *MUCOccupant) (prev, next *MUCOccupant) {
prev, next = oldOccupant.cutOut()
if lru.oldest == oldOccupant {
lru.oldest = next
}
if lru.newest == oldOccupant {
lru.newest = prev
}
return
}
func (lru *MUCOccupantsLRU) insertNewest(occupant *MUCOccupant) {
lru.newest.next = occupant
occupant.prev = lru.newest
occupant.next = nil
lru.newest = occupant
}
func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) {
lru.lock.Lock()
defer lru.lock.Unlock()
occupant.key = key
oldOccupant, oldOk := lru.m[key]
lru.m[key] = occupant
if oldOk {
lru.cutOut(oldOccupant)
}
if (lru.oldest == nil) != (lru.newest == nil) {
log.Fatal("MRD MUDAQ")
}
if lru.oldest == nil && lru.newest == nil {
lru.oldest = occupant
lru.newest = occupant
occupant.prev = nil
occupant.next = nil
} else {
lru.insertNewest(occupant)
}
if len(lru.m) > int(MUCOccupantsLimit) && lru.oldest != nil {
delete(lru.m, lru.oldest.key)
lru.cutOut(lru.oldest)
}
}
func (lru *MUCOccupantsLRU) Delete(key int64) {
lru.lock.Lock()
defer lru.lock.Unlock()
oldOccupant, oldOk := lru.m[key]
delete(lru.m, key)
if oldOk {
lru.cutOut(oldOccupant)
}
}
func (lru *MUCOccupantsLRU) Bump(occupant *MUCOccupant) {
lru.lock.Lock()
defer lru.lock.Unlock()
if lru.newest == occupant {
// already at the top, nothing to do
return
}
lru.cutOut(occupant)
lru.insertNewest(occupant)
}
func (lru *MUCOccupantsLRU) Range() chan *MUCOccupant {
lru.lock.Lock()
occupantChan := make(chan *MUCOccupant, 1)
go func() {
defer func() {
lru.lock.Unlock()
close(occupantChan)
}()
for _, occupant := range lru.m {
occupantChan <- occupant
}
}()
return occupantChan
}
func (lru *MUCOccupantsLRU) Clear() {
lru.lock.Lock()
defer lru.lock.Unlock()
for _, occupant := range lru.m {
occupant.prev = nil
occupant.next = nil
}
lru.m = make(map[int64]*MUCOccupant)
lru.oldest = nil
lru.newest = nil
}

419
telegram/muc_test.go Normal file
View file

@ -0,0 +1,419 @@
package telegram
import (
"testing"
)
// -x->[]-x-> +(.)
func TestSetMUCOccupantsLRUSetInitiallyEmpty(t *testing.T) {
// init
occupants := NewMUCOccupantsLRU()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(1, newOccupant)
// checks
if occupants.oldest != newOccupant || newOccupant.prev != nil || newOccupant.next != nil || occupants.newest != newOccupant {
t.Error("Broken")
}
}
func testMUCOccupantsLRUChainOfOne() (occupants *MUCOccupantsLRU, occupant1 *MUCOccupant) {
occupants = NewMUCOccupantsLRU()
occupant1 = &MUCOccupant{}
occupants.m[1] = occupant1
occupants.oldest = occupant1
occupants.newest = occupant1
return
}
func testMUCOccupantsLRUChainOfThree() (occupants *MUCOccupantsLRU, occupant1, occupant2, occupant3 *MUCOccupant) {
occupants = NewMUCOccupantsLRU()
occupant1 = &MUCOccupant{}
occupant2 = &MUCOccupant{}
occupant3 = &MUCOccupant{}
occupants.m[1] = occupant1
occupants.m[2] = occupant2
occupants.m[3] = occupant3
occupants.oldest = occupant1
occupants.newest = occupant3
occupant1.next = occupant2
occupant2.prev = occupant1
occupant2.next = occupant3
occupant3.prev = occupant2
return
}
// ->[]->()-> +(.)
func TestSetMUCOccupantsLRUSetOneOther(t *testing.T) {
// init
occupants, occupant1 := testMUCOccupantsLRUChainOfOne()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(2, newOccupant)
// checks
if occupants.oldest != occupant1 || occupant1.prev != nil || occupant1.next != newOccupant || newOccupant.prev != occupant1 || newOccupant.next != nil || occupants.newest != newOccupant {
t.Error("Broken")
}
}
// ->[]->()->()->()-> +(.)
func TestSetMUCOccupantsLRUSetThreeOthers(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(4, newOccupant)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant2 ||
occupant2.prev != occupant1 ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != newOccupant ||
newOccupant.prev != occupant3 ||
newOccupant.next != nil ||
occupants.newest != newOccupant {
t.Error("Broken")
}
}
// ->[]->(.)->()->()-> +(.)
func TestSetMUCOccupantsLRUSetReplaceFirst(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(1, newOccupant)
// checks
if occupants.oldest != occupant2 ||
occupant2.prev != nil ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != newOccupant ||
newOccupant.prev != occupant3 ||
newOccupant.next != nil ||
occupants.newest != newOccupant ||
occupant1.prev != nil ||
occupant1.next != nil {
t.Error("Broken")
}
}
// ->[]->()->(.)->()-> +(.)
func TestSetMUCOccupantsLRUSetReplaceMiddle(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(2, newOccupant)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant3 ||
occupant3.prev != occupant1 ||
occupant3.next != newOccupant ||
newOccupant.prev != occupant3 ||
newOccupant.next != nil ||
occupants.newest != newOccupant ||
occupant2.prev != nil ||
occupant2.next != nil {
t.Error("Broken")
}
}
// ->[]->()->()->(.)-> +(.)
func TestSetMUCOccupantsLRUSetReplaceLast(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(3, newOccupant)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant2 ||
occupant2.prev != occupant1 ||
occupant2.next != newOccupant ||
newOccupant.prev != occupant2 ||
newOccupant.next != nil ||
occupants.newest != newOccupant ||
occupant3.prev != nil ||
occupant3.next != nil {
t.Error("Broken")
}
}
// ->[]->(.)-> +(.)
func TestSetMUCOccupantsLRUSetReplaceOnly(t *testing.T) {
// init
occupants, occupant1 := testMUCOccupantsLRUChainOfOne()
// addition
newOccupant := &MUCOccupant{}
occupants.Set(1, newOccupant)
// checks
if occupants.oldest != newOccupant ||
occupants.newest != newOccupant ||
newOccupant.prev != nil ||
newOccupant.next != nil ||
occupant1.prev != nil ||
occupant1.next != nil {
t.Error("Broken")
}
}
// ->[]->(.)->()->()-> +s(.)
func TestSetMUCOccupantsLRUSetReplaceFirstWithSame(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
occupants.Set(1, occupant1)
// checks
if occupants.oldest != occupant2 ||
occupant2.prev != nil ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != occupant1 ||
occupant1.prev != occupant3 ||
occupant1.next != nil ||
occupants.newest != occupant1 {
t.Error("Broken")
}
}
// ->[]->()->(.)->()-> +s(.)
func TestSetMUCOccupantsLRUSetReplaceMiddleWithSame(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
occupants.Set(2, occupant2)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant3 ||
occupant3.prev != occupant1 ||
occupant3.next != occupant2 ||
occupant2.prev != occupant3 ||
occupant2.next != nil ||
occupants.newest != occupant2 {
t.Error("Broken")
}
}
// ->[]->()->()->(.)-> +s(.)
func TestSetMUCOccupantsLRUSetReplaceLastWithSame(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// addition
occupants.Set(3, occupant3)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant2 ||
occupant2.prev != occupant1 ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != nil ||
occupants.newest != occupant3 {
t.Error("Broken")
}
}
// ->[]->(.)-> +(.)
func TestSetMUCOccupantsLRUSetReplaceOnlyWithSame(t *testing.T) {
// init
occupants, occupant1 := testMUCOccupantsLRUChainOfOne()
// addition
occupants.Set(1, occupant1)
// checks
if occupants.oldest != occupant1 ||
occupants.newest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != nil {
t.Error("Broken")
}
}
// ->[]->(X)->
func TestSetMUCOccupantsLRUDeleteOnly(t *testing.T) {
// init
occupants, occupant1 := testMUCOccupantsLRUChainOfOne()
// deletion
occupants.Delete(1)
// checks
if occupants.oldest != nil ||
occupants.newest != nil ||
occupant1.prev != nil ||
occupant1.next != nil {
t.Error("Broken")
}
}
// ->[]->(X)->()->()->
func TestSetMUCOccupantsLRUDeleteFirst(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// deletion
occupants.Delete(1)
// checks
if occupants.oldest != occupant2 ||
occupant2.prev != nil ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != nil ||
occupants.newest != occupant3 ||
occupant1.prev != nil ||
occupant1.next != nil {
t.Error("Broken")
}
}
// ->[]->()->(X)->()->
func TestSetMUCOccupantsLRUDeleteMiddle(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// deletion
occupants.Delete(2)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant3 ||
occupant3.prev != occupant1 ||
occupant3.next != nil ||
occupants.newest != occupant3 ||
occupant2.prev != nil ||
occupant2.next != nil {
t.Error("Broken")
}
}
// ->[]->()->()->(X)->
func TestSetMUCOccupantsLRUDeleteLast(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// deletion
occupants.Delete(3)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant2 ||
occupant2.prev != occupant1 ||
occupant2.next != nil ||
occupants.newest != occupant2 ||
occupant3.prev != nil ||
occupant3.next != nil {
t.Error("Broken")
}
}
// ->[]->(.)->
func TestSetMUCOccupantsLRUBumpOnly(t *testing.T) {
// init
occupants, occupant1 := testMUCOccupantsLRUChainOfOne()
// bump
occupants.Bump(occupant1)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != nil ||
occupants.newest != occupant1 {
t.Error("Broken")
}
}
// ->[]->(.)->()->()->
func TestSetMUCOccupantsLRUBumpFirst(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// bump
occupants.Bump(occupant1)
// checks
if occupants.oldest != occupant2 ||
occupant2.prev != nil ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != occupant1 ||
occupant1.prev != occupant3 ||
occupant1.next != nil ||
occupants.newest != occupant1 {
t.Error("Broken")
}
}
// ->[]->()->(.)->()->
func TestSetMUCOccupantsLRUBumpMiddle(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// bump
occupants.Bump(occupant2)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant3 ||
occupant3.prev != occupant1 ||
occupant3.next != occupant2 ||
occupant2.prev != occupant3 ||
occupant2.next != nil ||
occupants.newest != occupant2 {
t.Error("Broken")
}
}
// ->[]->()->()->(.)->
func TestSetMUCOccupantsLRUBumpLast(t *testing.T) {
// init
occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree()
// bump
occupants.Bump(occupant3)
// checks
if occupants.oldest != occupant1 ||
occupant1.prev != nil ||
occupant1.next != occupant2 ||
occupant2.prev != occupant1 ||
occupant2.next != occupant3 ||
occupant3.prev != occupant2 ||
occupant3.next != nil ||
occupants.newest != occupant3 {
t.Error("Broken")
}
}

View file

@ -565,7 +565,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
c.locks.mucCacheLock.Lock() c.locks.mucCacheLock.Lock()
chatJid := gateway.CHATJID(chatID, true) chatJid := gateway.CHATJID(chatID, true)
for mucId, state := range c.mucCache { for mucId, state := range c.mucCache {
occupant, ok := state.Occupants[chatID] occupant, ok := state.Occupants.Get(chatID)
if ok { if ok {
_, toJids := c.getMUCJoinedJIDs(mucId, state, false) _, toJids := c.getMUCJoinedJIDs(mucId, state, false)
newMucArgs := append( newMucArgs := append(
@ -682,7 +682,7 @@ func (c *Client) sendMUCStatuses(chatID int64) {
members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{
ChatId: chatID, ChatId: chatID,
Limit: 200, Limit: MUCOccupantsLimit,
Filter: &client.ChatMembersFilterMembers{}, Filter: &client.ChatMembersFilterMembers{},
}) })
if err == nil { if err == nil {
@ -707,12 +707,12 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []
for _, member := range members { for _, member := range members {
senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat) senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat)
mucState.Occupants[senderId] = &MUCOccupant{ mucState.Occupants.Set(senderId, &MUCOccupant{
Nickname: nickname, Nickname: nickname,
Affiliation: affiliation, Affiliation: affiliation,
Role: role, Role: role,
Status: member.Status, Status: member.Status,
} })
if c.me != nil && senderId == c.me.Id { if c.me != nil && senderId == c.me.Id {
myNickname = nickname myNickname = nickname
@ -753,7 +753,7 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool {
return false // no MUC to be added to return false // no MUC to be added to
} }
_, ok = mucState.Occupants[memberID] _, ok = mucState.Occupants.Get(memberID)
return ok return ok
} }
@ -780,12 +780,12 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s
) )
if err == nil { if err == nil {
mucState.Occupants[memberID] = &MUCOccupant{ mucState.Occupants.Set(memberID, &MUCOccupant{
Nickname: nickname, Nickname: nickname,
Affiliation: affiliation, Affiliation: affiliation,
Role: role, Role: role,
Status: status, Status: status,
} })
return true return true
} }
@ -830,14 +830,14 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
realJid := gateway.CHATJID(memberID, true) realJid := gateway.CHATJID(memberID, true)
for mucId, state := range c.mucCache { for mucId, state := range c.mucCache {
oldOccupant, ok := state.Occupants[memberID] oldOccupant, ok := state.Occupants.Get(memberID)
if ok { if ok {
state.Occupants[memberID] = &MUCOccupant{ state.Occupants.Set(memberID, &MUCOccupant{
Nickname: newNickname, Nickname: newNickname,
Affiliation: oldOccupant.Affiliation, Affiliation: oldOccupant.Affiliation,
Role: oldOccupant.Role, Role: oldOccupant.Role,
Status: oldOccupant.Status, Status: oldOccupant.Status,
} })
sMucId := gateway.MUCNODE(mucId) sMucId := gateway.MUCNODE(mucId)
unavailableStatusCodes := []uint16{303, 210} unavailableStatusCodes := []uint16{303, 210}
@ -921,7 +921,7 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) {
if !ok || mucState == nil { if !ok || mucState == nil {
return "", false return "", false
} }
occupant, ok := mucState.Occupants[c.me.Id] occupant, ok := mucState.Occupants.Get(c.me.Id)
if !ok { if !ok {
return "", false return "", false
} }
@ -938,9 +938,9 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 {
return 0 return 0
} }
for memberId, occupant := range mucState.Occupants { for occupant := range mucState.Occupants.Range() {
if occupant.Nickname == nickname { if occupant.Nickname == nickname {
return memberId return occupant.key
} }
} }
@ -3068,9 +3068,9 @@ func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMember
mucState, ok := c.mucCache[chatID] mucState, ok := c.mucCache[chatID]
if ok && mucState != nil { if ok && mucState != nil {
if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned { if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned {
delete(mucState.Occupants, userID) mucState.Occupants.Delete(userID)
} else { } else {
occupant, ok := mucState.Occupants[userID] occupant, ok := mucState.Occupants.Get(userID)
if ok { if ok {
occupant.Affiliation = newAffiliation occupant.Affiliation = newAffiliation
occupant.Role = newRole occupant.Role = newRole