| /* |
| * 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 |
| minimumTags, err := rs.tagCommon.GetMinimumTags(ctx) |
| if err != nil { |
| return err |
| } |
| _, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags) |
| 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 |
| } |