package communities import ( "crypto/ecdsa" slices "golang.org/x/exp/slices" "github.com/status-im/status-go/protocol/protobuf" ) type CommunityChatChanges struct { ChatModified *protobuf.CommunityChat MembersAdded map[string]*protobuf.CommunityMember MembersRemoved map[string]*protobuf.CommunityMember CategoryModified string PositionModified int FirstMessageTimestampModified uint32 } type CommunityChanges struct { Community *Community `json:"community"` ControlNodeChanged *ecdsa.PublicKey `json:"controlNodeChanged"` MembersAdded map[string]*protobuf.CommunityMember `json:"membersAdded"` MembersRemoved map[string]*protobuf.CommunityMember `json:"membersRemoved"` MembersBanned map[string]bool `json:"membersBanned"` MembersUnbanned map[string]bool `json:"membersUnbanned"` TokenPermissionsAdded map[string]*CommunityTokenPermission `json:"tokenPermissionsAdded"` TokenPermissionsModified map[string]*CommunityTokenPermission `json:"tokenPermissionsModified"` TokenPermissionsRemoved map[string]*CommunityTokenPermission `json:"tokenPermissionsRemoved"` ChatsRemoved map[string]*protobuf.CommunityChat `json:"chatsRemoved"` ChatsAdded map[string]*protobuf.CommunityChat `json:"chatsAdded"` ChatsModified map[string]*CommunityChatChanges `json:"chatsModified"` CategoriesRemoved []string `json:"categoriesRemoved"` CategoriesAdded map[string]*protobuf.CommunityCategory `json:"categoriesAdded"` CategoriesModified map[string]*protobuf.CommunityCategory `json:"categoriesModified"` MemberWalletsRemoved []string `json:"memberWalletsRemoved"` MemberWalletsAdded map[string][]*protobuf.RevealedAccount `json:"memberWalletsAdded"` // ShouldMemberJoin indicates whether the user should join this community // automatically ShouldMemberJoin bool `json:"memberAdded"` // MemberKicked indicates whether the user has been kicked out MemberKicked bool `json:"memberRemoved"` // MemberSoftKicked indicates whether the user has been kicked out due to lack of specific data // No kick AC notification will be generated and member will join automatically // as soon as he provides missing data MemberSoftKicked bool `json:"memberSoftRemoved"` } func EmptyCommunityChanges() *CommunityChanges { return &CommunityChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), MembersRemoved: make(map[string]*protobuf.CommunityMember), MembersBanned: make(map[string]bool), MembersUnbanned: make(map[string]bool), TokenPermissionsAdded: make(map[string]*CommunityTokenPermission), TokenPermissionsModified: make(map[string]*CommunityTokenPermission), TokenPermissionsRemoved: make(map[string]*CommunityTokenPermission), ChatsRemoved: make(map[string]*protobuf.CommunityChat), ChatsAdded: make(map[string]*protobuf.CommunityChat), ChatsModified: make(map[string]*CommunityChatChanges), CategoriesRemoved: []string{}, CategoriesAdded: make(map[string]*protobuf.CommunityCategory), CategoriesModified: make(map[string]*protobuf.CommunityCategory), MemberWalletsRemoved: []string{}, MemberWalletsAdded: make(map[string][]*protobuf.RevealedAccount), } } func (c *CommunityChanges) Merge(other *CommunityChanges) { for memberID, member := range other.MembersAdded { c.MembersAdded[memberID] = member } for memberID := range other.MembersRemoved { c.MembersRemoved[memberID] = other.MembersRemoved[memberID] } for memberID, banned := range other.MembersBanned { c.MembersBanned[memberID] = banned } for memberID, unbanned := range other.MembersUnbanned { c.MembersUnbanned[memberID] = unbanned } for permissionID, permission := range other.TokenPermissionsAdded { c.TokenPermissionsAdded[permissionID] = permission } for permissionID, permission := range other.TokenPermissionsModified { c.TokenPermissionsModified[permissionID] = permission } for permissionID, permission := range other.TokenPermissionsRemoved { c.TokenPermissionsRemoved[permissionID] = permission } for chatID, chat := range other.ChatsRemoved { c.ChatsRemoved[chatID] = chat } for chatID, chat := range other.ChatsAdded { c.ChatsAdded[chatID] = chat } for chatID, changes := range other.ChatsModified { c.ChatsModified[chatID] = changes } c.CategoriesRemoved = append(c.CategoriesRemoved, other.CategoriesRemoved...) for categoryID, category := range other.CategoriesAdded { c.CategoriesAdded[categoryID] = category } for categoryID, category := range other.CategoriesModified { c.CategoriesModified[categoryID] = category } c.MemberWalletsRemoved = append(c.MemberWalletsRemoved, other.MemberWalletsRemoved...) for walletID, wallets := range other.MemberWalletsAdded { c.MemberWalletsAdded[walletID] = wallets } } func (c *CommunityChanges) HasNewMember(identity string) bool { if len(c.MembersAdded) == 0 { return false } _, ok := c.MembersAdded[identity] return ok } func (c *CommunityChanges) HasMemberLeft(identity string) bool { if len(c.MembersRemoved) == 0 { return false } _, ok := c.MembersRemoved[identity] return ok } func (c *CommunityChanges) IsMemberBanned(identity string) bool { if len(c.MembersBanned) == 0 { return false } _, ok := c.MembersBanned[identity] return ok } func (c *CommunityChanges) IsMemberUnbanned(identity string) bool { if len(c.MembersUnbanned) == 0 { return false } _, ok := c.MembersUnbanned[identity] return ok } func EvaluateCommunityChanges(origin, modified *Community) *CommunityChanges { changes := evaluateCommunityChangesByDescription(origin.Description(), modified.Description()) if origin.ControlNode() != nil && !modified.ControlNode().Equal(origin.ControlNode()) { changes.ControlNodeChanged = modified.ControlNode() } originTokenPermissions := origin.tokenPermissions() modifiedTokenPermissions := modified.tokenPermissions() // Check for modified or removed token permissions for id, originPermission := range originTokenPermissions { if modifiedPermission := modifiedTokenPermissions[id]; modifiedPermission != nil { if !modifiedPermission.Equals(originPermission) { changes.TokenPermissionsModified[id] = modifiedPermission } } else { changes.TokenPermissionsRemoved[id] = originPermission } } // Check for added token permissions for id, permission := range modifiedTokenPermissions { if _, ok := originTokenPermissions[id]; !ok { changes.TokenPermissionsAdded[id] = permission } } changes.Community = modified return changes } func evaluateCommunityChangesByDescription(origin, modified *protobuf.CommunityDescription) *CommunityChanges { changes := EmptyCommunityChanges() // Check for new members at the org level for pk, member := range modified.Members { if _, ok := origin.Members[pk]; !ok { changes.MembersAdded[pk] = member } } // Check ban/unban findDiffInBannedMembers(modified.BannedMembers, origin.BannedMembers, changes.MembersBanned) findDiffInBannedMembers(origin.BannedMembers, modified.BannedMembers, changes.MembersUnbanned) // Check for new banned members (from deprecated BanList) findDiffInBanList(modified.BanList, origin.BanList, changes.MembersBanned) // Check for new unbanned members (from deprecated BanList) findDiffInBanList(origin.BanList, modified.BanList, changes.MembersUnbanned) // Check for removed members at the org level for pk, member := range origin.Members { if _, ok := modified.Members[pk]; !ok { changes.MembersRemoved[pk] = member } } // check for removed chats for chatID, chat := range origin.Chats { if modified.Chats == nil { modified.Chats = make(map[string]*protobuf.CommunityChat) } if _, ok := modified.Chats[chatID]; !ok { changes.ChatsRemoved[chatID] = chat } } for chatID, chat := range modified.Chats { if origin.Chats == nil { origin.Chats = make(map[string]*protobuf.CommunityChat) } if _, ok := origin.Chats[chatID]; !ok { changes.ChatsAdded[chatID] = chat } else { // Check for members added for pk, member := range modified.Chats[chatID].Members { if _, ok := origin.Chats[chatID].Members[pk]; !ok { if changes.ChatsModified[chatID] == nil { changes.ChatsModified[chatID] = &CommunityChatChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), MembersRemoved: make(map[string]*protobuf.CommunityMember), } } changes.ChatsModified[chatID].MembersAdded[pk] = member } } // check for members removed for pk, member := range origin.Chats[chatID].Members { if _, ok := modified.Chats[chatID].Members[pk]; !ok { if changes.ChatsModified[chatID] == nil { changes.ChatsModified[chatID] = &CommunityChatChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), MembersRemoved: make(map[string]*protobuf.CommunityMember), } } changes.ChatsModified[chatID].MembersRemoved[pk] = member } } // check if first message timestamp was modified if origin.Chats[chatID].Identity.FirstMessageTimestamp != modified.Chats[chatID].Identity.FirstMessageTimestamp { if changes.ChatsModified[chatID] == nil { changes.ChatsModified[chatID] = &CommunityChatChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), MembersRemoved: make(map[string]*protobuf.CommunityMember), } } changes.ChatsModified[chatID].FirstMessageTimestampModified = modified.Chats[chatID].Identity.FirstMessageTimestamp } } } // Check for categories that were removed for categoryID := range origin.Categories { if modified.Categories == nil { modified.Categories = make(map[string]*protobuf.CommunityCategory) } if modified.Chats == nil { modified.Chats = make(map[string]*protobuf.CommunityChat) } if _, ok := modified.Categories[categoryID]; !ok { changes.CategoriesRemoved = append(changes.CategoriesRemoved, categoryID) } if origin.Chats == nil { origin.Chats = make(map[string]*protobuf.CommunityChat) } } // Check for categories that were added for categoryID, category := range modified.Categories { if origin.Categories == nil { origin.Categories = make(map[string]*protobuf.CommunityCategory) } if _, ok := origin.Categories[categoryID]; !ok { changes.CategoriesAdded[categoryID] = category } else { if origin.Categories[categoryID].Name != category.Name || origin.Categories[categoryID].Position != category.Position { changes.CategoriesModified[categoryID] = category } } } // Check for chat categories that were modified for chatID, chat := range modified.Chats { if origin.Chats == nil { origin.Chats = make(map[string]*protobuf.CommunityChat) } if _, ok := origin.Chats[chatID]; !ok { continue // It's a new chat } if origin.Chats[chatID].CategoryId != chat.CategoryId { if changes.ChatsModified[chatID] == nil { changes.ChatsModified[chatID] = &CommunityChatChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), MembersRemoved: make(map[string]*protobuf.CommunityMember), } } changes.ChatsModified[chatID].CategoryModified = chat.CategoryId } } return changes } func findDiffInBanList(searchFrom []string, searchIn []string, storeTo map[string]bool) { for _, memberToFind := range searchFrom { if _, stored := storeTo[memberToFind]; stored { continue } exists := slices.Contains(searchIn, memberToFind) if !exists { storeTo[memberToFind] = false } } } func findDiffInBannedMembers(searchFrom map[string]*protobuf.CommunityBanInfo, searchIn map[string]*protobuf.CommunityBanInfo, storeTo map[string]bool) { if searchFrom == nil { return } else if searchIn == nil { for memberToFind, value := range searchFrom { storeTo[memberToFind] = value.DeleteAllMessages } } else { for memberToFind, value := range searchFrom { if _, exists := searchIn[memberToFind]; !exists { storeTo[memberToFind] = value.DeleteAllMessages } } } }