diff --git a/internal/controller/plugin_importer_controller.go b/internal/controller/plugin_importer_controller.go new file mode 100644 index 000000000..90532acf2 --- /dev/null +++ b/internal/controller/plugin_importer_controller.go @@ -0,0 +1,209 @@ +/* + * 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 ( + "bytes" + "io" + + "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/base/translator" + "github.com/apache/incubator-answer/internal/base/validator" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/content" + "github.com/apache/incubator-answer/internal/service/permission" + "github.com/apache/incubator-answer/internal/service/rank" + usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +type ImporterController struct { + // userRepo *usercommon.UserRepo + questionService *content.QuestionService + rankService *rank.RankService + rateLimitMiddleware *middleware.RateLimitMiddleware + userCommon *usercommon.UserCommon +} + +func NewImporterController( + questionService *content.QuestionService, + rankService *rank.RankService, + rateLimitMiddleware *middleware.RateLimitMiddleware, + userCommon *usercommon.UserCommon, +) *ImporterController { + return &ImporterController{ + questionService: questionService, + rankService: rankService, + rateLimitMiddleware: rateLimitMiddleware, + userCommon: userCommon, + } +} + +// ImportCommand godoc +// @Summary ImportCommand +// @Description ImportCommand +// @Tags PluginImporter +// @Accept json +// @Produce json +// @Router /answer/api/v1/importer/command [post] +// @Success 200 {object} handler.RespBody{data=[]plugin.importStatus} +func (ipc *ImporterController) ImportCommand(ctx *gin.Context) { + questionInfo := &plugin.QuestionImporterInfo{} + if !plugin.ImporterEnabled() { + log.Errorf("error: plugin is not enabled") + ctx.JSON(200, gin.H{"text": "plugin is not enabled"}) + return + } + body, err := io.ReadAll(ctx.Request.Body) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + cmd := ctx.PostForm("command") + if cmd != "/ask" { + log.Errorf("error: Invalid command") + ctx.JSON(200, gin.H{"text": "Invalid command"}) + return + } + _ = plugin.CallImporter(func(importer plugin.Importer) error { + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + questionInfo, err = importer.GetQuestion(ctx) + return nil + }) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + + req := &schema.QuestionAdd{} + errFields := make([]*validator.FormErrorField, 0) + // errFields := handler.BindAndCheckReturnErr(ctx, req) + if ctx.IsAborted() { + return + } + + // reject, rejectKey := ipc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) + // if reject { + // return + // } + user_info, exist, err := ipc.userCommon.GetByEmail(ctx, questionInfo.UserEmail) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + if !exist { + log.Errorf("error: User Email not found") + ctx.JSON(200, gin.H{"text": "User Email not found"}) + return + } + + // defer func() { + // // If status is not 200 means that the bad request has been returned, so the record should be cleared + // if ctx.Writer.Status() != http.StatusOK { + // ipc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) + // } + // }() + req.UserID = user_info.ID + req.Title = questionInfo.Title + req.Content = questionInfo.Content + req.HTML = "

" + questionInfo.Content + "

" + req.Tags = make([]*schema.TagItem, len(questionInfo.Tags)) + for i, tag := range questionInfo.Tags { + req.Tags[i] = &schema.TagItem{ + SlugName: tag, + DisplayName: tag, + } + } + canList, requireRanks, err := ipc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ + permission.QuestionAdd, + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionClose, + permission.QuestionReopen, + permission.TagUseReservedTag, + permission.TagAdd, + permission.LinkUrlLimit, + }) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + req.CanClose = canList[3] + req.CanReopen = canList[4] + req.CanUseReservedTag = canList[5] + req.CanAddTag = canList[6] + if !req.CanAdd { + ctx.JSON(200, gin.H{"text": "No permission to add question"}) + log.Errorf("error: %v", err) + return + } + hasNewTag, err := ipc.questionService.HasNewTag(ctx, req.Tags) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + if !req.CanAddTag && hasNewTag { + lang := handler.GetLang(ctx) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) + log.Errorf("error: %v", msg) + ctx.JSON(200, gin.H{"text": msg}) + return + } + + errList, err := ipc.questionService.CheckAddQuestion(ctx, req) + if err != nil { + errlist, ok := errList.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + log.Errorf("error: RequestFormatError") + ctx.JSON(200, gin.H{"text": "RequestFormatError"}) + return + } + req.UserAgent = ctx.GetHeader("User-Agent") + req.IP = ctx.ClientIP() + resp, err := ipc.questionService.AddQuestion(ctx, req) + if err != nil { + errlist, ok := resp.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + log.Errorf("error: RequestFormatError") + ctx.JSON(200, gin.H{"text": "RequestFormatError"}) + return + } + + ctx.JSON(200, gin.H{"text": "Add Question Successfully"}) +} diff --git a/internal/router/plugin_api_router.go b/internal/router/plugin_api_router.go index e69e0b6b0..22a13b859 100644 --- a/internal/router/plugin_api_router.go +++ b/internal/router/plugin_api_router.go @@ -30,6 +30,7 @@ type PluginAPIRouter struct { captchaController *controller.CaptchaController embedController *controller.EmbedController renderController *controller.RenderController + importerController *controller.ImporterController } func NewPluginAPIRouter( @@ -38,6 +39,7 @@ func NewPluginAPIRouter( captchaController *controller.CaptchaController, embedController *controller.EmbedController, renderController *controller.RenderController, + importerController *controller.ImporterController, ) *PluginAPIRouter { return &PluginAPIRouter{ connectorController: connectorController, @@ -68,6 +70,9 @@ func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) { r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig) r.GET("/embed/config", pr.embedController.GetEmbedConfig) r.GET("/render/config", pr.renderController.GetRenderConfig) + + // importer plugin + r.POST("/importer/answer", pr.importerController.ImportCommand) } func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) { diff --git a/plugin/importer.go b/plugin/importer.go new file mode 100644 index 000000000..79a84c500 --- /dev/null +++ b/plugin/importer.go @@ -0,0 +1,56 @@ +/* + * 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 plugin + +import "github.com/gin-gonic/gin" + +type QuestionImporterInfo struct { + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + UserEmail string `json:"user_email"` +} + +type Importer interface { + Base + GetQuestion(ctx *gin.Context) (questionInfo *QuestionImporterInfo, err error) +} + +var ( + // CallImporter is a function that calls all registered parsers + CallImporter, + registerImporter = MakePlugin[Importer](false) +) + +func ImporterEnabled() (enabled bool) { + _ = CallImporter(func(fn Importer) error { + enabled = true + return nil + }) + return +} +func GetImporter() (ip Importer, ok bool) { + _ = CallImporter(func(fn Importer) error { + ip = fn + ok = true + return nil + }) + return +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 9c144346a..2e4bf749b 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -110,6 +110,10 @@ func Register(p Base) { if _, ok := p.(CDN); ok { registerCDN(p.(CDN)) } + + if _, ok := p.(Importer); ok { + registerImporter(p.(Importer)) + } } type Stack[T Base] struct {