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 <build@mattermost.com>
Этот коммит содержится в:
Miguel de la Cruz
2024-08-29 12:46:37 +02:00
коммит произвёл GitHub
родитель 60ffd00d30
Коммит 3dc0e63c03
20 изменённых файлов: 819 добавлений и 54 удалений

Просмотреть файл

@@ -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:

Просмотреть файл

@@ -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"

Просмотреть файл

@@ -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()

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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")
})
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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))

Просмотреть файл

@@ -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.

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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")

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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")

Просмотреть файл

@@ -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"

Просмотреть файл

@@ -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),

Просмотреть файл

@@ -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())

Просмотреть файл

@@ -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).