diff --git a/app/export.go b/app/export.go index cca7dd8066..fae99af054 100644 --- a/app/export.go +++ b/app/export.go @@ -626,7 +626,12 @@ func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError { continue } - channelLine := ImportLineFromDirectChannel(channel) + favoritedBy, err := a.buildFavoritedByList(channel.Id) + if err != nil { + return err + } + + channelLine := ImportLineFromDirectChannel(channel, favoritedBy) if err := a.exportWriteLine(writer, channelLine); err != nil { return err } @@ -636,6 +641,29 @@ func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError { return nil } +func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) { + prefs, err := a.Srv().Store().Preference().GetCategoryAndName(model.PreferenceCategoryFavoriteChannel, channelID) + if err != nil { + return nil, model.NewAppError("buildFavoritedByList", "app.preference.get_category.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + userIDs := make([]string, 0, len(prefs)) + for _, pref := range prefs { + if pref.Value != "true" { + continue + } + + user, err := a.Srv().Store().User().Get(context.Background(), pref.UserId) + if err != nil { + return nil, model.NewAppError("buildFavoritedByList", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + userIDs = append(userIDs, user.Username) + } + + return userIDs, nil +} + func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) diff --git a/app/export_converters.go b/app/export_converters.go index 6f6cd86cd6..750a0decb5 100644 --- a/app/export_converters.go +++ b/app/export_converters.go @@ -39,18 +39,25 @@ func ImportLineFromChannel(channel *model.ChannelForExport) *imports.LineImportD } } -func ImportLineFromDirectChannel(channel *model.DirectChannelForExport) *imports.LineImportData { +func ImportLineFromDirectChannel(channel *model.DirectChannelForExport, favoritedBy []string) *imports.LineImportData { channelMembers := *channel.Members if len(channelMembers) == 1 { channelMembers = []string{channelMembers[0], channelMembers[0]} } - return &imports.LineImportData{ + + line := &imports.LineImportData{ Type: "direct_channel", DirectChannel: &imports.DirectChannelImportData{ Header: &channel.Header, Members: &channelMembers, }, } + + if len(favoritedBy) != 0 { + line.DirectChannel.FavoritedBy = &favoritedBy + } + + return line } func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imports.LineImportData { diff --git a/app/export_test.go b/app/export_test.go index 00c92e39da..066aec3771 100644 --- a/app/export_test.go +++ b/app/export_test.go @@ -223,7 +223,16 @@ func TestExportDMChannel(t *testing.T) { defer th1.TearDown() // DM Channel - th1.CreateDmChannel(th1.BasicUser2) + ch := th1.CreateDmChannel(th1.BasicUser2) + + th1.App.Srv().Store().Preference().Save(model.Preferences{ + { + UserId: th1.BasicUser2.Id, + Category: model.PreferenceCategoryFavoriteChannel, + Name: ch.Id, + Value: "true", + }, + }) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) @@ -250,6 +259,12 @@ func TestExportDMChannel(t *testing.T) { require.NoError(t, nErr) require.Equal(t, 1, len(channels)) assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, *channels[0].Members) + + // Ensure the favorited channel was retained + fav, nErr := th2.App.Srv().Store().Preference().Get(th2.BasicUser2.Id, model.PreferenceCategoryFavoriteChannel, channels[0].Id) + require.NoError(t, nErr) + require.NotNil(t, fav) + require.Equal(t, "true", fav.Value) }) t.Run("Invalid DM channel export", func(t *testing.T) { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index f66f3352b4..4f44190b77 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -6624,6 +6624,24 @@ func (s *OpenTracingLayerPreferenceStore) GetCategory(userID string, category st return result, err } +func (s *OpenTracingLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetCategoryAndName") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PreferenceStore.GetCategoryAndName(category, nane) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPreferenceStore) PermanentDeleteByUser(userID string) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.PermanentDeleteByUser") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index c28caa0d31..5afcf7fd9d 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -7515,6 +7515,27 @@ func (s *RetryLayerPreferenceStore) GetCategory(userID string, category string) } +func (s *RetryLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) { + + tries := 0 + for { + result, err := s.PreferenceStore.GetCategoryAndName(category, nane) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPreferenceStore) PermanentDeleteByUser(userID string) error { tries := 0 diff --git a/store/sqlstore/preference_store.go b/store/sqlstore/preference_store.go index 74dffd3d50..6815e950e8 100644 --- a/store/sqlstore/preference_store.go +++ b/store/sqlstore/preference_store.go @@ -140,6 +140,23 @@ func (s SqlPreferenceStore) Get(userId string, category string, name string) (*m return &preference, nil } +func (s SqlPreferenceStore) GetCategoryAndName(category string, name string) (model.Preferences, error) { + var preferences model.Preferences + query, args, err := s.getQueryBuilder(). + Select("*"). + From("Preferences"). + Where(sq.Eq{"Category": category}). + Where(sq.Eq{"Name": name}). + ToSql() + if err != nil { + return nil, errors.Wrap(err, "could not build sql query to get preference") + } + if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil { + return nil, errors.Wrapf(err, "failed to find Preference with category=%s, name=%s", category, name) + } + return preferences, nil +} + func (s SqlPreferenceStore) GetCategory(userId string, category string) (model.Preferences, error) { var preferences model.Preferences query, args, err := s.getQueryBuilder(). diff --git a/store/store.go b/store/store.go index 5b4ca3dc6d..95979e7c30 100644 --- a/store/store.go +++ b/store/store.go @@ -633,6 +633,7 @@ type CommandWebhookStore interface { type PreferenceStore interface { Save(preferences model.Preferences) error GetCategory(userID string, category string) (model.Preferences, error) + GetCategoryAndName(category string, nane string) (model.Preferences, error) Get(userID string, category string, name string) (*model.Preference, error) GetAll(userID string) (model.Preferences, error) Delete(userID, category, name string) error diff --git a/store/storetest/mocks/PreferenceStore.go b/store/storetest/mocks/PreferenceStore.go index ef3dc3b432..c651e905bc 100644 --- a/store/storetest/mocks/PreferenceStore.go +++ b/store/storetest/mocks/PreferenceStore.go @@ -167,6 +167,29 @@ func (_m *PreferenceStore) GetCategory(userID string, category string) (model.Pr return r0, r1 } +// GetCategoryAndName provides a mock function with given fields: category, nane +func (_m *PreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) { + ret := _m.Called(category, nane) + + var r0 model.Preferences + if rf, ok := ret.Get(0).(func(string, string) model.Preferences); ok { + r0 = rf(category, nane) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.Preferences) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(category, nane) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PermanentDeleteByUser provides a mock function with given fields: userID func (_m *PreferenceStore) PermanentDeleteByUser(userID string) error { ret := _m.Called(userID) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index cec6251fc7..9e7e804646 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -5987,6 +5987,22 @@ func (s *TimerLayerPreferenceStore) GetCategory(userID string, category string) return result, err } +func (s *TimerLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) { + start := time.Now() + + result, err := s.PreferenceStore.GetCategoryAndName(category, nane) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetCategoryAndName", success, elapsed) + } + return result, err +} + func (s *TimerLayerPreferenceStore) PermanentDeleteByUser(userID string) error { start := time.Now()