mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
1573 lines
62 KiB
Go
1573 lines
62 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/public/utils"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
CONTENT_FLAGGING_MAX_PROPERTY_FIELDS = 100
|
|
CONTENT_FLAGGING_MAX_PROPERTY_VALUES = 100
|
|
|
|
POST_PROP_KEY_FLAGGED_POST_ID = "reported_post_id"
|
|
|
|
CONTENT_FLAGGING_REVIEWER_SEARCH_INDIVIDUAL_LIMIT = 50
|
|
)
|
|
|
|
func (a *App) ContentFlaggingEnabledForTeam(teamId string) (bool, *model.AppError) {
|
|
reviewerIDs, appErr := a.GetContentFlaggingConfigReviewerIDs()
|
|
if appErr != nil {
|
|
return false, appErr
|
|
}
|
|
|
|
reviewerSettings := a.Config().ContentFlaggingSettings.ReviewerSettings
|
|
commonReviewersEnabled := reviewerSettings.CommonReviewers != nil && *reviewerSettings.CommonReviewers
|
|
|
|
hasAdditionalReviewers := (reviewerSettings.TeamAdminsAsReviewers != nil && *reviewerSettings.TeamAdminsAsReviewers) ||
|
|
(reviewerSettings.SystemAdminsAsReviewers != nil && *reviewerSettings.SystemAdminsAsReviewers)
|
|
|
|
if commonReviewersEnabled {
|
|
if len(reviewerIDs.CommonReviewerIds) > 0 || hasAdditionalReviewers {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
teamSettings, exist := (reviewerIDs.TeamReviewersSetting)[teamId]
|
|
if !exist {
|
|
return false, nil
|
|
}
|
|
|
|
enabledForTeam := teamSettings.Enabled != nil && *teamSettings.Enabled
|
|
if !enabledForTeam {
|
|
return false, nil
|
|
}
|
|
|
|
hasTeamReviewers := len(teamSettings.ReviewerIds) > 0
|
|
if hasTeamReviewers || hasAdditionalReviewers {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (a *App) FlagPost(rctx request.CTX, post *model.Post, teamId, reportingUserId string, flagData model.FlagContentRequest) *model.AppError {
|
|
commentBytes, err := json.Marshal(flagData.Comment)
|
|
if err != nil {
|
|
return model.NewAppError("FlagPost", "app.data_spillage.flag_post.marshal_comment.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
// Storing marshalled content into RawMessage to ensure proper escaping of special characters and prevent
|
|
// generating unsafe JSON values
|
|
commentJsonValue := json.RawMessage(commentBytes)
|
|
|
|
reasonJson, err := json.Marshal(flagData.Reason)
|
|
if err != nil {
|
|
return model.NewAppError("FlagPost", "app.data_spillage.flag_post.marshal_reason.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
// Storing marshalled content into RawMessage to ensure proper escaping of special characters and prevent
|
|
// generating unsafe JSON values
|
|
reasonJsonValue := json.RawMessage(reasonJson)
|
|
|
|
commentRequired := a.Config().ContentFlaggingSettings.AdditionalSettings.ReporterCommentRequired
|
|
validReasons := a.Config().ContentFlaggingSettings.AdditionalSettings.Reasons
|
|
if appErr := flagData.IsValid(*commentRequired, *validReasons); appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
groupId, appErr := a.ContentFlaggingGroupId()
|
|
if appErr != nil {
|
|
return model.NewAppError("FlagPost", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
reportingUser, appErr := a.GetUser(reportingUserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
appErr = a.canFlagPost(groupId, post.Id, reportingUser.Locale)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
mappedFields, appErr := a.GetContentFlaggingMappedFields(groupId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
propertyValues := []*model.PropertyValue{
|
|
{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[ContentFlaggingPropertyNameStatus].ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusPending)),
|
|
},
|
|
{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameReportingUserID].ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, reportingUserId)),
|
|
},
|
|
{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameReportingReason].ID,
|
|
Value: reasonJsonValue,
|
|
},
|
|
{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
|
|
Value: commentJsonValue,
|
|
},
|
|
{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameReportingTime].ID,
|
|
Value: json.RawMessage(fmt.Sprintf("%d", model.GetMillis())),
|
|
},
|
|
}
|
|
|
|
if *a.Config().ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent {
|
|
propertyValues = append(propertyValues, &model.PropertyValue{
|
|
TargetID: post.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyManageByContentFlagging].ID,
|
|
Value: json.RawMessage("true"),
|
|
})
|
|
}
|
|
|
|
_, appErr = a.CreatePropertyValues(rctx, propertyValues)
|
|
if appErr != nil {
|
|
return model.NewAppError("FlagPost", "app.data_spillage.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
if *a.Config().ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent {
|
|
appErr = a.setContentFlaggingPropertiesForThreadReplies(rctx, post, groupId, mappedFields[contentFlaggingPropertyManageByContentFlagging].ID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
}
|
|
|
|
contentReviewBot, appErr := a.getContentReviewBot(rctx)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if *a.Config().ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent {
|
|
_, appErr = a.DeletePost(rctx, post.Id, contentReviewBot.UserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
}
|
|
|
|
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
|
|
if !ok {
|
|
return model.NewAppError("FlagPost", "app.data_spillage.missing_flagged_post_id_field.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
appErr = a.createContentReviewPost(rctx, post.Id, teamId, reportingUserId, flagData.Reason, post.ChannelId, post.UserId, flaggedPostIdField.ID, groupId)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to create content review post", mlog.Err(appErr), mlog.String("team_id", teamId), mlog.String("post_id", post.Id))
|
|
}
|
|
})
|
|
|
|
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) setContentFlaggingPropertiesForThreadReplies(rctx request.CTX, post *model.Post, contentFlaggingGroupId, contentFlaggingManagedFieldId string) *model.AppError {
|
|
if post.RootId != "" {
|
|
// Post is a reply, not a root post
|
|
return nil
|
|
}
|
|
|
|
replies, err := a.Srv().Store().Post().GetPostsByThread(post.Id, 0)
|
|
if err != nil {
|
|
return model.NewAppError("setContentFlaggingPropertiesForThreadReplies", "app.data_spillage.get_thread_replies.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if len(replies) == 0 {
|
|
return nil
|
|
}
|
|
|
|
propertyValues := make([]*model.PropertyValue, 0, len(replies))
|
|
for _, reply := range replies {
|
|
propertyValues = append(propertyValues, &model.PropertyValue{
|
|
TargetID: reply.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: contentFlaggingGroupId,
|
|
FieldID: contentFlaggingManagedFieldId,
|
|
Value: json.RawMessage("true"),
|
|
})
|
|
}
|
|
|
|
_, appErr := a.CreatePropertyValues(rctx, propertyValues)
|
|
if appErr != nil {
|
|
return model.NewAppError("setContentFlaggingPropertiesForThreadReplies", "app.data_spillage.set_thread_replies_properties.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ContentFlaggingGroupId() (string, *model.AppError) {
|
|
group, appErr := a.GetPropertyGroup(nil, model.ContentFlaggingGroupName)
|
|
if appErr != nil {
|
|
return "", model.NewAppError("getContentFlaggingGroupId", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
return group.ID, nil
|
|
}
|
|
|
|
func (a *App) GetPostContentFlaggingPropertyValue(postId, propertyFieldName string) (*model.PropertyValue, *model.AppError) {
|
|
groupId, err := a.ContentFlaggingGroupId()
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValue", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
statusPropertyField, appErr := a.GetPropertyFieldByName(nil, groupId, "", propertyFieldName)
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValue", "app.data_spillage.get_status_property.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
searchOptions := model.PropertyValueSearchOpts{TargetIDs: []string{postId}, PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES, FieldID: statusPropertyField.ID}
|
|
propertyValues, appErr := a.SearchPropertyValues(nil, groupId, searchOptions)
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValue", "app.data_spillage.search_status_property.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
if len(propertyValues) == 0 {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValue", "app.data_spillage.no_status_property.app_error", nil, "", http.StatusNotFound)
|
|
}
|
|
|
|
return propertyValues[0], nil
|
|
}
|
|
|
|
func (a *App) canFlagPost(groupId, postId, userLocal string) *model.AppError {
|
|
status, appErr := a.GetPostContentFlaggingPropertyValue(postId, ContentFlaggingPropertyNameStatus)
|
|
if appErr != nil {
|
|
if appErr.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
return appErr
|
|
}
|
|
|
|
var reason string
|
|
T := i18n.GetUserTranslations(userLocal)
|
|
|
|
switch strings.Trim(string(status.Value), `"`) {
|
|
case model.ContentFlaggingStatusPending, model.ContentFlaggingStatusAssigned:
|
|
reason = T("app.data_spillage.can_flag_post.in_progress")
|
|
case model.ContentFlaggingStatusRetained:
|
|
reason = T("app.data_spillage.can_flag_post.retained")
|
|
case model.ContentFlaggingStatusRemoved:
|
|
reason = T("app.data_spillage.can_flag_post.removed")
|
|
default:
|
|
reason = T("app.data_spillage.can_flag_post.unknown")
|
|
}
|
|
|
|
return model.NewAppError("canFlagPost", reason, nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
func (a *App) GetContentFlaggingMappedFields(groupId string) (map[string]*model.PropertyField, *model.AppError) {
|
|
fields, appErr := a.SearchPropertyFields(nil, groupId, model.PropertyFieldSearchOpts{PerPage: CONTENT_FLAGGING_MAX_PROPERTY_FIELDS})
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetContentFlaggingMappedFields", "app.data_spillage.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
mappedFields := map[string]*model.PropertyField{}
|
|
for _, field := range fields {
|
|
mappedFields[field.Name] = field
|
|
}
|
|
|
|
return mappedFields, nil
|
|
}
|
|
|
|
func (a *App) createContentReviewPost(rctx request.CTX, flaggedPostId, teamId, reportingUserId, reportingReason, flaggedPostChannelId, flaggedPostAuthorId, flaggedPostIdFieldId, contentFlaggingGroupId string) *model.AppError {
|
|
contentReviewBot, appErr := a.getContentReviewBot(rctx)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
channels, appErr := a.getContentReviewChannels(rctx, teamId, contentReviewBot.UserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
reportingUser, appErr := a.GetUser(reportingUserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
flaggedPostChannel, appErr := a.GetChannel(rctx, flaggedPostChannelId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
flaggedPostTeam, appErr := a.GetTeam(flaggedPostChannel.TeamId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
flaggedPostAuthor, appErr := a.GetUser(flaggedPostAuthorId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
message := fmt.Sprintf("@%s submitted a message for review.\n\nReason: %s\nChannel: ~%s\nTeam: %s\nPost Author: @%s\n\nOpen on a web browser or the Desktop app to view the full report and take action.",
|
|
reportingUser.Username,
|
|
reportingReason,
|
|
flaggedPostChannel.Name,
|
|
flaggedPostTeam.DisplayName,
|
|
flaggedPostAuthor.Username,
|
|
)
|
|
|
|
for _, channel := range channels {
|
|
post := &model.Post{
|
|
Message: message,
|
|
UserId: contentReviewBot.UserId,
|
|
Type: model.ContentFlaggingPostType,
|
|
ChannelId: channel.Id,
|
|
}
|
|
post.AddProp(POST_PROP_KEY_FLAGGED_POST_ID, flaggedPostId)
|
|
createdPost, _, appErr := a.CreatePost(rctx, post, channel, model.CreatePostFlags{})
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to create content review post in one of the channels", mlog.Err(appErr), mlog.String("channel_id", channel.Id), mlog.String("team_id", teamId))
|
|
continue // Don't stop processing other channels if one fails
|
|
}
|
|
|
|
propertyValue := &model.PropertyValue{
|
|
TargetID: createdPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: contentFlaggingGroupId,
|
|
FieldID: flaggedPostIdFieldId,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, flaggedPostId)),
|
|
}
|
|
_, appErr = a.CreatePropertyValue(nil, propertyValue)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to create content review post property value in one of the channels", mlog.Err(appErr), mlog.String("channel_id", channel.Id), mlog.String("team_id", teamId), mlog.String("post_id", createdPost.Id))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) getContentReviewChannels(rctx request.CTX, teamId, contentReviewBotId string) ([]*model.Channel, *model.AppError) {
|
|
reviewersUserIDs, appErr := a.getReviewersForTeam(teamId, true)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
var channels []*model.Channel
|
|
for _, userId := range reviewersUserIDs {
|
|
channel, appErr := a.GetOrCreateDirectChannel(rctx, userId, contentReviewBotId)
|
|
if appErr != nil {
|
|
// Don't stop processing other reviewers if one fails
|
|
rctx.Logger().Error("Failed to get or create direct channel for one of the reviewers and content review bot", mlog.Err(appErr), mlog.String("user_id", userId), mlog.String("bot_id", contentReviewBotId))
|
|
}
|
|
|
|
channels = append(channels, channel)
|
|
}
|
|
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) getContentReviewBot(rctx request.CTX) (*model.Bot, *model.AppError) {
|
|
desiredDisplayName := i18n.T("app.system.data_spillage_bot.bot_displayname")
|
|
bot, appErr := a.GetOrCreateSystemOwnedBot(rctx, model.ContentFlaggingBotUsername, desiredDisplayName)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if bot.DisplayName != desiredDisplayName {
|
|
newName := desiredDisplayName
|
|
patchedBot, patchErr := a.PatchBot(rctx, bot.UserId, &model.BotPatch{DisplayName: &newName})
|
|
if patchErr != nil {
|
|
rctx.Logger().Warn("Failed to update Data Spillage bot display name", mlog.Err(patchErr))
|
|
} else {
|
|
bot = patchedBot
|
|
}
|
|
}
|
|
|
|
return bot, nil
|
|
}
|
|
|
|
func (a *App) getReviewersForTeam(teamId string, includeAdditionalReviewers bool) ([]string, *model.AppError) {
|
|
reviewerIDs, appErr := a.GetContentFlaggingConfigReviewerIDs()
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
reviewerUserIDMap := map[string]bool{}
|
|
|
|
reviewerSettings := a.Config().ContentFlaggingSettings.ReviewerSettings
|
|
if *reviewerSettings.CommonReviewers {
|
|
for _, userID := range reviewerIDs.CommonReviewerIds {
|
|
reviewerUserIDMap[userID] = true
|
|
}
|
|
} else {
|
|
// If common reviewers are not enabled, we still need to check if the team has specific reviewers
|
|
teamSettings, exist := reviewerIDs.TeamReviewersSetting[teamId]
|
|
if exist && *teamSettings.Enabled && teamSettings.ReviewerIds != nil {
|
|
for _, userID := range teamSettings.ReviewerIds {
|
|
reviewerUserIDMap[userID] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if includeAdditionalReviewers {
|
|
var additionalReviewers []*model.User
|
|
if *reviewerSettings.TeamAdminsAsReviewers {
|
|
teamAdminReviewers, appErr := a.getAllUsersInTeamForRoles(teamId, nil, []string{model.TeamAdminRoleId})
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
additionalReviewers = append(additionalReviewers, teamAdminReviewers...)
|
|
}
|
|
|
|
if *reviewerSettings.SystemAdminsAsReviewers {
|
|
sysAdminReviewers, appErr := a.getAllUsersInTeamForRoles(teamId, []string{model.SystemAdminRoleId}, nil)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
additionalReviewers = append(additionalReviewers, sysAdminReviewers...)
|
|
}
|
|
|
|
for _, user := range additionalReviewers {
|
|
reviewerUserIDMap[user.Id] = true
|
|
}
|
|
}
|
|
|
|
reviewerUserIDs := make([]string, 0, len(reviewerUserIDMap))
|
|
for userID := range maps.Keys(reviewerUserIDMap) {
|
|
reviewerUserIDs = append(reviewerUserIDs, userID)
|
|
}
|
|
|
|
return reviewerUserIDs, nil
|
|
}
|
|
|
|
func (a *App) getAllUsersInTeamForRoles(teamId string, systemRoles, teamRoles []string) ([]*model.User, *model.AppError) {
|
|
var additionalReviewers []*model.User
|
|
|
|
options := &model.UserGetOptions{
|
|
InTeamId: teamId,
|
|
Page: 0,
|
|
PerPage: 100,
|
|
Active: true,
|
|
Roles: systemRoles,
|
|
TeamRoles: teamRoles,
|
|
}
|
|
|
|
fetchFunc := func(page int) ([]*model.User, error) {
|
|
options.Page = page
|
|
users, appErr := a.GetUsersInTeam(options)
|
|
// Checking for error this way instead of directly returning *model.AppError
|
|
// doesn't equate to error == nil (pointer vs non-pointer)
|
|
if appErr != nil {
|
|
return users, errors.New(appErr.Error())
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
additionalReviewers, err := utils.Pager(fetchFunc, options.PerPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("getAllUsersInTeamForRoles", "app.data_spillage.get_users_in_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return additionalReviewers, nil
|
|
}
|
|
|
|
func (a *App) sendContentFlaggingConfirmationMessage(rctx request.CTX, flaggingUserId, flaggedPostAuthorId, channelID string) *model.AppError {
|
|
flaggedPostAuthor, appErr := a.GetUser(flaggedPostAuthorId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
T := i18n.GetUserTranslations(flaggedPostAuthor.Locale)
|
|
post := &model.Post{
|
|
Message: T("app.data_spillage.flag_post_confirmation.message", map[string]any{"username": flaggedPostAuthor.Username}),
|
|
ChannelId: channelID,
|
|
}
|
|
|
|
a.SendEphemeralPost(rctx, flaggingUserId, post)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) IsUserTeamContentReviewer(userId, teamId string) (bool, *model.AppError) {
|
|
// not fetching additional reviewers because if the user exist in common or team
|
|
// specific reviewers, they are definitely a reviewer, and it saves multiple database calls.
|
|
reviewers, appErr := a.getReviewersForTeam(teamId, false)
|
|
if appErr != nil {
|
|
return false, appErr
|
|
}
|
|
|
|
if slices.Contains(reviewers, userId) {
|
|
return true, nil
|
|
}
|
|
|
|
// if user is not in common or team specific reviewers, we need to check if they are
|
|
// an additional reviewer.
|
|
reviewers, appErr = a.getReviewersForTeam(teamId, true)
|
|
if appErr != nil {
|
|
return false, appErr
|
|
}
|
|
|
|
return slices.Contains(reviewers, userId), nil
|
|
}
|
|
|
|
func (a *App) GetPostContentFlaggingPropertyValues(postId string) ([]*model.PropertyValue, *model.AppError) {
|
|
groupId, err := a.ContentFlaggingGroupId()
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValues", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
propertyValues, appErr := a.SearchPropertyValues(nil, groupId, model.PropertyValueSearchOpts{TargetIDs: []string{postId}, PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES})
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("GetPostContentFlaggingPropertyValues", "app.data_spillage.search_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
return propertyValues, nil
|
|
}
|
|
|
|
func (a *App) PermanentDeleteFlaggedPost(rctx request.CTX, actionRequest *model.FlagContentActionRequest, reviewerId string, flaggedPost *model.Post) *model.AppError {
|
|
// when a flagged post is removed, the following things need to be done
|
|
// 1. Hard delete corresponding file infos
|
|
// 2. Hard delete file infos associated to post's edit history
|
|
// 3. Hard delete post's edit history
|
|
// 4. Hard delete the files from file storage
|
|
// 5. Hard delete post's priority data
|
|
// 6. Hard delete post's post acknowledgements
|
|
// 7. Hard delete post reminders
|
|
// 8. Scrub the post's content - message, props
|
|
|
|
commentBytes, jsonErr := json.Marshal(actionRequest.Comment)
|
|
if jsonErr != nil {
|
|
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.data_spillage.permanently_delete.marshal_comment.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
// Storing marshaled content into RawMessage to ensure proper escaping of special characters and prevent
|
|
// generating unsafe JSON values
|
|
commentJsonValue := json.RawMessage(commentBytes)
|
|
|
|
status, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPost.Id, ContentFlaggingPropertyNameStatus)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
statusValue := strings.Trim(string(status.Value), `"`)
|
|
if statusValue != model.ContentFlaggingStatusPending && statusValue != model.ContentFlaggingStatusAssigned {
|
|
return model.NewAppError("PermanentlyRemoveFlaggedPost", "api.data_spillage.error.post_not_in_progress", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
groupId, err := a.ContentFlaggingGroupId()
|
|
if err != nil {
|
|
return model.NewAppError("PermanentDeleteFlaggedPost", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
deletionReport, appErr := a.PermanentDeletePostDataRetainStub(rctx, flaggedPost, reviewerId)
|
|
|
|
// Send the deletion report even if there is an error as there can be partial deletion of data
|
|
// which must be reported to the reviewers.
|
|
if deletionReport != nil {
|
|
a.Srv().Go(func() {
|
|
a.sendDeletionReportToReviewers(rctx, flaggedPost.Id, deletionReport, groupId)
|
|
})
|
|
}
|
|
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
mappedFields, appErr := a.GetContentFlaggingMappedFields(groupId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
existingComment, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPost.Id, contentFlaggingPropertyNameActorComment)
|
|
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
|
return appErr
|
|
}
|
|
|
|
propertyValues := []*model.PropertyValue{
|
|
{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, reviewerId)),
|
|
},
|
|
{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
|
|
Value: json.RawMessage(fmt.Sprintf("%d", model.GetMillis())),
|
|
},
|
|
}
|
|
if existingComment == nil {
|
|
propertyValues = append(propertyValues, &model.PropertyValue{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
|
|
Value: commentJsonValue,
|
|
})
|
|
}
|
|
|
|
_, appErr = a.CreatePropertyValues(rctx, propertyValues)
|
|
if appErr != nil {
|
|
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.data_spillage.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
status.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
|
|
_, appErr = a.UpdatePropertyValue(rctx, groupId, status)
|
|
if appErr != nil {
|
|
return model.NewAppError("PermanentlyRemoveFlaggedPost", "app.data_spillage.permanently_delete.update_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
channel, appErr := a.GetChannel(rctx, flaggedPost.ChannelId)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to get channel for flagged post while publishing report change after permanently removing flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id), mlog.String("channel_id", flaggedPost.ChannelId))
|
|
return
|
|
}
|
|
|
|
propertyValues = append(propertyValues, status)
|
|
if err := a.publishContentFlaggingReportUpdateEvent(flaggedPost.Id, channel.TeamId, propertyValues); err != nil {
|
|
rctx.Logger().Error("Failed to publish report change after permanently removing flagged post", mlog.Err(err), mlog.String("post_id", flaggedPost.Id))
|
|
}
|
|
})
|
|
|
|
a.Srv().Go(func() {
|
|
a.sendFlaggedPostRemovalNotification(rctx, flaggedPost, reviewerId, actionRequest.Comment, groupId)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) sendDeletionReportToReviewers(rctx request.CTX, flaggedPostId string, report *model.PostDeletionReport, contentFlaggingGroupId string) {
|
|
reportFileName := fmt.Sprintf("deletion_report_%s.md", flaggedPostId)
|
|
|
|
_, appErr := a.postReviewerMessage(rctx, "", contentFlaggingGroupId, flaggedPostId, report, reportFileName)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to send deletion report to reviewers", mlog.Err(appErr), mlog.String("post_id", flaggedPostId))
|
|
}
|
|
}
|
|
|
|
func (a *App) PermanentDeletePostDataRetainStub(rctx request.CTX, post *model.Post, deleteByID string) (*model.PostDeletionReport, *model.AppError) {
|
|
report := &model.PostDeletionReport{
|
|
PostID: post.Id,
|
|
Timestamp: time.Now().UTC(),
|
|
}
|
|
|
|
a.deleteFiles(rctx, post.Id, report)
|
|
a.deleteEditHistories(rctx, post.Id, deleteByID, report)
|
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
// Handling persistent notification
|
|
persistentNotification, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
|
|
if err != nil && !errors.As(err, &nfErr) {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to get persistent notification for the post", mlog.Err(err), mlog.String("post_id", post.Id))
|
|
}
|
|
|
|
if (err == nil && persistentNotification == nil) || errors.As(err, &nfErr) {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.persistent_notifications"), model.StepNotApplicable, i18n.TranslationId("app.data_spillage.report.detail.no_data_found"), nil)
|
|
} else {
|
|
if deleteErr := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); deleteErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to delete persistent notifications for the post", mlog.Err(deleteErr), mlog.String("post_id", post.Id))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.persistent_notifications"), model.StepFailed, "", []string{deleteErr.Error()})
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.persistent_notifications"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.deleted"), nil)
|
|
}
|
|
}
|
|
|
|
// Handling post acknowledgements
|
|
acknowledgements, appErr := a.GetAcknowledgementsForPost(post.Id)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to get post acknowledgements for the post", mlog.Err(appErr), mlog.String("post_id", post.Id))
|
|
}
|
|
|
|
if appErr == nil && len(acknowledgements) == 0 {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.acknowledgements"), model.StepNotApplicable, i18n.TranslationId("app.data_spillage.report.detail.no_data_found"), nil)
|
|
} else {
|
|
if deleteErr := a.Srv().Store().PostAcknowledgement().DeleteAllForPost(post.Id); deleteErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to delete post acknowledgements for the post", mlog.Err(deleteErr), mlog.String("post_id", post.Id))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.acknowledgements"), model.StepFailed, "", []string{deleteErr.Error()})
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.acknowledgements"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.deleted"), nil)
|
|
}
|
|
}
|
|
|
|
// Handling post priority
|
|
postPriorityData, appErr := a.GetPriorityForPost(post.Id)
|
|
if appErr != nil {
|
|
// we can still attempt a deletion even if retrieval failed
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to get post priority for the post", mlog.Err(appErr), mlog.String("post_id", post.Id))
|
|
}
|
|
|
|
if appErr == nil && postPriorityData == nil {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.priority_data"), model.StepNotApplicable, i18n.TranslationId("app.data_spillage.report.detail.no_data_found"), nil)
|
|
} else {
|
|
if deleteErr := a.DeletePriorityForPost(post.Id); deleteErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to delete post priority for the post", mlog.Err(deleteErr), mlog.String("post_id", post.Id))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.priority_data"), model.StepFailed, "", []string{deleteErr.Error()})
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.priority_data"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.deleted"), nil)
|
|
}
|
|
}
|
|
|
|
reminders, err := a.Srv().Store().Post().GetPostRemindersForPost(post.Id)
|
|
if err != nil && !errors.As(err, &nfErr) {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to get post reminders for the post", mlog.Err(err), mlog.String("post_id", post.Id))
|
|
}
|
|
|
|
if (err == nil && len(reminders) == 0) || errors.As(err, &nfErr) {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.reminders"), model.StepNotApplicable, i18n.TranslationId("app.data_spillage.report.detail.no_data_found"), nil)
|
|
} else {
|
|
if deleteErr := a.Srv().Store().Post().DeleteAllPostRemindersForPost(post.Id); deleteErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to delete post reminders for the post", mlog.Err(deleteErr), mlog.String("post_id", post.Id))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.reminders"), model.StepFailed, "", []string{deleteErr.Error()})
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.reminders"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.deleted"), nil)
|
|
}
|
|
}
|
|
|
|
if deleteErr := a.Srv().Store().Post().PermanentDeleteAssociatedData([]string{post.Id}); deleteErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to permanently delete associated data for the post", mlog.Err(deleteErr), mlog.String("post_id", post.Id))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.thread_data"), model.StepFailed, "", []string{deleteErr.Error()})
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.thread_data"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.thread_data_deleted"), nil)
|
|
}
|
|
|
|
postStepErrors := []string{}
|
|
postStepFailed := false
|
|
|
|
scrubPost(post)
|
|
_, err = a.Srv().Store().Post().Overwrite(rctx, post)
|
|
if err != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to scrub post content", mlog.Err(err), mlog.String("post_id", post.Id))
|
|
postStepErrors = append(postStepErrors, fmt.Sprintf("Failed to scrub post content: %s", err.Error()))
|
|
postStepFailed = true
|
|
}
|
|
|
|
// If the post is not already deleted, delete it now.
|
|
var deletePostErr *model.AppError
|
|
if post.DeleteAt == 0 {
|
|
// DeletePost is called to care of WebSocket events, cache invalidation, search index removal,
|
|
// and other cleanup tasks that need to happen on post deletion.
|
|
_, deletePostErr = a.DeletePost(rctx, post.Id, deleteByID)
|
|
if deletePostErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to delete the post after scrubbing content", mlog.Err(deletePostErr), mlog.String("post_id", post.Id))
|
|
|
|
postStepErrors = append(postStepErrors, deletePostErr.Error())
|
|
postStepFailed = true
|
|
}
|
|
}
|
|
|
|
if postStepFailed {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.post_itself"), model.StepFailed, "", postStepErrors)
|
|
} else {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.post_itself"), model.StepSuccess, i18n.TranslationId("app.data_spillage.report.detail.post_scrubbed_deleted"), nil)
|
|
}
|
|
|
|
return report, deletePostErr
|
|
}
|
|
|
|
func (a *App) deleteEditHistories(rctx request.CTX, postId, deleteByID string, report *model.PostDeletionReport) {
|
|
editHistories, appErr := a.GetEditHistoryForPost(postId)
|
|
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to get edit history for post", mlog.Err(appErr), mlog.String("post_id", postId))
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.edit_histories"), model.StepFailed, i18n.TranslationId("app.data_spillage.report.detail.failed_retrieve_edit_history"), []string{appErr.Error()})
|
|
|
|
return
|
|
}
|
|
|
|
if len(editHistories) == 0 {
|
|
report.AddStep(i18n.TranslationId("app.data_spillage.report.step.edit_histories"), model.StepNotApplicable, i18n.TranslationId("app.data_spillage.report.detail.no_data_found"), nil)
|
|
return
|
|
}
|
|
|
|
step := model.DeletionStepResult{
|
|
Name: i18n.TranslationId("app.data_spillage.report.step.edit_histories"),
|
|
SubSteps: make([]model.DeletionSubStep, 0, len(editHistories)),
|
|
}
|
|
|
|
allSuccess := true
|
|
anySuccess := false
|
|
|
|
for _, editHistory := range editHistories {
|
|
subStep := model.DeletionSubStep{Name: editHistory.Id}
|
|
|
|
if deletePostAppErr := a.PermanentDeletePost(rctx, editHistory.Id, deleteByID); deletePostAppErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to permanently delete one of the edit history posts", mlog.Err(deletePostAppErr), mlog.String("post_id", editHistory.Id))
|
|
subStep.Status = model.StepFailed
|
|
subStep.Errors = []string{deletePostAppErr.Error()}
|
|
allSuccess = false
|
|
} else {
|
|
subStep.Status = model.StepSuccess
|
|
anySuccess = true
|
|
}
|
|
|
|
step.SubSteps = append(step.SubSteps, subStep)
|
|
}
|
|
|
|
if allSuccess {
|
|
step.Status = model.StepSuccess
|
|
} else if anySuccess {
|
|
step.Status = model.StepPartial
|
|
} else {
|
|
step.Status = model.StepFailed
|
|
}
|
|
|
|
cleared := model.CountSubStepSuccesses(step.SubSteps)
|
|
total := len(step.SubSteps)
|
|
step.Detail = i18n.TranslationId("app.data_spillage.report.detail.revisions_cleared")
|
|
step.DetailParams = map[string]any{"Count": cleared, "Total": total}
|
|
report.Steps = append(report.Steps, step)
|
|
}
|
|
|
|
func (a *App) deleteFiles(rctx request.CTX, postId string, report *model.PostDeletionReport) {
|
|
appErr := a.PermanentDeleteFilesByPost(rctx, postId, report)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("PermanentDeletePostDataRetainStub: Failed to permanently delete files for the post", mlog.Err(appErr), mlog.String("post_id", postId))
|
|
}
|
|
}
|
|
|
|
func (a *App) KeepFlaggedPost(rctx request.CTX, actionRequest *model.FlagContentActionRequest, reviewerId string, flaggedPost *model.Post) *model.AppError {
|
|
// for keeping a flagged flaggedPost we need to-
|
|
// 1. Undelete the flaggedPost if it was deleted, that's it
|
|
|
|
status, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPost.Id, ContentFlaggingPropertyNameStatus)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
statusValue := strings.Trim(string(status.Value), `"`)
|
|
if statusValue != model.ContentFlaggingStatusPending && statusValue != model.ContentFlaggingStatusAssigned {
|
|
return model.NewAppError("KeepFlaggedPost", "api.data_spillage.error.post_not_in_progress", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
groupId, err := a.ContentFlaggingGroupId()
|
|
if err != nil {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
mappedFields, appErr := a.GetContentFlaggingMappedFields(groupId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
contentFlaggingManaged, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPost.Id, contentFlaggingPropertyManageByContentFlagging)
|
|
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
|
return appErr
|
|
}
|
|
|
|
postHiddenByContentFlagging := contentFlaggingManaged != nil && string(contentFlaggingManaged.Value) == "true"
|
|
|
|
if postHiddenByContentFlagging {
|
|
statusField, ok := mappedFields[ContentFlaggingPropertyNameStatus]
|
|
if !ok {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.missing_status_field.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
contentFlaggingManagedField, ok := mappedFields[contentFlaggingPropertyManageByContentFlagging]
|
|
if !ok {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.missing_manage_by_field.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
// Restore the post, its replies, and all associated files
|
|
if rErr := a.Srv().Store().Post().RestoreContentFlaggedPost(flaggedPost, statusField.ID, contentFlaggingManagedField.ID); rErr != nil {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.keep_post.undelete.app_error", nil, "", http.StatusInternalServerError).Wrap(rErr)
|
|
}
|
|
}
|
|
|
|
commentBytes, marshalErr := json.Marshal(actionRequest.Comment)
|
|
if marshalErr != nil {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.keep_flag_post.marshal_comment.app_error", nil, "", http.StatusInternalServerError).Wrap(marshalErr)
|
|
}
|
|
// Storing marshalled content into RawMessage to ensure proper escaping of special characters and prevent
|
|
// generating unsafe JSON values
|
|
commentJsonValue := json.RawMessage(commentBytes)
|
|
|
|
existingComment, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPost.Id, contentFlaggingPropertyNameActorComment)
|
|
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
|
return appErr
|
|
}
|
|
|
|
propertyValues := []*model.PropertyValue{
|
|
{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, reviewerId)),
|
|
},
|
|
{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
|
|
Value: json.RawMessage(fmt.Sprintf("%d", model.GetMillis())),
|
|
},
|
|
}
|
|
if existingComment == nil {
|
|
propertyValues = append(propertyValues, &model.PropertyValue{
|
|
TargetID: flaggedPost.Id,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
|
|
Value: commentJsonValue,
|
|
})
|
|
}
|
|
|
|
_, appErr = a.CreatePropertyValues(nil, propertyValues)
|
|
if appErr != nil {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.create_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
status.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
|
|
_, appErr = a.UpdatePropertyValue(nil, groupId, status)
|
|
if appErr != nil {
|
|
return model.NewAppError("KeepFlaggedPost", "app.data_spillage.keep_post.status_update.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
// Also need to remove the content flagging managed field value from root post and its replies (if any)
|
|
|
|
a.Srv().Go(func() {
|
|
channel, getChannelErr := a.GetChannel(rctx, flaggedPost.ChannelId)
|
|
if getChannelErr != nil {
|
|
rctx.Logger().Error("Failed to get channel for flagged post while publishing report change after permanently removing flagged post", mlog.Err(getChannelErr), mlog.String("post_id", flaggedPost.Id), mlog.String("channel_id", flaggedPost.ChannelId))
|
|
return
|
|
}
|
|
|
|
propertyValues = append(propertyValues, status)
|
|
if err := a.publishContentFlaggingReportUpdateEvent(flaggedPost.Id, channel.TeamId, propertyValues); err != nil {
|
|
rctx.Logger().Error("Failed to publish report change after permanently removing flagged flaggedPost", mlog.Err(err), mlog.String("post_id", flaggedPost.Id))
|
|
}
|
|
})
|
|
|
|
if postHiddenByContentFlagging {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", flaggedPost.ChannelId, "", nil, "")
|
|
appErr = a.publishWebsocketEventForPost(rctx, flaggedPost, message)
|
|
if appErr != nil {
|
|
rctx.Logger().Warn("Failed to publish websocket event for post edit while keeping flagged post", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
|
|
}
|
|
a.invalidateCacheForChannelPosts(flaggedPost.ChannelId)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
a.sendKeepFlaggedPostNotification(rctx, flaggedPost, reviewerId, actionRequest.Comment, groupId)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func scrubPost(post *model.Post) {
|
|
if post.Type == model.PostTypeBurnOnRead {
|
|
post.Message = "*Content deleted as part of burning the post*"
|
|
} else {
|
|
post.Message = "*Content deleted as part of Content Flagging review process*"
|
|
}
|
|
|
|
post.MessageSource = post.Message
|
|
post.Hashtags = ""
|
|
post.Metadata = nil
|
|
post.FileIds = []string{}
|
|
post.UpdateAt = model.GetMillis()
|
|
post.SetProps(make(map[string]any))
|
|
}
|
|
|
|
func (a *App) publishContentFlaggingReportUpdateEvent(targetId, teamId string, propertyValues []*model.PropertyValue) *model.AppError {
|
|
reviewersUserIDs, appErr := a.getReviewersForTeam(teamId, true)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
bytes, err := json.Marshal(propertyValues)
|
|
if err != nil {
|
|
return model.NewAppError("publishContentFlaggingReportUpdateEvent", "app.data_spillage.marshal_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, userId := range reviewersUserIDs {
|
|
message := model.NewWebSocketEvent(model.WebsocketContentFlaggingReportValueUpdated, "", "", userId, nil, "")
|
|
message.Add("property_values", string(bytes))
|
|
message.Add("target_id", targetId)
|
|
a.Publish(message)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SaveContentFlaggingConfig(config model.ContentFlaggingSettingsRequest) *model.AppError {
|
|
err := a.Srv().Store().ContentFlagging().SaveReviewerSettings(config.ReviewerSettings.ReviewerIDsSettings)
|
|
if err != nil {
|
|
return model.NewAppError("SaveContentFlaggingConfig", "app.data_spillage.save_reviewer_settings.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ContentFlaggingSettings = model.ContentFlaggingSettings{}
|
|
cfg.ContentFlaggingSettings.EnableContentFlagging = config.EnableContentFlagging
|
|
cfg.ContentFlaggingSettings.NotificationSettings = config.NotificationSettings
|
|
cfg.ContentFlaggingSettings.AdditionalSettings = config.AdditionalSettings
|
|
cfg.ContentFlaggingSettings.ReviewerSettings = &model.ReviewerSettings{
|
|
CommonReviewers: config.ReviewerSettings.CommonReviewers,
|
|
SystemAdminsAsReviewers: config.ReviewerSettings.SystemAdminsAsReviewers,
|
|
TeamAdminsAsReviewers: config.ReviewerSettings.TeamAdminsAsReviewers,
|
|
}
|
|
})
|
|
|
|
a.clearContentFlaggingConfigCache()
|
|
return nil
|
|
}
|
|
|
|
func (a *App) clearContentFlaggingConfigCache() {
|
|
a.Srv().Store().ContentFlagging().ClearCaches()
|
|
if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable {
|
|
cluster.SendClusterMessage(&model.ClusterMessage{
|
|
Event: model.ClusterEventInvalidateCacheForContentFlagging,
|
|
SendType: model.ClusterSendReliable,
|
|
Data: nil,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (a *App) GetContentFlaggingConfigReviewerIDs() (*model.ReviewerIDsSettings, *model.AppError) {
|
|
reviewerSettings, err := a.Srv().Store().ContentFlagging().GetReviewerSettings()
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetContentFlaggingConfigReviewerIDs", "app.data_spillage.get_reviewer_settings.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return reviewerSettings, nil
|
|
}
|
|
|
|
func (a *App) SearchReviewers(rctx request.CTX, term string, teamId string) ([]*model.User, *model.AppError) {
|
|
reviewerSettings := a.Config().ContentFlaggingSettings.ReviewerSettings
|
|
|
|
reviewers := map[string]*model.User{}
|
|
|
|
if reviewerSettings.CommonReviewers != nil && *reviewerSettings.CommonReviewers {
|
|
commonReviewers, err := a.Srv().Store().User().SearchCommonContentFlaggingReviewers(term)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchReviewers", "app.data_spillage.search_common_reviewers.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range commonReviewers {
|
|
reviewers[user.Id] = user
|
|
}
|
|
} else {
|
|
teamReviewers, err := a.Srv().Store().User().SearchTeamContentFlaggingReviewers(teamId, term)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchReviewers", "app.data_spillage.search_team_reviewers.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range teamReviewers {
|
|
reviewers[user.Id] = user
|
|
}
|
|
}
|
|
|
|
if reviewerSettings.SystemAdminsAsReviewers != nil && *reviewerSettings.SystemAdminsAsReviewers {
|
|
systemAdminReviewers, err := a.Srv().Store().User().Search(rctx, teamId, term, &model.UserSearchOptions{
|
|
AllowInactive: false,
|
|
Role: model.SystemAdminRoleId,
|
|
AllowEmails: false,
|
|
AllowFullNames: true,
|
|
Limit: CONTENT_FLAGGING_REVIEWER_SEARCH_INDIVIDUAL_LIMIT,
|
|
})
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchReviewers", "app.data_spillage.search_sysadmin_reviewers.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range systemAdminReviewers {
|
|
reviewers[user.Id] = user
|
|
}
|
|
}
|
|
|
|
if reviewerSettings.TeamAdminsAsReviewers != nil && *reviewerSettings.TeamAdminsAsReviewers {
|
|
teamAdminReviewers, err := a.Srv().Store().User().Search(rctx, teamId, term, &model.UserSearchOptions{
|
|
AllowInactive: false,
|
|
TeamRoles: []string{model.TeamAdminRoleId},
|
|
AllowEmails: false,
|
|
AllowFullNames: true,
|
|
Limit: CONTENT_FLAGGING_REVIEWER_SEARCH_INDIVIDUAL_LIMIT,
|
|
})
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchReviewers", "app.data_spillage.search_team_admin_reviewers.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range teamAdminReviewers {
|
|
reviewers[user.Id] = user
|
|
}
|
|
}
|
|
|
|
reviewersList := make([]*model.User, 0, len(reviewers))
|
|
for _, user := range reviewers {
|
|
a.SanitizeProfile(user, false)
|
|
reviewersList = append(reviewersList, user)
|
|
}
|
|
|
|
return reviewersList, nil
|
|
}
|
|
|
|
func (a *App) AssignFlaggedPostReviewer(rctx request.CTX, flaggedPostId, flaggedPostTeamId, reviewerId, assigneeId string) *model.AppError {
|
|
statusPropertyValue, appErr := a.GetPostContentFlaggingPropertyValue(flaggedPostId, ContentFlaggingPropertyNameStatus)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
status := strings.Trim(string(statusPropertyValue.Value), `"`)
|
|
|
|
groupId, err := a.ContentFlaggingGroupId()
|
|
if err != nil {
|
|
return model.NewAppError("AssignFlaggedPostReviewer", "app.data_spillage.get_group.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
mappedFields, appErr := a.GetContentFlaggingMappedFields(groupId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if _, ok := mappedFields[contentFlaggingPropertyNameReviewerUserID]; !ok {
|
|
return model.NewAppError("AssignFlaggedPostReviewer", "app.data_spillage.assign_reviewer.no_reviewer_field.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
assigneePropertyValue := &model.PropertyValue{
|
|
TargetID: flaggedPostId,
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
GroupID: groupId,
|
|
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, reviewerId)),
|
|
}
|
|
|
|
assigneePropertyValue, appErr = a.UpsertPropertyValue(rctx, assigneePropertyValue)
|
|
if appErr != nil {
|
|
return model.NewAppError("AssignFlaggedPostReviewer", "app.data_spillage.assign_reviewer.upsert_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
if status == model.ContentFlaggingStatusPending {
|
|
statusPropertyValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusAssigned))
|
|
statusPropertyValue, appErr = a.UpdatePropertyValue(rctx, groupId, statusPropertyValue)
|
|
if appErr != nil {
|
|
return model.NewAppError("AssignFlaggedPostReviewer", "app.data_spillage.assign_reviewer.update_status_property_value.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
_, 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))
|
|
}
|
|
})
|
|
|
|
a.Srv().Go(func() {
|
|
updateEventAppErr := a.publishContentFlaggingReportUpdateEvent(flaggedPostId, flaggedPostTeamId, []*model.PropertyValue{assigneePropertyValue, statusPropertyValue})
|
|
if updateEventAppErr != nil {
|
|
rctx.Logger().Error("Failed to publish report change after assigning reviewer", mlog.Err(updateEventAppErr), mlog.String("flagged_post_id", flaggedPostId), mlog.String("reviewer_id", reviewerId), mlog.String("assignee_id", assigneeId))
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
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, nil
|
|
}
|
|
|
|
if !slices.Contains(notificationSettings.EventTargetMapping[model.EventAssigned], model.TargetReviewers) {
|
|
return nil, nil
|
|
}
|
|
|
|
reviewerUser, appErr := a.GetUser(reviewerId)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
var assignedByUser *model.User
|
|
if reviewerId == assignedById {
|
|
assignedByUser = reviewerUser
|
|
} else {
|
|
assignedByUser, appErr = a.GetUser(assignedById)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
}
|
|
|
|
message := fmt.Sprintf("@%s was assigned as a reviewer by @%s", reviewerUser.Username, assignedByUser.Username)
|
|
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId, nil, "")
|
|
}
|
|
|
|
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 nil, appErr
|
|
}
|
|
|
|
message := fmt.Sprintf("The quarantined message was removed by @%s", actorUser.Username)
|
|
if comment != "" {
|
|
message = fmt.Sprintf("%s\n\nWith comment:\n\n> %s", message, comment)
|
|
}
|
|
|
|
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId, nil, "")
|
|
}
|
|
|
|
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 nil, appErr
|
|
}
|
|
|
|
message := fmt.Sprintf("The quarantined message was retained by @%s", actorUser.Username)
|
|
if comment != "" {
|
|
message = fmt.Sprintf("%s\n\nWith comment:\n\n> %s", message, comment)
|
|
}
|
|
|
|
return a.postReviewerMessage(rctx, message, contentFlaggingGroupId, flaggedPostId, nil, "")
|
|
}
|
|
|
|
func (a *App) getReporterUserId(flaggedPostId, contentFlaggingGroupId string) (string, *model.AppError) {
|
|
mappedFields, appErr := a.GetContentFlaggingMappedFields(contentFlaggingGroupId)
|
|
if appErr != nil {
|
|
return "", appErr
|
|
}
|
|
|
|
reporterUserIdField, ok := mappedFields[contentFlaggingPropertyNameReportingUserID]
|
|
if !ok {
|
|
return "", model.NewAppError("getReporterUserId", "app.data_spillage.missing_reporting_user_id_field.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
propertyValues, appErr := a.GetPostContentFlaggingPropertyValues(flaggedPostId)
|
|
if appErr != nil {
|
|
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.data_spillage.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, message string, recipientUserId string) (*model.Post, *model.AppError) {
|
|
contentReviewBot, appErr := a.getContentReviewBot(rctx)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
dmChannel, appErr := a.GetOrCreateDirectChannel(rctx, recipientUserId, contentReviewBot.UserId)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
post := &model.Post{
|
|
Message: message,
|
|
UserId: contentReviewBot.UserId,
|
|
ChannelId: dmChannel.Id,
|
|
}
|
|
|
|
// We can ignore the membership since the post itself is does not have a permalink
|
|
createdPost, _, appErr := a.CreatePost(rctx, post, dmChannel, model.CreatePostFlags{})
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
return createdPost, nil
|
|
}
|
|
|
|
func (a *App) postMessageToReporter(rctx request.CTX, contentFlaggingGroupId string, flaggedPost *model.Post, message string) (*model.Post, *model.AppError) {
|
|
userId, appErr := a.getReporterUserId(flaggedPost.Id, contentFlaggingGroupId)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return a.postContentReviewBotMessage(rctx, message, userId)
|
|
}
|
|
|
|
func (a *App) postReviewerMessage(rctx request.CTX, message, contentFlaggingGroupId, flaggedPostId string, report *model.PostDeletionReport, reportFileName 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.data_spillage.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 {
|
|
rctx.Logger().Error("Failed to get reviewer post while posting assign reviewer message", mlog.Err(appErr), mlog.String("post_id", postId))
|
|
continue
|
|
}
|
|
|
|
channel, appErr := a.GetChannel(rctx, reviewerPost.ChannelId)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to get channel for reviewer post while posting assign reviewer message", mlog.Err(appErr), mlog.String("post_id", postId), mlog.String("channel_id", reviewerPost.ChannelId))
|
|
continue
|
|
}
|
|
|
|
// Determine the post message and file data, localizing per-reviewer if a report is provided
|
|
postMessage := message
|
|
var postFileData []byte
|
|
var postFileName string
|
|
|
|
if report != nil {
|
|
T := i18n.GetUserTranslations("")
|
|
// Fetch reviewer user to get their locale
|
|
reviewerUserId := channel.GetOtherUserIdForDM(reviewerPost.UserId)
|
|
reviewer, userErr := a.GetUser(reviewerUserId)
|
|
if userErr != nil {
|
|
rctx.Logger().Error("Failed to get reviewer user for localization, falling back to default locale", mlog.Err(userErr), mlog.String("user_id", reviewerPost.UserId))
|
|
} else {
|
|
T = i18n.GetUserTranslations(reviewer.Locale)
|
|
}
|
|
|
|
postMessage = report.RenderSummary(T)
|
|
postFileData = []byte(report.Render(T))
|
|
postFileName = reportFileName
|
|
}
|
|
|
|
post := &model.Post{
|
|
Message: postMessage,
|
|
UserId: contentReviewBot.UserId,
|
|
ChannelId: reviewerPost.ChannelId,
|
|
RootId: postId,
|
|
}
|
|
|
|
// Upload file attachment if provided
|
|
if len(postFileData) > 0 {
|
|
fileInfo, uploadErr := a.UploadFile(rctx, postFileData, reviewerPost.ChannelId, postFileName)
|
|
if uploadErr != nil {
|
|
// When the report fails to upload, the details aren't lost as the logs of any item which failed to be deleted are still in the server logs.
|
|
rctx.Logger().Error("Failed to upload report file attachment, appending to message", mlog.Err(uploadErr), mlog.String("post_id", postId))
|
|
} else {
|
|
post.FileIds = []string{fileInfo.Id}
|
|
}
|
|
}
|
|
|
|
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 createdPosts, nil
|
|
}
|
|
|
|
func (a *App) getReviewerPostsForFlaggedPost(contentFlaggingGroupId, flaggedPostId, flaggedPostIdFieldId string) ([]string, *model.AppError) {
|
|
searchOptions := model.PropertyValueSearchOpts{
|
|
TargetType: model.PropertyValueTargetTypePost,
|
|
Value: json.RawMessage(fmt.Sprintf(`"%s"`, flaggedPostId)),
|
|
FieldID: flaggedPostIdFieldId,
|
|
PerPage: 100,
|
|
Cursor: model.PropertyValueSearchCursor{},
|
|
}
|
|
|
|
var propertyValues []*model.PropertyValue
|
|
|
|
for {
|
|
batch, appErr := a.SearchPropertyValues(nil, contentFlaggingGroupId, searchOptions)
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("getReviewerPostsForFlaggedPost", "app.data_spillage.search_reviewer_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
propertyValues = append(propertyValues, batch...)
|
|
|
|
if len(batch) < searchOptions.PerPage {
|
|
break
|
|
}
|
|
|
|
searchOptions.Cursor.PropertyValueID = propertyValues[len(propertyValues)-1].ID
|
|
searchOptions.Cursor.CreateAt = propertyValues[len(propertyValues)-1].CreateAt
|
|
}
|
|
|
|
reviewerPostIds := make([]string, 0, len(propertyValues))
|
|
for _, pv := range propertyValues {
|
|
reviewerPostIds = append(reviewerPostIds, pv.TargetID)
|
|
}
|
|
|
|
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 quarantined for review.", flaggedPost.Id, channel.DisplayName),
|
|
UserId: contentReviewBot.UserId,
|
|
ChannelId: dmChannel.Id,
|
|
}
|
|
|
|
_, _, appErr = a.CreatePost(rctx, post, dmChannel, model.CreatePostFlags{})
|
|
return appErr
|
|
}
|
|
|
|
// sendFlaggedPostRemovalNotification handles the notifications when flagged post is removed for all audiences - reviewers, author, and reporter as per configuration
|
|
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
|
|
}
|
|
|
|
channel, appErr := a.GetChannel(rctx, flaggedPost.ChannelId)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to get channel for notification", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
|
|
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) {
|
|
msg := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been permanently removed by a reviewer.", flaggedPost.Id, channel.DisplayName)
|
|
post, appErr := a.postContentReviewBotMessage(rctx, msg, 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) {
|
|
msg := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been permanently removed by a reviewer.", flaggedPost.Id, channel.DisplayName)
|
|
post, appErr := a.postMessageToReporter(rctx, contentFlaggingGroupId, flaggedPost, msg)
|
|
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
|
|
}
|
|
|
|
// sendKeepFlaggedPostNotification handles the notifications when flagged post is retained for all audiences - reviewers, author, and reporter as per configuration
|
|
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
|
|
}
|
|
|
|
channel, appErr := a.GetChannel(rctx, flaggedPost.ChannelId)
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to get channel for notification", mlog.Err(appErr), mlog.String("post_id", flaggedPost.Id))
|
|
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) {
|
|
msg := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been restored by a reviewer.", flaggedPost.Id, channel.DisplayName)
|
|
post, appErr := a.postContentReviewBotMessage(rctx, msg, 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) {
|
|
msg := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been restored by a reviewer.", flaggedPost.Id, channel.DisplayName)
|
|
post, appErr := a.postMessageToReporter(rctx, contentFlaggingGroupId, flaggedPost, msg)
|
|
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
|
|
}
|