feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,42 @@
package communities
import (
"github.com/status-im/status-go/protocol/protobuf"
)
func (o *Community) ToSyncInstallationCommunityProtobuf(clock uint64, communitySettings *CommunitySettings, syncControlNode *protobuf.SyncCommunityControlNode) (*protobuf.SyncInstallationCommunity, error) {
wrappedCommunity, err := o.ToProtocolMessageBytes()
if err != nil {
return nil, err
}
var rtjs []*protobuf.SyncCommunityRequestsToJoin
reqs := o.RequestsToJoin()
for _, req := range reqs {
rtjs = append(rtjs, req.ToSyncProtobuf())
}
settings := &protobuf.SyncCommunitySettings{
Clock: clock,
CommunityId: o.IDString(),
HistoryArchiveSupportEnabled: true,
}
if communitySettings != nil {
settings.HistoryArchiveSupportEnabled = communitySettings.HistoryArchiveSupportEnabled
}
return &protobuf.SyncInstallationCommunity{
Clock: clock,
Id: o.ID(),
Description: wrappedCommunity,
Joined: o.Joined(),
JoinedAt: o.JoinedAt(),
Verified: o.Verified(),
Muted: o.Muted(),
RequestsToJoin: rtjs,
Settings: settings,
ControlNode: syncControlNode,
LastOpenedAt: o.LastOpenedAt(),
}, nil
}

View File

@@ -0,0 +1,154 @@
package communities
import (
"encoding/json"
"sort"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/protocol/protobuf"
)
type CheckPermissionsResponse struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
NetworksNotSupported bool `json:"networksNotSupported"`
}
type CheckPermissionToJoinResponse = CheckPermissionsResponse
type HighestRoleResponse struct {
Role protobuf.CommunityTokenPermission_Type `json:"type"`
Satisfied bool `json:"satisfied"`
Criteria []*PermissionTokenCriteriaResult `json:"criteria"`
}
var joiningRoleOrders = map[protobuf.CommunityTokenPermission_Type]int{
protobuf.CommunityTokenPermission_BECOME_MEMBER: 1,
protobuf.CommunityTokenPermission_BECOME_ADMIN: 2,
protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER: 3,
protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER: 4,
}
type ByRoleDesc []*HighestRoleResponse
func (a ByRoleDesc) Len() int { return len(a) }
func (a ByRoleDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRoleDesc) Less(i, j int) bool {
return joiningRoleOrders[a[i].Role] > joiningRoleOrders[a[j].Role]
}
type rolesAndHighestRole struct {
Roles []*HighestRoleResponse
HighestRole *HighestRoleResponse
}
func calculateRolesAndHighestRole(permissions map[string]*PermissionTokenCriteriaResult) *rolesAndHighestRole {
item := &rolesAndHighestRole{}
byRoleMap := make(map[protobuf.CommunityTokenPermission_Type]*HighestRoleResponse)
for _, p := range permissions {
if joiningRoleOrders[p.Role] == 0 {
continue
}
if byRoleMap[p.Role] == nil {
byRoleMap[p.Role] = &HighestRoleResponse{
Role: p.Role,
}
}
satisfied := true
for _, tr := range p.TokenRequirements {
if !tr.Satisfied {
satisfied = false
break
}
}
if satisfied {
byRoleMap[p.Role].Satisfied = true
// we prepend
byRoleMap[p.Role].Criteria = append([]*PermissionTokenCriteriaResult{p}, byRoleMap[p.Role].Criteria...)
} else {
// we append then
byRoleMap[p.Role].Criteria = append(byRoleMap[p.Role].Criteria, p)
}
}
if byRoleMap[protobuf.CommunityTokenPermission_BECOME_MEMBER] == nil {
byRoleMap[protobuf.CommunityTokenPermission_BECOME_MEMBER] = &HighestRoleResponse{Satisfied: true, Role: protobuf.CommunityTokenPermission_BECOME_MEMBER}
}
for _, p := range byRoleMap {
item.Roles = append(item.Roles, p)
}
sort.Sort(ByRoleDesc(item.Roles))
for _, r := range item.Roles {
if r.Satisfied {
item.HighestRole = r
break
}
}
return item
}
func (c *CheckPermissionsResponse) MarshalJSON() ([]byte, error) {
type CheckPermissionsTypeAlias struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
Roles []*HighestRoleResponse `json:"roles"`
HighestRole *HighestRoleResponse `json:"highestRole"`
NetworksNotSupported bool `json:"networksNotSupported"`
}
c.calculateSatisfied()
item := &CheckPermissionsTypeAlias{
Satisfied: c.Satisfied,
Permissions: c.Permissions,
ValidCombinations: c.ValidCombinations,
NetworksNotSupported: c.NetworksNotSupported,
}
rolesAndHighestRole := calculateRolesAndHighestRole(c.Permissions)
item.Roles = rolesAndHighestRole.Roles
item.HighestRole = rolesAndHighestRole.HighestRole
return json.Marshal(item)
}
type TokenRequirementResponse struct {
Satisfied bool `json:"satisfied"`
TokenCriteria *protobuf.TokenCriteria `json:"criteria"`
}
type PermissionTokenCriteriaResult struct {
Role protobuf.CommunityTokenPermission_Type `json:"roles"`
TokenRequirements []TokenRequirementResponse `json:"tokenRequirement"`
Criteria []bool `json:"criteria"`
}
type AccountChainIDsCombination struct {
Address gethcommon.Address `json:"address"`
ChainIDs []uint64 `json:"chainIds"`
}
func (c *CheckPermissionsResponse) calculateSatisfied() {
if len(c.Permissions) == 0 {
c.Satisfied = true
return
}
c.Satisfied = false
for _, p := range c.Permissions {
satisfied := true
for _, criteria := range p.Criteria {
if !criteria {
satisfied = false
break
}
}
if satisfied {
c.Satisfied = true
return
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,543 @@
package communities
import (
"sort"
"github.com/status-im/status-go/protocol/protobuf"
)
func (o *Community) ChatsByCategoryID(categoryID string) []string {
o.mutex.Lock()
defer o.mutex.Unlock()
var chatIDs []string
if o.config == nil || o.config.CommunityDescription == nil {
return chatIDs
}
for chatID, chat := range o.config.CommunityDescription.Chats {
if chat.CategoryId == categoryID {
chatIDs = append(chatIDs, chatID)
}
}
return chatIDs
}
func (o *Community) CommunityChatsIDs() []string {
o.mutex.Lock()
defer o.mutex.Unlock()
var chatIDs []string
if o.config == nil || o.config.CommunityDescription == nil {
return chatIDs
}
for chatID := range o.config.CommunityDescription.Chats {
chatIDs = append(chatIDs, chatID)
}
return chatIDs
}
func (o *Community) CreateCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE)) {
return nil, ErrNotAuthorized
}
changes, err := o.createCategory(categoryID, categoryName, chatIDs)
if err != nil {
return nil, err
}
changes.CategoriesAdded[categoryID] = o.config.CommunityDescription.Categories[categoryID]
for i, cid := range chatIDs {
changes.ChatsModified[cid] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
CategoryModified: categoryID,
PositionModified: i,
}
}
if o.IsControlNode() {
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToCreateCategoryCommunityEvent(categoryID, categoryName, chatIDs))
if err != nil {
return nil, err
}
}
return changes, nil
}
func (o *Community) EditCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT)) {
return nil, ErrNotAuthorized
}
changes, err := o.editCategory(categoryID, categoryName, chatIDs)
if err != nil {
return nil, err
}
changes.CategoriesModified[categoryID] = o.config.CommunityDescription.Categories[categoryID]
for i, cid := range chatIDs {
changes.ChatsModified[cid] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
CategoryModified: categoryID,
PositionModified: i,
}
}
if o.IsControlNode() {
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToEditCategoryCommunityEvent(categoryID, categoryName, chatIDs))
if err != nil {
return nil, err
}
}
return changes, nil
}
func (o *Community) ReorderCategories(categoryID string, newPosition int) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER)) {
return nil, ErrNotAuthorized
}
changes, err := o.reorderCategories(categoryID, newPosition)
if err != nil {
return nil, err
}
if o.IsControlNode() {
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToReorderCategoryCommunityEvent(categoryID, newPosition))
if err != nil {
return nil, err
}
}
return changes, nil
}
func (o *Community) setModifiedCategories(changes *CommunityChanges, s sortSlice) {
sort.Sort(s)
for i, catSortHelper := range s {
if o.config.CommunityDescription.Categories[catSortHelper.catID].Position != int32(i) {
o.config.CommunityDescription.Categories[catSortHelper.catID].Position = int32(i)
changes.CategoriesModified[catSortHelper.catID] = o.config.CommunityDescription.Categories[catSortHelper.catID]
}
}
}
func (o *Community) ReorderChat(categoryID string, chatID string, newPosition int) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER)) {
return nil, ErrNotAuthorized
}
changes, err := o.reorderChat(categoryID, chatID, newPosition)
if err != nil {
return nil, err
}
if o.IsControlNode() {
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToReorderChannelCommunityEvent(categoryID, chatID, newPosition))
if err != nil {
return nil, err
}
}
return changes, nil
}
func (o *Community) SortCategoryChats(changes *CommunityChanges, categoryID string) {
var catChats []string
for k, c := range o.config.CommunityDescription.Chats {
if c.CategoryId == categoryID {
catChats = append(catChats, k)
}
}
sortedChats := make(sortSlice, 0, len(catChats))
for _, k := range catChats {
sortedChats = append(sortedChats, sorterHelperIdx{
pos: o.config.CommunityDescription.Chats[k].Position,
chatID: k,
})
}
sort.Sort(sortedChats)
for i, chatSortHelper := range sortedChats {
if o.config.CommunityDescription.Chats[chatSortHelper.chatID].Position != int32(i) {
o.config.CommunityDescription.Chats[chatSortHelper.chatID].Position = int32(i)
if changes.ChatsModified[chatSortHelper.chatID] != nil {
changes.ChatsModified[chatSortHelper.chatID].PositionModified = i
} else {
changes.ChatsModified[chatSortHelper.chatID] = &CommunityChatChanges{
PositionModified: i,
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
}
}
}
}
}
func (o *Community) insertAndSort(changes *CommunityChanges, oldCategoryID string, categoryID string, chatID string, chat *protobuf.CommunityChat, newPosition int) {
// We sort the chats here because maps are not guaranteed to keep order
var catChats []string
sortedChats := make(sortSlice, 0, len(o.config.CommunityDescription.Chats))
for k, v := range o.config.CommunityDescription.Chats {
sortedChats = append(sortedChats, sorterHelperIdx{
pos: v.Position,
chatID: k,
})
}
sort.Sort(sortedChats)
for _, k := range sortedChats {
if o.config.CommunityDescription.Chats[k.chatID].CategoryId == categoryID {
catChats = append(catChats, k.chatID)
}
}
if newPosition > 0 && newPosition >= len(catChats) {
newPosition = len(catChats) - 1
} else if newPosition < 0 {
newPosition = 0
}
decrease := false
if chat.Position > int32(newPosition) {
decrease = true
}
for k, v := range o.config.CommunityDescription.Chats {
if k != chatID && newPosition == int(v.Position) && v.CategoryId == categoryID {
if oldCategoryID == categoryID {
if decrease {
v.Position++
} else {
v.Position--
}
} else {
v.Position++
}
}
}
idx := -1
currChatID := ""
var sortedChatIDs []string
for i, k := range catChats {
if o.config.CommunityDescription.Chats[k] != chat && ((decrease && o.config.CommunityDescription.Chats[k].Position < int32(newPosition)) || (!decrease && o.config.CommunityDescription.Chats[k].Position <= int32(newPosition))) {
sortedChatIDs = append(sortedChatIDs, k)
} else {
if o.config.CommunityDescription.Chats[k] == chat {
idx = i
currChatID = k
}
}
}
sortedChatIDs = append(sortedChatIDs, currChatID)
for i, k := range catChats {
if i == idx || (decrease && o.config.CommunityDescription.Chats[k].Position < int32(newPosition)) || (!decrease && o.config.CommunityDescription.Chats[k].Position <= int32(newPosition)) {
continue
}
sortedChatIDs = append(sortedChatIDs, k)
}
for i, sortedChatID := range sortedChatIDs {
if o.config.CommunityDescription.Chats[sortedChatID].Position != int32(i) {
o.config.CommunityDescription.Chats[sortedChatID].Position = int32(i)
if changes.ChatsModified[sortedChatID] != nil {
changes.ChatsModified[sortedChatID].PositionModified = i
} else {
changes.ChatsModified[sortedChatID] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
PositionModified: i,
}
}
}
}
}
func (o *Community) getCategoryChatCount(categoryID string) int {
result := 0
for _, chat := range o.config.CommunityDescription.Chats {
if chat.CategoryId == categoryID {
result = result + 1
}
}
return result
}
func (o *Community) DeleteCategory(categoryID string) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE)) {
return nil, ErrNotAuthorized
}
changes, err := o.deleteCategory(categoryID)
if err != nil {
return nil, err
}
if o.IsControlNode() {
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToDeleteCategoryCommunityEvent(categoryID))
if err != nil {
return nil, err
}
}
return changes, nil
}
func (o *Community) createCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
if o.config.CommunityDescription.Categories == nil {
o.config.CommunityDescription.Categories = make(map[string]*protobuf.CommunityCategory)
}
if _, ok := o.config.CommunityDescription.Categories[categoryID]; ok {
return nil, ErrCategoryAlreadyExists
}
for _, cid := range chatIDs {
c, exists := o.config.CommunityDescription.Chats[cid]
if !exists {
return nil, ErrChatNotFound
}
if exists && c.CategoryId != categoryID && c.CategoryId != "" {
return nil, ErrChatAlreadyAssigned
}
}
changes := o.emptyCommunityChanges()
o.config.CommunityDescription.Categories[categoryID] = &protobuf.CommunityCategory{
CategoryId: categoryID,
Name: categoryName,
Position: int32(len(o.config.CommunityDescription.Categories)),
}
for i, cid := range chatIDs {
o.config.CommunityDescription.Chats[cid].CategoryId = categoryID
o.config.CommunityDescription.Chats[cid].Position = int32(i)
}
o.SortCategoryChats(changes, "")
return changes, nil
}
func (o *Community) editCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
if o.config.CommunityDescription.Categories == nil {
o.config.CommunityDescription.Categories = make(map[string]*protobuf.CommunityCategory)
}
if _, ok := o.config.CommunityDescription.Categories[categoryID]; !ok {
return nil, ErrCategoryNotFound
}
for _, cid := range chatIDs {
c, exists := o.config.CommunityDescription.Chats[cid]
if !exists {
return nil, ErrChatNotFound
}
if exists && c.CategoryId != categoryID && c.CategoryId != "" {
return nil, ErrChatAlreadyAssigned
}
}
changes := o.emptyCommunityChanges()
emptyCatLen := o.getCategoryChatCount("")
// remove any chat that might have been assigned before and now it's not part of the category
var chatsToRemove []string
for k, chat := range o.config.CommunityDescription.Chats {
if chat.CategoryId == categoryID {
found := false
for _, c := range chatIDs {
if k == c {
found = true
}
}
if !found {
chat.CategoryId = ""
chatsToRemove = append(chatsToRemove, k)
}
}
}
o.config.CommunityDescription.Categories[categoryID].Name = categoryName
for i, cid := range chatIDs {
o.config.CommunityDescription.Chats[cid].CategoryId = categoryID
o.config.CommunityDescription.Chats[cid].Position = int32(i)
}
for i, cid := range chatsToRemove {
o.config.CommunityDescription.Chats[cid].Position = int32(emptyCatLen + i)
changes.ChatsModified[cid] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
CategoryModified: "",
PositionModified: int(o.config.CommunityDescription.Chats[cid].Position),
}
}
o.SortCategoryChats(changes, "")
return changes, nil
}
func (o *Community) deleteCategory(categoryID string) (*CommunityChanges, error) {
if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
return nil, ErrCategoryNotFound
}
changes := o.emptyCommunityChanges()
emptyCategoryChatCount := o.getCategoryChatCount("")
i := 0
for _, chat := range o.config.CommunityDescription.Chats {
if chat.CategoryId == categoryID {
i++
chat.CategoryId = ""
chat.Position = int32(emptyCategoryChatCount + i)
}
}
o.SortCategoryChats(changes, "")
delete(o.config.CommunityDescription.Categories, categoryID)
changes.CategoriesRemoved = append(changes.CategoriesRemoved, categoryID)
// Reorder
s := make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
for _, cat := range o.config.CommunityDescription.Categories {
s = append(s, sorterHelperIdx{
pos: cat.Position,
catID: cat.CategoryId,
})
}
o.setModifiedCategories(changes, s)
return changes, nil
}
func (o *Community) reorderCategories(categoryID string, newPosition int) (*CommunityChanges, error) {
if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
return nil, ErrCategoryNotFound
}
if newPosition > 0 && newPosition >= len(o.config.CommunityDescription.Categories) {
newPosition = len(o.config.CommunityDescription.Categories) - 1
} else if newPosition < 0 {
newPosition = 0
}
category := o.config.CommunityDescription.Categories[categoryID]
if category.Position == int32(newPosition) {
return nil, ErrNoChangeInPosition
}
decrease := false
if category.Position > int32(newPosition) {
decrease = true
}
// Sorting the categories because maps are not guaranteed to keep order
s := make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
for k, v := range o.config.CommunityDescription.Categories {
s = append(s, sorterHelperIdx{
pos: v.Position,
catID: k,
})
}
sort.Sort(s)
var communityCategories []*protobuf.CommunityCategory
for _, currCat := range s {
communityCategories = append(communityCategories, o.config.CommunityDescription.Categories[currCat.catID])
}
var sortedCategoryIDs []string
for _, v := range communityCategories {
if v != category && ((decrease && v.Position < int32(newPosition)) || (!decrease && v.Position <= int32(newPosition))) {
sortedCategoryIDs = append(sortedCategoryIDs, v.CategoryId)
}
}
sortedCategoryIDs = append(sortedCategoryIDs, categoryID)
for _, v := range communityCategories {
if v.CategoryId == categoryID || (decrease && v.Position < int32(newPosition)) || (!decrease && v.Position <= int32(newPosition)) {
continue
}
sortedCategoryIDs = append(sortedCategoryIDs, v.CategoryId)
}
s = make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
for i, k := range sortedCategoryIDs {
s = append(s, sorterHelperIdx{
pos: int32(i),
catID: k,
})
}
changes := o.emptyCommunityChanges()
o.setModifiedCategories(changes, s)
return changes, nil
}
func (o *Community) reorderChat(categoryID string, chatID string, newPosition int) (*CommunityChanges, error) {
if categoryID != "" {
if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
return nil, ErrCategoryNotFound
}
}
var chat *protobuf.CommunityChat
var exists bool
if chat, exists = o.config.CommunityDescription.Chats[chatID]; !exists {
return nil, ErrChatNotFound
}
oldCategoryID := chat.CategoryId
chat.CategoryId = categoryID
changes := o.emptyCommunityChanges()
o.SortCategoryChats(changes, oldCategoryID)
o.insertAndSort(changes, oldCategoryID, categoryID, chatID, chat, newPosition)
return changes, nil
}

View File

@@ -0,0 +1,270 @@
package communities
import (
"crypto/ecdsa"
"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"`
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"`
}
func EmptyCommunityChanges() *CommunityChanges {
return &CommunityChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
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) 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 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 {
if changes.MembersAdded == nil {
changes.MembersAdded = make(map[string]*protobuf.CommunityMember)
}
changes.MembersAdded[pk] = member
}
}
// Check for removed members at the org level
for pk, member := range origin.Members {
if _, ok := modified.Members[pk]; !ok {
if changes.MembersRemoved == nil {
changes.MembersRemoved = make(map[string]*protobuf.CommunityMember)
}
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 {
if changes.ChatsRemoved == nil {
changes.ChatsRemoved = make(map[string]*protobuf.CommunityChat)
}
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 {
if changes.ChatsAdded == nil {
changes.ChatsAdded = make(map[string]*protobuf.CommunityChat)
}
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 {
if changes.CategoriesAdded == nil {
changes.CategoriesAdded = make(map[string]*protobuf.CommunityCategory)
}
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
}

View File

@@ -0,0 +1,111 @@
package communities
import (
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/protobuf"
)
type DescriptionEncryptor interface {
encryptCommunityDescription(community *Community, d *protobuf.CommunityDescription) (string, []byte, error)
encryptCommunityDescriptionChannel(community *Community, channelID string, d *protobuf.CommunityDescription) (string, []byte, error)
decryptCommunityDescription(keyIDSeqNo string, d []byte) (*DecryptCommunityResponse, error)
}
// Encrypts members and chats
func encryptDescription(encryptor DescriptionEncryptor, community *Community, description *protobuf.CommunityDescription) error {
description.PrivateData = make(map[string][]byte)
for channelID, channel := range description.Chats {
if !community.channelEncrypted(channelID) {
continue
}
descriptionToEncrypt := &protobuf.CommunityDescription{
Chats: map[string]*protobuf.CommunityChat{
channelID: proto.Clone(channel).(*protobuf.CommunityChat),
},
}
keyIDSeqNo, encryptedDescription, err := encryptor.encryptCommunityDescriptionChannel(community, channelID, descriptionToEncrypt)
if err != nil {
return err
}
// Set private data and cleanup unencrypted channel's members
description.PrivateData[keyIDSeqNo] = encryptedDescription
channel.Members = make(map[string]*protobuf.CommunityMember)
}
if community.Encrypted() {
descriptionToEncrypt := &protobuf.CommunityDescription{
Members: description.Members,
Chats: description.Chats,
}
keyIDSeqNo, encryptedDescription, err := encryptor.encryptCommunityDescription(community, descriptionToEncrypt)
if err != nil {
return err
}
// Set private data and cleanup unencrypted members and chats
description.PrivateData[keyIDSeqNo] = encryptedDescription
description.Members = make(map[string]*protobuf.CommunityMember)
description.Chats = make(map[string]*protobuf.CommunityChat)
}
return nil
}
type CommunityPrivateDataFailedToDecrypt struct {
GroupID []byte
KeyID []byte
}
// Decrypts members and chats
func decryptDescription(id types.HexBytes, encryptor DescriptionEncryptor, description *protobuf.CommunityDescription, logger *zap.Logger) ([]*CommunityPrivateDataFailedToDecrypt, error) {
if len(description.PrivateData) == 0 {
return nil, nil
}
var failedToDecrypt []*CommunityPrivateDataFailedToDecrypt
for keyIDSeqNo, encryptedDescription := range description.PrivateData {
decryptedDescriptionResponse, err := encryptor.decryptCommunityDescription(keyIDSeqNo, encryptedDescription)
if decryptedDescriptionResponse != nil && !decryptedDescriptionResponse.Decrypted {
failedToDecrypt = append(failedToDecrypt, &CommunityPrivateDataFailedToDecrypt{GroupID: id, KeyID: decryptedDescriptionResponse.KeyID})
}
if err != nil {
// ignore error, try to decrypt next data
logger.Debug("failed to decrypt community private data", zap.String("keyIDSeqNo", keyIDSeqNo), zap.Error(err))
continue
}
decryptedDescription := decryptedDescriptionResponse.Description
for pk, member := range decryptedDescription.Members {
if description.Members == nil {
description.Members = make(map[string]*protobuf.CommunityMember)
}
description.Members[pk] = member
}
for id, decryptedChannel := range decryptedDescription.Chats {
if description.Chats == nil {
description.Chats = make(map[string]*protobuf.CommunityChat)
}
if channel := description.Chats[id]; channel != nil {
if len(channel.Members) == 0 {
channel.Members = decryptedChannel.Members
}
} else {
description.Chats[id] = decryptedChannel
}
}
}
return failedToDecrypt, nil
}

View File

@@ -0,0 +1,153 @@
package communities
import (
"github.com/status-im/status-go/protocol/protobuf"
)
type KeyDistributor interface {
Generate(community *Community, keyActions *EncryptionKeyActions) error
Distribute(community *Community, keyActions *EncryptionKeyActions) error
}
type EncryptionKeyActionType int
const (
EncryptionKeyNone EncryptionKeyActionType = iota
EncryptionKeyAdd
EncryptionKeyRemove
EncryptionKeyRekey
EncryptionKeySendToMembers
)
type EncryptionKeyAction struct {
ActionType EncryptionKeyActionType
Members map[string]*protobuf.CommunityMember
RemovedMembers map[string]*protobuf.CommunityMember
}
type EncryptionKeyActions struct {
// community-level encryption key action
CommunityKeyAction EncryptionKeyAction
// channel-level encryption key actions
ChannelKeysActions map[string]EncryptionKeyAction // key is: chatID
}
func EvaluateCommunityEncryptionKeyActions(origin, modified *Community) *EncryptionKeyActions {
if origin == nil {
// `modified` is a new community, create empty `origin` community
origin = &Community{
config: &Config{
ID: modified.config.ID,
CommunityDescription: &protobuf.CommunityDescription{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{},
Identity: &protobuf.ChatIdentity{},
Chats: map[string]*protobuf.CommunityChat{},
Categories: map[string]*protobuf.CommunityCategory{},
AdminSettings: &protobuf.CommunityAdminSettings{},
TokenPermissions: map[string]*protobuf.CommunityTokenPermission{},
CommunityTokensMetadata: []*protobuf.CommunityTokenMetadata{},
},
},
}
}
changes := EvaluateCommunityChanges(origin, modified)
result := &EncryptionKeyActions{
CommunityKeyAction: *evaluateCommunityLevelEncryptionKeyAction(origin, modified, changes),
ChannelKeysActions: *evaluateChannelLevelEncryptionKeyActions(origin, modified, changes),
}
return result
}
func evaluateCommunityLevelEncryptionKeyAction(origin, modified *Community, changes *CommunityChanges) *EncryptionKeyAction {
return evaluateEncryptionKeyAction(
origin.Encrypted(),
modified.Encrypted(),
changes.ControlNodeChanged != nil,
modified.config.CommunityDescription.Members,
changes.MembersAdded,
changes.MembersRemoved,
)
}
func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, changes *CommunityChanges) *map[string]EncryptionKeyAction {
result := make(map[string]EncryptionKeyAction)
for channelID := range modified.config.CommunityDescription.Chats {
membersAdded := make(map[string]*protobuf.CommunityMember)
membersRemoved := make(map[string]*protobuf.CommunityMember)
chatChanges, ok := changes.ChatsModified[channelID]
if ok {
membersAdded = chatChanges.MembersAdded
membersRemoved = chatChanges.MembersRemoved
}
result[channelID] = *evaluateEncryptionKeyAction(
origin.ChannelEncrypted(channelID),
modified.ChannelEncrypted(channelID),
changes.ControlNodeChanged != nil,
modified.config.CommunityDescription.Chats[channelID].Members,
membersAdded,
membersRemoved,
)
}
return &result
}
func evaluateEncryptionKeyAction(originEncrypted, modifiedEncrypted, controlNodeChanged bool,
allMembers, membersAdded, membersRemoved map[string]*protobuf.CommunityMember) *EncryptionKeyAction {
result := &EncryptionKeyAction{
ActionType: EncryptionKeyNone,
Members: map[string]*protobuf.CommunityMember{},
}
copyMap := func(source map[string]*protobuf.CommunityMember) map[string]*protobuf.CommunityMember {
to := make(map[string]*protobuf.CommunityMember)
for pubKey, member := range source {
to[pubKey] = member
}
return to
}
// control node changed on closed community/channel
if controlNodeChanged && modifiedEncrypted {
result.ActionType = EncryptionKeyRekey
result.Members = copyMap(allMembers)
return result
}
// encryption was just added
if modifiedEncrypted && !originEncrypted {
result.ActionType = EncryptionKeyAdd
result.Members = copyMap(allMembers)
return result
}
// encryption was just removed
if !modifiedEncrypted && originEncrypted {
result.ActionType = EncryptionKeyRemove
result.Members = copyMap(allMembers)
return result
}
// open community/channel does not require any actions
if !modifiedEncrypted {
return result
}
if len(membersRemoved) > 0 {
result.ActionType = EncryptionKeyRekey
result.Members = copyMap(allMembers)
result.RemovedMembers = copyMap(membersRemoved)
} else if len(membersAdded) > 0 {
result.ActionType = EncryptionKeySendToMembers
result.Members = copyMap(membersAdded)
}
return result
}

View File

@@ -0,0 +1,414 @@
package communities
import (
"crypto/ecdsa"
"errors"
"time"
"github.com/golang/protobuf/proto"
utils "github.com/status-im/status-go/common"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
var ErrInvalidCommunityEventClock = errors.New("clock for admin event message is outdated")
func (o *Community) ToCreateChannelCommunityEvent(channelID string, channel *protobuf.CommunityChat) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CHANNEL_CREATE,
ChannelData: &protobuf.ChannelData{
ChannelId: channelID,
Channel: channel,
},
}
}
func (o *Community) ToEditChannelCommunityEvent(channelID string, channel *protobuf.CommunityChat) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CHANNEL_EDIT,
ChannelData: &protobuf.ChannelData{
ChannelId: channelID,
Channel: channel,
},
}
}
func (o *Community) ToDeleteChannelCommunityEvent(channelID string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CHANNEL_DELETE,
ChannelData: &protobuf.ChannelData{
ChannelId: channelID,
},
}
}
func (o *Community) ToReorderChannelCommunityEvent(categoryID string, channelID string, position int) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER,
ChannelData: &protobuf.ChannelData{
CategoryId: categoryID,
ChannelId: channelID,
Position: int32(position),
},
}
}
func (o *Community) ToCreateCategoryCommunityEvent(categoryID string, categoryName string, channelsIds []string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE,
CategoryData: &protobuf.CategoryData{
Name: categoryName,
CategoryId: categoryID,
ChannelsIds: channelsIds,
},
}
}
func (o *Community) ToEditCategoryCommunityEvent(categoryID string, categoryName string, channelsIds []string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT,
CategoryData: &protobuf.CategoryData{
Name: categoryName,
CategoryId: categoryID,
ChannelsIds: channelsIds,
},
}
}
func (o *Community) ToDeleteCategoryCommunityEvent(categoryID string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE,
CategoryData: &protobuf.CategoryData{
CategoryId: categoryID,
},
}
}
func (o *Community) ToReorderCategoryCommunityEvent(categoryID string, position int) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER,
CategoryData: &protobuf.CategoryData{
CategoryId: categoryID,
Position: int32(position),
},
}
}
func (o *Community) ToBanCommunityMemberCommunityEvent(pubkey string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN,
MemberToAction: pubkey,
}
}
func (o *Community) ToUnbanCommunityMemberCommunityEvent(pubkey string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_MEMBER_UNBAN,
MemberToAction: pubkey,
}
}
func (o *Community) ToKickCommunityMemberCommunityEvent(pubkey string) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK,
MemberToAction: pubkey,
}
}
func (o *Community) ToCommunityEditCommunityEvent(description *protobuf.CommunityDescription) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_EDIT,
CommunityConfig: &protobuf.CommunityConfig{
Identity: description.Identity,
Permissions: description.Permissions,
AdminSettings: description.AdminSettings,
IntroMessage: description.IntroMessage,
OutroMessage: description.OutroMessage,
Tags: description.Tags,
},
}
}
func (o *Community) ToCommunityTokenPermissionChangeCommunityEvent(permission *protobuf.CommunityTokenPermission) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE,
TokenPermission: permission,
}
}
func (o *Community) ToCommunityTokenPermissionDeleteCommunityEvent(permission *protobuf.CommunityTokenPermission) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE,
TokenPermission: permission,
}
}
func (o *Community) ToCommunityRequestToJoinAcceptCommunityEvent(changes *CommunityEventChanges) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT,
AcceptedRequestsToJoin: changes.AcceptedRequestsToJoin,
}
}
func (o *Community) ToCommunityRequestToJoinRejectCommunityEvent(changes *CommunityEventChanges) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT,
RejectedRequestsToJoin: changes.RejectedRequestsToJoin,
}
}
func (o *Community) ToAddTokenMetadataCommunityEvent(tokenMetadata *protobuf.CommunityTokenMetadata) *CommunityEvent {
return &CommunityEvent{
CommunityEventClock: o.NewCommunityEventClock(),
Type: protobuf.CommunityEvent_COMMUNITY_TOKEN_ADD,
TokenMetadata: tokenMetadata,
}
}
func (o *Community) UpdateCommunityByEvents(communityEventMessage *CommunityEventsMessage) error {
o.mutex.Lock()
defer o.mutex.Unlock()
// Validate that EventsBaseCommunityDescription was signed by the control node
description, err := validateAndGetEventsMessageCommunityDescription(communityEventMessage.EventsBaseCommunityDescription, o.ControlNode())
if err != nil {
return err
}
if description.Clock != o.config.CommunityDescription.Clock {
return ErrInvalidCommunityEventClock
}
// Merge community events to existing community. Community events must be stored to the db
// during saving the community
o.mergeCommunityEvents(communityEventMessage)
if o.encryptor != nil {
_, err = decryptDescription(o.ID(), o.encryptor, description, o.config.Logger)
if err != nil {
return err
}
}
o.config.CommunityDescription = description
o.config.CommunityDescriptionProtocolMessage = communityEventMessage.EventsBaseCommunityDescription
// Update the copy of the CommunityDescription by community events
err = o.updateCommunityDescriptionByEvents()
if err != nil {
return err
}
return nil
}
func (o *Community) updateCommunityDescriptionByEvents() error {
if o.config.EventsData == nil {
return nil
}
for _, event := range o.config.EventsData.Events {
err := o.updateCommunityDescriptionByCommunityEvent(event)
if err != nil {
return err
}
}
return nil
}
func (o *Community) updateCommunityDescriptionByCommunityEvent(communityEvent CommunityEvent) error {
switch communityEvent.Type {
case protobuf.CommunityEvent_COMMUNITY_EDIT:
o.config.CommunityDescription.Identity = communityEvent.CommunityConfig.Identity
o.config.CommunityDescription.Permissions = communityEvent.CommunityConfig.Permissions
o.config.CommunityDescription.AdminSettings = communityEvent.CommunityConfig.AdminSettings
o.config.CommunityDescription.IntroMessage = communityEvent.CommunityConfig.IntroMessage
o.config.CommunityDescription.OutroMessage = communityEvent.CommunityConfig.OutroMessage
o.config.CommunityDescription.Tags = communityEvent.CommunityConfig.Tags
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE:
if o.IsControlNode() {
_, err := o.upsertTokenPermission(communityEvent.TokenPermission)
if err != nil {
return err
}
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE:
if o.IsControlNode() {
_, err := o.deleteTokenPermission(communityEvent.TokenPermission.Id)
if err != nil {
return err
}
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE:
_, err := o.createCategory(communityEvent.CategoryData.CategoryId, communityEvent.CategoryData.Name, communityEvent.CategoryData.ChannelsIds)
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE:
_, err := o.deleteCategory(communityEvent.CategoryData.CategoryId)
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT:
_, err := o.editCategory(communityEvent.CategoryData.CategoryId, communityEvent.CategoryData.Name, communityEvent.CategoryData.ChannelsIds)
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_CREATE:
err := o.createChat(communityEvent.ChannelData.ChannelId, communityEvent.ChannelData.Channel)
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_DELETE:
o.deleteChat(communityEvent.ChannelData.ChannelId)
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_EDIT:
err := o.editChat(communityEvent.ChannelData.ChannelId, communityEvent.ChannelData.Channel)
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER:
_, err := o.reorderChat(communityEvent.ChannelData.CategoryId, communityEvent.ChannelData.ChannelId, int(communityEvent.ChannelData.Position))
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER:
_, err := o.reorderCategories(communityEvent.CategoryData.CategoryId, int(communityEvent.CategoryData.Position))
if err != nil {
return err
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK:
if o.IsControlNode() {
pk, err := common.HexToPubkey(communityEvent.MemberToAction)
if err != nil {
return err
}
o.removeMemberFromOrg(pk)
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN:
if o.IsControlNode() {
pk, err := common.HexToPubkey(communityEvent.MemberToAction)
if err != nil {
return err
}
o.banUserFromCommunity(pk)
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_UNBAN:
if o.IsControlNode() {
pk, err := common.HexToPubkey(communityEvent.MemberToAction)
if err != nil {
return err
}
o.unbanUserFromCommunity(pk)
}
case protobuf.CommunityEvent_COMMUNITY_TOKEN_ADD:
o.config.CommunityDescription.CommunityTokensMetadata = append(o.config.CommunityDescription.CommunityTokensMetadata, communityEvent.TokenMetadata)
}
return nil
}
func (o *Community) NewCommunityEventClock() uint64 {
return uint64(time.Now().Unix())
}
func (o *Community) addNewCommunityEvent(event *CommunityEvent) error {
err := validateCommunityEvent(event)
if err != nil {
return err
}
// All events must be built on top of the control node CommunityDescription
// If there were no events before, extract CommunityDescription from CommunityDescriptionProtocolMessage
// and check the signature
if o.config.EventsData == nil || len(o.config.EventsData.EventsBaseCommunityDescription) == 0 {
_, err := validateAndGetEventsMessageCommunityDescription(o.config.CommunityDescriptionProtocolMessage, o.ControlNode())
if err != nil {
return err
}
o.config.EventsData = &EventsData{
EventsBaseCommunityDescription: o.config.CommunityDescriptionProtocolMessage,
Events: []CommunityEvent{},
}
}
event.Payload, err = proto.Marshal(event.ToProtobuf())
if err != nil {
return err
}
o.config.EventsData.Events = append(o.config.EventsData.Events, *event)
return nil
}
func (o *Community) ToCommunityEventsMessage() *CommunityEventsMessage {
return &CommunityEventsMessage{
CommunityID: o.ID(),
EventsBaseCommunityDescription: o.config.EventsData.EventsBaseCommunityDescription,
Events: o.config.EventsData.Events,
}
}
func validateAndGetEventsMessageCommunityDescription(signedDescription []byte, signerPubkey *ecdsa.PublicKey) (*protobuf.CommunityDescription, error) {
metadata := &protobuf.ApplicationMetadataMessage{}
err := proto.Unmarshal(signedDescription, metadata)
if err != nil {
return nil, err
}
if metadata.Type != protobuf.ApplicationMetadataMessage_COMMUNITY_DESCRIPTION {
return nil, ErrInvalidMessage
}
signer, err := utils.RecoverKey(metadata)
if err != nil {
return nil, err
}
if signer == nil {
return nil, errors.New("CommunityDescription does not contain the control node signature")
}
if !signer.Equal(signerPubkey) {
return nil, errors.New("CommunityDescription was not signed by an owner")
}
description := &protobuf.CommunityDescription{}
err = proto.Unmarshal(metadata.Payload, description)
if err != nil {
return nil, err
}
return description, nil
}

View File

@@ -0,0 +1,285 @@
package communities
import (
"bytes"
"crypto/ecdsa"
"encoding/json"
"errors"
"sort"
"github.com/golang/protobuf/proto"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/protobuf"
)
type CommunityEvent struct {
CommunityEventClock uint64 `json:"communityEventClock"`
Type protobuf.CommunityEvent_EventType `json:"type"`
CommunityConfig *protobuf.CommunityConfig `json:"communityConfig,omitempty"`
TokenPermission *protobuf.CommunityTokenPermission `json:"tokenPermissions,omitempty"`
CategoryData *protobuf.CategoryData `json:"categoryData,omitempty"`
ChannelData *protobuf.ChannelData `json:"channelData,omitempty"`
MemberToAction string `json:"memberToAction,omitempty"`
MembersAdded map[string]*protobuf.CommunityMember `json:"membersAdded,omitempty"`
RejectedRequestsToJoin map[string]*protobuf.CommunityRequestToJoin `json:"rejectedRequestsToJoin,omitempty"`
AcceptedRequestsToJoin map[string]*protobuf.CommunityRequestToJoin `json:"acceptedRequestsToJoin,omitempty"`
TokenMetadata *protobuf.CommunityTokenMetadata `json:"tokenMetadata,omitempty"`
Payload []byte `json:"payload"`
Signature []byte `json:"signature"`
}
func (e *CommunityEvent) ToProtobuf() *protobuf.CommunityEvent {
return &protobuf.CommunityEvent{
CommunityEventClock: e.CommunityEventClock,
Type: e.Type,
CommunityConfig: e.CommunityConfig,
TokenPermission: e.TokenPermission,
CategoryData: e.CategoryData,
ChannelData: e.ChannelData,
MemberToAction: e.MemberToAction,
MembersAdded: e.MembersAdded,
RejectedRequestsToJoin: e.RejectedRequestsToJoin,
AcceptedRequestsToJoin: e.AcceptedRequestsToJoin,
TokenMetadata: e.TokenMetadata,
}
}
func communityEventFromProtobuf(msg *protobuf.SignedCommunityEvent) (*CommunityEvent, error) {
decodedEvent := protobuf.CommunityEvent{}
err := proto.Unmarshal(msg.Payload, &decodedEvent)
if err != nil {
return nil, err
}
return &CommunityEvent{
CommunityEventClock: decodedEvent.CommunityEventClock,
Type: decodedEvent.Type,
CommunityConfig: decodedEvent.CommunityConfig,
TokenPermission: decodedEvent.TokenPermission,
CategoryData: decodedEvent.CategoryData,
ChannelData: decodedEvent.ChannelData,
MemberToAction: decodedEvent.MemberToAction,
MembersAdded: decodedEvent.MembersAdded,
RejectedRequestsToJoin: decodedEvent.RejectedRequestsToJoin,
AcceptedRequestsToJoin: decodedEvent.AcceptedRequestsToJoin,
TokenMetadata: decodedEvent.TokenMetadata,
Payload: msg.Payload,
Signature: msg.Signature,
}, nil
}
func (e *CommunityEvent) RecoverSigner() (*ecdsa.PublicKey, error) {
if e.Signature == nil || len(e.Signature) == 0 {
return nil, errors.New("missing signature")
}
signer, err := crypto.SigToPub(
crypto.Keccak256(e.Payload),
e.Signature,
)
if err != nil {
return nil, errors.New("failed to recover signer")
}
return signer, nil
}
func (e *CommunityEvent) Sign(pk *ecdsa.PrivateKey) error {
sig, err := crypto.Sign(crypto.Keccak256(e.Payload), pk)
if err != nil {
return err
}
e.Signature = sig
return nil
}
type CommunityEventsMessage struct {
CommunityID []byte `json:"communityId"`
EventsBaseCommunityDescription []byte `json:"eventsBaseCommunityDescription"`
Events []CommunityEvent `json:"events,omitempty"`
}
func (m *CommunityEventsMessage) ToProtobuf() *protobuf.CommunityEventsMessage {
result := protobuf.CommunityEventsMessage{
CommunityId: m.CommunityID,
EventsBaseCommunityDescription: m.EventsBaseCommunityDescription,
SignedEvents: []*protobuf.SignedCommunityEvent{},
}
for _, event := range m.Events {
signedEvent := &protobuf.SignedCommunityEvent{
Signature: event.Signature,
Payload: event.Payload,
}
result.SignedEvents = append(result.SignedEvents, signedEvent)
}
return &result
}
func CommunityEventsMessageFromProtobuf(msg *protobuf.CommunityEventsMessage) (*CommunityEventsMessage, error) {
result := &CommunityEventsMessage{
CommunityID: msg.CommunityId,
EventsBaseCommunityDescription: msg.EventsBaseCommunityDescription,
Events: []CommunityEvent{},
}
for _, signedEvent := range msg.SignedEvents {
event, err := communityEventFromProtobuf(signedEvent)
if err != nil {
return nil, err
}
result.Events = append(result.Events, *event)
}
return result, nil
}
func (m *CommunityEventsMessage) Marshal() ([]byte, error) {
pb := m.ToProtobuf()
return proto.Marshal(pb)
}
func (c *Community) mergeCommunityEvents(communityEventMessage *CommunityEventsMessage) {
if c.config.EventsData == nil {
c.config.EventsData = &EventsData{
EventsBaseCommunityDescription: communityEventMessage.EventsBaseCommunityDescription,
Events: communityEventMessage.Events,
}
return
}
for _, update := range communityEventMessage.Events {
var exists bool
for _, existing := range c.config.EventsData.Events {
if isCommunityEventsEqual(update, existing) {
exists = true
break
}
}
if !exists {
c.config.EventsData.Events = append(c.config.EventsData.Events, update)
}
}
c.sortCommunityEvents()
}
func (c *Community) sortCommunityEvents() {
sort.Slice(c.config.EventsData.Events, func(i, j int) bool {
return c.config.EventsData.Events[i].CommunityEventClock < c.config.EventsData.Events[j].CommunityEventClock
})
}
func validateCommunityEvent(communityEvent *CommunityEvent) error {
switch communityEvent.Type {
case protobuf.CommunityEvent_COMMUNITY_EDIT:
if communityEvent.CommunityConfig == nil || communityEvent.CommunityConfig.Identity == nil ||
communityEvent.CommunityConfig.Permissions == nil || communityEvent.CommunityConfig.AdminSettings == nil {
return errors.New("invalid config change admin event")
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE:
if communityEvent.TokenPermission == nil || len(communityEvent.TokenPermission.Id) == 0 {
return errors.New("invalid token permission change event")
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE:
if communityEvent.TokenPermission == nil || len(communityEvent.TokenPermission.Id) == 0 {
return errors.New("invalid token permission delete event")
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE:
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
return errors.New("invalid community category create event")
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE:
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
return errors.New("invalid community category delete event")
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT:
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
return errors.New("invalid community category edit event")
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_CREATE:
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 ||
communityEvent.ChannelData.Channel == nil {
return errors.New("invalid community channel create event")
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_DELETE:
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 {
return errors.New("invalid community channel delete event")
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_EDIT:
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 ||
communityEvent.ChannelData.Channel == nil {
return errors.New("invalid community channel edit event")
}
case protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER:
if communityEvent.ChannelData == nil || len(communityEvent.ChannelData.ChannelId) == 0 {
return errors.New("invalid community channel reorder event")
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER:
if communityEvent.CategoryData == nil || len(communityEvent.CategoryData.CategoryId) == 0 {
return errors.New("invalid community category reorder event")
}
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT:
if communityEvent.AcceptedRequestsToJoin == nil {
return errors.New("invalid community request to join accepted event")
}
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT:
if communityEvent.RejectedRequestsToJoin == nil {
return errors.New("invalid community request to join reject event")
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK:
if len(communityEvent.MemberToAction) == 0 {
return errors.New("invalid community member kick event")
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN:
if len(communityEvent.MemberToAction) == 0 {
return errors.New("invalid community member ban event")
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_UNBAN:
if len(communityEvent.MemberToAction) == 0 {
return errors.New("invalid community member unban event")
}
case protobuf.CommunityEvent_COMMUNITY_TOKEN_ADD:
if communityEvent.TokenMetadata == nil || len(communityEvent.TokenMetadata.ContractAddresses) == 0 {
return errors.New("invalid add community token event")
}
}
return nil
}
func isCommunityEventsEqual(left CommunityEvent, right CommunityEvent) bool {
return bytes.Equal(left.Payload, right.Payload)
}
func communityEventsToJSONEncodedBytes(communityEvents []CommunityEvent) ([]byte, error) {
return json.Marshal(communityEvents)
}
func communityEventsFromJSONEncodedBytes(jsonEncodedRawEvents []byte) ([]CommunityEvent, error) {
var events []CommunityEvent
err := json.Unmarshal(jsonEncodedRawEvents, &events)
if err != nil {
return nil, err
}
return events, nil
}

View File

@@ -0,0 +1,65 @@
package communities
import (
"reflect"
"github.com/status-im/status-go/protocol/protobuf"
)
type TokenPermissionState uint8
const (
TokenPermissionApproved TokenPermissionState = iota
TokenPermissionAdditionPending
TokenPermissionUpdatePending
TokenPermissionRemovalPending
)
type CommunityTokenPermission struct {
*protobuf.CommunityTokenPermission
State TokenPermissionState `json:"state,omitempty"`
}
func NewCommunityTokenPermission(base *protobuf.CommunityTokenPermission) *CommunityTokenPermission {
return &CommunityTokenPermission{
CommunityTokenPermission: base,
State: TokenPermissionApproved,
}
}
func (p *CommunityTokenPermission) Equals(other *CommunityTokenPermission) bool {
if p.Id != other.Id ||
p.Type != other.Type ||
len(p.TokenCriteria) != len(other.TokenCriteria) ||
len(p.ChatIds) != len(other.ChatIds) ||
p.IsPrivate != other.IsPrivate ||
p.State != other.State {
return false
}
for i := range p.TokenCriteria {
if !compareTokenCriteria(p.TokenCriteria[i], other.TokenCriteria[i]) {
return false
}
}
return reflect.DeepEqual(p.ChatIds, other.ChatIds)
}
func compareTokenCriteria(a, b *protobuf.TokenCriteria) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Type == b.Type &&
a.Symbol == b.Symbol &&
a.Name == b.Name &&
a.Amount == b.Amount &&
a.EnsPattern == b.EnsPattern &&
a.Decimals == b.Decimals &&
reflect.DeepEqual(a.ContractAddresses, b.ContractAddresses) &&
reflect.DeepEqual(a.TokenIds, b.TokenIds)
}

View File

@@ -0,0 +1,87 @@
package communities
import (
"crypto/ecdsa"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
type CommunityPrivilegedMemberSyncMessage struct {
CommunityPrivateKey *ecdsa.PrivateKey
Receivers []*ecdsa.PublicKey
CommunityPrivilegedUserSyncMessage *protobuf.CommunityPrivilegedUserSyncMessage
}
func (m *Manager) HandleRequestToJoinPrivilegedUserSyncMessage(message *protobuf.CommunityPrivilegedUserSyncMessage, communityID types.HexBytes) ([]*RequestToJoin, error) {
var state RequestToJoinState
if message.Type == protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_ACCEPT_REQUEST_TO_JOIN {
state = RequestToJoinStateAccepted
} else {
state = RequestToJoinStateDeclined
}
requestsToJoin := make([]*RequestToJoin, 0)
for signer, requestToJoinProto := range message.RequestToJoin {
requestToJoin := &RequestToJoin{
PublicKey: signer,
Clock: requestToJoinProto.Clock,
ENSName: requestToJoinProto.EnsName,
CommunityID: requestToJoinProto.CommunityId,
State: state,
RevealedAccounts: requestToJoinProto.RevealedAccounts,
}
requestToJoin.CalculateID()
if _, err := m.saveOrUpdateRequestToJoin(communityID, requestToJoin); err != nil {
return nil, err
}
if err := m.persistence.RemoveRequestToJoinRevealedAddresses(requestToJoin.ID); err != nil {
return nil, err
}
if requestToJoin.RevealedAccounts != nil && len(requestToJoin.RevealedAccounts) > 0 {
if err := m.persistence.SaveRequestToJoinRevealedAddresses(requestToJoin.ID, requestToJoin.RevealedAccounts); err != nil {
return nil, err
}
}
requestsToJoin = append(requestsToJoin, requestToJoin)
}
return requestsToJoin, nil
}
func (m *Manager) HandleSyncAllRequestToJoinForNewPrivilegedMember(message *protobuf.CommunityPrivilegedUserSyncMessage, communityID types.HexBytes) ([]*RequestToJoin, error) {
nonAcceptedRequestsToJoin := []*RequestToJoin{}
myPk := common.PubkeyToHex(&m.identity.PublicKey)
// We received all requests to join from the control node. Remove all requests to join except our own
err := m.persistence.RemoveAllCommunityRequestsToJoinWithRevealedAddressesExceptPublicKey(myPk, communityID)
if err != nil {
return nil, err
}
for _, syncRequestToJoin := range message.SyncRequestsToJoin {
requestToJoin := new(RequestToJoin)
requestToJoin.InitFromSyncProtobuf(syncRequestToJoin)
if _, err := m.saveOrUpdateRequestToJoin(communityID, requestToJoin); err != nil {
return nil, err
}
if requestToJoin.RevealedAccounts != nil && len(requestToJoin.RevealedAccounts) > 0 {
if err := m.persistence.SaveRequestToJoinRevealedAddresses(requestToJoin.ID, requestToJoin.RevealedAccounts); err != nil {
return nil, err
}
}
if requestToJoin.State != RequestToJoinStateAccepted {
nonAcceptedRequestsToJoin = append(nonAcceptedRequestsToJoin, requestToJoin)
}
}
return nonAcceptedRequestsToJoin, nil
}

View File

@@ -0,0 +1,47 @@
package communities
import "errors"
var ErrChatNotFound = errors.New("chat not found")
var ErrCategoryNotFound = errors.New("category not found")
var ErrNoChangeInPosition = errors.New("no change in category position")
var ErrChatAlreadyAssigned = errors.New("chat already assigned to a category")
var ErrOrgNotFound = errors.New("community not found")
var ErrOrgAlreadyJoined = errors.New("community already joined")
var ErrChatAlreadyExists = errors.New("chat already exists")
var ErrCategoryAlreadyExists = errors.New("category already exists")
var ErrCantRequestAccess = errors.New("can't request access")
var ErrInvalidCommunityDescription = errors.New("invalid community description")
var ErrInvalidCommunityDescriptionNoOrgPermissions = errors.New("invalid community description no org permissions")
var ErrInvalidCommunityDescriptionNoChatPermissions = errors.New("invalid community description no chat permissions")
var ErrInvalidCommunityDescriptionUnknownChatAccess = errors.New("invalid community description unknown chat access")
var ErrInvalidCommunityDescriptionUnknownOrgAccess = errors.New("invalid community description unknown org access")
var ErrInvalidCommunityDescriptionMemberInChatButNotInOrg = errors.New("invalid community description member in chat but not in org")
var ErrInvalidCommunityDescriptionCategoryNoID = errors.New("invalid community category id")
var ErrInvalidCommunityDescriptionCategoryNoName = errors.New("invalid community category name")
var ErrInvalidCommunityDescriptionChatIdentity = errors.New("invalid community chat name, missing")
var ErrInvalidCommunityDescriptionDuplicatedName = errors.New("invalid community chat name, duplicated")
var ErrInvalidCommunityDescriptionUnknownChatCategory = errors.New("invalid community category in chat")
var ErrInvalidCommunityTags = errors.New("invalid community tags")
var ErrNotAdmin = errors.New("no admin privileges for this community")
var ErrNotOwner = errors.New("no owner privileges for this community")
var ErrNotControlNode = errors.New("not a control node")
var ErrInvalidGrant = errors.New("invalid grant")
var ErrNotAuthorized = errors.New("not authorized")
var ErrAlreadyMember = errors.New("already a member")
var ErrAlreadyJoined = errors.New("already joined")
var ErrInvalidMessage = errors.New("invalid community description message")
var ErrMemberNotFound = errors.New("member not found")
var ErrTokenPermissionAlreadyExists = errors.New("token permission already exists")
var ErrTokenPermissionNotFound = errors.New("token permission not found")
var ErrNoPermissionToJoin = errors.New("member has no permission to join")
var ErrMemberWalletAlreadyExists = errors.New("member wallet already exists")
var ErrMemberWalletNotFound = errors.New("member wallet not found")
var ErrNotEnoughPermissions = errors.New("not enough permissions for this community")
var ErrCannotRemoveOwnerOrAdmin = errors.New("not allowed to remove admin or owner")
var ErrCannotBanOwnerOrAdmin = errors.New("not allowed to ban admin or owner")
var ErrInvalidManageTokensPermission = errors.New("no privileges to manage tokens")
var ErrRevealedAccountsAbsent = errors.New("revealed accounts is absent")
var ErrNoRevealedAccountsSignature = errors.New("revealed accounts without the signature")
var ErrNoFreeSpaceForHistoryArchives = errors.New("history archive: No free space for downloading history archives")
var ErrPermissionToJoinNotSatisfied = errors.New("permission to join not satisfied")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,441 @@
package communities
import (
"context"
"errors"
"math"
"math/big"
"strconv"
"strings"
"go.uber.org/zap"
maps "golang.org/x/exp/maps"
slices "golang.org/x/exp/slices"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/protocol/ens"
"github.com/status-im/status-go/protocol/protobuf"
walletcommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty"
)
type PermissionChecker interface {
CheckPermissionToJoin(*Community, []gethcommon.Address) (*CheckPermissionToJoinResponse, error)
CheckPermissions(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error)
}
type DefaultPermissionChecker struct {
tokenManager TokenManager
collectiblesManager CollectiblesManager
ensVerifier *ens.Verifier
logger *zap.Logger
}
func (p *DefaultPermissionChecker) getOwnedENS(addresses []gethcommon.Address) ([]string, error) {
ownedENS := make([]string, 0)
if p.ensVerifier == nil {
p.logger.Warn("no ensVerifier configured for communities manager")
return ownedENS, nil
}
for _, address := range addresses {
name, err := p.ensVerifier.ReverseResolve(address)
if err != nil && err.Error() != "not a resolver" {
return ownedENS, err
}
if name != "" {
ownedENS = append(ownedENS, name)
}
}
return ownedENS, nil
}
func (p *DefaultPermissionChecker) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
if p.collectiblesManager == nil {
return nil, errors.New("no collectibles manager")
}
ctx := context.Background()
ownedERC721Tokens := make(CollectiblesByChain)
for chainID, erc721Tokens := range tokenRequirements {
skipChain := true
for _, cID := range chainIDs {
if chainID == cID {
skipChain = false
}
}
if skipChain {
continue
}
contractAddresses := make([]gethcommon.Address, 0)
for contractAddress := range erc721Tokens {
contractAddresses = append(contractAddresses, gethcommon.HexToAddress(contractAddress))
}
if _, exists := ownedERC721Tokens[chainID]; !exists {
ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
}
for _, owner := range walletAddresses {
balances, err := p.collectiblesManager.FetchBalancesByOwnerAndContractAddress(ctx, walletcommon.ChainID(chainID), owner, contractAddresses)
if err != nil {
p.logger.Info("couldn't fetch owner assets", zap.Error(err))
return nil, err
}
ownedERC721Tokens[chainID][owner] = balances
}
}
return ownedERC721Tokens, nil
}
func (p *DefaultPermissionChecker) accountChainsCombinationToMap(combinations []*AccountChainIDsCombination) map[gethcommon.Address][]uint64 {
result := make(map[gethcommon.Address][]uint64)
for _, combination := range combinations {
result[combination.Address] = combination.ChainIDs
}
return result
}
// merge valid combinations w/o duplicates
func (p *DefaultPermissionChecker) MergeValidCombinations(left, right []*AccountChainIDsCombination) []*AccountChainIDsCombination {
leftMap := p.accountChainsCombinationToMap(left)
rightMap := p.accountChainsCombinationToMap(right)
// merge maps, result in left map
for k, v := range rightMap {
if _, exists := leftMap[k]; !exists {
leftMap[k] = v
continue
} else {
// append chains which are new
chains := leftMap[k]
for _, chainID := range v {
if !slices.Contains(chains, chainID) {
chains = append(chains, chainID)
}
}
leftMap[k] = chains
}
}
result := []*AccountChainIDsCombination{}
for k, v := range leftMap {
result = append(result, &AccountChainIDsCombination{
Address: k,
ChainIDs: v,
})
}
return result
}
func (p *DefaultPermissionChecker) CheckPermissionToJoin(community *Community, addresses []gethcommon.Address) (*CheckPermissionToJoinResponse, error) {
becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
becomeTokenMasterPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)
adminOrTokenMasterPermissionsToJoin := append(becomeAdminPermissions, becomeTokenMasterPermissions...)
allChainIDs, err := p.tokenManager.GetAllChainIDs()
if err != nil {
return nil, err
}
accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)
// Check becomeMember and (admin & token master) permissions separately.
becomeMemberPermissionsResponse, err := p.checkPermissionsOrDefault(becomeMemberPermissions, accountsAndChainIDs)
if err != nil {
return nil, err
}
if len(adminOrTokenMasterPermissionsToJoin) <= 0 {
return becomeMemberPermissionsResponse, nil
}
// If there are any admin or token master permissions, combine result.
adminOrTokenPermissionsResponse, err := p.CheckPermissions(adminOrTokenMasterPermissionsToJoin, accountsAndChainIDs, false)
if err != nil {
return nil, err
}
mergedPermissions := make(map[string]*PermissionTokenCriteriaResult)
maps.Copy(mergedPermissions, becomeMemberPermissionsResponse.Permissions)
maps.Copy(mergedPermissions, adminOrTokenPermissionsResponse.Permissions)
mergedCombinations := p.MergeValidCombinations(becomeMemberPermissionsResponse.ValidCombinations, adminOrTokenPermissionsResponse.ValidCombinations)
combinedResponse := &CheckPermissionsResponse{
Satisfied: becomeMemberPermissionsResponse.Satisfied || adminOrTokenPermissionsResponse.Satisfied,
Permissions: mergedPermissions,
ValidCombinations: mergedCombinations,
}
return combinedResponse, nil
}
func (p *DefaultPermissionChecker) checkPermissionsOrDefault(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination) (*CheckPermissionsResponse, error) {
if len(permissions) == 0 {
// There are no permissions to join on this community at the moment,
// so we reveal all accounts + all chain IDs
response := &CheckPermissionsResponse{
Satisfied: true,
Permissions: make(map[string]*PermissionTokenCriteriaResult),
ValidCombinations: accountsAndChainIDs,
}
return response, nil
}
return p.CheckPermissions(permissions, accountsAndChainIDs, false)
}
// CheckPermissions will retrieve balances and check whether the user has
// permission to join the community, if shortcircuit is true, it will stop as soon
// as we know the answer
func (p *DefaultPermissionChecker) CheckPermissions(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
response := &CheckPermissionsResponse{
Satisfied: false,
Permissions: make(map[string]*PermissionTokenCriteriaResult),
ValidCombinations: make([]*AccountChainIDsCombination, 0),
}
erc20TokenRequirements, erc721TokenRequirements, _ := ExtractTokenCriteria(permissions)
erc20ChainIDsMap := make(map[uint64]bool)
erc721ChainIDsMap := make(map[uint64]bool)
erc20TokenAddresses := make([]gethcommon.Address, 0)
accounts := make([]gethcommon.Address, 0)
for _, accountAndChainIDs := range accountsAndChainIDs {
accounts = append(accounts, accountAndChainIDs.Address)
}
// figure out chain IDs we're interested in
for chainID, tokens := range erc20TokenRequirements {
erc20ChainIDsMap[chainID] = true
for contractAddress := range tokens {
erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
}
}
for chainID := range erc721TokenRequirements {
erc721ChainIDsMap[chainID] = true
}
chainIDsForERC20 := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsMap)
chainIDsForERC721 := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsMap)
// if there are no chain IDs that match token criteria chain IDs
// we aren't able to check balances on selected networks
if len(erc20ChainIDsMap) > 0 && len(chainIDsForERC20) == 0 {
response.NetworksNotSupported = true
return response, nil
}
ownedERC20TokenBalances := make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, 0)
if len(chainIDsForERC20) > 0 {
// this only returns balances for the networks we're actually interested in
balances, err := p.tokenManager.GetBalancesByChain(context.Background(), accounts, erc20TokenAddresses, chainIDsForERC20)
if err != nil {
return nil, err
}
ownedERC20TokenBalances = balances
}
ownedERC721Tokens := make(CollectiblesByChain)
if len(chainIDsForERC721) > 0 {
collectibles, err := p.GetOwnedERC721Tokens(accounts, erc721TokenRequirements, chainIDsForERC721)
if err != nil {
return nil, err
}
ownedERC721Tokens = collectibles
}
accountsChainIDsCombinations := make(map[gethcommon.Address]map[uint64]bool)
for _, tokenPermission := range permissions {
permissionRequirementsMet := true
response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{Role: tokenPermission.Type}
// There can be multiple token requirements per permission.
// If only one is not met, the entire permission is marked
// as not fulfilled
for _, tokenRequirement := range tokenPermission.TokenCriteria {
tokenRequirementMet := false
tokenRequirementResponse := TokenRequirementResponse{TokenCriteria: tokenRequirement}
if tokenRequirement.Type == protobuf.CommunityTokenType_ERC721 {
if len(ownedERC721Tokens) == 0 {
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false)
continue
}
chainIDLoopERC721:
for chainID, addressStr := range tokenRequirement.ContractAddresses {
contractAddress := gethcommon.HexToAddress(addressStr)
if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 {
continue chainIDLoopERC721
}
for account := range ownedERC721Tokens[chainID] {
if _, exists := ownedERC721Tokens[chainID][account]; !exists {
continue
}
tokenBalances := ownedERC721Tokens[chainID][account][contractAddress]
if len(tokenBalances) > 0 {
// 'account' owns some TokenID owned from contract 'address'
if _, exists := accountsChainIDsCombinations[account]; !exists {
accountsChainIDsCombinations[account] = make(map[uint64]bool)
}
if len(tokenRequirement.TokenIds) == 0 {
// no specific tokenId of this collection is needed
tokenRequirementMet = true
accountsChainIDsCombinations[account][chainID] = true
break chainIDLoopERC721
}
tokenIDsLoop:
for _, tokenID := range tokenRequirement.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
for _, asset := range tokenBalances {
if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
tokenRequirementMet = true
accountsChainIDsCombinations[account][chainID] = true
break tokenIDsLoop
}
}
}
}
}
}
} else if tokenRequirement.Type == protobuf.CommunityTokenType_ERC20 {
if len(ownedERC20TokenBalances) == 0 {
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false)
continue
}
accumulatedBalance := new(big.Float)
chainIDLoopERC20:
for chainID, address := range tokenRequirement.ContractAddresses {
if _, exists := ownedERC20TokenBalances[chainID]; !exists || len(ownedERC20TokenBalances[chainID]) == 0 {
continue chainIDLoopERC20
}
contractAddress := gethcommon.HexToAddress(address)
for account := range ownedERC20TokenBalances[chainID] {
if _, exists := ownedERC20TokenBalances[chainID][account][contractAddress]; !exists {
continue
}
value := ownedERC20TokenBalances[chainID][account][contractAddress]
accountChainBalance := new(big.Float).Quo(
new(big.Float).SetInt(value.ToInt()),
big.NewFloat(math.Pow(10, float64(tokenRequirement.Decimals))),
)
if _, exists := accountsChainIDsCombinations[account]; !exists {
accountsChainIDsCombinations[account] = make(map[uint64]bool)
}
if accountChainBalance.Cmp(big.NewFloat(0)) > 0 {
// account has balance > 0 on this chain for this token, so let's add it the chain IDs
accountsChainIDsCombinations[account][chainID] = true
}
// check if adding current chain account balance to accumulated balance
// satisfies required amount
prevBalance := accumulatedBalance
accumulatedBalance.Add(prevBalance, accountChainBalance)
requiredAmount, err := strconv.ParseFloat(tokenRequirement.Amount, 32)
if err != nil {
return nil, err
}
if accumulatedBalance.Cmp(big.NewFloat(requiredAmount)) != -1 {
tokenRequirementMet = true
if shortcircuit {
break chainIDLoopERC20
}
}
}
}
} else if tokenRequirement.Type == protobuf.CommunityTokenType_ENS {
for _, account := range accounts {
ownedENSNames, err := p.getOwnedENS([]gethcommon.Address{account})
if err != nil {
return nil, err
}
if _, exists := accountsChainIDsCombinations[account]; !exists {
accountsChainIDsCombinations[account] = make(map[uint64]bool)
}
if !strings.HasPrefix(tokenRequirement.EnsPattern, "*.") {
for _, ownedENS := range ownedENSNames {
if ownedENS == tokenRequirement.EnsPattern {
tokenRequirementMet = true
accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
}
}
} else {
parentName := tokenRequirement.EnsPattern[2:]
for _, ownedENS := range ownedENSNames {
if strings.HasSuffix(ownedENS, parentName) {
tokenRequirementMet = true
accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
}
}
}
}
}
if !tokenRequirementMet {
permissionRequirementsMet = false
}
tokenRequirementResponse.Satisfied = tokenRequirementMet
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementMet)
}
// multiple permissions are treated as logical OR, meaning
// if only one of them is fulfilled, the user gets permission
// to join and we can stop early
if shortcircuit && permissionRequirementsMet {
break
}
}
// attach valid account and chainID combinations to response
for account, chainIDs := range accountsChainIDsCombinations {
combination := &AccountChainIDsCombination{
Address: account,
}
for chainID := range chainIDs {
combination.ChainIDs = append(combination.ChainIDs, chainID)
}
response.ValidCombinations = append(response.ValidCombinations, combination)
}
response.calculateSatisfied()
return response, nil
}

View File

@@ -0,0 +1,284 @@
package communities
import (
"context"
"math/big"
"strconv"
"github.com/pkg/errors"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/thirdparty"
)
type PermissionedBalance struct {
Type protobuf.CommunityTokenType `json:"type"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Amount *bigint.BigInt `json:"amount"`
Decimals uint64 `json:"decimals"`
}
func calculatePermissionedBalancesERC20(
accountAddresses []gethcommon.Address,
balances BalancesByChain,
tokenPermissions []*CommunityTokenPermission,
) map[gethcommon.Address]map[string]*PermissionedBalance {
res := make(map[gethcommon.Address]map[string]*PermissionedBalance)
// Set with composite key (chain ID + wallet address + contract address) to
// store if we already processed the balance.
usedBalances := make(map[string]bool)
for _, permission := range tokenPermissions {
for _, criteria := range permission.TokenCriteria {
if criteria.Type != protobuf.CommunityTokenType_ERC20 {
continue
}
for _, accountAddress := range accountAddresses {
for chainID, hexContractAddress := range criteria.ContractAddresses {
usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress
if _, ok := balances[chainID]; !ok {
continue
}
if _, ok := balances[chainID][accountAddress]; !ok {
continue
}
contractAddress := gethcommon.HexToAddress(hexContractAddress)
value, ok := balances[chainID][accountAddress][contractAddress]
if !ok {
continue
}
// Skip the contract address if it has been used already in the sum.
if _, ok := usedBalances[usedKey]; ok {
continue
}
if _, ok := res[accountAddress]; !ok {
res[accountAddress] = make(map[string]*PermissionedBalance, 0)
}
if _, ok := res[accountAddress][criteria.Symbol]; !ok {
res[accountAddress][criteria.Symbol] = &PermissionedBalance{
Type: criteria.Type,
Symbol: criteria.Symbol,
Name: criteria.Name,
Decimals: criteria.Decimals,
Amount: &bigint.BigInt{Int: big.NewInt(0)},
}
}
res[accountAddress][criteria.Symbol].Amount.Add(
res[accountAddress][criteria.Symbol].Amount.Int,
value.ToInt(),
)
usedBalances[usedKey] = true
}
}
}
}
return res
}
func isERC721CriteriaSatisfied(tokenBalances []thirdparty.TokenBalance, criteria *protobuf.TokenCriteria) bool {
// No token IDs to compare against, so the criteria is satisfied.
if len(criteria.TokenIds) == 0 {
return true
}
for _, tokenID := range criteria.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
for _, asset := range tokenBalances {
if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
return true
}
}
}
return false
}
func (m *Manager) calculatePermissionedBalancesERC721(
accountAddresses []gethcommon.Address,
balances CollectiblesByChain,
tokenPermissions []*CommunityTokenPermission,
) map[gethcommon.Address]map[string]*PermissionedBalance {
res := make(map[gethcommon.Address]map[string]*PermissionedBalance)
// Set with composite key (chain ID + wallet address + contract address) to
// store if we already processed the balance.
usedBalances := make(map[string]bool)
for _, permission := range tokenPermissions {
for _, criteria := range permission.TokenCriteria {
if criteria.Type != protobuf.CommunityTokenType_ERC721 {
continue
}
for _, accountAddress := range accountAddresses {
for chainID, hexContractAddress := range criteria.ContractAddresses {
usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress
if _, ok := balances[chainID]; !ok {
continue
}
if _, ok := balances[chainID][accountAddress]; !ok {
continue
}
contractAddress := gethcommon.HexToAddress(hexContractAddress)
tokenBalances, ok := balances[chainID][accountAddress][contractAddress]
if !ok || len(tokenBalances) == 0 {
continue
}
// Skip the contract address if it has been used already in the sum.
if _, ok := usedBalances[usedKey]; ok {
continue
}
usedBalances[usedKey] = true
if _, ok := res[accountAddress]; !ok {
res[accountAddress] = make(map[string]*PermissionedBalance, 0)
}
if _, ok := res[accountAddress][criteria.Symbol]; !ok {
res[accountAddress][criteria.Symbol] = &PermissionedBalance{
Type: criteria.Type,
Symbol: criteria.Symbol,
Name: criteria.Name,
Decimals: criteria.Decimals,
Amount: &bigint.BigInt{Int: big.NewInt(0)},
}
}
if isERC721CriteriaSatisfied(tokenBalances, criteria) {
// We don't care about summing balances, thus setting as 1 is
// sufficient.
res[accountAddress][criteria.Symbol].Amount = &bigint.BigInt{Int: big.NewInt(1)}
}
}
}
}
}
return res
}
func (m *Manager) calculatePermissionedBalances(
chainIDs []uint64,
accountAddresses []gethcommon.Address,
erc20Balances BalancesByChain,
erc721Balances CollectiblesByChain,
tokenPermissions []*CommunityTokenPermission,
) map[gethcommon.Address][]PermissionedBalance {
res := make(map[gethcommon.Address][]PermissionedBalance, 0)
aggregatedERC721Balances := m.calculatePermissionedBalancesERC721(accountAddresses, erc721Balances, tokenPermissions)
for accountAddress, tokens := range aggregatedERC721Balances {
for _, permissionedToken := range tokens {
if permissionedToken.Amount.Sign() > 0 {
res[accountAddress] = append(res[accountAddress], *permissionedToken)
}
}
}
aggregatedERC20Balances := calculatePermissionedBalancesERC20(accountAddresses, erc20Balances, tokenPermissions)
for accountAddress, tokens := range aggregatedERC20Balances {
for _, permissionedToken := range tokens {
if permissionedToken.Amount.Sign() > 0 {
res[accountAddress] = append(res[accountAddress], *permissionedToken)
}
}
}
return res
}
func keepRoleTokenPermissions(tokenPermissions map[string]*CommunityTokenPermission) []*CommunityTokenPermission {
res := make([]*CommunityTokenPermission, 0)
for _, p := range tokenPermissions {
if p.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER ||
p.Type == protobuf.CommunityTokenPermission_BECOME_ADMIN ||
p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER ||
p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER {
res = append(res, p)
}
}
return res
}
// GetPermissionedBalances returns balances indexed by account address.
//
// It assumes balances in different chains with the same symbol can be summed.
// It also assumes the criteria's decimals field is the same across different
// criteria when they refer to the same asset (by symbol).
func (m *Manager) GetPermissionedBalances(
ctx context.Context,
communityID types.HexBytes,
accountAddresses []gethcommon.Address,
) (map[gethcommon.Address][]PermissionedBalance, error) {
community, err := m.GetByID(communityID)
if err != nil {
return nil, err
}
if community == nil {
return nil, errors.Errorf("community does not exist ID='%s'", communityID)
}
tokenPermissions := keepRoleTokenPermissions(community.TokenPermissions())
allChainIDs, err := m.tokenManager.GetAllChainIDs()
if err != nil {
return nil, err
}
accountsAndChainIDs := combineAddressesAndChainIDs(accountAddresses, allChainIDs)
erc20TokenCriteriaByChain, erc721TokenCriteriaByChain, _ := ExtractTokenCriteria(tokenPermissions)
accounts := make([]gethcommon.Address, 0, len(accountsAndChainIDs))
for _, accountAndChainIDs := range accountsAndChainIDs {
accounts = append(accounts, accountAndChainIDs.Address)
}
erc20ChainIDsSet := make(map[uint64]bool)
erc20TokenAddresses := make([]gethcommon.Address, 0)
for chainID, criterionByContractAddress := range erc20TokenCriteriaByChain {
erc20ChainIDsSet[chainID] = true
for contractAddress := range criterionByContractAddress {
erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
}
}
erc721ChainIDsSet := make(map[uint64]bool)
for chainID := range erc721TokenCriteriaByChain {
erc721ChainIDsSet[chainID] = true
}
erc20ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsSet)
erc721ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsSet)
erc20Balances, err := m.tokenManager.GetBalancesByChain(ctx, accounts, erc20TokenAddresses, erc20ChainIDs)
if err != nil {
return nil, err
}
erc721Balances := make(CollectiblesByChain)
if len(erc721ChainIDs) > 0 {
balances, err := m.GetOwnedERC721Tokens(accounts, erc721TokenCriteriaByChain, erc721ChainIDs)
if err != nil {
return nil, err
}
erc721Balances = balances
}
return m.calculatePermissionedBalances(allChainIDs, accountAddresses, erc20Balances, erc721Balances, tokenPermissions), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
package communities
import (
"crypto/ecdsa"
"go.uber.org/zap"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/common/shard"
)
func communityToRecord(community *Community) (*CommunityRecord, error) {
wrappedDescription, err := community.ToProtocolMessageBytes()
if err != nil {
return nil, err
}
var shardIndex, shardCluster *uint
if community.Shard() != nil {
index := uint(community.Shard().Index)
shardIndex = &index
cluster := uint(community.Shard().Cluster)
shardCluster = &cluster
}
return &CommunityRecord{
id: community.ID(),
privateKey: crypto.FromECDSA(community.PrivateKey()),
controlNode: crypto.FromECDSAPub(community.ControlNode()),
description: wrappedDescription,
joined: community.config.Joined,
joinedAt: community.config.JoinedAt,
lastOpenedAt: community.config.LastOpenedAt,
verified: community.config.Verified,
spectated: community.config.Spectated,
muted: community.config.Muted,
mutedTill: community.config.MuteTill,
shardCluster: shardCluster,
shardIndex: shardIndex,
}, nil
}
func communityToEventsRecord(community *Community) (*EventsRecord, error) {
if community.config.EventsData == nil {
return nil, nil
}
rawEvents, err := communityEventsToJSONEncodedBytes(community.config.EventsData.Events)
if err != nil {
return nil, err
}
return &EventsRecord{
id: community.ID(),
rawEvents: rawEvents,
rawDescription: community.config.EventsData.EventsBaseCommunityDescription,
}, nil
}
func recordToRequestToJoin(r *RequestToJoinRecord) *RequestToJoin {
// FIXME: fill revealed addresses
return &RequestToJoin{
ID: r.id,
PublicKey: r.publicKey,
Clock: uint64(r.clock),
ENSName: r.ensName,
ChatID: r.chatID,
CommunityID: r.communityID,
State: RequestToJoinState(r.state),
}
}
func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.PublicKey, installationID string,
logger *zap.Logger, timesource common.TimeSource, encryptor DescriptionEncryptor, initializer func(*Community) error) (*Community, error) {
var privateKey *ecdsa.PrivateKey
var controlNode *ecdsa.PublicKey
var err error
if r.community.privateKey != nil {
privateKey, err = crypto.ToECDSA(r.community.privateKey)
if err != nil {
return nil, err
}
}
if r.community.controlNode != nil {
controlNode, err = crypto.UnmarshalPubkey(r.community.controlNode)
if err != nil {
return nil, err
}
}
description, err := decodeWrappedCommunityDescription(r.community.description)
if err != nil {
return nil, err
}
id, err := crypto.DecompressPubkey(r.community.id)
if err != nil {
return nil, err
}
var eventsData *EventsData
if r.events != nil {
eventsData, err = decodeEventsData(r.events.rawEvents, r.events.rawDescription)
if err != nil {
return nil, err
}
}
var s *shard.Shard = nil
if r.community.shardCluster != nil && r.community.shardIndex != nil {
s = &shard.Shard{
Cluster: uint16(*r.community.shardCluster),
Index: uint16(*r.community.shardIndex),
}
}
isControlDevice := r.installationID != nil && *r.installationID == installationID
config := Config{
PrivateKey: privateKey,
ControlNode: controlNode,
ControlDevice: isControlDevice,
CommunityDescription: description,
MemberIdentity: memberIdentity,
CommunityDescriptionProtocolMessage: r.community.description,
Logger: logger,
ID: id,
Verified: r.community.verified,
Muted: r.community.muted,
MuteTill: r.community.mutedTill,
Joined: r.community.joined,
JoinedAt: r.community.joinedAt,
LastOpenedAt: r.community.lastOpenedAt,
Spectated: r.community.spectated,
EventsData: eventsData,
Shard: s,
}
community, err := New(config, timesource, encryptor)
if err != nil {
return nil, err
}
if r.requestToJoin != nil {
community.config.RequestedToJoinAt = uint64(r.requestToJoin.clock)
requestToJoin := recordToRequestToJoin(r.requestToJoin)
if !requestToJoin.Empty() {
community.AddRequestToJoin(requestToJoin)
}
}
if initializer != nil {
err = initializer(community)
if err != nil {
return nil, err
}
}
return community, nil
}

View File

@@ -0,0 +1,132 @@
package communities
import (
"database/sql"
"github.com/status-im/status-go/protocol/protobuf"
)
type RawCommunityRow struct {
ID []byte
PrivateKey []byte
Description []byte
Joined bool
JoinedAt int64
Spectated bool
Verified bool
SyncedAt uint64
Muted bool
LastOpenedAt int64
}
func fromSyncCommunityProtobuf(syncCommProto *protobuf.SyncInstallationCommunity) RawCommunityRow {
return RawCommunityRow{
ID: syncCommProto.Id,
Description: syncCommProto.Description,
Joined: syncCommProto.Joined,
JoinedAt: syncCommProto.JoinedAt,
Spectated: syncCommProto.Spectated,
Verified: syncCommProto.Verified,
SyncedAt: syncCommProto.Clock,
Muted: syncCommProto.Muted,
LastOpenedAt: syncCommProto.LastOpenedAt,
}
}
func (p *Persistence) scanRowToStruct(rowScan func(dest ...interface{}) error) (*RawCommunityRow, error) {
rcr := new(RawCommunityRow)
var syncedAt, muteTill sql.NullTime
err := rowScan(
&rcr.ID,
&rcr.PrivateKey,
&rcr.Description,
&rcr.Joined,
&rcr.JoinedAt,
&rcr.Verified,
&rcr.Spectated,
&rcr.Muted,
&muteTill,
&syncedAt,
&rcr.LastOpenedAt,
)
if syncedAt.Valid {
rcr.SyncedAt = uint64(syncedAt.Time.Unix())
}
if err != nil {
return nil, err
}
return rcr, nil
}
func (p *Persistence) getAllCommunitiesRaw() (rcrs []*RawCommunityRow, err error) {
var rows *sql.Rows
// Keep "*", if the db table is updated, syncing needs to match, this fail will force us to update syncing.
rows, err = p.db.Query(`SELECT id, private_key, description, joined, joined_at, verified, spectated, muted, muted_till, synced_at, last_opened_at FROM communities_communities`)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
// Don't shadow original error
_ = rows.Close()
return
}
err = rows.Close()
}()
for rows.Next() {
rcr, err := p.scanRowToStruct(rows.Scan)
if err != nil {
return nil, err
}
rcrs = append(rcrs, rcr)
}
return rcrs, nil
}
func (p *Persistence) getRawCommunityRow(id []byte) (*RawCommunityRow, error) {
qr := p.db.QueryRow(`SELECT id, private_key, description, joined, joined_at, verified, spectated, muted, muted_till, synced_at, last_opened_at FROM communities_communities WHERE id = ?`, id)
return p.scanRowToStruct(qr.Scan)
}
func (p *Persistence) getSyncedRawCommunity(id []byte) (*RawCommunityRow, error) {
qr := p.db.QueryRow(`SELECT id, private_key, description, joined, joined_at, verified, spectated, muted, muted_till, synced_at, last_opened_at FROM communities_communities WHERE id = ? AND synced_at > 0`, id)
return p.scanRowToStruct(qr.Scan)
}
func (p *Persistence) saveRawCommunityRow(rawCommRow RawCommunityRow) error {
_, err := p.db.Exec(
`INSERT INTO communities_communities ("id", "private_key", "description", "joined", "joined_at", "verified", "synced_at", "muted", "last_opened_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
rawCommRow.ID,
rawCommRow.PrivateKey,
rawCommRow.Description,
rawCommRow.Joined,
rawCommRow.JoinedAt,
rawCommRow.Verified,
rawCommRow.SyncedAt,
rawCommRow.Muted,
rawCommRow.LastOpenedAt,
)
return err
}
func (p *Persistence) saveRawCommunityRowWithoutSyncedAt(rawCommRow RawCommunityRow) error {
_, err := p.db.Exec(
`INSERT INTO communities_communities ("id", "private_key", "description", "joined", "joined_at", "verified", "muted", "last_opened_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
rawCommRow.ID,
rawCommRow.PrivateKey,
rawCommRow.Description,
rawCommRow.Joined,
rawCommRow.JoinedAt,
rawCommRow.Verified,
rawCommRow.Muted,
rawCommRow.LastOpenedAt,
)
return err
}

View File

@@ -0,0 +1,101 @@
package communities
import (
"fmt"
"strconv"
"time"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/protobuf"
)
type RequestToJoinState uint
const (
RequestToJoinStatePending RequestToJoinState = iota + 1
RequestToJoinStateDeclined
RequestToJoinStateAccepted
RequestToJoinStateCanceled
RequestToJoinStateAcceptedPending
RequestToJoinStateDeclinedPending
RequestToJoinStateAwaitingAddresses
)
type RequestToJoin struct {
ID types.HexBytes `json:"id"`
PublicKey string `json:"publicKey"`
Clock uint64 `json:"clock"`
ENSName string `json:"ensName,omitempty"`
ChatID string `json:"chatId"`
CommunityID types.HexBytes `json:"communityId"`
State RequestToJoinState `json:"state"`
Our bool `json:"our"`
Deleted bool `json:"deleted"`
RevealedAccounts []*protobuf.RevealedAccount `json:"revealedAccounts,omitempty"`
}
func (r *RequestToJoin) CalculateID() {
r.ID = CalculateRequestID(r.PublicKey, r.CommunityID)
}
func (r *RequestToJoin) ToCommunityRequestToJoinProtobuf() *protobuf.CommunityRequestToJoin {
return &protobuf.CommunityRequestToJoin{
Clock: r.Clock,
EnsName: r.ENSName,
CommunityId: r.CommunityID,
RevealedAccounts: r.RevealedAccounts,
}
}
func (r *RequestToJoin) ToSyncProtobuf() *protobuf.SyncCommunityRequestsToJoin {
return &protobuf.SyncCommunityRequestsToJoin{
Id: r.ID,
PublicKey: r.PublicKey,
Clock: r.Clock,
EnsName: r.ENSName,
ChatId: r.ChatID,
CommunityId: r.CommunityID,
State: uint64(r.State),
RevealedAccounts: r.RevealedAccounts,
}
}
func (r *RequestToJoin) InitFromSyncProtobuf(proto *protobuf.SyncCommunityRequestsToJoin) {
r.ID = proto.Id
r.PublicKey = proto.PublicKey
r.Clock = proto.Clock
r.ENSName = proto.EnsName
r.ChatID = proto.ChatId
r.CommunityID = proto.CommunityId
r.State = RequestToJoinState(proto.State)
r.RevealedAccounts = proto.RevealedAccounts
}
func (r *RequestToJoin) Empty() bool {
return len(r.ID)+len(r.PublicKey)+int(r.Clock)+len(r.ENSName)+len(r.ChatID)+len(r.CommunityID)+int(r.State) == 0
}
func (r *RequestToJoin) ShouldRetainDeclined(clock uint64) (bool, error) {
if r.State != RequestToJoinStateDeclined {
return false, nil
}
declineExpiryClock, err := AddTimeoutToRequestToJoinClock(r.Clock)
if err != nil {
return false, err
}
return clock < declineExpiryClock, nil
}
func AddTimeoutToRequestToJoinClock(clock uint64) (uint64, error) {
requestToJoinClock, err := strconv.ParseInt(fmt.Sprint(clock), 10, 64)
if err != nil {
return 0, err
}
// Adding 7 days to the request clock
requestTimeOutClock := uint64(time.Unix(requestToJoinClock, 0).AddDate(0, 0, 7).Unix())
return requestTimeOutClock, nil
}

View File

@@ -0,0 +1,22 @@
package communities
import (
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/protobuf"
)
type RequestToLeave struct {
ID types.HexBytes `json:"id"`
PublicKey string `json:"publicKey"`
Clock uint64 `json:"clock"`
CommunityID types.HexBytes `json:"communityId"`
}
func NewRequestToLeave(publicKey string, protobuf *protobuf.CommunityRequestToLeave) *RequestToLeave {
return &RequestToLeave{
ID: CalculateRequestID(publicKey, protobuf.CommunityId),
PublicKey: publicKey,
Clock: protobuf.Clock,
CommunityID: protobuf.CommunityId,
}
}

View File

@@ -0,0 +1,120 @@
package communities
import (
"golang.org/x/exp/slices"
"github.com/status-im/status-go/protocol/protobuf"
)
var adminAuthorizedEventTypes = []protobuf.CommunityEvent_EventType{
protobuf.CommunityEvent_COMMUNITY_EDIT,
protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE,
protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE,
protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE,
protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE,
protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT,
protobuf.CommunityEvent_COMMUNITY_CHANNEL_CREATE,
protobuf.CommunityEvent_COMMUNITY_CHANNEL_DELETE,
protobuf.CommunityEvent_COMMUNITY_CHANNEL_EDIT,
protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER,
protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER,
protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT,
protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT,
protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK,
protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN,
protobuf.CommunityEvent_COMMUNITY_MEMBER_UNBAN,
}
var tokenMasterAuthorizedEventTypes = append(adminAuthorizedEventTypes, []protobuf.CommunityEvent_EventType{
protobuf.CommunityEvent_COMMUNITY_TOKEN_ADD,
}...)
var ownerAuthorizedEventTypes = tokenMasterAuthorizedEventTypes
var rolesToAuthorizedEventTypes = map[protobuf.CommunityMember_Roles][]protobuf.CommunityEvent_EventType{
protobuf.CommunityMember_ROLE_NONE: []protobuf.CommunityEvent_EventType{},
protobuf.CommunityMember_ROLE_OWNER: ownerAuthorizedEventTypes,
protobuf.CommunityMember_ROLE_ADMIN: adminAuthorizedEventTypes,
protobuf.CommunityMember_ROLE_TOKEN_MASTER: tokenMasterAuthorizedEventTypes,
}
var adminAuthorizedPermissionTypes = []protobuf.CommunityTokenPermission_Type{
protobuf.CommunityTokenPermission_BECOME_MEMBER,
protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL,
}
var tokenMasterAuthorizedPermissionTypes = append(adminAuthorizedPermissionTypes, []protobuf.CommunityTokenPermission_Type{}...)
var ownerAuthorizedPermissionTypes = append(tokenMasterAuthorizedPermissionTypes, []protobuf.CommunityTokenPermission_Type{
protobuf.CommunityTokenPermission_BECOME_ADMIN,
protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
}...)
var rolesToAuthorizedPermissionTypes = map[protobuf.CommunityMember_Roles][]protobuf.CommunityTokenPermission_Type{
protobuf.CommunityMember_ROLE_NONE: []protobuf.CommunityTokenPermission_Type{},
protobuf.CommunityMember_ROLE_OWNER: ownerAuthorizedPermissionTypes,
protobuf.CommunityMember_ROLE_ADMIN: adminAuthorizedPermissionTypes,
protobuf.CommunityMember_ROLE_TOKEN_MASTER: tokenMasterAuthorizedPermissionTypes,
}
func canRolesPerformEvent(roles []protobuf.CommunityMember_Roles, eventType protobuf.CommunityEvent_EventType) bool {
for _, role := range roles {
if slices.Contains(rolesToAuthorizedEventTypes[role], eventType) {
return true
}
}
return false
}
func canRolesModifyPermission(roles []protobuf.CommunityMember_Roles, permissionType protobuf.CommunityTokenPermission_Type) bool {
for _, role := range roles {
if slices.Contains(rolesToAuthorizedPermissionTypes[role], permissionType) {
return true
}
}
return false
}
func canRolesKickOrBanMember(senderRoles []protobuf.CommunityMember_Roles, memberRoles []protobuf.CommunityMember_Roles) bool {
// Owner can kick everyone
if slices.Contains(senderRoles, protobuf.CommunityMember_ROLE_OWNER) {
return true
}
// TokenMaster can kick normal members and admins
if (slices.Contains(senderRoles, protobuf.CommunityMember_ROLE_TOKEN_MASTER)) &&
!(slices.Contains(memberRoles, protobuf.CommunityMember_ROLE_TOKEN_MASTER) ||
slices.Contains(memberRoles, protobuf.CommunityMember_ROLE_OWNER)) {
return true
}
// Admins can kick normal members
if (slices.Contains(senderRoles, protobuf.CommunityMember_ROLE_ADMIN)) &&
!(slices.Contains(memberRoles, protobuf.CommunityMember_ROLE_ADMIN) ||
slices.Contains(memberRoles, protobuf.CommunityMember_ROLE_TOKEN_MASTER) ||
slices.Contains(memberRoles, protobuf.CommunityMember_ROLE_OWNER)) {
return true
}
// Normal members can't kick anyone
return false
}
func RolesAuthorizedToPerformEvent(senderRoles []protobuf.CommunityMember_Roles, memberRoles []protobuf.CommunityMember_Roles, event *CommunityEvent) bool {
if !canRolesPerformEvent(senderRoles, event.Type) {
return false
}
if event.Type == protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE ||
event.Type == protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE {
return canRolesModifyPermission(senderRoles, event.TokenPermission.Type)
}
if event.Type == protobuf.CommunityEvent_COMMUNITY_MEMBER_BAN ||
event.Type == protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK {
return canRolesKickOrBanMember(senderRoles, memberRoles)
}
return true
}

View File

@@ -0,0 +1,41 @@
package token
import (
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/wallet/bigint"
)
type DeployState uint8
const (
Failed DeployState = iota
InProgress
Deployed
)
type PrivilegesLevel uint8
const (
OwnerLevel PrivilegesLevel = iota
MasterLevel
CommunityLevel
)
type CommunityToken struct {
TokenType protobuf.CommunityTokenType `json:"tokenType"`
CommunityID string `json:"communityId"`
Address string `json:"address"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Description string `json:"description"`
Supply *bigint.BigInt `json:"supply"`
InfiniteSupply bool `json:"infiniteSupply"`
Transferable bool `json:"transferable"`
RemoteSelfDestruct bool `json:"remoteSelfDestruct"`
ChainID int `json:"chainId"`
DeployState DeployState `json:"deployState"`
Base64Image string `json:"image"`
Decimals int `json:"decimals"`
Deployer string `json:"deployer"`
PrivilegesLevel PrivilegesLevel `json:"privilegesLevel"`
}

View File

@@ -0,0 +1,59 @@
package communities
import (
"fmt"
"strings"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/protobuf"
)
func CalculateRequestID(publicKey string, communityID types.HexBytes) types.HexBytes {
idString := fmt.Sprintf("%s-%s", publicKey, communityID)
return crypto.Keccak256([]byte(idString))
}
func ExtractTokenCriteria(permissions []*CommunityTokenPermission) (erc20TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, erc721TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, ensTokenCriteria []string) {
erc20TokenCriteria = make(map[uint64]map[string]*protobuf.TokenCriteria)
erc721TokenCriteria = make(map[uint64]map[string]*protobuf.TokenCriteria)
ensTokenCriteria = make([]string, 0)
for _, tokenPermission := range permissions {
for _, tokenRequirement := range tokenPermission.TokenCriteria {
isERC721 := tokenRequirement.Type == protobuf.CommunityTokenType_ERC721
isERC20 := tokenRequirement.Type == protobuf.CommunityTokenType_ERC20
isENS := tokenRequirement.Type == protobuf.CommunityTokenType_ENS
for chainID, contractAddress := range tokenRequirement.ContractAddresses {
_, existsERC721 := erc721TokenCriteria[chainID]
if isERC721 && !existsERC721 {
erc721TokenCriteria[chainID] = make(map[string]*protobuf.TokenCriteria)
}
_, existsERC20 := erc20TokenCriteria[chainID]
if isERC20 && !existsERC20 {
erc20TokenCriteria[chainID] = make(map[string]*protobuf.TokenCriteria)
}
_, existsERC721 = erc721TokenCriteria[chainID][contractAddress]
if isERC721 && !existsERC721 {
erc721TokenCriteria[chainID][strings.ToLower(contractAddress)] = tokenRequirement
}
_, existsERC20 = erc20TokenCriteria[chainID][contractAddress]
if isERC20 && !existsERC20 {
erc20TokenCriteria[chainID][strings.ToLower(contractAddress)] = tokenRequirement
}
if isENS {
ensTokenCriteria = append(ensTokenCriteria, tokenRequirement.EnsPattern)
}
}
}
}
return
}

View File

@@ -0,0 +1,83 @@
package communities
import (
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
)
func validateCommunityChat(desc *protobuf.CommunityDescription, chat *protobuf.CommunityChat) error {
if chat == nil {
return ErrInvalidCommunityDescription
}
if chat.Permissions == nil {
return ErrInvalidCommunityDescriptionNoChatPermissions
}
if chat.Permissions.Access == protobuf.CommunityPermissions_UNKNOWN_ACCESS {
return ErrInvalidCommunityDescriptionUnknownChatAccess
}
if len(chat.CategoryId) != 0 {
if _, exists := desc.Categories[chat.CategoryId]; !exists {
return ErrInvalidCommunityDescriptionUnknownChatCategory
}
}
if chat.Identity == nil {
return ErrInvalidCommunityDescriptionChatIdentity
}
for pk := range chat.Members {
if desc.Members == nil {
return ErrInvalidCommunityDescriptionMemberInChatButNotInOrg
}
// Check member is in the org as well
if _, ok := desc.Members[pk]; !ok {
return ErrInvalidCommunityDescriptionMemberInChatButNotInOrg
}
}
return nil
}
func validateCommunityCategory(category *protobuf.CommunityCategory) error {
if len(category.CategoryId) == 0 {
return ErrInvalidCommunityDescriptionCategoryNoID
}
if len(category.Name) == 0 {
return ErrInvalidCommunityDescriptionCategoryNoName
}
return nil
}
func ValidateCommunityDescription(desc *protobuf.CommunityDescription) error {
if desc == nil {
return ErrInvalidCommunityDescription
}
if desc.Permissions == nil {
return ErrInvalidCommunityDescriptionNoOrgPermissions
}
if desc.Permissions.Access == protobuf.CommunityPermissions_UNKNOWN_ACCESS {
return ErrInvalidCommunityDescriptionUnknownOrgAccess
}
valid := requests.ValidateTags(desc.Tags)
if !valid {
return ErrInvalidCommunityTags
}
for _, category := range desc.Categories {
if err := validateCommunityCategory(category); err != nil {
return err
}
}
for _, chat := range desc.Chats {
if err := validateCommunityChat(desc, chat); err != nil {
return err
}
}
return nil
}