diff --git a/Makefile b/Makefile index 637257f6e..9da39bec7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=1.2.5 +VERSION=1.3.0 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/README.md b/README.md index c0509510e..c3b30b717 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache ### Running with docker ```bash -docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.2.5 +docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.3.0 ``` For more information, see [Installation](https://answer.apache.org/docs/installation). diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 199c2ce9f..a2027d285 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -1,28 +1,8 @@ -//go:build !wireinject -// +build !wireinject - -/* - * 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. - */ - // Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject package answercmd @@ -53,6 +33,7 @@ import ( "github.com/apache/incubator-answer/internal/repo/rank" "github.com/apache/incubator-answer/internal/repo/reason" "github.com/apache/incubator-answer/internal/repo/report" + "github.com/apache/incubator-answer/internal/repo/review" "github.com/apache/incubator-answer/internal/repo/revision" "github.com/apache/incubator-answer/internal/repo/role" "github.com/apache/incubator-answer/internal/repo/search_common" @@ -64,17 +45,18 @@ import ( "github.com/apache/incubator-answer/internal/repo/user_external_login" "github.com/apache/incubator-answer/internal/repo/user_notification_config" "github.com/apache/incubator-answer/internal/router" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" activity2 "github.com/apache/incubator-answer/internal/service/activity" activity_common2 "github.com/apache/incubator-answer/internal/service/activity_common" "github.com/apache/incubator-answer/internal/service/activity_queue" "github.com/apache/incubator-answer/internal/service/answer_common" auth2 "github.com/apache/incubator-answer/internal/service/auth" + collection2 "github.com/apache/incubator-answer/internal/service/collection" "github.com/apache/incubator-answer/internal/service/collection_common" comment2 "github.com/apache/incubator-answer/internal/service/comment" "github.com/apache/incubator-answer/internal/service/comment_common" config2 "github.com/apache/incubator-answer/internal/service/config" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/dashboard" export2 "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/follow" @@ -88,8 +70,8 @@ import ( rank2 "github.com/apache/incubator-answer/internal/service/rank" reason2 "github.com/apache/incubator-answer/internal/service/reason" report2 "github.com/apache/incubator-answer/internal/service/report" - "github.com/apache/incubator-answer/internal/service/report_admin" - "github.com/apache/incubator-answer/internal/service/report_handle_admin" + "github.com/apache/incubator-answer/internal/service/report_handle" + review2 "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision_common" role2 "github.com/apache/incubator-answer/internal/service/role" "github.com/apache/incubator-answer/internal/service/search_parser" @@ -152,14 +134,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userNotificationConfigRepo := user_notification_config.NewUserNotificationConfigRepo(dataData) userNotificationConfigService := user_notification_config2.NewUserNotificationConfigService(userRepo, userNotificationConfigRepo) userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo, userNotificationConfigService) - userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService) - captchaRepo := captcha.NewCaptchaRepo(dataData) - captchaService := action.NewCaptchaService(captchaRepo) - userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService) - commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) - commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) - answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo) + answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) + voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) + followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo) tagRelRepo := tag.NewTagRelRepo(dataData, uniqueIDRepo) tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) @@ -167,8 +145,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) activityQueueService := activity_queue.NewActivityQueueService() tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, activityQueueService) + collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) + collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) + answerCommon := answercommon.NewAnswerCommon(answerRepo) + metaRepo := meta.NewMetaRepo(dataData) + metaService := meta2.NewMetaService(metaRepo) + questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, revisionRepo, dataData) + userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon) + captchaRepo := captcha.NewCaptchaRepo(dataData) + captchaService := action.NewCaptchaService(captchaRepo) + userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService) + commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) + commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) - voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) notificationQueueService := notice_queue.NewNotificationQueueService() externalNotificationQueueService := notice_queue.NewNewQuestionNotificationQueueService() commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService) @@ -179,43 +168,37 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo) commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) - reportService := report2.NewReportService(reportRepo, objService) + answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) + externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService) + reviewRepo := review.NewReviewRepo(dataData) + reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, notificationQueueService, siteInfoCommonService) + questionService := content.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService) + answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService) + reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) + reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService) reportController := controller.NewReportController(reportService, rankService, captchaService) - serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) - voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) + contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) voteController := controller.NewVoteController(voteService, rankService, captchaService) - followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) followController := controller.NewFollowController(followService) - collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) - collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) - answerCommon := answercommon.NewAnswerCommon(answerRepo) - metaRepo := meta.NewMetaRepo(dataData) - metaService := meta2.NewMetaService(metaRepo) - questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, dataData) - collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) + collectionService := collection2.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) collectionController := controller.NewCollectionController(collectionService) - answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) - answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) - externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService) - answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService) questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware) answerController := controller.NewAnswerController(answerService, rankService, captchaService, siteInfoCommonService, rateLimitMiddleware) searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon, tagCommonService) - searchService := service.NewSearchService(searchParser, searchRepo) + searchService := content.NewSearchService(searchParser, searchRepo) searchController := controller.NewSearchController(searchService, captchaService) - serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, notificationQueueService, activityQueueService) - revisionController := controller.NewRevisionController(serviceRevisionService, rankService) + reviewActivityRepo := activity.NewReviewActivityRepo(dataData, activityRepo, userRankRepo, configService) + contentRevisionService := content.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, notificationQueueService, activityQueueService, reportRepo, reviewService, reviewActivityRepo) + revisionController := controller.NewRevisionController(contentRevisionService, rankService) rankController := controller.NewRankController(rankService) - reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configService, notificationQueueService) - reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, objService) - controller_adminReportController := controller_admin.NewReportController(reportAdminService) userAdminRepo := user.NewUserAdminRepo(dataData, authRepo) userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo) userAdminController := controller_admin.NewUserAdminController(userAdminService) @@ -228,9 +211,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) notificationRepo := notification2.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService, userExternalLoginRepo, siteInfoCommonService) - notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo) + notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService) notificationController := controller.NewNotificationController(notificationService, rankService) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData) dashboardController := controller.NewDashboardController(dashboardService) uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) uploadController := controller.NewUploadController(uploaderService) @@ -246,7 +229,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, pluginController := controller_admin.NewPluginController(pluginCommonService) permissionController := controller.NewPermissionController(rankService) userPluginController := controller.NewUserPluginController(pluginCommonService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController) + reviewController := controller.NewReviewController(reviewService, rankService, captchaService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) diff --git a/docs/docs.go b/docs/docs.go index dc56880a8..30a8e1bc0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,22 +1,3 @@ -/* - * 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. - */ - // Code generated by swaggo/swag. DO NOT EDIT. package docs @@ -58,7 +39,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], @@ -85,7 +66,8 @@ const docTemplate = `{ { "enum": [ "available", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", @@ -388,7 +370,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Status:[available,closed,deleted]", + "description": "Status:[available,closed,deleted,pending]", "consumes": [ "application/json" ], @@ -416,7 +398,8 @@ const docTemplate = `{ "enum": [ "available", "closed", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", @@ -535,117 +518,6 @@ const docTemplate = `{ } } }, - "/answer/admin/api/report/": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "ApiKeyAuth": [] - } - ], - "description": "handle flag", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "handle flag", - "parameters": [ - { - "description": "flag", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.ReportHandleReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, - "/answer/admin/api/reports/page": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "ApiKeyAuth": [] - } - ], - "description": "list report records", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "list report page", - "parameters": [ - { - "enum": [ - "pending", - "completed" - ], - "type": "string", - "description": "status", - "name": "status", - "in": "query", - "required": true - }, - { - "enum": [ - "all", - "question", - "answer", - "comment" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/admin/api/roles": { "get": { "description": "get role list", @@ -4466,94 +4338,122 @@ const docTemplate = `{ } } }, - "/answer/api/v1/revisions": { - "get": { - "description": "get revision list", + "/answer/api/v1/report/review": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "review report", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Report" ], - "summary": "get revision list", + "summary": "review report", "parameters": [ { - "type": "string", - "description": "object id", - "name": "object_id", - "in": "query", - "required": true + "description": "flag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReviewReportReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRevisionResp" - } - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/revisions/audit": { - "put": { + "/answer/api/v1/report/unreviewed/post": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "revision audit operation:approve or reject", + "description": "get unreviewed report post page", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Report" ], - "summary": "revision audit", + "summary": "get unreviewed report post page", "parameters": [ { - "description": "audit", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RevisionAuditReq" - } + "type": "integer", + "description": "page", + "name": "page", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReportListPageResp" + } + } + } + } + ] + } + } + } + ] } } } } }, - "/answer/api/v1/revisions/edit/check": { - "get": { + "/answer/api/v1/review/pending/post": { + "put": { "security": [ + { + "ApiKeyAuth": [] + }, { "ApiKeyAuth": [] } ], - "description": "check can update revision", + "description": "update review", "consumes": [ "application/json" ], @@ -4561,17 +4461,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Revision" + "Review" ], - "summary": "check can update revision", + "summary": "update review", "parameters": [ { - "type": "string", - "default": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true + "description": "review", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateReviewReq" + } } ], "responses": { @@ -4584,28 +4485,36 @@ const docTemplate = `{ } } }, - "/answer/api/v1/revisions/unreviewed": { + "/answer/api/v1/review/pending/post/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get unreviewed revision list", + "description": "get unreviewed post page", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Review" ], - "summary": "get unreviewed revision list", + "summary": "get unreviewed post page", "parameters": [ { - "type": "string", - "description": "page id", + "type": "integer", + "description": "page", "name": "page", - "in": "query", - "required": true + "in": "query" + }, + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query" } ], "responses": { @@ -4630,7 +4539,7 @@ const docTemplate = `{ "list": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" } } } @@ -4645,34 +4554,253 @@ const docTemplate = `{ } } }, - "/answer/api/v1/search": { + "/answer/api/v1/reviewing/type": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "search object", + "description": "get reviewing type", "produces": [ "application/json" ], "tags": [ - "Search" + "Revision" ], - "summary": "search object", - "parameters": [ - { - "type": "string", - "description": "query string", - "name": "q", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "active", - "score", + "summary": "get reviewing type", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReviewingTypeResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions": { + "get": { + "description": "get revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get revision list", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "search object", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "search object", + "parameters": [ + { + "type": "string", + "description": "query string", + "name": "q", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "active", + "score", "relevance" ], "type": "string", @@ -6554,7 +6682,8 @@ const docTemplate = `{ "type": "string" }, "value": { - "type": "integer" + "type": "integer", + "minimum": 1 } } }, @@ -7094,6 +7223,12 @@ const docTemplate = `{ "action": { "$ref": "#/definitions/schema.UIOptionAction" }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, "input_type": { "type": "string" }, @@ -7374,6 +7509,10 @@ const docTemplate = `{ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" @@ -7686,36 +7825,122 @@ const docTemplate = `{ } } }, - "schema.GetRevisionResp": { + "schema.GetReportListPageResp": { "type": "object", "properties": { - "content": { - "description": "content parsed" + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "flag_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/schema.ReasonItem" + }, + "reason_content": { + "type": "string" + }, + "submit_at": { + "type": "integer" + }, + "submitter_user": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetReviewingTypeResp": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "todo_amount": { + "type": "integer" + } + } + }, + "schema.GetRevisionResp": { + "type": "object", + "properties": { + "content": {}, "create_at": { "type": "integer" }, "id": { - "description": "id", "type": "string" }, "object_id": { - "description": "object id", "type": "string" }, "reason": { "type": "string" }, "status": { - "description": "revision status(normal: 1; delete 2)", "type": "integer" }, "title": { - "description": "title", + "type": "string" + }, + "url_title": { "type": "string" }, "use_id": { - "description": "user id", "type": "string" }, "user_info": { @@ -7922,6 +8147,73 @@ const docTemplate = `{ } } }, + "schema.GetUnreviewedPostPageResp": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "review_id": { + "type": "integer" + }, + "submit_at": { + "type": "integer" + }, + "submitter_display_name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, "schema.GetUnreviewedRevisionResp": { "type": "object", "properties": { @@ -8161,12 +8453,14 @@ const docTemplate = `{ "enum": [ 1, 2, - 3 + 3, + 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", - "PrivilegeLevel3" + "PrivilegeLevel3", + "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { @@ -8484,6 +8778,29 @@ const docTemplate = `{ } } }, + "schema.ReasonItem": { + "type": "object", + "properties": { + "content_type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "reason_key": { + "type": "string" + }, + "reason_type": { + "type": "integer" + } + } + }, "schema.RecoverAnswerReq": { "type": "object", "required": [ @@ -8581,21 +8898,47 @@ const docTemplate = `{ } } }, - "schema.ReportHandleReq": { + "schema.ReviewReportReq": { "type": "object", "required": [ - "flagged_type", - "id" + "flag_id", + "operation_type" ], "properties": { - "flagged_content": { + "close_msg": { "type": "string" }, - "flagged_type": { + "close_type": { "type": "integer" }, - "id": { + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "flag_id": { "type": "string" + }, + "operation_type": { + "type": "string", + "enum": [ + "edit_post", + "close_post", + "delete_post", + "unlist_post", + "ignore_report" + ] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, @@ -8830,6 +9173,9 @@ const docTemplate = `{ "site_url" ], "properties": { + "check_update": { + "type": "boolean" + }, "contact_email": { "type": "string", "maxLength": 512 @@ -8860,6 +9206,9 @@ const docTemplate = `{ "site_url" ], "properties": { + "check_update": { + "type": "boolean" + }, "contact_email": { "type": "string", "maxLength": 512 @@ -9074,6 +9423,10 @@ const docTemplate = `{ "theme" ], "properties": { + "color_scheme": { + "type": "string", + "maxLength": 100 + }, "theme": { "type": "string", "maxLength": 255 @@ -9087,6 +9440,9 @@ const docTemplate = `{ "schema.SiteThemeResp": { "type": "object", "properties": { + "color_scheme": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9312,15 +9668,45 @@ const docTemplate = `{ "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" + }, + "comment_id": { + "type": "string" + }, "content": { "type": "string" }, + "created_at": { + "type": "integer" + }, "html": { "type": "string" }, + "object_creator_user_id": { + "type": "string" + }, "object_id": { "type": "string" }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "show_status": { + "type": "integer" + }, + "status": { + "type": "integer" + }, "tags": { "type": "array", "items": { @@ -9329,6 +9715,9 @@ const docTemplate = `{ }, "title": { "type": "string" + }, + "url_title": { + "type": "string" } } }, @@ -9435,8 +9824,13 @@ const docTemplate = `{ "level" ], "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, "level": { - "maximum": 3, "minimum": 1, "allOf": [ { @@ -9446,6 +9840,25 @@ const docTemplate = `{ } } }, + "schema.UpdateReviewReq": { + "type": "object", + "required": [ + "review_id", + "status" + ], + "properties": { + "review_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "approve", + "reject" + ] + } + } + }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { @@ -9542,9 +9955,15 @@ const docTemplate = `{ "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ + "color_scheme", "language" ], "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, "language": { "description": "language", "type": "string", @@ -9652,6 +10071,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "language": { + "type": "string" + }, "location": { "type": "string" }, @@ -9757,6 +10179,10 @@ const docTemplate = `{ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" diff --git a/docs/swagger.json b/docs/swagger.json index 83bde2211..00b06d9ae 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -27,7 +27,7 @@ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], @@ -54,7 +54,8 @@ { "enum": [ "available", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", @@ -357,7 +358,7 @@ "ApiKeyAuth": [] } ], - "description": "Status:[available,closed,deleted]", + "description": "Status:[available,closed,deleted,pending]", "consumes": [ "application/json" ], @@ -385,7 +386,8 @@ "enum": [ "available", "closed", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", @@ -504,117 +506,6 @@ } } }, - "/answer/admin/api/report/": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "ApiKeyAuth": [] - } - ], - "description": "handle flag", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "handle flag", - "parameters": [ - { - "description": "flag", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.ReportHandleReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, - "/answer/admin/api/reports/page": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "ApiKeyAuth": [] - } - ], - "description": "list report records", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "list report page", - "parameters": [ - { - "enum": [ - "pending", - "completed" - ], - "type": "string", - "description": "status", - "name": "status", - "in": "query", - "required": true - }, - { - "enum": [ - "all", - "question", - "answer", - "comment" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/admin/api/roles": { "get": { "description": "get role list", @@ -4435,94 +4326,122 @@ } } }, - "/answer/api/v1/revisions": { - "get": { - "description": "get revision list", + "/answer/api/v1/report/review": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "review report", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Report" ], - "summary": "get revision list", + "summary": "review report", "parameters": [ { - "type": "string", - "description": "object id", - "name": "object_id", - "in": "query", - "required": true + "description": "flag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReviewReportReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRevisionResp" - } - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/revisions/audit": { - "put": { + "/answer/api/v1/report/unreviewed/post": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "revision audit operation:approve or reject", + "description": "get unreviewed report post page", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Report" ], - "summary": "revision audit", + "summary": "get unreviewed report post page", "parameters": [ { - "description": "audit", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RevisionAuditReq" - } + "type": "integer", + "description": "page", + "name": "page", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReportListPageResp" + } + } + } + } + ] + } + } + } + ] } } } } }, - "/answer/api/v1/revisions/edit/check": { - "get": { + "/answer/api/v1/review/pending/post": { + "put": { "security": [ + { + "ApiKeyAuth": [] + }, { "ApiKeyAuth": [] } ], - "description": "check can update revision", + "description": "update review", "consumes": [ "application/json" ], @@ -4530,17 +4449,18 @@ "application/json" ], "tags": [ - "Revision" + "Review" ], - "summary": "check can update revision", + "summary": "update review", "parameters": [ { - "type": "string", - "default": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true + "description": "review", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateReviewReq" + } } ], "responses": { @@ -4553,28 +4473,36 @@ } } }, - "/answer/api/v1/revisions/unreviewed": { + "/answer/api/v1/review/pending/post/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get unreviewed revision list", + "description": "get unreviewed post page", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Revision" + "Review" ], - "summary": "get unreviewed revision list", + "summary": "get unreviewed post page", "parameters": [ { - "type": "string", - "description": "page id", + "type": "integer", + "description": "page", "name": "page", - "in": "query", - "required": true + "in": "query" + }, + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query" } ], "responses": { @@ -4599,7 +4527,7 @@ "list": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" } } } @@ -4614,35 +4542,254 @@ } } }, - "/answer/api/v1/search": { + "/answer/api/v1/reviewing/type": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "search object", + "description": "get reviewing type", "produces": [ "application/json" ], "tags": [ - "Search" + "Revision" ], - "summary": "search object", - "parameters": [ - { - "type": "string", - "description": "query string", - "name": "q", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "active", - "score", - "relevance" + "summary": "get reviewing type", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReviewingTypeResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions": { + "get": { + "description": "get revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get revision list", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "search object", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "search object", + "parameters": [ + { + "type": "string", + "description": "query string", + "name": "q", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "active", + "score", + "relevance" ], "type": "string", "description": "order", @@ -6523,7 +6670,8 @@ "type": "string" }, "value": { - "type": "integer" + "type": "integer", + "minimum": 1 } } }, @@ -7063,6 +7211,12 @@ "action": { "$ref": "#/definitions/schema.UIOptionAction" }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, "input_type": { "type": "string" }, @@ -7343,6 +7497,10 @@ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" @@ -7655,36 +7813,122 @@ } } }, - "schema.GetRevisionResp": { + "schema.GetReportListPageResp": { "type": "object", "properties": { - "content": { - "description": "content parsed" + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" + }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "flag_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" }, + "question_id": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/schema.ReasonItem" + }, + "reason_content": { + "type": "string" + }, + "submit_at": { + "type": "integer" + }, + "submitter_user": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetReviewingTypeResp": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "todo_amount": { + "type": "integer" + } + } + }, + "schema.GetRevisionResp": { + "type": "object", + "properties": { + "content": {}, "create_at": { "type": "integer" }, "id": { - "description": "id", "type": "string" }, "object_id": { - "description": "object id", "type": "string" }, "reason": { "type": "string" }, "status": { - "description": "revision status(normal: 1; delete 2)", "type": "integer" }, "title": { - "description": "title", + "type": "string" + }, + "url_title": { "type": "string" }, "use_id": { - "description": "user id", "type": "string" }, "user_info": { @@ -7891,6 +8135,73 @@ } } }, + "schema.GetUnreviewedPostPageResp": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "review_id": { + "type": "integer" + }, + "submit_at": { + "type": "integer" + }, + "submitter_display_name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, "schema.GetUnreviewedRevisionResp": { "type": "object", "properties": { @@ -8130,12 +8441,14 @@ "enum": [ 1, 2, - 3 + 3, + 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", - "PrivilegeLevel3" + "PrivilegeLevel3", + "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { @@ -8453,6 +8766,29 @@ } } }, + "schema.ReasonItem": { + "type": "object", + "properties": { + "content_type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "reason_key": { + "type": "string" + }, + "reason_type": { + "type": "integer" + } + } + }, "schema.RecoverAnswerReq": { "type": "object", "required": [ @@ -8550,21 +8886,47 @@ } } }, - "schema.ReportHandleReq": { + "schema.ReviewReportReq": { "type": "object", "required": [ - "flagged_type", - "id" + "flag_id", + "operation_type" ], "properties": { - "flagged_content": { + "close_msg": { "type": "string" }, - "flagged_type": { + "close_type": { "type": "integer" }, - "id": { + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "flag_id": { "type": "string" + }, + "operation_type": { + "type": "string", + "enum": [ + "edit_post", + "close_post", + "delete_post", + "unlist_post", + "ignore_report" + ] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, @@ -8799,6 +9161,9 @@ "site_url" ], "properties": { + "check_update": { + "type": "boolean" + }, "contact_email": { "type": "string", "maxLength": 512 @@ -8829,6 +9194,9 @@ "site_url" ], "properties": { + "check_update": { + "type": "boolean" + }, "contact_email": { "type": "string", "maxLength": 512 @@ -9043,6 +9411,10 @@ "theme" ], "properties": { + "color_scheme": { + "type": "string", + "maxLength": 100 + }, "theme": { "type": "string", "maxLength": 255 @@ -9056,6 +9428,9 @@ "schema.SiteThemeResp": { "type": "object", "properties": { + "color_scheme": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9281,15 +9656,45 @@ "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" + }, + "comment_id": { + "type": "string" + }, "content": { "type": "string" }, + "created_at": { + "type": "integer" + }, "html": { "type": "string" }, + "object_creator_user_id": { + "type": "string" + }, "object_id": { "type": "string" }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "show_status": { + "type": "integer" + }, + "status": { + "type": "integer" + }, "tags": { "type": "array", "items": { @@ -9298,6 +9703,9 @@ }, "title": { "type": "string" + }, + "url_title": { + "type": "string" } } }, @@ -9404,8 +9812,13 @@ "level" ], "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, "level": { - "maximum": 3, "minimum": 1, "allOf": [ { @@ -9415,6 +9828,25 @@ } } }, + "schema.UpdateReviewReq": { + "type": "object", + "required": [ + "review_id", + "status" + ], + "properties": { + "review_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "approve", + "reject" + ] + } + } + }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { @@ -9511,9 +9943,15 @@ "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ + "color_scheme", "language" ], "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, "language": { "description": "language", "type": "string", @@ -9621,6 +10059,9 @@ "id": { "type": "string" }, + "language": { + "type": "string" + }, "location": { "type": "string" }, @@ -9726,6 +10167,10 @@ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ca78bc499..e9bb895e3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,20 +1,3 @@ -# 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. - definitions: constant.NotificationChannelKey: enum: @@ -29,6 +12,7 @@ definitions: label: type: string value: + minimum: 1 type: integer type: object handler.RespBody: @@ -403,6 +387,10 @@ definitions: properties: action: $ref: '#/definitions/schema.UIOptionAction' + class_name: + type: string + field_class_name: + type: string input_type: type: string label: @@ -603,6 +591,9 @@ definitions: bio_html: description: bio html type: string + color_scheme: + description: Color scheme + type: string created_at: description: create time type: integer @@ -828,28 +819,84 @@ definitions: description: url title type: string type: object + schema.GetReportListPageResp: + properties: + answer_accepted: + type: boolean + answer_count: + type: integer + answer_id: + type: string + author_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + comment_id: + type: string + created_at: + type: integer + flag_id: + type: string + object_id: + type: string + object_show_status: + type: integer + object_status: + type: integer + object_type: + enum: + - question + - answer + - comment + type: string + original_text: + type: string + parsed_text: + type: string + question_id: + type: string + reason: + $ref: '#/definitions/schema.ReasonItem' + reason_content: + type: string + submit_at: + type: integer + submitter_user: + $ref: '#/definitions/schema.UserBasicInfo' + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + url_title: + type: string + type: object + schema.GetReviewingTypeResp: + properties: + label: + type: string + name: + type: string + todo_amount: + type: integer + type: object schema.GetRevisionResp: properties: - content: - description: content parsed + content: {} create_at: type: integer id: - description: id type: string object_id: - description: object id type: string reason: type: string status: - description: 'revision status(normal: 1; delete 2)' type: integer title: - description: title + type: string + url_title: type: string use_id: - description: user id type: string user_info: $ref: '#/definitions/schema.UserBasicInfo' @@ -991,6 +1038,51 @@ definitions: $ref: '#/definitions/schema.TagSynonym' type: array type: object + schema.GetUnreviewedPostPageResp: + properties: + answer_id: + type: string + author_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + comment_id: + type: string + created_at: + type: integer + object_id: + type: string + object_show_status: + type: integer + object_status: + type: integer + object_type: + enum: + - question + - answer + - comment + type: string + original_text: + type: string + parsed_text: + type: string + question_id: + type: string + reason: + type: string + review_id: + type: integer + submit_at: + type: integer + submitter_display_name: + type: string + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + url_title: + type: string + type: object schema.GetUnreviewedRevisionResp: properties: info: @@ -1156,11 +1248,13 @@ definitions: - 1 - 2 - 3 + - 99 type: integer x-enum-varnames: - PrivilegeLevel1 - PrivilegeLevel2 - PrivilegeLevel3 + - PrivilegeLevelCustom schema.PrivilegeOption: properties: level: @@ -1385,6 +1479,21 @@ definitions: required: - id type: object + schema.ReasonItem: + properties: + content_type: + type: string + description: + type: string + name: + type: string + placeholder: + type: string + reason_key: + type: string + reason_type: + type: integer + type: object schema.RecoverAnswerReq: properties: answer_id: @@ -1449,17 +1558,37 @@ definitions: question_id: type: string type: object - schema.ReportHandleReq: + schema.ReviewReportReq: properties: - flagged_content: + close_msg: type: string - flagged_type: + close_type: type: integer - id: + content: + maxLength: 65535 + minLength: 6 + type: string + flag_id: + type: string + operation_type: + enum: + - edit_post + - close_post + - delete_post + - unlist_post + - ignore_report + type: string + tags: + items: + $ref: '#/definitions/schema.TagItem' + type: array + title: + maxLength: 150 + minLength: 6 type: string required: - - flagged_type - - id + - flag_id + - operation_type type: object schema.RevisionAuditReq: properties: @@ -1614,6 +1743,8 @@ definitions: type: object schema.SiteGeneralReq: properties: + check_update: + type: boolean contact_email: maxLength: 512 type: string @@ -1636,6 +1767,8 @@ definitions: type: object schema.SiteGeneralResp: properties: + check_update: + type: boolean contact_email: maxLength: 512 type: string @@ -1783,6 +1916,9 @@ definitions: type: object schema.SiteThemeReq: properties: + color_scheme: + maxLength: 100 + type: string theme: maxLength: 255 type: string @@ -1794,6 +1930,8 @@ definitions: type: object schema.SiteThemeResp: properties: + color_scheme: + type: string theme: type: string theme_config: @@ -1947,18 +2085,40 @@ definitions: type: object schema.UnreviewedRevisionInfoInfo: properties: + answer_accepted: + type: boolean + answer_count: + type: integer + answer_id: + type: string + comment_id: + type: string content: type: string + created_at: + type: integer html: type: string + object_creator_user_id: + type: string object_id: type: string + object_type: + type: string + question_id: + type: string + show_status: + type: integer + status: + type: integer tags: items: $ref: '#/definitions/schema.TagResp' type: array title: type: string + url_title: + type: string type: object schema.UpdateCommentReq: properties: @@ -2030,14 +2190,30 @@ definitions: type: object schema.UpdatePrivilegesConfigReq: properties: + custom_privileges: + items: + $ref: '#/definitions/constant.Privilege' + type: array level: allOf: - $ref: '#/definitions/schema.PrivilegeLevel' - maximum: 3 minimum: 1 required: - level type: object + schema.UpdateReviewReq: + properties: + review_id: + type: integer + status: + enum: + - approve + - reject + type: string + required: + - review_id + - status + type: object schema.UpdateSMTPConfigReq: properties: encryption: @@ -2107,11 +2283,16 @@ definitions: type: object schema.UpdateUserInterfaceRequest: properties: + color_scheme: + description: Color scheme + maxLength: 100 + type: string language: description: language maxLength: 100 type: string required: + - color_scheme - language type: object schema.UpdateUserNotificationConfigReq: @@ -2183,6 +2364,8 @@ definitions: type: string id: type: string + language: + type: string location: type: string rank: @@ -2257,6 +2440,9 @@ definitions: bio_html: description: bio html type: string + color_scheme: + description: Color scheme + type: string created_at: description: create time type: integer @@ -2475,7 +2661,7 @@ paths: get: consumes: - application/json - description: Status:[available,deleted] + description: Status:[available,deleted,pending] parameters: - description: page size in: query @@ -2489,6 +2675,7 @@ paths: enum: - available - deleted + - pending in: query name: status type: string @@ -2676,7 +2863,7 @@ paths: get: consumes: - application/json - description: Status:[available,closed,deleted] + description: Status:[available,closed,deleted,pending] parameters: - description: page size in: query @@ -2691,6 +2878,7 @@ paths: - available - closed - deleted + - pending in: query name: status type: string @@ -2772,76 +2960,6 @@ paths: summary: get reasons by object type and action tags: - reason - /answer/admin/api/report/: - put: - consumes: - - application/json - description: handle flag - parameters: - - description: flag - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.ReportHandleReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - - ApiKeyAuth: [] - summary: handle flag - tags: - - admin - /answer/admin/api/reports/page: - get: - consumes: - - application/json - description: list report records - parameters: - - description: status - enum: - - pending - - completed - in: query - name: status - required: true - type: string - - description: object_type - enum: - - all - - question - - answer - - comment - in: query - name: object_type - required: true - type: string - - description: page size - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - - ApiKeyAuth: [] - summary: list report page - tags: - - admin /answer/admin/api/roles: get: description: get role list @@ -5147,6 +5265,150 @@ paths: summary: add report tags: - Report + /answer/api/v1/report/review: + put: + consumes: + - application/json + description: review report + parameters: + - description: flag + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.ReviewReportReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + - ApiKeyAuth: [] + summary: review report + tags: + - Report + /answer/api/v1/report/unreviewed/post: + get: + consumes: + - application/json + description: get unreviewed report post page + parameters: + - description: page + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetReportListPageResp' + type: array + type: object + type: object + security: + - ApiKeyAuth: [] + summary: get unreviewed report post page + tags: + - Report + /answer/api/v1/review/pending/post: + put: + consumes: + - application/json + description: update review + parameters: + - description: review + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateReviewReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + - ApiKeyAuth: [] + summary: update review + tags: + - Review + /answer/api/v1/review/pending/post/page: + get: + consumes: + - application/json + description: get unreviewed post page + parameters: + - description: page + in: query + name: page + type: integer + - description: object_id + in: query + name: object_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetUnreviewedPostPageResp' + type: array + type: object + type: object + security: + - ApiKeyAuth: [] + summary: get unreviewed post page + tags: + - Review + /answer/api/v1/reviewing/type: + get: + description: get reviewing type + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetReviewingTypeResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get reviewing type + tags: + - Revision /answer/api/v1/revisions: get: description: get revision list diff --git a/go.mod b/go.mod index f4ab56992..d88b7f7ef 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 github.com/microcosm-cc/bluemonday v1.0.21 - github.com/mojocn/base64Captcha v1.3.5 + github.com/mojocn/base64Captcha v1.3.6 github.com/ory/dockertest/v3 v3.10.0 github.com/robfig/cron/v3 v3.0.1 github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 @@ -58,9 +58,9 @@ require ( github.com/swaggo/swag v1.16.1 github.com/tidwall/gjson v1.14.4 github.com/yuin/goldmark v1.4.13 - golang.org/x/crypto v0.13.0 - golang.org/x/image v0.1.0 - golang.org/x/net v0.15.0 + golang.org/x/crypto v0.21.0 + golang.org/x/image v0.13.0 + golang.org/x/net v0.21.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.24.0 @@ -150,8 +150,8 @@ require ( go.uber.org/zap v1.23.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 3b97bb544..93af2f278 100644 --- a/go.sum +++ b/go.sum @@ -524,8 +524,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0= -github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY= +github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw= +github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -788,8 +788,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -801,11 +801,10 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= -golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= +golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -829,6 +828,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -876,9 +876,10 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -900,6 +901,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -970,8 +972,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -987,8 +989,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1055,6 +1058,7 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index e155edffa..943608dd6 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -18,6 +18,7 @@ # The following fields are used for back-end backend: + base: success: other: Success. @@ -141,6 +142,9 @@ backend: email_or_password_wrong_error: other: Email and password do not match. error: + common: + invalid_url: + other: Invalid URL. password: space_invalid: other: Password cannot contain spaces. @@ -201,6 +205,8 @@ backend: question: already_deleted: other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. not_found: other: Question not found. cannot_deleted: @@ -490,6 +496,15 @@ backend: other: accept accepted: other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits # The following fields are used for interface presentation(Front-end) ui: @@ -680,6 +695,8 @@ ui: empty: Cannot be empty. msg: empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. tag_modal: title: Create new tag form: @@ -1051,8 +1068,8 @@ ui: title: Related Questions answers: answers invite_to_answer: - title: People Asked - desc: Select people who you think might know the answer. + title: Invite People + desc: Invite people you think can answer. invite: Invite to answer add: Add people search: Search people @@ -1062,6 +1079,7 @@ ui: asked: asked update: Modified edit: edited + commented: commented Views: Viewed Follow: Follow Following: Following @@ -1079,6 +1097,7 @@ ui: title: Answers score: Score newest: Newest + oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: @@ -1103,6 +1122,14 @@ ui: confirm_btn: Reopen title: Reopen this post content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? pin: title: Pin this post content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. @@ -1130,6 +1157,9 @@ ui: save: Save delete: Delete undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted login: Log in signup: Sign up logout: Log out @@ -1158,6 +1188,15 @@ ui: system_setting: System setting default: Default reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + pending: Pending search: title: Search Results keywords: Keywords @@ -1413,7 +1452,8 @@ ui: votes: "Votes:" users: "Users:" flags: "Flags:" - site_health_status: Site health status + reviews: "Reviews:" + site_health: Site health version: "Version:" https: "HTTPS:" upload_folder: "Upload folder:" @@ -1428,11 +1468,12 @@ ui: database_size: "Database size:" storage_used: "Storage used:" uptime: "Uptime:" - answer_links: Answer links + links: Links plugins: Plugins github: GitHub blog: Blog contact: Contact + forum: Forum documents: Documents feedback: Feedback support: Support @@ -1535,9 +1576,7 @@ ui: content: A suspended user can't log in. questions: page_title: Questions - normal: Normal - closed: Closed - deleted: Deleted + unlisted: Unlisted post: Post votes: Votes answers: Answers @@ -1545,12 +1584,11 @@ ui: status: Status action: Action change: Change + pending: Pending filter: placeholder: "Filter by title, question:id" answers: page_title: Answers - normal: Normal - deleted: Deleted post: Post votes: Votes created: Created @@ -1583,6 +1621,9 @@ ui: msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates interface: page_title: Interface language: @@ -1661,8 +1702,8 @@ ui: page_title: Write restrict_answer: title: Restrict answer - label: Each user can only write one answer for each question - text: "They can use the edit link to refine and improve their existing answer, instead." + label: Each user can only write one answer for the same question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." recommend_tags: label: Recommend tags text: "Please input tag slug above, one tag per line." @@ -1796,6 +1837,22 @@ ui: edit_answer: Edit answer edit_tag: Edit tag empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted @@ -1846,3 +1903,6 @@ ui: post_hide_list: This post has been hidden from list. post_show_list: This post has been shown to list. post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml index fa8d5aec6..190ac0bf8 100644 --- a/i18n/i18n.yaml +++ b/i18n/i18n.yaml @@ -59,3 +59,6 @@ language_options: - label: "Slovak" value: "sk_SK" progress: 62 + - label: "فارسی" + value: "fa_IR" + progress: 85 diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 81ca99f43..cdabc86fd 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1050,6 +1050,7 @@ ui: title: 个回答 score: 评分 newest: 最新 + oldest: 最旧 btn_accept: 采纳 btn_accepted: 已被采纳 write_answer: @@ -1073,6 +1074,14 @@ ui: confirm_btn: 重新打开 title: 重新打开这个帖子 content: 确定要重新打开吗? + list: + confirm_btn: 列表显示 + title: 列表中显示这个帖子 + content: 确定要列表中显示这个帖子吗? + unlist: + confirm_btn: 列表隐藏 + title: 从列表中隐藏这个帖子 + content: 确定要从列表中隐藏这个帖子吗? pin: title: 置顶该帖子 content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 @@ -1094,6 +1103,8 @@ ui: save: 保存 delete: 删除 undelete: 撤消删除 + list: 列表显示 + unlist: 列表隐藏 login: 登录 signup: 注册 logout: 退出 @@ -1122,6 +1133,15 @@ ui: system_setting: System setting default: Default reset: Reset + tag: Tag + post_lowercase: 帖子 + filter: Filter + ignore: 忽略 + submit: 提交 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + pending: Pending search: title: 搜索结果 keywords: 关键词 @@ -1494,6 +1514,7 @@ ui: closed: 已关闭 deleted: 已删除 post: 标题 + unlisted: 已隐藏 votes: 得票数 answers: 回答数 created: 创建于 @@ -1538,6 +1559,9 @@ ui: msg: 联系人邮箱不能为空。 validate: 联系人邮箱无效。 text: 本网站的主要联系邮箱地址。 + check_update: + label: 软件更新 + text: 自动检查软件更新 interface: page_title: 界面 language: @@ -1616,8 +1640,8 @@ ui: page_title: 编辑 restrict_answer: title: 限制一个回答 - label: 每个用户对于每个问题只能有一个回答 - text: "用户可以使用编辑按钮优化已有的回答" + label: 每个用户只能为同一问题写一个回答 + text: "关闭以允许用户对同一问题编写多个回答,这可能会导致回答不集中。" recommend_tags: label: 推荐标签 text: "请在上方输入标签固定链接,每行一个标签。" @@ -1802,3 +1826,5 @@ ui: post_hide_list: 此帖子已经从列表中隐藏。 post_show_list: 该帖子已显示到列表中。 post_reopen: 这个帖子已被重新打开. + post_list: 这个帖子已经被显示 + post_unlist: 这个帖子已经被隐藏 diff --git a/internal/base/constant/reason.go b/internal/base/constant/reason.go new file mode 100644 index 000000000..53b298f3a --- /dev/null +++ b/internal/base/constant/reason.go @@ -0,0 +1,23 @@ +package constant + +const ( + ReasonSpam = "reason.spam" + ReasonRudeOrAbusive = "reason.rude_or_abusive" + ReasonSomething = "reason.something" + ReasonADuplicate = "reason.a_duplicate" + ReasonNotAAnswer = "reason.not_a_answer" + ReasonNoLongerNeeded = "reason.no_longer_needed" + ReasonCommunitySpecific = "reason.community_specific" + ReasonNotClarity = "reason.not_clarity" + ReasonNormal = "reason.normal" + ReasonNormalUser = "reason.normal.user" + ReasonClosed = "reason.closed" + ReasonDeleted = "reason.deleted" + ReasonDeletedUser = "reason.deleted.user" + ReasonSuspended = "reason.suspended" + ReasonInactive = "reason.inactive" + ReasonLooksOk = "reason.looks_ok" + ReasonNeedsEdit = "reason.needs_edit" + ReasonNeedsClose = "reason.needs_close" + ReasonNeedsDelete = "reason.needs_delete" +) diff --git a/internal/base/constant/revision.go b/internal/base/constant/revision.go new file mode 100644 index 000000000..0d7dc586e --- /dev/null +++ b/internal/base/constant/revision.go @@ -0,0 +1,25 @@ +package constant + +type ReviewingType string + +const ( + QueuedPost ReviewingType = "queued_post" + QueuedUser ReviewingType = "queued_user" + FlaggedPost ReviewingType = "flagged_post" + FlaggedUser ReviewingType = "flagged_user" + SuggestedPostEdit ReviewingType = "suggested_post_edit" +) + +const ( + ReportOperationEditPost = "edit_post" + ReportOperationClosePost = "close_post" + ReportOperationDeletePost = "delete_post" + ReportOperationUnlistPost = "unlist_post" + ReportOperationIgnoreReport = "ignore_report" +) + +const ( + ReviewQueuedPostLabel = "review.queued_post" + ReviewFlaggedPostLabel = "review.flagged_post" + ReviewSuggestedPostEditLabel = "review.suggested_post_edit" +) diff --git a/internal/base/cron/cron.go b/internal/base/cron/cron.go index bb9cb7e2b..b7b05c262 100644 --- a/internal/base/cron/cron.go +++ b/internal/base/cron/cron.go @@ -23,7 +23,7 @@ import ( "context" "fmt" - "github.com/apache/incubator-answer/internal/service" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/siteinfo_common" "github.com/robfig/cron/v3" "github.com/segmentfault/pacman/log" @@ -32,13 +32,13 @@ import ( // ScheduledTaskManager scheduled task manager type ScheduledTaskManager struct { siteInfoService siteinfo_common.SiteInfoCommonService - questionService *service.QuestionService + questionService *content.QuestionService } // NewScheduledTaskManager new scheduled task manager func NewScheduledTaskManager( siteInfoService siteinfo_common.SiteInfoCommonService, - questionService *service.QuestionService, + questionService *content.QuestionService, ) *ScheduledTaskManager { manager := &ScheduledTaskManager{ siteInfoService: siteInfoService, diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 99a7a6d91..0dbf73ace 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -45,6 +45,7 @@ const ( QuestionCannotClose = "error.question.cannot_close" QuestionCannotUpdate = "error.question.cannot_update" QuestionAlreadyDeleted = "error.question.already_deleted" + QuestionUnderReview = "error.question.under_review" AnswerNotFound = "error.answer.not_found" AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotUpdate = "error.answer.cannot_update" @@ -99,9 +100,9 @@ const ( AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status" UserAccessDenied = "error.user.access_denied" UserPageAccessDenied = "error.user.page_access_denied" - - AddBulkUsersFormatError = "error.user.add_bulk_users_format_error" - AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error" + AddBulkUsersFormatError = "error.user.add_bulk_users_format_error" + AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error" + InvalidURLError = "error.common.invalid_url" ) // user external login reasons diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 8fdb089ce..390aa4224 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -30,8 +30,8 @@ import ( "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/permission" "github.com/apache/incubator-answer/internal/service/rank" "github.com/apache/incubator-answer/internal/service/siteinfo_common" @@ -42,7 +42,7 @@ import ( // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService + answerService *content.AnswerService rankService *rank.RankService actionService *action.CaptchaService siteInfoCommonService siteinfo_common.SiteInfoCommonService @@ -51,7 +51,7 @@ type AnswerController struct { // NewAnswerController new controller func NewAnswerController( - answerService *service.AnswerService, + answerService *content.AnswerService, rankService *rank.RankService, actionService *action.CaptchaService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, diff --git a/internal/controller/collection_controller.go b/internal/controller/collection_controller.go index 4e3eff388..a35ee693a 100644 --- a/internal/controller/collection_controller.go +++ b/internal/controller/collection_controller.go @@ -23,18 +23,18 @@ import ( "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/middleware" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" + "github.com/apache/incubator-answer/internal/service/collection" "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" ) // CollectionController collection controller type CollectionController struct { - collectionService *service.CollectionService + collectionService *collection.CollectionService } // NewCollectionController new controller -func NewCollectionController(collectionService *service.CollectionService) *CollectionController { +func NewCollectionController(collectionService *collection.CollectionService) *CollectionController { return &CollectionController{collectionService: collectionService} } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 0dd8e3ef5..42c2c1d3e 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -47,4 +47,5 @@ var ProviderSetController = wire.NewSet( NewUserCenterController, NewPermissionController, NewUserPluginController, + NewReviewController, ) diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index ab923e968..15796b9c4 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -70,6 +70,7 @@ func (nc *NotificationController) GetRedDot(ctx *gin.Context) { req.CanReviewQuestion = canList[0] req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := nc.notificationService.GetRedDot(ctx, req) handler.HandleResponse(ctx, err, resp) diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 7d2872109..072ffd741 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -20,6 +20,8 @@ package controller import ( + "net/http" + "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/middleware" "github.com/apache/incubator-answer/internal/base/pager" @@ -28,8 +30,8 @@ import ( "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/permission" "github.com/apache/incubator-answer/internal/service/rank" "github.com/apache/incubator-answer/internal/service/siteinfo_common" @@ -37,13 +39,12 @@ import ( "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" - "net/http" ) // QuestionController question controller type QuestionController struct { - questionService *service.QuestionService - answerService *service.AnswerService + questionService *content.QuestionService + answerService *content.AnswerService rankService *rank.RankService siteInfoService siteinfo_common.SiteInfoCommonService actionService *action.CaptchaService @@ -52,8 +53,8 @@ type QuestionController struct { // NewQuestionController new controller func NewQuestionController( - questionService *service.QuestionService, - answerService *service.AnswerService, + questionService *content.QuestionService, + answerService *content.AnswerService, rankService *rank.RankService, siteInfoService siteinfo_common.SiteInfoCommonService, actionService *action.CaptchaService, @@ -542,7 +543,7 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { return } //add the question id to the answer - questionInfo, ok := resp.(*schema.QuestionInfo) + questionInfo, ok := resp.(*schema.QuestionInfoResp) if ok { answerReq := &schema.AnswerAddReq{} answerReq.QuestionID = uid.DeShortID(questionInfo.ID) @@ -656,7 +657,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) return } - respInfo, ok := resp.(*schema.QuestionInfo) + respInfo, ok := resp.(*schema.QuestionInfoResp) if !ok { handler.HandleResponse(ctx, err, resp) return @@ -816,6 +817,7 @@ func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) { } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := qc.questionService.PersonalQuestionPage(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -840,6 +842,7 @@ func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) { } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) resp, err := qc.questionService.PersonalAnswerPage(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -869,14 +872,14 @@ func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { // AdminQuestionPage admin question page // @Summary AdminQuestionPage admin question page -// @Description Status:[available,closed,deleted] +// @Description Status:[available,closed,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" -// @Param status query string false "user status" Enums(available, closed, deleted) +// @Param status query string false "user status" Enums(available, closed, deleted, pending) // @Param query query string false "question id or title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/page [get] @@ -893,14 +896,14 @@ func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) { // AdminAnswerPage admin answer page // @Summary AdminAnswerPage admin answer page -// @Description Status:[available,deleted] +// @Description Status:[available,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" -// @Param status query string false "user status" Enums(available,deleted) +// @Param status query string false "user status" Enums(available,deleted,pending) // @Param query query string false "answer id or question title" // @Param question_id query string false "question id" // @Success 200 {object} handler.RespBody diff --git a/internal/controller/report_controller.go b/internal/controller/report_controller.go index e0765154b..75ceb8235 100644 --- a/internal/controller/report_controller.go +++ b/internal/controller/report_controller.go @@ -103,3 +103,54 @@ func (rc *ReportController) AddReport(ctx *gin.Context) { } handler.HandleResponse(ctx, err, nil) } + +// GetUnreviewedReportPostPage get unreviewed report post page +// @Summary get unreviewed report post page +// @Description get unreviewed report post page +// @Tags Report +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetReportListPageResp}} +// @Router /answer/api/v1/report/unreviewed/post [get] +func (rc *ReportController) GetUnreviewedReportPostPage(ctx *gin.Context) { + req := &schema.GetUnreviewedReportPostPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := rc.reportService.GetUnreviewedReportPostPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// ReviewReport review report +// @Summary review report +// @Description review report +// @Security ApiKeyAuth +// @Tags Report +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.ReviewReportReq true "flag" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/report/review [put] +func (rc *ReportController) ReviewReport(ctx *gin.Context) { + req := &schema.ReviewReportReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + if !req.IsAdmin { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + + err := rc.reportService.ReviewReport(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/review_controller.go b/internal/controller/review_controller.go new file mode 100644 index 000000000..435cefa76 --- /dev/null +++ b/internal/controller/review_controller.go @@ -0,0 +1,112 @@ +/* + * 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 controller + +import ( + "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/middleware" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/action" + "github.com/apache/incubator-answer/internal/service/rank" + "github.com/apache/incubator-answer/internal/service/review" + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +// ReviewController review controller +type ReviewController struct { + reviewService *review.ReviewService + rankService *rank.RankService + actionService *action.CaptchaService +} + +// NewReviewController new controller +func NewReviewController( + reviewService *review.ReviewService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *ReviewController { + return &ReviewController{ + reviewService: reviewService, + rankService: rankService, + actionService: actionService, + } +} + +// GetUnreviewedPostPage get unreviewed post page +// @Summary get unreviewed post page +// @Description get unreviewed post page +// @Tags Review +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Param object_id query string false "object_id" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedPostPageResp}} +// @Router /answer/api/v1/review/pending/post/page [get] +func (rc *ReviewController) GetUnreviewedPostPage(ctx *gin.Context) { + req := &schema.GetUnreviewedPostPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + req.ReviewerMapping = make(map[string]string) + _ = plugin.CallReviewer(func(base plugin.Reviewer) error { + info := base.Info() + req.ReviewerMapping[info.SlugName] = info.Name.Translate(ctx) + return nil + }) + + resp, err := rc.reviewService.GetUnreviewedPostPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateReview update review +// @Summary update review +// @Description update review +// @Security ApiKeyAuth +// @Tags Review +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateReviewReq true "review" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/review/pending/post [put] +func (rc *ReviewController) UpdateReview(ctx *gin.Context) { + req := &schema.UpdateReviewReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + if !req.IsAdmin { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + + err := rc.reviewService.UpdateReview(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/revision_controller.go b/internal/controller/revision_controller.go index 7bc892cd9..fd2735e0f 100644 --- a/internal/controller/revision_controller.go +++ b/internal/controller/revision_controller.go @@ -26,7 +26,7 @@ import ( "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/permission" "github.com/apache/incubator-answer/internal/service/rank" "github.com/apache/incubator-answer/pkg/obj" @@ -37,13 +37,13 @@ import ( // RevisionController revision controller type RevisionController struct { - revisionListService *service.RevisionService + revisionListService *content.RevisionService rankService *rank.RankService } // NewRevisionController new controller func NewRevisionController( - revisionListService *service.RevisionService, + revisionListService *content.RevisionService, rankService *rank.RankService, ) *RevisionController { return &RevisionController{ @@ -191,3 +191,32 @@ func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) { resp, err := rc.revisionListService.CheckCanUpdateRevision(ctx, req) handler.HandleResponse(ctx, err, resp) } + +// GetReviewingType get reviewing type +// @Summary get reviewing type +// @Description get reviewing type +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} handler.RespBody{data=[]schema.GetReviewingTypeResp} +// @Router /answer/api/v1/reviewing/type [get] +func (rc *RevisionController) GetReviewingType(ctx *gin.Context) { + req := &schema.GetReviewingTypeReq{} + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := rc.revisionListService.GetReviewingType(ctx, req) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index d08fd8f15..b74c0e9e5 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -27,8 +27,8 @@ import ( "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/plugin" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" @@ -36,13 +36,13 @@ import ( // SearchController tag controller type SearchController struct { - searchService *service.SearchService + searchService *content.SearchService actionService *action.CaptchaService } // NewSearchController new controller func NewSearchController( - searchService *service.SearchService, + searchService *content.SearchService, actionService *action.CaptchaService, ) *SearchController { return &SearchController{ diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 6b4c4bbe8..b128b91ac 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -22,7 +22,6 @@ package controller import ( "encoding/json" "fmt" - "github.com/apache/incubator-answer/internal/entity" "html/template" "net/http" "regexp" @@ -32,6 +31,7 @@ import ( "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" templaterender "github.com/apache/incubator-answer/internal/controller/template_render" + "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/siteinfo_common" "github.com/apache/incubator-answer/pkg/checker" @@ -47,7 +47,7 @@ import ( var SiteUrl = "" type TemplateController struct { - scriptPath string + scriptPath []string cssPath string templateRenderController *templaterender.TemplateRenderController siteInfoService siteinfo_common.SiteInfoCommonService @@ -66,18 +66,21 @@ func NewTemplateController( siteInfoService: siteInfoService, } } -func GetStyle() (script, css string) { +func GetStyle() (script []string, css string) { file, err := ui.Build.ReadFile("build/index.html") if err != nil { return } - scriptRegexp := regexp.MustCompile(``) - scriptData := scriptRegexp.FindStringSubmatch(string(file)) + scriptRegexp := regexp.MustCompile(``) + scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1) + for _, s := range scriptData { + if len(s) == 2 { + script = append(script, s[1]) + } + } + cssRegexp := regexp.MustCompile(``) cssListData := cssRegexp.FindStringSubmatch(string(file)) - if len(scriptData) == 2 { - script = scriptData[1] - } if len(cssListData) == 2 { css = cssListData[1] } diff --git a/internal/controller/template_render/controller.go b/internal/controller/template_render/controller.go index 4fdcaf2bc..521651b73 100644 --- a/internal/controller/template_render/controller.go +++ b/internal/controller/template_render/controller.go @@ -20,15 +20,16 @@ package templaterender import ( - questioncommon "github.com/apache/incubator-answer/internal/service/question_common" "math" + "github.com/apache/incubator-answer/internal/service/content" + questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + "github.com/apache/incubator-answer/internal/service/comment" "github.com/apache/incubator-answer/internal/service/siteinfo_common" "github.com/google/wire" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/tag" ) @@ -38,20 +39,20 @@ var ProviderSetTemplateRenderController = wire.NewSet( ) type TemplateRenderController struct { - questionService *service.QuestionService - userService *service.UserService + questionService *content.QuestionService + userService *content.UserService tagService *tag.TagService - answerService *service.AnswerService + answerService *content.AnswerService commentService *comment.CommentService siteInfoService siteinfo_common.SiteInfoCommonService questionRepo questioncommon.QuestionRepo } func NewTemplateRenderController( - questionService *service.QuestionService, - userService *service.UserService, + questionService *content.QuestionService, + userService *content.UserService, tagService *tag.TagService, - answerService *service.AnswerService, + answerService *content.AnswerService, commentService *comment.CommentService, siteInfoService siteinfo_common.SiteInfoCommonService, questionRepo questioncommon.QuestionRepo, diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go index a2779e235..a65c3083d 100644 --- a/internal/controller/template_render/question.go +++ b/internal/controller/template_render/question.go @@ -34,7 +34,7 @@ func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionP return t.questionService.GetQuestionPage(ctx, req) } -func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) { +func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfoResp, err error) { return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{}) } diff --git a/internal/controller/template_render/userinfo.go b/internal/controller/template_render/userinfo.go index 731f6e8ed..d2c222c18 100644 --- a/internal/controller/template_render/userinfo.go +++ b/internal/controller/template_render/userinfo.go @@ -25,5 +25,5 @@ import ( ) func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) { - return q.userService.GetOtherUserInfoByUsername(ctx, req.Username) + return q.userService.GetOtherUserInfoByUsername(ctx, req) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 4ef50e9e8..f0e70cfe0 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -20,6 +20,8 @@ package controller import ( + "net/url" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/middleware" @@ -28,9 +30,9 @@ import ( "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" "github.com/apache/incubator-answer/internal/service/auth" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/siteinfo_common" "github.com/apache/incubator-answer/internal/service/user_notification_config" @@ -38,12 +40,11 @@ import ( "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "net/url" ) // UserController user controller type UserController struct { - userService *service.UserService + userService *content.UserService authService *auth.AuthService actionService *action.CaptchaService emailService *export.EmailService @@ -54,7 +55,7 @@ type UserController struct { // NewUserController new controller func NewUserController( authService *auth.AuthService, - userService *service.UserService, + userService *content.UserService, actionService *action.CaptchaService, emailService *export.EmailService, siteInfoCommonService siteinfo_common.SiteInfoCommonService, @@ -114,7 +115,10 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) { return } - resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req.Username) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 6417087b7..09b5fb07b 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -27,8 +27,8 @@ import ( "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/action" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/rank" "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" @@ -37,14 +37,14 @@ import ( // VoteController activity controller type VoteController struct { - VoteService *service.VoteService + VoteService *content.VoteService rankService *rank.RankService actionService *action.CaptchaService } // NewVoteController new controller func NewVoteController( - voteService *service.VoteService, + voteService *content.VoteService, rankService *rank.RankService, actionService *action.CaptchaService, ) *VoteController { diff --git a/internal/controller_admin/controller.go b/internal/controller_admin/controller.go index f51c62a04..de87d105e 100644 --- a/internal/controller_admin/controller.go +++ b/internal/controller_admin/controller.go @@ -23,7 +23,6 @@ import "github.com/google/wire" // ProviderSetController is controller providers. var ProviderSetController = wire.NewSet( - NewReportController, NewUserAdminController, NewThemeController, NewSiteInfoController, diff --git a/internal/controller_admin/report_controller.go b/internal/controller_admin/report_controller.go deleted file mode 100644 index 435b5af81..000000000 --- a/internal/controller_admin/report_controller.go +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 controller_admin - -import ( - "github.com/apache/incubator-answer/internal/base/handler" - "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service/report_admin" - "github.com/apache/incubator-answer/pkg/converter" - "github.com/gin-gonic/gin" -) - -// ReportController report controller -type ReportController struct { - reportService *report_admin.ReportAdminService -} - -// NewReportController new controller -func NewReportController(reportService *report_admin.ReportAdminService) *ReportController { - return &ReportController{reportService: reportService} -} - -// ListReportPage godoc -// @Summary list report page -// @Description list report records -// @Security ApiKeyAuth -// @Tags admin -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param status query string true "status" Enums(pending, completed) -// @Param object_type query string true "object_type" Enums(all, question,answer,comment) -// @Param page query int false "page size" -// @Param page_size query int false "page size" -// @Success 200 {object} handler.RespBody -// @Router /answer/admin/api/reports/page [get] -func (rc *ReportController) ListReportPage(ctx *gin.Context) { - var ( - objectType = ctx.Query("object_type") - status = ctx.Query("status") - page = converter.StringToInt(ctx.DefaultQuery("page", "1")) - pageSize = converter.StringToInt(ctx.DefaultQuery("page_size", "20")) - ) - - dto := schema.GetReportListPageDTO{ - ObjectType: objectType, - Status: status, - Page: page, - PageSize: pageSize, - } - - resp, err := rc.reportService.ListReportPage(ctx, dto) - if err != nil { - handler.HandleResponse(ctx, err, schema.ErrTypeModal) - } else { - handler.HandleResponse(ctx, err, resp) - } -} - -// Handle godoc -// @Summary handle flag -// @Description handle flag -// @Security ApiKeyAuth -// @Tags admin -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param data body schema.ReportHandleReq true "flag" -// @Success 200 {object} handler.RespBody -// @Router /answer/admin/api/report/ [put] -func (rc *ReportController) Handle(ctx *gin.Context) { - req := schema.ReportHandleReq{} - if handler.BindAndCheck(ctx, &req) { - return - } - - err := rc.reportService.HandleReported(ctx, req) - handler.HandleResponse(ctx, err, nil) -} diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index e3520b8cb..4c9436ecb 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -25,14 +25,17 @@ const ( AnswerSearchOrderByDefault = "default" AnswerSearchOrderByTime = "updated" AnswerSearchOrderByVote = "vote" + AnswerSearchOrderByTimeAsc = "created" AnswerStatusAvailable = 1 AnswerStatusDeleted = 10 + AnswerStatusPending = 11 ) var AdminAnswerSearchStatus = map[string]int{ "available": AnswerStatusAvailable, "deleted": AnswerStatusDeleted, + "pending": AnswerStatusPending, } // Answer answer @@ -61,6 +64,14 @@ type AnswerSearch struct { PageSize int `json:"page_size" form:"page_size"` // Search page size } +type PersonalAnswerPageQueryCond struct { + Page int + PageSize int + UserID string + Order string + ShowPending bool +} + // TableName answer table name func (Answer) TableName() string { return "answer" diff --git a/internal/entity/comment_entity.go b/internal/entity/comment_entity.go index 37f511983..8b79663a9 100644 --- a/internal/entity/comment_entity.go +++ b/internal/entity/comment_entity.go @@ -30,6 +30,7 @@ import ( const ( CommentStatusAvailable = 1 CommentStatusDeleted = 10 + CommentStatusPending = 11 ) // Comment comment diff --git a/internal/entity/question_entity.go b/internal/entity/question_entity.go index 2dd4ec2e7..280cc670b 100644 --- a/internal/entity/question_entity.go +++ b/internal/entity/question_entity.go @@ -27,6 +27,7 @@ const ( QuestionStatusAvailable = 1 QuestionStatusClosed = 2 QuestionStatusDeleted = 10 + QuestionStatusPending = 11 QuestionUnPin = 1 QuestionPin = 2 QuestionShow = 1 @@ -37,12 +38,14 @@ var AdminQuestionSearchStatus = map[string]int{ "available": QuestionStatusAvailable, "closed": QuestionStatusClosed, "deleted": QuestionStatusDeleted, + "pending": QuestionStatusPending, } var AdminQuestionSearchStatusIntToString = map[int]string{ QuestionStatusAvailable: "available", QuestionStatusClosed: "closed", QuestionStatusDeleted: "deleted", + QuestionStatusPending: "pending", } // Question question diff --git a/internal/entity/report_entity.go b/internal/entity/report_entity.go index be0d76c7e..07f5bc840 100644 --- a/internal/entity/report_entity.go +++ b/internal/entity/report_entity.go @@ -24,6 +24,7 @@ import "time" const ( ReportStatusPending = 1 ReportStatusCompleted = 2 + ReportStatusIgnore = 3 ReportStatusDeleted = 10 ) diff --git a/internal/entity/review_entity.go b/internal/entity/review_entity.go new file mode 100644 index 000000000..c88de0074 --- /dev/null +++ b/internal/entity/review_entity.go @@ -0,0 +1,47 @@ +/* + * 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 entity + +import "time" + +const ( + ReviewStatusPending = 1 + ReviewStatusApproved = 2 + ReviewStatusRejected = 3 +) + +// Review review +type Review struct { + ID int `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null BIGINT(20) user_id"` + ObjectID string `xorm:"not null BIGINT(20) object_id"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ReviewerUserID string `xorm:"not null default 0 BIGINT(20) reviewer_user_id"` + Submitter string `xorm:"not null default '' VARCHAR(100) submitter"` + Reason string `xorm:"not null TEXT reason"` + Status int `xorm:"not null default 0 INT(11) status"` +} + +// TableName review table name +func (Review) TableName() string { + return "review" +} diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index 494e0cf5b..adbe71753 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -68,6 +68,7 @@ var ( &entity.UserExternalLogin{}, &entity.UserNotificationConfig{}, &entity.PluginUserConfig{}, + &entity.Review{}, } roles = []*entity.Role{ @@ -275,7 +276,7 @@ var ( {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn't meet a community guideline."}`}, - {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, + {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`}, {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can't answer, but still can edit, vote and comment."}`}, diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index dbd3acdb8..8a259684d 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -95,6 +95,7 @@ var migrations = []Migration{ NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true), NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true), NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true), + NewMigration("v1.3.0", "add review", addReview, false), } func GetMigrations() []Migration { diff --git a/internal/migrations/v13.go b/internal/migrations/v13.go index 2c2c80a68..4fcde3811 100644 --- a/internal/migrations/v13.go +++ b/internal/migrations/v13.go @@ -24,6 +24,7 @@ import ( "encoding/json" "fmt" "time" + "xorm.io/builder" "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/entity" @@ -184,7 +185,7 @@ func updateTagCount(ctx context.Context, x *xorm.Engine) error { questionsHideMap[item.ObjectID] = false } questionList := make([]QuestionV13, 0) - err = x.Context(ctx).In("id", questionIDs).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) + err = x.Context(ctx).In("id", questionIDs).And(builder.Lt{"question.status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } @@ -256,7 +257,7 @@ func updateTagCount(ctx context.Context, x *xorm.Engine) error { // updateUserQuestionCount update user question count func updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error { questionList := make([]QuestionV13, 0) - err := x.Context(ctx).In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) + err := x.Context(ctx).Where(builder.Lt{"status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get question failed: %w", err) } diff --git a/internal/migrations/v20.go b/internal/migrations/v20.go new file mode 100644 index 000000000..a2349787d --- /dev/null +++ b/internal/migrations/v20.go @@ -0,0 +1,38 @@ +/* + * 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 migrations + +import ( + "context" + "fmt" + + "github.com/apache/incubator-answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addReview(ctx context.Context, x *xorm.Engine) error { + c := &entity.Config{Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`} + if _, err := x.Context(ctx).Update(c, &entity.Config{Key: "reason.not_clarity"}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + return x.Context(ctx).Sync(new(entity.Review)) +} diff --git a/internal/repo/activity/review_repo.go b/internal/repo/activity/review_repo.go new file mode 100644 index 000000000..3325a7625 --- /dev/null +++ b/internal/repo/activity/review_repo.go @@ -0,0 +1,125 @@ +/* + * 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 activity + +import ( + "context" + "fmt" + + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/pkg/converter" + "xorm.io/builder" + + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/activity" + "github.com/apache/incubator-answer/internal/service/activity_common" + "github.com/apache/incubator-answer/internal/service/config" + "github.com/apache/incubator-answer/internal/service/rank" + "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" +) + +// ReviewActivityRepo answer accepted +type ReviewActivityRepo struct { + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + configService *config.ConfigService +} + +const ( + EditAccepted = "edit.accepted" +) + +// NewReviewActivityRepo new repository +func NewReviewActivityRepo( + data *data.Data, + activityRepo activity_common.ActivityRepo, + userRankRepo rank.UserRankRepo, + configService *config.ConfigService, +) activity.ReviewActivityRepo { + return &ReviewActivityRepo{ + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + configService: configService, + } +} + +// Review user active +func (ar *ReviewActivityRepo) Review(ctx context.Context, act *schema.PassReviewActivity) (err error) { + cfg, err := ar.configService.GetConfigByKey(ctx, EditAccepted) + if err != nil { + return err + } + addActivity := &entity.Activity{ + UserID: act.UserID, + TriggerUserID: converter.StringToInt64(act.TriggerUserID), + ObjectID: act.ObjectID, + OriginalObjectID: act.OriginalObjectID, + ActivityType: cfg.ID, + Rank: cfg.GetIntValue(), + HasRank: 1, + RevisionID: converter.StringToInt64(act.RevisionID), + } + + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + user := &entity.User{} + exist, err := session.ID(addActivity.UserID).ForUpdate().Get(user) + if err != nil { + return nil, err + } + if !exist { + return nil, fmt.Errorf("user not exist") + } + + existsActivity := &entity.Activity{} + exist, err = session. + And(builder.Eq{"user_id": addActivity.UserID}). + And(builder.Eq{"activity_type": addActivity.ActivityType}). + And(builder.Eq{"revision_id": addActivity.RevisionID}). + Get(existsActivity) + if err != nil { + return nil, err + } + if exist { + return nil, nil + } + + err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) + if err != nil { + return nil, err + } + + _, err = session.Insert(addActivity) + if err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index e7d38d3eb..7e8b02e64 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -22,9 +22,11 @@ package activity import ( "context" "fmt" - "github.com/segmentfault/pacman/log" "time" + "github.com/apache/incubator-answer/internal/service/content" + "github.com/segmentfault/pacman/log" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/pkg/converter" @@ -39,7 +41,6 @@ import ( "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/apache/incubator-answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" @@ -59,7 +60,7 @@ func NewVoteRepo( activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, notificationQueueService notice_queue.NotificationQueueService, -) service.VoteRepo { +) content.VoteRepo { return &VoteRepo{ data: data, activityRepo: activityRepo, diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 60a49fa44..3edbec341 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -183,7 +183,7 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( return } -// GetQuestionCount +// GetAnswerCount count answer func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { var resp = new(entity.Answer) count, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusAvailable).Count(resp) @@ -336,6 +336,8 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc switch search.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") + case entity.AnswerSearchOrderByTimeAsc: + session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: @@ -363,6 +365,42 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc return rows, count, nil } +// GetPersonalAnswerPage personal answer page +func (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) ( + resp []*entity.Answer, total int64, err error) { + cond := &entity.Answer{ + UserID: req.UserID, + } + session := ar.data.DB.Context(ctx) + switch req.Order { + case entity.AnswerSearchOrderByTime: + session = session.OrderBy("created_at desc") + case entity.AnswerSearchOrderByTimeAsc: + session = session.OrderBy("created_at asc") + case entity.AnswerSearchOrderByVote: + session = session.OrderBy("vote_count desc") + default: + session = session.OrderBy("adopted desc,vote_count desc,created_at asc") + } + if req.ShowPending { + session = session.And("status != ?", entity.AnswerStatusDeleted) + } else { + session = session.And("status = ?", entity.AnswerStatusAvailable) + } + resp = make([]*entity.Answer, 0) + total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) + if err != nil { + return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range resp { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return resp, total, nil +} + func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( resp []*entity.Answer, total int64, err error) { cond := &entity.Answer{} diff --git a/internal/repo/collection/collection_group_repo.go b/internal/repo/collection/collection_group_repo.go index 0fc9c27a5..05711f5af 100644 --- a/internal/repo/collection/collection_group_repo.go +++ b/internal/repo/collection/collection_group_repo.go @@ -21,6 +21,8 @@ package collection import ( "context" + + "github.com/apache/incubator-answer/internal/service/collection" "xorm.io/xorm" "github.com/apache/incubator-answer/internal/base/data" @@ -28,7 +30,6 @@ import ( "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service" "github.com/segmentfault/pacman/errors" ) @@ -38,7 +39,7 @@ type collectionGroupRepo struct { } // NewCollectionGroupRepo new repository -func NewCollectionGroupRepo(data *data.Data) service.CollectionGroupRepo { +func NewCollectionGroupRepo(data *data.Data) collection.CollectionGroupRepo { return &collectionGroupRepo{ data: data, } diff --git a/internal/repo/comment/comment_repo.go b/internal/repo/comment/comment_repo.go index c8a135cfa..79f0eaa94 100644 --- a/internal/repo/comment/comment_repo.go +++ b/internal/repo/comment/comment_repo.go @@ -21,6 +21,7 @@ package comment import ( "context" + "github.com/segmentfault/pacman/log" "github.com/apache/incubator-answer/internal/base/data" @@ -93,6 +94,17 @@ func (cr *commentRepo) UpdateCommentContent( // GetComment get comment one func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( + comment *entity.Comment, exist bool, err error) { + comment = &entity.Comment{} + exist, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).ID(commentID).Get(comment) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetCommentWithoutStatus get comment one without status +func (cr *commentRepo) GetCommentWithoutStatus(ctx context.Context, commentID string) ( comment *entity.Comment, exist bool, err error) { comment = &entity.Comment{} exist, err = cr.data.DB.Context(ctx).ID(commentID).Get(comment) diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 931ccde19..3a517120e 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -38,6 +38,7 @@ import ( "github.com/apache/incubator-answer/internal/repo/rank" "github.com/apache/incubator-answer/internal/repo/reason" "github.com/apache/incubator-answer/internal/repo/report" + "github.com/apache/incubator-answer/internal/repo/review" "github.com/apache/incubator-answer/internal/repo/revision" "github.com/apache/incubator-answer/internal/repo/role" "github.com/apache/incubator-answer/internal/repo/search_common" @@ -75,6 +76,7 @@ var ProviderSetRepo = wire.NewSet( activity.NewAnswerActivityRepo, activity.NewUserActiveActivityRepo, activity.NewActivityRepo, + activity.NewReviewActivityRepo, tag.NewTagRepo, tag_common.NewTagCommonRepo, tag.NewTagRelRepo, @@ -97,4 +99,5 @@ var ProviderSetRepo = wire.NewSet( user_notification_config.NewUserNotificationConfigRepo, limit.NewRateLimitRepo, plugin_config.NewPluginUserConfigRepo, + review.NewReviewRepo, ) diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 6c97620a0..29ab9c5d7 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -276,7 +276,7 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { session := qr.data.DB.Context(ctx) - session.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -284,10 +284,10 @@ func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err return count, nil } -func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) { +func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) { session := qr.data.DB.Context(ctx) - session.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) - count, err = session.Count(&entity.Question{UserID: userID}) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) + count, err = session.Count(&entity.Question{UserID: userID, Show: show}) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -344,12 +344,16 @@ func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int } // GetQuestionPage query question page -func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden bool) ( +func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, + tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) - - session := qr.data.DB.Context(ctx).Where("question.status = ? OR question.status = ?", - entity.QuestionStatusAvailable, entity.QuestionStatusClosed) + session := qr.data.DB.Context(ctx) + status := []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed} + if showPending { + status = append(status, entity.QuestionStatusPending) + } + session.In("question.status", status) if len(tagIDs) > 0 { session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id") session.In("tag_rel.tag_id", tagIDs) diff --git a/internal/repo/repo_test/comment_repo_test.go b/internal/repo/repo_test/comment_repo_test.go index 669a8e236..642e3d75b 100644 --- a/internal/repo/repo_test/comment_repo_test.go +++ b/internal/repo/repo_test/comment_repo_test.go @@ -95,3 +95,19 @@ func Test_commentRepo_UpdateComment(t *testing.T) { err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) assert.NoError(t, err) } + +func Test_commentRepo_CannotGetDeletedComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + + _, exist, err := commentRepo.GetComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + assert.False(t, exist) +} diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index 653802069..d1c365e40 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -24,6 +24,7 @@ import ( "database/sql" "fmt" "os" + "path/filepath" "testing" "time" @@ -56,7 +57,7 @@ var ( } sqlite3DBSetting = TestDBSetting{ Driver: string(schemas.SQLITE), - Connection: os.TempDir() + "answer-test-data.db", + Connection: filepath.Join(os.TempDir(), "answer-test-data.db"), } dbSettingMapping = map[string]TestDBSetting{ mysqlDBSetting.Driver: mysqlDBSetting, diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go index 0120f31cc..013d036d3 100644 --- a/internal/repo/report/report_repo.go +++ b/internal/repo/report/report_repo.go @@ -22,7 +22,6 @@ package report import ( "context" - "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/report_common" @@ -62,31 +61,11 @@ func (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err } // GetReportListPage get report list page -func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) { - var ( - ok bool - status int - objectType int - session = rr.data.DB.Context(ctx) - cond = entity.Report{} - ) - - // parse status - status, ok = entity.ReportStatus[dto.Status] - if !ok { - status = entity.ReportStatus["pending"] - } - cond.Status = status - - // parse object type - objectType, ok = constant.ObjectTypeStrMapping[dto.ObjectType] - if ok { - cond.ObjectType = objectType - } - - // order - session.OrderBy("updated_at desc") - +func (rr *reportRepo) GetReportListPage(ctx context.Context, dto *schema.GetReportListPageDTO) ( + reports []*entity.Report, total int64, err error) { + cond := &entity.Report{} + cond.Status = dto.Status + session := rr.data.DB.Context(ctx).Desc("updated_at") total, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -104,9 +83,9 @@ func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Re return } -// UpdateByID handle report by ID -func (rr *reportRepo) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) { - _, err = rr.data.DB.Context(ctx).ID(id).Update(&handleData) +// UpdateStatus update report status by ID +func (rr *reportRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) { + _, err = rr.data.DB.Context(ctx).ID(id).Update(&entity.Report{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/review/review_repo.go b/internal/repo/review/review_repo.go new file mode 100644 index 000000000..91bc046c5 --- /dev/null +++ b/internal/repo/review/review_repo.go @@ -0,0 +1,94 @@ +/* + * 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 review + +import ( + "context" + + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/review" + "github.com/segmentfault/pacman/errors" +) + +// reviewRepo review repository +type reviewRepo struct { + data *data.Data +} + +// NewReviewRepo new repository +func NewReviewRepo(data *data.Data) review.ReviewRepo { + return &reviewRepo{ + data: data, + } +} + +// AddReview add review +func (cr *reviewRepo) AddReview(ctx context.Context, review *entity.Review) (err error) { + _, err = cr.data.DB.Context(ctx).Insert(review) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateReviewStatus update review status +func (cr *reviewRepo) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) { + _, err = cr.data.DB.Context(ctx).ID(reviewID).Update(&entity.Review{ + ReviewerUserID: reviewerUserID, Status: status}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReview get review one +func (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) ( + review *entity.Review, exist bool, err error) { + review = &entity.Review{} + exist, err = cr.data.DB.Context(ctx).ID(reviewID).Get(review) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReviewCount get review count +func (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) { + count, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReviewPage get review page +func (cr *reviewRepo) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) ( + reviewList []*entity.Review, total int64, err error) { + session := cr.data.DB.Context(ctx).Asc("created_at") + reviewList = make([]*entity.Review, 0) + total, err = pager.Help(page, pageSize, &reviewList, cond, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 958e6d7ed..eba232512 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -206,3 +206,18 @@ func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, } return } + +// CountUnreviewedRevision get unreviewed revision count +func (rr *revisionRepo) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) { + if len(objectTypeList) == 0 { + return 0, nil + } + session := rr.data.DB.Context(ctx) + session = session.And("status = ?", entity.RevisionUnreviewedStatus) + session = session.In("object_type", objectTypeList) + count, err = session.Count(&entity.Revision{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index 0443bfe9a..cd7de6960 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -22,12 +22,13 @@ package search_common import ( "context" "fmt" - tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" - "github.com/apache/incubator-answer/plugin" "strconv" "strings" "time" + tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" + "github.com/apache/incubator-answer/plugin" + "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/internal/base/data" @@ -234,7 +235,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } else { - resp, err = sr.parseResult(ctx, res) + resp, err = sr.parseResult(ctx, res, words) return } } @@ -342,7 +343,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID if len(tr) != 0 { total = converter.StringToInt64(string(tr[0]["total"])) } - resp, err = sr.parseResult(ctx, res) + resp, err = sr.parseResult(ctx, res, words) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -436,7 +437,7 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs } total = converter.StringToInt64(string(tr[0]["total"])) - resp, err = sr.parseResult(ctx, res) + resp, err = sr.parseResult(ctx, res, words) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -460,7 +461,7 @@ func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) } // ParseSearchPluginResult parse search plugin result -func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []*schema.SearchResult, err error) { +func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) { var ( qres []map[string][]byte res = make([]map[string][]byte, 0) @@ -483,11 +484,11 @@ func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin } res = append(res, qres[0]) } - return sr.parseResult(ctx, res) + return sr.parseResult(ctx, res, words) } // parseResult parse search result, return the data structure -func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []*schema.SearchResult, err error) { +func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte, words []string) (resp []*schema.SearchResult, err error) { questionIDs := make([]string, 0) userIDs := make([]string, 0) resultList := make([]*schema.SearchResult, 0) @@ -508,7 +509,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) QuestionID: QuestionID, Title: string(r["title"]), UrlTitle: htmltext.UrlTitle(string(r["title"])), - Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240), + Excerpt: htmltext.FetchMatchedExcerpt(string(r["parsed_text"]), words, "...", 100), CreatedAtParsed: tp.Unix(), UserInfo: &schema.SearchObjectUser{ ID: string(r["user_id"]), diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 7e856903d..1ea2be892 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -40,7 +40,6 @@ type AnswerAPIRouter struct { searchController *controller.SearchController revisionController *controller.RevisionController rankController *controller.RankController - adminReportController *controller_admin.ReportController adminUserController *controller_admin.UserAdminController reasonController *controller.ReasonController themeController *controller_admin.ThemeController @@ -54,6 +53,7 @@ type AnswerAPIRouter struct { pluginController *controller_admin.PluginController permissionController *controller.PermissionController userPluginController *controller.UserPluginController + reviewController *controller.ReviewController } func NewAnswerAPIRouter( @@ -70,7 +70,6 @@ func NewAnswerAPIRouter( searchController *controller.SearchController, revisionController *controller.RevisionController, rankController *controller.RankController, - adminReportController *controller_admin.ReportController, adminUserController *controller_admin.UserAdminController, reasonController *controller.ReasonController, themeController *controller_admin.ThemeController, @@ -84,6 +83,7 @@ func NewAnswerAPIRouter( pluginController *controller_admin.PluginController, permissionController *controller.PermissionController, userPluginController *controller.UserPluginController, + reviewController *controller.ReviewController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, @@ -99,7 +99,6 @@ func NewAnswerAPIRouter( searchController: searchController, revisionController: revisionController, rankController: rankController, - adminReportController: adminReportController, adminUserController: adminUserController, reasonController: reasonController, themeController: themeController, @@ -113,6 +112,7 @@ func NewAnswerAPIRouter( pluginController: pluginController, permissionController: permissionController, userPluginController: userPluginController, + reviewController: reviewController, } } @@ -194,6 +194,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/revisions/unreviewed", a.revisionController.GetUnreviewedRevisionList) r.PUT("/revisions/audit", a.revisionController.RevisionAudit) r.GET("/revisions/edit/check", a.revisionController.CheckCanUpdateRevision) + r.GET("/reviewing/type", a.revisionController.GetReviewingType) // comment r.POST("/comment", a.commentController.AddComment) @@ -202,6 +203,12 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // report r.POST("/report", a.reportController.AddReport) + r.GET("/report/unreviewed/post", a.reportController.GetUnreviewedReportPostPage) + r.PUT("/report/review", a.reportController.ReviewReport) + + // review + r.GET("/review/pending/post/page", a.reviewController.GetUnreviewedPostPage) + r.PUT("/review/pending/post", a.reviewController.UpdateReview) // vote r.POST("/vote/up", a.voteController.VoteUp) @@ -286,10 +293,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.GET("/answer/page", a.questionController.AdminAnswerPage) r.PUT("/answer/status", a.answerController.AdminUpdateAnswerStatus) - // report - r.GET("/reports/page", a.adminReportController.ListReportPage) - r.PUT("/report", a.adminReportController.Handle) - // user r.GET("/users/page", a.adminUserController.GetUserPage) r.PUT("/user/status", a.adminUserController.UpdateUserStatus) diff --git a/internal/schema/activity.go b/internal/schema/activity.go index 66a6b1881..2ec837aa2 100644 --- a/internal/schema/activity.go +++ b/internal/schema/activity.go @@ -101,3 +101,12 @@ type ObjectTimelineTag struct { Recommend bool `json:"recommend"` Reserved bool `json:"reserved"` } + +// PassReviewActivity pass review activity +type PassReviewActivity struct { + UserID string `json:"user_id"` + TriggerUserID string `json:"trigger_user_id"` + ObjectID string `json:"object_id"` + OriginalObjectID string `json:"original_object_id"` + RevisionID string `json:"revision_id"` +} diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index 2cd459478..d0c208e69 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -98,22 +98,22 @@ type AnswerListReq struct { } type AnswerInfo struct { - ID string `json:"id"` - QuestionID string `json:"question_id"` - Content string `json:"content"` - HTML string `json:"html"` - CreateTime int64 `json:"create_time"` - UpdateTime int64 `json:"update_time"` - Accepted int `json:"accepted"` - UserID string `json:"-"` - UpdateUserID string `json:"-"` - UserInfo *UserBasicInfo `json:"user_info,omitempty"` - UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` - Collected bool `json:"collected"` - VoteStatus string `json:"vote_status"` - VoteCount int `json:"vote_count"` - QuestionInfo *QuestionInfo `json:"question_info,omitempty"` - Status int `json:"status"` + ID string `json:"id"` + QuestionID string `json:"question_id"` + Content string `json:"content"` + HTML string `json:"html"` + CreateTime int64 `json:"create_time"` + UpdateTime int64 `json:"update_time"` + Accepted int `json:"accepted"` + UserID string `json:"-"` + UpdateUserID string `json:"-"` + UserInfo *UserBasicInfo `json:"user_info,omitempty"` + UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` + Collected bool `json:"collected"` + VoteStatus string `json:"vote_status"` + VoteCount int `json:"vote_count"` + QuestionInfo *QuestionInfoResp `json:"question_info,omitempty"` + Status int `json:"status"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index c3b64fb87..4e0e93169 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -62,6 +62,7 @@ type GetRedDot struct { CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` UserID string `json:"-"` + IsAdmin bool `json:"-"` } // NotificationMsg notification message diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 5b5839933..48fd97c56 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -214,7 +214,7 @@ type QuestionBaseInfo struct { AcceptedAnswer bool `json:"accepted_answer"` } -type QuestionInfo struct { +type QuestionInfoResp struct { ID string `json:"id" ` Title string `json:"title"` UrlTitle string `json:"url_title"` @@ -265,6 +265,8 @@ type AdminQuestionInfo struct { ID string `json:"id"` Title string `json:"title"` VoteCount int `json:"vote_count"` + Show int `json:"show"` + Pin int `json:"pin"` AnswerCount int `json:"answer_count"` AcceptedAnswerID string `json:"accepted_answer_id"` CreateTime int64 `json:"create_time"` @@ -277,9 +279,10 @@ type AdminQuestionInfo struct { type OperationLevel string const ( - OperationLevelInfo OperationLevel = "info" - OperationLevelDanger OperationLevel = "danger" - OperationLevelWarning OperationLevel = "warning" + OperationLevelInfo OperationLevel = "info" + OperationLevelDanger OperationLevel = "danger" + OperationLevelWarning OperationLevel = "warning" + OperationLevelSecondary OperationLevel = "secondary" ) type Operation struct { @@ -353,6 +356,7 @@ type QuestionPageReq struct { LoginUserID string `json:"-"` UserIDBeSearched string `json:"-"` TagID string `json:"-"` + ShowPending bool `json:"-"` } const ( @@ -403,7 +407,7 @@ type QuestionPageRespOperator struct { type AdminQuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` - StatusCond string `validate:"omitempty,oneof=normal closed deleted" form:"status"` + StatusCond string `validate:"omitempty,oneof=normal closed deleted pending" form:"status"` Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` Status int `json:"-"` LoginUserID string `json:"-"` @@ -424,7 +428,7 @@ func (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, type AdminAnswerPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` - StatusCond string `validate:"omitempty,oneof=normal deleted" form:"status"` + StatusCond string `validate:"omitempty,oneof=normal deleted pending" form:"status"` Query string `validate:"omitempty,gt=0,lte=100" form:"query"` QuestionID string `validate:"omitempty,gt=0,lte=24" form:"question_id"` QuestionTitle string `json:"-"` @@ -470,6 +474,7 @@ type PersonalQuestionPageReq struct { OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` LoginUserID string `json:"-"` + IsAdmin bool `json:"-"` } type PersonalAnswerPageReq struct { @@ -478,6 +483,7 @@ type PersonalAnswerPageReq struct { OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` LoginUserID string `json:"-"` + IsAdmin bool `json:"-"` } type PersonalCollectionPageReq struct { diff --git a/internal/schema/reason_schema.go b/internal/schema/reason_schema.go index 82d627438..b408ce9bc 100644 --- a/internal/schema/reason_schema.go +++ b/internal/schema/reason_schema.go @@ -25,6 +25,7 @@ import ( ) type ReasonItem struct { + ReasonKey string `json:"reason_key"` ReasonType int `json:"reason_type"` Name string `json:"name"` Description string `json:"description"` @@ -52,9 +53,10 @@ func (r *ReasonItem) Translate(keyPrefix string, lang i18n.Language) { return fieldTr } // If i18n key not exists, return fieldData original value - return fieldData + "没翻译" + return fieldData } + r.ReasonKey = keyPrefix r.Name = trField("name", r.Name) r.Description = trField("desc", r.Description) r.Placeholder = trField("placeholder", r.Placeholder) diff --git a/internal/schema/report_schema.go b/internal/schema/report_schema.go index e853259cf..1f702df47 100644 --- a/internal/schema/report_schema.go +++ b/internal/schema/report_schema.go @@ -19,12 +19,6 @@ package schema -import ( - "time" - - "github.com/apache/incubator-answer/internal/base/constant" -) - // AddReportReq add report request type AddReportReq struct { // object id @@ -70,52 +64,52 @@ type ReportHandleReq struct { // GetReportListPageDTO report list data transfer object type GetReportListPageDTO struct { - ObjectType string - Status string - Page int - PageSize int + Page int + PageSize int + Status int } // GetReportListPageResp get report list type GetReportListPageResp struct { - ID string `json:"id"` - ReportedUser *UserBasicInfo `json:"reported_user"` - ReportUser *UserBasicInfo `json:"report_user"` - - Content string `json:"content"` - FlaggedContent string `json:"flagged_content"` - OType string `json:"object_type"` - - ObjectID string `json:"-"` - QuestionID string `json:"question_id"` - AnswerID string `json:"answer_id"` - CommentID string `json:"comment_id"` - - Title string `json:"title"` - Excerpt string `json:"excerpt"` - - // create time - CreatedAt time.Time `json:"-"` - CreatedAtParsed int64 `json:"created_at"` - - UpdatedAt time.Time `json:"_"` - UpdatedAtParsed int64 `json:"updated_at"` - - Reason *ReasonItem `json:"reason"` - FlaggedReason *ReasonItem `json:"flagged_reason"` - - UserID string `json:"-"` - ReportedUserID string `json:"-"` - Status int `json:"-"` - ObjectType int `json:"-"` - ReportType int `json:"-"` - FlaggedType int `json:"-"` + FlagID string `json:"flag_id"` + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type" enums:"question,answer,comment"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + OriginalText string `json:"original_text"` + ParsedText string `json:"parsed_text"` + AnswerCount int `json:"answer_count"` + AnswerAccepted bool `json:"answer_accepted"` + Tags []*TagResp `json:"tags"` + ObjectStatus int `json:"object_status"` + ObjectShowStatus int `json:"object_show_status"` + AuthorUserInfo UserBasicInfo `json:"author_user_info"` + SubmitAt int64 `json:"submit_at"` + SubmitterUser UserBasicInfo `json:"submitter_user"` + Reason *ReasonItem `json:"reason"` + ReasonContent string `json:"reason_content"` } -// Format format result -func (r *GetReportListPageResp) Format() { - r.OType = constant.ObjectTypeNumberMapping[r.ObjectType] +// GetUnreviewedReportPostPageReq get unreviewed report post page request +type GetUnreviewedReportPostPageReq struct { + Page int `json:"page" form:"page"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} - r.CreatedAtParsed = r.CreatedAt.Unix() - r.UpdatedAtParsed = r.UpdatedAt.Unix() +// ReviewReportReq review report request +type ReviewReportReq struct { + FlagID string `validate:"required" json:"flag_id"` + OperationType string `validate:"required,oneof=edit_post close_post delete_post unlist_post ignore_report" json:"operation_type"` + CloseType int `validate:"omitempty" json:"close_type"` + CloseMsg string `validate:"omitempty" json:"close_msg"` + Title string `validate:"omitempty,notblank,gte=6,lte=150" json:"title"` + Content string `validate:"omitempty,notblank,gte=6,lte=65535" json:"content"` + Tags []*TagItem `validate:"omitempty,dive" json:"tags"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` } diff --git a/internal/schema/review_schema.go b/internal/schema/review_schema.go new file mode 100644 index 000000000..8c5ba0d6f --- /dev/null +++ b/internal/schema/review_schema.go @@ -0,0 +1,61 @@ +package schema + +import ( + "github.com/apache/incubator-answer/internal/base/validator" + "github.com/apache/incubator-answer/pkg/uid" +) + +// UpdateReviewReq update review request +type UpdateReviewReq struct { + ReviewID int `validate:"required" json:"review_id"` + Status string `validate:"required,oneof=approve reject" json:"status"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +func (r *UpdateReviewReq) IsApprove() bool { + return r.Status == "approve" +} + +func (r *UpdateReviewReq) IsReject() bool { + return r.Status == "reject" +} + +// GetUnreviewedPostPageReq get review page request +type GetUnreviewedPostPageReq struct { + ObjectID string `validate:"omitempty" form:"object_id"` + Page int `validate:"omitempty" form:"page"` + ReviewerMapping map[string]string `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +func (r *GetUnreviewedPostPageReq) Check() (errField []*validator.FormErrorField, err error) { + if len(r.ObjectID) > 0 { + r.Page = 1 + r.ObjectID = uid.DeShortID(r.ObjectID) + } + return +} + +// GetUnreviewedPostPageResp get review page response +type GetUnreviewedPostPageResp struct { + ReviewID int `json:"review_id"` + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type" enums:"question,answer,comment"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + OriginalText string `json:"original_text"` + ParsedText string `json:"parsed_text"` + Tags []*TagResp `json:"tags"` + ObjectStatus int `json:"object_status"` + ObjectShowStatus int `json:"object_show_status"` + AuthorUserInfo UserBasicInfo `json:"author_user_info"` + SubmitAt int64 `json:"submit_at"` + SubmitterDisplayName string `json:"submitter_display_name"` + Reason string `json:"reason"` +} diff --git a/internal/schema/revision_schema.go b/internal/schema/revision_schema.go index d52c1c4ad..f1b116882 100644 --- a/internal/schema/revision_schema.go +++ b/internal/schema/revision_schema.go @@ -90,25 +90,47 @@ type GetUnreviewedRevisionResp struct { // GetRevisionResp get revision response type GetRevisionResp struct { - // id - ID string `json:"id"` - // user id - UserID string `json:"use_id"` - // object id - ObjectID string `json:"object_id"` - // object type - ObjectType int `json:"-"` - // title - Title string `json:"title"` - // content - Content string `json:"-"` - // content parsed - ContentParsed interface{} `json:"content"` - // revision status(normal: 1; delete 2) - Status int `json:"status"` - // create time + ID string `json:"id"` + UserID string `json:"use_id"` + ObjectID string `json:"object_id"` + ObjectType int `json:"-"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Content string `json:"-"` + ContentParsed interface{} `json:"content"` + Status int `json:"status"` CreatedAt time.Time `json:"-"` CreatedAtParsed int64 `json:"create_at"` UserInfo UserBasicInfo `json:"user_info"` Log string `json:"reason"` } + +// GetReviewingTypeReq get reviewing type request +type GetReviewingTypeReq struct { + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + IsAdmin bool `json:"-"` + UserID string `json:"-"` +} + +func (r *GetReviewingTypeReq) GetCanReviewObjectTypes() []int { + objectType := make([]int, 0) + if r.CanReviewAnswer { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) + } + if r.CanReviewQuestion { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) + } + if r.CanReviewTag { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) + } + return objectType +} + +// GetReviewingTypeResp get reviewing type response +type GetReviewingTypeResp struct { + Name string `json:"name"` + Label string `json:"label"` + TodoAmount int64 `json:"todo_amount"` +} diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 90b08cebe..59f32c5ec 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -20,11 +20,12 @@ package schema import ( + "regexp" + "strings" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/plugin" - "regexp" - "strings" ) type SearchDTO struct { @@ -40,12 +41,34 @@ type SearchDTO struct { func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) { // Replace special characters. // Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format. - s.Query = regexp.MustCompile(`[+#.<>\-_()*]`).ReplaceAllString(s.Query, " ") - s.Query = regexp.MustCompile(`\s+`).ReplaceAllString(s.Query, " ") - s.Query = strings.TrimSpace(s.Query) + replacedContent, patterns := ReplaceSearchContent(s.Query) + s.Query = strings.Join(append(patterns, replacedContent), " ") + return nil, nil } +func ReplaceSearchContent(content string) (string, []string) { + // Define the regular expressions for key:value pairs and [tag] + keyValueRegex := regexp.MustCompile(`\w+:\S+`) + tagRegex := regexp.MustCompile(`\[\w+\]`) + // Define the pattern for characters to replace + replaceCharsPattern := regexp.MustCompile(`[+#.<>\-_()*]`) + + // Extract key:value pairs + keyValues := keyValueRegex.FindAllString(content, -1) + // Extract [tag] + tags := tagRegex.FindAllString(content, -1) + + // Replace key:value pairs and [tag] with empty string + contentWithoutPatterns := keyValueRegex.ReplaceAllString(content, "") + contentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, "") + + // Replace characters with pattern [+#.<>_()*] with space + replacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, " ") + + return strings.TrimSpace(replacedContent), append(keyValues, tags...) +} + type SearchCondition struct { // search target type: all/question/answer TargetType string diff --git a/internal/schema/search_schema_test.go b/internal/schema/search_schema_test.go new file mode 100644 index 000000000..50ffdf14b --- /dev/null +++ b/internal/schema/search_schema_test.go @@ -0,0 +1,22 @@ +package schema + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplaceSearchContent(t *testing.T) { + content := "user:aaa [tag] ssssfdfdf-as#fsadf" + replacedContent, patterns := ReplaceSearchContent(content) + ret := strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa [tag] ssssfdfdf as fsadf", ret) + + content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3" + replacedContent, patterns = ReplaceSearchContent(content) + ret = strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf", ret) +} diff --git a/internal/schema/simple_obj_info_schema.go b/internal/schema/simple_obj_info_schema.go index 7d2d4057e..7c4fa9af5 100644 --- a/internal/schema/simple_obj_info_schema.go +++ b/internal/schema/simple_obj_info_schema.go @@ -34,9 +34,20 @@ type SimpleObjectInfo struct { } type UnreviewedRevisionInfoInfo struct { - ObjectID string `json:"object_id"` - Title string `json:"title"` - Content string `json:"content"` - Html string `json:"html"` - Tags []*TagResp `json:"tags"` + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type"` + ObjectCreatorUserID string `json:"object_creator_user_id"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Content string `json:"content"` + Html string `json:"html"` + AnswerCount int `json:"answer_count"` + AnswerAccepted bool `json:"answer_accepted"` + Tags []*TagResp `json:"tags"` + Status int `json:"status"` + ShowStatus int `json:"show_status"` } diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 2953b944a..ea19d0462 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -40,6 +40,7 @@ type SiteGeneralReq struct { Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"` SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"` ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` + CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"` } func (r *SiteGeneralReq) FormatSiteUrl() { diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 539090663..a95acfc77 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -21,6 +21,7 @@ package schema import ( "encoding/json" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/translator" "github.com/segmentfault/pacman/errors" @@ -279,6 +280,12 @@ func CustomAvatar(url string) *AvatarInfo { func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) { req.BioHTML = converter.Markdown2BasicHTML(req.Bio) + if len(req.Website) > 0 && !checker.IsURL(req.Website) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "website", + ErrorMsg: reason.InvalidURLError, + }), errors.BadRequest(reason.InvalidURLError) + } return nil, nil } @@ -355,6 +362,7 @@ type UserBasicInfo struct { type GetOtherUserInfoByUsernameReq struct { Username string `validate:"required,gt=0,lte=500" form:"username"` UserID string `json:"-"` + IsAdmin bool `json:"-"` } type GetOtherUserInfoResp struct { diff --git a/internal/service/activity/review_active.go b/internal/service/activity/review_active.go new file mode 100644 index 000000000..0f0a9c240 --- /dev/null +++ b/internal/service/activity/review_active.go @@ -0,0 +1,31 @@ +/* + * 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 activity + +import ( + "context" + + "github.com/apache/incubator-answer/internal/schema" +) + +// ReviewActivityRepo interface +type ReviewActivityRepo interface { + Review(ctx context.Context, sct *schema.PassReviewActivity) (err error) +} diff --git a/internal/service/activity_type/activity_type.go b/internal/service/activity_type/activity_type.go index b41d0770b..5db98041d 100644 --- a/internal/service/activity_type/activity_type.go +++ b/internal/service/activity_type/activity_type.go @@ -31,6 +31,7 @@ const ( AnswerAccepted = "answer.accepted" AnswerAccept = "answer.accept" CommentVoteUp = "comment.vote_up" + EditAccepted = "edit.accepted" ) var ( @@ -70,5 +71,6 @@ var ( AnswerAccepted: "action_activity_type.accepted", AnswerAccept: "action_activity_type.accept", CommentVoteUp: "action_activity_type.upvote", + EditAccepted: "action_activity_type.edit", } ) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 4d8959fe4..af2be2f05 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -43,6 +43,8 @@ type AnswerRepo interface { GetCountByUserID(ctx context.Context, userID string) (int64, error) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) + GetPersonalAnswerPage(ctx context.Context, cond *entity.PersonalAnswerPageQueryCond) ( + resp []*entity.Answer, total int64, err error) AdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) GetAnswerCount(ctx context.Context) (count int64, err error) @@ -88,6 +90,11 @@ func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) return list, count, err } +func (as *AnswerCommon) PersonalAnswerPage(ctx context.Context, + cond *entity.PersonalAnswerPageQueryCond) ([]*entity.Answer, int64, error) { + return as.answerRepo.GetPersonalAnswerPage(ctx, cond) +} + func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { info := schema.AnswerInfo{} info.ID = data.ID diff --git a/internal/service/collection_group_service.go b/internal/service/collection/collection_group_service.go similarity index 99% rename from internal/service/collection_group_service.go rename to internal/service/collection/collection_group_service.go index 2620c54c7..3f6f3db13 100644 --- a/internal/service/collection_group_service.go +++ b/internal/service/collection/collection_group_service.go @@ -17,7 +17,7 @@ * under the License. */ -package service +package collection import ( "context" diff --git a/internal/service/collection_service.go b/internal/service/collection/collection_service.go similarity index 99% rename from internal/service/collection_service.go rename to internal/service/collection/collection_service.go index 1072d1abc..1820ac523 100644 --- a/internal/service/collection_service.go +++ b/internal/service/collection/collection_service.go @@ -17,10 +17,11 @@ * under the License. */ -package service +package collection import ( "context" + "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 0caa2a14c..a689a95dd 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -21,6 +21,8 @@ package comment import ( "context" + "time" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/base/reason" @@ -40,7 +42,6 @@ import ( "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "time" ) // CommentRepo comment repository @@ -211,7 +212,8 @@ func (cs *CommentService) addCommentNotification( resp.ReplyUserDisplayName = replyUser.DisplayName resp.ReplyUserStatus = replyUser.Status } - cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID) + cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID, + objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) alreadyNotifiedUserID[replyUser.ID] = true return nil, nil } @@ -227,10 +229,10 @@ func (cs *CommentService) addCommentNotification( if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, - objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText) + objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, - objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText) + objInfo.ObjectCreatorUserID, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } return nil, nil } @@ -579,7 +581,8 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } -func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) { +func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID, + questionID, questionTitle, commentSummary string) { msg := &schema.NotificationMsg{ ReceiverUserID: replyUserID, TriggerUserID: commentUserID, @@ -589,6 +592,35 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationReplyToYou cs.notificationQueueService.Send(ctx, msg) + + // Send external notification. + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", replyUserID) + return + } + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewCommentTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + CommentID: commentID, + CommentSummary: commentSummary, + UnsubscribeCode: token.GenerateToken(), + } + commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) + if commentUser != nil { + rawData.CommentUserDisplayName = commentUser.DisplayName + } + externalNotificationMsg.NewCommentTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } func (cs *CommentService) notificationMention( diff --git a/internal/service/comment_common/comment_service.go b/internal/service/comment_common/comment_service.go index eea32e6c3..3554a358b 100644 --- a/internal/service/comment_common/comment_service.go +++ b/internal/service/comment_common/comment_service.go @@ -31,6 +31,7 @@ import ( // CommentCommonRepo comment repository type CommentCommonRepo interface { GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) + GetCommentWithoutStatus(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentCount(ctx context.Context) (count int64, err error) RemoveAllUserComment(ctx context.Context, userID string) (err error) } diff --git a/internal/service/answer_service.go b/internal/service/content/answer_service.go similarity index 94% rename from internal/service/answer_service.go rename to internal/service/content/answer_service.go index 7da98582a..d96d75c5c 100644 --- a/internal/service/answer_service.go +++ b/internal/service/content/answer_service.go @@ -17,7 +17,7 @@ * under the License. */ -package service +package content import ( "context" @@ -37,10 +37,12 @@ import ( "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/permission" questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision_common" "github.com/apache/incubator-answer/internal/service/role" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/converter" + "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/pkg/token" "github.com/apache/incubator-answer/pkg/uid" "github.com/segmentfault/pacman/errors" @@ -64,6 +66,7 @@ type AnswerService struct { notificationQueueService notice_queue.NotificationQueueService externalNotificationQueueService notice_queue.ExternalNotificationQueueService activityQueueService activity_queue.ActivityQueueService + reviewService *review.ReviewService } func NewAnswerService( @@ -82,6 +85,7 @@ func NewAnswerService( notificationQueueService notice_queue.NotificationQueueService, externalNotificationQueueService notice_queue.ExternalNotificationQueueService, activityQueueService activity_queue.ActivityQueueService, + reviewService *review.ReviewService, ) *AnswerService { return &AnswerService{ answerRepo: answerRepo, @@ -99,6 +103,7 @@ func NewAnswerService( notificationQueueService: notificationQueueService, externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, + reviewService: reviewService, } } @@ -165,6 +170,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns //} as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: answerInfo.ID, OriginalObjectID: answerInfo.ID, ActivityTypeKey: constant.ActAnswerDeleted, @@ -222,7 +228,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion) return "", err } - insertData := new(entity.Answer) + insertData := &entity.Answer{} insertData.UserID = req.UserID insertData.OriginalText = req.Content insertData.ParsedText = req.HTML @@ -230,11 +236,18 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( insertData.QuestionID = req.QuestionID insertData.RevisionID = "0" insertData.LastEditUserID = "0" - insertData.Status = entity.AnswerStatusAvailable + insertData.Status = entity.AnswerStatusPending //insertData.UpdatedAt = now if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil { return "", err } + if as.reviewService.AddAnswerReview(ctx, insertData) { + if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, entity.AnswerStatusAvailable); err != nil { + return "", err + } else { + insertData.Status = entity.AnswerStatusAvailable + } + } err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID) if err != nil { log.Error("IncreaseAnswerCount error", err.Error()) @@ -267,8 +280,10 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( if err != nil { return insertData.ID, err } - as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title, - insertData.OriginalText) + if insertData.Status == entity.AnswerStatusAvailable { + as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title, + htmltext.FetchExcerpt(insertData.ParsedText, "...", 240)) + } as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, @@ -448,7 +463,7 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, } } -func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfo, bool, error) { +func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) { answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) if err != nil { return nil, nil, has, err @@ -511,25 +526,15 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A if !exist { return errors.BadRequest(reason.AnswerNotFound) } - err = as.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, setStatus) - if err != nil { - return err - } if setStatus == entity.AnswerStatusDeleted { - // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, - // facing the problem of recovery. - //err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) - //if err != nil { - // log.Errorf("admin delete question then rank rollback error %s", err.Error()) - //} - as.activityQueueService.Send(ctx, &schema.ActivityMsg{ - UserID: req.UserID, - TriggerUserID: converter.StringToInt64(req.UserID), - ObjectID: answerInfo.ID, - OriginalObjectID: answerInfo.ID, - ActivityTypeKey: constant.ActAnswerDeleted, - }) + if err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ + ID: req.AnswerID, + UserID: req.UserID, + CanDelete: true, + }); err != nil { + return err + } msg := &schema.NotificationMsg{} msg.ObjectID = answerInfo.ID @@ -543,13 +548,12 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A // recover if setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted { - as.activityQueueService.Send(ctx, &schema.ActivityMsg{ - UserID: req.UserID, - TriggerUserID: converter.StringToInt64(req.UserID), - ObjectID: answerInfo.ID, - OriginalObjectID: answerInfo.ID, - ActivityTypeKey: constant.ActAnswerUndeleted, - }) + if err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{ + AnswerID: req.AnswerID, + UserID: req.UserID, + }); err != nil { + return err + } } return nil } diff --git a/internal/service/question_service.go b/internal/service/content/question_service.go similarity index 93% rename from internal/service/question_service.go rename to internal/service/content/question_service.go index a4d5ffe68..e4f2b499c 100644 --- a/internal/service/question_service.go +++ b/internal/service/content/question_service.go @@ -17,7 +17,7 @@ * under the License. */ -package service +package content import ( "encoding/json" @@ -36,17 +36,20 @@ import ( "github.com/apache/incubator-answer/internal/service/activity" "github.com/apache/incubator-answer/internal/service/activity_queue" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" + "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/meta" "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/notification" "github.com/apache/incubator-answer/internal/service/permission" questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision_common" "github.com/apache/incubator-answer/internal/service/role" "github.com/apache/incubator-answer/internal/service/siteinfo_common" tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/checker" "github.com/apache/incubator-answer/pkg/converter" "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/pkg/token" @@ -77,6 +80,8 @@ type QuestionService struct { activityQueueService activity_queue.ActivityQueueService siteInfoService siteinfo_common.SiteInfoCommonService newQuestionNotificationService *notification.ExternalNotificationService + reviewService *review.ReviewService + configService *config.ConfigService } func NewQuestionService( @@ -96,6 +101,8 @@ func NewQuestionService( activityQueueService activity_queue.ActivityQueueService, siteInfoService siteinfo_common.SiteInfoCommonService, newQuestionNotificationService *notification.ExternalNotificationService, + reviewService *review.ReviewService, + configService *config.ConfigService, ) *QuestionService { return &QuestionService{ questionRepo: questionRepo, @@ -114,6 +121,8 @@ func NewQuestionService( activityQueueService: activityQueueService, siteInfoService: siteInfoService, newQuestionNotificationService: newQuestionNotificationService, + reviewService: reviewService, + configService: configService, } } @@ -126,6 +135,14 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ return nil } + cf, err := qs.configService.GetConfigByID(ctx, req.CloseType) + if err != nil || cf == nil { + return errors.BadRequest(reason.ReportNotFound) + } + if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.CloseMsg) { + return errors.BadRequest(reason.InvalidURLError) + } + questionInfo.Status = entity.QuestionStatusClosed err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { @@ -299,7 +316,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question question.LastAnswerID = "0" question.LastEditUserID = "0" //question.PostUpdateTime = nil - question.Status = entity.QuestionStatusAvailable + question.Status = entity.QuestionStatusPending question.RevisionID = "0" question.CreatedAt = now question.PostUpdateTime = now @@ -310,6 +327,13 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question if err != nil { return } + if qs.reviewService.AddQuestionReview(ctx, question, req.Tags) { + if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, entity.QuestionStatusAvailable); err != nil { + return nil, err + } else { + question.Status = entity.AnswerStatusAvailable + } + } objectTagData := schema.TagChange{} objectTagData.ObjectID = question.ID objectTagData.Tags = req.Tags @@ -356,8 +380,10 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question RevisionID: revisionID, }) - qs.externalNotificationQueueService.Send(ctx, - schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags)) + if question.Status == entity.QuestionStatusAvailable { + qs.externalNotificationQueueService.Send(ctx, + schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags)) + } questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return @@ -513,7 +539,8 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov // log.Errorf("user DeleteQuestion rank rollback error %s", err.Error()) // } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ - UserID: req.UserID, + UserID: questionInfo.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionDeleted, @@ -616,7 +643,7 @@ func (qs *QuestionService) RecoverQuestion(ctx context.Context, req *schema.Ques } // update tag's question count - if err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, questionInfo.ID); err != nil { + if err = qs.tagCommon.RecoverTagRelListByObjectID(ctx, questionInfo.ID); err != nil { log.Errorf("remove tag rel list by object id error %v", err) } @@ -754,7 +781,7 @@ func (qs *QuestionService) notificationInviteUser( // UpdateQuestion update question func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { var canUpdate bool - questionInfo = &schema.QuestionInfo{} + questionInfo = &schema.QuestionInfoResp{} _, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) if err != nil { @@ -917,13 +944,14 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest // GetQuestion get question one func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string, - per schema.QuestionPermission) (resp *schema.QuestionInfo, err error) { + per schema.QuestionPermission) (resp *schema.QuestionInfoResp, err error) { question, err := qs.questioncommon.Info(ctx, questionID, userID) if err != nil { return } - // If the question is deleted, only the administrator and the author can view it - if question.Status == entity.QuestionStatusDeleted && !per.CanReopen && question.UserID != userID { + // If the question is deleted or pending, only the administrator and the author can view it + if (question.Status == entity.QuestionStatusDeleted || + question.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID { return nil, errors.NotFound(reason.QuestionNotFound) } if question.Status != entity.QuestionStatusClosed { @@ -953,6 +981,12 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s operation.Level = schema.OperationLevelDanger question.Operation = operation } + if question.Status == entity.QuestionStatusPending { + operation := &schema.Operation{} + operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionUnderReview) + operation.Level = schema.OperationLevelSecondary + question.Operation = operation + } question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240) question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.Status, @@ -966,7 +1000,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s // GetQuestionAndAddPV get question one func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string, per schema.QuestionPermission) ( - resp *schema.QuestionInfo, err error) { + resp *schema.QuestionInfoResp, err error) { err = qs.questioncommon.UpdatePv(ctx, questionID) if err != nil { log.Error(err) @@ -1003,6 +1037,10 @@ func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema search.PageSize = req.PageSize search.UserIDBeSearched = userinfo.ID search.LoginUserID = req.LoginUserID + // Only author and administrator can view the pending question + if req.LoginUserID == userinfo.ID || req.IsAdmin { + search.ShowPending = true + } questionList, total, err := qs.GetQuestionPage(ctx, search) if err != nil { return nil, err @@ -1029,17 +1067,18 @@ func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.P if !exist { return nil, errors.BadRequest(reason.UserNotFound) } - answersearch := &entity.AnswerSearch{} - answersearch.UserID = userinfo.ID - answersearch.PageSize = req.PageSize - answersearch.Page = req.Page + cond := &entity.PersonalAnswerPageQueryCond{} + cond.UserID = userinfo.ID + cond.Page = req.Page + cond.PageSize = req.PageSize + cond.ShowPending = req.IsAdmin || req.LoginUserID == cond.UserID if req.OrderCond == "newest" { - answersearch.Order = entity.AnswerSearchOrderByTime + cond.Order = entity.AnswerSearchOrderByTime } else { - answersearch.Order = entity.AnswerSearchOrderByDefault + cond.Order = entity.AnswerSearchOrderByDefault } questionIDs := make([]string, 0) - answerList, total, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) + answerList, total, err := qs.questioncommon.AnswerCommon.PersonalAnswerPage(ctx, cond) if err != nil { return nil, err } @@ -1080,7 +1119,7 @@ func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.P // PersonalCollectionPage get collection list by user func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) ( pageModel *pager.PageModel, err error) { - list := make([]*schema.QuestionInfo, 0) + list := make([]*schema.QuestionInfoResp, 0) collectionSearch := &entity.CollectionSearch{} collectionSearch.UserID = req.UserID collectionSearch.Page = req.Page @@ -1235,7 +1274,17 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin search.Tag = tagNames[0] } search.LoginUserID = loginUserID - return qs.GetQuestionPage(ctx, search) + similarQuestions, _, err := qs.GetQuestionPage(ctx, search) + if err != nil { + return nil, 0, err + } + var result []*schema.QuestionPageResp + for _, v := range similarQuestions { + if uid.DeShortID(v.ID) != questionID { + result = append(result, v) + } + } + return result, int64(len(result)), nil } // GetQuestionPage query questions page @@ -1283,7 +1332,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques } questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize, - tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden) + tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden, req.ShowPending) if err != nil { return nil, 0, err } diff --git a/internal/service/revision_service.go b/internal/service/content/revision_service.go similarity index 80% rename from internal/service/revision_service.go rename to internal/service/content/revision_service.go index 552bccd44..b69a05684 100644 --- a/internal/service/revision_service.go +++ b/internal/service/content/revision_service.go @@ -17,7 +17,7 @@ * under the License. */ -package service +package content import ( "context" @@ -25,21 +25,28 @@ import ( "time" "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/activity" "github.com/apache/incubator-answer/internal/service/activity_queue" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/object_info" questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + "github.com/apache/incubator-answer/internal/service/report_common" + "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision" "github.com/apache/incubator-answer/internal/service/tag_common" tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/converter" + "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/pkg/obj" + "github.com/apache/incubator-answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -58,6 +65,9 @@ type RevisionService struct { tagCommon *tagcommon.TagCommonService notificationQueueService notice_queue.NotificationQueueService activityQueueService activity_queue.ActivityQueueService + reportRepo report_common.ReportRepo + reviewService *review.ReviewService + reviewActivity activity.ReviewActivityRepo } func NewRevisionService( @@ -72,6 +82,9 @@ func NewRevisionService( 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, @@ -85,6 +98,9 @@ func NewRevisionService( tagCommon: tagCommon, notificationQueueService: notificationQueueService, activityQueueService: activityQueueService, + reportRepo: reportRepo, + reviewService: reviewService, + reviewActivity: reviewActivity, } } @@ -136,6 +152,28 @@ func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.Revisi 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 } @@ -143,7 +181,7 @@ func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.Revisi } func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { - questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfo) + questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp) if ok { var PostUpdateTime time.Time dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID) @@ -333,6 +371,8 @@ func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *s _ = 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 @@ -380,13 +420,17 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi var ( err error question entity.QuestionWithTagsRevision - questionInfo *schema.QuestionInfo + 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) @@ -394,6 +438,9 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi 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) @@ -401,6 +448,10 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi 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) @@ -442,3 +493,49 @@ func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *sche } 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 +} diff --git a/internal/service/search_service.go b/internal/service/content/search_service.go similarity index 98% rename from internal/service/search_service.go rename to internal/service/content/search_service.go index 46d71374c..42d7c521b 100644 --- a/internal/service/search_service.go +++ b/internal/service/content/search_service.go @@ -17,10 +17,11 @@ * under the License. */ -package service +package content import ( "context" + "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/search_common" "github.com/apache/incubator-answer/internal/service/search_parser" @@ -93,6 +94,6 @@ func (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Searc res, resp.Total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) } - resp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res) + resp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res, cond.Words) return resp, err } diff --git a/internal/service/user_service.go b/internal/service/content/user_service.go similarity index 97% rename from internal/service/user_service.go rename to internal/service/content/user_service.go index c6d98fc2e..4c9843a39 100644 --- a/internal/service/user_service.go +++ b/internal/service/content/user_service.go @@ -17,15 +17,17 @@ * under the License. */ -package service +package content import ( "context" "encoding/json" "fmt" + "time" + "github.com/apache/incubator-answer/internal/base/constant" + questioncommon "github.com/apache/incubator-answer/internal/service/question_common" "github.com/apache/incubator-answer/internal/service/user_notification_config" - "time" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" @@ -62,6 +64,7 @@ type UserService struct { userExternalLoginService *user_external_login.UserExternalLoginService userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo userNotificationConfigService *user_notification_config.UserNotificationConfigService + questionService *questioncommon.QuestionCommon } func NewUserService(userRepo usercommon.UserRepo, @@ -75,6 +78,7 @@ func NewUserService(userRepo usercommon.UserRepo, userExternalLoginService *user_external_login.UserExternalLoginService, userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, userNotificationConfigService *user_notification_config.UserNotificationConfigService, + questionService *questioncommon.QuestionCommon, ) *UserService { return &UserService{ userCommonService: userCommonService, @@ -88,6 +92,7 @@ func NewUserService(userRepo usercommon.UserRepo, userExternalLoginService: userExternalLoginService, userNotificationConfigRepo: userNotificationConfigRepo, userNotificationConfigService: userNotificationConfigService, + questionService: questionService, } } @@ -117,9 +122,9 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st return resp, nil } -func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( +func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( resp *schema.GetOtherUserInfoByUsernameResp, err error) { - userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) + userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) if err != nil { return nil, err } @@ -129,6 +134,13 @@ func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username resp = &schema.GetOtherUserInfoByUsernameResp{} resp.ConvertFromUserEntity(userInfo) resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + + // Only the user himself and the administrator can see the hidden questions + questionCount, err := us.questionService.GetPersonalUserQuestionCount(ctx, req.UserID, userInfo.ID, req.IsAdmin) + if err != nil { + return nil, err + } + resp.QuestionCount = int(questionCount) return resp, nil } @@ -644,6 +656,12 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string if err != nil { return nil, err } + // if email status is to be verified, active user as well + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { + log.Error(err) + } + } roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { diff --git a/internal/service/vote_service.go b/internal/service/content/vote_service.go similarity index 99% rename from internal/service/vote_service.go rename to internal/service/content/vote_service.go index b5ce73b82..9785e30d6 100644 --- a/internal/service/vote_service.go +++ b/internal/service/content/vote_service.go @@ -17,13 +17,14 @@ * under the License. */ -package service +package content import ( "context" - "github.com/apache/incubator-answer/internal/service/activity_common" "strings" + "github.com/apache/incubator-answer/internal/service/activity_common" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/pager" diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index c9b6efe16..d4a528607 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -23,11 +23,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/apache/incubator-answer/pkg/converter" "io" "net/http" "net/url" "time" + + "github.com/apache/incubator-answer/internal/service/review" + "github.com/apache/incubator-answer/internal/service/revision" + "github.com/apache/incubator-answer/pkg/converter" "xorm.io/xorm/schemas" "github.com/apache/incubator-answer/internal/base/constant" @@ -57,6 +60,8 @@ type dashboardService struct { configService *config.ConfigService siteInfoService siteinfo_common.SiteInfoCommonService serviceConfig *service_config.ServiceConfig + reviewService *review.ReviewService + revisionRepo revision.RevisionRepo data *data.Data } @@ -70,6 +75,8 @@ func NewDashboardService( configService *config.ConfigService, siteInfoService siteinfo_common.SiteInfoCommonService, serviceConfig *service_config.ServiceConfig, + reviewService *review.ReviewService, + revisionRepo revision.RevisionRepo, data *data.Data, ) DashboardService { return &dashboardService{ @@ -82,6 +89,8 @@ func NewDashboardService( configService: configService, siteInfoService: siteInfoService, serviceConfig: serviceConfig, + reviewService: reviewService, + revisionRepo: revisionRepo, data: data, } } @@ -101,7 +110,14 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.ReportCount = ds.reportCount(ctx) dashboardInfo.VoteCount = ds.voteCount(ctx) dashboardInfo.OccupyingStorageSpace = ds.calculateStorage() - dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) + general, err := ds.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get general site info failed: %s", err) + return dashboardInfo, nil + } + if general.CheckUpdate { + dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) + } dashboardInfo.DatabaseVersion = ds.getDatabaseInfo() dashboardInfo.DatabaseSize = ds.GetDatabaseSize() } @@ -179,11 +195,23 @@ func (ds *dashboardService) userCount(ctx context.Context) int64 { } func (ds *dashboardService) reportCount(ctx context.Context) int64 { + reviewCount, err := ds.reviewService.GetReviewPendingCount(ctx) + if err != nil { + log.Errorf("get review count failed: %s", err) + } reportCount, err := ds.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %s", err) } - return reportCount + countUnreviewedRevision, err := ds.revisionRepo.CountUnreviewedRevision(ctx, []int{ + constant.ObjectTypeStrMapping[constant.AnswerObjectType], + constant.ObjectTypeStrMapping[constant.QuestionObjectType], + constant.ObjectTypeStrMapping[constant.TagObjectType], + }) + if err != nil { + log.Errorf("get revision count failed: %s", err) + } + return reviewCount + reportCount + countUnreviewedRevision } // count vote diff --git a/internal/service/notification/external_notification.go b/internal/service/notification/external_notification.go index 58228884c..1db83f0b6 100644 --- a/internal/service/notification/external_notification.go +++ b/internal/service/notification/external_notification.go @@ -21,7 +21,9 @@ package notification import ( "context" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/activity_common" "github.com/apache/incubator-answer/internal/service/export" @@ -71,6 +73,12 @@ func NewExternalNotificationService( func (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.ExternalNotificationMsg) error { log.Debugf("try to send external notification %+v", msg) + // If receiver not set language, use site default language. + if len(msg.ReceiverLang) == 0 || msg.ReceiverLang == translator.DefaultLangOption { + if interfaceInfo, _ := ns.siteInfoService.GetSiteInterface(ctx); interfaceInfo != nil { + msg.ReceiverLang = interfaceInfo.Language + } + } if msg.NewQuestionTemplateRawData != nil { return ns.handleNewQuestionNotification(ctx, msg) } diff --git a/internal/service/notification/new_question_notification.go b/internal/service/notification/new_question_notification.go index c079ac282..d41733974 100644 --- a/internal/service/notification/new_question_notification.go +++ b/internal/service/notification/new_question_notification.go @@ -25,6 +25,7 @@ import ( "time" "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/pkg/display" "github.com/apache/incubator-answer/pkg/token" @@ -229,7 +230,7 @@ func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx c if len(subscriberUserID) > 0 { userInfo, _, _ := ns.userRepo.GetByUserID(ctx, subscriberUserID) - if userInfo != nil { + if userInfo != nil && len(userInfo.Language) > 0 && userInfo.Language != translator.DefaultLangOption { newMsg.ReceiverLang = userInfo.Language } } @@ -264,6 +265,13 @@ func (ns *ExternalNotificationService) newPluginQuestionNotification( if err != nil { return raw } + interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return raw + } + if len(raw.ReceiverLang) == 0 || raw.ReceiverLang == translator.DefaultLangOption { + raw.ReceiverLang = interfaceInfo.Language + } raw.QuestionUrl = display.QuestionURL( seoInfo.Permalink, siteInfo.SiteUrl, msg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle) diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index bda225d1a..71febb677 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -23,6 +23,9 @@ import ( "context" "encoding/json" "fmt" + + "github.com/apache/incubator-answer/internal/service/report_common" + "github.com/apache/incubator-answer/internal/service/review" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/converter" @@ -46,6 +49,8 @@ type NotificationService struct { notificationRepo notficationcommon.NotificationRepo notificationCommon *notficationcommon.NotificationCommon revisionService *revision_common.RevisionService + reportRepo report_common.ReportRepo + reviewService *review.ReviewService userRepo usercommon.UserRepo } @@ -55,6 +60,8 @@ func NewNotificationService( notificationCommon *notficationcommon.NotificationCommon, revisionService *revision_common.RevisionService, userRepo usercommon.UserRepo, + reportRepo report_common.ReportRepo, + reviewService *review.ReviewService, ) *NotificationService { return &NotificationService{ data: data, @@ -62,10 +69,12 @@ func NewNotificationService( notificationCommon: notificationCommon, revisionService: revisionService, userRepo: userRepo, + reportRepo: reportRepo, + reviewService: reviewService, } } -func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (*schema.RedDot, error) { +func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { redBot := &schema.RedDot{} inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID) achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID) @@ -85,14 +94,46 @@ func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRed _ = copier.Copy(revisionCount, req) if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { redBot.CanRevision = true - revisionCountNum, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, revisionCount) + redBot.Revision = ns.countAllReviewAmount(ctx, req) + } + + return redBot, nil +} + +func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { + // get queue amount + if req.IsAdmin { + reviewCount, err := ns.reviewService.GetReviewPendingCount(ctx) if err != nil { - return redBot, err + log.Errorf("get report count failed: %v", err) + } else { + amount += reviewCount } - redBot.Revision = revisionCountNum } - return redBot, nil + // get flag amount + if req.IsAdmin { + reportCount, err := ns.reportRepo.GetReportCount(ctx) + if err != nil { + log.Errorf("get report count failed: %v", err) + } else { + amount += reportCount + } + } + + // get suggestion amount + countUnreviewedRevision, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, &schema.RevisionSearch{ + CanReviewQuestion: req.CanReviewQuestion, + CanReviewAnswer: req.CanReviewAnswer, + CanReviewTag: req.CanReviewTag, + UserID: req.UserID, + }) + if err != nil { + log.Errorf("get unreviewed revision count failed: %v", err) + } else { + amount += countUnreviewedRevision + } + return amount } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 2338023ce..319403b24 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -24,6 +24,7 @@ import ( "fmt" "time" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/service/siteinfo_common" "github.com/apache/incubator-answer/internal/service/user_external_login" "github.com/apache/incubator-answer/pkg/display" @@ -253,6 +254,11 @@ func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objI log.Errorf("get site seo info failed: %v", err) return } + interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) + if err != nil { + log.Errorf("get site interface info failed: %v", err) + return + } objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) @@ -294,6 +300,10 @@ func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objI if userInfo != nil { pluginNotificationMsg.ReceiverLang = userInfo.Language } + // If receiver not set language, use site default language. + if len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption { + pluginNotificationMsg.ReceiverLang = interfaceInfo.Language + } } _ = plugin.CallNotification(func(fn plugin.Notification) error { diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index 9ef5a8926..0e1dbdc26 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -23,15 +23,14 @@ import ( "context" "github.com/apache/incubator-answer/internal/base/constant" - "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/schema" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/comment_common" questioncommon "github.com/apache/incubator-answer/internal/service/question_common" tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" + "github.com/apache/incubator-answer/pkg/checker" "github.com/apache/incubator-answer/pkg/obj" - "github.com/apache/incubator-answer/pkg/uid" "github.com/segmentfault/pacman/errors" ) @@ -71,9 +70,6 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if err != nil { return nil, err } - if handler.GetEnableShortID(ctx) { - questionInfo.ID = uid.EnShortID(questionInfo.ID) - } if !exist { break } @@ -87,11 +83,19 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st return nil, err } objInfo = &schema.UnreviewedRevisionInfoInfo{ - ObjectID: questionInfo.ID, - Title: questionInfo.Title, - Content: questionInfo.OriginalText, - Html: questionInfo.ParsedText, - Tags: tags, + CreatedAt: questionInfo.CreatedAt.Unix(), + ObjectID: questionInfo.ID, + QuestionID: questionInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: questionInfo.UserID, + Title: questionInfo.Title, + Content: questionInfo.OriginalText, + Html: questionInfo.ParsedText, + AnswerCount: questionInfo.AnswerCount, + AnswerAccepted: !checker.IsNotZeroString(questionInfo.AcceptedAnswerID), + Tags: tags, + Status: questionInfo.Status, + ShowStatus: questionInfo.Show, } case constant.AnswerObjectType: answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) @@ -109,16 +113,19 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if !exist { break } - if handler.GetEnableShortID(ctx) { - questionInfo.ID = uid.EnShortID(questionInfo.ID) - } objInfo = &schema.UnreviewedRevisionInfoInfo{ - ObjectID: answerInfo.ID, - Title: questionInfo.Title, - Content: answerInfo.OriginalText, - Html: answerInfo.ParsedText, + CreatedAt: answerInfo.CreatedAt.Unix(), + ObjectID: answerInfo.ID, + QuestionID: answerInfo.QuestionID, + AnswerID: answerInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: answerInfo.UserID, + Title: questionInfo.Title, + Content: answerInfo.OriginalText, + Html: answerInfo.ParsedText, + Status: answerInfo.Status, + AnswerAccepted: questionInfo.AcceptedAnswerID == answerInfo.ID, } - case constant.TagObjectType: tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true) if err != nil { @@ -128,10 +135,47 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st break } objInfo = &schema.UnreviewedRevisionInfoInfo{ - ObjectID: tagInfo.ID, - Title: tagInfo.SlugName, - Content: tagInfo.OriginalText, - Html: tagInfo.ParsedText, + CreatedAt: tagInfo.CreatedAt.Unix(), + ObjectID: tagInfo.ID, + ObjectType: objectType, + Title: tagInfo.SlugName, + Content: tagInfo.OriginalText, + Html: tagInfo.ParsedText, + Status: tagInfo.Status, + } + case constant.CommentObjectType: + commentInfo, exist, err := os.commentRepo.GetCommentWithoutStatus(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + CreatedAt: commentInfo.CreatedAt.Unix(), + ObjectID: commentInfo.ID, + CommentID: commentInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: commentInfo.UserID, + Content: commentInfo.OriginalText, + Html: commentInfo.ParsedText, + Status: commentInfo.Status, + } + if len(commentInfo.QuestionID) > 0 { + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) + if err != nil { + return nil, err + } + if exist { + objInfo.QuestionID = questionInfo.ID + } + answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID) + if err != nil { + return nil, err + } + if exist { + objInfo.AnswerID = answerInfo.ID + } } } if objInfo == nil { diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go index 1df244e39..b93cdfc1d 100644 --- a/internal/service/permission/question_permission.go +++ b/internal/service/permission/question_permission.go @@ -92,7 +92,8 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str Type: "confirm", }) } - if canDelete || userID == creatorUserID { + + if (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), diff --git a/internal/service/provider.go b/internal/service/provider.go index 38b9cd72a..2bdd42b6a 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -26,10 +26,12 @@ import ( "github.com/apache/incubator-answer/internal/service/activity_queue" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/auth" + "github.com/apache/incubator-answer/internal/service/collection" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" "github.com/apache/incubator-answer/internal/service/comment" "github.com/apache/incubator-answer/internal/service/comment_common" "github.com/apache/incubator-answer/internal/service/config" + "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/dashboard" "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/follow" @@ -43,8 +45,8 @@ import ( "github.com/apache/incubator-answer/internal/service/rank" "github.com/apache/incubator-answer/internal/service/reason" "github.com/apache/incubator-answer/internal/service/report" - "github.com/apache/incubator-answer/internal/service/report_admin" - "github.com/apache/incubator-answer/internal/service/report_handle_admin" + "github.com/apache/incubator-answer/internal/service/report_handle" + "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision_common" "github.com/apache/incubator-answer/internal/service/role" "github.com/apache/incubator-answer/internal/service/search_parser" @@ -65,16 +67,16 @@ var ProviderSetService = wire.NewSet( comment.NewCommentService, comment_common.NewCommentCommonService, report.NewReportService, - NewVoteService, + content.NewVoteService, tag.NewTagService, follow.NewFollowService, - NewCollectionGroupService, - NewCollectionService, + collection.NewCollectionGroupService, + collection.NewCollectionService, action.NewCaptchaService, auth.NewAuthService, - NewUserService, - NewQuestionService, - NewAnswerService, + content.NewUserService, + content.NewQuestionService, + content.NewAnswerService, export.NewEmailService, tagcommon.NewTagCommonService, usercommon.NewUserCommon, @@ -83,14 +85,13 @@ var ProviderSetService = wire.NewSet( uploader.NewUploaderService, collectioncommon.NewCollectionCommon, revision_common.NewRevisionService, - NewRevisionService, + content.NewRevisionService, rank.NewRankService, search_parser.NewSearchParser, - NewSearchService, + content.NewSearchService, meta.NewMetaService, object_info.NewObjService, - report_handle_admin.NewReportHandle, - report_admin.NewReportAdminService, + report_handle.NewReportHandle, user_admin.NewUserAdminService, reason.NewReasonService, siteinfo_common.NewSiteInfoCommonService, @@ -113,4 +114,5 @@ var ProviderSetService = wire.NewSet( user_notification_config.NewUserNotificationConfigService, notification.NewExternalNotificationService, notice_queue.NewNewQuestionNotificationQueueService, + review.NewReviewService, ) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 9fbda693e..ff0b9da5b 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -33,6 +33,7 @@ import ( "github.com/apache/incubator-answer/internal/service/activity_queue" "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/meta" + "github.com/apache/incubator-answer/internal/service/revision" "github.com/apache/incubator-answer/pkg/checker" "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/pkg/uid" @@ -54,7 +55,7 @@ type QuestionRepo interface { UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error) GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error) - GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden bool) ( + GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) @@ -69,7 +70,7 @@ type QuestionRepo interface { FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) GetQuestionCount(ctx context.Context) (count int64, err error) - GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) + GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) UpdateSearch(ctx context.Context, questionID string) (err error) @@ -88,6 +89,7 @@ type QuestionCommon struct { metaService *meta.MetaService configService *config.ConfigService activityQueueService activity_queue.ActivityQueueService + revisionRepo revision.RevisionRepo data *data.Data } @@ -102,6 +104,7 @@ func NewQuestionCommon(questionRepo QuestionRepo, metaService *meta.MetaService, configService *config.ConfigService, activityQueueService activity_queue.ActivityQueueService, + revisionRepo revision.RevisionRepo, data *data.Data, ) *QuestionCommon { return &QuestionCommon{ @@ -116,12 +119,21 @@ func NewQuestionCommon(questionRepo QuestionRepo, metaService: metaService, configService: configService, activityQueueService: activityQueueService, + revisionRepo: revisionRepo, data: data, } } func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) { - return qs.questionRepo.GetUserQuestionCount(ctx, userID) + return qs.questionRepo.GetUserQuestionCount(ctx, userID, 0) +} + +func (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) { + show := entity.QuestionShow + if loginUserID == userID || isAdmin { + show = 0 + } + return qs.questionRepo.GetUserQuestionCount(ctx, userID, show) } func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error { @@ -168,8 +180,8 @@ func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID stri return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } -func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfo, error) { - list := make(map[string]*schema.QuestionInfo) +func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) { + list := make(map[string]*schema.QuestionInfoResp) questionList, err := qs.questionRepo.FindByID(ctx, questionIDs) if err != nil { return list, err @@ -212,30 +224,27 @@ func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) return InviteUserInfo, nil } -func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) { - dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) +func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { - return showinfo, err + return resp, err } - dbinfo.ID = uid.DeShortID(dbinfo.ID) + questionInfo.ID = uid.DeShortID(questionInfo.ID) if !has { - return showinfo, errors.NotFound(reason.QuestionNotFound) + return resp, errors.NotFound(reason.QuestionNotFound) } - showinfo = qs.ShowFormat(ctx, dbinfo) - - if showinfo.Status == 2 { - var metainfo *entity.Meta - metainfo, err = qs.metaService.GetMetaByObjectIdAndKey(ctx, dbinfo.ID, entity.QuestionCloseReasonKey) + resp = qs.ShowFormat(ctx, questionInfo) + if resp.Status == entity.QuestionStatusClosed { + metaInfo, err := qs.metaService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey) if err != nil { log.Error(err) } else { - // metainfo.Value - closemsg := &schema.CloseQuestionMeta{} - err = json.Unmarshal([]byte(metainfo.Value), closemsg) + closeMsg := &schema.CloseQuestionMeta{} + err = json.Unmarshal([]byte(metaInfo.Value), closeMsg) if err != nil { log.Error("json.Unmarshal CloseQuestionMeta error", err.Error()) } else { - cfg, err := qs.configService.GetConfigByID(ctx, closemsg.CloseType) + cfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType) if err != nil { log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { @@ -245,76 +254,84 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser operation := &schema.Operation{} operation.Type = reasonItem.Name operation.Description = reasonItem.Description - operation.Msg = closemsg.CloseMsg - operation.Time = metainfo.CreatedAt.Unix() + operation.Msg = closeMsg.CloseMsg + operation.Time = metaInfo.CreatedAt.Unix() operation.Level = schema.OperationLevelInfo - showinfo.Operation = operation + resp.Operation = operation } } } } - tagmap, err := qs.tagCommon.GetObjectTag(ctx, questionID) - if err != nil { - return showinfo, err + if resp.Status != entity.QuestionStatusDeleted { + if resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil { + return resp, err + } + } else { + revisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID) + if err != nil { + log.Errorf("get revision error %s", err) + } + if exist { + questionWithTagsRevision := &entity.QuestionWithTagsRevision{} + if err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + for _, tag := range questionWithTagsRevision.Tags { + resp.Tags = append(resp.Tags, &schema.TagResp{ + ID: tag.ID, + SlugName: tag.SlugName, + DisplayName: tag.DisplayName, + MainTagSlugName: tag.MainTagSlugName, + Recommend: tag.Recommend, + Reserved: tag.Reserved, + }) + } + } } - showinfo.Tags = tagmap userIds := make([]string, 0) - if checker.IsNotZeroString(dbinfo.UserID) { - userIds = append(userIds, dbinfo.UserID) + if checker.IsNotZeroString(questionInfo.UserID) { + userIds = append(userIds, questionInfo.UserID) } - if checker.IsNotZeroString(dbinfo.LastEditUserID) { - userIds = append(userIds, dbinfo.LastEditUserID) + if checker.IsNotZeroString(questionInfo.LastEditUserID) { + userIds = append(userIds, questionInfo.LastEditUserID) } - if checker.IsNotZeroString(showinfo.LastAnsweredUserID) { - userIds = append(userIds, showinfo.LastAnsweredUserID) + if checker.IsNotZeroString(resp.LastAnsweredUserID) { + userIds = append(userIds, resp.LastAnsweredUserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { - return showinfo, err - } - - _, ok := userInfoMap[dbinfo.UserID] - if ok { - showinfo.UserInfo = userInfoMap[dbinfo.UserID] + return resp, err } - _, ok = userInfoMap[dbinfo.LastEditUserID] - if ok { - showinfo.UpdateUserInfo = userInfoMap[dbinfo.LastEditUserID] + resp.UserInfo = userInfoMap[questionInfo.UserID] + resp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID] + resp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID] + if len(loginUserID) == 0 { + return resp, nil } - _, ok = userInfoMap[showinfo.LastAnsweredUserID] - if ok { - showinfo.LastAnsweredUserInfo = userInfoMap[showinfo.LastAnsweredUserID] - } - - if loginUserID == "" { - return showinfo, nil - } - - showinfo.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID) - // // check is followed - isFollowed, _ := qs.followCommon.IsFollowed(ctx, loginUserID, questionID) - showinfo.IsFollowed = isFollowed + resp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID) + resp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID) - ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, dbinfo.ID) + ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID) if err != nil { log.Error("AnswerFunc.SearchAnswerIDs", err) } - showinfo.Answered = len(ids) > 0 - if showinfo.Answered { - showinfo.FirstAnswerId = ids[0] + resp.Answered = len(ids) > 0 + if resp.Answered { + resp.FirstAnswerId = ids[0] } - collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{dbinfo.ID}) + collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID}) if err != nil { return nil, err } if len(collectedMap) > 0 { - showinfo.Collected = true + resp.Collected = true } - return showinfo, nil + return resp, nil } func (qs *QuestionCommon) FormatQuestionsPage( @@ -419,8 +436,8 @@ func (qs *QuestionCommon) FormatQuestionsPage( return formattedQuestions, nil } -func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfo, error) { - list := make([]*schema.QuestionInfo, 0) +func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) { + list := make([]*schema.QuestionInfoResp, 0) objectIds := make([]string, 0) userIds := make([]string, 0) @@ -526,8 +543,8 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu } // RemoveAnswer delete answer -func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) { - answerinfo, has, err := as.answerRepo.GetByID(ctx, id) +func (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) { + answerinfo, has, err := qs.answerRepo.GetByID(ctx, id) if err != nil { return err } @@ -537,20 +554,20 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro // user add question count - err = as.UpdateAnswerCount(ctx, answerinfo.QuestionID) + err = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID) if err != nil { log.Error("UpdateAnswerCount error", err.Error()) } - userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerinfo.UserID) + userAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID) if err != nil { log.Error("GetCountByUserID error", err.Error()) } - err = as.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount)) + err = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount)) if err != nil { log.Error("user UpdateAnswerCount error", err.Error()) } - return as.answerRepo.RemoveAnswer(ctx, id) + return qs.answerRepo.RemoveAnswer(ctx, id) } func (qs *QuestionCommon) SitemapCron(ctx context.Context) { @@ -590,12 +607,12 @@ func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info in return nil } -func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo { +func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { return qs.ShowFormat(ctx, data) } -func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo { - info := schema.QuestionInfo{} +func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { + info := schema.QuestionInfoResp{} info.ID = data.ID if handler.GetEnableShortID(ctx) { info.ID = uid.EnShortID(data.ID) @@ -641,7 +658,7 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) info.Tags = make([]*schema.TagResp, 0) return &info } -func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfo { +func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp { info := qs.ShowFormat(ctx, &data.Question) Tags := make([]*schema.TagResp, 0) for _, tag := range data.Tags { diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index 55ac42724..f25fb3514 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -20,11 +20,28 @@ package report import ( + "encoding/json" + + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" + answercommon "github.com/apache/incubator-answer/internal/service/answer_common" + "github.com/apache/incubator-answer/internal/service/comment_common" + "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/object_info" + questioncommon "github.com/apache/incubator-answer/internal/service/question_common" "github.com/apache/incubator-answer/internal/service/report_common" + "github.com/apache/incubator-answer/internal/service/report_handle" + usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/checker" + "github.com/apache/incubator-answer/pkg/htmltext" "github.com/apache/incubator-answer/pkg/obj" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) @@ -32,15 +49,34 @@ import ( type ReportService struct { reportRepo report_common.ReportRepo objectInfoService *object_info.ObjService + commonUser *usercommon.UserCommon + answerRepo answercommon.AnswerRepo + questionRepo questioncommon.QuestionRepo + commentCommonRepo comment_common.CommentCommonRepo + reportHandle *report_handle.ReportHandle + configService *config.ConfigService } // NewReportService new report service -func NewReportService(reportRepo report_common.ReportRepo, +func NewReportService( + reportRepo report_common.ReportRepo, objectInfoService *object_info.ObjService, + commonUser *usercommon.UserCommon, + answerRepo answercommon.AnswerRepo, + questionRepo questioncommon.QuestionRepo, + commentCommonRepo comment_common.CommentCommonRepo, + reportHandle *report_handle.ReportHandle, + configService *config.ConfigService, ) *ReportService { return &ReportService{ reportRepo: reportRepo, objectInfoService: objectInfoService, + commonUser: commonUser, + answerRepo: answerRepo, + questionRepo: questionRepo, + commentCommonRepo: commentCommonRepo, + reportHandle: reportHandle, + configService: configService, } } @@ -51,12 +87,19 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq return err } - // TODO this reported user id should be get by revision objInfo, err := rs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return err } + cf, err := rs.configService.GetConfigByID(ctx, req.ReportType) + if err != nil || cf == nil { + return errors.BadRequest(reason.ReportNotFound) + } + if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.Content) { + return errors.BadRequest(reason.InvalidURLError) + } + report := &entity.Report{ UserID: req.UserID, ReportedUserID: objInfo.ObjectCreatorUserID, @@ -68,3 +111,107 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq } return rs.reportRepo.AddReport(ctx, report) } + +// GetUnreviewedReportPostPage get unreviewed report post page +func (rs *ReportService) GetUnreviewedReportPostPage(ctx context.Context, req *schema.GetUnreviewedReportPostPageReq) ( + pageModel *pager.PageModel, err error) { + if !req.IsAdmin { + return pager.NewPageModel(0, make([]*schema.GetReportListPageResp, 0)), nil + } + lang := handler.GetLangByCtx(ctx) + reports, total, err := rs.reportRepo.GetReportListPage(ctx, &schema.GetReportListPageDTO{ + Page: req.Page, + PageSize: 1, + Status: entity.ReportStatusPending, + }) + if err != nil { + return + } + + resp := make([]*schema.GetReportListPageResp, 0) + for _, report := range reports { + info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, report.ObjectID) + if err != nil { + log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) + continue + } + + r := &schema.GetReportListPageResp{ + FlagID: report.ID, + CreatedAt: info.CreatedAt, + ObjectID: info.ObjectID, + ObjectType: info.ObjectType, + QuestionID: info.QuestionID, + AnswerID: info.AnswerID, + CommentID: info.CommentID, + Title: info.Title, + UrlTitle: htmltext.UrlTitle(info.Title), + OriginalText: info.Content, + ParsedText: info.Html, + AnswerCount: info.AnswerCount, + AnswerAccepted: info.AnswerAccepted, + Tags: info.Tags, + SubmitAt: report.CreatedAt.Unix(), + ObjectStatus: info.Status, + ObjectShowStatus: info.ShowStatus, + ReasonContent: report.Content, + } + + // get user info + userInfo, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.AuthorUserInfo, userInfo) + } + + // get submitter info + submitter, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, report.ReportedUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.SubmitterUser, submitter) + } + + if report.ReportType > 0 { + r.Reason = &schema.ReasonItem{ReasonType: report.ReportType} + cf, err := rs.configService.GetConfigByID(ctx, report.ReportType) + if err != nil { + log.Error(err) + } else { + _ = json.Unmarshal([]byte(cf.Value), r.Reason) + r.Reason.Translate(cf.Key, lang) + } + } + resp = append(resp, r) + } + return pager.NewPageModel(total, resp), nil +} + +// ReviewReport review report +func (rs *ReportService) ReviewReport(ctx context.Context, req *schema.ReviewReportReq) (err error) { + report, exist, err := rs.reportRepo.GetByID(ctx, req.FlagID) + if err != nil { + return err + } + if !exist { + return errors.NotFound(reason.ReportNotFound) + } + // check if handle or not + if report.Status != entity.ReportStatusPending { + return nil + } + + // ignore this report + if req.OperationType == constant.ReportOperationIgnoreReport { + return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusIgnore) + } + + if err = rs.reportHandle.UpdateReportedObject(ctx, report, req); err != nil { + return + } + + return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusCompleted) +} diff --git a/internal/service/report_admin/report_backyard.go b/internal/service/report_admin/report_backyard.go deleted file mode 100644 index 3b1417968..000000000 --- a/internal/service/report_admin/report_backyard.go +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 report_admin - -import ( - "context" - "encoding/json" - - "github.com/apache/incubator-answer/internal/base/handler" - "github.com/apache/incubator-answer/internal/service/config" - "github.com/apache/incubator-answer/internal/service/object_info" - "github.com/apache/incubator-answer/pkg/htmltext" - "github.com/segmentfault/pacman/log" - - "github.com/apache/incubator-answer/internal/base/pager" - "github.com/apache/incubator-answer/internal/base/reason" - "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/schema" - answercommon "github.com/apache/incubator-answer/internal/service/answer_common" - "github.com/apache/incubator-answer/internal/service/comment_common" - questioncommon "github.com/apache/incubator-answer/internal/service/question_common" - "github.com/apache/incubator-answer/internal/service/report_common" - "github.com/apache/incubator-answer/internal/service/report_handle_admin" - usercommon "github.com/apache/incubator-answer/internal/service/user_common" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" -) - -// ReportAdminService user service -type ReportAdminService struct { - reportRepo report_common.ReportRepo - commonUser *usercommon.UserCommon - answerRepo answercommon.AnswerRepo - questionRepo questioncommon.QuestionRepo - commentCommonRepo comment_common.CommentCommonRepo - reportHandle *report_handle_admin.ReportHandle - configService *config.ConfigService - objectInfoService *object_info.ObjService -} - -// NewReportAdminService new report service -func NewReportAdminService( - reportRepo report_common.ReportRepo, - commonUser *usercommon.UserCommon, - answerRepo answercommon.AnswerRepo, - questionRepo questioncommon.QuestionRepo, - commentCommonRepo comment_common.CommentCommonRepo, - reportHandle *report_handle_admin.ReportHandle, - configService *config.ConfigService, - objectInfoService *object_info.ObjService) *ReportAdminService { - return &ReportAdminService{ - reportRepo: reportRepo, - commonUser: commonUser, - answerRepo: answerRepo, - questionRepo: questionRepo, - commentCommonRepo: commentCommonRepo, - reportHandle: reportHandle, - configService: configService, - objectInfoService: objectInfoService, - } -} - -// ListReportPage list report pages -func (rs *ReportAdminService) ListReportPage(ctx context.Context, dto schema.GetReportListPageDTO) (pageModel *pager.PageModel, err error) { - var ( - resp []*schema.GetReportListPageResp - flags []entity.Report - total int64 - - flaggedUserIds, - userIds []string - - flaggedUsers, - users map[string]*schema.UserBasicInfo - ) - - flags, total, err = rs.reportRepo.GetReportListPage(ctx, dto) - if err != nil { - return - } - - _ = copier.Copy(&resp, flags) - for _, r := range resp { - flaggedUserIds = append(flaggedUserIds, r.ReportedUserID) - userIds = append(userIds, r.UserID) - r.Format() - } - - // flagged users - flaggedUsers, err = rs.commonUser.BatchUserBasicInfoByID(ctx, flaggedUserIds) - if err != nil { - return nil, err - } - - // flag users - users, err = rs.commonUser.BatchUserBasicInfoByID(ctx, userIds) - if err != nil { - return nil, err - } - for _, r := range resp { - r.ReportedUser = flaggedUsers[r.ReportedUserID] - r.ReportUser = users[r.UserID] - rs.decorateReportResp(ctx, r) - } - return pager.NewPageModel(total, resp), nil -} - -// HandleReported handle the reported object -func (rs *ReportAdminService) HandleReported(ctx context.Context, req schema.ReportHandleReq) (err error) { - var ( - reported *entity.Report - handleData = entity.Report{ - FlaggedContent: req.FlaggedContent, - FlaggedType: req.FlaggedType, - Status: entity.ReportStatusCompleted, - } - exist bool - ) - - reported, exist, err = rs.reportRepo.GetByID(ctx, req.ID) - if err != nil { - err = errors.BadRequest(reason.ReportHandleFailed).WithError(err).WithStack() - return - } - if !exist { - err = errors.NotFound(reason.ReportNotFound) - return - } - - // check if handle or not - if reported.Status != entity.ReportStatusPending { - return - } - - if err = rs.reportHandle.HandleObject(ctx, reported, req); err != nil { - return - } - - err = rs.reportRepo.UpdateByID(ctx, reported.ID, handleData) - return -} - -func (rs *ReportAdminService) decorateReportResp(ctx context.Context, resp *schema.GetReportListPageResp) { - lang := handler.GetLangByCtx(ctx) - objectInfo, err := rs.objectInfoService.GetInfo(ctx, resp.ObjectID) - if err != nil { - log.Error(err) - return - } - - resp.QuestionID = objectInfo.QuestionID - resp.AnswerID = objectInfo.AnswerID - resp.CommentID = objectInfo.CommentID - resp.Title = objectInfo.Title - resp.Excerpt = htmltext.FetchExcerpt(objectInfo.Content, "...", 240) - - if resp.ReportType > 0 { - resp.Reason = &schema.ReasonItem{ReasonType: resp.ReportType} - cf, err := rs.configService.GetConfigByID(ctx, resp.ReportType) - if err != nil { - log.Error(err) - } else { - _ = json.Unmarshal([]byte(cf.Value), resp.Reason) - resp.Reason.Translate(cf.Key, lang) - } - } - if resp.FlaggedType > 0 { - resp.FlaggedReason = &schema.ReasonItem{ReasonType: resp.FlaggedType} - cf, err := rs.configService.GetConfigByID(ctx, resp.FlaggedType) - if err != nil { - log.Error(err) - } else { - _ = json.Unmarshal([]byte(cf.Value), resp.Reason) - resp.Reason.Translate(cf.Key, lang) - } - } -} diff --git a/internal/service/report_common/report_common.go b/internal/service/report_common/report_common.go index e27ff1626..4b504630f 100644 --- a/internal/service/report_common/report_common.go +++ b/internal/service/report_common/report_common.go @@ -29,8 +29,9 @@ import ( // ReportRepo report repository type ReportRepo interface { AddReport(ctx context.Context, report *entity.Report) (err error) - GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) + GetReportListPage(ctx context.Context, query *schema.GetReportListPageDTO) ( + reports []*entity.Report, total int64, err error) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) - UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) + UpdateStatus(ctx context.Context, id string, status int) (err error) GetReportCount(ctx context.Context) (count int64, err error) } diff --git a/internal/service/report_handle/report_handle.go b/internal/service/report_handle/report_handle.go new file mode 100644 index 000000000..df6cff68b --- /dev/null +++ b/internal/service/report_handle/report_handle.go @@ -0,0 +1,132 @@ +/* + * 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 report_handle + +import ( + "context" + + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/comment" + "github.com/apache/incubator-answer/internal/service/content" + "github.com/apache/incubator-answer/pkg/converter" + "github.com/apache/incubator-answer/pkg/obj" +) + +type ReportHandle struct { + questionService *content.QuestionService + answerService *content.AnswerService + commentService *comment.CommentService +} + +func NewReportHandle( + questionService *content.QuestionService, + answerService *content.AnswerService, + commentService *comment.CommentService, +) *ReportHandle { + return &ReportHandle{ + questionService: questionService, + answerService: answerService, + commentService: commentService, + } +} + +// UpdateReportedObject this handle object status +func (rh *ReportHandle) UpdateReportedObject(ctx context.Context, + report *entity.Report, req *schema.ReviewReportReq) (err error) { + objectKey, err := obj.GetObjectTypeStrByObjectID(report.ObjectID) + if err != nil { + return err + } + switch objectKey { + case constant.QuestionObjectType: + err = rh.updateReportedQuestionReport(ctx, report, req) + case constant.AnswerObjectType: + err = rh.updateReportedAnswerReport(ctx, report, req) + case constant.CommentObjectType: + err = rh.updateReportedCommentReport(ctx, report, req) + } + return +} + +func (rh *ReportHandle) updateReportedQuestionReport(ctx context.Context, + report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationUnlistPost: + err = rh.questionService.OperationQuestion(ctx, &schema.OperationQuestionReq{ + ID: report.ObjectID, Operation: schema.QuestionOperationHide, UserID: req.UserID}) + case constant.ReportOperationDeletePost: + err = rh.questionService.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ + ID: report.ObjectID, UserID: req.UserID, IsAdmin: true}) + case constant.ReportOperationClosePost: + err = rh.questionService.CloseQuestion(ctx, &schema.CloseQuestionReq{ + ID: report.ObjectID, + CloseType: req.CloseType, + CloseMsg: req.CloseMsg, + UserID: req.UserID, + }) + case constant.ReportOperationEditPost: + _, err = rh.questionService.UpdateQuestion(ctx, &schema.QuestionUpdate{ + ID: report.ObjectID, + Title: req.Title, + Content: req.Content, + HTML: converter.Markdown2HTML(req.Content), + Tags: req.Tags, + UserID: req.UserID, + NoNeedReview: true, + }) + } + return +} + +func (rh *ReportHandle) updateReportedAnswerReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationDeletePost: + err = rh.answerService.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ + ID: report.ObjectID, UserID: req.UserID}) + case constant.ReportOperationEditPost: + _, err = rh.answerService.Update(ctx, &schema.AnswerUpdateReq{ + ID: report.ObjectID, + Title: req.Title, + Content: req.Content, + HTML: converter.Markdown2HTML(req.Content), + UserID: req.UserID, + NoNeedReview: true, + }) + } + return nil +} + +func (rh *ReportHandle) updateReportedCommentReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationDeletePost: + err = rh.commentService.RemoveComment(ctx, &schema.RemoveCommentReq{ + CommentID: report.ObjectID, UserID: req.UserID}) + case constant.ReportOperationEditPost: + _, err = rh.commentService.UpdateComment(ctx, &schema.UpdateCommentReq{ + CommentID: report.ObjectID, + OriginalText: req.Content, + ParsedText: converter.Markdown2HTML(req.Content), + UserID: req.UserID, + }) + } + return nil +} diff --git a/internal/service/report_handle_admin/report_handle.go b/internal/service/report_handle_admin/report_handle.go deleted file mode 100644 index 371703969..000000000 --- a/internal/service/report_handle_admin/report_handle.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 report_handle_admin - -import ( - "context" - - "github.com/apache/incubator-answer/internal/service/config" - "github.com/apache/incubator-answer/internal/service/notice_queue" - - "github.com/apache/incubator-answer/internal/base/constant" - "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service/comment" - questioncommon "github.com/apache/incubator-answer/internal/service/question_common" - "github.com/apache/incubator-answer/pkg/obj" -) - -type ReportHandle struct { - questionCommon *questioncommon.QuestionCommon - commentRepo comment.CommentRepo - configService *config.ConfigService - notificationQueueService notice_queue.NotificationQueueService -} - -func NewReportHandle( - questionCommon *questioncommon.QuestionCommon, - commentRepo comment.CommentRepo, - configService *config.ConfigService, - notificationQueueService notice_queue.NotificationQueueService, -) *ReportHandle { - return &ReportHandle{ - questionCommon: questionCommon, - commentRepo: commentRepo, - configService: configService, - notificationQueueService: notificationQueueService, - } -} - -// HandleObject this handle object status -func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Report, req schema.ReportHandleReq) (err error) { - reasonDeleteCfg, err := rh.configService.GetConfigByKey(ctx, "reason.needs_delete") - if err != nil { - return err - } - reasonCloseCfg, err := rh.configService.GetConfigByKey(ctx, "reason.needs_close") - if err != nil { - return err - } - var ( - objectID = reported.ObjectID - reportedUserID = reported.ReportedUserID - objectKey string - ) - - objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - return err - } - switch objectKey { - case "question": - switch req.FlaggedType { - case reasonDeleteCfg.ID: - err = rh.questionCommon.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ID: objectID}) - case reasonCloseCfg.ID: - err = rh.questionCommon.CloseQuestion(ctx, &schema.CloseQuestionReq{ - ID: objectID, - CloseType: req.FlaggedType, - CloseMsg: req.FlaggedContent, - }) - } - case "answer": - switch req.FlaggedType { - case reasonDeleteCfg.ID: - err = rh.questionCommon.RemoveAnswer(ctx, objectID) - } - case "comment": - switch req.FlaggedType { - case reasonCloseCfg.ID: - err = rh.commentRepo.RemoveComment(ctx, objectID) - rh.sendNotification(ctx, reportedUserID, objectID, constant.NotificationYourCommentWasDeleted) - } - } - return -} - -// sendNotification send rank triggered notification -func (rh *ReportHandle) sendNotification(ctx context.Context, reportedUserID, objectID, notificationAction string) { - msg := &schema.NotificationMsg{ - TriggerUserID: reportedUserID, - ReceiverUserID: reportedUserID, - Type: schema.NotificationTypeInbox, - ObjectID: objectID, - ObjectType: constant.ReportObjectType, - NotificationAction: notificationAction, - } - rh.notificationQueueService.Send(ctx, msg) -} diff --git a/internal/service/review/review_service.go b/internal/service/review/review_service.go new file mode 100644 index 000000000..80c318b11 --- /dev/null +++ b/internal/service/review/review_service.go @@ -0,0 +1,390 @@ +/* + * 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 review + +import ( + "context" + + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" + answercommon "github.com/apache/incubator-answer/internal/service/answer_common" + "github.com/apache/incubator-answer/internal/service/notice_queue" + "github.com/apache/incubator-answer/internal/service/object_info" + questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + "github.com/apache/incubator-answer/internal/service/role" + "github.com/apache/incubator-answer/internal/service/siteinfo_common" + tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" + usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/htmltext" + "github.com/apache/incubator-answer/pkg/token" + "github.com/apache/incubator-answer/pkg/uid" + "github.com/apache/incubator-answer/plugin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// ReviewRepo review repository +type ReviewRepo interface { + AddReview(ctx context.Context, review *entity.Review) (err error) + UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) + GetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error) + GetReviewCount(ctx context.Context, status int) (count int64, err error) + GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error) +} + +// ReviewService user service +type ReviewService struct { + reviewRepo ReviewRepo + objectInfoService *object_info.ObjService + userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + userRoleService *role.UserRoleRelService + tagCommon *tagcommon.TagCommonService + externalNotificationQueueService notice_queue.ExternalNotificationQueueService + notificationQueueService notice_queue.NotificationQueueService + siteInfoService siteinfo_common.SiteInfoCommonService +} + +// NewReviewService new review service +func NewReviewService( + reviewRepo ReviewRepo, + objectInfoService *object_info.ObjService, + userCommon *usercommon.UserCommon, + userRepo usercommon.UserRepo, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + userRoleService *role.UserRoleRelService, + externalNotificationQueueService notice_queue.ExternalNotificationQueueService, + tagCommon *tagcommon.TagCommonService, + notificationQueueService notice_queue.NotificationQueueService, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *ReviewService { + return &ReviewService{ + reviewRepo: reviewRepo, + objectInfoService: objectInfoService, + userCommon: userCommon, + userRepo: userRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + userRoleService: userRoleService, + externalNotificationQueueService: externalNotificationQueueService, + tagCommon: tagCommon, + notificationQueueService: notificationQueueService, + siteInfoService: siteInfoService, + } +} + +// AddQuestionReview add review for question if needed +func (cs *ReviewService) AddQuestionReview(ctx context.Context, + question *entity.Question, tags []*schema.TagItem) (needReview bool) { + reviewContent := &plugin.ReviewContent{ + ObjectType: constant.QuestionObjectType, + Title: question.Title, + Content: question.ParsedText, + } + for _, tag := range tags { + reviewContent.Tags = append(reviewContent.Tags, tag.SlugName) + } + reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID) + return cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent) +} + +// AddAnswerReview add review for answer if needed +func (cs *ReviewService) AddAnswerReview(ctx context.Context, + answer *entity.Answer) (needReview bool) { + reviewContent := &plugin.ReviewContent{ + ObjectType: constant.AnswerObjectType, + Content: answer.ParsedText, + } + reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID) + return cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent) +} + +// get review content author info +func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) { + user, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID) + if err != nil { + log.Errorf("get user info failed, err: %v", err) + return + } + if !exist { + log.Errorf("user not found by id: %s", userID) + return + } + author.Rank = user.Rank + author.ApprovedQuestionAmount, _ = cs.questionRepo.GetUserQuestionCount(ctx, userID, 0) + author.ApprovedAnswerAmount, _ = cs.answerRepo.GetCountByUserID(ctx, userID) + author.Role, _ = cs.userRoleService.GetUserRole(ctx, userID) + return +} + +// call plugin to review +func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string, + reviewContent *plugin.ReviewContent) (approved bool) { + // As default, no need review + approved = true + objectID = uid.DeShortID(objectID) + + r := &entity.Review{ + UserID: userID, + ObjectID: objectID, + ObjectType: constant.ObjectTypeStrMapping[reviewContent.ObjectType], + ReviewerUserID: "0", + Status: entity.ReviewStatusPending, + } + if siteInterface, _ := cs.siteInfoService.GetSiteInterface(ctx); siteInterface != nil { + reviewContent.Language = siteInterface.Language + } + + _ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error { + // If one of the reviewer plugin return false, then the review is not approved + if !approved { + return nil + } + if result := reviewer.Review(reviewContent); !result.Approved { + approved = false + r.Reason = result.Reason + r.Submitter = reviewer.Info().SlugName + } + return nil + }) + + if !approved { + if err := cs.reviewRepo.AddReview(ctx, r); err != nil { + log.Errorf("add review failed, err: %v", err) + } + } + return approved +} + +// UpdateReview update review +func (cs *ReviewService) UpdateReview(ctx context.Context, req *schema.UpdateReviewReq) (err error) { + review, exist, err := cs.reviewRepo.GetReview(ctx, req.ReviewID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if review.Status != entity.ReviewStatusPending { + return nil + } + + if err = cs.updateObjectStatus(ctx, review, req.IsApprove()); err != nil { + return err + } + + if req.IsApprove() { + err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusApproved) + } else { + err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusRejected) + } + return +} + +// update object status +func (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.Review, isApprove bool) (err error) { + objectType := constant.ObjectTypeNumberMapping[review.ObjectType] + switch objectType { + case constant.QuestionObjectType: + questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, review.ObjectID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + questionInfo.Status = entity.QuestionStatusAvailable + } else { + questionInfo.Status = entity.QuestionStatusDeleted + } + if err := cs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status); err != nil { + return err + } + if isApprove { + tags, err := cs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID) + if err != nil { + log.Errorf("get question tags failed, err: %v", err) + } + cs.externalNotificationQueueService.Send(ctx, + schema.CreateNewQuestionNotificationMsg(questionInfo.ID, questionInfo.Title, questionInfo.UserID, tags)) + } + userQuestionCount, err := cs.questionRepo.GetUserQuestionCount(ctx, questionInfo.UserID, 0) + if err != nil { + log.Errorf("get user question count failed, err: %v", err) + } else { + err = cs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) + if err != nil { + log.Errorf("update user question count failed, err: %v", err) + } + } + case constant.AnswerObjectType: + answerInfo, exist, err := cs.answerRepo.GetAnswer(ctx, review.ObjectID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + answerInfo.Status = entity.AnswerStatusAvailable + } else { + answerInfo.Status = entity.AnswerStatusDeleted + } + if err := cs.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, answerInfo.Status); err != nil { + return err + } + questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + cs.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, answerInfo.ID, + answerInfo.UserID, questionInfo.Title, answerInfo.OriginalText) + } + userAnswerCount, err := cs.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) + if err != nil { + log.Errorf("get user answer count failed, err: %v", err) + } else { + err = cs.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) + if err != nil { + log.Errorf("update user answer count failed, err: %v", err) + } + } + } + return +} + +func (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context, + questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) { + // If the question is answered by me, there is no notification for myself. + if questionUserID == answerUserID { + return + } + msg := &schema.NotificationMsg{ + TriggerUserID: answerUserID, + ReceiverUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationAnswerTheQuestion + cs.notificationQueueService.Send(ctx, msg) + + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewAnswerTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + AnswerID: answerID, + AnswerSummary: answerSummary, + UnsubscribeCode: token.GenerateToken(), + } + answerUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, answerUserID) + if answerUser != nil { + rawData.AnswerUserDisplayName = answerUser.DisplayName + } + externalNotificationMsg.NewAnswerTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) +} + +// GetReviewPendingCount get review pending count +func (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) { + return cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending) +} + +// GetUnreviewedPostPage get review page +func (cs *ReviewService) GetUnreviewedPostPage(ctx context.Context, req *schema.GetUnreviewedPostPageReq) ( + pageModel *pager.PageModel, err error) { + if !req.IsAdmin { + return pager.NewPageModel(0, make([]*schema.GetUnreviewedPostPageResp, 0)), nil + } + cond := &entity.Review{ + ObjectID: req.ObjectID, + Status: entity.ReviewStatusPending, + } + reviewList, total, err := cs.reviewRepo.GetReviewPage(ctx, req.Page, 1, cond) + if err != nil { + return + } + + resp := make([]*schema.GetUnreviewedPostPageResp, 0) + for _, review := range reviewList { + info, err := cs.objectInfoService.GetUnreviewedRevisionInfo(ctx, review.ObjectID) + if err != nil { + log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) + continue + } + + r := &schema.GetUnreviewedPostPageResp{ + ReviewID: review.ID, + CreatedAt: info.CreatedAt, + ObjectID: info.ObjectID, + QuestionID: info.QuestionID, + AnswerID: info.AnswerID, + CommentID: info.CommentID, + ObjectType: info.ObjectType, + Title: info.Title, + UrlTitle: htmltext.UrlTitle(info.Title), + OriginalText: info.Content, + ParsedText: info.Html, + Tags: info.Tags, + ObjectStatus: info.Status, + ObjectShowStatus: info.ShowStatus, + SubmitAt: review.CreatedAt.Unix(), + SubmitterDisplayName: req.ReviewerMapping[review.Submitter], + Reason: review.Reason, + } + + // get user info + userInfo, exists, e := cs.userCommon.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.AuthorUserInfo, userInfo) + } + resp = append(resp, r) + } + return pager.NewPageModel(total, resp), nil +} diff --git a/internal/service/revision/revision.go b/internal/service/revision/revision.go index 658c92f62..6844054b9 100644 --- a/internal/service/revision/revision.go +++ b/internal/service/revision/revision.go @@ -35,5 +35,6 @@ type RevisionRepo interface { UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) GetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error) + CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) } diff --git a/internal/service/revision_common/revision_service.go b/internal/service/revision_common/revision_service.go index c9d454e36..ba73f797f 100644 --- a/internal/service/revision_common/revision_service.go +++ b/internal/service/revision_common/revision_service.go @@ -53,8 +53,7 @@ func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req * if len(req.GetCanReviewObjectTypes()) == 0 { return 0, nil } - _, count, err = rs.revisionRepo.GetUnreviewedRevisionPage(ctx, req.Page, 1, req.GetCanReviewObjectTypes()) - return count, err + return rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) } // AddRevision add revision diff --git a/internal/service/search_common/search.go b/internal/service/search_common/search.go index c19fdc494..ba6f6295b 100644 --- a/internal/service/search_common/search.go +++ b/internal/service/search_common/search.go @@ -21,6 +21,7 @@ package search_common import ( "context" + "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/plugin" ) @@ -29,5 +30,5 @@ type SearchRepo interface { SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) - ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []*schema.SearchResult, err error) + ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) } diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go index e37fb7b17..2036ec6e2 100644 --- a/internal/service/siteinfo_common/siteinfo_service.go +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -66,7 +66,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService { // GetSiteGeneral get site info general func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { - resp = &schema.SiteGeneralResp{} + resp = &schema.SiteGeneralResp{CheckUpdate: true} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { return nil, err } diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index fe8b3b532..953b96f09 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -403,11 +403,11 @@ func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, } func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) { - tagIDList := make([]string, 0) tagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId) if err != nil { return nil, err } + tagIDList := make([]string, 0) for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 6cd69aec6..0923908fb 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -252,7 +252,7 @@ func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo } // try to update user avatar - if len(externalUserInfo.Avatar) > 0 { + if oldUserInfo.Avatar == "" && len(externalUserInfo.Avatar) > 0 { avatarInfo := &schema.AvatarInfo{ Type: constant.AvatarTypeCustom, Custom: externalUserInfo.Avatar, diff --git a/licenserc.toml b/licenserc.toml index 44f719b43..166dd5012 100644 --- a/licenserc.toml +++ b/licenserc.toml @@ -36,5 +36,5 @@ excludes = [ ] [properties] -inceptionYear = 2023 +inceptionYear = 2024 copyrightOwner = "tison " diff --git a/pkg/checker/url.go b/pkg/checker/url.go new file mode 100644 index 000000000..1ec97a595 --- /dev/null +++ b/pkg/checker/url.go @@ -0,0 +1,24 @@ +package checker + +import ( + "net/url" + "strings" +) + +func IsURL(str string) bool { + s := strings.ToLower(str) + + if len(s) == 0 { + return false + } + + u, err := url.Parse(s) + if err != nil || u.Scheme == "" { + return false + } + + if u.Host == "" && u.Fragment == "" && u.Opaque == "" { + return false + } + return u.Scheme == "http" || u.Scheme == "https" +} diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index 792063f46..26afedf7d 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -20,18 +20,36 @@ package htmltext import ( - "github.com/Chain-Zhang/pinyin" - "github.com/apache/incubator-answer/pkg/checker" "io" "net/http" "net/url" "regexp" "strings" + "unicode/utf8" + "github.com/Chain-Zhang/pinyin" "github.com/Machiel/slugify" strip "github.com/grokify/html-strip-tags-go" + + "github.com/apache/incubator-answer/pkg/checker" + "github.com/apache/incubator-answer/pkg/converter" ) +// min() and max() can be removed starting from Go1.21 +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + // ClearText clear HTML, get the clear text func ClearText(html string) (text string) { if len(html) == 0 { @@ -72,6 +90,9 @@ func UrlTitle(title string) (text string) { title = slugify.Slugify(title) title = url.QueryEscape(title) title = cutLongTitle(title) + if len(title) == 0 { + title = "topic" + } return title } @@ -107,22 +128,85 @@ func cutLongTitle(title string) string { // FetchExcerpt return the excerpt from the HTML string func FetchExcerpt(html, trimMarker string, limit int) (text string) { + return FetchRangedExcerpt(html, trimMarker, 0, limit) +} + +// findFirstMatchedWord returns the first matched word and its index +func findFirstMatchedWord(text string, words []string) (string, int) { + if len(text) == 0 || len(words) == 0 { + return "", 0 + } + + words = converter.UniqueArray(words) + firstWord := "" + firstIndex := len(text) + + for _, word := range words { + if idx := strings.Index(text, word); idx != -1 && idx < firstIndex { + firstIndex = idx + firstWord = word + } + } + + if firstIndex != len(text) { + return firstWord, firstIndex + } + + return "", 0 +} + +// getRuneRange returns the valid begin and end indexes of the runeText +func getRuneRange(runeText []rune, offset, limit int) (begin, end int) { + runeLen := len(runeText) + + limit = min(runeLen, max(0, limit)) + begin = min(runeLen, max(0, offset)) + end = min(runeLen, begin+limit) + + return +} + +// FetchRangedExcerpt returns a ranged excerpt from the HTML string. +// Note: offset is a rune index, not a byte index +func FetchRangedExcerpt(html, trimMarker string, offset int, limit int) (text string) { if len(html) == 0 { text = html return } - text = ClearText(html) - runeText := []rune(text) - if len(runeText) <= limit { - text = string(runeText) - return + runeText := []rune(ClearText(html)) + begin, end := getRuneRange(runeText, offset, limit) + text = string(runeText[begin:end]) + + if begin > 0 { + text = trimMarker + text + } + if end < len(runeText) { + text = text + trimMarker } - text = string(runeText[0:limit]) + trimMarker return } +// FetchMatchedExcerpt returns the matched excerpt according to the words +func FetchMatchedExcerpt(html string, words []string, trimMarker string, trimLength int) string { + text := ClearText(html) + matchedWord, matchedIndex := findFirstMatchedWord(text, words) + runeIndex := utf8.RuneCountInString(text[0:matchedIndex]) + + trimLength = max(0, trimLength) + runeOffset := runeIndex - trimLength + runeLimit := trimLength + trimLength + utf8.RuneCountInString(matchedWord) + + textRuneCount := utf8.RuneCountInString(text) + if runeOffset+runeLimit > textRuneCount { + // Reserved extra chars before the matched word + runeOffset = textRuneCount - runeLimit + } + + return FetchRangedExcerpt(html, trimMarker, runeOffset, runeLimit) +} + func GetPicByUrl(Url string) string { res, err := http.Get(Url) if err != nil { diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 4c520d7a2..d549d8874 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -85,3 +85,122 @@ func TestUrlTitle(t *testing.T) { fmt.Println(formatTitle) } } + +func TestFindFirstMatchedWord(t *testing.T) { + var ( + expectedWord, + actualWord string + expectedIndex, + actualIndex int + ) + + text := "Hello, I have 中文 and 😂 and I am supposed to work fine." + + // test find nothing + expectedWord, expectedIndex = "", 0 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"youcantfindme"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) + + // test find one word + expectedWord, expectedIndex = "文", 17 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"文"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) + + // test find multiple matched words + expectedWord, expectedIndex = "Hello", 0 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"Hello", "文"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) +} + +func TestGetRuneRange(t *testing.T) { + var ( + expectedBegin, + expectedEnd, + actualBegin, + actualEnd int + ) + + runeText := []rune("Hello, I have 中文 and 😂.") + runeLen := len(runeText) + + // test get range of negative offset and negative limit + expectedBegin, expectedEnd = 0, 0 + actualBegin, actualEnd = getRuneRange(runeText, -1, -1) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of exceeding offset and exceeding limit + expectedBegin, expectedEnd = runeLen, runeLen + actualBegin, actualEnd = getRuneRange(runeText, runeLen+1, runeLen+1) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of normal offset and exceeding limit + expectedBegin, expectedEnd = 3, runeLen + actualBegin, actualEnd = getRuneRange(runeText, 3, runeLen) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of normal offset and normal limit + expectedBegin, expectedEnd = 3, 10 + actualBegin, actualEnd = getRuneRange(runeText, 3, 7) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) +} + +func TestFetchRangedExcerpt(t *testing.T) { + var ( + expected, + actual string + ) + + // test english string + expected = "hello..." + actual = FetchRangedExcerpt("

hello world

", "...", 0, 5) + assert.Equal(t, expected, actual) + + // test string with offset + expected = "...llo你好..." + actual = FetchRangedExcerpt("

hello你好world

", "...", 2, 5) + assert.Equal(t, expected, actual) + + // test mixed string with emoticon with offset + expected = "...你好😂..." + actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 3) + assert.Equal(t, expected, actual) + + // test mixed string with offset and exceeding limit + expected = "...你好😂world" + actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 100) + assert.Equal(t, expected, actual) +} + +func TestFetchMatchedExcerpt(t *testing.T) { + var ( + expected, + actual string + ) + + html := "

Hello, I have 中文 and 😂 and I am supposed to work fine

" + + // test find nothing + // it should return from the begin with double trimLength text + expected = "Hello, I h..." + actual = FetchMatchedExcerpt(html, []string{"youcantfindme"}, "...", 5) + assert.Equal(t, expected, actual) + + // test find the word at the end + // it should return the word beginning with double trimLenth plus len(word) + expected = "... work fine" + actual = FetchMatchedExcerpt(html, []string{"youcant", "fine"}, "...", 3) + assert.Equal(t, expected, actual) + + // test find multiple words + // it should return the first matched word with trimmedText + expected = "... have 中文 and 😂..." + actual = FetchMatchedExcerpt(html, []string{"中文", "😂"}, "...", 6) + assert.Equal(t, expected, actual) +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 79e43f687..e492fdc42 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -21,6 +21,7 @@ package plugin import ( "encoding/json" + "github.com/segmentfault/pacman/i18n" "github.com/apache/incubator-answer/internal/base/handler" @@ -88,6 +89,10 @@ func Register(p Base) { if _, ok := p.(Notification); ok { registerNotification(p.(Notification)) } + + if _, ok := p.(Reviewer); ok { + registerReviewer(p.(Reviewer)) + } } type Stack[T Base] struct { diff --git a/plugin/reviewer.go b/plugin/reviewer.go new file mode 100644 index 000000000..95569d390 --- /dev/null +++ b/plugin/reviewer.go @@ -0,0 +1,48 @@ +package plugin + +type Reviewer interface { + Base + Review(content *ReviewContent) (result *ReviewResult) +} + +// ReviewContent is a struct that contains the content of a review +type ReviewContent struct { + // The type of the content, e.g. question, answer + ObjectType string + // The title of the content, only available for the question + Title string + // The content of the review, always available + Content string + // The tags of the content, only available for the question + Tags []string + // The author of the content + Author ReviewContentAuthor + // Review Language, the site language. e.g. en_US + // The plugin may reply the review result according to the language + Language string +} + +type ReviewContentAuthor struct { + // The user's reputation + Rank int + // The amount of questions that has approved + ApprovedQuestionAmount int64 + // The amount of answers that has approved + ApprovedAnswerAmount int64 + // 1:User 2:Admin 3:Moderator + Role int +} + +// ReviewResult is a struct that contains the result of a review +type ReviewResult struct { + // If the review is approved + Approved bool + // The reason for the result + Reason string +} + +var ( + // CallReviewer is a function that calls all registered parsers + CallReviewer, + registerReviewer = MakePlugin[Reviewer](false) +) diff --git a/script/plugin_list b/script/plugin_list index 5e6a4182a..49324ca0c 100644 --- a/script/plugin_list +++ b/script/plugin_list @@ -1 +1,2 @@ -github.com/apache/incubator-answer-plugins/connector-basic@latest \ No newline at end of file +github.com/apache/incubator-answer-plugins/connector-basic@latest +github.com/apache/incubator-answer-plugins/reviewer-basic@latest \ No newline at end of file diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 293d6bc17..b3ff6e8ae 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -20,6 +20,7 @@ const { addWebpackModuleRule, addWebpackAlias, + setWebpackOptimizationSplitChunks, } = require("customize-cra"); const path = require("path"); @@ -37,6 +38,101 @@ module.exports = { use: "yaml-loader" })(config); + setWebpackOptimizationSplitChunks({ + maxInitialRequests: 20, + minSize: 20 * 1024, + minChunks: 2, + cacheGroups: { + automaticNamePrefix: 'chunk', + components: { + test: /[\\/]components[\\/]/, + name: 'components', + priority: 14, + reuseExistingChunk: true, + minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, + chunks: 'initial', + }, + i18next: { + name: 'i18next', + test: /[\/]node_modules[\/](i18next)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 12, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + reactBootstrap: { + name: 'react-bootstrap', + test: /[\/]node_modules[\/](react-bootstrap)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 11, + minChunks: 1, + chunks: 'initial', + reuseExistingChunk: true, + }, + lodash: { + name: 'lodash', + test: /[\/]node_modules[\/](lodash)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 10, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + codemirror: { + name: 'codemirror', + test: /[\/]node_modules[\/](codemirror)[\/]/, + priority: 9, + reuseExistingChunk: true, + enforce: true, + }, + nextShare: { + name: 'next-share', + test: /[\/]node_modules[\/](next-share)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 8, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + marked: { + name: 'marked', + test: /[\/]node_modules[\/](marked)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 7, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + reactDom: { + name: 'react-dom', + test: /[\/]node_modules[\/](react-dom)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 7, + reuseExistingChunk: true, + chunks: 'all', + enforce: true, + }, + nodesAsync: { + name: 'chunk-nodesAsync', + test: /[\/]node_modules[\/]/, + priority: 2, + minChunks: 2, + chunks: 'async', // only package dependencies that are referenced asynchronously + reuseExistingChunk: true, // reuse an existing block + }, + nodesInitial: { + name: 'chunk-nodesInitial', + filename: 'static/js/[name].[contenthash:8].chunk.js', + test: /[\/]node_modules[\/]/, + priority: 1, + minChunks: 1, + chunks: 'initial', + reuseExistingChunk: true, + }, + }, + })(config); + // add i18n dir to ModuleScopePlugin allowedPaths const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); if (moduleScopePlugin) { diff --git a/ui/package.json b/ui/package.json index ac7e4411f..63a1f85d8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,7 +12,8 @@ "prepare": "pnpm build:packages", "pre-commit": "lint-staged", "build:packages": "pnpm -r --filter=./src/plugins/* run build", - "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules" + "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules", + "analyze": "source-map-explorer 'build/static/js/*.js'" }, "dependencies": { "axios": "^0.27.2", @@ -82,6 +83,7 @@ "react-app-rewired": "^2.2.1", "react-scripts": "5.0.1", "sass": "^1.54.4", + "source-map-explorer": "^2.5.3", "typescript": "^4.9.5", "yaml-loader": "^0.8.0" }, diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 77f3c9b1f..83bdc9e24 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -1,20 +1,3 @@ -# 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. - lockfileVersion: '6.0' settings: @@ -221,6 +204,9 @@ importers: sass: specifier: ^1.54.4 version: 1.54.9 + source-map-explorer: + specifier: ^2.5.3 + version: 2.5.3 typescript: specifier: ^4.9.5 version: 4.9.5 @@ -2223,7 +2209,7 @@ packages: chalk: 4.1.2 emittery: 0.8.1 exit: 0.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-changed-files: 27.5.1 jest-config: 27.5.1(ts-node@10.9.1) jest-haste-map: 27.5.1 @@ -2295,7 +2281,7 @@ packages: collect-v8-coverage: 1.0.1 exit: 0.1.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 5.2.0 istanbul-lib-report: 3.0.0 @@ -2324,7 +2310,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: callsites: 3.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 source-map: 0.6.1 /@jest/test-result@27.5.1: @@ -2350,7 +2336,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/test-result': 27.5.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 jest-runtime: 27.5.1 transitivePeerDependencies: @@ -4550,6 +4536,12 @@ packages: dependencies: node-int64: 0.4.0 + /btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + dev: true + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -5914,7 +5906,7 @@ packages: resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==} engines: {node: '>=10.13.0'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 /enhanced-resolve@5.15.0: @@ -7899,7 +7891,7 @@ packages: '@jest/types': 27.5.1 chalk: 4.1.2 exit: 0.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 import-local: 3.1.0 jest-config: 27.5.1(ts-node@10.9.1) jest-util: 27.5.1 @@ -7930,7 +7922,7 @@ packages: ci-info: 3.4.0 deepmerge: 4.2.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-circus: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -8034,7 +8026,7 @@ packages: '@types/node': 16.11.59 anymatch: 3.1.2 fb-watchman: 2.0.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-regex-util: 27.5.1 jest-serializer: 27.5.1 jest-util: 27.5.1 @@ -8102,7 +8094,7 @@ packages: '@jest/types': 27.5.1 '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 4.0.5 pretty-format: 27.5.1 slash: 3.0.0 @@ -8185,7 +8177,7 @@ packages: '@types/node': 16.11.59 chalk: 4.1.2 emittery: 0.8.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-docblock: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -8220,7 +8212,7 @@ packages: collect-v8-coverage: 1.0.1 execa: 5.1.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 jest-message-util: 27.5.1 jest-mock: 27.5.1 @@ -8238,7 +8230,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@types/node': 16.11.59 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jest-snapshot@27.5.1: resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} @@ -8256,7 +8248,7 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.1) chalk: 4.1.2 expect: 27.5.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-diff: 27.5.1 jest-get-type: 27.5.1 jest-haste-map: 27.5.1 @@ -8277,7 +8269,7 @@ packages: '@types/node': 16.11.59 chalk: 4.1.2 ci-info: 3.4.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 picomatch: 2.3.1 /jest-util@28.1.3: @@ -8483,7 +8475,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jsonp@0.2.1: resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==} @@ -9153,6 +9145,14 @@ packages: mimic-fn: 4.0.0 dev: true + /open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + /open@8.4.0: resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} engines: {node: '>=12'} @@ -10792,6 +10792,13 @@ packages: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} dev: true + /rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -11151,6 +11158,25 @@ packages: /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + /source-map-explorer@2.5.3: + resolution: {integrity: sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==} + engines: {node: '>=12'} + hasBin: true + dependencies: + btoa: 1.2.1 + chalk: 4.1.2 + convert-source-map: 1.8.0 + ejs: 3.1.8 + escape-html: 1.0.3 + glob: 7.2.3 + gzip-size: 6.0.0 + lodash: 4.17.21 + open: 7.4.2 + source-map: 0.7.4 + temp: 0.9.4 + yargs: 16.2.0 + dev: true + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -11593,6 +11619,14 @@ packages: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} + /temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + dev: true + /tempy@0.6.0: resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} engines: {node: '>=10'} @@ -12184,7 +12218,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /wbuf@1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} diff --git a/ui/src/behaviour/useLegalClick.tsx b/ui/src/behaviour/useLegalClick.tsx new file mode 100644 index 000000000..a098b3e8e --- /dev/null +++ b/ui/src/behaviour/useLegalClick.tsx @@ -0,0 +1,32 @@ +import { MouseEvent, useCallback } from 'react'; + +import { useLegalPrivacy, useLegalTos } from '@/services/client/legal'; + +export const useLegalClick = () => { + const { data: tos } = useLegalTos(); + const { data: privacy } = useLegalPrivacy(); + + const legalClick = useCallback( + (evt: MouseEvent, type: 'tos' | 'privacy') => { + evt.stopPropagation(); + const contentText = + type === 'tos' + ? tos?.terms_of_service_original_text + : privacy?.privacy_policy_original_text; + let matchUrl: URL | undefined; + try { + if (contentText) { + matchUrl = new URL(contentText); + } + // eslint-disable-next-line no-empty + } catch (ex) {} + if (matchUrl) { + evt.preventDefault(); + window.open(matchUrl.toString()); + } + }, + [tos, privacy], + ); + + return legalClick; +}; diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 373e41d21..e76c60b03 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -30,6 +30,7 @@ export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_'; export const DEFAULT_THEME = 'system'; export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99; +export const SKELETON_SHOW_TIME = 1000; export const USER_AGENT_NAMES = { SegmentFault: 'SegmentFault', @@ -54,6 +55,11 @@ export const ADMIN_LIST_STATUS = { variant: 'text-bg-danger', name: 'deleted', }, + // pending + 11: { + variant: 'text-bg-warning', + name: 'pending', + }, normal: { variant: 'text-bg-success', name: 'normal', @@ -66,6 +72,14 @@ export const ADMIN_LIST_STATUS = { variant: 'text-bg-danger', name: 'deleted', }, + pending: { + variant: 'text-bg-warning', + name: 'pending', + }, + unlisted: { + variant: 'text-bg-secondary', + name: 'unlisted', + }, }; export const ADMIN_NAV_MENUS = [ @@ -80,10 +94,6 @@ export const ADMIN_NAV_MENUS = [ { name: 'users', }, - { - name: 'flags', - // badgeContent: 5, - }, { name: 'customize', children: [ diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 201a5ae0e..53f8eef59 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -56,6 +56,7 @@ export interface TagBase { export interface Tag extends TagBase { main_tag_slug_name?: string; parsed_text?: string; + tag_id?: string; } export interface SynonymsTag extends Tag { @@ -250,7 +251,7 @@ export interface QuestionDetailRes { } export interface AnswersReq extends Paging { - order?: 'default' | 'updated'; + order?: 'default' | 'updated' | 'created'; question_id: string; } @@ -299,9 +300,13 @@ export interface QueryQuestionsReq extends Paging { in_days?: number; } -export type AdminQuestionStatus = 'available' | 'closed' | 'deleted'; +export type AdminQuestionStatus = + | 'available' + | 'pending' + | 'closed' + | 'deleted'; -export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted'; +export type AdminContentsFilterBy = 'normal' | 'pending' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; @@ -347,6 +352,8 @@ export interface AdminSettingsGeneral { description: string; site_url: string; contact_email: string; + check_update: boolean; + permalink?: number; } export interface HelmetBase { @@ -561,7 +568,7 @@ export interface TimelineRes { timeline: TimelineItem[]; } -export interface ReviewItem { +export interface SuggestReviewItem { type: 'question' | 'answer' | 'tag'; info: { url_title?: string; @@ -583,9 +590,60 @@ export interface ReviewItem { content: Tag | QuestionDetailRes | AnswerItem; }; } -export interface ReviewResp { +export interface SuggestReviewResp { count: number; - list: ReviewItem[]; + list: SuggestReviewItem[]; +} + +export interface ReasonItem { + content_type: string; + description: string; + name: string; + placeholder: string; + reason_type: number; +} + +export interface BaseReviewItem { + object_type: 'question' | 'answer' | 'comment' | 'user'; + object_id: string; + object_show_status: number; + object_status: number; + tags: Tag[]; + title: string; + original_text: string; + author_user_info: UserInfoBase; + created_at: number; + submit_at: number; + comment_id: string; + question_id: string; + answer_id: string; + answer_count: number; + answer_accepted?: boolean; + flag_id: string; + url_title: string; + parsed_text: string; +} + +export interface FlagReviewItem extends BaseReviewItem { + reason: ReasonItem; + reason_content: string; + submitter_user: UserInfoBase; +} + +export interface FlagReviewResp { + count: number; + list: FlagReviewItem[]; +} + +export interface QueuedReviewItem extends BaseReviewItem { + review_id: number; + reason: string; + submitter_display_name: string; +} + +export interface QueuedReviewResp { + count: number; + list: QueuedReviewItem[]; } export interface UserRoleItem { @@ -637,3 +695,27 @@ export interface UserPluginsConfigRes { name: string; slug_name: string; } + +export interface ReviewTypeItem { + label: string; + name: string; + todo_amount: number; +} + +export interface PutFlagReviewParams { + operation_type: + | 'edit_post' + | 'close_post' + | 'delete_post' + | 'unlist_post' + | 'ignore_report'; + flag_id: string; + close_msg?: string; + close_type?: number; + title?: string; + content?: string; + tags?: Tag[]; + // mention_username_list?: any; + captcha_code?: any; + captcha_id?: any; +} diff --git a/ui/src/common/pattern.ts b/ui/src/common/pattern.ts index 847c6d769..133228b18 100644 --- a/ui/src/common/pattern.ts +++ b/ui/src/common/pattern.ts @@ -20,6 +20,8 @@ const pattern = { email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, + search: + /(\[.*\])|(is:answer)|(is:question)|(score:\d*)|(user:\S*)|(answers:\d*)/g, uaWeChat: /micromessenger/i, uaWeCom: /wxwork/i, uaDingTalk: /dingtalk/i, diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx index 8809778c5..05ae5b2d5 100644 --- a/ui/src/components/Comment/components/Form/index.tsx +++ b/ui/src/components/Comment/components/Form/index.tsx @@ -25,6 +25,7 @@ import classNames from 'classnames'; import { TextArea, Mentions } from '@/components'; import { usePageUsers, usePromptWithUnload } from '@/hooks'; +import { parseEditMentionUser } from '@/utils'; const Index = ({ className = '', @@ -78,7 +79,11 @@ const Index = ({ -