diff --git a/server/channels/app/export.go b/server/channels/app/export.go index fe3749e8cd..e12937912f 100644 --- a/server/channels/app/export.go +++ b/server/channels/app/export.go @@ -121,7 +121,11 @@ func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job } } - if job != nil && job.Data == nil { + if job == nil { + job = &model.Job{ + Data: make(model.StringMap), + } + } else if job.Data == nil { job.Data = make(model.StringMap) } @@ -944,14 +948,38 @@ func (a *App) exportAllDirectChannels(ctx request.CTX, job *model.Job, writer io for _, channel := range channels { afterId = channel.Id + // Skip deleted. + if channel.DeleteAt != 0 { + continue + } + // Skip if there are no active members in the channel if len(channel.Members) == 0 { continue } - // Skip deleted. - if channel.DeleteAt != 0 { - continue + // Skip if the channel member structure is not intact + switch channel.Type { + case model.ChannelTypeGroup: + groupMembers := make([]string, len(channel.Members)) + for i, m := range channel.Members { + groupMembers[i] = m.UserId + } + if channel.Name != model.GetGroupNameFromUserIds(groupMembers) { + // this is the case of a group channel when other user is permanently deleted + // we skip this channel and inform by logging it. + job.Data["skipped_direct_channels"] = job.Data["skipped_direct_channels"] + "," + channel.Id + ctx.Logger().Warn("Skipping group channels with partially deleted members", mlog.String("channel_id", channel.Id)) + continue + } + case model.ChannelTypeDirect: + if _, u2 := channel.GetBothUsersForDM(); u2 != "" && len(channel.Members) == 1 { + // this is the case of a direct channel when other user is permanently deleted + // we skip this channel and inform by logging it. + job.Data["skipped_direct_channels"] = job.Data["skipped_direct_channels"] + "," + channel.Id + ctx.Logger().Info("Skipping direct channel with one active member", mlog.String("channel_id", channel.Id), mlog.String("user_id", channel.Members[0].UserId)) + continue + } } favoritedBy, err := a.buildFavoritedByList(channel.Id) @@ -1070,6 +1098,7 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Wr cnt += len(posts) updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_posts_exported", cnt) + channelsToSkip := model.SliceToMapKey(strings.Split(job.Data["skipped_direct_channels"], ",")...) for _, post := range posts { afterId = post.Id postProcessCount++ @@ -1079,6 +1108,10 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Wr continue } + if _, ok := channelsToSkip[post.ChannelId]; ok { + continue + } + // Handle attachments. var postAttachments []imports.AttachmentImportData var err *model.AppError diff --git a/server/channels/app/export_test.go b/server/channels/app/export_test.go index 4762c378b5..0e6a4db62b 100644 --- a/server/channels/app/export_test.go +++ b/server/channels/app/export_test.go @@ -355,6 +355,50 @@ func TestExportDMChannel(t *testing.T) { require.NoError(t, nErr) assert.Empty(t, channels) }) + + t.Run("Should not export DM channel if other user is permanently deleted", func(t *testing.T) { + th1 := Setup(t).InitBasic() + defer th1.TearDown() + + // Create a DM Channel with another user + dmc1 := th1.CreateDmChannel(th1.BasicUser2) + th1.CreatePost(dmc1) + + // Create a DM Channel with self + dmc2 := th1.CreateDmChannel(th1.BasicUser) + th1.CreatePost(dmc2) + + channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) + require.NoError(t, nErr) + assert.Equal(t, 2, len(channels)) + + // Permanentley delete other user + th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser2) + + var b bytes.Buffer + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) + require.Nil(t, err) + + th2 := Setup(t).InitBasic() + defer th2.TearDown() + + // import the exported channel + err, _ = th2.App.BulkImport(th2.Context, &b, nil, false, 5) + require.Nil(t, err) + + channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) + require.NoError(t, nErr) + assert.Equal(t, 1, len(channels)) + + // Ensure the posts of the deleted DM channel do not leak to the self-DM channel + posts, nErr := th2.App.Srv().Store().Post().GetPosts(model.GetPostsOptions{ + ChannelId: channels[0].Id, + PerPage: 1000, + IncludeDeleted: true, + }, false, nil) + require.NoError(t, nErr) + assert.Equal(t, 1, len(posts.Posts)) + }) } func TestExportDMChannelToSelf(t *testing.T) { diff --git a/server/channels/app/import_functions.go b/server/channels/app/import_functions.go index 58e602bb74..8d00c4e171 100644 --- a/server/channels/app/import_functions.go +++ b/server/channels/app/import_functions.go @@ -2327,12 +2327,15 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, "", http.StatusBadRequest).Wrap(err) } channel = ch - } else { + } else if len(userIDs) > 2 { ch, err = a.createGroupChannel(rctx, userIDs) if err != nil && err.Id != store.ChannelExistsError { return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err) } channel = ch + } else { + rctx.Logger().Warn("Not enough users to create a direct channel", mlog.Int("line_number", line.LineNumber)) + continue } user := users[strings.ToLower(*line.DirectPost.User)] diff --git a/server/public/model/metrics.go b/server/public/model/metrics.go index a6e6f3ff55..0fe3aab276 100644 --- a/server/public/model/metrics.go +++ b/server/public/model/metrics.go @@ -40,11 +40,11 @@ const ( var ( performanceReportVersion = semver.MustParse("0.1.0") - acceptedPlatforms = sliceToMapKey("linux", "macos", "ios", "android", "windows", "other") - acceptedAgents = sliceToMapKey("desktop", "firefox", "chrome", "safari", "edge", "other") + acceptedPlatforms = SliceToMapKey("linux", "macos", "ios", "android", "windows", "other") + acceptedAgents = SliceToMapKey("desktop", "firefox", "chrome", "safari", "edge", "other") - AcceptedInteractions = sliceToMapKey("keyboard", "pointer", "other") - AcceptedLCPRegions = sliceToMapKey( + AcceptedInteractions = SliceToMapKey("keyboard", "pointer", "other") + AcceptedLCPRegions = SliceToMapKey( "post", "post_textbox", "channel_sidebar", @@ -56,8 +56,8 @@ var ( "modal_content", "other", ) - AcceptedTrueFalseLabels = sliceToMapKey("true", "false") - AcceptedSplashScreenOrigins = sliceToMapKey("root", "team_controller") + AcceptedTrueFalseLabels = SliceToMapKey("true", "false") + AcceptedSplashScreenOrigins = SliceToMapKey("root", "team_controller") ) type MetricSample struct { diff --git a/server/public/model/utils.go b/server/public/model/utils.go index 662c6ada72..49fd332609 100644 --- a/server/public/model/utils.go +++ b/server/public/model/utils.go @@ -860,7 +860,7 @@ func IsCloud() bool { return os.Getenv("MM_CLOUD_INSTALLATION_ID") != "" } -func sliceToMapKey(s ...string) map[string]any { +func SliceToMapKey(s ...string) map[string]any { m := make(map[string]any) for i := range s { m[s[i]] = struct{}{}