* Migrate feature/wrangler to mono-repo

* Add wrangler files

* Fix linters, types, etc

* Fix snapshots

* Fix playwright

* Fix pipelines

* Fix more pipeline

* Fixes for pipelines

* More changes for pipeline

* Fix types

* Add support for a feature flag, but leave it defaulted on for spinwick usage for now

* Update snapshot

* fix js error when removing last value of multiselect, support CSV marshaling to string array for textsetting

* Fix linter

* Remove TODO

* Remove another TODO

* fix tests

* Fix i18n

* Add server tests

* Fix linter

* Fix linter

* Use proper icon for dot menu

* Update snapshot

* Add Cypress UI tests for various entrypoints to move thread modal, split SCSS out from forward post into its own thing

* clean up

* fix linter

* More cleanup

* Revert files to master

* Fix linter for e2e tests

* Make ForwardPostChannelSelect channel types configurable with a prop

* Add missing return

* Fixes from PR feedback

* First batch of PR Feedback

* Another batch of PR changes

* Fix linter

* Update snapshots

* Wrangler system messages are translated to each user's locale

* Initially translate Wrangler into system locale rather than initiating user

* More fixes for PR Feedback

* Fix some server tests

* More updates with master. Fixes around pipelines. Enforce Enterprise license on front/back end

* Add tests for dot_menu

* More pipeline fixes

* Fix e2etests prettier

* Update cypress tests, change occurrences of 'Wrangler' with 'Move Thread'

* Fix linter

* Remove enterprise lock

* A couple more occurrences of wrangler strings, and one more enterprise lock

* Fix server tests

* Fix i18n

* Fix e2e linter

* Feature flag shouldn't be on by default

* Enable move threads feature in smoke tests (#25657)

* enable move threads feature
* add @prod tag

* Fix move_thread_from_public_channel e2e test

* Fix e2e style

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
Nick Misasi
2023-12-11 15:27:34 -05:00
коммит произвёл GitHub
родитель bfb8320afd
Коммит f0a336ba07
56 изменённых файлов: 3107 добавлений и 16 удалений

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

@@ -44,6 +44,8 @@ func (api *API) InitPost() {
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods("DELETE")
api.BaseRoutes.Post.Handle("/move", api.APISessionRequired(moveThread)).Methods("POST")
}
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1128,6 +1130,83 @@ func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func moveThread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.Config().FeatureFlags.MoveThreadsEnabled {
c.Err = model.NewAppError("moveThread", "api.post.move_thread.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
var moveThreadParams model.MoveThreadParams
if jsonErr := json.NewDecoder(r.Body).Decode(&moveThreadParams); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
auditRec := c.MakeAuditRecord("moveThread", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameter(auditRec, "original_post_id", c.Params.PostId)
audit.AddEventParameter(auditRec, "to_channel_id", moveThreadParams.ChannelId)
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// If there are no configured PermittedWranglerRoles, skip the check
userHasRole := len(c.App.Config().WranglerSettings.PermittedWranglerRoles) == 0
for _, role := range c.App.Config().WranglerSettings.PermittedWranglerRoles {
if user.IsInRole(role) {
userHasRole = true
break
}
}
// Sysadmins are always permitted
if !userHasRole && !user.IsSystemAdmin() {
c.Err = model.NewAppError("moveThread", "api.post.move_thread.no_permission", nil, "", http.StatusForbidden)
return
}
userHasEmailDomain := len(c.App.Config().WranglerSettings.AllowedEmailDomain) == 0
for _, domain := range c.App.Config().WranglerSettings.AllowedEmailDomain {
if user.EmailDomain() == domain {
userHasEmailDomain = true
break
}
}
if !userHasEmailDomain && !user.IsSystemAdmin() {
c.Err = model.NewAppError("moveThread", "api.post.move_thread.no_permission", nil, fmt.Sprintf("User: %+v", user), http.StatusForbidden)
return
}
sourcePost, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), false)
if err != nil {
c.Err = err
if err.Id == "app.post.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessiblePostTime, "1")
}
return
}
err = c.App.MoveThread(c.AppContext, c.Params.PostId, sourcePost.ChannelId, moveThreadParams.ChannelId, user)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {

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

@@ -730,6 +730,239 @@ func TestCreatePostWithOutgoingHook_no_content_type(t *testing.T) {
})
}
func TestMoveThread(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_MOVETHREADSENABLED", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_MOVETHREADSENABLED")
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
ctx := context.Background()
basicUser1 := th.BasicUser
basicUser2 := th.BasicUser2
basicUser3 := th.CreateUser()
// Create a new public channel to move the post to
publicChannel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-public-channel",
DisplayName: "Test Public Channel",
Type: model.ChannelTypeOpen,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, publicChannel)
// Create a new private channel to move the post to
privateChannel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-private-channel",
DisplayName: "Test Private Channel",
Type: model.ChannelTypePrivate,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, privateChannel)
// Create a new direct message channel to move the post to
dmChannel, resp, err := client.CreateDirectChannel(ctx, basicUser1.Id, basicUser2.Id)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, dmChannel)
// Create a new group message channel to move the post to
gmChannel, resp, err := client.CreateGroupChannel(ctx, []string{basicUser1.Id, basicUser2.Id, basicUser3.Id})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, gmChannel)
t.Run("Move to public channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the public channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: publicChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to private channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the private channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: privateChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the private channel
posts, resp, err := client.GetPostsForChannel(ctx, privateChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved
require.Equal(t, 2, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to direct message channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the direct message channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: dmChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the direct message channel
posts, resp, err := client.GetPostsForChannel(ctx, dmChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 1 post, the post we moved
require.Equal(t, 1, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move to group message channel", func(t *testing.T) {
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Move the post to the group message channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: gmChannel.Id,
}
resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the post was moved to the group message channel
posts, resp, err := client.GetPostsForChannel(ctx, gmChannel.Id, 0, 100, "", true, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 1 post, the post we moved
require.Equal(t, 1, len(posts.Posts))
require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message)
})
t.Run("Move thread with more than one post", func(t *testing.T) {
// Create a new public channel to move the post to
pChannel, resp, err := client.CreateChannel(ctx, &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-public-channel2",
DisplayName: "Test Public Channel",
Type: model.ChannelTypeOpen,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, pChannel)
// Create a new post to use as the root post
rootPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "root post",
}
rootPost, resp, err = client.CreatePost(ctx, rootPost)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, rootPost)
// Create a new post to move
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post",
RootId: rootPost.Id,
}
newPost, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost)
// Create another post in the thread
post = &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test post 2",
RootId: rootPost.Id,
}
newPost2, resp, err := client.CreatePost(ctx, post)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, newPost2)
// Move the thread to the public channel
moveThreadParams := &model.MoveThreadParams{
ChannelId: pChannel.Id,
}
resp, err = client.MoveThread(ctx, rootPost.Id, moveThreadParams)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check that the thread was moved to the public channel
posts, resp, err := client.GetPostsForChannel(ctx, pChannel.Id, 0, 100, "", false, false)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, posts)
// There should be 3 posts, the system join message for the user who moved it joining the channel, and the two posts in the thread
// require.Equal(t, 3, len(posts.Posts))
fmt.Println(posts.Order)
for _, p := range posts.Order {
fmt.Println(posts.Posts[p].Id)
fmt.Println(posts.Posts[p].Message)
}
require.Equal(t, "This thread was moved from another channel", posts.Posts[posts.Order[0]].Message)
require.Equal(t, newPost2.Message, posts.Posts[posts.Order[1]].Message)
require.Equal(t, newPost.Message, posts.Posts[posts.Order[2]].Message)
require.Equal(t, rootPost.Message, posts.Posts[posts.Order[3]].Message)
})
}
func TestCreatePostPublic(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

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

@@ -398,6 +398,10 @@ type AppIface interface {
ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError
// validateMoveOrCopy performs validation on a provided post list to determine
// if all permissions are in place to allow the for the posts to be moved or
// copied.
ValidateMoveOrCopy(c request.CTX, wpl *model.WranglerPostList, originalChannel *model.Channel, targetChannel *model.Channel, user *model.User) error
AccountMigration() einterfaces.AccountMigrationInterface
ActivateMfa(userID, token string) *model.AppError
ActiveSearchBackend() string
@@ -478,6 +482,7 @@ type AppIface interface {
Config() *model.Config
ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError)
CopyFileInfos(rctx request.CTX, userID string, fileIDs []string) ([]string, *model.AppError)
CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError)
CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
CreateCommand(cmd *model.Command) (*model.Command, *model.AppError)
@@ -918,6 +923,7 @@ type AppIface interface {
MigrateIdLDAP(c request.CTX, toAttribute string) *model.AppError
MoveCommand(team *model.Team, command *model.Command) *model.AppError
MoveFile(oldPath, newPath string) *model.AppError
MoveThread(c request.CTX, postID string, sourceChannelID, channelID string, user *model.User) *model.AppError
NewPluginAPI(c request.CTX, manifest *model.Manifest) plugin.API
Notification() einterfaces.NotificationInterface
NotificationsLog() *mlog.Logger

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

@@ -1884,6 +1884,28 @@ func (a *OpenTracingAppLayer) CopyFileInfos(rctx request.CTX, userID string, fil
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CopyWranglerPostlist")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CopyWranglerPostlist(c, wpl, targetChannel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateBot")
@@ -12664,6 +12686,28 @@ func (a *OpenTracingAppLayer) MoveFile(oldPath string, newPath string) *model.Ap
return resultVar0
}
func (a *OpenTracingAppLayer) MoveThread(c request.CTX, postID string, sourceChannelID string, channelID string, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveThread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveThread(c, postID, sourceChannelID, channelID, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NewPluginAPI(c request.CTX, manifest *model.Manifest) plugin.API {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewPluginAPI")
@@ -18590,6 +18634,28 @@ func (a *OpenTracingAppLayer) ValidateDesktopToken(token string, expiryTime int6
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ValidateMoveOrCopy(c request.CTX, wpl *model.WranglerPostList, originalChannel *model.Channel, targetChannel *model.Channel, user *model.User) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateMoveOrCopy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ValidateMoveOrCopy(c, wpl, originalChannel, targetChannel, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels")

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

@@ -2349,3 +2349,255 @@ func (a *App) applyPostWillBeConsumedHook(post **model.Post) {
return true
}, plugin.MessagesWillBeConsumedID)
}
func makePostLink(siteURL, teamName, postID string) string {
return fmt.Sprintf("%s/%s/pl/%s", siteURL, teamName, postID)
}
// validateMoveOrCopy performs validation on a provided post list to determine
// if all permissions are in place to allow the for the posts to be moved or
// copied.
func (a *App) ValidateMoveOrCopy(c request.CTX, wpl *model.WranglerPostList, originalChannel *model.Channel, targetChannel *model.Channel, user *model.User) error {
if wpl.NumPosts() == 0 {
return errors.New("The wrangler post list contains no posts")
}
config := a.Config().WranglerSettings
switch originalChannel.Type {
case model.ChannelTypePrivate:
if !*config.MoveThreadFromPrivateChannelEnable {
return errors.New("Wrangler is currently configured to not allow moving posts from private channels")
}
case model.ChannelTypeDirect:
if !*config.MoveThreadFromDirectMessageChannelEnable {
return errors.New("Wrangler is currently configured to not allow moving posts from direct message channels")
}
case model.ChannelTypeGroup:
if !*config.MoveThreadFromGroupMessageChannelEnable {
return errors.New("Wrangler is currently configured to not allow moving posts from group message channels")
}
}
if !originalChannel.IsGroupOrDirect() && !targetChannel.IsGroupOrDirect() {
// DM and GM channels are "teamless" so it doesn't make sense to check
// the MoveThreadToAnotherTeamEnable config when dealing with those.
if !*config.MoveThreadToAnotherTeamEnable && targetChannel.TeamId != originalChannel.TeamId {
return errors.New("Wrangler is currently configured to not allow moving messages to different teams")
}
}
if *config.MoveThreadMaxCount != int64(0) && *config.MoveThreadMaxCount < int64(wpl.NumPosts()) {
return fmt.Errorf("the thread is %d posts long, but this command is configured to only move threads of up to %d posts", wpl.NumPosts(), *config.MoveThreadMaxCount)
}
_, appErr := a.GetChannelMember(c, targetChannel.Id, user.Id)
if appErr != nil {
return fmt.Errorf("channel with ID %s doesn't exist or you are not a member", targetChannel.Id)
}
_, appErr = a.GetChannelMember(c, originalChannel.Id, user.Id)
if appErr != nil {
return fmt.Errorf("channel with ID %s doesn't exist or you are not a member", originalChannel.Id)
}
return nil
}
func (a *App) CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError) {
var appErr *model.AppError
var newRootPost *model.Post
if wpl.ContainsFileAttachments() {
// The thread contains at least one attachment. To properly move the
// thread, the files will have to be re-uploaded. This is completed
// before any messages are moved.
// TODO: check number of files that need to be re-uploaded or file size?
c.Logger().Info("Wrangler is re-uploading file attachments",
mlog.String("file_count", fmt.Sprintf("%d", wpl.FileAttachmentCount)),
)
for _, post := range wpl.Posts {
var newFileIDs []string
var fileBytes []byte
var oldFileInfo, newFileInfo *model.FileInfo
for _, fileID := range post.FileIds {
oldFileInfo, appErr = a.GetFileInfo(c, fileID)
if appErr != nil {
return nil, appErr
}
fileBytes, appErr = a.GetFile(c, fileID)
if appErr != nil {
return nil, appErr
}
newFileInfo, appErr = a.UploadFile(c, fileBytes, targetChannel.Id, oldFileInfo.Name)
if appErr != nil {
return nil, appErr
}
newFileIDs = append(newFileIDs, newFileInfo.Id)
}
post.FileIds = newFileIDs
}
}
for i, post := range wpl.Posts {
var reactions []*model.Reaction
// Store reactions to be reapplied later.
reactions, appErr = a.GetReactionsForPost(post.Id)
if appErr != nil {
// Reaction-based errors are logged, but do not abort
c.Logger().Error("Failed to get reactions on original post")
}
newPost := post.Clone()
newPost = newPost.CleanPost()
newPost.ChannelId = targetChannel.Id
if i == 0 {
newPost, appErr = a.CreatePost(c, newPost, targetChannel, false, false)
if appErr != nil {
return nil, appErr
}
newRootPost = newPost.Clone()
} else {
newPost.RootId = newRootPost.Id
newPost, appErr = a.CreatePost(c, newPost, targetChannel, false, false)
if appErr != nil {
return nil, appErr
}
}
for _, reaction := range reactions {
reaction.PostId = newPost.Id
_, appErr = a.SaveReactionForPost(c, reaction)
if appErr != nil {
// Reaction-based errors are logged, but do not abort
c.Logger().Error("Failed to reapply reactions to post")
}
}
}
return newRootPost, nil
}
func (a *App) MoveThread(c request.CTX, postID string, sourceChannelID, channelID string, user *model.User) *model.AppError {
postListResponse, appErr := a.GetPostThread(postID, model.GetPostsOptions{}, user.Id)
if appErr != nil {
return model.NewAppError("getPostThread", "app.post.move_thread_command.error", nil, "postID="+postID+", "+"UserId="+user.Id+"", http.StatusBadRequest)
}
wpl := postListResponse.BuildWranglerPostList()
originalChannel, appErr := a.GetChannel(c, sourceChannelID)
if appErr != nil {
return appErr
}
targetChannel, appErr := a.GetChannel(c, channelID)
if appErr != nil {
return appErr
}
err := a.ValidateMoveOrCopy(c, wpl, originalChannel, targetChannel, user)
if err != nil {
return model.NewAppError("validateMoveOrCopy", "app.post.move_thread_command.error", nil, err.Error(), http.StatusBadRequest)
}
var targetTeam *model.Team
if targetChannel.IsGroupOrDirect() {
if !originalChannel.IsGroupOrDirect() {
targetTeam, appErr = a.GetTeam(originalChannel.TeamId)
}
} else {
targetTeam, appErr = a.GetTeam(targetChannel.TeamId)
}
if appErr != nil {
return appErr
}
if targetTeam == nil {
return model.NewAppError("validateMoveOrCopy", "app.post.move_thread_command.error", nil, "target team is nil", http.StatusBadRequest)
}
// Begin creating the new thread.
c.Logger().Info("Wrangler is moving a thread", mlog.String("user_id", user.Id), mlog.String("original_post_id", wpl.RootPost().Id), mlog.String("original_channel_id", originalChannel.Id))
// To simulate the move, we first copy the original messages(s) to the
// new channel and later delete the original messages(s).
newRootPost, appErr := a.CopyWranglerPostlist(c, wpl, targetChannel)
if appErr != nil {
return appErr
}
T, err := i18n.GetTranslationsBySystemLocale()
if err != nil {
return model.NewAppError("MoveThread", "app.post.move_thread_command.error", nil, err.Error(), http.StatusInternalServerError)
}
ephemeralPostProps := model.StringInterface{
"TranslationID": "app.post.move_thread.from_another_channel",
}
_, appErr = a.CreatePost(c, &model.Post{
UserId: user.Id,
Type: model.PostTypeWrangler,
RootId: newRootPost.Id,
ChannelId: channelID,
Message: T("app.post.move_thread.from_another_channel"),
Props: ephemeralPostProps,
}, targetChannel, false, false)
if appErr != nil {
return appErr
}
// Cleanup is handled by simply deleting the root post. Any comments/replies
// are automatically marked as deleted for us.
_, appErr = a.DeletePost(c, wpl.RootPost().Id, user.Id)
if appErr != nil {
return appErr
}
c.Logger().Info("Wrangler thread move complete", mlog.String("user_id", user.Id), mlog.String("new_post_id", newRootPost.Id), mlog.String("channel_id", channelID))
// Translate to the system locale, webapp will attempt to render in each user's specific locale (based on the TranslationID prop) before falling back on the initiating user's locale
ephemeralPostProps = model.StringInterface{}
msg := T("app.post.move_thread_command.direct_or_group.multiple_messages", model.StringInterface{"NumMessages": wpl.NumPosts()})
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.direct_or_group.multiple_messages"
if wpl.NumPosts() == 1 {
msg = T("app.post.move_thread_command.direct_or_group.one_message")
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.direct_or_group.one_message"
}
if targetChannel.TeamId != "" {
targetTeam, teamErr := a.GetTeam(targetChannel.TeamId)
if teamErr != nil {
return teamErr
}
targetName := targetTeam.Name
newPostLink := makePostLink(*a.Config().ServiceSettings.SiteURL, targetName, newRootPost.Id)
msg = T("app.post.move_thread_command.channel.multiple_messages", model.StringInterface{"NumMessages": wpl.NumPosts(), "Link": newPostLink})
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.channel.multiple_messages"
if wpl.NumPosts() == 1 {
msg = T("app.post.move_thread_command.channel.one_message", model.StringInterface{"Link": newPostLink})
ephemeralPostProps["TranslationID"] = "app.post.move_thread_command.channel.one_message"
}
ephemeralPostProps["MovedThreadPermalink"] = newPostLink
}
ephemeralPostProps["NumMessages"] = wpl.NumPosts()
_, appErr = a.CreatePost(c, &model.Post{
UserId: user.Id,
Type: model.PostTypeWrangler,
ChannelId: originalChannel.Id,
Message: msg,
Props: ephemeralPostProps,
}, originalChannel, false, false)
if appErr != nil {
return appErr
}
c.Logger().Info(msg)
return nil
}

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

@@ -3286,3 +3286,179 @@ func TestGetEditHistoryForPost(t *testing.T) {
require.Empty(t, edits)
})
}
func TestCopyWranglerPostlist(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// Create a post with a file attachment
fileBytes := []byte("file contents")
fileInfo, err := th.App.UploadFile(th.Context, fileBytes, th.BasicChannel.Id, "file.txt")
require.Nil(t, err)
post := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "test message",
UserId: th.BasicUser.Id,
FileIds: []string{fileInfo.Id},
}
rootPost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
require.Nil(t, err)
// Add a reaction to the post
reaction := &model.Reaction{
UserId: th.BasicUser.Id,
PostId: rootPost.Id,
EmojiName: "smile",
}
_, err = th.App.SaveReactionForPost(th.Context, reaction)
require.Nil(t, err)
// Copy the post to a new channel
targetChannel := &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-channel",
Type: model.ChannelTypeOpen,
}
targetChannel, err = th.App.CreateChannel(th.Context, targetChannel, false)
require.Nil(t, err)
wpl := &model.WranglerPostList{
Posts: []*model.Post{rootPost},
FileAttachmentCount: 1,
}
newRootPost, err := th.App.CopyWranglerPostlist(th.Context, wpl, targetChannel)
require.Nil(t, err)
// Check that the new post has the same message and file attachment
require.Equal(t, rootPost.Message, newRootPost.Message)
require.Len(t, newRootPost.FileIds, 1)
// Check that the new post has the same reaction
reactions, err := th.App.GetReactionsForPost(newRootPost.Id)
require.Nil(t, err)
require.Len(t, reactions, 1)
require.Equal(t, reaction.EmojiName, reactions[0].EmojiName)
}
func TestValidateMoveOrCopy(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings.MoveThreadFromPrivateChannelEnable = model.NewBool(true)
cfg.WranglerSettings.MoveThreadFromDirectMessageChannelEnable = model.NewBool(true)
cfg.WranglerSettings.MoveThreadFromGroupMessageChannelEnable = model.NewBool(true)
cfg.WranglerSettings.MoveThreadToAnotherTeamEnable = model.NewBool(true)
cfg.WranglerSettings.MoveThreadMaxCount = model.NewInt64(100)
})
t.Run("empty post list", func(t *testing.T) {
err := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{}, th.BasicChannel, th.BasicChannel, th.BasicUser)
require.Error(t, err)
require.Equal(t, "The wrangler post list contains no posts", err.Error())
})
t.Run("moving from private channel with MoveThreadFromPrivateChannelEnable disabled", func(t *testing.T) {
privateChannel := &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "private-channel",
Type: model.ChannelTypePrivate,
}
privateChannel, err := th.App.CreateChannel(th.Context, privateChannel, false)
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings.MoveThreadFromPrivateChannelEnable = model.NewBool(false)
})
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: privateChannel.Id}}}, privateChannel, th.BasicChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, "Wrangler is currently configured to not allow moving posts from private channels", e.Error())
})
t.Run("moving from direct channel with MoveThreadFromDirectMessageChannelEnable disabled", func(t *testing.T) {
directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
require.Nil(t, err)
require.NotNil(t, directChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings.MoveThreadFromDirectMessageChannelEnable = model.NewBool(false)
})
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: directChannel.Id}}}, directChannel, th.BasicChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, "Wrangler is currently configured to not allow moving posts from direct message channels", e.Error())
})
t.Run("moving from group channel with MoveThreadFromGroupMessageChannelEnable disabled", func(t *testing.T) {
groupChannel := &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "group-channel",
Type: model.ChannelTypeGroup,
}
groupChannel, err := th.App.CreateChannel(th.Context, groupChannel, false)
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings.MoveThreadFromGroupMessageChannelEnable = model.NewBool(false)
})
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: groupChannel.Id}}}, groupChannel, th.BasicChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, "Wrangler is currently configured to not allow moving posts from group message channels", e.Error())
})
t.Run("moving to different team with MoveThreadToAnotherTeamEnable disabled", func(t *testing.T) {
team := &model.Team{
Name: "testteam",
DisplayName: "testteam",
Type: model.TeamOpen,
}
targetTeam, err := th.App.CreateTeam(th.Context, team)
require.Nil(t, err)
require.NotNil(t, targetTeam)
targetChannel := &model.Channel{
TeamId: targetTeam.Id,
Name: "test-channel",
Type: model.ChannelTypeOpen,
}
targetChannel, err = th.App.CreateChannel(th.Context, targetChannel, false)
require.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.WranglerSettings.MoveThreadToAnotherTeamEnable = model.NewBool(false)
})
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, targetChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, "Wrangler is currently configured to not allow moving messages to different teams", e.Error())
})
t.Run("moving to channel user is not a member of", func(t *testing.T) {
targetChannel := &model.Channel{
TeamId: th.BasicTeam.Id,
Name: "test-channel",
Type: model.ChannelTypePrivate,
}
targetChannel, err := th.App.CreateChannel(th.Context, targetChannel, false)
require.Nil(t, err)
err = th.App.RemoveUserFromChannel(th.Context, th.BasicUser.Id, th.SystemAdminUser.Id, th.BasicChannel)
require.Nil(t, err)
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, targetChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, fmt.Sprintf("channel with ID %s doesn't exist or you are not a member", targetChannel.Id), e.Error())
})
t.Run("moving thread longer than MoveThreadMaxCount", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.WranglerSettings.MoveThreadMaxCount = 1
})
e := th.App.ValidateMoveOrCopy(th.Context, &model.WranglerPostList{Posts: []*model.Post{{ChannelId: th.BasicChannel.Id}, {ChannelId: th.BasicChannel.Id}}}, th.BasicChannel, th.BasicChannel, th.BasicUser)
require.Error(t, e)
require.Equal(t, "the thread is 2 posts long, but this command is configured to only move threads of up to 1 posts", e.Error())
})
}