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
}
// 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
type HashedAvatar struct {
Hash string

View file

@ -506,7 +506,7 @@ func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInf
mucState, ok := c.mucCache[chatID]
if ok && mucState != nil {
mucState.Occupants = make(map[int64]*MUCOccupant)
mucState.Occupants.Clear()
c.updateMUCOccupants(mucState, chatID, update.BasicGroupFullInfo.Members)
}
@ -528,7 +528,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) {
mucState, ok := c.mucCache[update.ChatId]
if ok && mucState != nil {
_, 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)
if affiliation != occupant.Affiliation || role != occupant.Role {
occupant.Affiliation = affiliation
@ -538,7 +538,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) {
gateway.SPFrom(gateway.MUCNODE(update.ChatId)),
gateway.SPResource(occupant.Nickname),
gateway.SPImmed(true),
gateway.SPMUCJid(gateway.CHATJID(memberID, true)),
gateway.SPMUCJid(gateway.CHATJID(occupant.key, true)),
gateway.SPMUCAffiliation(affiliation),
gateway.SPMUCRole(role),
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()
chatJid := gateway.CHATJID(chatID, true)
for mucId, state := range c.mucCache {
occupant, ok := state.Occupants[chatID]
occupant, ok := state.Occupants.Get(chatID)
if ok {
_, toJids := c.getMUCJoinedJIDs(mucId, state, false)
newMucArgs := append(
@ -682,7 +682,7 @@ func (c *Client) sendMUCStatuses(chatID int64) {
members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{
ChatId: chatID,
Limit: 200,
Limit: MUCOccupantsLimit,
Filter: &client.ChatMembersFilterMembers{},
})
if err == nil {
@ -707,12 +707,12 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []
for _, member := range members {
senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat)
mucState.Occupants[senderId] = &MUCOccupant{
mucState.Occupants.Set(senderId, &MUCOccupant{
Nickname: nickname,
Affiliation: affiliation,
Role: role,
Status: member.Status,
}
})
if c.me != nil && senderId == c.me.Id {
myNickname = nickname
@ -753,7 +753,7 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool {
return false // no MUC to be added to
}
_, ok = mucState.Occupants[memberID]
_, ok = mucState.Occupants.Get(memberID)
return ok
}
@ -780,12 +780,12 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s
)
if err == nil {
mucState.Occupants[memberID] = &MUCOccupant{
mucState.Occupants.Set(memberID, &MUCOccupant{
Nickname: nickname,
Affiliation: affiliation,
Role: role,
Status: status,
}
})
return true
}
@ -830,14 +830,14 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) {
realJid := gateway.CHATJID(memberID, true)
for mucId, state := range c.mucCache {
oldOccupant, ok := state.Occupants[memberID]
oldOccupant, ok := state.Occupants.Get(memberID)
if ok {
state.Occupants[memberID] = &MUCOccupant{
state.Occupants.Set(memberID, &MUCOccupant{
Nickname: newNickname,
Affiliation: oldOccupant.Affiliation,
Role: oldOccupant.Role,
Status: oldOccupant.Status,
}
})
sMucId := gateway.MUCNODE(mucId)
unavailableStatusCodes := []uint16{303, 210}
@ -921,7 +921,7 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) {
if !ok || mucState == nil {
return "", false
}
occupant, ok := mucState.Occupants[c.me.Id]
occupant, ok := mucState.Occupants.Get(c.me.Id)
if !ok {
return "", false
}
@ -938,9 +938,9 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 {
return 0
}
for memberId, occupant := range mucState.Occupants {
for occupant := range mucState.Occupants.Range() {
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]
if ok && mucState != nil {
if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned {
delete(mucState.Occupants, userID)
mucState.Occupants.Delete(userID)
} else {
occupant, ok := mucState.Occupants[userID]
occupant, ok := mucState.Occupants.Get(userID)
if ok {
occupant.Affiliation = newAffiliation
occupant.Role = newRole