From 92032c7158b80908508cd67c08ac6f6c75eee35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rich=CE=9Brd?= Date: Sun, 23 May 2021 09:34:17 -0400 Subject: [PATCH] Community categories (#2228) * create and edit community categories * edit categories order * adding category to chat * Adding categories to json --- VERSION | 2 +- account/generator/path_decoder.go | 2 +- go.mod | 1 + go.sum | 10 +- protocol/chat.go | 5 + protocol/communities/community.go | 141 ++++++- protocol/communities/community_categories.go | 359 ++++++++++++++++++ .../communities/community_categories_test.go | 246 ++++++++++++ protocol/communities/community_test.go | 8 +- protocol/communities/errors.go | 6 + protocol/communities/manager.go | 125 ++++++ protocol/communities/validator.go | 24 ++ protocol/communities_messenger_test.go | 37 ++ protocol/messenger_communities.go | 80 ++++ protocol/protobuf/communities.pb.go | 209 +++++++--- protocol/protobuf/communities.proto | 9 + .../requests/create_community_category.go | 28 ++ .../requests/delete_community_category.go | 28 ++ protocol/requests/edit_community_category.go | 34 ++ .../requests/reorder_community_category.go | 33 ++ protocol/requests/reorder_community_chat.go | 39 ++ services/ext/api.go | 25 ++ 22 files changed, 1377 insertions(+), 74 deletions(-) create mode 100644 protocol/communities/community_categories.go create mode 100644 protocol/communities/community_categories_test.go create mode 100644 protocol/requests/create_community_category.go create mode 100644 protocol/requests/delete_community_category.go create mode 100644 protocol/requests/edit_community_category.go create mode 100644 protocol/requests/reorder_community_category.go create mode 100644 protocol/requests/reorder_community_chat.go diff --git a/VERSION b/VERSION index 892666608..de4de0978 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.79.1 +0.79.2 diff --git a/account/generator/path_decoder.go b/account/generator/path_decoder.go index 9eb99a7a2..c1c05776d 100644 --- a/account/generator/path_decoder.go +++ b/account/generator/path_decoder.go @@ -176,7 +176,7 @@ func (d *pathDecoder) parseSeparator() error { return d.saveSegment() } - return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b)) + return fmt.Errorf("expected %s, got %s", string(rune(tokenSeparator)), string(b)) } func (d *pathDecoder) parseSegment() error { diff --git a/go.mod b/go.mod index 69f41fd7e..3bfd919de 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( golang.org/x/tools v0.0.0-20200211045251-2de505fc5306 // indirect google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect google.golang.org/grpc v1.25.1 // indirect + google.golang.org/protobuf v1.26.0-rc.1 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index d5e139ef4..14cb61b37 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -850,23 +852,20 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20180302121509-abf0ba0be5d5/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff h1:uuol9OUzSvZntY1v963NAbVd7A+PHLMz1FlCe3Lorcs= @@ -874,7 +873,6 @@ gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff/go.mod h1:uAJ gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8= gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= @@ -884,13 +882,11 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.6 h1:97YCGUei5WVbkKfogoJQsLwUJ17cWvpLrgNvlcbxikE= gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= diff --git a/protocol/chat.go b/protocol/chat.go index f89b6a2a8..ccc3cca93 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -85,6 +85,10 @@ type Chat struct { // CommunityID is the id of the community it belongs to CommunityID string `json:"communityId,omitempty"` + + // CategoryID is the id of the community category this chat belongs to. + CategoryID string `json:"categoryId,omitempty"` + // Joined is a timestamp that indicates when the chat was joined Joined int64 `json:"joined,omitempty"` @@ -305,6 +309,7 @@ func CreateCommunityChat(orgID, chatID string, orgChat *protobuf.CommunityChat, return &Chat{ CommunityID: orgID, + CategoryID: orgChat.CategoryId, Name: orgChat.Identity.DisplayName, Active: true, Color: color, diff --git a/protocol/communities/community.go b/protocol/communities/community.go index f9687a432..2f6420c8e 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -62,6 +62,14 @@ type CommunityChat struct { Members map[string]*protobuf.CommunityMember `json:"members"` Permissions *protobuf.CommunityPermissions `json:"permissions"` CanPost bool `json:"canPost"` + Position int `json:"position"` + CategoryID string `json:"categoryID"` +} + +type CommunityCategory struct { + ID string `json:"id"` + Name string `json:"name"` + Position int `json:"position"` // Position is used to sort the categories } func (o *Community) MarshalJSON() ([]byte, error) { @@ -77,6 +85,7 @@ func (o *Community) MarshalJSON() ([]byte, error) { Name string `json:"name"` Description string `json:"description"` Chats map[string]CommunityChat `json:"chats"` + Categories map[string]CommunityCategory `json:"categories"` Images map[string]images.IdentityImage `json:"images"` Permissions *protobuf.CommunityPermissions `json:"permissions"` Members map[string]*protobuf.CommunityMember `json:"members"` @@ -91,6 +100,7 @@ func (o *Community) MarshalJSON() ([]byte, error) { Admin: o.IsAdmin(), Verified: o.config.Verified, Chats: make(map[string]CommunityChat), + Categories: make(map[string]CommunityCategory), Joined: o.config.Joined, CanRequestAccess: o.CanRequestAccess(o.config.MemberIdentity), CanJoin: o.canJoin(), @@ -99,6 +109,14 @@ func (o *Community) MarshalJSON() ([]byte, error) { IsMember: o.isMember(), } if o.config.CommunityDescription != nil { + for id, c := range o.config.CommunityDescription.Categories { + category := CommunityCategory{ + ID: id, + Name: c.Name, + Position: int(c.Position), + } + communityItem.Categories[id] = category + } for id, c := range o.config.CommunityDescription.Chats { canPost, err := o.CanPost(o.config.MemberIdentity, id, nil) if err != nil { @@ -110,6 +128,8 @@ func (o *Community) MarshalJSON() ([]byte, error) { Permissions: c.Permissions, Members: c.Members, CanPost: canPost, + CategoryID: c.CategoryId, + Position: int(c.Position), } communityItem.Chats[id] = chat } @@ -169,8 +189,10 @@ func (o *Community) initialize() { } type CommunityChatChanges struct { - MembersAdded map[string]*protobuf.CommunityMember - MembersRemoved map[string]*protobuf.CommunityMember + MembersAdded map[string]*protobuf.CommunityMember + MembersRemoved map[string]*protobuf.CommunityMember + CategoryModified string + PositionModified int } type CommunityChanges struct { @@ -182,6 +204,10 @@ type CommunityChanges struct { 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"` + // ShouldMemberJoin indicates whether the user should join this community // automatically ShouldMemberJoin bool `json:"memberAdded"` @@ -233,6 +259,14 @@ func (o *Community) CreateChat(chatID string, chat *protobuf.CommunityChat) (*Co return nil, ErrChatAlreadyExists } + // Sets the chat position to be the last within its category + chat.Position = 0 + for _, c := range o.config.CommunityDescription.Chats { + if c.CategoryId == chat.CategoryId { + chat.Position++ + } + } + o.config.CommunityDescription.Chats[chatID] = chat o.increaseClock() @@ -253,6 +287,15 @@ func (o *Community) DeleteChat(chatID string) (*protobuf.CommunityDescription, e if o.config.CommunityDescription.Chats == nil { o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat) } + + changes := o.emptyCommunityChanges() + + if chat, exists := o.config.CommunityDescription.Chats[chatID]; exists { + tmpCatID := chat.CategoryId + chat.CategoryId = "" + o.SortCategoryChats(changes, tmpCatID) + } + delete(o.config.CommunityDescription.Chats, chatID) o.increaseClock() @@ -570,6 +613,7 @@ func (o *Community) UpdateCommunityDescription(signer *ecdsa.PublicKey, descript if o.config.CommunityDescription.Chats == nil { o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat) } + if _, ok := o.config.CommunityDescription.Chats[chatID]; !ok { if response.ChatsAdded == nil { response.ChatsAdded = make(map[string]*protobuf.CommunityChat) @@ -606,6 +650,65 @@ func (o *Community) UpdateCommunityDescription(signer *ecdsa.PublicKey, descript } } } + + // Check for categories that were removed + for categoryID := range o.config.CommunityDescription.Categories { + if description.Categories == nil { + description.Categories = make(map[string]*protobuf.CommunityCategory) + } + + if description.Chats == nil { + description.Chats = make(map[string]*protobuf.CommunityChat) + } + + if _, ok := description.Categories[categoryID]; !ok { + response.CategoriesRemoved = append(response.CategoriesRemoved, categoryID) + } + + if o.config.CommunityDescription.Chats == nil { + o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat) + } + } + + // Check for categories that were added + for categoryID, category := range description.Categories { + if o.config.CommunityDescription.Categories == nil { + o.config.CommunityDescription.Categories = make(map[string]*protobuf.CommunityCategory) + } + if _, ok := o.config.CommunityDescription.Categories[categoryID]; !ok { + if response.CategoriesAdded == nil { + response.CategoriesAdded = make(map[string]*protobuf.CommunityCategory) + } + + response.CategoriesAdded[categoryID] = category + } else { + if o.config.CommunityDescription.Categories[categoryID].Name != category.Name || o.config.CommunityDescription.Categories[categoryID].Position != category.Position { + response.CategoriesModified[categoryID] = category + } + } + } + + // Check for chat categories that were modified + for chatID, chat := range description.Chats { + if o.config.CommunityDescription.Chats == nil { + o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat) + } + + if _, ok := o.config.CommunityDescription.Chats[chatID]; !ok { + continue // It's a new chat + } + + if o.config.CommunityDescription.Chats[chatID].CategoryId != chat.CategoryId { + if response.ChatsModified[chatID] == nil { + response.ChatsModified[chatID] = &CommunityChatChanges{ + MembersAdded: make(map[string]*protobuf.CommunityMember), + MembersRemoved: make(map[string]*protobuf.CommunityMember), + } + } + + response.ChatsModified[chatID].CategoryModified = chat.CategoryId + } + } } o.config.CommunityDescription = description @@ -765,6 +868,17 @@ func (o *Community) Chats() map[string]*protobuf.CommunityChat { return response } +func (o *Community) Categories() map[string]*protobuf.CommunityCategory { + o.mutex.Lock() + defer o.mutex.Unlock() + + response := make(map[string]*protobuf.CommunityCategory) + for k, v := range o.config.CommunityDescription.Categories { + response[k] = v + } + return response +} + func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) { if len(data) <= signatureLength { return nil, ErrInvalidGrant @@ -1015,5 +1129,28 @@ func emptyCommunityChanges() *CommunityChanges { 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), } } + +type sortSlice []sorterHelperIdx +type sorterHelperIdx struct { + pos int32 + catID string + chatID string +} + +func (d sortSlice) Len() int { + return len(d) +} + +func (d sortSlice) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +func (d sortSlice) Less(i, j int) bool { + return d[i].pos < d[j].pos +} diff --git a/protocol/communities/community_categories.go b/protocol/communities/community_categories.go new file mode 100644 index 000000000..315f38a6b --- /dev/null +++ b/protocol/communities/community_categories.go @@ -0,0 +1,359 @@ +package communities + +import ( + "sort" + + "github.com/status-im/status-go/protocol/protobuf" +) + +func (o *Community) CreateCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) { + o.mutex.Lock() + defer o.mutex.Unlock() + + if o.config.PrivateKey == nil { + return nil, ErrNotAdmin + } + + 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, "") + + o.increaseClock() + + 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, + } + } + + return changes, nil +} + +func (o *Community) EditCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) { + o.mutex.Lock() + defer o.mutex.Unlock() + + if o.config.PrivateKey == nil { + return nil, ErrNotAdmin + } + + 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, "") + + o.increaseClock() + + 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, + } + } + + return changes, nil +} + +func (o *Community) ReorderCategories(categoryID string, newPosition int) (*CommunityChanges, error) { + o.mutex.Lock() + defer o.mutex.Unlock() + + if o.config.PrivateKey == nil { + return nil, ErrNotAdmin + } + + if newPosition > 0 && newPosition >= len(o.config.CommunityDescription.Categories) { + newPosition = len(o.config.CommunityDescription.Categories) - 1 + } else if newPosition < 0 { + newPosition = 0 + } + + o.config.CommunityDescription.Categories[categoryID].Position = int32(newPosition) + + s := make(sortSlice, 0, len(o.config.CommunityDescription.Categories)) + for catID, category := range o.config.CommunityDescription.Categories { + + position := category.Position + if category.CategoryId != categoryID && position >= int32(newPosition) { + position = position + 1 + } + + s = append(s, sorterHelperIdx{ + pos: position, + catID: catID, + }) + } + + changes := o.emptyCommunityChanges() + + o.setModifiedCategories(changes, s) + + o.increaseClock() + + 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.config.PrivateKey == nil { + return nil, ErrNotAdmin + } + + 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, categoryID, chat, newPosition) + + o.increaseClock() + + 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, categoryID string, chat *protobuf.CommunityChat, newPosition int) { + var catChats []string + for k, c := range o.config.CommunityDescription.Chats { + if c.CategoryId == categoryID { + catChats = append(catChats, k) + } + } + + if newPosition > 0 && newPosition >= len(catChats) { + newPosition = len(catChats) - 1 + } else if newPosition < 0 { + newPosition = 0 + } + + sortedChats := make(sortSlice, 0, len(catChats)) + for _, k := range catChats { + position := chat.Position + if o.config.CommunityDescription.Chats[k] != chat && position >= int32(newPosition) { + position = position + 1 + } + + sortedChats = append(sortedChats, sorterHelperIdx{ + pos: 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{ + 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.config.PrivateKey == nil { + return nil, ErrNotAdmin + } + + 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) + + o.increaseClock() + + return changes, nil +} diff --git a/protocol/communities/community_categories_test.go b/protocol/communities/community_categories_test.go new file mode 100644 index 000000000..9fe02f38a --- /dev/null +++ b/protocol/communities/community_categories_test.go @@ -0,0 +1,246 @@ +package communities + +import ( + "github.com/status-im/status-go/protocol/protobuf" +) + +func (s *CommunitySuite) TestCreateCategory() { + newCategoryID := "new-category-id" + newCategoryName := "new-category-name" + + org := s.buildCommunity(&s.identity.PublicKey) + org.config.PrivateKey = nil + + _, err := org.CreateCategory(newCategoryID, newCategoryName, []string{}) + s.Require().Equal(ErrNotAdmin, err) + + org.config.PrivateKey = s.identity + + changes, err := org.CreateCategory(newCategoryID, newCategoryName, []string{}) + + description := org.config.CommunityDescription + + s.Require().NoError(err) + s.Require().NotNil(description.Categories) + s.Require().NotNil(description.Categories[newCategoryID]) + s.Require().Equal(newCategoryName, description.Categories[newCategoryID].Name) + s.Require().Equal(newCategoryID, description.Categories[newCategoryID].CategoryId) + s.Require().Equal(int32(len(description.Categories)-1), description.Categories[newCategoryID].Position) + s.Require().NotNil(changes) + s.Require().NotNil(changes.CategoriesAdded[newCategoryID]) + s.Require().Equal(description.Categories[newCategoryID], changes.CategoriesAdded[newCategoryID]) + s.Require().Nil(changes.CategoriesModified[newCategoryID]) + + _, err = org.CreateCategory(newCategoryID, newCategoryName, []string{}) + s.Require().Equal(ErrCategoryAlreadyExists, err) + + newCategoryID2 := "new-category-id2" + newCategoryName2 := "new-category-name2" + + changes, err = org.CreateCategory(newCategoryID2, newCategoryName2, []string{}) + s.Require().NoError(err) + s.Require().Equal(int32(len(description.Categories)-1), description.Categories[newCategoryID2].Position) + s.Require().NotNil(changes.CategoriesAdded[newCategoryID2]) + s.Require().Nil(changes.CategoriesModified[newCategoryID2]) + + newCategoryID3 := "new-category-id3" + newCategoryName3 := "new-category-name3" + _, err = org.CreateCategory(newCategoryID3, newCategoryName3, []string{"some-chat-id"}) + s.Require().Equal(ErrChatNotFound, err) + + newChatID := "new-chat-id" + identity := &protobuf.ChatIdentity{ + DisplayName: "new-chat-display-name", + Description: "new-chat-description", + } + permissions := &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + + _, _ = org.CreateChat(newChatID, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + + changes, err = org.CreateCategory(newCategoryID3, newCategoryName3, []string{newChatID}) + s.Require().NoError(err) + s.Require().NotNil(changes.ChatsModified[newChatID]) + s.Require().Equal(newCategoryID3, changes.ChatsModified[newChatID].CategoryModified) + + newCategoryID4 := "new-category-id4" + newCategoryName4 := "new-category-name4" + + _, err = org.CreateCategory(newCategoryID4, newCategoryName4, []string{newChatID}) + s.Require().Equal(ErrChatAlreadyAssigned, err) +} + +func (s *CommunitySuite) TestEditCategory() { + newCategoryID := "new-category-id" + newCategoryName := "new-category-name" + editedCategoryName := "edited-category-name" + + org := s.buildCommunity(&s.identity.PublicKey) + org.config.PrivateKey = s.identity + _, _ = org.CreateCategory(newCategoryID, newCategoryName, []string{testChatID1}) + org.config.PrivateKey = nil + + _, err := org.EditCategory(newCategoryID, editedCategoryName, []string{testChatID1}) + s.Require().Equal(ErrNotAdmin, err) + + org.config.PrivateKey = s.identity + + _, err = org.EditCategory("some-random-category", editedCategoryName, []string{testChatID1}) + s.Require().Equal(ErrCategoryNotFound, err) + + changes, err := org.EditCategory(newCategoryID, editedCategoryName, []string{testChatID1}) + + description := org.config.CommunityDescription + + s.Require().NoError(err) + s.Require().Equal(editedCategoryName, description.Categories[newCategoryID].Name) + s.Require().NotNil(changes) + s.Require().NotNil(changes.CategoriesModified[newCategoryID]) + s.Require().Equal(description.Categories[newCategoryID], changes.CategoriesModified[newCategoryID]) + s.Require().Nil(changes.CategoriesAdded[newCategoryID]) + + _, err = org.EditCategory(newCategoryID, editedCategoryName, []string{"some-random-chat-id"}) + s.Require().Equal(ErrChatNotFound, err) + + _, err = org.EditCategory(testCategoryID1, testCategoryName1, []string{testChatID1}) + s.Require().Equal(ErrChatAlreadyAssigned, err) + + // Edit by removing the chats + + identity := &protobuf.ChatIdentity{ + DisplayName: "new-chat-display-name", + Description: "new-chat-description", + } + permissions := &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + + testChatID2 := "test-chat-id-2" + testChatID3 := "test-chat-id-3" + + _, _ = org.CreateChat(testChatID2, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + _, _ = org.CreateChat(testChatID3, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + + _, err = org.EditCategory(newCategoryID, editedCategoryName, []string{testChatID1, testChatID2, testChatID3}) + s.Require().NoError(err) + + s.Require().Equal(newCategoryID, description.Chats[testChatID1].CategoryId) + s.Require().Equal(newCategoryID, description.Chats[testChatID2].CategoryId) + s.Require().Equal(newCategoryID, description.Chats[testChatID3].CategoryId) + + s.Require().Equal(int32(0), description.Chats[testChatID1].Position) + s.Require().Equal(int32(1), description.Chats[testChatID2].Position) + s.Require().Equal(int32(2), description.Chats[testChatID3].Position) + + _, _ = org.EditCategory(newCategoryID, editedCategoryName, []string{testChatID1, testChatID3}) + s.Require().Equal("", description.Chats[testChatID2].CategoryId) + s.Require().Equal(int32(0), description.Chats[testChatID1].Position) + s.Require().Equal(int32(1), description.Chats[testChatID3].Position) + + _, _ = org.EditCategory(newCategoryID, editedCategoryName, []string{testChatID3}) + s.Require().Equal("", description.Chats[testChatID1].CategoryId) + s.Require().Equal(int32(0), description.Chats[testChatID3].Position) +} + +func (s *CommunitySuite) TestDeleteCategory() { + org := s.buildCommunity(&s.identity.PublicKey) + org.config.PrivateKey = s.identity + identity := &protobuf.ChatIdentity{ + DisplayName: "new-chat-display-name", + Description: "new-chat-description", + } + permissions := &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + + testChatID2 := "test-chat-id-2" + testChatID3 := "test-chat-id-3" + newCategoryID := "new-category-id" + newCategoryName := "new-category-name" + + _, _ = org.CreateChat(testChatID2, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + _, _ = org.CreateChat(testChatID3, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + + _, _ = org.CreateCategory(newCategoryID, newCategoryName, []string{}) + + description := org.config.CommunityDescription + + _, _ = org.EditCategory(newCategoryID, newCategoryName, []string{testChatID2, testChatID1}) + + s.Require().Equal(newCategoryID, description.Chats[testChatID1].CategoryId) + s.Require().Equal(newCategoryID, description.Chats[testChatID2].CategoryId) + + s.Require().Equal(int32(0), description.Chats[testChatID3].Position) + s.Require().Equal(int32(0), description.Chats[testChatID2].Position) + s.Require().Equal(int32(1), description.Chats[testChatID1].Position) + + org.config.PrivateKey = nil + _, err := org.DeleteCategory(testCategoryID1) + s.Require().Equal(ErrNotAdmin, err) + + org.config.PrivateKey = s.identity + _, err = org.DeleteCategory("some-category-id") + s.Require().Equal(ErrCategoryNotFound, err) + + changes, err := org.DeleteCategory(newCategoryID) + s.Require().NoError(err) + s.Require().NotNil(changes) + + s.Require().Equal("", description.Chats[testChatID1].CategoryId) + s.Require().Equal("", description.Chats[testChatID2].CategoryId) + s.Require().Equal("", description.Chats[testChatID3].CategoryId) +} + +func (s *CommunitySuite) TestDeleteChatOrder() { + org := s.buildCommunity(&s.identity.PublicKey) + org.config.PrivateKey = s.identity + identity := &protobuf.ChatIdentity{ + DisplayName: "new-chat-display-name", + Description: "new-chat-description", + } + permissions := &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + + testChatID2 := "test-chat-id-2" + testChatID3 := "test-chat-id-3" + newCategoryID := "new-category-id" + newCategoryName := "new-category-name" + + _, _ = org.CreateChat(testChatID2, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + _, _ = org.CreateChat(testChatID3, &protobuf.CommunityChat{ + Identity: identity, + Permissions: permissions, + }) + + _, _ = org.CreateCategory(newCategoryID, newCategoryName, []string{testChatID1, testChatID2, testChatID3}) + + description, _ := org.DeleteChat(testChatID2) + s.Require().Equal(int32(0), description.Chats[testChatID1].Position) + s.Require().Equal(int32(1), description.Chats[testChatID3].Position) + + description, _ = org.DeleteChat(testChatID1) + s.Require().Equal(int32(0), description.Chats[testChatID3].Position) + + _, err := org.DeleteChat(testChatID3) + s.Require().NoError(err) +} diff --git a/protocol/communities/community_test.go b/protocol/communities/community_test.go index f53abad76..627d21561 100644 --- a/protocol/communities/community_test.go +++ b/protocol/communities/community_test.go @@ -18,6 +18,8 @@ func TestCommunitySuite(t *testing.T) { } const testChatID1 = "chat-id-1" +const testCategoryID1 = "category-id-1" +const testCategoryName1 = "category-name-1" const testChatID2 = "chat-id-2" type CommunitySuite struct { @@ -131,9 +133,9 @@ func (s *CommunitySuite) TestCreateChat() { s.Require().NoError(err) s.Require().NotNil(description) - s.Require().NotNil(description.Chats[newChatID]) s.Require().NotEmpty(description.Clock) + s.Require().Equal(len(description.Chats)-1, int(description.Chats[newChatID].Position)) s.Require().Equal(permissions, description.Chats[newChatID].Permissions) s.Require().Equal(identity, description.Chats[newChatID].Identity) @@ -705,10 +707,12 @@ func (s *CommunitySuite) emptyCommunityDescriptionWithChat() *protobuf.Community Members: make(map[string]*protobuf.CommunityMember), Clock: 1, Chats: make(map[string]*protobuf.CommunityChat), + Categories: make(map[string]*protobuf.CommunityCategory), Permissions: &protobuf.CommunityPermissions{}, } - desc.Chats[testChatID1] = &protobuf.CommunityChat{Permissions: &protobuf.CommunityPermissions{}, Members: make(map[string]*protobuf.CommunityMember)} + desc.Categories[testCategoryID1] = &protobuf.CommunityCategory{CategoryId: testCategoryID1, Name: testCategoryName1, Position: 0} + desc.Chats[testChatID1] = &protobuf.CommunityChat{Position: 0, Permissions: &protobuf.CommunityPermissions{}, Members: make(map[string]*protobuf.CommunityMember)} desc.Members[common.PubkeyToHex(&s.member1.PublicKey)] = &protobuf.CommunityMember{} desc.Chats[testChatID1].Members[common.PubkeyToHex(&s.member1.PublicKey)] = &protobuf.CommunityMember{} diff --git a/protocol/communities/errors.go b/protocol/communities/errors.go index c870ed97c..61d26b87c 100644 --- a/protocol/communities/errors.go +++ b/protocol/communities/errors.go @@ -3,8 +3,11 @@ package communities import "errors" var ErrChatNotFound = errors.New("chat not found") +var ErrCategoryNotFound = errors.New("category not found") +var ErrChatAlreadyAssigned = errors.New("chat already assigned to a category") var ErrOrgNotFound = errors.New("community not found") 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") @@ -12,6 +15,9 @@ var ErrInvalidCommunityDescriptionNoChatPermissions = errors.New("invalid commun 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 ErrInvalidCommunityDescriptionUnknownChatCategory = errors.New("invalid community category in chat") var ErrNotAdmin = errors.New("no admin privileges for this community") var ErrInvalidGrant = errors.New("invalid grant") var ErrNotAuthorized = errors.New("not authorized") diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 80c179945..b20306524 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -310,6 +310,131 @@ func (m *Manager) CreateChat(communityID types.HexBytes, chat *protobuf.Communit return community, changes, nil } +func (m *Manager) CreateCategory(request *requests.CreateCommunityCategory) (*Community, *CommunityChanges, error) { + community, err := m.GetByID(request.CommunityID) + if err != nil { + return nil, nil, err + } + if community == nil { + return nil, nil, ErrOrgNotFound + } + categoryID := uuid.New().String() + changes, err := community.CreateCategory(categoryID, request.CategoryName, request.ChatIDs) + if err != nil { + return nil, nil, err + } + + err = m.persistence.SaveCommunity(community) + if err != nil { + return nil, nil, err + } + + // Advertise changes + m.publish(&Subscription{Community: community}) + + return community, changes, nil +} + +func (m *Manager) EditCategory(request *requests.EditCommunityCategory) (*Community, *CommunityChanges, error) { + community, err := m.GetByID(request.CommunityID) + if err != nil { + return nil, nil, err + } + if community == nil { + return nil, nil, ErrOrgNotFound + } + + changes, err := community.EditCategory(request.CategoryID, request.CategoryName, request.ChatIDs) + if err != nil { + return nil, nil, err + } + + err = m.persistence.SaveCommunity(community) + if err != nil { + return nil, nil, err + } + + // Advertise changes + m.publish(&Subscription{Community: community}) + + return community, changes, nil +} + +func (m *Manager) ReorderCategories(request *requests.ReorderCommunityCategories) (*Community, *CommunityChanges, error) { + community, err := m.GetByID(request.CommunityID) + if err != nil { + return nil, nil, err + } + if community == nil { + return nil, nil, ErrOrgNotFound + } + + changes, err := community.ReorderCategories(request.CategoryID, request.Position) + if err != nil { + return nil, nil, err + } + + err = m.persistence.SaveCommunity(community) + if err != nil { + return nil, nil, err + } + + // Advertise changes + m.publish(&Subscription{Community: community}) + + return community, changes, nil +} + +func (m *Manager) ReorderChat(request *requests.ReorderCommunityChat) (*Community, *CommunityChanges, error) { + community, err := m.GetByID(request.CommunityID) + if err != nil { + return nil, nil, err + } + if community == nil { + return nil, nil, ErrOrgNotFound + } + + changes, err := community.ReorderChat(request.CategoryID, request.ChatID, request.Position) + if err != nil { + return nil, nil, err + } + + err = m.persistence.SaveCommunity(community) + if err != nil { + return nil, nil, err + } + + // Advertise changes + m.publish(&Subscription{Community: community}) + + return community, changes, nil +} + +func (m *Manager) DeleteCategory(request *requests.DeleteCommunityCategory) (*Community, *CommunityChanges, error) { + community, err := m.GetByID(request.CommunityID) + if err != nil { + return nil, nil, err + } + if community == nil { + return nil, nil, ErrOrgNotFound + } + + changes, err := community.DeleteCategory(request.CategoryID) + if err != nil { + return nil, nil, err + } + + err = m.persistence.SaveCommunity(community) + if err != nil { + return nil, nil, err + } + + // Advertise changes + m.publish(&Subscription{Community: community}) + + return community, changes, nil +} + func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, description *protobuf.CommunityDescription, payload []byte) (*CommunityResponse, error) { id := crypto.CompressPubkey(signer) community, err := m.persistence.GetByID(m.identity, id) diff --git a/protocol/communities/validator.go b/protocol/communities/validator.go index 671e6118f..77ba479bc 100644 --- a/protocol/communities/validator.go +++ b/protocol/communities/validator.go @@ -15,6 +15,12 @@ func validateCommunityChat(desc *protobuf.CommunityDescription, chat *protobuf.C return ErrInvalidCommunityDescriptionUnknownChatAccess } + if len(chat.CategoryId) != 0 { + if _, exists := desc.Categories[chat.CategoryId]; !exists { + return ErrInvalidCommunityDescriptionUnknownChatCategory + } + } + for pk := range chat.Members { if desc.Members == nil { return ErrInvalidCommunityDescriptionMemberInChatButNotInOrg @@ -28,6 +34,18 @@ func validateCommunityChat(desc *protobuf.CommunityDescription, chat *protobuf.C 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 @@ -39,6 +57,12 @@ func ValidateCommunityDescription(desc *protobuf.CommunityDescription) error { return ErrInvalidCommunityDescriptionUnknownOrgAccess } + 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 diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index cabb6d610..74ba0ba0d 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -186,6 +186,26 @@ func (s *MessengerCommunitiesSuite) TestJoinCommunity() { s.Require().NotEmpty(createdChat.Timestamp) s.Require().True(strings.HasPrefix(createdChat.ID, community.IDString())) + // Make sure the changes are reflect in the community + community = response.Communities()[0] + + var chatIds []string + for k := range community.Chats() { + chatIds = append(chatIds, k) + } + + category := &requests.CreateCommunityCategory{ + CommunityID: community.ID(), + CategoryName: "category-name", + ChatIDs: chatIds, + } + + response, err = s.bob.CreateCommunityCategory(category) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + s.Require().Len(response.Communities()[0].Categories(), 1) + // Make sure the changes are reflect in the community community = response.Communities()[0] chats := community.Chats() @@ -231,6 +251,12 @@ func (s *MessengerCommunitiesSuite) TestJoinCommunity() { s.Require().Len(response.Communities(), 1) s.Require().True(response.Communities()[0].Joined()) s.Require().Len(response.Chats(), 1) + s.Require().Len(response.Communities()[0].Categories(), 1) + + var categoryID string + for k := range response.Communities()[0].Categories() { + categoryID = k + } // The chat should be created createdChat = response.Chats()[0] @@ -238,6 +264,7 @@ func (s *MessengerCommunitiesSuite) TestJoinCommunity() { s.Require().Equal(orgChat.Identity.DisplayName, createdChat.Name) s.Require().NotEmpty(createdChat.ID) s.Require().Equal(ChatTypeCommunityChat, createdChat.ChatType) + s.Require().Equal(categoryID, createdChat.CategoryID) s.Require().True(createdChat.Active) s.Require().NotEmpty(createdChat.Timestamp) s.Require().True(strings.HasPrefix(createdChat.ID, community.IDString())) @@ -464,6 +491,15 @@ func (s *MessengerCommunitiesSuite) TestImportCommunity() { community := response.Communities()[0] + category := &requests.CreateCommunityCategory{ + CommunityID: community.ID(), + CategoryName: "category-name", + ChatIDs: []string{}, + } + + response, err = s.bob.CreateCommunityCategory(category) + community = response.Communities()[0] + privateKey, err := s.bob.ExportCommunity(community.ID()) s.Require().NoError(err) @@ -496,6 +532,7 @@ func (s *MessengerCommunitiesSuite) TestImportCommunity() { s.Require().NoError(err) s.Require().Len(response.Communities(), 1) + s.Require().Len(response.Communities()[0].Categories(), 1) community = response.Communities()[0] s.Require().True(community.Joined()) s.Require().True(community.IsAdmin()) diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 10dbfabd9..d24325bae 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -235,6 +235,86 @@ func (m *Messenger) RequestToJoinCommunity(request *requests.RequestToJoinCommun return response, nil } +func (m *Messenger) CreateCommunityCategory(request *requests.CreateCommunityCategory) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + var response MessengerResponse + community, changes, err := m.communitiesManager.CreateCategory(request) + if err != nil { + return nil, err + } + response.AddCommunity(community) + response.CommunityChanges = []*communities.CommunityChanges{changes} + + return &response, nil +} + +func (m *Messenger) EditCommunityCategory(request *requests.EditCommunityCategory) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + var response MessengerResponse + community, changes, err := m.communitiesManager.EditCategory(request) + if err != nil { + return nil, err + } + response.AddCommunity(community) + response.CommunityChanges = []*communities.CommunityChanges{changes} + + return &response, nil +} + +func (m *Messenger) ReorderCommunityCategories(request *requests.ReorderCommunityCategories) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + var response MessengerResponse + community, changes, err := m.communitiesManager.ReorderCategories(request) + if err != nil { + return nil, err + } + response.AddCommunity(community) + response.CommunityChanges = []*communities.CommunityChanges{changes} + + return &response, nil +} + +func (m *Messenger) ReorderCommunityChat(request *requests.ReorderCommunityChat) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + var response MessengerResponse + community, changes, err := m.communitiesManager.ReorderChat(request) + if err != nil { + return nil, err + } + response.AddCommunity(community) + response.CommunityChanges = []*communities.CommunityChanges{changes} + + return &response, nil +} + +func (m *Messenger) DeleteCommunityCategory(request *requests.DeleteCommunityCategory) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + var response MessengerResponse + community, changes, err := m.communitiesManager.DeleteCategory(request) + if err != nil { + return nil, err + } + response.AddCommunity(community) + response.CommunityChanges = []*communities.CommunityChanges{changes} + + return &response, nil +} + func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequestToJoinCommunity) (*MessengerResponse, error) { if err := request.Validate(); err != nil { return nil, err diff --git a/protocol/protobuf/communities.pb.go b/protocol/protobuf/communities.pb.go index d8eaff5b5..ca1d49ba4 100644 --- a/protocol/protobuf/communities.pb.go +++ b/protocol/protobuf/communities.pb.go @@ -238,15 +238,16 @@ func (m *CommunityPermissions) GetAccess() CommunityPermissions_Access { } type CommunityDescription struct { - Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"` - Members map[string]*CommunityMember `protobuf:"bytes,2,rep,name=members,proto3" json:"members,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Permissions *CommunityPermissions `protobuf:"bytes,3,opt,name=permissions,proto3" json:"permissions,omitempty"` - Identity *ChatIdentity `protobuf:"bytes,5,opt,name=identity,proto3" json:"identity,omitempty"` - Chats map[string]*CommunityChat `protobuf:"bytes,6,rep,name=chats,proto3" json:"chats,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - BanList []string `protobuf:"bytes,7,rep,name=ban_list,json=banList,proto3" json:"ban_list,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"` + Members map[string]*CommunityMember `protobuf:"bytes,2,rep,name=members,proto3" json:"members,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Permissions *CommunityPermissions `protobuf:"bytes,3,opt,name=permissions,proto3" json:"permissions,omitempty"` + Identity *ChatIdentity `protobuf:"bytes,5,opt,name=identity,proto3" json:"identity,omitempty"` + Chats map[string]*CommunityChat `protobuf:"bytes,6,rep,name=chats,proto3" json:"chats,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + BanList []string `protobuf:"bytes,7,rep,name=ban_list,json=banList,proto3" json:"ban_list,omitempty"` + Categories map[string]*CommunityCategory `protobuf:"bytes,8,rep,name=categories,proto3" json:"categories,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *CommunityDescription) Reset() { *m = CommunityDescription{} } @@ -316,10 +317,19 @@ func (m *CommunityDescription) GetBanList() []string { return nil } +func (m *CommunityDescription) GetCategories() map[string]*CommunityCategory { + if m != nil { + return m.Categories + } + return nil +} + type CommunityChat struct { Members map[string]*CommunityMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Permissions *CommunityPermissions `protobuf:"bytes,2,opt,name=permissions,proto3" json:"permissions,omitempty"` Identity *ChatIdentity `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` + CategoryId string `protobuf:"bytes,4,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + Position int32 `protobuf:"varint,5,opt,name=position,proto3" json:"position,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -371,6 +381,75 @@ func (m *CommunityChat) GetIdentity() *ChatIdentity { return nil } +func (m *CommunityChat) GetCategoryId() string { + if m != nil { + return m.CategoryId + } + return "" +} + +func (m *CommunityChat) GetPosition() int32 { + if m != nil { + return m.Position + } + return 0 +} + +type CommunityCategory struct { + CategoryId string `protobuf:"bytes,1,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Position int32 `protobuf:"varint,3,opt,name=position,proto3" json:"position,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CommunityCategory) Reset() { *m = CommunityCategory{} } +func (m *CommunityCategory) String() string { return proto.CompactTextString(m) } +func (*CommunityCategory) ProtoMessage() {} +func (*CommunityCategory) Descriptor() ([]byte, []int) { + return fileDescriptor_f937943d74c1cd8b, []int{5} +} + +func (m *CommunityCategory) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CommunityCategory.Unmarshal(m, b) +} +func (m *CommunityCategory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CommunityCategory.Marshal(b, m, deterministic) +} +func (m *CommunityCategory) XXX_Merge(src proto.Message) { + xxx_messageInfo_CommunityCategory.Merge(m, src) +} +func (m *CommunityCategory) XXX_Size() int { + return xxx_messageInfo_CommunityCategory.Size(m) +} +func (m *CommunityCategory) XXX_DiscardUnknown() { + xxx_messageInfo_CommunityCategory.DiscardUnknown(m) +} + +var xxx_messageInfo_CommunityCategory proto.InternalMessageInfo + +func (m *CommunityCategory) GetCategoryId() string { + if m != nil { + return m.CategoryId + } + return "" +} + +func (m *CommunityCategory) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *CommunityCategory) GetPosition() int32 { + if m != nil { + return m.Position + } + return 0 +} + type CommunityInvitation struct { CommunityDescription []byte `protobuf:"bytes,1,opt,name=community_description,json=communityDescription,proto3" json:"community_description,omitempty"` Grant []byte `protobuf:"bytes,2,opt,name=grant,proto3" json:"grant,omitempty"` @@ -385,7 +464,7 @@ func (m *CommunityInvitation) Reset() { *m = CommunityInvitation{} } func (m *CommunityInvitation) String() string { return proto.CompactTextString(m) } func (*CommunityInvitation) ProtoMessage() {} func (*CommunityInvitation) Descriptor() ([]byte, []int) { - return fileDescriptor_f937943d74c1cd8b, []int{5} + return fileDescriptor_f937943d74c1cd8b, []int{6} } func (m *CommunityInvitation) XXX_Unmarshal(b []byte) error { @@ -448,7 +527,7 @@ func (m *CommunityRequestToJoin) Reset() { *m = CommunityRequestToJoin{} func (m *CommunityRequestToJoin) String() string { return proto.CompactTextString(m) } func (*CommunityRequestToJoin) ProtoMessage() {} func (*CommunityRequestToJoin) Descriptor() ([]byte, []int) { - return fileDescriptor_f937943d74c1cd8b, []int{6} + return fileDescriptor_f937943d74c1cd8b, []int{7} } func (m *CommunityRequestToJoin) XXX_Unmarshal(b []byte) error { @@ -511,7 +590,7 @@ func (m *CommunityRequestToJoinResponse) Reset() { *m = CommunityRequest func (m *CommunityRequestToJoinResponse) String() string { return proto.CompactTextString(m) } func (*CommunityRequestToJoinResponse) ProtoMessage() {} func (*CommunityRequestToJoinResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_f937943d74c1cd8b, []int{7} + return fileDescriptor_f937943d74c1cd8b, []int{8} } func (m *CommunityRequestToJoinResponse) XXX_Unmarshal(b []byte) error { @@ -567,10 +646,12 @@ func init() { proto.RegisterType((*CommunityMember)(nil), "protobuf.CommunityMember") proto.RegisterType((*CommunityPermissions)(nil), "protobuf.CommunityPermissions") proto.RegisterType((*CommunityDescription)(nil), "protobuf.CommunityDescription") + proto.RegisterMapType((map[string]*CommunityCategory)(nil), "protobuf.CommunityDescription.CategoriesEntry") proto.RegisterMapType((map[string]*CommunityChat)(nil), "protobuf.CommunityDescription.ChatsEntry") proto.RegisterMapType((map[string]*CommunityMember)(nil), "protobuf.CommunityDescription.MembersEntry") proto.RegisterType((*CommunityChat)(nil), "protobuf.CommunityChat") proto.RegisterMapType((map[string]*CommunityMember)(nil), "protobuf.CommunityChat.MembersEntry") + proto.RegisterType((*CommunityCategory)(nil), "protobuf.CommunityCategory") proto.RegisterType((*CommunityInvitation)(nil), "protobuf.CommunityInvitation") proto.RegisterType((*CommunityRequestToJoin)(nil), "protobuf.CommunityRequestToJoin") proto.RegisterType((*CommunityRequestToJoinResponse)(nil), "protobuf.CommunityRequestToJoinResponse") @@ -581,53 +662,59 @@ func init() { } var fileDescriptor_f937943d74c1cd8b = []byte{ - // 758 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x54, 0xef, 0x6e, 0xda, 0x48, - 0x10, 0x8f, 0x31, 0x7f, 0xcc, 0x40, 0x88, 0xb3, 0xf9, 0xe7, 0x70, 0xba, 0x1c, 0x67, 0xdd, 0x49, - 0x9c, 0x4e, 0xc7, 0x49, 0x44, 0x27, 0x9d, 0xaa, 0x36, 0x2d, 0x4d, 0xad, 0xd4, 0x0d, 0x98, 0x64, - 0x81, 0x56, 0xfd, 0x64, 0x19, 0xb3, 0x6d, 0xad, 0xc0, 0x9a, 0x7a, 0x0d, 0x12, 0x0f, 0x50, 0xa9, - 0x8f, 0xd0, 0x0f, 0xfd, 0xdc, 0xe7, 0xea, 0x1b, 0xf4, 0x15, 0x2a, 0xef, 0x02, 0x76, 0x52, 0x48, - 0x23, 0x55, 0xfd, 0x64, 0xcf, 0xce, 0xcc, 0x6f, 0x67, 0x7e, 0x33, 0xfb, 0x83, 0x6d, 0xd7, 0x1f, - 0x8d, 0x26, 0xd4, 0x0b, 0x3d, 0xc2, 0x6a, 0xe3, 0xc0, 0x0f, 0x7d, 0xa4, 0xf0, 0x4f, 0x7f, 0xf2, - 0xaa, 0xbc, 0xe3, 0xbe, 0x71, 0x42, 0xdb, 0x1b, 0x10, 0x1a, 0x7a, 0xe1, 0x4c, 0xb8, 0xf5, 0x29, - 0x64, 0xce, 0x02, 0x87, 0x86, 0xe8, 0x77, 0x28, 0x2e, 0x92, 0x67, 0xb6, 0x37, 0xd0, 0xa4, 0x8a, - 0x54, 0x2d, 0xe2, 0xc2, 0xf2, 0xcc, 0x1c, 0xa0, 0x5f, 0x20, 0x3f, 0x22, 0xa3, 0x3e, 0x09, 0x22, - 0x7f, 0x8a, 0xfb, 0x15, 0x71, 0x60, 0x0e, 0xd0, 0x01, 0xe4, 0xe6, 0xf8, 0x9a, 0x5c, 0x91, 0xaa, - 0x79, 0x9c, 0x8d, 0x4c, 0x73, 0x80, 0x76, 0x21, 0xe3, 0x0e, 0x7d, 0xf7, 0x4a, 0x4b, 0x57, 0xa4, - 0x6a, 0x1a, 0x0b, 0x43, 0x7f, 0x2f, 0xc1, 0xd6, 0xe9, 0x02, 0xbb, 0xc5, 0x41, 0xd0, 0x7f, 0x90, - 0x09, 0xfc, 0x21, 0x61, 0x9a, 0x54, 0x91, 0xab, 0xa5, 0xfa, 0x6f, 0xb5, 0x45, 0xe9, 0xb5, 0x1b, - 0x91, 0x35, 0x1c, 0x85, 0x61, 0x11, 0xad, 0x9f, 0x40, 0x86, 0xdb, 0x48, 0x85, 0x62, 0xcf, 0x3a, - 0xb7, 0xda, 0x2f, 0x2c, 0x1b, 0xb7, 0x9b, 0x86, 0xba, 0x81, 0x8a, 0xa0, 0x44, 0x7f, 0x76, 0xa3, - 0xd9, 0x54, 0x25, 0xb4, 0x07, 0xdb, 0xdc, 0x6a, 0x35, 0xac, 0xc6, 0x99, 0x61, 0xf7, 0x3a, 0x06, - 0xee, 0xa8, 0x29, 0xfd, 0xb3, 0x04, 0xbb, 0xcb, 0x0b, 0x2e, 0x48, 0x30, 0xf2, 0x18, 0xf3, 0x7c, - 0xca, 0xd0, 0x21, 0x28, 0x84, 0x32, 0xdb, 0xa7, 0xc3, 0x19, 0xa7, 0x43, 0xc1, 0x39, 0x42, 0x59, - 0x9b, 0x0e, 0x67, 0x48, 0x83, 0xdc, 0x38, 0xf0, 0xa6, 0x4e, 0x48, 0x38, 0x11, 0x0a, 0x5e, 0x98, - 0xe8, 0x01, 0x64, 0x1d, 0xd7, 0x25, 0x8c, 0x71, 0x1a, 0x4a, 0xf5, 0x3f, 0x57, 0x74, 0x91, 0xb8, - 0xa4, 0xd6, 0xe0, 0xc1, 0x78, 0x9e, 0xa4, 0x77, 0x21, 0x2b, 0x4e, 0x10, 0x82, 0xd2, 0xa2, 0x9b, - 0xc6, 0xe9, 0xa9, 0xd1, 0xe9, 0xa8, 0x1b, 0x68, 0x1b, 0x36, 0xad, 0xb6, 0xdd, 0x32, 0x5a, 0x8f, - 0x0d, 0xdc, 0x79, 0x6a, 0x5e, 0xa8, 0x12, 0xda, 0x81, 0x2d, 0xd3, 0x7a, 0x6e, 0x76, 0x1b, 0x5d, - 0xb3, 0x6d, 0xd9, 0x6d, 0xab, 0xf9, 0x52, 0x4d, 0xa1, 0x12, 0x40, 0xdb, 0xb2, 0xb1, 0x71, 0xd9, - 0x33, 0x3a, 0x5d, 0x55, 0xd6, 0xbf, 0xc8, 0x89, 0x16, 0x9f, 0x10, 0xe6, 0x06, 0xde, 0x38, 0xf4, - 0x7c, 0x1a, 0x0f, 0x47, 0x4a, 0x0c, 0x07, 0x19, 0x90, 0x13, 0x73, 0x65, 0x5a, 0xaa, 0x22, 0x57, - 0x0b, 0xf5, 0xbf, 0x57, 0x34, 0x91, 0x80, 0xa9, 0x89, 0xb1, 0x30, 0x83, 0x86, 0xc1, 0x0c, 0x2f, - 0x72, 0xd1, 0x23, 0x28, 0x8c, 0xe3, 0x4e, 0x39, 0x1f, 0x85, 0xfa, 0xd1, 0xed, 0x7c, 0xe0, 0x64, - 0x0a, 0xaa, 0x83, 0xb2, 0xd8, 0x57, 0x2d, 0xc3, 0xd3, 0xf7, 0x13, 0xe9, 0x7c, 0xbf, 0x84, 0x17, - 0x2f, 0xe3, 0xd0, 0x43, 0xc8, 0x44, 0x9b, 0xc7, 0xb4, 0x2c, 0x2f, 0xfd, 0xaf, 0xef, 0x94, 0x1e, - 0xa1, 0xcc, 0x0b, 0x17, 0x79, 0xd1, 0xd8, 0xfb, 0x0e, 0xb5, 0x87, 0x1e, 0x0b, 0xb5, 0x5c, 0x45, - 0xae, 0xe6, 0x71, 0xae, 0xef, 0xd0, 0xa6, 0xc7, 0xc2, 0x72, 0x0f, 0x8a, 0xc9, 0x56, 0x91, 0x0a, - 0xf2, 0x15, 0x11, 0xcb, 0x91, 0xc7, 0xd1, 0x2f, 0xfa, 0x17, 0x32, 0x53, 0x67, 0x38, 0x11, 0x6b, - 0x51, 0xa8, 0x1f, 0xae, 0xdd, 0x61, 0x2c, 0xe2, 0xee, 0xa5, 0xfe, 0x97, 0xca, 0x97, 0x00, 0x71, - 0x19, 0x2b, 0x40, 0xff, 0xb9, 0x0e, 0x7a, 0xb0, 0x02, 0x34, 0xca, 0x4f, 0x40, 0xea, 0x1f, 0x53, - 0xb0, 0x79, 0xcd, 0x89, 0x4e, 0xe2, 0xa1, 0x4a, 0x9c, 0x99, 0x3f, 0xd6, 0xc0, 0xdc, 0x6d, 0x9a, - 0xa9, 0x1f, 0x9b, 0xa6, 0x7c, 0xb7, 0x69, 0xfe, 0x24, 0xc6, 0xf5, 0x0f, 0x12, 0xec, 0x2c, 0xdd, - 0x26, 0x9d, 0x7a, 0xa1, 0xc3, 0xdf, 0xc3, 0x31, 0xec, 0xc5, 0x2a, 0x38, 0x88, 0xd7, 0x64, 0x2e, - 0x87, 0xbb, 0xee, 0x9a, 0x47, 0xf4, 0x3a, 0xd2, 0xd0, 0xb9, 0x26, 0x0a, 0x63, 0xbd, 0x20, 0xfe, - 0x0a, 0x30, 0x9e, 0xf4, 0x87, 0x9e, 0x6b, 0x47, 0x9d, 0xa4, 0x79, 0x4e, 0x5e, 0x9c, 0x9c, 0x93, - 0x99, 0xfe, 0x4e, 0x82, 0xfd, 0x65, 0x69, 0x98, 0xbc, 0x9d, 0x10, 0x16, 0x76, 0xfd, 0x67, 0xbe, - 0xb7, 0xee, 0xb5, 0xce, 0x65, 0x8a, 0x3a, 0x23, 0xc1, 0x41, 0x9e, 0xcb, 0x94, 0xe5, 0x8c, 0xc8, - 0xfa, 0x1a, 0x6e, 0xaa, 0x7d, 0xfa, 0x1b, 0xb5, 0xd7, 0x3f, 0x49, 0x70, 0xb4, 0xba, 0x0e, 0x4c, - 0xd8, 0xd8, 0xa7, 0x8c, 0xac, 0xa9, 0xe7, 0x3e, 0xe4, 0x97, 0x38, 0xb7, 0xac, 0x49, 0x82, 0x41, - 0x1c, 0x27, 0xa0, 0x32, 0x28, 0x91, 0x14, 0x8e, 0x43, 0x22, 0x6a, 0x56, 0xf0, 0xd2, 0x8e, 0x89, - 0x4e, 0x27, 0x88, 0xee, 0x67, 0x39, 0xf6, 0xf1, 0xd7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x16, 0xf8, - 0x6e, 0x5b, 0xfd, 0x06, 0x00, 0x00, + // 850 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xef, 0x8e, 0xdb, 0x44, + 0x10, 0xef, 0xc6, 0x71, 0xe2, 0x4c, 0xd2, 0x3b, 0xdf, 0xde, 0xb5, 0x75, 0xaf, 0xa2, 0x0d, 0x16, + 0x48, 0x41, 0x88, 0x20, 0x52, 0x21, 0x21, 0x04, 0x85, 0x70, 0x58, 0xc5, 0x34, 0xe7, 0xb4, 0x9b, + 0x1c, 0x08, 0xbe, 0x58, 0x8e, 0xb3, 0x94, 0x55, 0x13, 0xdb, 0x78, 0x9d, 0x93, 0xf2, 0x00, 0x48, + 0x3c, 0x02, 0x12, 0xdf, 0x79, 0x27, 0xbe, 0xf1, 0x28, 0x68, 0x77, 0xe3, 0x3f, 0xb9, 0x4b, 0xda, + 0x93, 0x50, 0x3f, 0xc5, 0xb3, 0xbb, 0xf3, 0x9b, 0xdf, 0xfc, 0x66, 0x32, 0x03, 0x47, 0x61, 0xbc, + 0x5c, 0xae, 0x22, 0x96, 0x31, 0xca, 0xfb, 0x49, 0x1a, 0x67, 0x31, 0x36, 0xe4, 0xcf, 0x6c, 0xf5, + 0xcb, 0xe9, 0x71, 0xf8, 0x6b, 0x90, 0xf9, 0x6c, 0x4e, 0xa3, 0x8c, 0x65, 0x6b, 0x75, 0x6d, 0x5f, + 0x82, 0xfe, 0x34, 0x0d, 0xa2, 0x0c, 0xbf, 0x0b, 0x9d, 0xdc, 0x79, 0xed, 0xb3, 0xb9, 0x85, 0xba, + 0xa8, 0xd7, 0x21, 0xed, 0xe2, 0xcc, 0x9d, 0xe3, 0x07, 0xd0, 0x5a, 0xd2, 0xe5, 0x8c, 0xa6, 0xe2, + 0xbe, 0x26, 0xef, 0x0d, 0x75, 0xe0, 0xce, 0xf1, 0x3d, 0x68, 0x6e, 0xf0, 0x2d, 0xad, 0x8b, 0x7a, + 0x2d, 0xd2, 0x10, 0xa6, 0x3b, 0xc7, 0x27, 0xa0, 0x87, 0x8b, 0x38, 0x7c, 0x65, 0xd5, 0xbb, 0xa8, + 0x57, 0x27, 0xca, 0xb0, 0xff, 0x40, 0x70, 0x78, 0x96, 0x63, 0x9f, 0x4b, 0x10, 0xfc, 0x29, 0xe8, + 0x69, 0xbc, 0xa0, 0xdc, 0x42, 0x5d, 0xad, 0x77, 0x30, 0x78, 0xd4, 0xcf, 0xa9, 0xf7, 0xaf, 0xbc, + 0xec, 0x13, 0xf1, 0x8c, 0xa8, 0xd7, 0xf6, 0x13, 0xd0, 0xa5, 0x8d, 0x4d, 0xe8, 0x5c, 0x78, 0xcf, + 0xbc, 0xf1, 0x8f, 0x9e, 0x4f, 0xc6, 0x23, 0xc7, 0xbc, 0x85, 0x3b, 0x60, 0x88, 0x2f, 0x7f, 0x38, + 0x1a, 0x99, 0x08, 0xdf, 0x81, 0x23, 0x69, 0x9d, 0x0f, 0xbd, 0xe1, 0x53, 0xc7, 0xbf, 0x98, 0x38, + 0x64, 0x62, 0xd6, 0xec, 0x7f, 0x11, 0x9c, 0x14, 0x01, 0x9e, 0xd3, 0x74, 0xc9, 0x38, 0x67, 0x71, + 0xc4, 0xf1, 0x7d, 0x30, 0x68, 0xc4, 0xfd, 0x38, 0x5a, 0xac, 0xa5, 0x1c, 0x06, 0x69, 0xd2, 0x88, + 0x8f, 0xa3, 0xc5, 0x1a, 0x5b, 0xd0, 0x4c, 0x52, 0x76, 0x19, 0x64, 0x54, 0x0a, 0x61, 0x90, 0xdc, + 0xc4, 0x5f, 0x42, 0x23, 0x08, 0x43, 0xca, 0xb9, 0x94, 0xe1, 0x60, 0xf0, 0xfe, 0x8e, 0x2c, 0x2a, + 0x41, 0xfa, 0x43, 0xf9, 0x98, 0x6c, 0x9c, 0xec, 0x29, 0x34, 0xd4, 0x09, 0xc6, 0x70, 0x90, 0x67, + 0x33, 0x3c, 0x3b, 0x73, 0x26, 0x13, 0xf3, 0x16, 0x3e, 0x82, 0xdb, 0xde, 0xd8, 0x3f, 0x77, 0xce, + 0xbf, 0x71, 0xc8, 0xe4, 0x3b, 0xf7, 0xb9, 0x89, 0xf0, 0x31, 0x1c, 0xba, 0xde, 0x0f, 0xee, 0x74, + 0x38, 0x75, 0xc7, 0x9e, 0x3f, 0xf6, 0x46, 0x3f, 0x99, 0x35, 0x7c, 0x00, 0x30, 0xf6, 0x7c, 0xe2, + 0xbc, 0xb8, 0x70, 0x26, 0x53, 0x53, 0xb3, 0xff, 0xd2, 0x2b, 0x29, 0x7e, 0x4b, 0x79, 0x98, 0xb2, + 0x24, 0x63, 0x71, 0x54, 0x16, 0x07, 0x55, 0x8a, 0x83, 0x1d, 0x68, 0xaa, 0xba, 0x72, 0xab, 0xd6, + 0xd5, 0x7a, 0xed, 0xc1, 0x87, 0x3b, 0x92, 0xa8, 0xc0, 0xf4, 0x55, 0x59, 0xb8, 0x13, 0x65, 0xe9, + 0x9a, 0xe4, 0xbe, 0xf8, 0x6b, 0x68, 0x27, 0x65, 0xa6, 0x52, 0x8f, 0xf6, 0xe0, 0xe1, 0xeb, 0xf5, + 0x20, 0x55, 0x17, 0x3c, 0x00, 0x23, 0xef, 0x57, 0x4b, 0x97, 0xee, 0x77, 0x2b, 0xee, 0xb2, 0xbf, + 0xd4, 0x2d, 0x29, 0xde, 0xe1, 0xaf, 0x40, 0x17, 0x9d, 0xc7, 0xad, 0x86, 0xa4, 0xfe, 0xc1, 0x1b, + 0xa8, 0x0b, 0x94, 0x0d, 0x71, 0xe5, 0x27, 0xca, 0x3e, 0x0b, 0x22, 0x7f, 0xc1, 0x78, 0x66, 0x35, + 0xbb, 0x5a, 0xaf, 0x45, 0x9a, 0xb3, 0x20, 0x1a, 0x31, 0x9e, 0x61, 0x0f, 0x20, 0x0c, 0x32, 0xfa, + 0x32, 0x4e, 0x19, 0xe5, 0x96, 0x21, 0x03, 0xf4, 0xdf, 0x14, 0xa0, 0x70, 0x50, 0x51, 0x2a, 0x08, + 0xa7, 0x17, 0xd0, 0xa9, 0x4a, 0x87, 0x4d, 0xd0, 0x5e, 0x51, 0xd5, 0x6c, 0x2d, 0x22, 0x3e, 0xf1, + 0xc7, 0xa0, 0x5f, 0x06, 0x8b, 0x95, 0x6a, 0xb3, 0xf6, 0xe0, 0xfe, 0xde, 0xff, 0x04, 0x51, 0xef, + 0x3e, 0xaf, 0x7d, 0x86, 0x4e, 0x5f, 0x00, 0x94, 0x69, 0xed, 0x00, 0xfd, 0x68, 0x1b, 0xf4, 0xde, + 0x0e, 0x50, 0xe1, 0x5f, 0x85, 0xfc, 0x19, 0x0e, 0xaf, 0x24, 0xb2, 0x03, 0xf7, 0x93, 0x6d, 0xdc, + 0x07, 0xbb, 0x70, 0x15, 0xc8, 0xba, 0x82, 0x6d, 0xff, 0x53, 0x83, 0xdb, 0x5b, 0x81, 0xf1, 0x93, + 0xb2, 0x01, 0x91, 0x14, 0xf9, 0xbd, 0x3d, 0x14, 0x6f, 0xd6, 0x79, 0xb5, 0xff, 0xd7, 0x79, 0xda, + 0x0d, 0x3b, 0xef, 0x11, 0xb4, 0x37, 0xb5, 0x95, 0x13, 0xb4, 0x2e, 0x85, 0xc9, 0xcb, 0x2d, 0x06, + 0xe8, 0x29, 0x18, 0x49, 0xcc, 0x99, 0x68, 0x0b, 0xd9, 0xce, 0x3a, 0x29, 0xec, 0xb7, 0xd4, 0x0a, + 0xf6, 0x1c, 0x8e, 0xae, 0x69, 0x7f, 0x95, 0x28, 0xba, 0x46, 0x14, 0x43, 0x3d, 0x0a, 0x96, 0x2a, + 0x52, 0x8b, 0xc8, 0xef, 0x2d, 0xf2, 0xda, 0x36, 0x79, 0xfb, 0x4f, 0x04, 0xc7, 0x45, 0x18, 0x37, + 0xba, 0x64, 0x59, 0x20, 0xc7, 0xcb, 0x63, 0xb8, 0x53, 0x2e, 0x95, 0x79, 0xf9, 0xa7, 0xd8, 0x6c, + 0x97, 0x93, 0x70, 0xcf, 0x4c, 0x7a, 0x29, 0x56, 0xd2, 0x66, 0xc5, 0x28, 0x63, 0xff, 0x7e, 0x79, + 0x07, 0x20, 0x59, 0xcd, 0x16, 0x2c, 0xf4, 0x85, 0x5e, 0x75, 0xe9, 0xd3, 0x52, 0x27, 0xcf, 0xe8, + 0xda, 0xfe, 0x1d, 0xc1, 0xdd, 0x82, 0x1a, 0xa1, 0xbf, 0xad, 0x28, 0xcf, 0xa6, 0xf1, 0xf7, 0x31, + 0xdb, 0x37, 0xfc, 0x36, 0x53, 0xbf, 0x92, 0xbf, 0x98, 0xfa, 0x9e, 0x90, 0x60, 0x2f, 0x87, 0xab, + 0xcb, 0xb3, 0x7e, 0x6d, 0x79, 0xda, 0x7f, 0x23, 0x78, 0xb8, 0x9b, 0x07, 0xa1, 0x3c, 0x89, 0x23, + 0x4e, 0xf7, 0xf0, 0xf9, 0x02, 0x5a, 0x05, 0xce, 0x6b, 0x3a, 0xb9, 0xa2, 0x20, 0x29, 0x1d, 0x44, + 0xd5, 0xc4, 0x66, 0x49, 0x32, 0xaa, 0x38, 0x1b, 0xa4, 0xb0, 0x4b, 0xa1, 0xeb, 0x15, 0xa1, 0x67, + 0x0d, 0x89, 0xfd, 0xf8, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x77, 0xbe, 0x23, 0x4c, 0x08, + 0x00, 0x00, } diff --git a/protocol/protobuf/communities.proto b/protocol/protobuf/communities.proto index ba6d2f8c4..2db9c37a3 100644 --- a/protocol/protobuf/communities.proto +++ b/protocol/protobuf/communities.proto @@ -41,12 +41,21 @@ message CommunityDescription { ChatIdentity identity = 5; map chats = 6; repeated string ban_list = 7; + map categories = 8; } message CommunityChat { map members = 1; CommunityPermissions permissions = 2; ChatIdentity identity = 3; + string category_id = 4; + int32 position = 5; +} + +message CommunityCategory { + string category_id = 1; + string name = 2; + int32 position = 3; } message CommunityInvitation { diff --git a/protocol/requests/create_community_category.go b/protocol/requests/create_community_category.go new file mode 100644 index 000000000..75a19a148 --- /dev/null +++ b/protocol/requests/create_community_category.go @@ -0,0 +1,28 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrCreateCommunityCategoryInvalidCommunityID = errors.New("create-community-category: invalid community id") +var ErrCreateCommunityCategoryInvalidName = errors.New("create-community-category: invalid category name") + +type CreateCommunityCategory struct { + CommunityID types.HexBytes `json:"communityId"` + CategoryName string `json:"categoryName"` + ChatIDs []string `json:"chatIds"` +} + +func (j *CreateCommunityCategory) Validate() error { + if len(j.CommunityID) == 0 { + return ErrCreateCommunityCategoryInvalidCommunityID + } + + if len(j.CategoryName) == 0 { + return ErrCreateCommunityCategoryInvalidName + } + + return nil +} diff --git a/protocol/requests/delete_community_category.go b/protocol/requests/delete_community_category.go new file mode 100644 index 000000000..c945d5a24 --- /dev/null +++ b/protocol/requests/delete_community_category.go @@ -0,0 +1,28 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrDeleteCommunityCategoryInvalidCommunityID = errors.New("set-community-chat-category: invalid community id") +var ErrDeleteCommunityCategoryInvalidCategoryID = errors.New("set-community-chat-category: invalid category id") + +type DeleteCommunityCategory struct { + CommunityID types.HexBytes `json:"communityId"` + CategoryID string `json:"categoryId"` +} + +func (j *DeleteCommunityCategory) Validate() error { + if len(j.CommunityID) == 0 { + return ErrDeleteCommunityCategoryInvalidCommunityID + } + + if len(j.CategoryID) == 0 { + return ErrDeleteCommunityCategoryInvalidCategoryID + + } + + return nil +} diff --git a/protocol/requests/edit_community_category.go b/protocol/requests/edit_community_category.go new file mode 100644 index 000000000..245832b83 --- /dev/null +++ b/protocol/requests/edit_community_category.go @@ -0,0 +1,34 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrEditCommunityCategoryInvalidCommunityID = errors.New("edit-community-category: invalid community id") +var ErrEditCommunityCategoryInvalidCategoryID = errors.New("edit-community-category: invalid category id") +var ErrEditCommunityCategoryInvalidName = errors.New("edit-community-category: invalid category name") + +type EditCommunityCategory struct { + CommunityID types.HexBytes `json:"communityId"` + CategoryID string `json:"categoryId"` + CategoryName string `json:"categoryName"` + ChatIDs []string `json:"chatIds"` +} + +func (j *EditCommunityCategory) Validate() error { + if len(j.CommunityID) == 0 { + return ErrEditCommunityCategoryInvalidCommunityID + } + + if len(j.CategoryID) == 0 { + return ErrEditCommunityCategoryInvalidCategoryID + } + + if len(j.CategoryName) == 0 { + return ErrEditCommunityCategoryInvalidName + } + + return nil +} diff --git a/protocol/requests/reorder_community_category.go b/protocol/requests/reorder_community_category.go new file mode 100644 index 000000000..c4da6c5e2 --- /dev/null +++ b/protocol/requests/reorder_community_category.go @@ -0,0 +1,33 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrReorderCommunityCategoryInvalidCommunityID = errors.New("edit-community-category: invalid community id") +var ErrReorderCommunityCategoryInvalidCategoryID = errors.New("edit-community-category: invalid category id") +var ErrReorderCommunityCategoryInvalidPosition = errors.New("edit-community-category: invalid position") + +type ReorderCommunityCategories struct { + CommunityID types.HexBytes `json:"communityId"` + CategoryID string `json:"categoryId"` + Position int `json:"position"` +} + +func (j *ReorderCommunityCategories) Validate() error { + if len(j.CommunityID) == 0 { + return ErrReorderCommunityCategoryInvalidCommunityID + } + + if len(j.CategoryID) == 0 { + return ErrEditCommunityCategoryInvalidCategoryID + } + + if j.Position < 0 { + return ErrReorderCommunityCategoryInvalidPosition + } + + return nil +} diff --git a/protocol/requests/reorder_community_chat.go b/protocol/requests/reorder_community_chat.go new file mode 100644 index 000000000..71d381e01 --- /dev/null +++ b/protocol/requests/reorder_community_chat.go @@ -0,0 +1,39 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrReorderCommunityChatInvalidCommunityID = errors.New("edit-community-category: invalid community id") +var ErrReorderCommunityChatInvalidCategoryID = errors.New("edit-community-category: invalid category id") +var ErrReorderCommunityChatInvalidChatID = errors.New("edit-community-category: invalid chat id") +var ErrReorderCommunityChatInvalidPosition = errors.New("edit-community-category: invalid position") + +type ReorderCommunityChat struct { + CommunityID types.HexBytes `json:"communityId"` + CategoryID string `json:"categoryId"` + ChatID string `json:"chatId"` + Position int `json:"position"` +} + +func (j *ReorderCommunityChat) Validate() error { + if len(j.CommunityID) == 0 { + return ErrReorderCommunityChatInvalidCommunityID + } + + if len(j.CategoryID) == 0 { + return ErrReorderCommunityChatInvalidCategoryID + } + + if len(j.ChatID) == 0 { + return ErrReorderCommunityChatInvalidChatID + } + + if j.Position < 0 { + return ErrReorderCommunityCategoryInvalidPosition + } + + return nil +} diff --git a/services/ext/api.go b/services/ext/api.go index e8a06d36d..5106937a0 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -412,6 +412,31 @@ func (api *PublicAPI) RequestToJoinCommunity(request *requests.RequestToJoinComm return api.service.messenger.RequestToJoinCommunity(request) } +// CreateCommunityCategory creates a category within a particular community +func (api *PublicAPI) CreateCommunityCategory(request *requests.CreateCommunityCategory) (*protocol.MessengerResponse, error) { + return api.service.messenger.CreateCommunityCategory(request) +} + +// ReorderCommunityCategories is used to change the order of the categories of a community +func (api *PublicAPI) ReorderCommunityCategories(request *requests.ReorderCommunityCategories) (*protocol.MessengerResponse, error) { + return api.service.messenger.ReorderCommunityCategories(request) +} + +// ReorderCommunityChat allows changing the order of the chat or switching its category +func (api *PublicAPI) ReorderCommunityChat(request *requests.ReorderCommunityChat) (*protocol.MessengerResponse, error) { + return api.service.messenger.ReorderCommunityChat(request) +} + +// EditCommunityCategory modifies a category within a particular community +func (api *PublicAPI) EditCommunityCategory(request *requests.EditCommunityCategory) (*protocol.MessengerResponse, error) { + return api.service.messenger.EditCommunityCategory(request) +} + +// DeleteCommunityCategory deletes a category within a particular community and removes this category from any chat that has it +func (api *PublicAPI) DeleteCommunityCategory(request *requests.DeleteCommunityCategory) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeleteCommunityCategory(request) +} + type ApplicationMessagesResponse struct { Messages []*common.Message `json:"messages"` Cursor string `json:"cursor"`