From 3dc0e63c03a74eb5792a8ca1976f18e7e390a167 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Thu, 29 Aug 2024 12:46:37 +0200 Subject: [PATCH] Adds Shared Channel related API endpoints (#27436) * Adds Shared Channel management API endpoints New endpoints for the following routes are added: - Get Shared Channel Remotes by Remote Cluster at `GET /api/v4/remotecluster/{remote_id}/sharedchannelremotes` - Invite Remote Cluster to Channel at `POST /api/v4/remotecluster/{remote_id}/channels/invite` - Uninvite Remote Cluster to Channel at `POST /api/v4/remotecluster/{remote_id}/channels/uninvite` These endpoints are planned to be used from the system console, and gated through the `manage_secure_connections` permission. * Adds i18n messages for API errors * Fix pagination flaky test * Fix linter * Adds the posibility of filtering shared channel remotes by home --------- Co-authored-by: Mattermost Build --- api/v4/source/definitions.yaml | 38 +++ api/v4/source/sharedchannels.yaml | 123 ++++++++ server/channels/api4/api.go | 8 +- server/channels/api4/shared_channel.go | 149 +++++++++ server/channels/api4/shared_channel_test.go | 284 +++++++++++++++++- server/channels/app/app_iface.go | 2 +- .../app/opentracing/opentracing_layer.go | 4 +- server/channels/app/shared_channel.go | 4 +- .../opentracinglayer/opentracinglayer.go | 4 +- .../channels/store/retrylayer/retrylayer.go | 4 +- .../store/sqlstore/shared_channel_store.go | 42 ++- server/channels/store/store.go | 2 +- .../storetest/mocks/SharedChannelStore.go | 18 +- .../store/storetest/shared_channel_store.go | 104 +++++-- .../channels/store/timerlayer/timerlayer.go | 4 +- server/channels/web/params.go | 4 + server/i18n/en.json | 16 + .../services/sharedchannel/sync_send.go | 2 +- server/public/model/client4.go | 59 ++++ server/public/model/shared_channel.go | 2 + 20 files changed, 819 insertions(+), 54 deletions(-) diff --git a/api/v4/source/definitions.yaml b/api/v4/source/definitions.yaml index 44fc458ee6..ed31359058 100644 --- a/api/v4/source/definitions.yaml +++ b/api/v4/source/definitions.yaml @@ -3535,6 +3535,44 @@ components: description: The time in milliseconds a remote cluster was last pinged successfully type: integer format: int64 + SharedChannelRemote: + type: object + properties: + id: + description: The id of the shared channel remote + type: string + channel_id: + description: The id of the channel + type: string + creator_id: + description: Id of the user that invited the remote to share the channel + type: string + create_at: + description: Time in milliseconds that the remote was invited to the channel + type: integer + update_at: + description: Time in milliseconds that the shared channel remote record was last updated + type: integer + is_invite_accepted: + description: Indicates if the invite has been accepted by the remote + type: boolean + is_invite_confirmed: + description: Indicates if the invite has been confirmed by the remote + type: boolean + remote_id: + description: Id of the remote cluster that the channel is shared with + type: string + last_post_update_at: + description: Time in milliseconds of the last post in the channel that was synchronized with the remote update_at + type: integer + last_post_id: + description: Id of the last post in the channel that was synchronized with the remote + type: string + last_post_create_at: + description: Time in milliseconds of the last post in the channel that was synchronized with the remote create_at + type: string + last_post_create_id: + type: string SystemStatusResponse: type: object properties: diff --git a/api/v4/source/sharedchannels.yaml b/api/v4/source/sharedchannels.yaml index f059268367..3c2637d743 100644 --- a/api/v4/source/sharedchannels.yaml +++ b/api/v4/source/sharedchannels.yaml @@ -45,6 +45,61 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" + + "/api/v4/remotecluster/{remote_id}/sharedchannelremotes": + get: + tags: + - shared channels + summary: Get shared channel remotes by remote cluster. + description: | + Get a list of the channels shared with a given remote cluster + and their status. + + ##### Permissions + `manage_secure_connections` + operationId: GetSharedChannelRemotesByRemoteCluster + parameters: + - name: remote_id + in: path + description: The remote cluster GUID + required: true + schema: + type: string + - name: exclude_home + in: query + description: Show only those Shared channel remotes that were shared with this server + schema: + type: boolean + - name: exclude_remote + in: query + description: Show only those Shared channel remotes that were shared from this server + schema: + type: boolean + - name: page + in: query + description: The page to select + schema: + type: integer + - name: per_page + in: query + description: The number of shared channels per page + schema: + type: integer + responses: + "200": + description: Shared channel remotes fetch successful. Result might be empty. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SharedChannelRemote" + + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "/api/v4/sharedchannels/remote_info/{remote_id}": get: tags: @@ -80,3 +135,71 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" + + "/api/v4/remotecluster/{remote_id}/channels/{channel_id}/invite": + post: + tags: + - shared channels + summary: Invites a remote cluster to a channel. + description: | + Invites a remote cluster to a channel, sharing the channel if + needed. If the remote cluster was already invited to the + channel, calling this endpoint will have no effect. + + ##### Permissions + `manage_shared_channels` + operationId: InviteRemoteClusterToChannel + parameters: + - name: remote_id + in: path + description: The remote cluster GUID + required: true + schema: + type: string + - name: channel_id + in: path + description: The channel GUID to invite the remote cluster to + required: true + schema: + type: string + responses: + "204": + description: Remote cluster invited successfully + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + "/api/v4/remotecluster/{remote_id}/channels/{channel_id}/uninvite": + post: + tags: + - shared channels + summary: Uninvites a remote cluster to a channel. + description: | + Stops sharing a channel with a remote cluster. If the channel + was not shared with the remote, calling this endpoint will + have no effect. + + ##### Permissions + `manage_shared_channels` + operationId: UninviteRemoteClusterToChannel + parameters: + - name: remote_id + in: path + description: The remote cluster GUID + required: true + schema: + type: string + - name: channel_id + in: path + description: The channel GUID to uninvite the remote cluster to + required: true + schema: + type: string + responses: + "204": + description: Remote cluster uninvited successfully + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" diff --git a/server/channels/api4/api.go b/server/channels/api4/api.go index da735eaced..bb28fd6ce3 100644 --- a/server/channels/api4/api.go +++ b/server/channels/api4/api.go @@ -129,8 +129,10 @@ type Routes struct { Exports *mux.Router // 'api/v4/exports' Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}' - RemoteCluster *mux.Router // 'api/v4/remotecluster' - SharedChannels *mux.Router // 'api/v4/sharedchannels' + RemoteCluster *mux.Router // 'api/v4/remotecluster' + SharedChannels *mux.Router // 'api/v4/sharedchannels' + ChannelForRemote *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}' + SharedChannelRemotes *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes' Permissions *mux.Router // 'api/v4/permissions' @@ -265,6 +267,8 @@ func Init(srv *app.Server) (*API, error) { api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter() api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter() + api.BaseRoutes.SharedChannelRemotes = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes").Subrouter() + api.BaseRoutes.ChannelForRemote = api.BaseRoutes.RemoteCluster.PathPrefix("/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}").Subrouter() api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter() diff --git a/server/channels/api4/shared_channel.go b/server/channels/api4/shared_channel.go index a1db22d1fa..20d2bd9ba7 100644 --- a/server/channels/api4/shared_channel.go +++ b/server/channels/api4/shared_channel.go @@ -8,11 +8,17 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/v8/channels/audit" ) func (api *API) InitSharedChannels() { api.BaseRoutes.SharedChannels.Handle("/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getSharedChannels)).Methods(http.MethodGet) api.BaseRoutes.SharedChannels.Handle("/remote_info/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteClusterInfo)).Methods(http.MethodGet) + + api.BaseRoutes.SharedChannelRemotes.Handle("", api.APISessionRequired(getSharedChannelRemotesByRemoteCluster)).Methods(http.MethodGet) + api.BaseRoutes.ChannelForRemote.Handle("/invite", api.APISessionRequired(inviteRemoteClusterToChannel)).Methods(http.MethodPost) + api.BaseRoutes.ChannelForRemote.Handle("/uninvite", api.APISessionRequired(uninviteRemoteClusterToChannel)).Methods(http.MethodPost) } func getSharedChannels(c *Context, w http.ResponseWriter, r *http.Request) { @@ -86,3 +92,146 @@ func getRemoteClusterInfo(c *Context, w http.ResponseWriter, r *http.Request) { } w.Write(b) } + +func getSharedChannelRemotesByRemoteCluster(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireRemoteId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) { + c.SetPermissionError(model.PermissionManageSecureConnections) + return + } + + // make sure remote cluster service is enabled. + if _, appErr := c.App.GetRemoteClusterService(); appErr != nil { + c.Err = appErr + return + } + + if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil { + c.Err = appErr + return + } + + filter := model.SharedChannelRemoteFilterOpts{ + RemoteId: c.Params.RemoteId, + ExcludeHome: c.Params.ExcludeHome, + ExcludeRemote: c.Params.ExcludeRemote, + } + sharedChannelRemotes, err := c.App.GetSharedChannelRemotes(c.Params.Page, c.Params.PerPage, filter) + if err != nil { + c.Err = model.NewAppError("getSharedChannelRemotesByRemoteCluster", "api.shared_channel.get_shared_channel_remotes_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + if err := json.NewEncoder(w).Encode(sharedChannelRemotes); err != nil { + c.Logger.Warn("Error while writing response", mlog.Err(err)) + } +} + +func inviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireRemoteId() + if c.Err != nil { + return + } + + c.RequireChannelId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) { + c.SetPermissionError(model.PermissionManageSharedChannels) + return + } + + // make sure remote cluster service is enabled. + if _, appErr := c.App.GetRemoteClusterService(); appErr != nil { + c.Err = appErr + return + } + + if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil { + c.SetInvalidRemoteIdError(c.Params.RemoteId) + return + } + + if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil { + c.SetInvalidURLParam("channel_id") + return + } + + auditRec := c.MakeAuditRecord("inviteRemoteClusterToChannel", audit.Fail) + defer c.LogAuditRec(auditRec) + audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId) + audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId) + audit.AddEventParameter(auditRec, "user_id", c.AppContext.Session().UserId) + + if err := c.App.InviteRemoteToChannel(c.Params.ChannelId, c.Params.RemoteId, c.AppContext.Session().UserId, true); err != nil { + c.Err = model.NewAppError("inviteRemoteClusterToChannel", "api.shared_channel.invite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + auditRec.Success() + w.WriteHeader(http.StatusNoContent) +} + +func uninviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireRemoteId() + if c.Err != nil { + return + } + + c.RequireChannelId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSecureConnections) { + c.SetPermissionError(model.PermissionManageSharedChannels) + return + } + + // make sure remote cluster service is enabled. + if _, appErr := c.App.GetRemoteClusterService(); appErr != nil { + c.Err = appErr + return + } + + if _, appErr := c.App.GetRemoteCluster(c.Params.RemoteId); appErr != nil { + c.SetInvalidRemoteIdError(c.Params.RemoteId) + return + } + + if _, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId); appErr != nil { + c.SetInvalidURLParam("channel_id") + return + } + + auditRec := c.MakeAuditRecord("uninviteRemoteClusterToChannel", audit.Fail) + defer c.LogAuditRec(auditRec) + audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId) + audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId) + + hasRemote, err := c.App.HasRemote(c.Params.ChannelId, c.Params.RemoteId) + if err != nil { + c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.has_remote_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + // if the channel is not shared with the remote, we return early + if !hasRemote { + w.WriteHeader(http.StatusNoContent) + return + } + + if err := c.App.UninviteRemoteFromChannel(c.Params.ChannelId, c.Params.RemoteId); err != nil { + c.Err = model.NewAppError("uninviteRemoteClusterToChannel", "api.shared_channel.uninvite_remote_to_channel_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + auditRec.Success() + w.WriteHeader(http.StatusNoContent) +} diff --git a/server/channels/api4/shared_channel_test.go b/server/channels/api4/shared_channel_test.go index 17a40f8159..90a00d0ec9 100644 --- a/server/channels/api4/shared_channel_test.go +++ b/server/channels/api4/shared_channel_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "math/rand" + "net/http" "sort" "testing" "time" @@ -22,10 +23,16 @@ var ( ) func setupForSharedChannels(tb testing.TB) *TestHelper { - return SetupConfig(tb, func(cfg *model.Config) { + th := SetupConfig(tb, func(cfg *model.Config) { *cfg.ExperimentalSettings.EnableRemoteClusterService = true *cfg.ExperimentalSettings.EnableSharedChannels = true }) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.SiteURL = fmt.Sprintf("http://localhost:%d", th.Server.ListenAddr.Port) + }) + + return th } func TestGetAllSharedChannels(t *testing.T) { @@ -232,3 +239,278 @@ func TestCreateDirectChannelWithRemoteUser(t *testing.T) { require.True(t, dm.IsShared()) }) } + +func TestGetSharedChannelRemotesByRemoteCluster(t *testing.T) { + t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + resp, err := th.SystemAdminClient.DeleteRemoteCluster(context.Background(), model.NewId()) + CheckNotImplementedStatus(t, resp) + require.Error(t, err) + }) + + th := setupForSharedChannels(t).InitBasic() + defer th.TearDown() + + newRC1 := &model.RemoteCluster{Name: "rc1", SiteURL: "http://example1.com", CreatorId: th.SystemAdminUser.Id} + newRC2 := &model.RemoteCluster{Name: "rc2", SiteURL: "http://example2.com", CreatorId: th.SystemAdminUser.Id} + + rc1, appErr := th.App.AddRemoteCluster(newRC1) + require.Nil(t, appErr) + rc2, appErr := th.App.AddRemoteCluster(newRC2) + require.Nil(t, appErr) + + c1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id) + sc1 := &model.SharedChannel{ + ChannelId: c1.Id, + TeamId: th.BasicTeam.Id, + ShareName: "shared_1", + ShareDisplayName: "Shared Channel 1", // for sorting purposes + CreatorId: th.BasicUser.Id, + RemoteId: rc1.RemoteId, + Home: true, + } + _, err := th.App.ShareChannel(th.Context, sc1) + require.NoError(t, err) + + c2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id) + sc2 := &model.SharedChannel{ + ChannelId: c2.Id, + TeamId: th.BasicTeam.Id, + ShareName: "shared_2", + ShareDisplayName: "Shared Channel 2", + CreatorId: th.BasicUser.Id, + RemoteId: rc1.RemoteId, + Home: false, + } + + _, err = th.App.ShareChannel(th.Context, sc2) + require.NoError(t, err) + + c3 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, th.BasicTeam.Id) + sc3 := &model.SharedChannel{ + ChannelId: c3.Id, + TeamId: th.BasicTeam.Id, + ShareName: "shared_3", + CreatorId: th.BasicUser.Id, + RemoteId: rc2.RemoteId, + } + _, err = th.App.ShareChannel(th.Context, sc3) + require.NoError(t, err) + + // for the pagination test, we need to get the channelId of the + // second SharedChannelRemote that belongs to RC1, sorted by ID, + // so we accumulate those SharedChannelRemotes on creation and + // later sort them to be able to get the right one for the test + // result + sharedChannelRemotesFromRC1 := []*model.SharedChannelRemote{} + + // create the shared channel remotes + for _, sc := range []*model.SharedChannel{sc1, sc2, sc3} { + scr := &model.SharedChannelRemote{ + Id: model.NewId(), + ChannelId: sc.ChannelId, + CreatorId: sc.CreatorId, + IsInviteAccepted: true, + IsInviteConfirmed: true, + RemoteId: sc.RemoteId, + } + _, err = th.App.SaveSharedChannelRemote(scr) + require.NoError(t, err) + + if scr.RemoteId == rc1.RemoteId { + sharedChannelRemotesFromRC1 = append(sharedChannelRemotesFromRC1, scr) + } + } + + sort.Slice(sharedChannelRemotesFromRC1, func(i, j int) bool { + return sharedChannelRemotesFromRC1[i].Id < sharedChannelRemotesFromRC1[j].Id + }) + + t.Run("should return the expected shared channels", func(t *testing.T) { + testCases := []struct { + Name string + Client *model.Client4 + RemoteId string + ExcludeHome bool + ExcludeRemote bool + Page int + PerPage int + ExpectedStatusCode int + ExpectedError bool + ExpectedIds []string + }{ + { + Name: "should not work if the user doesn't have the right permissions", + Client: th.Client, + RemoteId: rc1.RemoteId, + Page: 0, + PerPage: 100, + ExpectedStatusCode: http.StatusForbidden, + ExpectedError: true, + }, + { + Name: "should not work if the remote cluster is nonexistent", + Client: th.SystemAdminClient, + RemoteId: model.NewId(), + Page: 0, + PerPage: 100, + ExpectedStatusCode: http.StatusNotFound, + ExpectedError: true, + }, + { + Name: "should return the complete list of shared channel remotes for a remote cluster", + Client: th.SystemAdminClient, + RemoteId: rc1.RemoteId, + Page: 0, + PerPage: 100, + ExpectedStatusCode: http.StatusOK, + ExpectedError: false, + ExpectedIds: []string{sc1.ChannelId, sc2.ChannelId}, + }, + { + Name: "should return only the shared channel remotes homed localy", + Client: th.SystemAdminClient, + RemoteId: rc1.RemoteId, + ExcludeRemote: true, + Page: 0, + PerPage: 100, + ExpectedStatusCode: http.StatusOK, + ExpectedError: false, + ExpectedIds: []string{sc1.ChannelId}, + }, + { + Name: "should return only the shared channel remotes homed remotely", + Client: th.SystemAdminClient, + RemoteId: rc1.RemoteId, + ExcludeHome: true, + Page: 0, + PerPage: 100, + ExpectedStatusCode: http.StatusOK, + ExpectedError: false, + ExpectedIds: []string{sc2.ChannelId}, + }, + { + Name: "should correctly paginate the results", + Client: th.SystemAdminClient, + RemoteId: rc1.RemoteId, + Page: 1, + PerPage: 1, + ExpectedStatusCode: http.StatusOK, + ExpectedError: false, + ExpectedIds: []string{sharedChannelRemotesFromRC1[1].ChannelId}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + scrs, resp, err := tc.Client.GetSharedChannelRemotesByRemoteCluster(context.Background(), tc.RemoteId, tc.ExcludeHome, tc.ExcludeRemote, tc.Page, tc.PerPage) + checkHTTPStatus(t, resp, tc.ExpectedStatusCode) + if tc.ExpectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Len(t, scrs, len(tc.ExpectedIds)) + + foundIds := []string{} + for _, scr := range scrs { + require.Equal(t, tc.RemoteId, scr.RemoteId) + foundIds = append(foundIds, scr.ChannelId) + } + require.ElementsMatch(t, tc.ExpectedIds, foundIds) + }) + } + }) +} + +func TestInviteRemoteClusterToChannel(t *testing.T) { + t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckNotImplementedStatus(t, resp) + require.Error(t, err) + }) + + th := setupForSharedChannels(t).InitBasic() + defer th.TearDown() + + newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id} + + rc, appErr := th.App.AddRemoteCluster(newRC) + require.Nil(t, appErr) + + t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) { + resp, err := th.Client.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckForbiddenStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should not work if the remote cluster is nonexistent", func(t *testing.T) { + resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckBadRequestStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should not work if the channel is nonexistent", func(t *testing.T) { + resp, err := th.SystemAdminClient.InviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId()) + CheckBadRequestStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should correctly invite the remote cluster to the channel", func(t *testing.T) { + t.Skip("Requires server2server communication: ToBeImplemented") + }) + + t.Run("should do nothing but return 204 if the remote cluster is already invited to the channel", func(t *testing.T) { + t.Skip("Requires server2server communication: ToBeImplemented") + }) +} + +func TestUninviteRemoteClusterToChannel(t *testing.T) { + t.Run("Should not work if the remote cluster service is not enabled", func(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckNotImplementedStatus(t, resp) + require.Error(t, err) + }) + + th := setupForSharedChannels(t).InitBasic() + defer th.TearDown() + + newRC := &model.RemoteCluster{Name: "rc", SiteURL: "http://example.com", CreatorId: th.SystemAdminUser.Id} + + rc, appErr := th.App.AddRemoteCluster(newRC) + require.Nil(t, appErr) + + t.Run("Should not work if the user doesn't have the right permissions", func(t *testing.T) { + resp, err := th.Client.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckForbiddenStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should not work if the remote cluster is nonexistent", func(t *testing.T) { + resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), model.NewId(), model.NewId()) + CheckBadRequestStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should not work if the channel is nonexistent", func(t *testing.T) { + resp, err := th.SystemAdminClient.UninviteRemoteClusterToChannel(context.Background(), rc.RemoteId, model.NewId()) + CheckBadRequestStatus(t, resp) + require.Error(t, err) + }) + + t.Run("should correctly uninvite the remote cluster to the channel", func(t *testing.T) { + t.Skip("Requires server2server communication: ToBeImplemented") + }) + + t.Run("should do nothing but return 204 if the remote cluster is not sharing the channel", func(t *testing.T) { + t.Skip("Requires server2server communication: ToBeImplemented") + }) +} diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 4232865199..d92fa65b7e 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -817,7 +817,7 @@ type AppIface interface { GetSharedChannel(channelID string) (*model.SharedChannel, error) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) - GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) + GetSharedChannelRemotes(page, perPage int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index 5e3dcdc552..b7ae597b11 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -9744,7 +9744,7 @@ func (a *OpenTracingAppLayer) GetSharedChannelRemoteByIds(channelID string, remo return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { +func (a *OpenTracingAppLayer) GetSharedChannelRemotes(page int, perPage int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotes") @@ -9756,7 +9756,7 @@ func (a *OpenTracingAppLayer) GetSharedChannelRemotes(opts model.SharedChannelRe }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetSharedChannelRemotes(opts) + resultVar0, resultVar1 := a.app.GetSharedChannelRemotes(page, perPage, opts) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/server/channels/app/shared_channel.go b/server/channels/app/shared_channel.go index 5986c02c23..2cfe7841c3 100644 --- a/server/channels/app/shared_channel.go +++ b/server/channels/app/shared_channel.go @@ -137,8 +137,8 @@ func (a *App) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*m return a.Srv().Store().SharedChannel().GetRemoteByIds(channelID, remoteID) } -func (a *App) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { - return a.Srv().Store().SharedChannel().GetRemotes(opts) +func (a *App) GetSharedChannelRemotes(page, perPage int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { + return a.Srv().Store().SharedChannel().GetRemotes(page*perPage, perPage, opts) } // HasRemote returns whether a given channelID is present in the channel remotes or not. diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 81dbca13ab..aeb196dfd7 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -9106,7 +9106,7 @@ func (s *OpenTracingLayerSharedChannelStore) GetRemoteForUser(remoteId string, u return result, err } -func (s *OpenTracingLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { +func (s *OpenTracingLayerSharedChannelStore) GetRemotes(offset int, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemotes") s.Root.Store.SetContext(newCtx) @@ -9115,7 +9115,7 @@ func (s *OpenTracingLayerSharedChannelStore) GetRemotes(opts model.SharedChannel }() defer span.Finish() - result, err := s.SharedChannelStore.GetRemotes(opts) + result, err := s.SharedChannelStore.GetRemotes(offset, limit, opts) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index c7dea7a757..05c276bdfa 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -10400,11 +10400,11 @@ func (s *RetryLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId } -func (s *RetryLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { +func (s *RetryLayerSharedChannelStore) GetRemotes(offset int, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { tries := 0 for { - result, err := s.SharedChannelStore.GetRemotes(opts) + result, err := s.SharedChannelStore.GetRemotes(offset, limit, opts) if err == nil { return result, nil } diff --git a/server/channels/store/sqlstore/shared_channel_store.go b/server/channels/store/sqlstore/shared_channel_store.go index fa1fbcd9c2..309d096246 100644 --- a/server/channels/store/sqlstore/shared_channel_store.go +++ b/server/channels/store/sqlstore/shared_channel_store.go @@ -159,8 +159,10 @@ func (s SqlSharedChannelStore) GetAll(offset, limit int, opts model.SharedChanne return nil, err } - query := s.getSharedChannelsQuery(opts, false) - query = query.OrderBy("sc.ShareDisplayName, sc.ShareName").Limit(safeLimit).Offset(safeOffset) + query := s.getSharedChannelsQuery(opts, false). + OrderBy("sc.ShareDisplayName, sc.ShareName"). + Limit(safeLimit). + Offset(safeOffset) squery, args, err := query.ToSql() if err != nil { @@ -459,25 +461,49 @@ func (s SqlSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) } // GetRemotes fetches all shared channel remotes associated with channel_id. -func (s SqlSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { +func (s SqlSharedChannelStore) GetRemotes(offset, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { + if opts.ExcludeHome && opts.ExcludeRemote { + return nil, errors.New("cannot exclude home and remote shared channel remotes") + } + + if offset < 0 { + return nil, errors.New("offset must be a positive integer") + } + if limit < 0 { + return nil, errors.New("limit must be a positive integer") + } + remotes := []*model.SharedChannelRemote{} query := s.getQueryBuilder(). - Select(sharedChannelRemoteFields("")...). - From("SharedChannelRemotes") + Select(sharedChannelRemoteFields("scr")...). + From("SharedChannelRemotes scr"). + OrderBy("scr.Id") if opts.ChannelId != "" { - query = query.Where(sq.Eq{"ChannelId": opts.ChannelId}) + query = query.Where(sq.Eq{"scr.ChannelId": opts.ChannelId}) } if opts.RemoteId != "" { - query = query.Where(sq.Eq{"RemoteId": opts.RemoteId}) + query = query.Where(sq.Eq{"scr.RemoteId": opts.RemoteId}) } if !opts.InclUnconfirmed { - query = query.Where(sq.Eq{"IsInviteConfirmed": true}) + query = query.Where(sq.Eq{"scr.IsInviteConfirmed": true}) } + if opts.ExcludeHome { + query = query.Join("SharedChannels sc ON (scr.ChannelId = sc.ChannelId)"). + Where(sq.Eq{"sc.Home": false}) + } + + if opts.ExcludeRemote { + query = query.Join("SharedChannels sc ON (scr.ChannelId = sc.ChannelId)"). + Where(sq.Eq{"sc.Home": true}) + } + + query = query.Offset(uint64(offset)).Limit(uint64(limit)) + squery, args, err := query.ToSql() if err != nil { return nil, errors.Wrapf(err, "get_shared_channel_remotes_tosql") diff --git a/server/channels/store/store.go b/server/channels/store/store.go index cbcb8325f9..73b70cee4f 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -980,7 +980,7 @@ type SharedChannelStore interface { HasRemote(channelID string, remoteId string) (bool, error) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) - GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) + GetRemotes(offset, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error DeleteRemote(remoteId string) (bool, error) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) diff --git a/server/channels/store/storetest/mocks/SharedChannelStore.go b/server/channels/store/storetest/mocks/SharedChannelStore.go index a867d5943f..eeb3a888d1 100644 --- a/server/channels/store/storetest/mocks/SharedChannelStore.go +++ b/server/channels/store/storetest/mocks/SharedChannelStore.go @@ -278,9 +278,9 @@ func (_m *SharedChannelStore) GetRemoteForUser(remoteId string, userId string) ( return r0, r1 } -// GetRemotes provides a mock function with given fields: opts -func (_m *SharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { - ret := _m.Called(opts) +// GetRemotes provides a mock function with given fields: offset, limit, opts +func (_m *SharedChannelStore) GetRemotes(offset int, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { + ret := _m.Called(offset, limit, opts) if len(ret) == 0 { panic("no return value specified for GetRemotes") @@ -288,19 +288,19 @@ func (_m *SharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpt var r0 []*model.SharedChannelRemote var r1 error - if rf, ok := ret.Get(0).(func(model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error)); ok { - return rf(opts) + if rf, ok := ret.Get(0).(func(int, int, model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error)); ok { + return rf(offset, limit, opts) } - if rf, ok := ret.Get(0).(func(model.SharedChannelRemoteFilterOpts) []*model.SharedChannelRemote); ok { - r0 = rf(opts) + if rf, ok := ret.Get(0).(func(int, int, model.SharedChannelRemoteFilterOpts) []*model.SharedChannelRemote); ok { + r0 = rf(offset, limit, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.SharedChannelRemote) } } - if rf, ok := ret.Get(1).(func(model.SharedChannelRemoteFilterOpts) error); ok { - r1 = rf(opts) + if rf, ok := ret.Get(1).(func(int, int, model.SharedChannelRemoteFilterOpts) error); ok { + r1 = rf(offset, limit, opts) } else { r1 = ret.Error(1) } diff --git a/server/channels/store/storetest/shared_channel_store.go b/server/channels/store/storetest/shared_channel_store.go index 4c2a4af972..c95aa62aa1 100644 --- a/server/channels/store/storetest/shared_channel_store.go +++ b/server/channels/store/storetest/shared_channel_store.go @@ -191,16 +191,17 @@ func testGetSharedChannels(t *testing.T, rctx request.CTX, ss store.Store) { creator := model.NewId() team1 := model.NewId() team2 := model.NewId() - rid := model.NewId() + rid1 := model.NewId() + rid2 := model.NewId() data := []model.SharedChannel{ {CreatorId: creator, TeamId: team1, ShareName: "test1", Home: true}, - {CreatorId: creator, TeamId: team1, ShareName: "test2", Home: false, RemoteId: rid}, - {CreatorId: creator, TeamId: team1, ShareName: "test3", Home: false, RemoteId: rid}, + {CreatorId: creator, TeamId: team1, ShareName: "test2", Home: false, RemoteId: rid1}, + {CreatorId: creator, TeamId: team1, ShareName: "test3", Home: false, RemoteId: rid2}, {CreatorId: creator, TeamId: team1, ShareName: "test4", Home: true}, {CreatorId: creator, TeamId: team2, ShareName: "test5", Home: true}, - {CreatorId: creator, TeamId: team2, ShareName: "test6", Home: false, RemoteId: rid}, - {CreatorId: creator, TeamId: team2, ShareName: "test7", Home: false, RemoteId: rid}, + {CreatorId: creator, TeamId: team2, ShareName: "test6", Home: false, RemoteId: rid1}, + {CreatorId: creator, TeamId: team2, ShareName: "test7", Home: false, RemoteId: rid2}, {CreatorId: creator, TeamId: team2, ShareName: "test8", Home: true}, {CreatorId: creator, TeamId: team2, ShareName: "test9", Home: true}, } @@ -407,7 +408,7 @@ func testDeleteSharedChannel(t *testing.T, rctx request.CTX, ss store.Store) { require.Nil(t, sc) // make sure the remotes were deleted. - remotes, err := ss.SharedChannel().GetRemotes(model.SharedChannelRemoteFilterOpts{ChannelId: channel.Id}) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, model.SharedChannelRemoteFilterOpts{ChannelId: channel.Id}) require.NoError(t, err) require.Len(t, remotes, 0, "expected empty remotes list") @@ -578,21 +579,29 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) creator := model.NewId() remoteId := model.NewId() + remoteId2 := model.NewId() data := []model.SharedChannelRemote{ {ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true}, {ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true}, - {ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true}, + {ChannelId: channel.Id, CreatorId: creator, RemoteId: remoteId2, IsInviteConfirmed: true}, {CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true}, {CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true}, {CreatorId: creator, RemoteId: remoteId}, } + // first three remotes are homed locally + _, scErr := shareChannel(ss, channel, true, "") + require.NoError(t, scErr) + for i, r := range data { if r.ChannelId == "" { c, err := createTestChannel(ss, rctx, "test_remotes_get2_"+strconv.Itoa(i)) require.NoError(t, err) r.ChannelId = c.Id + + // next three remotes are homed outside + shareChannel(ss, c, false, r.RemoteId) } _, err := ss.SharedChannel().SaveRemote(&r) require.NoError(t, err, "error saving shared channel remote") @@ -602,7 +611,7 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) opts := model.SharedChannelRemoteFilterOpts{ ChannelId: channel.Id, } - remotes, err := ss.SharedChannel().GetRemotes(opts) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) require.Len(t, remotes, 3) for _, r := range remotes { @@ -614,7 +623,7 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) opts := model.SharedChannelRemoteFilterOpts{ ChannelId: model.NewId(), } - remotes, err := ss.SharedChannel().GetRemotes(opts) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) require.Len(t, remotes, 0) }) @@ -623,7 +632,7 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) opts := model.SharedChannelRemoteFilterOpts{ RemoteId: remoteId, } - remotes, err := ss.SharedChannel().GetRemotes(opts) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) require.Len(t, remotes, 2) // only confirmed invitations for _, r := range remotes { @@ -636,7 +645,7 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) opts := model.SharedChannelRemoteFilterOpts{ RemoteId: model.NewId(), } - remotes, err := ss.SharedChannel().GetRemotes(opts) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) require.Len(t, remotes, 0) }) @@ -646,13 +655,61 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) RemoteId: remoteId, InclUnconfirmed: true, } - remotes, err := ss.SharedChannel().GetRemotes(opts) + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) require.Len(t, remotes, 3) for _, r := range remotes { require.Equal(t, remoteId, r.RemoteId) } }) + + t.Run("Get shared channel remotes with bad options", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + ExcludeHome: true, + ExcludeRemote: true, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.Error(t, err, "error expected") + require.Empty(t, remotes) + }) + + t.Run("Get shared channel remotes excluding shared from outside", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + ExcludeRemote: true, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.NoError(t, err, "should not error", err) + require.Len(t, remotes, 3) + }) + + t.Run("Get shared channel remotes excluding shared from home", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + ExcludeHome: true, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.NoError(t, err, "should not error", err) + require.Len(t, remotes, 2) + }) + + t.Run("Get shared channel remotes excluding shared from outside and by remote_id", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + ExcludeRemote: true, + RemoteId: remoteId2, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.NoError(t, err, "should not error", err) + require.Len(t, remotes, 1) + }) + + t.Run("Get shared channel remotes excluding shared from home including unconfirmed", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + ExcludeHome: true, + InclUnconfirmed: true, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.NoError(t, err, "should not error", err) + require.Len(t, remotes, 3) + }) } func testHasRemote(t *testing.T, rctx request.CTX, ss store.Store) { @@ -892,21 +949,26 @@ func createSharedTestChannel(ss store.Store, rctx request.CTX, name string, shar } if shared { - sc := &model.SharedChannel{ - ChannelId: channel.Id, - TeamId: channel.TeamId, - CreatorId: channel.CreatorId, - ShareName: channel.Name, - Home: true, - } - _, err = ss.SharedChannel().Save(sc) - if err != nil { + if _, err := shareChannel(ss, channel, true, ""); err != nil { return nil, err } } return channel, nil } +func shareChannel(ss store.Store, channel *model.Channel, home bool, remoteId string) (*model.SharedChannel, error) { + sc := &model.SharedChannel{ + ChannelId: channel.Id, + TeamId: channel.TeamId, + CreatorId: channel.CreatorId, + ShareName: channel.Name, + Home: home, + RemoteId: remoteId, + } + + return ss.SharedChannel().Save(sc) +} + func clearSharedChannels(ss store.Store) error { opts := model.SharedChannelFilterOpts{} all, err := ss.SharedChannel().GetAll(0, 1000, opts) diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 3fbef4d0f3..c9d8bc7a7d 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -8201,10 +8201,10 @@ func (s *TimerLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId return result, err } -func (s *TimerLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { +func (s *TimerLayerSharedChannelStore) GetRemotes(offset int, limit int, opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) { start := time.Now() - result, err := s.SharedChannelStore.GetRemotes(opts) + result, err := s.SharedChannelStore.GetRemotes(offset, limit, opts) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/server/channels/web/params.go b/server/channels/web/params.go index bed73298c5..314223ad49 100644 --- a/server/channels/web/params.go +++ b/server/channels/web/params.go @@ -100,6 +100,8 @@ type Params struct { OnlyConfirmed bool OnlyPlugins bool ExcludePlugins bool + ExcludeHome bool + ExcludeRemote bool //Bookmarks ChannelBookmarkId string @@ -169,6 +171,8 @@ func ParamsFromRequest(r *http.Request) *Params { params.OnlyConfirmed, _ = strconv.ParseBool(query.Get("only_confirmed")) params.OnlyPlugins, _ = strconv.ParseBool(query.Get("only_plugins")) params.ExcludePlugins, _ = strconv.ParseBool(query.Get("exclude_plugins")) + params.ExcludeHome, _ = strconv.ParseBool(query.Get("exclude_home")) + params.ExcludeRemote, _ = strconv.ParseBool(query.Get("exclude_remote")) params.ChannelBookmarkId = props["bookmark_id"] params.Scope = query.Get("scope") diff --git a/server/i18n/en.json b/server/i18n/en.json index 4e7caf260d..26227fdf89 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2874,6 +2874,22 @@ "id": "api.server.start_server.starting.critical", "translation": "Error starting server, err:%v" }, + { + "id": "api.shared_channel.get_shared_channel_remotes_error", + "translation": "Could not fetch shared channel remotes" + }, + { + "id": "api.shared_channel.has_remote_error", + "translation": "Could not determine if channel is shared with the remote" + }, + { + "id": "api.shared_channel.invite_remote_to_channel_error", + "translation": "Could not invite remote to channel" + }, + { + "id": "api.shared_channel.uninvite_remote_to_channel_error", + "translation": "Could not uninvite remote to channel" + }, { "id": "api.slackimport.slack_add_bot_user.email_pwd", "translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n" diff --git a/server/platform/services/sharedchannel/sync_send.go b/server/platform/services/sharedchannel/sync_send.go index bbf675c661..ca8a76ce52 100644 --- a/server/platform/services/sharedchannel/sync_send.go +++ b/server/platform/services/sharedchannel/sync_send.go @@ -104,7 +104,7 @@ func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) { opts := model.SharedChannelRemoteFilterOpts{ RemoteId: rc.RemoteId, } - scrs, err := scs.server.GetStore().SharedChannel().GetRemotes(opts) + scrs, err := scs.server.GetStore().SharedChannel().GetRemotes(0, 999999, opts) if err != nil { scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch shared channel remotes", mlog.String("remote", rc.DisplayName), diff --git a/server/public/model/client4.go b/server/public/model/client4.go index 784da4e168..d1320ac6fe 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -572,6 +572,14 @@ func (c *Client4) remoteClusterRoute() string { return "/remotecluster" } +func (c *Client4) sharedChannelRemotesRoute(remoteId string) string { + return fmt.Sprintf("%s/%s/sharedchannelremotes", c.remoteClusterRoute(), remoteId) +} + +func (c *Client4) channelRemoteRoute(remoteId, channelId string) string { + return fmt.Sprintf("%s/%s/channels/%s", c.remoteClusterRoute(), remoteId, channelId) +} + func (c *Client4) sharedChannelsRoute() string { return "/sharedchannels" } @@ -8887,6 +8895,57 @@ func (c *Client4) DeleteRemoteCluster(ctx context.Context, remoteClusterId strin return BuildResponse(r), nil } +func (c *Client4) GetSharedChannelRemotesByRemoteCluster(ctx context.Context, remoteId string, excludeHome, excludeRemote bool, page, perPage int) ([]*SharedChannelRemote, *Response, error) { + v := url.Values{} + if excludeHome { + v.Set("exclude_home", "true") + } + if excludeRemote { + v.Set("exclude_remote", "true") + } + if page != 0 { + v.Set("page", fmt.Sprintf("%d", page)) + } + if perPage != 0 { + v.Set("per_page", fmt.Sprintf("%d", perPage)) + } + url := c.sharedChannelRemotesRoute(remoteId) + if len(v) > 0 { + url += "?" + v.Encode() + } + + r, err := c.DoAPIGet(ctx, url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var scs []*SharedChannelRemote + json.NewDecoder(r.Body).Decode(&scs) + + return scs, BuildResponse(r), nil +} + +func (c *Client4) InviteRemoteClusterToChannel(ctx context.Context, remoteId, channelId string) (*Response, error) { + url := fmt.Sprintf("%s/invite", c.channelRemoteRoute(remoteId, channelId)) + r, err := c.DoAPIPost(ctx, url, "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +func (c *Client4) UninviteRemoteClusterToChannel(ctx context.Context, remoteId, channelId string) (*Response, error) { + url := fmt.Sprintf("%s/uninvite", c.channelRemoteRoute(remoteId, channelId)) + r, err := c.DoAPIPost(ctx, url, "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + func (c *Client4) GetAncillaryPermissions(ctx context.Context, subsectionPermissions []string) ([]string, *Response, error) { var returnedPermissions []string url := fmt.Sprintf("%s/ancillary", c.permissionsRoute()) diff --git a/server/public/model/shared_channel.go b/server/public/model/shared_channel.go index 94f9f8b7a4..03794b9859 100644 --- a/server/public/model/shared_channel.go +++ b/server/public/model/shared_channel.go @@ -263,6 +263,8 @@ type SharedChannelRemoteFilterOpts struct { ChannelId string RemoteId string InclUnconfirmed bool + ExcludeHome bool + ExcludeRemote bool } // SyncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).