diff --git a/internal/payload/incoming/channel.go b/internal/payload/incoming/channel.go index 71f7c5214..f910f93b6 100644 --- a/internal/payload/incoming/channel.go +++ b/internal/payload/incoming/channel.go @@ -29,10 +29,6 @@ type ( Type *string `json:"type"` } - ChannelDelete struct { - ChannelID string `json:"id"` - } - // ChannelActivity is sent from the client when there is an activity on the channel... ChannelActivity struct { ChannelID uint64 `json:"ID,string"` diff --git a/internal/payload/incoming/payload.go b/internal/payload/incoming/payload.go index 94f5dd9f9..e814f559b 100644 --- a/internal/payload/incoming/payload.go +++ b/internal/payload/incoming/payload.go @@ -8,7 +8,6 @@ type Payload struct { *ChannelCreate `json:"createChannel"` *ChannelUpdate `json:"updateChannel"` - *ChannelDelete `json:"deleteChannel"` *ChannelViewRecord `json:"recordChannelView"` diff --git a/sam/docs/README.md b/sam/docs/README.md index 41f693b79..6762c1cac 100644 --- a/sam/docs/README.md +++ b/sam/docs/README.md @@ -265,9 +265,23 @@ A channel is a representation of a sequence of messages. It has meta data like c | name | string | POST | Name of Channel | N/A | NO | | topic | string | POST | Subject of Channel | N/A | NO | | type | string | POST | Channel type | N/A | NO | -| archive | bool | POST | Request channel to be archived or unarchived | N/A | NO | | organisationID | uint64 | POST | Move channel to different organisation | N/A | NO | +## Update channel state + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/channels/{channelID}/state` | HTTP/S | PUT | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| channelID | uint64 | PATH | Channel ID | N/A | YES | +| state | string | POST | Valid values: delete, restore, archive, unarchive | N/A | YES | + ## Read channel details #### Method @@ -282,20 +296,6 @@ A channel is a representation of a sequence of messages. It has meta data like c | --------- | ---- | ------ | ----------- | ------- | --------- | | channelID | uint64 | PATH | Channel ID | N/A | YES | -## Remove channel - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/channels/{channelID}` | HTTP/S | DELETE | Client ID, Session ID | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| channelID | uint64 | PATH | Channel ID | N/A | YES | - ## List channel members #### Method diff --git a/sam/docs/src/spec.json b/sam/docs/src/spec.json index e2f36495f..9ed77213f 100644 --- a/sam/docs/src/spec.json +++ b/sam/docs/src/spec.json @@ -221,7 +221,7 @@ } }, { - "name": "edit", + "name": "update", "method": "PUT", "path": "/{channelID}", "title": "Update channel details", @@ -233,11 +233,24 @@ { "type": "string", "name": "name", "required": false, "title": "Name of Channel" }, { "type": "string", "name": "topic", "required": false, "title": "Subject of Channel" }, { "type": "string", "name": "type", "required": false, "title": "Channel type" }, - { "type": "bool", "name": "archive", "required": false, "title": "Request channel to be archived or unarchived" }, { "type": "uint64", "name": "organisationID", "required": false, "title": "Move channel to different organisation" } ] } }, + { + "name": "state", + "method": "PUT", + "path": "/{channelID}/state", + "title": "Update channel state", + "parameters": { + "path": [ + { "type": "uint64", "name": "channelID", "required": true, "title": "Channel ID" } + ], + "post": [ + { "type": "string", "name": "state", "required": true, "title": "Valid values: delete, undelete, archive, unarchive" } + ] + } + }, { "name": "read", "method": "GET", @@ -249,17 +262,6 @@ ] } }, - { - "name": "delete", - "method": "DELETE", - "path": "/{channelID}", - "title": "Remove channel", - "parameters": { - "path": [ - { "type": "uint64", "name": "channelID", "required": true, "title": "Channel ID" } - ] - } - }, { "name": "members", "method": "GET", diff --git a/sam/docs/src/spec/channel.json b/sam/docs/src/spec/channel.json index c32f9971f..59ebf3c03 100644 --- a/sam/docs/src/spec/channel.json +++ b/sam/docs/src/spec/channel.json @@ -63,7 +63,7 @@ } }, { - "Name": "edit", + "Name": "update", "Method": "PUT", "Title": "Update channel details", "Path": "/{channelID}", @@ -95,12 +95,6 @@ "title": "Channel type", "type": "string" }, - { - "name": "archive", - "required": false, - "title": "Request channel to be archived or unarchived", - "type": "bool" - }, { "name": "organisationID", "required": false, @@ -111,10 +105,10 @@ } }, { - "Name": "read", - "Method": "GET", - "Title": "Read channel details", - "Path": "/{channelID}", + "Name": "state", + "Method": "PUT", + "Title": "Update channel state", + "Path": "/{channelID}/state", "Parameters": { "path": [ { @@ -123,13 +117,21 @@ "title": "Channel ID", "type": "uint64" } + ], + "post": [ + { + "name": "state", + "required": true, + "title": "Valid values: delete, restore, archive, unarchive", + "type": "string" + } ] } }, { - "Name": "delete", - "Method": "DELETE", - "Title": "Remove channel", + "Name": "read", + "Method": "GET", + "Title": "Read channel details", "Path": "/{channelID}", "Parameters": { "path": [ diff --git a/sam/repository/channel.go b/sam/repository/channel.go index 7a836d0d1..d4afac834 100644 --- a/sam/repository/channel.go +++ b/sam/repository/channel.go @@ -24,6 +24,7 @@ type ( ArchiveChannelByID(id uint64) error UnarchiveChannelByID(id uint64) error DeleteChannelByID(id uint64) error + UndeleteChannelByID(id uint64) error } channel struct { @@ -32,13 +33,22 @@ type ( ) const ( - sqlChannelValidOnly = ` true - AND c.archived_at IS NULL - AND c.deleted_at IS NULL` + sqlChannelColumns = " id," + + "name, " + + "meta, " + + "created_at, " + + "updated_at, " + + "archived_at, " + + "deleted_at, " + + "rel_organisation, " + + "rel_creator, " + + "type , " + + "rel_last_message, " + + "topic" - sqlChannelSelect = `SELECT * + sqlChannelSelect = `SELECT ` + sqlChannelColumns + ` FROM channels AS c - WHERE ` + sqlChannelValidOnly + WHERE true ` sqlChannelGroupByMemberSet = sqlChannelSelect + ` AND c.type = ? AND c.id IN ( SELECT rel_channel @@ -49,7 +59,7 @@ const ( )` // subquery that filters out all channels that current user has access to as a member - // or via channel type (public chans) + // or via channel type (public channels) sqlChannelAccess = ` ( SELECT id FROM channels c @@ -115,7 +125,6 @@ func (r *channel) FindChannels(filter *types.ChannelFilter) ([]*types.Channel, e if filter.CurrentUserID > 0 { sql += " AND c.id IN " + sqlChannelAccess params = append(params, filter.CurrentUserID, types.ChannelTypePublic) - } } @@ -162,6 +171,6 @@ func (r *channel) DeleteChannelByID(id uint64) error { return r.updateColumnByID("channels", "deleted_at", time.Now(), id) } -func (r *channel) RecoverChannelByID(id uint64) error { +func (r *channel) UndeleteChannelByID(id uint64) error { return r.updateColumnByID("channels", "deleted_at", nil, id) } diff --git a/sam/repository/channel_member.go b/sam/repository/channel_member.go index 393384fc6..92a209e54 100644 --- a/sam/repository/channel_member.go +++ b/sam/repository/channel_member.go @@ -27,9 +27,6 @@ type ( ) const ( - // Copy definitions to make it more obvious that we're reusing channel-scope sql - sqlChannelMemberChannelValidOnly = sqlChannelValidOnly - // Copy definitions to make it more obvious that we're reusing channel-scope sql sqlChannelMemberChannelAccess = sqlChannelAccess @@ -37,12 +34,8 @@ const ( sqlChannelMemberSelect = `SELECT m.* FROM channel_members AS m INNER JOIN channels AS c ON (m.rel_channel = c.id) - WHERE ` + sqlChannelMemberChannelValidOnly - - // Selects all user's memberships - sqlChannelMemberships = `SELECT * - FROM channel_members AS cm - WHERE true` + WHERE c.archived_at IS NULL + AND c.deleted_at IS NULL` ) // ChannelMember creates new instance of channel member repository diff --git a/sam/repository/channel_test.go b/sam/repository/channel_test.go index 80f05d3ce..ff1b6ed9b 100644 --- a/sam/repository/channel_test.go +++ b/sam/repository/channel_test.go @@ -65,5 +65,10 @@ func TestChannel(t *testing.T) { err = rpo.DeleteChannelByID(chn.ID) assert(t, err == nil, "DeleteChannelByID error: %v", err) } + + { + err = rpo.UndeleteChannelByID(chn.ID) + assert(t, err == nil, "UndeleteChannelByID error: %v", err) + } } } diff --git a/sam/rest/channel.go b/sam/rest/channel.go index a6bb4e1bd..556bbdd6c 100644 --- a/sam/rest/channel.go +++ b/sam/rest/channel.go @@ -41,7 +41,7 @@ func (ctrl *Channel) Create(ctx context.Context, r *request.ChannelCreate) (inte return ctrl.wrap(ctrl.svc.ch.With(ctx).Create(channel)) } -func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interface{}, error) { +func (ctrl *Channel) Update(ctx context.Context, r *request.ChannelUpdate) (interface{}, error) { channel := &types.Channel{ ID: r.ChannelID, Name: r.Name, @@ -50,11 +50,21 @@ func (ctrl *Channel) Edit(ctx context.Context, r *request.ChannelEdit) (interfac } return ctrl.wrap(ctrl.svc.ch.With(ctx).Update(channel)) - } -func (ctrl *Channel) Delete(ctx context.Context, r *request.ChannelDelete) (interface{}, error) { - return nil, ctrl.svc.ch.With(ctx).Delete(r.ChannelID) +func (ctrl *Channel) State(ctx context.Context, r *request.ChannelState) (interface{}, error) { + switch r.State { + case "delete": + return ctrl.wrap(ctrl.svc.ch.With(ctx).Delete(r.ChannelID)) + case "undelete": + return ctrl.wrap(ctrl.svc.ch.With(ctx).Undelete(r.ChannelID)) + case "archive": + return ctrl.wrap(ctrl.svc.ch.With(ctx).Archive(r.ChannelID)) + case "unarchive": + return ctrl.wrap(ctrl.svc.ch.With(ctx).Unarchive(r.ChannelID)) + } + + return nil, nil } func (ctrl *Channel) Read(ctx context.Context, r *request.ChannelRead) (interface{}, error) { diff --git a/sam/rest/handlers/channel.go b/sam/rest/handlers/channel.go index c25a154b6..85a20e02e 100644 --- a/sam/rest/handlers/channel.go +++ b/sam/rest/handlers/channel.go @@ -29,9 +29,9 @@ import ( type ChannelAPI interface { List(context.Context, *request.ChannelList) (interface{}, error) Create(context.Context, *request.ChannelCreate) (interface{}, error) - Edit(context.Context, *request.ChannelEdit) (interface{}, error) + Update(context.Context, *request.ChannelUpdate) (interface{}, error) + State(context.Context, *request.ChannelState) (interface{}, error) Read(context.Context, *request.ChannelRead) (interface{}, error) - Delete(context.Context, *request.ChannelDelete) (interface{}, error) Members(context.Context, *request.ChannelMembers) (interface{}, error) Join(context.Context, *request.ChannelJoin) (interface{}, error) Part(context.Context, *request.ChannelPart) (interface{}, error) @@ -43,9 +43,9 @@ type ChannelAPI interface { type Channel struct { List func(http.ResponseWriter, *http.Request) Create func(http.ResponseWriter, *http.Request) - Edit func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + State func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) Members func(http.ResponseWriter, *http.Request) Join func(http.ResponseWriter, *http.Request) Part func(http.ResponseWriter, *http.Request) @@ -69,11 +69,18 @@ func NewChannel(ch ChannelAPI) *Channel { return ch.Create(r.Context(), params) }) }, - Edit: func(w http.ResponseWriter, r *http.Request) { + Update: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewChannelEdit() + params := request.NewChannelUpdate() resputil.JSON(w, params.Fill(r), func() (interface{}, error) { - return ch.Edit(r.Context(), params) + return ch.Update(r.Context(), params) + }) + }, + State: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewChannelState() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ch.State(r.Context(), params) }) }, Read: func(w http.ResponseWriter, r *http.Request) { @@ -83,13 +90,6 @@ func NewChannel(ch ChannelAPI) *Channel { return ch.Read(r.Context(), params) }) }, - Delete: func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - params := request.NewChannelDelete() - resputil.JSON(w, params.Fill(r), func() (interface{}, error) { - return ch.Delete(r.Context(), params) - }) - }, Members: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewChannelMembers() @@ -134,9 +134,9 @@ func (ch *Channel) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h r.Route("/channels", func(r chi.Router) { r.Get("/", ch.List) r.Post("/", ch.Create) - r.Put("/{channelID}", ch.Edit) + r.Put("/{channelID}", ch.Update) + r.Put("/{channelID}/state", ch.State) r.Get("/{channelID}", ch.Read) - r.Delete("/{channelID}", ch.Delete) r.Get("/{channelID}/members", ch.Members) r.Put("/{channelID}/members/{userID}", ch.Join) r.Delete("/{channelID}/members/{userID}", ch.Part) diff --git a/sam/rest/request/channel.go b/sam/rest/request/channel.go index 5da7ac79c..b068043da 100644 --- a/sam/rest/request/channel.go +++ b/sam/rest/request/channel.go @@ -133,21 +133,20 @@ func (c *ChannelCreate) Fill(r *http.Request) (err error) { var _ RequestFiller = NewChannelCreate() -// Channel edit request parameters -type ChannelEdit struct { +// Channel update request parameters +type ChannelUpdate struct { ChannelID uint64 `json:",string"` Name string Topic string Type string - Archive bool OrganisationID uint64 `json:",string"` } -func NewChannelEdit() *ChannelEdit { - return &ChannelEdit{} +func NewChannelUpdate() *ChannelUpdate { + return &ChannelUpdate{} } -func (c *ChannelEdit) Fill(r *http.Request) (err error) { +func (c *ChannelUpdate) Fill(r *http.Request) (err error) { if strings.ToLower(r.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(r.Body).Decode(c) @@ -187,10 +186,6 @@ func (c *ChannelEdit) Fill(r *http.Request) (err error) { c.Type = val } - if val, ok := post["archive"]; ok { - - c.Archive = parseBool(val) - } if val, ok := post["organisationID"]; ok { c.OrganisationID = parseUInt64(val) @@ -199,7 +194,55 @@ func (c *ChannelEdit) Fill(r *http.Request) (err error) { return err } -var _ RequestFiller = NewChannelEdit() +var _ RequestFiller = NewChannelUpdate() + +// Channel state request parameters +type ChannelState struct { + ChannelID uint64 `json:",string"` + State string +} + +func NewChannelState() *ChannelState { + return &ChannelState{} +} + +func (c *ChannelState) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(c) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + c.ChannelID = parseUInt64(chi.URLParam(r, "channelID")) + if val, ok := post["state"]; ok { + + c.State = val + } + + return err +} + +var _ RequestFiller = NewChannelState() // Channel read request parameters type ChannelRead struct { @@ -244,49 +287,6 @@ func (c *ChannelRead) Fill(r *http.Request) (err error) { var _ RequestFiller = NewChannelRead() -// Channel delete request parameters -type ChannelDelete struct { - ChannelID uint64 `json:",string"` -} - -func NewChannelDelete() *ChannelDelete { - return &ChannelDelete{} -} - -func (c *ChannelDelete) Fill(r *http.Request) (err error) { - if strings.ToLower(r.Header.Get("content-type")) == "application/json" { - err = json.NewDecoder(r.Body).Decode(c) - - switch { - case err == io.EOF: - err = nil - case err != nil: - return errors.Wrap(err, "error parsing http request body") - } - } - - if err = r.ParseForm(); err != nil { - return err - } - - get := map[string]string{} - post := map[string]string{} - urlQuery := r.URL.Query() - for name, param := range urlQuery { - get[name] = string(param[0]) - } - postVars := r.Form - for name, param := range postVars { - post[name] = string(param[0]) - } - - c.ChannelID = parseUInt64(chi.URLParam(r, "channelID")) - - return err -} - -var _ RequestFiller = NewChannelDelete() - // Channel members request parameters type ChannelMembers struct { ChannelID uint64 `json:",string"` diff --git a/sam/service/channel.go b/sam/service/channel.go index a445c0215..c1f68ab02 100644 --- a/sam/service/channel.go +++ b/sam/service/channel.go @@ -46,9 +46,10 @@ type ( AddMember(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error) DeleteMember(channelID uint64, memberIDs ...uint64) (err error) - Archive(ID uint64) error - Unarchive(ID uint64) error - Delete(ID uint64) error + Archive(ID uint64) (*types.Channel, error) + Unarchive(ID uint64) (*types.Channel, error) + Delete(ID uint64) (*types.Channel, error) + Undelete(ID uint64) (*types.Channel, error) RecordView(userID, channelID, lastMessageID uint64) error } @@ -88,12 +89,14 @@ func (svc *channel) With(ctx context.Context) ChannelService { } } -func (svc *channel) FindByID(id uint64) (ch *types.Channel, err error) { - ch, err = svc.channel.FindChannelByID(id) +func (svc *channel) FindByID(ID uint64) (ch *types.Channel, err error) { + ch, err = svc.channel.FindChannelByID(ID) if err != nil { return } + // @todo [SECURITY] check if user can access this channel + // @todo [SECURITY] check if user can access inactive channels // if !svc.sec.ch.CanRead(ch) { // return nil, errors.New("Not allowed to access channel") // } @@ -111,6 +114,11 @@ func (svc *channel) Find(filter *types.ChannelFilter) (cc types.ChannelSet, err return } + // @todo [SECURITY] check if user can access inactive channels + cc, _ = cc.Filter(func(i *types.Channel) (b bool, e error) { + return true || i.DeletedAt == nil, nil + }) + return }) } @@ -425,19 +433,18 @@ func (svc *channel) Update(in *types.Channel) (ch *types.Channel, err error) { return } - svc.flushSystemMessages() + _ = svc.flushSystemMessages() return svc.sendChannelEvent(ch) }) } -func (svc *channel) Delete(id uint64) error { - return svc.db.Transaction(func() (err error) { +func (svc *channel) Delete(ID uint64) (ch *types.Channel, err error) { + return ch, svc.db.Transaction(func() (err error) { var userID = repository.Identity(svc.ctx) - var ch *types.Channel // @todo [SECURITY] can user access this channel? - if ch, err = svc.channel.FindChannelByID(id); err != nil { + if ch, err = svc.channel.FindChannelByID(ID); err != nil { return } @@ -452,8 +459,11 @@ func (svc *channel) Delete(id uint64) error { svc.scheduleSystemMessage(ch, "<@%d> deleted this channel", userID) - if err = svc.channel.DeleteChannelByID(id); err != nil { + if err = svc.channel.DeleteChannelByID(ID); err != nil { return + } else { + // Set deletedAt timestamp so that our clients can react properly... + ch.DeletedAt = timeNowPtr() } _ = svc.sendChannelEvent(ch) @@ -462,13 +472,12 @@ func (svc *channel) Delete(id uint64) error { }) } -func (svc *channel) Recover(id uint64) error { - return svc.db.Transaction(func() (err error) { +func (svc *channel) Undelete(ID uint64) (ch *types.Channel, err error) { + return ch, svc.db.Transaction(func() (err error) { var userID = repository.Identity(svc.ctx) - var ch *types.Channel // @todo [SECURITY] can user access this channel? - if ch, err = svc.channel.FindChannelByID(id); err != nil { + if ch, err = svc.channel.FindChannelByID(ID); err != nil { return } @@ -478,10 +487,13 @@ func (svc *channel) Recover(id uint64) error { return errors.New("Channel not deleted") } - svc.scheduleSystemMessage(ch, "<@%d> recovered this channel", userID) + svc.scheduleSystemMessage(ch, "<@%d> undeleted this channel", userID) - if err = svc.channel.UnarchiveChannelByID(id); err != nil { + if err = svc.channel.UndeleteChannelByID(ID); err != nil { return + } else { + // Remove deletedAt timestamp so that our clients can react properly... + ch.DeletedAt = nil } svc.flushSystemMessages() @@ -489,14 +501,12 @@ func (svc *channel) Recover(id uint64) error { }) } -func (svc *channel) Archive(id uint64) error { - return svc.db.Transaction(func() (err error) { - +func (svc *channel) Archive(ID uint64) (ch *types.Channel, err error) { + return ch, svc.db.Transaction(func() (err error) { var userID = repository.Identity(svc.ctx) - var ch *types.Channel // @todo [SECURITY] can user access this channel? - if ch, err = svc.channel.FindChannelByID(id); err != nil { + if ch, err = svc.channel.FindChannelByID(ID); err != nil { return } @@ -508,8 +518,11 @@ func (svc *channel) Archive(id uint64) error { svc.scheduleSystemMessage(ch, "<@%d> archived this channel", userID) - if err = svc.channel.ArchiveChannelByID(id); err != nil { + if err = svc.channel.ArchiveChannelByID(ID); err != nil { return + } else { + // Set archivedAt timestamp so that our clients can react properly... + ch.ArchivedAt = timeNowPtr() } svc.flushSystemMessages() @@ -517,13 +530,12 @@ func (svc *channel) Archive(id uint64) error { }) } -func (svc *channel) Unarchive(id uint64) error { - return svc.db.Transaction(func() (err error) { +func (svc *channel) Unarchive(ID uint64) (ch *types.Channel, err error) { + return ch, svc.db.Transaction(func() (err error) { var userID = repository.Identity(svc.ctx) - var ch *types.Channel // @todo [SECURITY] can user access this channel? - if ch, err = svc.channel.FindChannelByID(id); err != nil { + if ch, err = svc.channel.FindChannelByID(ID); err != nil { return } @@ -531,6 +543,9 @@ func (svc *channel) Unarchive(id uint64) error { if ch.ArchivedAt == nil { return errors.New("Channel not archived") + } else { + // Unset archivedAt timestamp so that our clients can react properly... + ch.ArchivedAt = nil } svc.scheduleSystemMessage(ch, "<@%d> unarchived this channel", userID) diff --git a/sam/service/message.go b/sam/service/message.go index 68c30479a..1ee0941a8 100644 --- a/sam/service/message.go +++ b/sam/service/message.go @@ -3,7 +3,6 @@ package service import ( "context" "strings" - "time" "github.com/pkg/errors" @@ -280,12 +279,11 @@ func (svc *message) Delete(ID uint64) error { return } - // Set deletedAt timestamp so that our clients can react properly... - now := time.Now() - deletedMsg.DeletedAt = &now - if err = svc.cview.Dec(deletedMsg.ChannelID, deletedMsg.UserID); err != nil { return err + } else { + // Set deletedAt timestamp so that our clients can react properly... + deletedMsg.DeletedAt = timeNowPtr() } return svc.sendEvent(append(bq, deletedMsg)...) diff --git a/sam/service/service.go b/sam/service/service.go index f25f8f2be..cfb4c695f 100644 --- a/sam/service/service.go +++ b/sam/service/service.go @@ -3,6 +3,7 @@ package service import ( "log" "sync" + "time" "github.com/crusttech/crust/internal/store" ) @@ -40,3 +41,8 @@ func Init() { DefaultTeam = Team() }) } + +func timeNowPtr() *time.Time { + now := time.Now() + return &now +} diff --git a/sam/types/channel.go b/sam/types/channel.go index 7e9d95e31..a7e0030bd 100644 --- a/sam/types/channel.go +++ b/sam/types/channel.go @@ -17,8 +17,9 @@ type ( CreatorID uint64 `json:"creatorId" db:"rel_creator"` OrganisationID uint64 `json:"organisationId" db:"rel_organisation"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` + ArchivedAt *time.Time `json:"archivedAt,omitempty" db:"archived_at"` DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` @@ -35,7 +36,8 @@ type ( // Only return channels accessible by this user CurrentUserID uint64 - IncludeMembers bool + // Do not filter out deleted channels + IncludeDeleted bool } ChannelType string @@ -44,18 +46,22 @@ type ( ) // Scope returns permissions group that for this type -func (r *Channel) Scope() string { +func (c *Channel) Scope() string { return "channel" } // Resource returns a RBAC resource ID for this type -func (r *Channel) Resource() string { - return fmt.Sprintf("%s:%d", r.Scope(), r.ID) +func (c *Channel) Resource() string { + return fmt.Sprintf("%s:%d", c.Scope(), c.ID) } // Operation returns a RBAC resource-scoped role name for an operation -func (r *Channel) Operation(name string) string { - return fmt.Sprintf("%s/%s", r.Resource(), name) +func (c *Channel) Operation(name string) string { + return fmt.Sprintf("%s/%s", c.Resource(), name) +} + +func (c *Channel) IsValid() bool { + return c.ArchivedAt == nil && c.DeletedAt == nil } func (cc ChannelSet) Walk(w func(*Channel) error) (err error) { @@ -68,6 +74,20 @@ func (cc ChannelSet) Walk(w func(*Channel) error) (err error) { return } +func (cc ChannelSet) Filter(f func(*Channel) (bool, error)) (out ChannelSet, err error) { + var ok bool + out = ChannelSet{} + for i := range cc { + if ok, err = f(cc[i]); err != nil { + return + } else if ok { + out = append(out, cc[i]) + } + } + + return +} + func (cc ChannelSet) FindById(ID uint64) *Channel { for i := range cc { if cc[i].ID == ID { diff --git a/sam/websocket/session.go b/sam/websocket/session.go index a61348b69..7899b1f00 100644 --- a/sam/websocket/session.go +++ b/sam/websocket/session.go @@ -92,7 +92,7 @@ func (sess *Session) connected() (err error) { } // Push user info about all channels he has access to... - if cc, err = sess.svc.ch.With(sess.ctx).Find(&types.ChannelFilter{IncludeMembers: true}); err != nil { + if cc, err = sess.svc.ch.With(sess.ctx).Find(&types.ChannelFilter{}); err != nil { log.Printf("Error: %v", err) } else { if err = sess.sendReply(payload.Channels(cc)); err != nil { diff --git a/sam/websocket/session_incoming.go b/sam/websocket/session_incoming.go index 2981cce3d..db907cdea 100644 --- a/sam/websocket/session_incoming.go +++ b/sam/websocket/session_incoming.go @@ -35,8 +35,6 @@ func (s *Session) dispatch(raw []byte) error { return s.channelList(ctx, p.Channels) case p.ChannelCreate != nil: return s.channelCreate(ctx, p.ChannelCreate) - case p.ChannelDelete != nil: - return s.channelDelete(ctx, p.ChannelDelete) case p.ChannelUpdate != nil: return s.channelUpdate(ctx, p.ChannelUpdate) case p.ChannelViewRecord != nil: diff --git a/sam/websocket/session_incoming_channel.go b/sam/websocket/session_incoming_channel.go index ad33ad43c..63b5ae9dc 100644 --- a/sam/websocket/session_incoming_channel.go +++ b/sam/websocket/session_incoming_channel.go @@ -45,7 +45,7 @@ func (s *Session) channelPart(ctx context.Context, p *incoming.ChannelPart) erro } func (s *Session) channelList(ctx context.Context, p *incoming.Channels) error { - channels, err := s.svc.ch.With(ctx).Find(&types.ChannelFilter{IncludeMembers: true}) + channels, err := s.svc.ch.With(ctx).Find(&types.ChannelFilter{}) if err != nil { return err } @@ -80,10 +80,6 @@ func (s *Session) channelCreate(ctx context.Context, p *incoming.ChannelCreate) return nil } -func (s *Session) channelDelete(ctx context.Context, p *incoming.ChannelDelete) (err error) { - return s.svc.ch.With(ctx).Delete(payload.ParseUInt64(p.ChannelID)) -} - func (s *Session) channelUpdate(ctx context.Context, p *incoming.ChannelUpdate) error { ch, err := s.svc.ch.With(ctx).FindByID(payload.ParseUInt64(p.ID)) if err != nil {