42
vendor/github.com/status-im/status-go/protocol/communities/adaptors.go
generated
vendored
Normal file
42
vendor/github.com/status-im/status-go/protocol/communities/adaptors.go
generated
vendored
Normal 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
|
||||
}
|
||||
154
vendor/github.com/status-im/status-go/protocol/communities/check_permissions_response.go
generated
vendored
Normal file
154
vendor/github.com/status-im/status-go/protocol/communities/check_permissions_response.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
2428
vendor/github.com/status-im/status-go/protocol/communities/community.go
generated
vendored
Normal file
2428
vendor/github.com/status-im/status-go/protocol/communities/community.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
543
vendor/github.com/status-im/status-go/protocol/communities/community_categories.go
generated
vendored
Normal file
543
vendor/github.com/status-im/status-go/protocol/communities/community_categories.go
generated
vendored
Normal 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
|
||||
}
|
||||
270
vendor/github.com/status-im/status-go/protocol/communities/community_changes.go
generated
vendored
Normal file
270
vendor/github.com/status-im/status-go/protocol/communities/community_changes.go
generated
vendored
Normal 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
|
||||
}
|
||||
111
vendor/github.com/status-im/status-go/protocol/communities/community_description_encryption.go
generated
vendored
Normal file
111
vendor/github.com/status-im/status-go/protocol/communities/community_description_encryption.go
generated
vendored
Normal 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
|
||||
}
|
||||
153
vendor/github.com/status-im/status-go/protocol/communities/community_encryption_key_action.go
generated
vendored
Normal file
153
vendor/github.com/status-im/status-go/protocol/communities/community_encryption_key_action.go
generated
vendored
Normal 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
|
||||
}
|
||||
414
vendor/github.com/status-im/status-go/protocol/communities/community_event.go
generated
vendored
Normal file
414
vendor/github.com/status-im/status-go/protocol/communities/community_event.go
generated
vendored
Normal 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
|
||||
}
|
||||
285
vendor/github.com/status-im/status-go/protocol/communities/community_event_message.go
generated
vendored
Normal file
285
vendor/github.com/status-im/status-go/protocol/communities/community_event_message.go
generated
vendored
Normal 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
|
||||
}
|
||||
65
vendor/github.com/status-im/status-go/protocol/communities/community_token_permission.go
generated
vendored
Normal file
65
vendor/github.com/status-im/status-go/protocol/communities/community_token_permission.go
generated
vendored
Normal 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)
|
||||
}
|
||||
87
vendor/github.com/status-im/status-go/protocol/communities/communnity_privileged_member_sync_msg.go
generated
vendored
Normal file
87
vendor/github.com/status-im/status-go/protocol/communities/communnity_privileged_member_sync_msg.go
generated
vendored
Normal 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
|
||||
}
|
||||
47
vendor/github.com/status-im/status-go/protocol/communities/errors.go
generated
vendored
Normal file
47
vendor/github.com/status-im/status-go/protocol/communities/errors.go
generated
vendored
Normal 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")
|
||||
5216
vendor/github.com/status-im/status-go/protocol/communities/manager.go
generated
vendored
Normal file
5216
vendor/github.com/status-im/status-go/protocol/communities/manager.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
441
vendor/github.com/status-im/status-go/protocol/communities/permission_checker.go
generated
vendored
Normal file
441
vendor/github.com/status-im/status-go/protocol/communities/permission_checker.go
generated
vendored
Normal 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
|
||||
}
|
||||
284
vendor/github.com/status-im/status-go/protocol/communities/permissioned_balances.go
generated
vendored
Normal file
284
vendor/github.com/status-im/status-go/protocol/communities/permissioned_balances.go
generated
vendored
Normal 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
|
||||
}
|
||||
1783
vendor/github.com/status-im/status-go/protocol/communities/persistence.go
generated
vendored
Normal file
1783
vendor/github.com/status-im/status-go/protocol/communities/persistence.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
163
vendor/github.com/status-im/status-go/protocol/communities/persistence_mapping.go
generated
vendored
Normal file
163
vendor/github.com/status-im/status-go/protocol/communities/persistence_mapping.go
generated
vendored
Normal 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
|
||||
}
|
||||
132
vendor/github.com/status-im/status-go/protocol/communities/persistence_test_helpers.go
generated
vendored
Normal file
132
vendor/github.com/status-im/status-go/protocol/communities/persistence_test_helpers.go
generated
vendored
Normal 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
|
||||
}
|
||||
101
vendor/github.com/status-im/status-go/protocol/communities/request_to_join.go
generated
vendored
Normal file
101
vendor/github.com/status-im/status-go/protocol/communities/request_to_join.go
generated
vendored
Normal 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
|
||||
}
|
||||
22
vendor/github.com/status-im/status-go/protocol/communities/request_to_leave.go
generated
vendored
Normal file
22
vendor/github.com/status-im/status-go/protocol/communities/request_to_leave.go
generated
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
120
vendor/github.com/status-im/status-go/protocol/communities/roles_authorization.go
generated
vendored
Normal file
120
vendor/github.com/status-im/status-go/protocol/communities/roles_authorization.go
generated
vendored
Normal 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
|
||||
}
|
||||
41
vendor/github.com/status-im/status-go/protocol/communities/token/community_token.go
generated
vendored
Normal file
41
vendor/github.com/status-im/status-go/protocol/communities/token/community_token.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
59
vendor/github.com/status-im/status-go/protocol/communities/utils.go
generated
vendored
Normal file
59
vendor/github.com/status-im/status-go/protocol/communities/utils.go
generated
vendored
Normal 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
|
||||
}
|
||||
83
vendor/github.com/status-im/status-go/protocol/communities/validator.go
generated
vendored
Normal file
83
vendor/github.com/status-im/status-go/protocol/communities/validator.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user