[MM-59367] export: enable exporting thread followers for CRT (#27623)

Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2024-08-29 14:06:41 +02:00
коммит произвёл GitHub
родитель 3dc0e63c03
Коммит d5cc2eb2f6
17 изменённых файлов: 1430 добавлений и 63 удалений

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

@@ -607,6 +607,15 @@ func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer,
return nil, err
}
followers, err := a.buildThreadFollowers(ctx, post.Id)
if err != nil {
return nil, err
}
if len(followers) > 0 {
postLine.Post.ThreadFollowers = &followers
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
@@ -674,6 +683,21 @@ func (a *App) buildPostReplies(ctx request.CTX, postID string, withAttachments b
return replies, attachments, nil
}
func (a *App) buildThreadFollowers(_ request.CTX, postID string) ([]imports.ThreadFollowerImportData, *model.AppError) {
var followers []imports.ThreadFollowerImportData
threadFollowers, nErr := a.Srv().Store().Thread().GetThreadMembershipsForExport(postID)
if nErr != nil {
return nil, model.NewAppError("buildThreadFollowers", "app.thread.get_threadmembers_for_export.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, member := range threadFollowers {
followers = append(followers, *ImportFollowerFromThreadMember(member))
}
return followers, nil
}
func (a *App) BuildPostReactions(ctx request.CTX, postID string) (*[]ReactionImportData, *model.AppError) {
var reactionsOfPost []imports.ReactionImportData
@@ -978,6 +1002,16 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Wr
if len(postAttachments) > 0 {
postLine.DirectPost.Attachments = &postAttachments
}
followers, err := a.buildThreadFollowers(ctx, post.Id)
if err != nil {
return nil, err
}
if len(followers) > 0 {
postLine.DirectPost.ThreadFollowers = &followers
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return nil, err
}

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

@@ -342,3 +342,11 @@ func ImportLineFromScheme(scheme *model.Scheme, rolesMap map[string]*model.Role)
Scheme: data,
}
}
func ImportFollowerFromThreadMember(threadMember *model.ThreadMembershipForExport) *imports.ThreadFollowerImportData {
return &imports.ThreadFollowerImportData{
User: &threadMember.Username,
LastViewed: &threadMember.LastViewed,
UnreadMentions: &threadMember.UnreadMentions,
}
}

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

@@ -4,7 +4,9 @@
package app
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -16,6 +18,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app/imports"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
@@ -647,6 +650,105 @@ func TestExportDMPostWithSelf(t *testing.T) {
assert.Equal(t, th1.BasicUser.Username, (*posts[0].ChannelMembers)[0])
}
func TestExportPostsWithThread(t *testing.T) {
th1 := Setup(t).InitBasic()
defer th1.TearDown()
assertThreadFollowers := func(t *testing.T, b *bytes.Buffer, postCreateAt int64, userNames []string) {
scanner := bufio.NewScanner(b)
usersToAssert := make([]string, 0)
for scanner.Scan() {
var line imports.LineImportData
err := json.Unmarshal(scanner.Bytes(), &line)
require.NoError(t, err)
switch line.Type {
case "post":
postLine := line.Post
require.NotNil(t, postLine)
if postLine.CreateAt != nil && *postLine.CreateAt != postCreateAt {
continue
}
for _, follower := range *postLine.ThreadFollowers {
if follower.User == nil {
require.Fail(t, "follower.User is nil")
}
usersToAssert = append(usersToAssert, *follower.User)
}
case "direct_post":
postLine := line.DirectPost
require.NotNil(t, postLine)
if postLine.CreateAt != nil && *postLine.CreateAt != postCreateAt {
continue
}
for _, follower := range *postLine.ThreadFollowers {
if follower.User == nil {
require.Fail(t, "follower.User is nil")
}
usersToAssert = append(usersToAssert, *follower.User)
}
default:
continue
}
}
require.ElementsMatch(t, userNames, usersToAssert)
}
t.Run("Export thread followers for a thread (public channel)", func(t *testing.T) {
thread := th1.CreatePost(th1.BasicChannel)
_ = th1.CreatePostReply(thread)
appErr := th1.App.UpdateThreadFollowForUser(th1.BasicUser2.Id, th1.BasicTeam.Id, thread.Id, true)
require.Nil(t, appErr)
member1, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser.Id, thread.Id)
require.Nil(t, appErr)
require.NotNil(t, member1)
member2, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser2.Id, thread.Id)
require.Nil(t, appErr)
require.NotNil(t, member2)
var b bytes.Buffer
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
require.Nil(t, err)
assertThreadFollowers(t, &b, thread.CreateAt, []string{th1.BasicUser.Username, th1.BasicUser2.Username})
})
t.Run("Export thread followers for a thread (direct messages)", func(t *testing.T) {
dmc := th1.CreateDmChannel(th1.BasicUser2)
thread := th1.CreatePost(dmc)
_ = th1.CreatePostReply(thread)
appErr := th1.App.UpdateThreadFollowForUser(th1.BasicUser2.Id, th1.BasicTeam.Id, thread.Id, true)
require.Nil(t, appErr)
member1, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser.Id, thread.Id)
require.Nil(t, appErr)
require.NotNil(t, member1)
member2, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser2.Id, thread.Id)
require.Nil(t, appErr)
require.NotNil(t, member2)
var b bytes.Buffer
err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
require.Nil(t, err)
assertThreadFollowers(t, &b, thread.CreateAt, []string{th1.BasicUser.Username, th1.BasicUser2.Username})
})
}
func TestBulkExport(t *testing.T) {
th := Setup(t)
testsDir, _ := fileutils.FindDir("tests")

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

@@ -477,6 +477,26 @@ func (th *TestHelper) CreateMessagePost(channel *model.Channel, message string)
return post
}
func (th *TestHelper) CreatePostReply(root *model.Post) *model.Post {
id := model.NewId()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: root.ChannelId,
RootId: root.Id,
Message: "message_" + id,
CreateAt: model.GetMillis() - 10000,
}
ch, err := th.App.GetChannel(th.Context, root.ChannelId)
if err != nil {
panic(err)
}
if post, err = th.App.CreatePost(th.Context, post, ch, false, true); err != nil {
panic(err)
}
return post
}
func (th *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
_, err := th.App.JoinUserToTeam(th.Context, team, user, "")
if err != nil {

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

@@ -1567,11 +1567,13 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
threadMembersToCreateMap = map[string][]*model.ThreadMembership{}
threadMembersToOverwriteList = []*model.ThreadMembership{}
)
for _, line := range lines {
@@ -1615,6 +1617,18 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo
if line.Post.IsPinned != nil {
post.IsPinned = *line.Post.IsPinned
}
if line.Post.ThreadFollowers != nil {
threadMemberships, lineNumber, err := a.extractThreadMembers(&line, users, post)
if err != nil {
return lineNumber, err
}
if post.Id == "" {
threadMembersToCreateMap[getPostStrID(post)] = threadMemberships
} else {
threadMembersToOverwriteList = append(threadMembersToOverwriteList, threadMemberships...)
}
}
fileIDs := a.uploadAttachments(rctx, line.Post.Attachments, post, team.Id, extractContent)
for _, fileID := range post.FileIds {
@@ -1634,11 +1648,13 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
// Tip: the post ID is getting populated after the post is saved, if it's a new post. Otherwise, it's already set.
postsWithData = append(postsWithData, postAndData{post: post, postData: line.Post, team: team, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, nErr := a.Srv().Store().Post().SaveMultiple(postsForCreateList); nErr != nil {
_, idx, nErr := a.Srv().Store().Post().SaveMultiple(postsForCreateList)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
@@ -1659,6 +1675,36 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo
}
return 0, retErr
}
var membersToCreate []*model.ThreadMembership
for _, post := range postsForCreateList {
members, ok := threadMembersToCreateMap[getPostStrID(post)]
if !ok {
continue
}
for _, member := range members {
if post.Id == "" {
appErr := model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(errors.New("post id cannot be empty"))
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, appErr
}
return 0, appErr
}
member.PostId = post.Id
}
membersToCreate = append(membersToCreate, members...)
}
// we have an assumption here is that all these memberships should be brand new because the corresponding posts
// do not exist in the target until the import.
if _, err := a.Srv().Store().Thread().SaveMultipleMemberships(membersToCreate); err != nil {
// we don't know the line number of the post that caused the error
// so we return 0. But at this stage, it's unlikely to receive an error
// due to the thread member itself, most likely it's due to the DB connection etc.
return 0, model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
@@ -1671,6 +1717,15 @@ func (a *App) importMultiplePostLines(rctx request.CTX, lines []imports.LineImpo
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Update thread memberships for posts that were overwritten. Here some of the memberships
// can be brand new, needs to be updated or an older membership should not get updated.
// MaintainMembership method has some logic within to handle those decisions. Unfortunately
// some application code leaked to the store layer here, which should be revisited when there
// is resource (eg. time, human or maybe AI).
if _, sErr := a.Srv().Store().Thread().MaintainMultipleFromImport(threadMembersToOverwriteList); sErr != nil {
return 0, model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(sErr)
}
for _, postWithData := range postsWithData {
postWithData := postWithData
if postWithData.postData.FlaggedBy != nil {
@@ -2008,11 +2063,13 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
threadMembersToCreateMap = map[string][]*model.ThreadMembership{}
threadMembersToOverwriteList = []*model.ThreadMembership{}
)
for _, line := range lines {
@@ -2077,6 +2134,18 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li
if line.DirectPost.IsPinned != nil {
post.IsPinned = *line.DirectPost.IsPinned
}
if line.DirectPost.ThreadFollowers != nil {
threadMemberships, lineNumber, err := a.extractThreadMembers(&line, users, post)
if err != nil {
return lineNumber, err
}
if post.Id == "" {
threadMembersToCreateMap[getPostStrID(post)] = threadMemberships
} else {
threadMembersToOverwriteList = append(threadMembersToOverwriteList, threadMemberships...)
}
}
fileIDs := a.uploadAttachments(rctx, line.DirectPost.Attachments, post, "noteam", extractContent)
for _, fileID := range post.FileIds {
@@ -2121,7 +2190,33 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li
}
return 0, retErr
}
var membersToCreate []*model.ThreadMembership
for _, post := range postsForCreateList {
members, ok := threadMembersToCreateMap[getPostStrID(post)]
if !ok {
continue
}
for _, member := range members {
if post.Id == "" {
appErr := model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(errors.New("post id cannot be empty"))
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, appErr
}
return 0, appErr
}
member.PostId = post.Id
}
membersToCreate = append(membersToCreate, members...)
}
if _, err := a.Srv().Store().Thread().SaveMultipleMemberships(membersToCreate); err != nil {
return 0, model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
@@ -2132,6 +2227,10 @@ func (a *App) importMultipleDirectPostLines(rctx request.CTX, lines []imports.Li
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if _, sErr := a.Srv().Store().Thread().MaintainMultipleFromImport(threadMembersToOverwriteList); sErr != nil {
return 0, model.NewAppError("importMultiplePostLines", "app.post.save.thread_membership.app_error", nil, "", http.StatusInternalServerError).Wrap(sErr)
}
for _, postWithData := range postsWithData {
if postWithData.directPostData.FlaggedBy != nil {
var preferences model.Preferences
@@ -2240,3 +2339,48 @@ func (a *App) importEmoji(rctx request.CTX, data *imports.EmojiImportData, dryRu
return nil
}
func (a *App) extractThreadMembers(line *imports.LineImportWorkerData, users map[string]*model.User, post *model.Post) ([]*model.ThreadMembership, int, *model.AppError) {
threadMemberships := []*model.ThreadMembership{}
var importedFollowers []imports.ThreadFollowerImportData
if line.Post != nil {
importedFollowers = *line.Post.ThreadFollowers
} else if line.DirectPost != nil {
importedFollowers = *line.DirectPost.ThreadFollowers
}
participants := make([]*model.User, len(importedFollowers))
for i, member := range importedFollowers {
user, ok := users[strings.ToLower(*member.User)]
if !ok {
// maybe it's a user on target instance but not in the import data.
// This is a rare case, but we need to or can to handle it.
// alternatively, we can continue and discard this follower as maybe they
// were deleted.
var uErr error
user, uErr = a.Srv().Store().User().GetByUsername(*member.User)
if uErr != nil {
return nil, line.LineNumber, model.NewAppError("importMultiplePostLines", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest).Wrap(uErr)
}
}
membership := &model.ThreadMembership{
PostId: post.Id, // empty if it's a new post, will set later while inserting to the DB.
UserId: user.Id,
Following: true,
}
if member.LastViewed != nil {
membership.LastViewed = *member.LastViewed
}
if member.UnreadMentions != nil {
membership.UnreadMentions = *member.UnreadMentions
}
// We only need the user ID to update the thread.
participants[i] = &model.User{Id: user.Id}
threadMemberships = append(threadMemberships, membership)
}
post.Participants = participants
return threadMemberships, 0, nil
}

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

@@ -11,6 +11,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -2103,7 +2104,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCount, 0, team.Id)
// Try adding a valid post in apply mode.
time := model.GetMillis()
createAt := model.GetMillis()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
@@ -2111,7 +2112,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
Channel: &channelName,
User: &username,
Message: ptrStr("Message"),
CreateAt: &time,
CreateAt: &createAt,
},
},
LineNumber: 1,
@@ -2122,7 +2123,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCount, 1, team.Id)
// Check the post values.
posts, nErr := th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, time)
posts, nErr := th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, createAt)
require.NoError(t, nErr)
require.Len(t, posts, 1, "Unexpected number of posts found.")
@@ -2139,7 +2140,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
Channel: &channelName,
User: &username,
Message: ptrStr("Message"),
CreateAt: &time,
CreateAt: &createAt,
},
},
LineNumber: 1,
@@ -2150,7 +2151,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCount, 1, team.Id)
// Check the post values.
posts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, time)
posts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, createAt)
require.NoError(t, nErr)
require.Len(t, posts, 1, "Unexpected number of posts found.")
@@ -2160,7 +2161,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
require.False(t, postBool, "Post properties not as expected")
// Save the post with a different time.
newTime := time + 1
newTime := createAt + 1
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
@@ -2186,7 +2187,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
Channel: &channelName,
User: &username,
Message: ptrStr("Message 2"),
CreateAt: &time,
CreateAt: &createAt,
},
},
LineNumber: 1,
@@ -2197,7 +2198,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
AssertAllPostsCount(t, th.App, initialPostCount, 3, team.Id)
// Test with hashtags
hashtagTime := time + 2
hashtagTime := createAt + 2
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
@@ -2505,7 +2506,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
Channel: &channelName,
User: &username,
Message: ptrStr("another message"),
CreateAt: &time,
CreateAt: &createAt,
},
},
LineNumber: 1,
@@ -2517,7 +2518,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
Channel: &channelName,
User: &username,
Message: ptrStr("another message"),
CreateAt: &time,
CreateAt: &createAt,
},
},
LineNumber: 1,
@@ -2554,6 +2555,166 @@ func TestImportimportMultiplePostLines(t *testing.T) {
// Posts should be added to the right team
AssertAllPostsCount(t, th.App, initialPostCountForTeam2, 1, team2.Id)
AssertAllPostsCount(t, th.App, initialPostCount, 15, team.Id)
t.Run("Importing a post with a thread", 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: ptrStr("Thread Message"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: &user.Username,
LastViewed: ptrInt64(model.GetMillis()),
}, {
User: &user2.Username,
LastViewed: ptrInt64(model.GetMillis()),
}},
},
},
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))
followers, err := th.App.Srv().Store().Thread().GetThreadFollowers(resultPosts[0].Id, true)
require.NoError(t, err)
assert.ElementsMatch(t, []string{user.Id, user2.Id}, followers)
})
t.Run("Importing a post with a non existent follower", 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: ptrStr("Thread Message"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: &user.Username,
LastViewed: ptrInt64(model.GetMillis()),
}, {
User: ptrStr("invalid.user"),
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.NotNil(t, err)
require.Equal(t, 1, errLine)
})
t.Run("Importing a post with a non existent follower", func(t *testing.T) {
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: ptrStr("Thread Message"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: &user.Username,
LastViewed: ptrInt64(model.GetMillis()),
}, {
User: ptrStr("invalid.user"),
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.NotNil(t, err)
require.Equal(t, 1, errLine)
})
t.Run("Importing a post with new followers", func(t *testing.T) {
importCreate := time.Now().Add(-5 * time.Minute).UnixMilli()
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &username,
Message: ptrStr("Hello"),
CreateAt: ptrInt64(importCreate),
},
},
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))
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
Post: &imports.PostImportData{
Team: &teamName,
Channel: &channelName,
User: &user.Username,
Message: ptrStr("Hello"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: &user.Username,
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: &user.Username,
LastViewed: ptrInt64(model.GetMillis()),
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
followers, err := th.App.Srv().Store().Thread().GetThreadFollowers(resultPosts[0].Id, true)
require.NoError(t, err)
assert.ElementsMatch(t, []string{user.Id}, followers)
})
}
func TestImportImportPost(t *testing.T) {
@@ -3890,6 +4051,109 @@ func TestImportImportDirectPost(t *testing.T) {
require.True(t, post.IsPinned)
})
t.Run("Importing a direct post with a thread", func(t *testing.T) {
// Create a thread.
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
data := imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
DirectPost: &imports.DirectPostImportData{
ChannelMembers: &[]string{
th.BasicUser.Username,
th.BasicUser2.Username,
},
User: ptrStr(th.BasicUser.Username),
Message: ptrStr("Thread Message"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: ptrStr(th.BasicUser.Username),
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: ptrStr(th.BasicUser.Username),
LastViewed: ptrInt64(model.GetMillis()),
}, {
User: ptrStr(th.BasicUser2.Username),
LastViewed: ptrInt64(model.GetMillis()),
}},
},
},
LineNumber: 1,
}
errLine, err := th.App.importMultipleDirectPostLines(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))
followers, nErr := th.App.Srv().Store().Thread().GetThreadFollowers(resultPosts[0].Id, true)
require.NoError(t, nErr)
assert.ElementsMatch(t, []string{th.BasicUser.Id, th.BasicUser2.Id}, followers)
})
t.Run("Importing a direct post with new followers", func(t *testing.T) {
importCreate := time.Now().Add(-5 * time.Minute).UnixMilli()
data := imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
DirectPost: &imports.DirectPostImportData{
ChannelMembers: &[]string{
th.BasicUser.Username,
th.BasicUser2.Username,
},
User: ptrStr(th.BasicUser.Username),
Message: ptrStr("Hello"),
CreateAt: ptrInt64(importCreate),
},
},
LineNumber: 1,
}
errLine, err := th.App.importMultipleDirectPostLines(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))
data = imports.LineImportWorkerData{
LineImportData: imports.LineImportData{
DirectPost: &imports.DirectPostImportData{
ChannelMembers: &[]string{
th.BasicUser.Username,
th.BasicUser2.Username,
},
User: ptrStr(th.BasicUser.Username),
Message: ptrStr("Hello"),
CreateAt: ptrInt64(importCreate),
Replies: &[]imports.ReplyImportData{{
User: ptrStr(th.BasicUser.Username),
Message: ptrStr("Reply"),
CreateAt: ptrInt64(model.GetMillis()),
}},
ThreadFollowers: &[]imports.ThreadFollowerImportData{{
User: ptrStr(th.BasicUser.Username),
LastViewed: ptrInt64(model.GetMillis()),
}},
},
},
LineNumber: 1,
}
errLine, err = th.App.importMultipleDirectPostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
require.Nil(t, err)
require.Equal(t, 0, errLine)
followers, nErr := th.App.Srv().Store().Thread().GetThreadFollowers(resultPosts[0].Id, true)
require.NoError(t, nErr)
assert.ElementsMatch(t, []string{th.BasicUser.Id}, followers)
})
// ------------------ Group Channel -------------------------
// Create the GROUP channel.

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

@@ -187,6 +187,8 @@ type PostImportData struct {
Replies *[]ReplyImportData `json:"replies,omitempty"`
Attachments *[]AttachmentImportData `json:"attachments,omitempty"`
IsPinned *bool `json:"is_pinned,omitempty"`
ThreadFollowers *[]ThreadFollowerImportData `json:"thread_followers,omitempty"`
}
type DirectChannelImportData struct {
@@ -213,6 +215,8 @@ type DirectPostImportData struct {
Replies *[]ReplyImportData `json:"replies"`
Attachments *[]AttachmentImportData `json:"attachments"`
IsPinned *bool `json:"is_pinned,omitempty"`
ThreadFollowers *[]ThreadFollowerImportData `json:"thread_followers,omitempty"`
}
type SchemeImportData struct {
@@ -255,3 +259,11 @@ type ComparablePreference struct {
Category string
Name string
}
type ThreadFollowerImportData struct {
// User is the username of the follower. It's the general convention
// for import data types to name it as user for the username.
User *string `json:"user"`
LastViewed *int64 `json:"last_viewed,omitempty"`
UnreadMentions *int64 `json:"unread_mentions,omitempty"`
}