internal/service/content/revision_service.go (479 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package content import ( "context" "encoding/json" "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/activity_queue" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/notice_queue" "github.com/apache/answer/internal/service/object_info" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision" "github.com/apache/answer/internal/service/tag_common" tagcommon "github.com/apache/answer/internal/service/tag_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // RevisionService user service type RevisionService struct { revisionRepo revision.RevisionRepo userCommon *usercommon.UserCommon questionCommon *questioncommon.QuestionCommon answerService *AnswerService objectInfoService *object_info.ObjService questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo tagRepo tag_common.TagRepo tagCommon *tagcommon.TagCommonService notificationQueueService notice_queue.NotificationQueueService activityQueueService activity_queue.ActivityQueueService reportRepo report_common.ReportRepo reviewService *review.ReviewService reviewActivity activity.ReviewActivityRepo } func NewRevisionService( revisionRepo revision.RevisionRepo, userCommon *usercommon.UserCommon, questionCommon *questioncommon.QuestionCommon, answerService *AnswerService, objectInfoService *object_info.ObjService, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, tagRepo tag_common.TagRepo, tagCommon *tagcommon.TagCommonService, notificationQueueService notice_queue.NotificationQueueService, activityQueueService activity_queue.ActivityQueueService, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, reviewActivity activity.ReviewActivityRepo, ) *RevisionService { return &RevisionService{ revisionRepo: revisionRepo, userCommon: userCommon, questionCommon: questionCommon, answerService: answerService, objectInfoService: objectInfoService, questionRepo: questionRepo, answerRepo: answerRepo, tagRepo: tagRepo, tagCommon: tagCommon, notificationQueueService: notificationQueueService, activityQueueService: activityQueueService, reportRepo: reportRepo, reviewService: reviewService, reviewActivity: reviewActivity, } } func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) { revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID) if err != nil { return } if !exist { return } if revisioninfo.Status != entity.RevisionUnreviewedStatus { return } if req.Operation == schema.RevisionAuditReject { err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID) return } if req.Operation == schema.RevisionAuditApprove { objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) if objectTypeerr != nil { return objectTypeerr } revisionitem := &schema.GetRevisionResp{} _ = copier.Copy(revisionitem, revisioninfo) rs.parseItem(ctx, revisionitem) var saveErr error switch objectType { case constant.QuestionObjectType: if !req.CanReviewQuestion { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditQuestion(ctx, revisionitem) } case constant.AnswerObjectType: if !req.CanReviewAnswer { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditAnswer(ctx, revisionitem) } case constant.TagObjectType: if !req.CanReviewTag { saveErr = errors.BadRequest(reason.RevisionNoPermission) } else { saveErr = rs.revisionAuditTag(ctx, revisionitem) } } if saveErr != nil { return saveErr } err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID) if err != nil { return err } err = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{ UserID: revisioninfo.UserID, TriggerUserID: req.UserID, ObjectID: revisioninfo.ObjectID, OriginalObjectID: "0", RevisionID: revisioninfo.ID, }) if err != nil { log.Errorf("add review activity failed: %v", err) } msg := &schema.NotificationMsg{ TriggerUserID: req.UserID, ReceiverUserID: revisioninfo.UserID, Type: schema.NotificationTypeAchievement, ObjectID: revisioninfo.ObjectID, ObjectType: objectType, } rs.notificationQueueService.Send(ctx, msg) return } return nil } func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp) if ok { var PostUpdateTime time.Time dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID) if dberr != nil || !exist { return } PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0) if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { PostUpdateTime = dbquestion.PostUpdateTime } question := &entity.Question{} question.ID = questioninfo.ID question.Title = questioninfo.Title question.OriginalText = questioninfo.Content question.ParsedText = questioninfo.HTML question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0) question.PostUpdateTime = PostUpdateTime question.LastEditUserID = revisionitem.UserID saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) if saveerr != nil { return saveerr } objectTagTags := make([]*schema.TagItem, 0) for _, tag := range questioninfo.Tags { item := &schema.TagItem{} item.SlugName = tag.SlugName objectTagTags = append(objectTagTags, item) } objectTagData := schema.TagChange{} objectTagData.ObjectID = question.ID objectTagData.Tags = objectTagTags saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData) if saveerr != nil { return saveerr } rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: revisionitem.ObjectID, ActivityTypeKey: constant.ActQuestionEdited, RevisionID: revisionitem.ID, OriginalObjectID: revisionitem.ObjectID, }) } return nil } func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo) if ok { var PostUpdateTime time.Time dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) if dberr != nil || !exist { return } PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0) if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { PostUpdateTime = dbquestion.PostUpdateTime } insertData := new(entity.Answer) insertData.ID = answerinfo.ID insertData.OriginalText = answerinfo.Content insertData.ParsedText = answerinfo.HTML insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0) insertData.LastEditUserID = revisionitem.UserID saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}) if saveerr != nil { return saveerr } saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) if saveerr != nil { return saveerr } questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) if err != nil { return err } if !exist { return errors.BadRequest(reason.QuestionNotFound) } msg := &schema.NotificationMsg{ TriggerUserID: revisionitem.UserID, ReceiverUserID: questionInfo.UserID, Type: schema.NotificationTypeInbox, ObjectID: answerinfo.ID, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationUpdateAnswer rs.notificationQueueService.Send(ctx, msg) rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, ActivityTypeKey: constant.ActAnswerEdited, RevisionID: revisionitem.ID, }) } return nil } func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp) if ok { tag := &entity.Tag{} tag.ID = taginfo.TagID tag.OriginalText = taginfo.OriginalText tag.ParsedText = taginfo.ParsedText saveerr := rs.tagRepo.UpdateTag(ctx, tag) if saveerr != nil { return saveerr } tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID) if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 { log.Debugf("tag %s update slug_name", tagInfo.SlugName) tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) if err != nil { return err } updateTagSlugNames := make([]string, 0) for _, tag := range tagList { updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) } err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) if err != nil { return err } } rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: taginfo.TagID, OriginalObjectID: taginfo.TagID, ActivityTypeKey: constant.ActTagEdited, RevisionID: revisionitem.ID, }) } return nil } // GetUnreviewedRevisionPage get unreviewed list func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) ( resp *pager.PageModel, err error) { revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0) if len(req.GetCanReviewObjectTypes()) == 0 { return pager.NewPageModel(0, revisionResp), nil } revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage( ctx, req.Page, 1, req.GetCanReviewObjectTypes()) if err != nil { return nil, err } for _, rev := range revisionPage { item := &schema.GetUnreviewedRevisionResp{} _, ok := constant.ObjectTypeNumberMapping[rev.ObjectType] if !ok { continue } item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType] info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID) if err != nil { return nil, err } item.Info = info revisionitem := &schema.GetRevisionResp{} _ = copier.Copy(revisionitem, rev) rs.parseItem(ctx, revisionitem) item.UnreviewedInfo = revisionitem // get user info userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID) if e != nil { return nil, e } if exists { var uinfo schema.UserBasicInfo _ = copier.Copy(&uinfo, userInfo) item.UnreviewedInfo.UserInfo = uinfo } item.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title) item.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title) revisionResp = append(revisionResp, item) } return pager.NewPageModel(total, revisionResp), nil } // GetRevisionList get revision list all func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { var ( rev entity.Revision revs []entity.Revision ) resp = []schema.GetRevisionResp{} _ = copier.Copy(&rev, req) revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev) if err != nil { return } for _, r := range revs { var ( uinfo schema.UserBasicInfo item schema.GetRevisionResp ) _ = copier.Copy(&item, r) rs.parseItem(ctx, &item) // get user info userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID) if e != nil { return nil, e } if exists { err = copier.Copy(&uinfo, userInfo) item.UserInfo = uinfo } resp = append(resp, item) } return } func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) { var ( err error question entity.QuestionWithTagsRevision questionInfo *schema.QuestionInfoResp answer entity.Answer answerInfo *schema.AnswerInfo tag entity.Tag tagInfo *schema.GetTagResp ) shortID := handler.GetEnableShortID(ctx) if shortID { item.ObjectID = uid.EnShortID(item.ObjectID) } switch item.ObjectType { case constant.ObjectTypeStrMapping["question"]: err = json.Unmarshal([]byte(item.Content), &question) if err != nil { break } questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question) if shortID { questionInfo.ID = uid.EnShortID(questionInfo.ID) } item.ContentParsed = questionInfo case constant.ObjectTypeStrMapping["answer"]: err = json.Unmarshal([]byte(item.Content), &answer) if err != nil { break } answerInfo = rs.answerService.ShowFormat(ctx, &answer) if shortID { answerInfo.ID = uid.EnShortID(answerInfo.ID) answerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID) } item.ContentParsed = answerInfo case constant.ObjectTypeStrMapping["tag"]: err = json.Unmarshal([]byte(item.Content), &tag) if err != nil { break } tagInfo = &schema.GetTagResp{ TagID: tag.ID, CreatedAt: tag.CreatedAt.Unix(), UpdatedAt: tag.UpdatedAt.Unix(), SlugName: tag.SlugName, DisplayName: tag.DisplayName, OriginalText: tag.OriginalText, ParsedText: tag.ParsedText, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, Recommend: tag.Recommend, Reserved: tag.Reserved, } tagInfo.GetExcerpt() item.ContentParsed = tagInfo } if err != nil { item.ContentParsed = item.Content } item.CreatedAtParsed = item.CreatedAt.Unix() } // CheckCanUpdateRevision can check revision func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) ( resp *schema.ErrTypeData, err error) { _, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID) if err != nil { return nil, nil } if exist { return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway) } return nil, nil } // GetReviewingType get reviewing type func (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) { resp = make([]*schema.GetReviewingTypeResp, 0) // get queue amount if req.IsAdmin { reviewCount, err := rs.reviewService.GetReviewPendingCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.QueuedPost), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel), TodoAmount: reviewCount, }) } } // get flag amount if req.IsAdmin { reportCount, err := rs.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.FlaggedPost), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel), TodoAmount: reportCount, }) } } // get suggestion amount countUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) if err != nil { log.Errorf("get unreviewed revision count failed: %v", err) } else { resp = append(resp, &schema.GetReviewingTypeResp{ Name: string(constant.SuggestedPostEdit), Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel), TodoAmount: countUnreviewedRevision, }) } return resp, nil }