Allows invites to be sent to offline remotes (#28176)

* Allows invites to be sent to offline remotes

Invites sent to remotes marked as offline will be stored as pending,
and when the remote comes back online, it will process the invites as
part of the synchronization process.

* Update condition name for excluding confirmed invites
Этот коммит содержится в:
Miguel de la Cruz
2024-09-13 10:54:51 +02:00
коммит произвёл GitHub
родитель d879927876
Коммит f41d54b336
7 изменённых файлов: 117 добавлений и 16 удалений

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

@@ -495,7 +495,9 @@ func (s SqlSharedChannelStore) GetRemotes(offset, limit int, opts model.SharedCh
query = query.Where(sq.Eq{"scr.RemoteId": opts.RemoteId}) 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}) query = query.Where(sq.Eq{"scr.IsInviteConfirmed": true})
} }

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

@@ -393,7 +393,7 @@ func testDeleteSharedChannel(t *testing.T, rctx request.CTX, ss store.Store) {
ChannelId: channel.Id, ChannelId: channel.Id,
CreatorId: model.NewId(), CreatorId: model.NewId(),
RemoteId: 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) _, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't add remote", err) 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) { t.Run("Get shared channel remotes by remote_id including unconfirmed", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{ opts := model.SharedChannelRemoteFilterOpts{
RemoteId: remoteId, RemoteId: remoteId,
InclUnconfirmed: true, IncludeUnconfirmed: true,
} }
remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts)
require.NoError(t, err, "should not error", err) 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) { t.Run("Get shared channel remotes with bad options", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{ opts := model.SharedChannelRemoteFilterOpts{
ExcludeHome: true, 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) { t.Run("Get shared channel remotes excluding shared from home including unconfirmed", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{ opts := model.SharedChannelRemoteFilterOpts{
ExcludeHome: true, ExcludeHome: true,
InclUnconfirmed: true, IncludeUnconfirmed: true,
} }
remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts) remotes, err := ss.SharedChannel().GetRemotes(0, 999999, opts)
require.NoError(t, err, "should not error", err) require.NoError(t, err, "should not error", err)

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

@@ -52,6 +52,30 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
return err 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{ invite := channelInviteMsg{
ChannelId: channel.Id, ChannelId: channel.Id,
ReadOnly: sc.ReadOnly, ReadOnly: sc.ReadOnly,
@@ -89,18 +113,20 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
curTime := model.GetMillis() curTime := model.GetMillis()
if existingScr != nil { if existingScr != nil {
if existingScr.DeleteAt == 0 { if existingScr.DeleteAt == 0 && existingScr.IsInviteConfirmed {
// the shared channel remote exists and is not // the shared channel remote exists and is not
// deleted, nothing to do here // deleted, nothing to do here
return return
} }
// the shared channel remote was deleted in the past, so // the shared channel remote was deleted in the past or
// with the new invite we restore it // pending confirmation, so with the new invite we restore
// it
existingScr.DeleteAt = 0 existingScr.DeleteAt = 0
existingScr.UpdateAt = curTime existingScr.UpdateAt = curTime
existingScr.LastPostCreateAt = curTime existingScr.LastPostCreateAt = curTime
existingScr.LastPostUpdateAt = curTime existingScr.LastPostUpdateAt = curTime
existingScr.IsInviteConfirmed = true
if _, sErr := scs.server.GetStore().SharedChannel().UpdateRemote(existingScr); sErr != nil { 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)) scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, sErr))
return return

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

@@ -244,6 +244,7 @@ func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError
func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) { func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) {
if online { if online {
// when a previously offline remote comes back online force a sync. // when a previously offline remote comes back online force a sync.
scs.SendPendingInvitesForRemote(rc)
scs.ForceSyncForRemote(rc) scs.ForceSyncForRemote(rc)
} }

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

@@ -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. // ForceSyncForRemote causes all channels shared with the remote to be synchronized.
func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) { func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil { if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
@@ -327,9 +382,10 @@ func (scs *Service) processTask(task syncTask) error {
remotesMap[r.RemoteId] = r remotesMap[r.RemoteId] = r
} }
// add all remotes that have the autoinvited option. // add all confirmed remotes that have the autoinvited option.
filter = model.RemoteClusterQueryFilter{ filter = model.RemoteClusterQueryFilter{
RequireOptions: model.BitflagOptionAutoInvited, RequireOptions: model.BitflagOptionAutoInvited,
OnlyConfirmed: true,
} }
remotesAutoInvited, err := scs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter) remotesAutoInvited, err := scs.server.GetStore().RemoteCluster().GetAll(0, 999999, filter)
if err != nil { if err != nil {

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

@@ -33,6 +33,8 @@ const (
var ( var (
validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`) validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`)
ErrOfflineRemote = errors.New("remote is offline")
) )
type Bitmask uint32 type Bitmask uint32

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

@@ -261,12 +261,13 @@ type SharedChannelFilterOpts struct {
} }
type SharedChannelRemoteFilterOpts struct { type SharedChannelRemoteFilterOpts struct {
ChannelId string ChannelId string
RemoteId string RemoteId string
InclUnconfirmed bool IncludeUnconfirmed bool
ExcludeHome bool ExcludeConfirmed bool
ExcludeRemote bool ExcludeHome bool
IncludeDeleted bool ExcludeRemote bool
IncludeDeleted bool
} }
// SyncMsg represents a change in content (post add/edit/delete, reaction add/remove, users). // SyncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).