diff --git a/server/channels/store/sqlstore/shared_channel_store.go b/server/channels/store/sqlstore/shared_channel_store.go index c6ccf916da..73d8d2c92d 100644 --- a/server/channels/store/sqlstore/shared_channel_store.go +++ b/server/channels/store/sqlstore/shared_channel_store.go @@ -495,7 +495,9 @@ func (s SqlSharedChannelStore) GetRemotes(offset, limit int, opts model.SharedCh query = query.Where(sq.Eq{"scr.RemoteId": opts.RemoteId}) } - if !opts.InclUnconfirmed { + if opts.ExcludeConfirmed { + query = query.Where(sq.Eq{"scr.IsInviteConfirmed": false}) + } else if !opts.IncludeUnconfirmed { query = query.Where(sq.Eq{"scr.IsInviteConfirmed": true}) } diff --git a/server/channels/store/storetest/shared_channel_store.go b/server/channels/store/storetest/shared_channel_store.go index 60dcc26e8a..a4f8dfc2ad 100644 --- a/server/channels/store/storetest/shared_channel_store.go +++ b/server/channels/store/storetest/shared_channel_store.go @@ -393,7 +393,7 @@ func testDeleteSharedChannel(t *testing.T, rctx request.CTX, ss store.Store) { ChannelId: channel.Id, CreatorId: model.NewId(), RemoteId: model.NewId(), - IsInviteConfirmed: true, // to avoid adding the InclUnconfirmed filter + IsInviteConfirmed: true, // to avoid adding the IncludeUnconfirmed filter } _, err := ss.SharedChannel().SaveRemote(remote) require.NoError(t, err, "couldn't add remote", err) @@ -688,8 +688,8 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) t.Run("Get shared channel remotes by remote_id including unconfirmed", func(t *testing.T) { opts := model.SharedChannelRemoteFilterOpts{ - RemoteId: remoteId, - InclUnconfirmed: true, + RemoteId: remoteId, + IncludeUnconfirmed: true, } remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) @@ -699,6 +699,19 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) } }) + t.Run("Get only unconfirmed shared channel remotes for remote", func(t *testing.T) { + opts := model.SharedChannelRemoteFilterOpts{ + RemoteId: remoteId, + ExcludeConfirmed: true, + } + remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) + require.NoError(t, err, "should not error", err) + require.Len(t, remotes, 1) + for _, r := range remotes { + require.False(t, r.IsInviteConfirmed) + } + }) + t.Run("Get shared channel remotes with bad options", func(t *testing.T) { opts := model.SharedChannelRemoteFilterOpts{ ExcludeHome: true, @@ -739,8 +752,8 @@ func testGetSharedChannelRemotes(t *testing.T, rctx request.CTX, ss store.Store) t.Run("Get shared channel remotes excluding shared from home including unconfirmed", func(t *testing.T) { opts := model.SharedChannelRemoteFilterOpts{ - ExcludeHome: true, - InclUnconfirmed: true, + ExcludeHome: true, + IncludeUnconfirmed: true, } remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) require.NoError(t, err, "should not error", err) diff --git a/server/platform/services/sharedchannel/channelinvite.go b/server/platform/services/sharedchannel/channelinvite.go index 2a45467e1b..ff0e016ec4 100644 --- a/server/platform/services/sharedchannel/channelinvite.go +++ b/server/platform/services/sharedchannel/channelinvite.go @@ -52,6 +52,30 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc return err } + // if the remote is not currently online, we store the invite to + // send it when the connection is restored + if !rc.IsOnline() { + if len(options) > 0 { + // pending invites with options are currently not supported + scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error sending channel invite for %s: %s", rc.DisplayName, model.ErrOfflineRemote)) + return model.ErrOfflineRemote + } + + scr := &model.SharedChannelRemote{ + ChannelId: sc.ChannelId, + CreatorId: userId, + RemoteId: rc.RemoteId, + IsInviteAccepted: true, + IsInviteConfirmed: false, + } + if _, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil { + scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error saving channel invite for %s: %v", rc.DisplayName, err)) + return err + } + + return nil + } + invite := channelInviteMsg{ ChannelId: channel.Id, ReadOnly: sc.ReadOnly, @@ -89,18 +113,20 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc curTime := model.GetMillis() if existingScr != nil { - if existingScr.DeleteAt == 0 { + if existingScr.DeleteAt == 0 && existingScr.IsInviteConfirmed { // the shared channel remote exists and is not // deleted, nothing to do here return } - // the shared channel remote was deleted in the past, so - // with the new invite we restore it + // the shared channel remote was deleted in the past or + // pending confirmation, so with the new invite we restore + // it existingScr.DeleteAt = 0 existingScr.UpdateAt = curTime existingScr.LastPostCreateAt = curTime existingScr.LastPostUpdateAt = curTime + existingScr.IsInviteConfirmed = true if _, sErr := scs.server.GetStore().SharedChannel().UpdateRemote(existingScr); sErr != nil { scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, sErr)) return diff --git a/server/platform/services/sharedchannel/service.go b/server/platform/services/sharedchannel/service.go index 192d8a8a2b..771538fe97 100644 --- a/server/platform/services/sharedchannel/service.go +++ b/server/platform/services/sharedchannel/service.go @@ -244,6 +244,7 @@ func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) { if online { // when a previously offline remote comes back online force a sync. + scs.SendPendingInvitesForRemote(rc) scs.ForceSyncForRemote(rc) } diff --git a/server/platform/services/sharedchannel/sync_send.go b/server/platform/services/sharedchannel/sync_send.go index 220e01e216..b42382e1bb 100644 --- a/server/platform/services/sharedchannel/sync_send.go +++ b/server/platform/services/sharedchannel/sync_send.go @@ -150,6 +150,61 @@ func (scs *Service) NotifyUserStatusChanged(status *model.Status) { } } +func (scs *Service) SendPendingInvitesForRemote(rc *model.RemoteCluster) { + if rcs := scs.server.GetRemoteClusterService(); rcs == nil { + return + } + + scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Processing pending invites for remote after reconnection", + mlog.String("remote", rc.DisplayName), + mlog.String("remoteId", rc.RemoteId), + ) + + opts := model.SharedChannelRemoteFilterOpts{ + RemoteId: rc.RemoteId, + ExcludeConfirmed: true, + } + 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 for pending invites", + mlog.String("remote", rc.DisplayName), + mlog.String("remoteId", rc.RemoteId), + mlog.Err(err), + ) + return + } + + for _, scr := range scrs { + channel, err := scs.server.GetStore().Channel().Get(scr.ChannelId, true) + if err != nil { + scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch channel for pending invite", + mlog.String("remote_id", scr.RemoteId), + mlog.String("channel_id", scr.ChannelId), + mlog.String("sharedchannelremote_id", scr.Id), + mlog.Err(err), + ) + continue + } + + if err := scs.SendChannelInvite(channel, scr.CreatorId, rc); err != nil { + scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to send pending invite", + mlog.String("remote_id", scr.RemoteId), + mlog.String("channel_id", scr.ChannelId), + mlog.String("sharedchannelremote_id", scr.Id), + mlog.Err(err), + ) + continue + } + + scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Pending invite sent", + mlog.String("remote", rc.DisplayName), + mlog.String("remoteId", rc.RemoteId), + mlog.String("channel_id", scr.ChannelId), + mlog.String("sharedchannelremote_id", scr.Id), + ) + } +} + // ForceSyncForRemote causes all channels shared with the remote to be synchronized. func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) { if rcs := scs.server.GetRemoteClusterService(); rcs == nil { @@ -327,9 +382,10 @@ func (scs *Service) processTask(task syncTask) error { remotesMap[r.RemoteId] = r } - // add all remotes that have the autoinvited option. + // add all confirmed remotes that have the autoinvited option. filter = model.RemoteClusterQueryFilter{ RequireOptions: model.BitflagOptionAutoInvited, + OnlyConfirmed: true, } remotesAutoInvited, err := scs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter) if err != nil { diff --git a/server/public/model/remote_cluster.go b/server/public/model/remote_cluster.go index 83304cc8de..fd6aeba617 100644 --- a/server/public/model/remote_cluster.go +++ b/server/public/model/remote_cluster.go @@ -33,6 +33,8 @@ const ( var ( validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`) + + ErrOfflineRemote = errors.New("remote is offline") ) type Bitmask uint32 diff --git a/server/public/model/shared_channel.go b/server/public/model/shared_channel.go index 6ddd975a5e..9aec719720 100644 --- a/server/public/model/shared_channel.go +++ b/server/public/model/shared_channel.go @@ -261,12 +261,13 @@ type SharedChannelFilterOpts struct { } type SharedChannelRemoteFilterOpts struct { - ChannelId string - RemoteId string - InclUnconfirmed bool - ExcludeHome bool - ExcludeRemote bool - IncludeDeleted bool + ChannelId string + RemoteId string + IncludeUnconfirmed bool + ExcludeConfirmed bool + ExcludeHome bool + ExcludeRemote bool + IncludeDeleted bool } // SyncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).