diff --git a/telegram/client.go b/telegram/client.go index 445281a..52fb326 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -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 diff --git a/telegram/handlers.go b/telegram/handlers.go index fe9da04..0880332 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -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), diff --git a/telegram/muc.go b/telegram/muc.go new file mode 100644 index 0000000..c933ae1 --- /dev/null +++ b/telegram/muc.go @@ -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 +} diff --git a/telegram/muc_test.go b/telegram/muc_test.go new file mode 100644 index 0000000..f03fef1 --- /dev/null +++ b/telegram/muc_test.go @@ -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") + } +} diff --git a/telegram/utils.go b/telegram/utils.go index 3c20ec7..aadcc28 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -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