Content flagging notifications (#34092)

* linter fix

* WIP

* sent post flagging confirmation message

* fixed i18n nissues

* fixed i18n nissues

* CI

* WIP

* WIP

* Added API call

* test: add test for Client4.flagPost API call in FlagPostModal

* fix: remove userEvent.setup() from flag post modal test

* test: wrap submit button click in act for proper state updates

* Updated tests

* lint fix

* Updated test

* fix: reset contentFlaggingGroupId for test isolation in content flagging tests

* removed cached group ID

* removed debug log

* CI

* Updated to allow special characters in comments

* Handled empty comment

* Created getContentFlaggingFields API

* created getPostPropertyValues API

* Used finally

* WIP

* Created useContentFlaggingFields hook

* WIP

* WIP

* Added option to retain data for reviewers

* Displayed deleted post's preview

* DIsplayed all properties

* Adding field name i18n

* WIP - managing i18n able texts

* Finished displaying all fields

* Manual cleanup

* lint fixes

* team role filter logic fix

* Fixed tests

* created new API to fetch flagged posts

* lint fix

* Added new client methods

* test: add comprehensive tests for content flagging APIs

* Added new API tests

* fixed openapi spec

* Fixed DataSpillageReport tests

* Fixed PostMarkdown test

* Fixed PostPreviewPropertyRenderer test

* Added metadata to card renderer

* test fixes

* Added no comment placeholder

* Added view detail button

* Created RemoveFlaggedMessageConfirmationModal modal

* Added key and remove flag request modal

* IMplemented delete flagged post

* Handled edge cases of deleting flagged post

* keep message

* UI integration

* Added WS event for post report update and handled deleted files of flagged post

* Added error handling in keep/remove forms

* i18n fixes

* Fixed test

* Updated OpenAPI specs

* fixed types

* fixed types

* refactoring

* refactor: improve test mocking for data spillage report component

* test mock updates

* Fixed tests

* Updated reducer

* not resetting mocks

* Added migrations for content flagging tables

* Created new structure

* review fixes

* Used correct ot name

* WIP

* review fixes

* review fixes

* Added new property translations

* CI

* CI

* CI

* Improved test

* fixed test

* CI

* New UI component

* WIP

* Updated settings APIs

* cached DB data

* used cached reviewer data

* Updated tests

* Lint fixes

* test: add tests for saveContentFlaggingSettings and getContentFlaggingSettings APIs

* test fix

* test: add tests for SaveContentFlaggingConfig and GetContentFlaggingConfigReviewerIDs

* Updated tests

* test: add content flagging test for local cache layer

* test: add comprehensive tests for content flagging store cache

* Updated tests

* lint fix

* Updated mobile text

* Added content flagging SQL store mocks

* Added API specs for new APIs

* fixed tests

* feat: add TestContentFlaggingStore function for content flagging store testing

* feat: add comprehensive tests for content flagging store

* Added SQL store tests

* test: add content flagging test for local cache layer

* test: add tests for content flagging store caching

* Added cache layer tests

* Updated tests

* Fixed

* Handled JSON error

* fixes

* fixes

* Fixed retry layer test

* fixerdf i18n

* Fixed test

* CI

* building index concurrently

* CI

* fixed a test

* CI

* cleanup

* Implemented reviewer search API

* feat: add tests for SearchCommonContentFlaggingReviewers and SearchTeamContentFlaggingReviewers

* Added store tests

* test: add comprehensive tests for SearchReviewers function

* feat: add comprehensive tests for searchReviewers endpoint

* API tests

* Integrate flag post api (#33798)

* WIP

* WIP

* Added API call

* test: add test for Client4.flagPost API call in FlagPostModal

* fix: remove userEvent.setup() from flag post modal test

* test: wrap submit button click in act for proper state updates

* Updated tests

* lint fix

* CI

* Updated to allow special characters in comments

* Handled empty comment

* Used finally

* CI

* Fixed test

* Spillage card integration (#33832)

* Created getContentFlaggingFields API

* created getPostPropertyValues API

* WIP

* Created useContentFlaggingFields hook

* WIP

* WIP

* Added option to retain data for reviewers

* Displayed deleted post's preview

* DIsplayed all properties

* Adding field name i18n

* WIP - managing i18n able texts

* Finished displaying all fields

* Manual cleanup

* lint fixes

* team role filter logic fix

* Fixed tests

* created new API to fetch flagged posts

* lint fix

* Added new client methods

* test: add comprehensive tests for content flagging APIs

* Added new API tests

* fixed openapi spec

* Fixed DataSpillageReport tests

* Fixed PostMarkdown test

* Fixed PostPreviewPropertyRenderer test

* Added metadata to card renderer

* test fixes

* Added no comment placeholder

* Fixed test

* refactor: improve test mocking for data spillage report component

* test mock updates

* Updated reducer

* not resetting mocks

* WIP

* review fixes

* CI

* Fixed

* fixes

* Content flagging actions implementation (#33852)

* Added view detail button

* Created RemoveFlaggedMessageConfirmationModal modal

* Added key and remove flag request modal

* IMplemented delete flagged post

* Handled edge cases of deleting flagged post

* keep message

* UI integration

* Added WS event for post report update and handled deleted files of flagged post

* Added error handling in keep/remove forms

* i18n fixes

* Updated OpenAPI specs

* fixed types

* fixed types

* refactoring

* Fixed tests

* review fixes

* Added new property translations

* Improved test

* fixed test

* CI

* fixes

* CI

* fixed a test

* fixed  abad commit

* CI

* WIP

* IMplemented assign reviewer API

* Display reviewers

* Review fixes

* UI integration

* lint fix

* Added API docs

* test: add comprehensive tests for assignFlaggedPostReviewer function

* test: add comprehensive tests for AssignFlaggedPostReviewer

* Added tests

* Fixed test

* Sequential tests

* minor improvemenmts

* WIP

* Added keep/delete message notifications

* refactor: update AssignFlaggedPostReviewer method signature to include context

* test: add tests for getReviewerPostsForFlaggedPost and postReviewerMessage

* lint fixes

* handled reviewer updates

* Handled preference

* Implemented notifications

* test: add comprehensive tests for content flagging notification functions

* refactor: Replace th.UpdateConfig with SaveContentFlaggingConfig in tests

* test: add test case for content flagging with string comparison

* refactor: simplify content flagging test config setup

* refactor: Update content flagging notification settings types in test cases

* refactor: Update content flagging tests to use exact message matching

* Added tests

* lint fixes

* review fixes

* Fixed a test

* Fixed a test

* review fixes

* Reduced code duplication

* Refactored tests to reduce duplication

* review fixes

* lint fix

* Review fixes

* WIP

* test: add comprehensive tests for sendFlaggedPostRemovalNotification and sendKeepFlaggedPostNotification

* Updated tests

* review fixes

* Updated logs

* i18n fixes
This commit is contained in:
Harshil Sharma
2025-10-15 09:02:14 +05:30
committed by GitHub
parent 144face19f
commit 220dbd89ce
3 changed files with 654 additions and 188 deletions
+234 -41
View File
@@ -138,7 +138,7 @@ func (a *App) FlagPost(rctx request.CTX, post *model.Post, teamId, reportingUser
_, err = a.Srv().propertyService.CreatePropertyValues(propertyValues)
if err != nil {
return model.NewAppError("FlagPostForContentReview", "app.content_flagging.create_property_values.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("FlagPost", "app.content_flagging.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
contentReviewBot, appErr := a.getContentReviewBot(rctx)
@@ -149,7 +149,7 @@ func (a *App) FlagPost(rctx request.CTX, post *model.Post, teamId, reportingUser
if *a.Config().ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent {
_, appErr = a.DeletePost(rctx, post.Id, contentReviewBot.UserId)
if appErr != nil {
return model.NewAppError("FlagPostForContentReview", "app.content_flagging.delete_post.app_error", nil, appErr.Error(), http.StatusInternalServerError).Wrap(appErr)
return appErr
}
}
@@ -165,13 +165,19 @@ func (a *App) FlagPost(rctx request.CTX, post *model.Post, teamId, reportingUser
}
})
a.Srv().Go(func() {
if appErr := a.sendFlagPostNotification(rctx, post); appErr != nil {
rctx.Logger().Error("Failed to send flag post notification", mlog.Err(appErr), mlog.String("post_id", post.Id))
}
})
return a.sendContentFlaggingConfirmationMessage(rctx, reportingUserId, post.UserId, post.ChannelId)
}
func (a *App) ContentFlaggingGroupId() (string, *model.AppError) {
group, err := a.Srv().propertyService.GetPropertyGroup(model.ContentFlaggingGroupName)
if err != nil {
return "", model.NewAppError("getContentFlaggingGroupId", "app.content_flagging.get_group.error", nil, err.Error(), http.StatusInternalServerError)
return "", model.NewAppError("getContentFlaggingGroupId", "app.content_flagging.get_group.error", nil, "", http.StatusInternalServerError)
}
return group.ID, nil
}
@@ -414,7 +420,7 @@ func (a *App) getAllUsersInTeamForRoles(teamId string, systemRoles, teamRoles []
additionalReviewers, err := utils.Pager(fetchFunc, options.PerPage)
if err != nil {
return nil, model.NewAppError("getReviewersForTeam", "app.content_flagging.get_users_in_team.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return nil, model.NewAppError("getAllUsersInTeamForRoles", "app.content_flagging.get_users_in_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return additionalReviewers, nil
@@ -498,7 +504,7 @@ func (a *App) PermanentDeleteFlaggedPost(rctx request.CTX, actionRequest *model.
statusValue := strings.Trim(string(status.Value), `"`)
if statusValue != model.ContentFlaggingStatusPending && statusValue != model.ContentFlaggingStatusAssigned {
return model.NewAppError("removeFlaggedPost", "api.content_flagging.error.post_not_in_progress", nil, "", http.StatusBadRequest)
return model.NewAppError("PermanentlyRemoveFlaggedPost", "api.content_flagging.error.post_not_in_progress", nil, "", http.StatusBadRequest)
}
editHistories, appErr := a.GetEditHistoryForPost(flaggedPost.Id)
@@ -539,7 +545,7 @@ func (a *App) PermanentDeleteFlaggedPost(rctx request.CTX, actionRequest *model.
scrubPost(flaggedPost)
_, err := a.Srv().Store().Post().Overwrite(rctx, flaggedPost)
if err != nil {
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.permanently_delete.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.permanently_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
groupId, appErr := a.ContentFlaggingGroupId()
@@ -578,13 +584,13 @@ func (a *App) PermanentDeleteFlaggedPost(rctx request.CTX, actionRequest *model.
_, err = a.Srv().propertyService.CreatePropertyValues(propertyValues)
if err != nil {
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.create_property_values.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
status.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = a.Srv().propertyService.UpdatePropertyValue(groupId, status)
if err != nil {
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.permanently_delete.update_property_value.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.permanently_delete.update_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Go(func() {
@@ -601,10 +607,7 @@ func (a *App) PermanentDeleteFlaggedPost(rctx request.CTX, actionRequest *model.
})
a.Srv().Go(func() {
postErr := a.postDeletePostReviewerMessage(rctx, flaggedPost.Id, reviewerId, actionRequest.Comment, groupId)
if postErr != nil {
rctx.Logger().Error("Failed to post delete post reviewer message after permanently removing flagged post", mlog.Err(postErr), mlog.String("post_id", flaggedPost.Id))
}
a.sendFlaggedPostRemovalNotification(rctx, flaggedPost, reviewerId, actionRequest.Comment, groupId)
})
return nil
@@ -621,7 +624,7 @@ func (a *App) KeepFlaggedPost(rctx request.CTX, actionRequest *model.FlagContent
statusValue := strings.Trim(string(status.Value), `"`)
if statusValue != model.ContentFlaggingStatusPending && statusValue != model.ContentFlaggingStatusAssigned {
return model.NewAppError("removeFlaggedPost", "api.content_flagging.error.post_not_in_progress", nil, "", http.StatusBadRequest)
return model.NewAppError("KeepFlaggedPost", "api.content_flagging.error.post_not_in_progress", nil, "", http.StatusBadRequest)
}
if flaggedPost.DeleteAt > 0 {
@@ -630,12 +633,12 @@ func (a *App) KeepFlaggedPost(rctx request.CTX, actionRequest *model.FlagContent
flaggedPost.PreCommit()
_, err := a.Srv().Store().Post().Overwrite(rctx, flaggedPost)
if err != nil {
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.keep_post.undelete.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.keep_post.undelete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().Store().FileInfo().RestoreForPostByIds(rctx, flaggedPost.Id, flaggedPost.FileIds)
if err != nil {
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.restore_file_info.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.restore_file_info.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
@@ -683,13 +686,13 @@ func (a *App) KeepFlaggedPost(rctx request.CTX, actionRequest *model.FlagContent
_, err = a.Srv().propertyService.CreatePropertyValues(propertyValues)
if err != nil {
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.create_property_values.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
status.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = a.Srv().propertyService.UpdatePropertyValue(groupId, status)
if err != nil {
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.content_flagging.keep_post.status_update.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("KeepFlaggedPost", "app.content_flagging.keep_post.status_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Go(func() {
@@ -713,12 +716,16 @@ func (a *App) KeepFlaggedPost(rctx request.CTX, actionRequest *model.FlagContent
a.invalidateCacheForChannelPosts(flaggedPost.ChannelId)
a.Srv().Go(func() {
postErr := a.postKeepPostReviewerMessage(rctx, flaggedPost.Id, reviewerId, actionRequest.Comment, groupId)
_, postErr := a.postKeepPostReviewerMessage(rctx, flaggedPost.Id, reviewerId, actionRequest.Comment, groupId)
if postErr != nil {
rctx.Logger().Error("Failed to post keep post reviewer message after retaining flagged post", mlog.Err(postErr), mlog.String("post_id", flaggedPost.Id))
}
})
a.Srv().Go(func() {
a.sendKeepFlaggedPostNotification(rctx, flaggedPost, reviewerId, actionRequest.Comment, groupId)
})
return nil
}
@@ -739,7 +746,7 @@ func (a *App) publishContentFlaggingReportUpdateEvent(targetId, teamId string, p
bytes, err := json.Marshal(propertyValues)
if err != nil {
return model.NewAppError("publishContentFlaggingReportUpdateEvent", "app.content_flagging.marshal_property_values.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("publishContentFlaggingReportUpdateEvent", "app.content_flagging.marshal_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, userId := range reviewersUserIDs {
@@ -788,7 +795,7 @@ func (a *App) clearContentFlaggingConfigCache() {
func (a *App) GetContentFlaggingConfigReviewerIDs() (*model.ReviewerIDsSettings, *model.AppError) {
reviewerSettings, err := a.Srv().Store().ContentFlagging().GetReviewerSettings()
if err != nil {
return nil, model.NewAppError("GetContentFlaggingConfig", "app.content_flagging.get_reviewer_settings.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return nil, model.NewAppError("GetContentFlaggingConfigReviewerIDs", "app.content_flagging.get_reviewer_settings.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return reviewerSettings, nil
@@ -896,19 +903,19 @@ func (a *App) AssignFlaggedPostReviewer(rctx request.CTX, flaggedPostId, flagged
assigneePropertyValue, err := a.Srv().propertyService.UpsertPropertyValue(assigneePropertyValue)
if err != nil {
return model.NewAppError("AssignFlaggedPostReviewer", "app.content_flagging.assign_reviewer.upsert_property_value.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("AssignFlaggedPostReviewer", "app.content_flagging.assign_reviewer.upsert_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if status == model.ContentFlaggingStatusPending {
statusPropertyValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusAssigned))
statusPropertyValue, err = a.Srv().propertyService.UpdatePropertyValue(groupId, statusPropertyValue)
if err != nil {
return model.NewAppError("AssignFlaggedPostReviewer", "app.content_flagging.assign_reviewer.update_status_property_value.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
return model.NewAppError("AssignFlaggedPostReviewer", "app.content_flagging.assign_reviewer.update_status_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.Srv().Go(func() {
postErr := a.postAssignReviewerMessage(rctx, groupId, flaggedPostId, reviewerId, assigneeId)
_, postErr := a.postAssignReviewerMessage(rctx, groupId, flaggedPostId, reviewerId, assigneeId)
if postErr != nil {
rctx.Logger().Error("Failed to post assign reviewer message", mlog.Err(postErr), mlog.String("flagged_post_id", flaggedPostId), mlog.String("reviewer_id", reviewerId), mlog.String("assignee_id", assigneeId))
}
@@ -924,19 +931,19 @@ func (a *App) AssignFlaggedPostReviewer(rctx request.CTX, flaggedPostId, flagged
return nil
}
func (a *App) postAssignReviewerMessage(rctx request.CTX, contentFlaggingGroupId, flaggedPostId, reviewerId, assignedById string) *model.AppError {
func (a *App) postAssignReviewerMessage(rctx request.CTX, contentFlaggingGroupId, flaggedPostId, reviewerId, assignedById string) ([]*model.Post, *model.AppError) {
notificationSettings := a.Config().ContentFlaggingSettings.NotificationSettings
if notificationSettings == nil {
return nil
return nil, nil
}
if !slices.Contains(notificationSettings.EventTargetMapping[model.EventAssigned], model.TargetReviewers) {
return nil
return nil, nil
}
reviewerUser, appErr := a.GetUser(reviewerId)
if appErr != nil {
return appErr
return nil, appErr
}
var assignedByUser *model.User
@@ -945,7 +952,7 @@ func (a *App) postAssignReviewerMessage(rctx request.CTX, contentFlaggingGroupId
} else {
assignedByUser, appErr = a.GetUser(assignedById)
if appErr != nil {
return appErr
return nil, appErr
}
}
@@ -953,10 +960,10 @@ func (a *App) postAssignReviewerMessage(rctx request.CTX, contentFlaggingGroupId
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId)
}
func (a *App) postDeletePostReviewerMessage(rctx request.CTX, flaggedPostId, actorUserId, comment, contentFlaggingGroupId string) *model.AppError {
func (a *App) postDeletePostReviewerMessage(rctx request.CTX, flaggedPostId, actorUserId, comment, contentFlaggingGroupId string) ([]*model.Post, *model.AppError) {
actorUser, appErr := a.GetUser(actorUserId)
if appErr != nil {
return appErr
return nil, appErr
}
message := fmt.Sprintf("The flagged message was removed by @%s", actorUser.Username)
@@ -967,10 +974,10 @@ func (a *App) postDeletePostReviewerMessage(rctx request.CTX, flaggedPostId, act
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId)
}
func (a *App) postKeepPostReviewerMessage(rctx request.CTX, flaggedPostId, actorUserId, comment, contentFlaggingGroupId string) *model.AppError {
func (a *App) postKeepPostReviewerMessage(rctx request.CTX, flaggedPostId, actorUserId, comment, contentFlaggingGroupId string) ([]*model.Post, *model.AppError) {
actorUser, appErr := a.GetUser(actorUserId)
if appErr != nil {
return appErr
return nil, appErr
}
message := fmt.Sprintf("The flagged message was retained by @%s", actorUser.Username)
@@ -981,27 +988,94 @@ func (a *App) postKeepPostReviewerMessage(rctx request.CTX, flaggedPostId, actor
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId)
}
func (a *App) postReviewerMessage(rctx request.CTX, message, contentFlaggingGroupId, flaggedPostId string) *model.AppError {
func (a *App) getReporterUserId(flaggedPostId, contentFlaggingGroupId string) (string, *model.AppError) {
mappedFields, appErr := a.GetContentFlaggingMappedFields(contentFlaggingGroupId)
if appErr != nil {
return appErr
return "", appErr
}
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
reporterUserIdField, ok := mappedFields[contentFlaggingPropertyNameReportingUserID]
if !ok {
return model.NewAppError("postAssignReviewerMessage", "app.content_flagging.missing_flagged_post_id_field.app_error", nil, "", http.StatusInternalServerError)
return "", model.NewAppError("getReporterUserId", "app.content_flagging.missing_reporting_user_id_field.app_error", nil, "", http.StatusInternalServerError)
}
postIds, appErr := a.getReviewerPostsForFlaggedPost(contentFlaggingGroupId, flaggedPostId, flaggedPostIdField.ID)
propertyValues, appErr := a.GetPostContentFlaggingPropertyValues(flaggedPostId)
if appErr != nil {
return appErr
return "", appErr
}
var reporterPropertyValue *model.PropertyValue
for _, pv := range propertyValues {
if pv.FieldID == reporterUserIdField.ID {
reporterPropertyValue = pv
break
}
}
if reporterPropertyValue == nil {
return "", model.NewAppError("getReporterUserId", "app.content_flagging.missing_reporting_user_id_property_value.app_error", nil, "", http.StatusInternalServerError)
}
reporterUserId := strings.Trim(string(reporterPropertyValue.Value), `"`)
return reporterUserId, nil
}
func (a *App) postContentReviewBotMessage(rctx request.CTX, flaggedPost *model.Post, messageTemplate string, flaggedPostAuthorUserId string) (*model.Post, *model.AppError) {
channel, appErr := a.GetChannel(rctx, flaggedPost.ChannelId)
if appErr != nil {
return nil, appErr
}
contentReviewBot, appErr := a.getContentReviewBot(rctx)
if appErr != nil {
return appErr
return nil, appErr
}
dmChannel, appErr := a.GetOrCreateDirectChannel(rctx, flaggedPostAuthorUserId, contentReviewBot.UserId)
if appErr != nil {
return nil, appErr
}
post := &model.Post{
Message: fmt.Sprintf(messageTemplate, flaggedPost.Id, channel.DisplayName),
UserId: contentReviewBot.UserId,
ChannelId: dmChannel.Id,
}
return a.CreatePost(rctx, post, dmChannel, model.CreatePostFlags{})
}
func (a *App) postMessageToReporter(rctx request.CTX, contentFlaggingGroupId string, flaggedPost *model.Post, messageTemplate string) (*model.Post, *model.AppError) {
userId, appErr := a.getReporterUserId(flaggedPost.Id, contentFlaggingGroupId)
if appErr != nil {
return nil, appErr
}
return a.postContentReviewBotMessage(rctx, flaggedPost, messageTemplate, userId)
}
func (a *App) postReviewerMessage(rctx request.CTX, message, contentFlaggingGroupId, flaggedPostId string) ([]*model.Post, *model.AppError) {
mappedFields, appErr := a.GetContentFlaggingMappedFields(contentFlaggingGroupId)
if appErr != nil {
return nil, appErr
}
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
if !ok {
return nil, model.NewAppError("postReviewerMessage", "app.content_flagging.missing_flagged_post_id_field.app_error", nil, "", http.StatusInternalServerError)
}
postIds, appErr := a.getReviewerPostsForFlaggedPost(contentFlaggingGroupId, flaggedPostId, flaggedPostIdField.ID)
if appErr != nil {
return nil, appErr
}
contentReviewBot, appErr := a.getContentReviewBot(rctx)
if appErr != nil {
return nil, appErr
}
createdPosts := make([]*model.Post, 0, len(postIds))
for _, postId := range postIds {
reviewerPost, appErr := a.GetSinglePost(rctx, postId, false)
if appErr != nil {
@@ -1022,14 +1096,15 @@ func (a *App) postReviewerMessage(rctx request.CTX, message, contentFlaggingGrou
RootId: postId,
}
_, appErr = a.CreatePost(rctx, post, channel, model.CreatePostFlags{})
createdPost, appErr := a.CreatePost(rctx, post, channel, model.CreatePostFlags{})
if appErr != nil {
rctx.Logger().Error("Failed to create assign reviewer post in one of the channels", mlog.Err(appErr), mlog.String("channel_id", channel.Id), mlog.String("post_id", postId))
continue
}
createdPosts = append(createdPosts, createdPost)
}
return nil
return createdPosts, nil
}
func (a *App) getReviewerPostsForFlaggedPost(contentFlaggingGroupId, flaggedPostId, flaggedPostIdFieldId string) ([]string, *model.AppError) {
@@ -1066,3 +1141,121 @@ func (a *App) getReviewerPostsForFlaggedPost(contentFlaggingGroupId, flaggedPost
return reviewerPostIds, nil
}
func (a *App) sendFlagPostNotification(rctx request.CTX, flaggedPost *model.Post) *model.AppError {
notificationSettings := a.Config().ContentFlaggingSettings.NotificationSettings
flagPostNotifications := notificationSettings.EventTargetMapping[model.EventFlagged]
if flagPostNotifications == nil {
return nil
}
if !slices.Contains(flagPostNotifications, model.TargetAuthor) {
return nil
}
channel, appErr := a.GetChannel(rctx, flaggedPost.ChannelId)
if appErr != nil {
return appErr
}
contentReviewBot, appErr := a.getContentReviewBot(rctx)
if appErr != nil {
return appErr
}
dmChannel, appErr := a.GetOrCreateDirectChannel(rctx, flaggedPost.UserId, contentReviewBot.UserId)
if appErr != nil {
return appErr
}
post := &model.Post{
Message: fmt.Sprintf("Your post having ID `%s` in the channel `%s` has been flagged for review.", flaggedPost.Id, channel.DisplayName),
UserId: contentReviewBot.UserId,
ChannelId: dmChannel.Id,
}
_, appErr = a.CreatePost(rctx, post, dmChannel, model.CreatePostFlags{})
return appErr
}
func (a *App) sendFlaggedPostRemovalNotification(rctx request.CTX, flaggedPost *model.Post, actorUserId, comment, contentFlaggingGroupId string) []*model.Post {
notificationSettings := a.Config().ContentFlaggingSettings.NotificationSettings
deletePostNotifications := notificationSettings.EventTargetMapping[model.EventContentRemoved]
if deletePostNotifications == nil {
return nil
}
var createdPosts []*model.Post
if slices.Contains(deletePostNotifications, model.TargetReviewers) {
posts, appErr := a.postDeletePostReviewerMessage(rctx, flaggedPost.Id, actorUserId, comment, contentFlaggingGroupId)
if appErr != nil {
rctx.Logger().Error("Failed to post delete post reviewer message after permanently removing flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = posts
}
}
if slices.Contains(deletePostNotifications, model.TargetAuthor) {
template := "Your post having ID `%s` in the channel `%s` which was flagged for review has been permanently removed by a reviewer."
post, appErr := a.postContentReviewBotMessage(rctx, flaggedPost, template, flaggedPost.UserId)
if appErr != nil {
rctx.Logger().Error("Failed to post delete post author message after permanently removing flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = append(createdPosts, post)
}
}
if slices.Contains(deletePostNotifications, model.TargetReporter) {
template := "The post having ID `%s` in the channel `%s` which you flagged for review has been permanently removed by a reviewer."
post, appErr := a.postMessageToReporter(rctx, contentFlaggingGroupId, flaggedPost, template)
if appErr != nil {
rctx.Logger().Error("Failed to post delete post reporter message after permanently removing flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = append(createdPosts, post)
}
}
return createdPosts
}
func (a *App) sendKeepFlaggedPostNotification(rctx request.CTX, flaggedPost *model.Post, actorUserId, comment, contentFlaggingGroupId string) []*model.Post {
notificationSettings := a.Config().ContentFlaggingSettings.NotificationSettings
keepPostNotifications := notificationSettings.EventTargetMapping[model.EventContentDismissed]
if keepPostNotifications == nil {
return nil
}
var createdPosts []*model.Post
if slices.Contains(keepPostNotifications, model.TargetReviewers) {
posts, appErr := a.postKeepPostReviewerMessage(rctx, flaggedPost.Id, actorUserId, comment, contentFlaggingGroupId)
if appErr != nil {
rctx.Logger().Error("Failed to post retain post reviewer message after restoring flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = posts
}
}
if slices.Contains(keepPostNotifications, model.TargetAuthor) {
template := "Your post having ID `%s` in the channel `%s` which was flagged for review has been restored by a reviewer."
post, appErr := a.postContentReviewBotMessage(rctx, flaggedPost, template, flaggedPost.UserId)
if appErr != nil {
rctx.Logger().Error("Failed to post retain post author message after restoring flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = append(createdPosts, post)
}
}
if slices.Contains(keepPostNotifications, model.TargetReporter) {
template := "The post having ID `%s` in the channel `%s` which you flagged for review has been restored by a reviewer."
post, appErr := a.postMessageToReporter(rctx, contentFlaggingGroupId, flaggedPost, template)
if appErr != nil {
rctx.Logger().Error("Failed to post retain post reporter message after restoring flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
} else {
createdPosts = append(createdPosts, post)
}
}
return createdPosts
}
+412 -143
View File
@@ -5,6 +5,7 @@ package app
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
@@ -13,6 +14,46 @@ import (
"github.com/stretchr/testify/require"
)
func getBaseConfig(th *TestHelper) model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
config.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return config
}
func setBaseConfig(th *TestHelper) *model.AppError {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig(th))
if appErr != nil {
return appErr
}
return nil
}
func setupFlaggedPost(th *TestHelper) (*model.Post, *model.AppError) {
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr := th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
if appErr != nil {
return nil, appErr
}
time.Sleep(2 * time.Second)
return post, nil
}
func TestContentFlaggingEnabledForTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
@@ -117,38 +158,13 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
config.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return config
}
setupFlaggedPost := func() *model.Post {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
return post
}
t.Run("should successfully assign reviewer to pending flagged post", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
@@ -174,10 +190,13 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should successfully reassign reviewer to already assigned flagged post", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// First assignment
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Second assignment (reassignment)
@@ -207,6 +226,7 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should fail when trying to assign reviewer to non-flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(th.BasicChannel)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
@@ -215,13 +235,16 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should fail when trying to assign reviewer to retained post", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// First retain the post
actionRequest := &model.FlagContentActionRequest{
Comment: "Keeping this post",
}
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.BasicUser.Id, post)
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.BasicUser.Id, post)
require.Nil(t, appErr)
// Try to assign reviewer to retained post
@@ -232,13 +255,16 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should fail when trying to assign reviewer to removed post", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// First remove the post
actionRequest := &model.FlagContentActionRequest{
Comment: "Removing this post",
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.BasicUser.Id, post)
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.BasicUser.Id, post)
require.Nil(t, appErr)
// Try to assign reviewer to removed post
@@ -249,10 +275,13 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should handle assignment with same reviewer ID", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Assign reviewer
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Assign same reviewer again
@@ -282,9 +311,12 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should handle assignment with empty reviewer ID", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, "", th.SystemAdminUser.Id)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, "", th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
@@ -310,6 +342,7 @@ func TestAssignFlaggedPostReviewer(t *testing.T) {
})
t.Run("should handle assignment with invalid post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, "invalid_post_id", th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
@@ -1087,13 +1120,11 @@ func TestFlagPost(t *testing.T) {
})
t.Run("should fail when comment is required but not provided", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
config := getBaseConfig()
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
th.UpdateConfig(func(conf *model.Config) {
conf.ContentFlaggingSettings.AdditionalSettings.ReporterCommentRequired = model.NewPointer(true)
})
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
@@ -1103,11 +1134,6 @@ func TestFlagPost(t *testing.T) {
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.NotNil(t, appErr)
// Reset config
th.UpdateConfig(func(conf *model.Config) {
conf.ContentFlaggingSettings.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
})
})
t.Run("should fail when trying to flag already flagged post", func(t *testing.T) {
@@ -1132,13 +1158,11 @@ func TestFlagPost(t *testing.T) {
})
t.Run("should hide flagged content when configured", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
config := getBaseConfig()
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
th.UpdateConfig(func(conf *model.Config) {
conf.ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent = model.NewPointer(true)
})
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
@@ -1153,11 +1177,6 @@ func TestFlagPost(t *testing.T) {
deletedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
require.NotNil(t, appErr)
require.Nil(t, deletedPost)
// Reset config
th.UpdateConfig(func(conf *model.Config) {
conf.ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
})
})
t.Run("should create content review post for reviewers", func(t *testing.T) {
@@ -1569,39 +1588,11 @@ func TestGetReviewerPostsForFlaggedPost(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
config.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return config
}
setupFlaggedPost := func() *model.Post {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
return post
}
t.Run("should return reviewer posts for flagged post", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
@@ -1625,6 +1616,7 @@ func TestGetReviewerPostsForFlaggedPost(t *testing.T) {
})
t.Run("should return empty list when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
@@ -1643,7 +1635,7 @@ func TestGetReviewerPostsForFlaggedPost(t *testing.T) {
t.Run("should handle multiple reviewer posts for same flagged post", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig()
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
@@ -1685,6 +1677,7 @@ func TestGetReviewerPostsForFlaggedPost(t *testing.T) {
})
t.Run("should handle invalid flagged post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
@@ -1707,50 +1700,19 @@ func TestPostReviewerMessage(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
config.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return config
}
setupFlaggedPost := func() *model.Post {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
return post
}
t.Run("should post reviewer message to thread", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test reviewer message"
appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Wait for async message posting to complete
time.Sleep(2 * time.Second)
// Verify message was posted to the reviewer thread
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
@@ -1783,7 +1745,7 @@ func TestPostReviewerMessage(t *testing.T) {
t.Run("should handle multiple reviewer channels", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig()
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
@@ -1805,12 +1767,9 @@ func TestPostReviewerMessage(t *testing.T) {
require.Nil(t, appErr)
testMessage := "Test message for multiple reviewers"
appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Wait for async message posting to complete
time.Sleep(2 * time.Second)
// Verify message was posted to both reviewer threads
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
@@ -1858,35 +1817,30 @@ func TestPostReviewerMessage(t *testing.T) {
})
t.Run("should handle case when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message for non-flagged post"
appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Should not error, but no messages should be posted since no reviewer posts exist
// This is a graceful handling case
})
t.Run("should handle message with special characters", func(t *testing.T) {
post := setupFlaggedPost()
require.Nil(t, setBaseConfig(th))
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message with special chars: @user #channel ~team & <script>alert('xss')</script>"
appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Wait for async message posting to complete
time.Sleep(2 * time.Second)
// Verify message was posted correctly with special characters preserved
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
@@ -1913,3 +1867,318 @@ func TestPostReviewerMessage(t *testing.T) {
require.Equal(t, testMessage, testMessagePost.Message)
})
}
// Helper function to setup notification config for testing
func setupNotificationConfig(th *TestHelper, eventTargetMapping map[model.ContentFlaggingEvent][]model.NotificationTarget) *model.AppError {
config := getBaseConfig(th)
config.NotificationSettings = &model.ContentFlaggingNotificationSettings{
EventTargetMapping: eventTargetMapping,
}
return th.App.SaveContentFlaggingConfig(config)
}
// Helper function to verify post message content and properties
func verifyNotificationPost(t *testing.T, post *model.Post, expectedMessage string, expectedUserId string, expectedChannelId string) {
require.NotNil(t, post)
require.Equal(t, expectedMessage, post.Message)
require.Equal(t, expectedUserId, post.UserId)
require.Equal(t, expectedChannelId, post.ChannelId)
require.True(t, post.CreateAt > 0)
require.True(t, post.UpdateAt > 0)
}
func TestSendFlaggedPostRemovalNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
actorComment := "This post violates community guidelines"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The flagged message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply to the flag review post
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was flagged for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you flagged for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only author
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Setup notification config for only author
appErr = setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "Test comment", groupId)
// Should create only 1 post for author
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The flagged message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, "Test comment")
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged message was removed by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
func TestSendKeepFlaggedPostNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
actorComment := "This post is acceptable after review"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The flagged message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was flagged for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you flagged for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only reporter
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
comment := "Test comment"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, comment, groupId)
// Should create only 1 post for reporter
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The flagged message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, comment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged message was retained by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle different actor users", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
// Use BasicUser as actor instead of SystemAdminUser
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.BasicUser.Id, "Reviewed by different user", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged message was retained by @%s\n\nWith comment:\n\n> %s", th.BasicUser.Username, "Reviewed by different user")
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
+8 -4
View File
@@ -5206,10 +5206,6 @@
"id": "app.content_flagging.create_property_values.app_error",
"translation": "Unable to save property values for the flagged post."
},
{
"id": "app.content_flagging.delete_post.app_error",
"translation": "Unable to soft-delete the flagged post."
},
{
"id": "app.content_flagging.flag_post.marshal_comment.app_error",
"translation": "Failed to marshal flagging user's comment"
@@ -5258,6 +5254,14 @@
"id": "app.content_flagging.missing_flagged_post_id_field.app_error",
"translation": "Unable to find Flagged Post ID property field."
},
{
"id": "app.content_flagging.missing_reporting_user_id_field.app_error",
"translation": "Unable to find reporting user field."
},
{
"id": "app.content_flagging.missing_reporting_user_id_property_value.app_error",
"translation": "Unable to find value for reporting user field."
},
{
"id": "app.content_flagging.no_status_property.app_error",
"translation": "Cannot fetch flagged post as the post is not flagged."