[MM-58834] Review user preferences export and import (#28286)

This commit is contained in:
Ibrahim Serdar Acikgoz 2024-10-11 12:28:37 +02:00 committed by GitHub
parent e8ebbcc980
commit 9d5993d89d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 526 additions and 140 deletions

View File

@ -29,43 +29,83 @@ import (
// We use this map to identify the exportable preferences.
// Here we link the preference category and name, to the name of the relevant field in the import struct.
var exportablePreferences = map[imports.ComparablePreference]string{{
var exportablePreferences = map[imports.ComparablePreference]string{
{
Category: model.PreferenceCategoryTheme,
Name: "",
}: "Theme", {
}: "Theme",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "feature_enabled_markdown_preview",
}: "UseMarkdownPreview", {
}: "UseMarkdownPreview",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "formatting",
}: "UseFormatting", {
}: "UseFormatting",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "send_on_ctrl_enter",
}: "SendOnCtrlEnter",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "code_block_ctrl_enter",
}: "CodeBlockCtrlEnter",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "join_leave",
}: "ShowJoinLeave",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "unread_scroll_position",
}: "ShowUnreadScrollPosition",
{
Category: model.PreferenceCategoryAdvancedSettings,
Name: "sync_drafts",
}: "SyncDrafts",
{
Category: model.PreferenceCategorySidebarSettings,
Name: "show_unread_section",
}: "ShowUnreadSection", {
Name: model.PreferenceNameShowUnreadSection,
}: "ShowUnreadSection",
{
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
}: "LimitVisibleDmsGms",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
}: "UseMilitaryTime", {
}: "UseMilitaryTime",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
}: "CollapsePreviews", {
}: "CollapsePreviews",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
}: "MessageDisplay", {
}: "MessageDisplay",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
}: "CollapseConsecutive", {
Name: model.PreferenceNameChannelDisplayMode,
}: "ChannelDisplayMode",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: "collapse_consecutive_messages",
}: "ColorizeUsernames", {
Name: model.PreferenceNameCollapseConsecutive,
}: "CollapseConsecutive",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: "colorize_usernames",
}: "ChannelDisplayMode", {
Name: model.PreferenceNameColorizeUsernames,
}: "ColorizeUsernames",
{
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameNameFormat,
}: "NameFormat",
{
Category: model.PreferenceCategoryTutorialSteps,
Name: "",
}: "TutorialStep", {
}: "TutorialStep",
{
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
}: "EmailInterval",
}: "EmailInterval",
}
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {

View File

@ -165,6 +165,13 @@ func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imp
ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"],
TutorialStep: exportedPrefs["TutorialStep"],
EmailInterval: exportedPrefs["EmailInterval"],
NameFormat: exportedPrefs["NameFormat"],
SendOnCtrlEnter: exportedPrefs["SendOnCtrlEnter"],
ShowJoinLeave: exportedPrefs["ShowJoinLeave"],
SyncDrafts: exportedPrefs["SyncDrafts"],
ShowUnreadScrollPosition: exportedPrefs["ShowUnreadScrollPosition"],
LimitVisibleDmsGms: exportedPrefs["LimitVisibleDmsGms"],
CodeBlockCtrlEnter: exportedPrefs["CodeBlockCtrlEnter"],
DeleteAt: &user.DeleteAt,
},
}
@ -238,6 +245,7 @@ func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelM
}
func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
f := []string(post.FlaggedBy)
return &imports.LineImportData{
Type: "post",
Post: &imports.PostImportData{
@ -249,6 +257,8 @@ func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
},
}
}
@ -258,6 +268,7 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]}
}
f := []string(post.FlaggedBy)
return &imports.LineImportData{
Type: "direct_post",
DirectPost: &imports.DirectPostImportData{
@ -268,17 +279,22 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
},
}
}
func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData {
f := []string(post.FlaggedBy)
return &imports.ReplyImportData{
User: &post.Username,
Type: &post.Type,
Message: &post.Message,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
IsPinned: &post.IsPinned,
FlaggedBy: &f,
}
}

View File

@ -722,7 +722,7 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
Name: model.PreferenceNameChannelDisplayMode,
Value: *data.ChannelDisplayMode,
})
}
@ -763,6 +763,69 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
})
}
if data.SendOnCtrlEnter != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "send_on_ctrl_enter",
Value: *data.SendOnCtrlEnter,
})
}
if data.CodeBlockCtrlEnter != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "code_block_ctrl_enter",
Value: *data.CodeBlockCtrlEnter,
})
}
if data.ShowJoinLeave != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "join_leave",
Value: *data.ShowJoinLeave,
})
}
if data.ShowUnreadScrollPosition != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "unread_scroll_position",
Value: *data.ShowUnreadScrollPosition,
})
}
if data.SyncDrafts != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "sync_drafts",
Value: *data.SyncDrafts,
})
}
if data.LimitVisibleDmsGms != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: model.PreferenceLimitVisibleDmsGms,
Value: *data.LimitVisibleDmsGms,
})
}
if data.NameFormat != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameNameFormat,
Value: *data.NameFormat,
})
}
if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" {
var intervalSeconds string
if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" {
@ -1184,6 +1247,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
return err
}
usernames = append(usernames, *replyData.User)
if replyData.FlaggedBy != nil {
usernames = append(usernames, *replyData.FlaggedBy...)
}
}
users, err := a.getUsersByUsernames(usernames)
@ -1233,6 +1299,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
if replyData.EditAt != nil {
reply.EditAt = *replyData.EditAt
}
if replyData.IsPinned != nil {
reply.IsPinned = *replyData.IsPinned
}
fileIDs := a.uploadAttachments(rctx, replyData.Attachments, reply, teamID, extractContent)
for _, fileID := range reply.FileIds {
@ -1274,6 +1343,27 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
for _, postWithData := range postsWithData {
a.updateFileInfoWithPostId(rctx, postWithData.post)
if postWithData.replyData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.replyData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
}
return nil

View File

@ -1179,6 +1179,13 @@ func TestImportImportUser(t *testing.T) {
UseFormatting: ptrStr("true"),
ShowUnreadSection: ptrStr("true"),
EmailInterval: ptrStr("immediately"),
NameFormat: model.NewPointer("full_name"),
SendOnCtrlEnter: model.NewPointer("true"),
CodeBlockCtrlEnter: model.NewPointer("true"),
ShowJoinLeave: model.NewPointer("false"),
SyncDrafts: model.NewPointer("false"),
ShowUnreadScrollPosition: model.NewPointer("start_from_newest"),
LimitVisibleDmsGms: model.NewPointer("20"),
}
appErr = th.App.importUser(th.Context, &data, false)
assert.Nil(t, appErr)
@ -1197,7 +1204,13 @@ func TestImportImportUser(t *testing.T) {
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "feature_enabled_markdown_preview", *data.UseMarkdownPreview)
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "formatting", *data.UseFormatting)
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, "show_unread_section", *data.ShowUnreadSection)
checkPreference(t, th.App, user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval, "30")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat, "full_name")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "send_on_ctrl_enter", "true")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "code_block_ctrl_enter", "true")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "join_leave", "false")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "sync_drafts", "false")
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "unread_scroll_position", "start_from_newest")
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, model.PreferenceLimitVisibleDmsGms, "20")
// Change those preferences.
data = imports.UserImportData{
@ -2556,6 +2569,44 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCountForTeam2, 1, team2.Id)
AssertAllPostsCount(t, th.App, initialPostCount, 15, team.Id)
t.Run("Importing a post with a reply both pinned", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Thread Message"),
CreateAt: model.NewPointer(importCreate),
IsPinned: model.NewPointer(true),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: model.NewPointer("Reply"),
CreateAt: model.NewPointer(replyCreate),
IsPinned: model.NewPointer(true),
}},
},
},
LineNumber: 1,
}
_, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
require.True(t, resultPosts[0].IsPinned)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
require.True(t, resultPosts[0].IsPinned)
})
t.Run("Importing a post with a thread", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
@ -2715,6 +2766,76 @@ func TestImportimportMultiplePostLines(t *testing.T) {
assert.ElementsMatch(t, []string{user.Id}, followers)
})
t.Run("Importing a post that someone flagged", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Flagged Message"),
CreateAt: model.NewPointer(importCreate),
FlaggedBy: &[]string{user.Username},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
require.NoError(t, err)
require.Len(t, pref, 1)
assert.Equal(t, user.Id, pref[0].UserId)
})
t.Run("Importing a post that someone flagged its replies", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: model.NewPointer("Flagged Message"),
CreateAt: model.NewPointer(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: model.NewPointer("Reply"),
CreateAt: model.NewPointer(replyCreate),
FlaggedBy: &[]string{user2.Username},
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
require.NoError(t, nErr)
require.Equal(t, 1, len(resultPosts))
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
require.NoError(t, err)
require.Len(t, pref, 1)
assert.Equal(t, user2.Id, pref[0].UserId)
})
}
func TestImportImportPost(t *testing.T) {

View File

@ -73,6 +73,14 @@ type UserImportData struct {
ShowUnreadSection *string `json:"show_unread_section,omitempty"`
DeleteAt *int64 `json:"delete_at,omitempty"`
SendOnCtrlEnter *string `json:"send_on_ctrl_enter,omitempty"`
CodeBlockCtrlEnter *string `json:"code_block_ctrl_enter,omitempty"`
ShowJoinLeave *string `json:"show_join_leave,omitempty"`
ShowUnreadScrollPosition *string `json:"show_unread_scroll_position,omitempty"`
SyncDrafts *string `json:"sync_drafts,omitempty"`
LimitVisibleDmsGms *string `json:"limit_visible_dms_gms,omitempty"`
NameFormat *string `json:"name_format,omitempty"`
Teams *[]UserTeamImportData `json:"teams,omitempty"`
Theme *string `json:"theme,omitempty"`
@ -169,6 +177,7 @@ type ReplyImportData struct {
FlaggedBy *[]string `json:"flagged_by,omitempty"`
Reactions *[]ReactionImportData `json:"reactions,omitempty"`
Attachments *[]AttachmentImportData `json:"attachments,omitempty"`
IsPinned *bool `json:"is_pinned,omitempty"`
}
type PostImportData struct {
@ -210,7 +219,7 @@ type DirectPostImportData struct {
CreateAt *int64 `json:"create_at"`
EditAt *int64 `json:"edit_at"`
FlaggedBy *[]string `json:"flagged_by"`
FlaggedBy *[]string `json:"flagged_by,omitempty"`
Reactions *[]ReactionImportData `json:"reactions"`
Replies *[]ReplyImportData `json:"replies"`
Attachments *[]AttachmentImportData `json:"attachments"`

View File

@ -295,7 +295,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
a.sendUpdatedUserEvent(*nUser)
}
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"}
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryRecommendedNextSteps, Name: model.PreferenceNameRecommendedNextStepsHide, Value: "false"}
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"}

View File

@ -107,6 +107,15 @@ func postSliceColumns() []string {
return cols
}
func postSliceColumnsWithName(name string) []string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
cols[i] = name + "." + colInfo.Name
}
return cols
}
func postSliceCoalesceQuery() string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
@ -2661,13 +2670,22 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
excludeDeletedCond = append(excludeDeletedCond, sq.Eq{"Channels.DeleteAt": 0})
}
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
if s.DriverName() == model.DatabaseDriverMysql {
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
}
result := []*model.PostForExport{}
builder := s.getQueryBuilder().
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName").
Select(fmt.Sprintf("%s, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName, %s as FlaggedBy", strings.Join(postSliceColumnsWithName("p1"), ", "), aggFn)).
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
LeftJoin("Preferences ON p1.Id = Preferences.Name").
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
InnerJoin("Channels ON p1.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
InnerJoin("Users ON p1.UserId = Users.Id").
Where(excludeDeletedCond).
GroupBy(fmt.Sprintf("%s, Users.Username, Teams.Name, Channels.Name", strings.Join(postSliceColumnsWithName("p1"), ", "))).
OrderBy("p1.Id")
query, args, err := builder.ToSql()
@ -2675,56 +2693,72 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
return nil, errors.Wrap(err, "postsForExport_toSql")
}
err = s.GetSearchReplicaX().Select(&postsForExport, query, args...)
err = s.GetSearchReplicaX().Select(&result, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(postsForExport) == 0 {
if len(result) == 0 {
// All of the posts were in channels or teams that were deleted.
// Update the afterId and try again.
afterId = rootIds[len(rootIds)-1]
continue
}
return postsForExport, nil
return result, nil
}
}
func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) {
posts := []*model.ReplyForExport{}
err := s.GetSearchReplicaX().Select(&posts, `
SELECT
Posts.*,
Users.Username as Username
FROM
Posts
INNER JOIN
Users ON Posts.UserId = Users.Id
WHERE
Posts.RootId = ?
AND Posts.DeleteAt = 0
ORDER BY
Posts.Id`, rootId)
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
if s.DriverName() == model.DatabaseDriverMysql {
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
}
result := []*model.ReplyForExport{}
qb := s.getQueryBuilder().Select(fmt.Sprintf("Posts.*, u2.Username as Username, %s as FlaggedBy", aggFn)).
From("Posts").
LeftJoin("Preferences ON Posts.Id = Preferences.Name").
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
InnerJoin("Users u2 ON Posts.UserId = u2.Id").
Where(sq.And{sq.Eq{"Posts.RootId": rootId}, sq.Eq{"Posts.DeleteAt": 0}}).
GroupBy("Posts.Id, u2.Username").
OrderBy("Posts.Id")
query, args, err := qb.ToSql()
if err != nil {
return nil, errors.Wrap(err, "postsForExport_toSql")
}
err = s.GetSearchReplicaX().Select(&result, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
return result, nil
}
func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string, includeArchivedChannels bool) ([]*model.DirectPostForExport, error) {
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
if s.DriverName() == model.DatabaseDriverMysql {
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
}
result := []*model.DirectPostForExport{}
query := s.getQueryBuilder().
Select("p.*", "Users.Username as User").
Select(fmt.Sprintf("p.*, u2.Username as User, %s as FlaggedBy", aggFn)).
From("Posts p").
LeftJoin("Preferences ON p.Id = Preferences.Name").
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
Join("Channels ON p.ChannelId = Channels.Id").
Join("Users ON p.UserId = Users.Id").
Join("Users u2 ON p.UserId = u2.Id").
Where(sq.And{
sq.Gt{"p.Id": afterId},
sq.Eq{"p.RootId": ""},
sq.Eq{"p.DeleteAt": 0},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}).
GroupBy("p.Id, u2.Username").
OrderBy("p.Id").
Limit(uint64(limit))
@ -2739,13 +2773,12 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
return nil, errors.Wrap(err, "post_tosql")
}
posts := []*model.DirectPostForExport{}
if err2 := s.GetReplicaX().Select(&posts, queryString, args...); err2 != nil {
if err2 := s.GetReplicaX().Select(&result, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find Posts")
}
var channelIds []string
for _, post := range posts {
channelIds = append(channelIds, post.ChannelId)
for _, p := range result {
channelIds = append(channelIds, p.ChannelId)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
@ -2761,13 +2794,13 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
}
channelMembers := []*model.ChannelMemberForExport{}
if err := s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
if err = s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
// Build a map of channels and their posts
postsChannelMap := make(map[string][]*model.DirectPostForExport)
for _, post := range posts {
for _, post := range result {
post.ChannelMembers = &[]string{}
postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post)
}
@ -2784,7 +2817,8 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
*post.ChannelMembers = channelMembersMap[channelId]
}
}
return posts, nil
return result, nil
}
//nolint:unparam

View File

@ -4349,6 +4349,28 @@ func testPostStoreGetParentsForExportAfter(t *testing.T, rctx request.CTX, ss st
}
assert.True(t, found)
})
t.Run("with flagged post", func(t *testing.T) {
err := ss.Preference().Save(model.Preferences([]model.Preference{
{
UserId: u1.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: p1.Id,
Value: "true",
},
}))
require.NoError(t, err)
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, err)
for _, p := range posts {
if p.Id == p1.Id {
require.NotNil(t, p.FlaggedBy)
assert.Equal(t, model.StringArray([]string{u1.Username}), p.FlaggedBy)
}
}
})
}
func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.Store) {
@ -4394,7 +4416,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
r1, err := ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
require.Len(t, r1, 1)
reply1 := r1[0]
assert.Equal(t, reply1.Id, p2.Id)
@ -4409,7 +4431,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
r1, err = ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
require.Len(t, r1, 1)
reply1 = r1[0]
assert.Equal(t, reply1.Id, p2.Id)

View File

@ -235,17 +235,20 @@ type PostForExport struct {
ChannelName string
Username string
ReplyCount int
FlaggedBy StringArray
}
type DirectPostForExport struct {
Post
User string
ChannelMembers *[]string
FlaggedBy StringArray
}
type ReplyForExport struct {
Post
Username string
FlaggedBy StringArray
}
type PostForIndexing struct {

View File

@ -13,15 +13,70 @@ import (
)
const (
// The primary key for the preference table is the combination of User.Id, Category, and Name.
// PreferenceCategoryDirectChannelShow and PreferenceCategoryGroupChannelShow
// are used to store the user's preferences for which channels to show in the sidebar.
// The Name field is the channel ID.
PreferenceCategoryDirectChannelShow = "direct_channel_show"
PreferenceCategoryGroupChannelShow = "group_channel_show"
// PreferenceCategoryTutorialStep is used to store the user's progress in the tutorial.
// The Name field is the user ID again (for whatever reason).
PreferenceCategoryTutorialSteps = "tutorial_step"
// PreferenceCategoryAdvancedSettings has settings for the user's advanced settings.
// The Name field is the setting name. Possible values are:
// - "formatting"
// - "send_on_ctrl_enter"
// - "join_leave"
// - "unread_scroll_position"
// - "sync_drafts"
// - "feature_enabled_markdown_preview" <- deprecated in favor of "formatting"
PreferenceCategoryAdvancedSettings = "advanced_settings"
// PreferenceCategoryFlaggedPost is used to store the user's saved posts.
// The Name field is the post ID.
PreferenceCategoryFlaggedPost = "flagged_post"
// PreferenceCategoryFavoriteChannel is used to store the user's favorite channels to be
// shown in the sidebar. The Name field is the channel ID.
PreferenceCategoryFavoriteChannel = "favorite_channel"
// PreferenceCategorySidebarSettings is used to store the user's sidebar settings.
// The Name field is the setting name. (ie. PreferenceNameShowUnreadSection or PreferenceLimitVisibleDmsGms)
PreferenceCategorySidebarSettings = "sidebar_settings"
// PreferenceCategoryDisplaySettings is used to store the user's various display settings.
// The possible Name fields are:
// - PreferenceNameUseMilitaryTime
// - PreferenceNameCollapseSetting
// - PreferenceNameMessageDisplay
// - PreferenceNameCollapseConsecutive
// - PreferenceNameColorizeUsernames
// - PreferenceNameChannelDisplayMode
// - PreferenceNameNameFormat
PreferenceCategoryDisplaySettings = "display_settings"
// PreferenceCategorySystemNotice is used store system admin notices.
// Possible Name values are not defined here. It can be anything with the notice name.
PreferenceCategorySystemNotice = "system_notice"
// Deprecated: PreferenceCategoryLast is not used anymore.
PreferenceCategoryLast = "last"
// PreferenceCategoryCustomStatus is used to store the user's custom status preferences.
// Possible Name values are:
// - PreferenceNameRecentCustomStatuses
// - PreferenceNameCustomStatusTutorialState
// - PreferenceCustomStatusModalViewed
PreferenceCategoryCustomStatus = "custom_status"
// PreferenceCategoryNotifications is used to store the user's notification settings.
// Possible Name values are:
// - PreferenceNameEmailInterval
PreferenceCategoryNotifications = "notifications"
// Deprecated: PreferenceRecommendedNextSteps is not used anymore.
// Use PreferenceCategoryRecommendedNextSteps instead.
// PreferenceRecommendedNextSteps is actually a Category. The only possible
// Name vaule is PreferenceRecommendedNextStepsHide for now.
PreferenceRecommendedNextSteps = PreferenceCategoryRecommendedNextSteps
PreferenceCategoryRecommendedNextSteps = "recommended_next_steps"
// PreferenceCategoryTheme has the name for the team id where theme is set.
PreferenceCategoryTheme = "theme"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
PreferenceNameChannelDisplayMode = "channel_display_mode"
PreferenceNameCollapseSetting = "collapse_previews"
@ -30,27 +85,25 @@ const (
PreferenceNameColorizeUsernames = "colorize_usernames"
PreferenceNameNameFormat = "name_format"
PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceRecommendedNextSteps = "recommended_next_steps"
PreferenceCategorySystemNotice = "system_notice"
PreferenceNameShowUnreadSection = "show_unread_section"
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
PreferenceCategoryTheme = "theme"
// the name for theme props is the team id
PreferenceMaxLimitVisibleDmsGmsValue = 40
MaxPreferenceValueLength = 20000
PreferenceCategoryAuthorizedOAuthApp = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope
PreferenceCategoryLast = "last"
// Deprecated: PreferenceCategoryLastChannel is not used anymore.
PreferenceNameLastChannel = "channel"
// Deprecated: PreferenceCategoryLastTeam is not used anymore.
PreferenceNameLastTeam = "team"
PreferenceCategoryCustomStatus = "custom_status"
PreferenceNameRecentCustomStatuses = "recent_custom_statuses"
PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state"
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed"
PreferenceCategoryNotifications = "notifications"
PreferenceNameEmailInterval = "email_interval"
PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s
@ -62,9 +115,7 @@ const (
PreferenceEmailIntervalHourAsSeconds = "3600"
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
PreferenceMaxLimitVisibleDmsGmsValue = 40
MaxPreferenceValueLength = 20000
PreferenceNameRecommendedNextStepsHide = "hide"
)
type Preference struct {