chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/activity"
|
||||
"github.com/apache/answer/internal/service/role"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ActivityController struct {
|
||||
activityService *activity.ActivityService
|
||||
}
|
||||
|
||||
// NewActivityController new activity controller.
|
||||
func NewActivityController(
|
||||
activityService *activity.ActivityService) *ActivityController {
|
||||
return &ActivityController{activityService: activityService}
|
||||
}
|
||||
|
||||
// GetObjectTimeline get object timeline
|
||||
// @Summary get object timeline
|
||||
// @Description get object timeline
|
||||
// @Tags Comment
|
||||
// @Produce json
|
||||
// @Param object_id query string false "object id"
|
||||
// @Param tag_slug_name query string false "tag slug name"
|
||||
// @Param object_type query string false "object type" Enums(question, answer, tag)
|
||||
// @Param show_vote query boolean false "is show vote"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}
|
||||
// @Router /answer/api/v1/activity/timeline [get]
|
||||
func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {
|
||||
req := &schema.GetObjectTimelineReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil {
|
||||
req.IsAdmin = userInfo.RoleID == role.RoleAdminID
|
||||
}
|
||||
|
||||
resp, err := ac.activityService.GetObjectTimeline(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetObjectTimelineDetail get object timeline detail
|
||||
// @Summary get object timeline detail
|
||||
// @Description get object timeline detail
|
||||
// @Tags Comment
|
||||
// @Produce json
|
||||
// @Param revision_id query string true "revision id"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}
|
||||
// @Router /answer/api/v1/activity/timeline/detail [get]
|
||||
func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) {
|
||||
req := &schema.GetObjectTimelineDetailReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
/*
|
||||
* 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/schema/mcp_tools"
|
||||
"github.com/apache/answer/internal/service/ai_conversation"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
"github.com/apache/answer/internal/service/comment"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/feature_toggle"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
tagcommonser "github.com/apache/answer/internal/service/tag_common"
|
||||
usercommon "github.com/apache/answer/internal/service/user_common"
|
||||
"github.com/apache/answer/pkg/token"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type AIController struct {
|
||||
searchService *content.SearchService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
tagCommonService *tagcommonser.TagCommonService
|
||||
questioncommon *questioncommon.QuestionCommon
|
||||
commentRepo comment.CommentRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
answerRepo answercommon.AnswerRepo
|
||||
mcpController *MCPController
|
||||
aiConversationService ai_conversation.AIConversationService
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService
|
||||
}
|
||||
|
||||
// NewAIController new site info controller.
|
||||
func NewAIController(
|
||||
searchService *content.SearchService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
tagCommonService *tagcommonser.TagCommonService,
|
||||
questioncommon *questioncommon.QuestionCommon,
|
||||
commentRepo comment.CommentRepo,
|
||||
userCommon *usercommon.UserCommon,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
mcpController *MCPController,
|
||||
aiConversationService ai_conversation.AIConversationService,
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService,
|
||||
) *AIController {
|
||||
return &AIController{
|
||||
searchService: searchService,
|
||||
siteInfoService: siteInfoService,
|
||||
tagCommonService: tagCommonService,
|
||||
questioncommon: questioncommon,
|
||||
commentRepo: commentRepo,
|
||||
userCommon: userCommon,
|
||||
answerRepo: answerRepo,
|
||||
mcpController: mcpController,
|
||||
aiConversationService: aiConversationService,
|
||||
featureToggleSvc: featureToggleSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AIController) ensureAIChatEnabled(ctx *gin.Context) bool {
|
||||
if c.featureToggleSvc == nil {
|
||||
return true
|
||||
}
|
||||
if err := c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type ChatCompletionsRequest struct {
|
||||
Messages []Message `validate:"required,gte=1" json:"messages"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
type ChatCompletionsResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
ChatCompletionID string `json:"chat_completion_id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []StreamChoice `json:"choices"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type StreamChoice struct {
|
||||
Index int `json:"index"`
|
||||
Delta Delta `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type ConversationContext struct {
|
||||
ConversationID string
|
||||
UserID string
|
||||
UserQuestion string
|
||||
Messages []*ai_conversation.ConversationMessage
|
||||
IsNewConversation bool
|
||||
Model string
|
||||
}
|
||||
|
||||
func (c *ConversationContext) GetOpenAIMessages() []openai.ChatCompletionMessage {
|
||||
messages := make([]openai.ChatCompletionMessage, len(c.Messages))
|
||||
for i, msg := range c.Messages {
|
||||
messages[i] = openai.ChatCompletionMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
// sendStreamData
|
||||
func sendStreamData(w http.ResponseWriter, data StreamResponse) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AIController) ChatCompletions(ctx *gin.Context) {
|
||||
if !c.ensureAIChatEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
aiConfig, err := c.siteInfoService.GetSiteAI(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get AI config: %v", err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest("AI service configuration error"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !aiConfig.Enabled {
|
||||
handler.HandleResponse(ctx, errors.ServiceUnavailable("AI service is not enabled"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
aiProvider := aiConfig.GetProvider()
|
||||
|
||||
req := &ChatCompletionsRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
data, _ := json.Marshal(req)
|
||||
log.Infof("ai chat request data: %s", string(data))
|
||||
|
||||
ctx.Header("Content-Type", "text/event-stream")
|
||||
ctx.Header("Cache-Control", "no-cache")
|
||||
ctx.Header("Connection", "keep-alive")
|
||||
ctx.Header("Access-Control-Allow-Origin", "*")
|
||||
ctx.Header("Access-Control-Allow-Headers", "Cache-Control")
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
|
||||
w := ctx.Writer
|
||||
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
chatcmplID := "chatcmpl-" + token.GenerateToken()
|
||||
created := time.Now().Unix()
|
||||
|
||||
firstResponse := StreamResponse{
|
||||
ChatCompletionID: chatcmplID,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: time.Now().Unix(),
|
||||
Model: aiProvider.Model,
|
||||
Choices: []StreamChoice{{Index: 0, Delta: Delta{Role: "assistant"}, FinishReason: nil}},
|
||||
}
|
||||
|
||||
sendStreamData(w, firstResponse)
|
||||
|
||||
conversationCtx := c.initializeConversationContext(ctx, aiProvider.Model, req)
|
||||
if conversationCtx == nil {
|
||||
log.Error("Failed to initialize conversation context")
|
||||
c.sendErrorResponse(w, chatcmplID, aiProvider.Model, "Failed to initialize conversation context")
|
||||
return
|
||||
}
|
||||
|
||||
c.redirectRequestToAI(ctx, w, chatcmplID, conversationCtx)
|
||||
|
||||
finishReason := "stop"
|
||||
endResponse := StreamResponse{
|
||||
ChatCompletionID: chatcmplID,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: aiProvider.Model,
|
||||
Choices: []StreamChoice{{Index: 0, Delta: Delta{}, FinishReason: &finishReason}},
|
||||
}
|
||||
|
||||
sendStreamData(w, endResponse)
|
||||
|
||||
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
c.saveConversationRecord(ctx, chatcmplID, conversationCtx)
|
||||
}
|
||||
|
||||
func (c *AIController) redirectRequestToAI(ctx *gin.Context, w http.ResponseWriter, id string, conversationCtx *ConversationContext) {
|
||||
client := c.createOpenAIClient()
|
||||
|
||||
c.handleAIConversation(ctx, w, id, client, conversationCtx)
|
||||
}
|
||||
|
||||
// createOpenAIClient
|
||||
func (c *AIController) createOpenAIClient() *openai.Client {
|
||||
config := openai.DefaultConfig("")
|
||||
config.BaseURL = ""
|
||||
|
||||
aiConfig, err := c.siteInfoService.GetSiteAI(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get AI config: %v", err)
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
if !aiConfig.Enabled {
|
||||
log.Warn("AI feature is disabled")
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
aiProvider := aiConfig.GetProvider()
|
||||
|
||||
config = openai.DefaultConfig(aiProvider.APIKey)
|
||||
config.BaseURL = aiProvider.APIHost
|
||||
if !strings.HasSuffix(config.BaseURL, "/v1") {
|
||||
config.BaseURL += "/v1"
|
||||
}
|
||||
return openai.NewClientWithConfig(config)
|
||||
}
|
||||
|
||||
// getPromptByLanguage
|
||||
func (c *AIController) getPromptByLanguage(language i18n.Language, question string) string {
|
||||
aiConfig, err := c.siteInfoService.GetSiteAI(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get AI config: %v", err)
|
||||
return c.getDefaultPrompt(language, question)
|
||||
}
|
||||
|
||||
var promptTemplate string
|
||||
|
||||
switch language {
|
||||
case i18n.LanguageChinese:
|
||||
promptTemplate = aiConfig.PromptConfig.ZhCN
|
||||
case i18n.LanguageEnglish:
|
||||
promptTemplate = aiConfig.PromptConfig.EnUS
|
||||
default:
|
||||
promptTemplate = aiConfig.PromptConfig.EnUS
|
||||
}
|
||||
|
||||
if promptTemplate == "" {
|
||||
return c.getDefaultPrompt(language, question)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(promptTemplate, question)
|
||||
}
|
||||
|
||||
// getDefaultPrompt prompt
|
||||
func (c *AIController) getDefaultPrompt(language i18n.Language, question string) string {
|
||||
switch language {
|
||||
case i18n.LanguageChinese:
|
||||
return fmt.Sprintf(constant.DefaultAIPromptConfigZhCN, question)
|
||||
case i18n.LanguageEnglish:
|
||||
return fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question)
|
||||
default:
|
||||
return fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question)
|
||||
}
|
||||
}
|
||||
|
||||
// initializeConversationContext
|
||||
func (c *AIController) initializeConversationContext(ctx *gin.Context, model string, req *ChatCompletionsRequest) *ConversationContext {
|
||||
if len(req.ConversationID) == 0 {
|
||||
req.ConversationID = token.GenerateToken()
|
||||
}
|
||||
conversationCtx := &ConversationContext{
|
||||
UserID: req.UserID,
|
||||
Messages: make([]*ai_conversation.ConversationMessage, 0),
|
||||
ConversationID: req.ConversationID,
|
||||
Model: model,
|
||||
}
|
||||
|
||||
conversationDetail, exist, err := c.aiConversationService.GetConversationDetail(ctx, &schema.AIConversationDetailReq{
|
||||
ConversationID: req.ConversationID,
|
||||
UserID: req.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get conversation detail: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !exist {
|
||||
conversationCtx.UserQuestion = req.Messages[0].Content
|
||||
conversationCtx.Messages = c.buildInitialMessages(ctx, req)
|
||||
conversationCtx.IsNewConversation = true
|
||||
return conversationCtx
|
||||
}
|
||||
conversationCtx.IsNewConversation = false
|
||||
|
||||
for _, record := range conversationDetail.Records {
|
||||
conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{
|
||||
ChatCompletionID: record.ChatCompletionID,
|
||||
Role: record.Role,
|
||||
Content: record.Content,
|
||||
})
|
||||
}
|
||||
conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{
|
||||
Role: req.Messages[0].Role,
|
||||
Content: req.Messages[0].Content,
|
||||
})
|
||||
return conversationCtx
|
||||
}
|
||||
|
||||
// buildInitialMessages
|
||||
func (c *AIController) buildInitialMessages(ctx *gin.Context, req *ChatCompletionsRequest) []*ai_conversation.ConversationMessage {
|
||||
question := ""
|
||||
if len(req.Messages) == 1 {
|
||||
question = req.Messages[0].Content
|
||||
} else {
|
||||
messages := make([]*ai_conversation.ConversationMessage, len(req.Messages))
|
||||
for i, msg := range req.Messages {
|
||||
messages[i] = &ai_conversation.ConversationMessage{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
currentLang := handler.GetLangByCtx(ctx)
|
||||
|
||||
prompt := c.getPromptByLanguage(currentLang, question)
|
||||
|
||||
return []*ai_conversation.ConversationMessage{{Role: openai.ChatMessageRoleUser, Content: prompt}}
|
||||
}
|
||||
|
||||
// saveConversationRecord
|
||||
func (c *AIController) saveConversationRecord(ctx context.Context, chatcmplID string, conversationCtx *ConversationContext) {
|
||||
if conversationCtx == nil || len(conversationCtx.Messages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if conversationCtx.IsNewConversation {
|
||||
topic := conversationCtx.UserQuestion
|
||||
if topic == "" {
|
||||
log.Warn("No user message found for new conversation")
|
||||
return
|
||||
}
|
||||
|
||||
err := c.aiConversationService.CreateConversation(ctx, conversationCtx.UserID, conversationCtx.ConversationID, topic)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create conversation: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := c.aiConversationService.SaveConversationRecords(ctx, conversationCtx.ConversationID, chatcmplID, conversationCtx.Messages)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to save conversation records: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AIController) handleAIConversation(ctx *gin.Context, w http.ResponseWriter, id string, client *openai.Client, conversationCtx *ConversationContext) {
|
||||
maxRounds := 10
|
||||
messages := conversationCtx.GetOpenAIMessages()
|
||||
|
||||
for round := range maxRounds {
|
||||
log.Debugf("AI conversation round: %d", round+1)
|
||||
|
||||
aiReq := openai.ChatCompletionRequest{
|
||||
Model: conversationCtx.Model,
|
||||
Messages: messages,
|
||||
Tools: c.getMCPTools(),
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
toolCalls, newMessages, finished, aiResponse, reasoningContent := c.processAIStream(ctx, w, id, conversationCtx.Model, client, aiReq, messages)
|
||||
messages = newMessages
|
||||
|
||||
log.Debugf("Round %d: toolCalls=%v", round+1, toolCalls)
|
||||
if aiResponse != "" || reasoningContent != "" {
|
||||
conversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{
|
||||
Role: "assistant",
|
||||
Content: aiResponse,
|
||||
ReasoningContent: reasoningContent,
|
||||
})
|
||||
}
|
||||
|
||||
if finished {
|
||||
return
|
||||
}
|
||||
|
||||
if len(toolCalls) > 0 {
|
||||
messages = c.executeToolCalls(ctx, w, id, conversationCtx.Model, toolCalls, messages, aiResponse, reasoningContent)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("AI conversation reached maximum rounds limit: %d", maxRounds)
|
||||
}
|
||||
|
||||
// processAIStream
|
||||
func (c *AIController) processAIStream(
|
||||
_ *gin.Context, w http.ResponseWriter, id, model string, client *openai.Client, aiReq openai.ChatCompletionRequest, messages []openai.ChatCompletionMessage) (
|
||||
[]openai.ToolCall, []openai.ChatCompletionMessage, bool, string, string) {
|
||||
stream, err := client.CreateChatCompletionStream(context.Background(), aiReq)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create stream: %v", err)
|
||||
c.sendErrorResponse(w, id, model, "Failed to create AI stream")
|
||||
return nil, messages, true, "", ""
|
||||
}
|
||||
defer func() {
|
||||
_ = stream.Close()
|
||||
}()
|
||||
|
||||
var currentToolCalls []openai.ToolCall
|
||||
var accumulatedContent strings.Builder
|
||||
var accumulatedReasoning strings.Builder
|
||||
var accumulatedMessage openai.ChatCompletionMessage
|
||||
toolCallsMap := make(map[int]*openai.ToolCall)
|
||||
|
||||
for {
|
||||
response, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
log.Info("Stream finished")
|
||||
break
|
||||
}
|
||||
log.Errorf("Stream error: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
if len(response.Choices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
choice := response.Choices[0]
|
||||
|
||||
if len(choice.Delta.ToolCalls) > 0 {
|
||||
for _, deltaToolCall := range choice.Delta.ToolCalls {
|
||||
index := *deltaToolCall.Index
|
||||
|
||||
if _, exists := toolCallsMap[index]; !exists {
|
||||
toolCallsMap[index] = &openai.ToolCall{
|
||||
ID: deltaToolCall.ID,
|
||||
Type: deltaToolCall.Type,
|
||||
Function: openai.FunctionCall{
|
||||
Name: deltaToolCall.Function.Name,
|
||||
Arguments: deltaToolCall.Function.Arguments,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if deltaToolCall.Function.Arguments != "" {
|
||||
toolCallsMap[index].Function.Arguments += deltaToolCall.Function.Arguments
|
||||
}
|
||||
if deltaToolCall.Function.Name != "" {
|
||||
toolCallsMap[index].Function.Name = deltaToolCall.Function.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if choice.Delta.ReasoningContent != "" {
|
||||
accumulatedReasoning.WriteString(choice.Delta.ReasoningContent)
|
||||
|
||||
reasoningResponse := StreamResponse{
|
||||
ChatCompletionID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: time.Now().Unix(),
|
||||
Model: model,
|
||||
Choices: []StreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: Delta{
|
||||
ReasoningContent: choice.Delta.ReasoningContent,
|
||||
},
|
||||
FinishReason: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
sendStreamData(w, reasoningResponse)
|
||||
}
|
||||
|
||||
if choice.Delta.Content != "" {
|
||||
accumulatedContent.WriteString(choice.Delta.Content)
|
||||
|
||||
contentResponse := StreamResponse{
|
||||
ChatCompletionID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: time.Now().Unix(),
|
||||
Model: model,
|
||||
Choices: []StreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: Delta{
|
||||
Content: choice.Delta.Content,
|
||||
},
|
||||
FinishReason: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
sendStreamData(w, contentResponse)
|
||||
}
|
||||
|
||||
if len(choice.FinishReason) > 0 {
|
||||
if choice.FinishReason == "tool_calls" {
|
||||
for _, toolCall := range toolCallsMap {
|
||||
currentToolCalls = append(currentToolCalls, *toolCall)
|
||||
}
|
||||
return currentToolCalls, messages, false, accumulatedContent.String(), accumulatedReasoning.String()
|
||||
} else {
|
||||
aiResponseContent := accumulatedContent.String()
|
||||
aiReasoningContent := accumulatedReasoning.String()
|
||||
if aiResponseContent != "" || aiReasoningContent != "" {
|
||||
accumulatedMessage = openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
Content: aiResponseContent,
|
||||
ReasoningContent: aiReasoningContent,
|
||||
}
|
||||
messages = append(messages, accumulatedMessage)
|
||||
}
|
||||
return nil, messages, true, aiResponseContent, aiReasoningContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aiResponseContent := accumulatedContent.String()
|
||||
aiReasoningContent := accumulatedReasoning.String()
|
||||
if aiResponseContent != "" || aiReasoningContent != "" {
|
||||
accumulatedMessage = openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
Content: aiResponseContent,
|
||||
ReasoningContent: aiReasoningContent,
|
||||
}
|
||||
messages = append(messages, accumulatedMessage)
|
||||
}
|
||||
|
||||
if len(toolCallsMap) > 0 {
|
||||
for _, toolCall := range toolCallsMap {
|
||||
currentToolCalls = append(currentToolCalls, *toolCall)
|
||||
}
|
||||
return currentToolCalls, messages, false, aiResponseContent, aiReasoningContent
|
||||
}
|
||||
|
||||
return currentToolCalls, messages, len(currentToolCalls) == 0, aiResponseContent, aiReasoningContent
|
||||
}
|
||||
|
||||
// executeToolCalls
|
||||
func (c *AIController) executeToolCalls(ctx *gin.Context, _ http.ResponseWriter, _, _ string, toolCalls []openai.ToolCall, messages []openai.ChatCompletionMessage, assistantContent, reasoningContent string) []openai.ChatCompletionMessage {
|
||||
validToolCalls := make([]openai.ToolCall, 0)
|
||||
for _, toolCall := range toolCalls {
|
||||
if toolCall.ID == "" || toolCall.Function.Name == "" {
|
||||
log.Errorf("Invalid tool call: missing required fields. ID: %s, Function: %v", toolCall.ID, toolCall.Function)
|
||||
continue
|
||||
}
|
||||
|
||||
if toolCall.Function.Arguments == "" {
|
||||
toolCall.Function.Arguments = "{}"
|
||||
}
|
||||
|
||||
validToolCalls = append(validToolCalls, toolCall)
|
||||
log.Debugf("Valid tool call: ID=%s, Name=%s, Arguments=%s", toolCall.ID, toolCall.Function.Name, toolCall.Function.Arguments)
|
||||
}
|
||||
|
||||
if len(validToolCalls) == 0 {
|
||||
log.Warn("No valid tool calls found")
|
||||
return messages
|
||||
}
|
||||
|
||||
assistantMsg := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
Content: assistantContent,
|
||||
ReasoningContent: reasoningContent,
|
||||
ToolCalls: validToolCalls,
|
||||
}
|
||||
messages = append(messages, assistantMsg)
|
||||
|
||||
for _, toolCall := range validToolCalls {
|
||||
if toolCall.Function.Name != "" {
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
|
||||
log.Errorf("Failed to parse tool arguments for %s: %v, arguments: %s", toolCall.Function.Name, err, toolCall.Function.Arguments)
|
||||
errorResult := fmt.Sprintf("Error parsing tool arguments: %v", err)
|
||||
toolMessage := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: errorResult,
|
||||
ToolCallID: toolCall.ID,
|
||||
}
|
||||
messages = append(messages, toolMessage)
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := c.callMCPTool(ctx, toolCall.Function.Name, args)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to call MCP tool %s: %v", toolCall.Function.Name, err)
|
||||
result = fmt.Sprintf("Error calling tool %s: %v", toolCall.Function.Name, err)
|
||||
}
|
||||
|
||||
toolMessage := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: result,
|
||||
ToolCallID: toolCall.ID,
|
||||
}
|
||||
messages = append(messages, toolMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// sendErrorResponse send error response in stream
|
||||
func (c *AIController) sendErrorResponse(w http.ResponseWriter, id, model, errorMsg string) {
|
||||
errorResponse := StreamResponse{
|
||||
ChatCompletionID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: time.Now().Unix(),
|
||||
Model: model,
|
||||
Choices: []StreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: Delta{
|
||||
Content: fmt.Sprintf("Error: %s", errorMsg),
|
||||
},
|
||||
FinishReason: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
sendStreamData(w, errorResponse)
|
||||
}
|
||||
|
||||
// getMCPTools
|
||||
func (c *AIController) getMCPTools() []openai.Tool {
|
||||
openaiTools := make([]openai.Tool, 0)
|
||||
for _, mcpTool := range mcp_tools.MCPToolsList {
|
||||
openaiTool := c.convertMCPToolToOpenAI(mcpTool)
|
||||
openaiTools = append(openaiTools, openaiTool)
|
||||
}
|
||||
|
||||
return openaiTools
|
||||
}
|
||||
|
||||
// convertMCPToolToOpenAI
|
||||
func (c *AIController) convertMCPToolToOpenAI(mcpTool mcp.Tool) openai.Tool {
|
||||
properties := make(map[string]any)
|
||||
required := make([]string, 0)
|
||||
|
||||
maps.Copy(properties, mcpTool.InputSchema.Properties)
|
||||
|
||||
required = append(required, mcpTool.InputSchema.Required...)
|
||||
|
||||
parameters := map[string]any{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
}
|
||||
|
||||
if len(required) > 0 {
|
||||
parameters["required"] = required
|
||||
}
|
||||
|
||||
return openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: mcpTool.Name,
|
||||
Description: mcpTool.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// callMCPTool
|
||||
func (c *AIController) callMCPTool(ctx context.Context, toolName string, arguments map[string]any) (string, error) {
|
||||
request := mcp.CallToolRequest{
|
||||
Request: mcp.Request{},
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments any `json:"arguments,omitempty"`
|
||||
Meta *mcp.Meta `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: toolName,
|
||||
Arguments: arguments,
|
||||
},
|
||||
}
|
||||
|
||||
var result *mcp.CallToolResult
|
||||
var err error
|
||||
|
||||
log.Debugf("Calling MCP tool: %s with arguments: %v", toolName, arguments)
|
||||
|
||||
switch toolName {
|
||||
case "get_questions":
|
||||
result, err = c.mcpController.MCPQuestionsHandler()(ctx, request)
|
||||
case "get_answers_by_question_id":
|
||||
result, err = c.mcpController.MCPAnswersHandler()(ctx, request)
|
||||
case "get_comments":
|
||||
result, err = c.mcpController.MCPCommentsHandler()(ctx, request)
|
||||
case "get_tags":
|
||||
result, err = c.mcpController.MCPTagsHandler()(ctx, request)
|
||||
case "get_tag_detail":
|
||||
result, err = c.mcpController.MCPTagDetailsHandler()(ctx, request)
|
||||
case "get_user":
|
||||
result, err = c.mcpController.MCPUserDetailsHandler()(ctx, request)
|
||||
case "semantic_search":
|
||||
result, err = c.mcpController.MCPSemanticSearchHandler()(ctx, request)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown tool: %s", toolName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(result)
|
||||
log.Debugf("MCP tool %s called successfully, result: %v", toolName, string(data))
|
||||
|
||||
if result != nil && len(result.Content) > 0 {
|
||||
if textContent, ok := result.Content[0].(mcp.TextContent); ok {
|
||||
return textContent.Text, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "No result found", nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/ai_conversation"
|
||||
"github.com/apache/answer/internal/service/feature_toggle"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AIConversationController ai conversation controller
|
||||
type AIConversationController struct {
|
||||
aiConversationService ai_conversation.AIConversationService
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService
|
||||
}
|
||||
|
||||
// NewAIConversationController creates a new AI conversation controller
|
||||
func NewAIConversationController(
|
||||
aiConversationService ai_conversation.AIConversationService,
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService,
|
||||
) *AIConversationController {
|
||||
return &AIConversationController{
|
||||
aiConversationService: aiConversationService,
|
||||
featureToggleSvc: featureToggleSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *AIConversationController) ensureEnabled(ctx *gin.Context) bool {
|
||||
if ctrl.featureToggleSvc == nil {
|
||||
return true
|
||||
}
|
||||
if err := ctrl.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetConversationList gets conversation list
|
||||
// @Summary get conversation list
|
||||
// @Description get conversation list
|
||||
// @Tags ai-conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "page"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.AIConversationListItem}}
|
||||
// @Router /answer/api/v1/ai/conversation/page [get]
|
||||
func (ctrl *AIConversationController) GetConversationList(ctx *gin.Context) {
|
||||
if !ctrl.ensureEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
req := &schema.AIConversationListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := ctrl.aiConversationService.GetConversationList(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetConversationDetail gets conversation detail
|
||||
// @Summary get conversation detail
|
||||
// @Description get conversation detail
|
||||
// @Tags ai-conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param conversation_id query string true "conversation id"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.AIConversationDetailResp}
|
||||
// @Router /answer/api/v1/ai/conversation [get]
|
||||
func (ctrl *AIConversationController) GetConversationDetail(ctx *gin.Context) {
|
||||
if !ctrl.ensureEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
req := &schema.AIConversationDetailReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, _, err := ctrl.aiConversationService.GetConversationDetail(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// VoteRecord vote record
|
||||
// @Summary vote record
|
||||
// @Description vote record
|
||||
// @Tags ai-conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.AIConversationVoteReq true "vote request"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/ai/conversation/vote [post]
|
||||
func (ctrl *AIConversationController) VoteRecord(ctx *gin.Context) {
|
||||
if !ctrl.ensureEnabled(ctx) {
|
||||
return
|
||||
}
|
||||
req := &schema.AIConversationVoteReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := ctrl.aiConversationService.VoteRecord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// AnswerController answer controller
|
||||
type AnswerController struct {
|
||||
answerService *content.AnswerService
|
||||
rankService *rank.RankService
|
||||
actionService *action.CaptchaService
|
||||
siteInfoCommonService siteinfo_common.SiteInfoCommonService
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
}
|
||||
|
||||
// NewAnswerController new controller
|
||||
func NewAnswerController(
|
||||
answerService *content.AnswerService,
|
||||
rankService *rank.RankService,
|
||||
actionService *action.CaptchaService,
|
||||
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
) *AnswerController {
|
||||
return &AnswerController{
|
||||
answerService: answerService,
|
||||
rankService: rankService,
|
||||
actionService: actionService,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
// @Summary delete answer
|
||||
// @Description delete answer
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.RemoveAnswerReq true "answer"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/answer [delete]
|
||||
func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
|
||||
req := &schema.RemoveAnswerReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanDelete = canList[0] || objectOwner
|
||||
if !req.CanDelete {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = ac.answerService.RemoveAnswer(ctx, req)
|
||||
if !isAdmin {
|
||||
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// RecoverAnswer recover answer
|
||||
// @Summary recover answer
|
||||
// @Description recover the deleted answer
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.RecoverAnswerReq true "answer"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/answer/recover [post]
|
||||
func (ac *AnswerController) RecoverAnswer(ctx *gin.Context) {
|
||||
req := &schema.RecoverAnswerReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.AnswerID = uid.DeShortID(req.AnswerID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = ac.answerService.RecoverAnswer(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetAnswerInfo get answer info
|
||||
// @Summary Get Answer Detail
|
||||
// @Description Get Answer Detail
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetAnswerInfoResp}
|
||||
// @Router /answer/api/v1/answer/info [get]
|
||||
func (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) {
|
||||
id := ctx.Query("id")
|
||||
id = uid.DeShortID(id)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdminModerator := middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID, isAdminModerator)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
handler.HandleResponse(ctx, fmt.Errorf(""), gin.H{})
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, &schema.GetAnswerInfoResp{
|
||||
Info: info,
|
||||
Question: questionInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// AddAnswer add answer
|
||||
// @Summary Add Answer
|
||||
// @Description add answer
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AnswerAddReq true "add answer request"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/answer [post]
|
||||
func (ac *AnswerController) AddAnswer(ctx *gin.Context) {
|
||||
req := &schema.AnswerAddReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
reject, rejectKey := ac.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)
|
||||
if reject {
|
||||
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 {
|
||||
ac.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)
|
||||
}
|
||||
}()
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerEdit,
|
||||
permission.AnswerDelete,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
linkUrlLimitUser := canList[2]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionAnswer, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
write, err := ac.siteInfoCommonService.GetSiteQuestion(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if write.RestrictAnswer {
|
||||
// check if there's already an answer by this user
|
||||
ids, err := ac.answerService.GetCountByUserIDQuestionID(ctx, req.UserID, req.QuestionID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if len(ids) >= 1 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.AnswerRestrictAnswer), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req.UserAgent = ctx.GetHeader("User-Agent")
|
||||
req.IP = ctx.ClientIP()
|
||||
|
||||
answerID, err := ac.answerService.Insert(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID)
|
||||
}
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID)
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
req.CanDelete = canList[1] || objectOwner
|
||||
info.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, info.UserID,
|
||||
0, req.CanEdit, req.CanDelete, false)
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"info": info,
|
||||
"question": questionInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAnswer update answer
|
||||
// @Summary Update Answer
|
||||
// @Description Update Answer
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AnswerUpdateReq true "AnswerUpdateReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/answer [put]
|
||||
func (ac *AnswerController) UpdateAnswer(ctx *gin.Context) {
|
||||
req := &schema.AnswerUpdateReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerEdit,
|
||||
permission.AnswerEditWithoutReview,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
linkUrlLimitUser := canList[2]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
req.NoNeedReview = canList[1] || objectOwner
|
||||
if !req.CanEdit {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ac.answerService.Update(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)
|
||||
}
|
||||
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview})
|
||||
}
|
||||
|
||||
// AnswerList godoc
|
||||
// @Summary AnswerList
|
||||
// @Description AnswerList <br> <b>order</b> (default or updated)
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param question_id query string true "question_id"
|
||||
// @Param order query string true "order"
|
||||
// @Param page query string true "page"
|
||||
// @Param page_size query string true "page_size"
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/answer/page [get]
|
||||
func (ac *AnswerController) AnswerList(ctx *gin.Context) {
|
||||
req := &schema.AnswerListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerEdit,
|
||||
permission.AnswerDelete,
|
||||
permission.AnswerUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = canList[0]
|
||||
req.CanDelete = canList[1]
|
||||
req.CanRecover = canList[2]
|
||||
|
||||
list, count, err := ac.answerService.SearchList(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// AcceptAnswer accept answer
|
||||
// @Summary Accept Answer
|
||||
// @Description Accept Answer
|
||||
// @Tags Answer
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AcceptAnswerReq true "AcceptAnswerReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/answer/acceptance [post]
|
||||
func (ac *AnswerController) AcceptAnswer(ctx *gin.Context) {
|
||||
req := &schema.AcceptAnswerReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.AnswerID = uid.DeShortID(req.AnswerID)
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = ac.answerService.AcceptAnswer(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// AdminUpdateAnswerStatus update answer status
|
||||
// @Summary update answer status
|
||||
// @Description update answer status
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AdminUpdateAnswerStatusReq true "AdminUpdateAnswerStatusReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/answer/status [put]
|
||||
func (ac *AnswerController) AdminUpdateAnswerStatus(ctx *gin.Context) {
|
||||
req := &schema.AdminUpdateAnswerStatusReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.AnswerID = uid.DeShortID(req.AnswerID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := ac.answerService.AdminSetAnswerStatus(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/badge"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BadgeController struct {
|
||||
badgeService *badge.BadgeService
|
||||
badgeAwardService *badge.BadgeAwardService
|
||||
}
|
||||
|
||||
func NewBadgeController(
|
||||
badgeService *badge.BadgeService,
|
||||
badgeAwardService *badge.BadgeAwardService) *BadgeController {
|
||||
return &BadgeController{
|
||||
badgeService: badgeService,
|
||||
badgeAwardService: badgeAwardService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBadgeList list all badges
|
||||
// @Summary list all badges group by group
|
||||
// @Description list all badges group by group
|
||||
// @Tags api-badge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListResp}
|
||||
// @Router /answer/api/v1/badges [get]
|
||||
func (b *BadgeController) GetBadgeList(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := b.badgeService.ListByGroup(ctx, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetBadgeInfo get badge info
|
||||
// @Summary get badge info
|
||||
// @Description get badge info
|
||||
// @Tags api-badge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "id" default(string)
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp}
|
||||
// @Router /answer/api/v1/badge [get]
|
||||
func (b *BadgeController) GetBadgeInfo(ctx *gin.Context) {
|
||||
id := ctx.Query("id")
|
||||
id = uid.DeShortID(id)
|
||||
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := b.badgeService.GetBadgeInfo(ctx, id, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetBadgeAwardList get badge award list
|
||||
// @Summary get badge award list
|
||||
// @Description get badge award list
|
||||
// @Tags api-badge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "page"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param badge_id query string true "badge id"
|
||||
// @Param username query string false "only list the award by username"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp}
|
||||
// @Router /answer/api/v1/badge/awards/page [get]
|
||||
func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) {
|
||||
req := &schema.GetBadgeAwardWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.BadgeID = uid.DeShortID(req.BadgeID)
|
||||
|
||||
resp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))
|
||||
}
|
||||
|
||||
// GetAllBadgeAwardListByUsername get user badge award list
|
||||
// @Summary get user badge award list
|
||||
// @Description get user badge award list
|
||||
// @Tags api-badge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string true "user name"
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp}
|
||||
// @Router /answer/api/v1/badge/user/awards [get]
|
||||
func (b *BadgeController) GetAllBadgeAwardListByUsername(ctx *gin.Context) {
|
||||
req := &schema.GetUserBadgeAwardListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, total, err := b.badgeAwardService.GetUserBadgeAwardList(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))
|
||||
}
|
||||
|
||||
// GetRecentBadgeAwardListByUsername get user badge award list
|
||||
// @Summary get user badge award list
|
||||
// @Description get user badge award list
|
||||
// @Tags api-badge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string true "user name"
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp}
|
||||
// @Router /answer/api/v1/badge/user/awards/recent [get]
|
||||
func (b *BadgeController) GetRecentBadgeAwardListByUsername(ctx *gin.Context) {
|
||||
req := &schema.GetUserBadgeAwardListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Limit = 10
|
||||
|
||||
resp, total, err := b.badgeAwardService.GetUserRecentBadgeAwardList(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/collection"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CollectionController collection controller
|
||||
type CollectionController struct {
|
||||
collectionService *collection.CollectionService
|
||||
}
|
||||
|
||||
// NewCollectionController new controller
|
||||
func NewCollectionController(collectionService *collection.CollectionService) *CollectionController {
|
||||
return &CollectionController{collectionService: collectionService}
|
||||
}
|
||||
|
||||
// CollectionSwitch add collection
|
||||
// @Summary add collection
|
||||
// @Description add collection
|
||||
// @Tags Collection
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.CollectionSwitchReq true "collection"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.CollectionSwitchResp}
|
||||
// @Router /answer/api/v1/collection/switch [post]
|
||||
func (cc *CollectionController) CollectionSwitch(ctx *gin.Context) {
|
||||
req := &schema.CollectionSwitchReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := cc.collectionService.CollectionSwitch(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/comment"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// CommentController comment controller
|
||||
type CommentController struct {
|
||||
commentService *comment.CommentService
|
||||
rankService *rank.RankService
|
||||
actionService *action.CaptchaService
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
}
|
||||
|
||||
// NewCommentController new controller
|
||||
func NewCommentController(
|
||||
commentService *comment.CommentService,
|
||||
rankService *rank.RankService,
|
||||
actionService *action.CaptchaService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
) *CommentController {
|
||||
return &CommentController{
|
||||
commentService: commentService,
|
||||
rankService: rankService,
|
||||
actionService: actionService,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
// AddComment add comment
|
||||
// @Summary add comment
|
||||
// @Description add comment
|
||||
// @Tags Comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AddCommentReq true "comment"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetCommentResp}
|
||||
// @Router /answer/api/v1/comment [post]
|
||||
func (cc *CommentController) AddComment(ctx *gin.Context) {
|
||||
req := &schema.AddCommentReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
reject, rejectKey := cc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)
|
||||
if reject {
|
||||
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 {
|
||||
cc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)
|
||||
}
|
||||
}()
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentAdd,
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
linkUrlLimitUser := canList[3]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionComment, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
req.CanAdd = canList[0]
|
||||
req.CanEdit = canList[1]
|
||||
req.CanDelete = canList[2]
|
||||
if !req.CanAdd {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
req.UserAgent = ctx.GetHeader("User-Agent")
|
||||
req.IP = ctx.ClientIP()
|
||||
|
||||
resp, err := cc.commentService.AddComment(ctx, req)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// RemoveComment remove comment
|
||||
// @Summary remove comment
|
||||
// @Description remove comment
|
||||
// @Tags Comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.RemoveCommentReq true "comment"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/comment [delete]
|
||||
func (cc *CommentController) RemoveComment(ctx *gin.Context) {
|
||||
req := &schema.RemoveCommentReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = cc.commentService.RemoveComment(ctx, req)
|
||||
if !isAdmin {
|
||||
cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateComment update comment
|
||||
// @Summary update comment
|
||||
// @Description update comment
|
||||
// @Tags Comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdateCommentReq true "comment"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/comment [put]
|
||||
func (cc *CommentController) UpdateComment(ctx *gin.Context) {
|
||||
req := &schema.UpdateCommentReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = canList[0] || cc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.CommentID)
|
||||
linkUrlLimitUser := canList[1]
|
||||
if !req.CanEdit {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsAdmin || !linkUrlLimitUser {
|
||||
captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := cc.commentService.UpdateComment(ctx, req)
|
||||
if !req.IsAdmin || !linkUrlLimitUser {
|
||||
cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetCommentWithPage get comment page
|
||||
// @Summary get comment page
|
||||
// @Description get comment page
|
||||
// @Tags Comment
|
||||
// @Produce json
|
||||
// @Param page query int false "page"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param object_id query string true "object id"
|
||||
// @Param query_cond query string false "query condition" Enums(vote)
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}
|
||||
// @Router /answer/api/v1/comment/page [get]
|
||||
func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
|
||||
req := &schema.GetCommentWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.CommentID = uid.DeShortID(req.CommentID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = canList[0]
|
||||
req.CanDelete = canList[1]
|
||||
|
||||
resp, err := cc.commentService.GetCommentWithPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetCommentPersonalWithPage user personal comment list
|
||||
// @Summary user personal comment list
|
||||
// @Description user personal comment list
|
||||
// @Tags Comment
|
||||
// @Produce json
|
||||
// @Param page query int false "page"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param username query string false "username"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentPersonalWithPageResp}}
|
||||
// @Router /answer/api/v1/personal/comment/page [get]
|
||||
func (cc *CommentController) GetCommentPersonalWithPage(ctx *gin.Context) {
|
||||
req := &schema.GetCommentPersonalWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := cc.commentService.GetCommentPersonalWithPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetComment godoc
|
||||
// @Summary get comment by id
|
||||
// @Description get comment by id
|
||||
// @Tags Comment
|
||||
// @Produce json
|
||||
// @Param id query string true "id"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}
|
||||
// @Router /answer/api/v1/comment [get]
|
||||
func (cc *CommentController) GetComment(ctx *gin.Context) {
|
||||
req := &schema.GetCommentReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = canList[0]
|
||||
req.CanDelete = canList[1]
|
||||
|
||||
resp, err := cc.commentService.GetComment(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/export"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/internal/service/user_external_login"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const (
|
||||
commonRouterPrefix = "/answer/api/v1"
|
||||
ConnectorLoginRouterPrefix = "/connector/login/"
|
||||
ConnectorRedirectRouterPrefix = "/connector/redirect/"
|
||||
)
|
||||
|
||||
// ConnectorController comment controller
|
||||
type ConnectorController struct {
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
userExternalService *user_external_login.UserExternalLoginService
|
||||
emailService *export.EmailService
|
||||
}
|
||||
|
||||
// NewConnectorController new controller
|
||||
func NewConnectorController(
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
emailService *export.EmailService,
|
||||
userExternalService *user_external_login.UserExternalLoginService,
|
||||
) *ConnectorController {
|
||||
return &ConnectorController{
|
||||
siteInfoService: siteInfoService,
|
||||
userExternalService: userExternalService,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name
|
||||
// We can't register specific router for each connector when application start, because the plugin status will be changed by admin.
|
||||
// If the plugin is disabled, the router should be unavailable.
|
||||
func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) {
|
||||
slugName := ctx.Param("name")
|
||||
var c plugin.Connector
|
||||
_ = plugin.CallConnector(func(connector plugin.Connector) error {
|
||||
if connector.ConnectorSlugName() == slugName {
|
||||
c = connector
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if c == nil {
|
||||
log.Errorf("connector %s not found", slugName)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
cc.ConnectorLogin(c)(ctx)
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) {
|
||||
slugName := ctx.Param("name")
|
||||
var c plugin.Connector
|
||||
_ = plugin.CallConnector(func(connector plugin.Connector) error {
|
||||
if connector.ConnectorSlugName() == slugName {
|
||||
c = connector
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if c == nil {
|
||||
log.Errorf("connector %s not found", slugName)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
cc.ConnectorRedirect(c)(ctx)
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) {
|
||||
return func(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
state := ctx.Query("state")
|
||||
if len(state) > 0 {
|
||||
stateInfo, err := cc.userExternalService.GetOAuthState(ctx, state)
|
||||
if err != nil || stateInfo == nil || stateInfo.Provider != connector.ConnectorSlugName() {
|
||||
log.Errorf("invalid connector oauth state for provider %s", connector.ConnectorSlugName())
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
state, err = cc.userExternalService.GenerateOAuthState(ctx, connector.ConnectorSlugName(),
|
||||
schema.ExternalLoginOAuthStateLoginIntent, "")
|
||||
if err != nil {
|
||||
log.Errorf("generate connector oauth state failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
q := ctx.Request.URL.Query()
|
||||
q.Set("state", state)
|
||||
ctx.Request.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl,
|
||||
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
|
||||
redirectURL := connector.ConnectorSender(ctx, receiverURL)
|
||||
if len(redirectURL) > 0 {
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {
|
||||
return func(ctx *gin.Context) {
|
||||
siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
|
||||
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
|
||||
if err != nil {
|
||||
log.Errorf("connector received failed, error info: %v, response data is: %s", err, userInfo.MetaInfo)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
log.Debugf("connector received: %+v", userInfo)
|
||||
u := &schema.ExternalLoginUserInfoCache{
|
||||
Provider: connector.ConnectorSlugName(),
|
||||
ExternalID: userInfo.ExternalID,
|
||||
DisplayName: userInfo.DisplayName,
|
||||
Username: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
Avatar: userInfo.Avatar,
|
||||
MetaInfo: userInfo.MetaInfo,
|
||||
}
|
||||
stateInfo, err := cc.userExternalService.ConsumeOAuthState(ctx, ctx.Query("state"))
|
||||
if err != nil {
|
||||
log.Errorf("get connector oauth state failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if stateInfo != nil && stateInfo.Provider != connector.ConnectorSlugName() {
|
||||
log.Errorf("connector oauth state provider mismatch: %s != %s",
|
||||
stateInfo.Provider, connector.ConnectorSlugName())
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if stateInfo != nil && stateInfo.Intent == schema.ExternalLoginOAuthStateBindIntent {
|
||||
if err = cc.userExternalService.BindExternalLoginToUser(ctx, stateInfo.UserID, u); err != nil {
|
||||
log.Errorf("bind external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/settings/account", siteGeneral.SiteUrl))
|
||||
return
|
||||
}
|
||||
resp, err := cc.userExternalService.ExternalLogin(ctx, u)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.ErrMsg) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
|
||||
return
|
||||
}
|
||||
if len(resp.AccessToken) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
} else {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s",
|
||||
siteGeneral.SiteUrl, resp.BindingKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectorsInfo get all enabled connectors
|
||||
// @Summary get all enabled connectors
|
||||
// @Description get all enabled connectors
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp}
|
||||
// @Router /answer/api/v1/connector/info [get]
|
||||
func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]*schema.ConnectorInfoResp, 0)
|
||||
_ = plugin.CallConnector(func(fn plugin.Connector) error {
|
||||
connectorName := fn.ConnectorName()
|
||||
resp = append(resp, &schema.ConnectorInfoResp{
|
||||
Name: connectorName.Translate(ctx),
|
||||
Icon: fn.ConnectorLogoSVG(),
|
||||
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
|
||||
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserSendEmail external login binding user send email
|
||||
// @Summary external login binding user send email
|
||||
// @Description external login binding user send email
|
||||
// @Tags PluginConnector
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp}
|
||||
// @Router /answer/api/v1/connector/binding/email [post]
|
||||
func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {
|
||||
req := &schema.ExternalLoginBindingUserSendEmailReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// ConnectorsUserInfo get all connectors info about user
|
||||
// @Summary get all connectors info about user
|
||||
// @Description get all connectors info about user
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp}
|
||||
// @Router /answer/api/v1/connector/user/info [get]
|
||||
func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
userExternalLoginMapping := make(map[string]string)
|
||||
for _, userInfo := range userInfoList {
|
||||
userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID
|
||||
}
|
||||
|
||||
resp := make([]*schema.ConnectorUserInfoResp, 0)
|
||||
err = plugin.CallConnector(func(fn plugin.Connector) error {
|
||||
externalID := userExternalLoginMapping[fn.ConnectorSlugName()]
|
||||
connectorName := fn.ConnectorName()
|
||||
link := fmt.Sprintf("%s%s%s%s", general.SiteUrl,
|
||||
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName())
|
||||
if len(externalID) == 0 {
|
||||
state, err := cc.userExternalService.GenerateOAuthState(ctx, fn.ConnectorSlugName(),
|
||||
schema.ExternalLoginOAuthStateBindIntent, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link = fmt.Sprintf("%s?state=%s", link, url.QueryEscape(state))
|
||||
}
|
||||
resp = append(resp, &schema.ConnectorUserInfoResp{
|
||||
Name: connectorName.Translate(ctx),
|
||||
Icon: fn.ConnectorLogoSVG(),
|
||||
Link: link,
|
||||
Binding: len(externalID) > 0,
|
||||
ExternalID: externalID,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// ExternalLoginUnbinding unbind external user login
|
||||
// @Summary unbind external user login
|
||||
// @Description unbind external user login
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/connector/user/unbinding [delete]
|
||||
func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) {
|
||||
req := &schema.ExternalLoginUnbindingReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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/google/wire"
|
||||
|
||||
// ProviderSetController is controller providers.
|
||||
var ProviderSetController = wire.NewSet(
|
||||
NewLangController,
|
||||
NewCommentController,
|
||||
NewReportController,
|
||||
NewVoteController,
|
||||
NewTagController,
|
||||
NewFollowController,
|
||||
NewCollectionController,
|
||||
NewUserController,
|
||||
NewQuestionController,
|
||||
NewAnswerController,
|
||||
NewSearchController,
|
||||
NewRevisionController,
|
||||
NewRankController,
|
||||
NewReasonController,
|
||||
NewNotificationController,
|
||||
NewSiteInfoController,
|
||||
NewDashboardController,
|
||||
NewUploadController,
|
||||
NewActivityController,
|
||||
NewTemplateController,
|
||||
NewConnectorController,
|
||||
NewUserCenterController,
|
||||
NewPermissionController,
|
||||
NewUserPluginController,
|
||||
NewReviewController,
|
||||
NewCaptchaController,
|
||||
NewMetaController,
|
||||
NewEmbedController,
|
||||
NewBadgeController,
|
||||
NewRenderController,
|
||||
NewSidebarController,
|
||||
NewMCPController,
|
||||
NewAIController,
|
||||
NewAIConversationController,
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/service/dashboard"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DashboardController struct {
|
||||
dashboardService dashboard.DashboardService
|
||||
}
|
||||
|
||||
// NewDashboardController new controller
|
||||
func NewDashboardController(
|
||||
dashboardService dashboard.DashboardService,
|
||||
) *DashboardController {
|
||||
return &DashboardController{
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardInfo godoc
|
||||
// @Summary DashboardInfo
|
||||
// @Description DashboardInfo
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /answer/admin/api/dashboard [get]
|
||||
// @Success 200 {object} handler.RespBody
|
||||
func (ac *DashboardController) DashboardInfo(ctx *gin.Context) {
|
||||
info, err := ac.dashboardService.Statistical(ctx)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"info": info,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type EmbedController struct {
|
||||
}
|
||||
|
||||
func NewEmbedController() *EmbedController {
|
||||
return &EmbedController{}
|
||||
}
|
||||
|
||||
// GetEmbedConfig get embed plugin config
|
||||
// @Summary get embed plugin config
|
||||
// @Description get embed plugin config
|
||||
// @Tags Plugin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]plugin.EmbedConfig}
|
||||
// @Router /answer/api/v1/embed/config [get]
|
||||
func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) {
|
||||
resp := make([]*plugin.EmbedConfig, 0)
|
||||
|
||||
err := plugin.CallEmbed(func(embed plugin.Embed) (err error) {
|
||||
resp, err = embed.GetEmbedConfigs(ctx)
|
||||
return err
|
||||
})
|
||||
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/follow"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
// FollowController activity controller
|
||||
type FollowController struct {
|
||||
followService *follow.FollowService
|
||||
}
|
||||
|
||||
// NewFollowController new controller
|
||||
func NewFollowController(followService *follow.FollowService) *FollowController {
|
||||
return &FollowController{followService: followService}
|
||||
}
|
||||
|
||||
// Follow godoc
|
||||
// @Summary follow object or cancel follow operation
|
||||
// @Description follow object or cancel follow operation
|
||||
// @Tags Activity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.FollowReq true "follow"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.FollowResp}
|
||||
// @Router /answer/api/v1/follow [post]
|
||||
func (fc *FollowController) Follow(ctx *gin.Context) {
|
||||
req := &schema.FollowReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
dto := &schema.FollowDTO{}
|
||||
_ = copier.Copy(dto, req)
|
||||
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := fc.followService.Follow(ctx, dto)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateFollowTags update user follow tags
|
||||
// @Summary update user follow tags
|
||||
// @Description update user follow tags
|
||||
// @Tags Activity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdateFollowTagsReq true "follow"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/follow/tags [put]
|
||||
func (fc *FollowController) UpdateFollowTags(ctx *gin.Context) {
|
||||
req := &schema.UpdateFollowTagsReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := fc.followService.UpdateFollowTags(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
type LangController struct {
|
||||
translator i18n.Translator
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewLangController new language controller.
|
||||
func NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController {
|
||||
return &LangController{translator: tr, siteInfoService: siteInfoService}
|
||||
}
|
||||
|
||||
// GetLangMapping get language config mapping
|
||||
// @Summary get language config mapping
|
||||
// @Description get language config mapping
|
||||
// @Tags Lang
|
||||
// @Param Accept-Language header string true "Accept-Language"
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/config [get]
|
||||
func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
||||
data, _ := u.translator.Dump(handler.GetLangByCtx(ctx))
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(data, &resp)
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetAdminLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/language/options [get]
|
||||
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// GetUserLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
|
||||
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
options := translator.LanguageOptions
|
||||
if len(siteInterfaceResp.Language) > 0 {
|
||||
defaultOption := []*translator.LangOption{
|
||||
{Label: translator.DefaultLangOption, Value: translator.DefaultLangOption},
|
||||
}
|
||||
options = append(defaultOption, options...)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, options)
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
"github.com/apache/answer/internal/service/comment"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/embedding"
|
||||
"github.com/apache/answer/internal/service/feature_toggle"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
tagcommonser "github.com/apache/answer/internal/service/tag_common"
|
||||
usercommon "github.com/apache/answer/internal/service/user_common"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type MCPController struct {
|
||||
searchService *content.SearchService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
tagCommonService *tagcommonser.TagCommonService
|
||||
questioncommon *questioncommon.QuestionCommon
|
||||
commentRepo comment.CommentRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
answerRepo answercommon.AnswerRepo
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService
|
||||
embeddingService *embedding.EmbeddingService
|
||||
}
|
||||
|
||||
// NewMCPController new site info controller.
|
||||
func NewMCPController(
|
||||
searchService *content.SearchService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
tagCommonService *tagcommonser.TagCommonService,
|
||||
questioncommon *questioncommon.QuestionCommon,
|
||||
commentRepo comment.CommentRepo,
|
||||
userCommon *usercommon.UserCommon,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
featureToggleSvc *feature_toggle.FeatureToggleService,
|
||||
embeddingService *embedding.EmbeddingService,
|
||||
) *MCPController {
|
||||
return &MCPController{
|
||||
searchService: searchService,
|
||||
siteInfoService: siteInfoService,
|
||||
tagCommonService: tagCommonService,
|
||||
questioncommon: questioncommon,
|
||||
commentRepo: commentRepo,
|
||||
userCommon: userCommon,
|
||||
answerRepo: answerRepo,
|
||||
featureToggleSvc: featureToggleSvc,
|
||||
embeddingService: embeddingService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) ensureMCPEnabled(ctx context.Context) error {
|
||||
if c.featureToggleSvc == nil {
|
||||
return nil
|
||||
}
|
||||
return c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureMCP)
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPQuestionsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchResp, err := c.searchService.Search(ctx, &schema.SearchDTO{
|
||||
Query: cond.ToQueryString() + " is:question",
|
||||
Page: 1,
|
||||
Size: 5,
|
||||
Order: "newest",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := make([]*schema.MCPSearchQuestionInfoResp, 0)
|
||||
for _, question := range searchResp.SearchResults {
|
||||
t := &schema.MCPSearchQuestionInfoResp{
|
||||
QuestionID: question.Object.QuestionID,
|
||||
Title: question.Object.Title,
|
||||
Content: question.Object.Excerpt,
|
||||
Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.Object.QuestionID),
|
||||
}
|
||||
resp = append(resp, t)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPQuestionDetailHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchQuestionDetail(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
question, err := c.questioncommon.Info(ctx, cond.QuestionID, "")
|
||||
if err != nil {
|
||||
log.Errorf("get question failed: %v", err)
|
||||
return mcp.NewToolResultText("No question found."), nil
|
||||
}
|
||||
|
||||
resp := &schema.MCPSearchQuestionInfoResp{
|
||||
QuestionID: question.ID,
|
||||
Title: question.Title,
|
||||
Content: question.Content,
|
||||
Link: fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, question.ID),
|
||||
}
|
||||
res, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(res)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchAnswerCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cond.QuestionID) > 0 {
|
||||
answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID})
|
||||
if err != nil {
|
||||
log.Errorf("get answers failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*schema.MCPSearchAnswerInfoResp, 0)
|
||||
for _, answer := range answerList {
|
||||
if answer.Status != entity.AnswerStatusAvailable {
|
||||
continue
|
||||
}
|
||||
t := &schema.MCPSearchAnswerInfoResp{
|
||||
QuestionID: answer.QuestionID,
|
||||
AnswerID: answer.ID,
|
||||
AnswerContent: answer.OriginalText,
|
||||
Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID),
|
||||
}
|
||||
resp = append(resp, t)
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
|
||||
answerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID})
|
||||
if err != nil {
|
||||
log.Errorf("get answers failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*schema.MCPSearchAnswerInfoResp, 0)
|
||||
for _, answer := range answerList {
|
||||
if answer.Status != entity.AnswerStatusAvailable {
|
||||
continue
|
||||
}
|
||||
t := &schema.MCPSearchAnswerInfoResp{
|
||||
QuestionID: answer.QuestionID,
|
||||
AnswerID: answer.ID,
|
||||
AnswerContent: answer.OriginalText,
|
||||
Link: fmt.Sprintf("%s/questions/%s/answers/%s", siteGeneral.SiteUrl, answer.QuestionID, answer.ID),
|
||||
}
|
||||
resp = append(resp, t)
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPCommentsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchCommentCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dto := &comment.CommentQuery{
|
||||
PageCond: pager.PageCond{Page: 1, PageSize: 5},
|
||||
QueryCond: "newest",
|
||||
ObjectID: cond.ObjectID,
|
||||
}
|
||||
commentList, total, err := c.commentRepo.GetCommentPage(ctx, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return mcp.NewToolResultText("No comments found."), nil
|
||||
}
|
||||
|
||||
resp := make([]*schema.MCPSearchCommentInfoResp, 0)
|
||||
for _, comment := range commentList {
|
||||
t := &schema.MCPSearchCommentInfoResp{
|
||||
CommentID: comment.ID,
|
||||
Content: comment.OriginalText,
|
||||
ObjectID: comment.ObjectID,
|
||||
Link: fmt.Sprintf("%s/comments/%s", siteGeneral.SiteUrl, comment.ID),
|
||||
}
|
||||
resp = append(resp, t)
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPTagsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchTagCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, total, err := c.tagCommonService.GetTagPage(ctx, 1, 10, &entity.Tag{DisplayName: cond.TagName}, "newest")
|
||||
if err != nil {
|
||||
log.Errorf("get tags failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
res := strings.Builder{}
|
||||
res.WriteString("No tags found.\n")
|
||||
return mcp.NewToolResultText(res.String()), nil
|
||||
}
|
||||
|
||||
resp := make([]*schema.MCPSearchTagResp, 0)
|
||||
for _, tag := range tags {
|
||||
t := &schema.MCPSearchTagResp{
|
||||
TagName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
Description: tag.OriginalText,
|
||||
Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName),
|
||||
}
|
||||
resp = append(resp, t)
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPTagDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchTagCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag, exist, err := c.tagCommonService.GetTagBySlugName(ctx, cond.TagName)
|
||||
if err != nil {
|
||||
log.Errorf("get tag failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return mcp.NewToolResultText("Tag not found."), nil
|
||||
}
|
||||
|
||||
resp := &schema.MCPSearchTagResp{
|
||||
TagName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
Description: tag.OriginalText,
|
||||
Link: fmt.Sprintf("%s/tags/%s", siteGeneral.SiteUrl, tag.SlugName),
|
||||
}
|
||||
res, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(res)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSearchUserCond(request)
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, exist, err := c.userCommon.GetUserBasicInfoByUserName(ctx, cond.Username)
|
||||
if err != nil {
|
||||
log.Errorf("get user failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return mcp.NewToolResultText("User not found."), nil
|
||||
}
|
||||
|
||||
resp := &schema.MCPSearchUserInfoResp{
|
||||
Username: user.Username,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Link: fmt.Sprintf("%s/users/%s", siteGeneral.SiteUrl, user.Username),
|
||||
}
|
||||
res, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(res)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MCPController) MCPSemanticSearchHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if err := c.ensureMCPEnabled(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := schema.NewMCPSemanticSearchCond(request)
|
||||
if len(cond.Query) == 0 {
|
||||
return mcp.NewToolResultText("Query is required for semantic search."), nil
|
||||
}
|
||||
|
||||
siteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general info failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results, err := c.embeddingService.SearchSimilar(ctx, cond.Query, cond.TopK)
|
||||
if err != nil {
|
||||
log.Errorf("semantic search failed: %v", err)
|
||||
return mcp.NewToolResultText("Semantic search is not available. Embedding may not be configured."), nil
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return mcp.NewToolResultText("No semantically similar content found."), nil
|
||||
}
|
||||
|
||||
resp := make([]*schema.MCPSemanticSearchResp, 0, len(results))
|
||||
for _, r := range results {
|
||||
var meta plugin.VectorSearchMetadata
|
||||
_ = json.Unmarshal([]byte(r.Metadata), &meta)
|
||||
|
||||
item := &schema.MCPSemanticSearchResp{
|
||||
ObjectID: r.ObjectID,
|
||||
ObjectType: r.ObjectType,
|
||||
Score: r.Score,
|
||||
}
|
||||
|
||||
// Compose link from metadata
|
||||
if r.ObjectType == "answer" && meta.AnswerID != "" {
|
||||
item.Link = fmt.Sprintf("%s/questions/%s/%s", siteGeneral.SiteUrl, meta.QuestionID, meta.AnswerID)
|
||||
} else {
|
||||
item.Link = fmt.Sprintf("%s/questions/%s", siteGeneral.SiteUrl, meta.QuestionID)
|
||||
}
|
||||
|
||||
// Query content from DB using IDs stored in metadata
|
||||
if r.ObjectType == "question" {
|
||||
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
|
||||
if qErr != nil {
|
||||
log.Warnf("get question %s for semantic search failed: %v", meta.QuestionID, qErr)
|
||||
} else {
|
||||
item.Title = question.Title
|
||||
item.Content = question.Content
|
||||
}
|
||||
|
||||
// Fetch answers by ID from metadata
|
||||
for _, a := range meta.Answers {
|
||||
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, a.AnswerID)
|
||||
if aErr != nil || !exist {
|
||||
continue
|
||||
}
|
||||
answerItem := &schema.MCPSemanticSearchAnswer{
|
||||
AnswerID: a.AnswerID,
|
||||
Content: answerEntity.OriginalText,
|
||||
}
|
||||
// Fetch comments on this answer from DB
|
||||
for _, ac := range a.Comments {
|
||||
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
|
||||
if cErr == nil && cExist {
|
||||
answerItem.Comments = append(answerItem.Comments, &schema.MCPSemanticSearchComment{
|
||||
CommentID: ac.CommentID,
|
||||
Content: cmt.OriginalText,
|
||||
})
|
||||
}
|
||||
}
|
||||
item.Answers = append(item.Answers, answerItem)
|
||||
}
|
||||
|
||||
// Fetch question comments from DB
|
||||
for _, qc := range meta.Comments {
|
||||
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, qc.CommentID)
|
||||
if cErr == nil && cExist {
|
||||
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
|
||||
CommentID: qc.CommentID,
|
||||
Content: cmt.OriginalText,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if r.ObjectType == "answer" {
|
||||
// Fetch question title for context
|
||||
question, qErr := c.questioncommon.Info(ctx, meta.QuestionID, "")
|
||||
if qErr == nil {
|
||||
item.Title = question.Title
|
||||
}
|
||||
|
||||
// Fetch answer content from DB
|
||||
if meta.AnswerID != "" {
|
||||
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.AnswerID)
|
||||
if aErr == nil && exist {
|
||||
item.Content = answerEntity.OriginalText
|
||||
}
|
||||
} else if len(meta.Answers) > 0 {
|
||||
answerEntity, exist, aErr := c.answerRepo.GetAnswer(ctx, meta.Answers[0].AnswerID)
|
||||
if aErr == nil && exist {
|
||||
item.Content = answerEntity.OriginalText
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch answer comments from DB
|
||||
if len(meta.Answers) > 0 {
|
||||
for _, ac := range meta.Answers[0].Comments {
|
||||
cmt, cExist, cErr := c.commentRepo.GetComment(ctx, ac.CommentID)
|
||||
if cErr == nil && cExist {
|
||||
item.Comments = append(item.Comments, &schema.MCPSemanticSearchComment{
|
||||
CommentID: ac.CommentID,
|
||||
Content: cmt.OriginalText,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp = append(resp, item)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(resp)
|
||||
return mcp.NewToolResultText(string(data)), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/meta"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MetaController struct {
|
||||
metaService *meta.MetaService
|
||||
}
|
||||
|
||||
func NewMetaController(
|
||||
metaService *meta.MetaService,
|
||||
) *MetaController {
|
||||
return &MetaController{
|
||||
metaService: metaService,
|
||||
}
|
||||
}
|
||||
|
||||
// AddOrUpdateReaction add or update reaction
|
||||
// @Summary add or update reaction
|
||||
// @Description update reaction. if not exist, add one
|
||||
// @Tags Meta
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdateReactionReq true "reaction"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/meta/reaction [put]
|
||||
func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) {
|
||||
req := &schema.UpdateReactionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := mc.metaService.AddOrUpdateReaction(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetReaction get reaction
|
||||
// @Summary get reaction
|
||||
// @Description get reaction for an object
|
||||
// @Tags Meta
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param object_id query string true "object_id"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.ReactionRespItem}
|
||||
// @Router /answer/api/v1/meta/reaction [get]
|
||||
func (mc *MetaController) GetReaction(ctx *gin.Context) {
|
||||
req := &schema.GetReactionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := mc.metaService.GetReactionByObjectId(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/notification"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// NotificationController notification controller
|
||||
type NotificationController struct {
|
||||
notificationService *notification.NotificationService
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewNotificationController new controller
|
||||
func NewNotificationController(
|
||||
notificationService *notification.NotificationService,
|
||||
rankService *rank.RankService,
|
||||
) *NotificationController {
|
||||
return &NotificationController{
|
||||
notificationService: notificationService,
|
||||
rankService: rankService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRedDot
|
||||
// @Summary GetRedDot
|
||||
// @Description GetRedDot
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/status [get]
|
||||
func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
|
||||
req := &schema.GetRedDot{}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := nc.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 := nc.notificationService.GetRedDot(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// ClearRedDot
|
||||
// @Summary DelRedDot
|
||||
// @Description DelRedDot
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.NotificationClearRequest true "NotificationClearRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/status [put]
|
||||
func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
|
||||
req := &schema.NotificationClearRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := nc.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]
|
||||
|
||||
resp, err := nc.notificationService.ClearRedDot(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// ClearUnRead
|
||||
// @Summary ClearUnRead
|
||||
// @Description ClearUnRead
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.NotificationClearRequest true "NotificationClearRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/read/state/all [put]
|
||||
func (nc *NotificationController) ClearUnRead(ctx *gin.Context) {
|
||||
req := &schema.NotificationClearRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType)
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
}
|
||||
|
||||
// ClearIDUnRead
|
||||
// @Summary ClearUnRead
|
||||
// @Description ClearUnRead
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.NotificationClearIDRequest true "NotificationClearIDRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/read/state [put]
|
||||
func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {
|
||||
req := &schema.NotificationClearIDRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := nc.notificationService.ClearIDUnRead(ctx, userID, req.ID)
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
}
|
||||
|
||||
// GetList get notification list
|
||||
// @Summary get notification list
|
||||
// @Description get notification list
|
||||
// @Tags Notification
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query int false "page size"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param type query string true "type" Enums(inbox,achievement)
|
||||
// @Param inbox_type query string true "inbox_type" Enums(all,posts,invites,votes)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/page [get]
|
||||
func (nc *NotificationController) GetList(ctx *gin.Context) {
|
||||
req := &schema.NotificationSearch{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := nc.notificationService.GetNotificationPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PermissionController struct {
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewPermissionController new language controller.
|
||||
func NewPermissionController(rankService *rank.RankService) *PermissionController {
|
||||
return &PermissionController{rankService: rankService}
|
||||
}
|
||||
|
||||
// GetPermission check user permission
|
||||
// @Summary check user permission
|
||||
// @Description check user permission
|
||||
// @Tags Permission
|
||||
// @Security ApiKeyAuth
|
||||
// @Param Authorization header string true "access-token"
|
||||
// @Produce json
|
||||
// @Param action query string true "permission key" Enums(question.add, question.edit, question.edit_without_review, question.delete, question.close, question.reopen, question.vote_up, question.vote_down, question.pin, question.unpin, question.hide, question.show, answer.add, answer.edit, answer.edit_without_review, answer.delete, answer.accept, answer.vote_up, answer.vote_down, answer.invite_someone_to_answer, comment.add, comment.edit, comment.delete, comment.vote_up, comment.vote_down, report.add, tag.add, tag.edit, tag.edit_slug_name, tag.edit_without_review, tag.delete, tag.synonym, link.url_limit, vote.detail, answer.audit, question.audit, tag.audit, tag.use_reserved_tag)
|
||||
// @Success 200 {object} handler.RespBody{data=map[string]bool}
|
||||
// @Router /answer/api/v1/permission [get]
|
||||
func (u *PermissionController) GetPermission(ctx *gin.Context) {
|
||||
req := &schema.GetPermissionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
ops, requireRanks, err := u.rankService.CheckOperationPermissionsForRanks(ctx, userID, req.Actions)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
mapping := make(map[string]*schema.GetPermissionResp, len(ops))
|
||||
for i, action := range req.Actions {
|
||||
t := &schema.GetPermissionResp{HasPermission: ops[i]}
|
||||
t.TrTip(lang, requireRanks[i])
|
||||
mapping[action] = t
|
||||
}
|
||||
handler.HandleResponse(ctx, err, mapping)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CaptchaController comment controller
|
||||
type CaptchaController struct {
|
||||
}
|
||||
|
||||
// NewCaptchaController new controller
|
||||
func NewCaptchaController() *CaptchaController {
|
||||
return &CaptchaController{}
|
||||
}
|
||||
|
||||
type GetCaptchaConfigResp struct {
|
||||
SlugName string `json:"slug_name"`
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
// GetCaptchaConfig get captcha config
|
||||
func (uc *CaptchaController) GetCaptchaConfig(ctx *gin.Context) {
|
||||
resp := &GetCaptchaConfigResp{}
|
||||
_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {
|
||||
resp.SlugName = fn.Info().SlugName
|
||||
_ = json.Unmarshal([]byte(fn.GetConfig()), &resp.Config)
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SidebarController is the controller for the sidebar plugin.
|
||||
type SidebarController struct{}
|
||||
|
||||
// NewSidebarController creates a new instance of SidebarController.
|
||||
func NewSidebarController() *SidebarController {
|
||||
return &SidebarController{}
|
||||
}
|
||||
|
||||
// GetSidebarConfig retrieves the sidebar configuration from the registered sidebar plugins.
|
||||
func (uc *SidebarController) GetSidebarConfig(ctx *gin.Context) {
|
||||
resp := &plugin.SidebarConfig{}
|
||||
_ = plugin.CallSidebar(func(fn plugin.Sidebar) error {
|
||||
cfg, err := fn.GetSidebarConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp = cfg
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/internal/service/user_external_login"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const (
|
||||
UserCenterLoginRouter = "/user-center/login/redirect"
|
||||
UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect"
|
||||
)
|
||||
|
||||
// UserCenterController comment controller
|
||||
type UserCenterController struct {
|
||||
userCenterLoginService *user_external_login.UserCenterLoginService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewUserCenterController new controller
|
||||
func NewUserCenterController(
|
||||
userCenterLoginService *user_external_login.UserCenterLoginService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
) *UserCenterController {
|
||||
return &UserCenterController{
|
||||
userCenterLoginService: userCenterLoginService,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// UserCenterAgent get user center agent info
|
||||
func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
|
||||
resp := &schema.UserCenterAgentResp{}
|
||||
resp.Enabled = plugin.UserCenterEnabled()
|
||||
if !resp.Enabled {
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
return
|
||||
}
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
resp.AgentInfo = &schema.AgentInfo{}
|
||||
resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, UserCenterLoginRouter)
|
||||
resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, UserCenterSignUpRedirectRouter)
|
||||
|
||||
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
|
||||
info := uc.Description()
|
||||
resp.AgentInfo.Name = info.Name
|
||||
resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)
|
||||
resp.AgentInfo.Icon = info.Icon
|
||||
resp.AgentInfo.Url = info.Url
|
||||
resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)
|
||||
resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem
|
||||
items := uc.ControlCenterItems()
|
||||
for _, item := range items {
|
||||
resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{
|
||||
Name: item.Name,
|
||||
Label: item.Label,
|
||||
Url: item.Url,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// UserCenterPersonalBranding get user center personal user info
|
||||
func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
|
||||
var redirectURL string
|
||||
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
|
||||
info := userCenter.Description()
|
||||
redirectURL = info.LoginRedirectURL
|
||||
return nil
|
||||
})
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {
|
||||
var redirectURL string
|
||||
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
|
||||
info := userCenter.Description()
|
||||
redirectURL = info.LoginRedirectURL
|
||||
return nil
|
||||
})
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
ctx.Redirect(http.StatusFound, "/404")
|
||||
return
|
||||
}
|
||||
userInfo, err := userCenter.LoginCallback(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
if !ctx.IsAborted() {
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.ErrMsg) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
|
||||
return
|
||||
}
|
||||
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
ctx.Redirect(http.StatusFound, "/404")
|
||||
return
|
||||
}
|
||||
userInfo, err := userCenter.SignUpCallback(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.ErrMsg) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
|
||||
return
|
||||
}
|
||||
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
}
|
||||
|
||||
// UserCenterUserSettings user center user settings
|
||||
func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserCenterAdminFunctionAgent user center admin function agent
|
||||
func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {
|
||||
resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,999 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// QuestionController question controller
|
||||
type QuestionController struct {
|
||||
questionService *content.QuestionService
|
||||
answerService *content.AnswerService
|
||||
rankService *rank.RankService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
actionService *action.CaptchaService
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
}
|
||||
|
||||
// NewQuestionController new controller
|
||||
func NewQuestionController(
|
||||
questionService *content.QuestionService,
|
||||
answerService *content.AnswerService,
|
||||
rankService *rank.RankService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
actionService *action.CaptchaService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
) *QuestionController {
|
||||
return &QuestionController{
|
||||
questionService: questionService,
|
||||
answerService: answerService,
|
||||
rankService: rankService,
|
||||
siteInfoService: siteInfoService,
|
||||
actionService: actionService,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveQuestion delete question
|
||||
// @Summary delete question
|
||||
// @Description delete question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.RemoveQuestionReq true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question [delete]
|
||||
func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
|
||||
req := &schema.RemoveQuestionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
err = qc.questionService.RemoveQuestion(ctx, req)
|
||||
if !isAdmin {
|
||||
qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// OperationQuestion Operation question
|
||||
// @Summary Operation question
|
||||
// @Description Operation question \n operation [pin unpin hide show]
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.OperationQuestionReq true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/operation [put]
|
||||
func (qc *QuestionController) OperationQuestion(ctx *gin.Context) {
|
||||
req := &schema.OperationQuestionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.QuestionPin,
|
||||
permission.QuestionUnPin,
|
||||
permission.QuestionHide,
|
||||
permission.QuestionShow,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanPin = canList[0]
|
||||
req.CanList = canList[1]
|
||||
if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
err = qc.questionService.OperationQuestion(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// CloseQuestion Close question
|
||||
// @Summary Close question
|
||||
// @Description Close question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.CloseQuestionReq true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/status [put]
|
||||
func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
|
||||
req := &schema.CloseQuestionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = qc.questionService.CloseQuestion(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// ReopenQuestion reopen question
|
||||
// @Summary reopen question
|
||||
// @Description reopen question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.ReopenQuestionReq true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/reopen [put]
|
||||
func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
|
||||
req := &schema.ReopenQuestionReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = qc.questionService.ReopenQuestion(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetQuestion get question details
|
||||
// @Summary get question details
|
||||
// @Description get question details
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "Question TagID" default(1)
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/question/info [get]
|
||||
func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
||||
id := ctx.Query("id")
|
||||
id = uid.DeShortID(id)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
req := schema.QuestionPermission{}
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
|
||||
permission.QuestionEdit,
|
||||
permission.QuestionDelete,
|
||||
permission.QuestionClose,
|
||||
permission.QuestionReopen,
|
||||
permission.QuestionPin,
|
||||
permission.QuestionUnPin,
|
||||
permission.QuestionHide,
|
||||
permission.QuestionShow,
|
||||
permission.AnswerInviteSomeoneToAnswer,
|
||||
permission.QuestionUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)
|
||||
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
req.CanDelete = canList[1]
|
||||
req.CanClose = canList[2]
|
||||
req.CanReopen = canList[3]
|
||||
req.CanPin = canList[4]
|
||||
req.CanUnPin = canList[5]
|
||||
req.CanHide = canList[6]
|
||||
req.CanShow = canList[7]
|
||||
req.CanInviteOtherToAnswer = canList[8]
|
||||
req.CanRecover = canList[9]
|
||||
|
||||
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if handler.GetEnableShortID(ctx) {
|
||||
info.ID = uid.EnShortID(info.ID)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, info)
|
||||
}
|
||||
|
||||
// GetQuestionInviteUserInfo get question invite user info
|
||||
// @Summary get question invite user info
|
||||
// @Description get question invite user info
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "Question ID" default(1)
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/question/invite [get]
|
||||
func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) {
|
||||
questionID := uid.DeShortID(ctx.Query("id"))
|
||||
resp, err := qc.questionService.InviteUserInfo(ctx, questionID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// SimilarQuestion godoc
|
||||
// @Summary Search Similar Question
|
||||
// @Description Search Similar Question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param question_id query string true "question_id" default()
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/question/similar/tag [get]
|
||||
func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
|
||||
questionID := ctx.Query("question_id")
|
||||
questionID = uid.DeShortID(questionID)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// QuestionPage get questions by page
|
||||
// @Summary get questions by page
|
||||
// @Description get questions by page
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionPageReq true "QuestionPageReq"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}
|
||||
// @Router /answer/api/v1/question/page [get]
|
||||
func (qc *QuestionController) QuestionPage(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
questions, total, err := qc.questionService.GetQuestionPage(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if pager.ValPageOutOfRange(total, req.Page, req.PageSize) {
|
||||
handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
|
||||
// QuestionRecommendPage get recommend questions by page
|
||||
// @Summary get recommend questions by page
|
||||
// @Description get recommend questions by page
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionPageReq true "QuestionPageReq"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}
|
||||
// @Router /answer/api/v1/question/recommend/page [get]
|
||||
func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
if req.LoginUserID == "" {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
|
||||
// AddQuestion add question
|
||||
// @Summary add question
|
||||
// @Description add question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionAdd true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question [post]
|
||||
func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
|
||||
req := &schema.QuestionAdd{}
|
||||
errFields := handler.BindAndCheckReturnErr(ctx, req)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
reject, rejectKey := qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)
|
||||
if reject {
|
||||
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 {
|
||||
qc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)
|
||||
}
|
||||
}()
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, requireRanks, err := qc.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 {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
linkUrlLimitUser := canList[7]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
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 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// can add tag
|
||||
hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !req.CanAddTag && hasNewTag {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]})
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
|
||||
return
|
||||
}
|
||||
|
||||
errList, err := qc.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)
|
||||
return
|
||||
}
|
||||
|
||||
req.UserAgent = ctx.GetHeader("User-Agent")
|
||||
req.IP = ctx.ClientIP()
|
||||
|
||||
resp, err := qc.questionService.AddQuestion(ctx, req)
|
||||
if err != nil {
|
||||
errlist, ok := resp.([]*validator.FormErrorField)
|
||||
if ok {
|
||||
errFields = append(errFields, errlist...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errFields) > 0 {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
|
||||
return
|
||||
}
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AddQuestionByAnswer add question
|
||||
// @Summary add question and answer
|
||||
// @Description add question and answer
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionAddByAnswer true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/answer [post]
|
||||
func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {
|
||||
req := &schema.QuestionAddByAnswer{}
|
||||
errFields := handler.BindAndCheckReturnErr(ctx, req)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.QuestionAdd,
|
||||
permission.QuestionEdit,
|
||||
permission.QuestionDelete,
|
||||
permission.QuestionClose,
|
||||
permission.QuestionReopen,
|
||||
permission.TagUseReservedTag,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
linkUrlLimitUser := canList[6]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
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]
|
||||
if !req.CanAdd {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
questionReq := new(schema.QuestionAdd)
|
||||
err = copier.Copy(questionReq, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
errList, err := qc.questionService.CheckAddQuestion(ctx, questionReq)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
req.UserAgent = ctx.GetHeader("User-Agent")
|
||||
req.IP = ctx.ClientIP()
|
||||
resp, err := qc.questionService.AddQuestion(ctx, questionReq)
|
||||
if err != nil {
|
||||
errlist, ok := resp.([]*validator.FormErrorField)
|
||||
if ok {
|
||||
errFields = append(errFields, errlist...)
|
||||
}
|
||||
}
|
||||
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID)
|
||||
}
|
||||
|
||||
if len(errFields) > 0 {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
|
||||
return
|
||||
}
|
||||
// add the question id to the answer
|
||||
questionInfo, ok := resp.(*schema.QuestionInfoResp)
|
||||
if ok {
|
||||
answerReq := &schema.AnswerAddReq{}
|
||||
answerReq.QuestionID = uid.DeShortID(questionInfo.ID)
|
||||
answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
answerReq.Content = req.AnswerContent
|
||||
answerReq.HTML = req.AnswerHTML
|
||||
answerID, err := qc.answerService.Insert(ctx, answerReq)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"info": info,
|
||||
"question": questionInfo,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateQuestion update question
|
||||
// @Summary update question
|
||||
// @Description update question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionUpdate true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question [put]
|
||||
func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
|
||||
req := &schema.QuestionUpdate{}
|
||||
errFields := handler.BindAndCheckReturnErr(ctx, req)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{
|
||||
permission.QuestionEdit,
|
||||
permission.QuestionDelete,
|
||||
permission.QuestionEditWithoutReview,
|
||||
permission.TagUseReservedTag,
|
||||
permission.TagAdd,
|
||||
permission.LinkUrlLimit,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
linkUrlLimitUser := canList[5]
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
req.CanDelete = canList[1]
|
||||
req.NoNeedReview = canList[2] || objectOwner
|
||||
req.CanUseReservedTag = canList[3]
|
||||
req.CanAddTag = canList[4]
|
||||
if !req.CanEdit {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req)
|
||||
if err != nil {
|
||||
errFields = append(errFields, errlist...)
|
||||
}
|
||||
|
||||
if len(errFields) > 0 {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
|
||||
return
|
||||
}
|
||||
|
||||
// can add tag
|
||||
hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !req.CanAddTag && hasNewTag {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[4]})
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := qc.questionService.UpdateQuestion(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
respInfo, ok := resp.(*schema.QuestionInfoResp)
|
||||
if !ok {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{UrlTitle: respInfo.UrlTitle, WaitForReview: !req.NoNeedReview})
|
||||
}
|
||||
|
||||
// QuestionRecover recover deleted question
|
||||
// @Summary recover deleted question
|
||||
// @Description recover deleted question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionRecoverReq true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/recover [post]
|
||||
func (qc *QuestionController) QuestionRecover(ctx *gin.Context) {
|
||||
req := &schema.QuestionRecoverReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.QuestionUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = qc.questionService.RecoverQuestion(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateQuestionInviteUser update question invite user
|
||||
// @Summary update question invite user
|
||||
// @Description update question invite user
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionUpdateInviteUser true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/invite [put]
|
||||
func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) {
|
||||
req := &schema.QuestionUpdateInviteUser{}
|
||||
errFields := handler.BindAndCheckReturnErr(ctx, req)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
if len(errFields) > 0 {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerInviteSomeoneToAnswer,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
req.CanInviteOtherToAnswer = canList[0]
|
||||
if !req.CanInviteOtherToAnswer {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
err = qc.questionService.UpdateQuestionInviteUser(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// GetSimilarQuestions fuzzy query similar questions based on title
|
||||
// @Summary fuzzy query similar questions based on title
|
||||
// @Description fuzzy query similar questions based on title
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param title query string true "title" default(string)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/similar [get]
|
||||
func (qc *QuestionController) GetSimilarQuestions(ctx *gin.Context) {
|
||||
title := ctx.Query("title")
|
||||
resp, err := qc.questionService.GetQuestionsByTitle(ctx, title)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserTop godoc
|
||||
// @Summary UserTop
|
||||
// @Description UserTop
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/qa/top [get]
|
||||
func (qc *QuestionController) UserTop(ctx *gin.Context) {
|
||||
userName := ctx.Query("username")
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, answerList, err := qc.questionService.SearchUserTopList(ctx, userName, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"question": questionList,
|
||||
"answer": answerList,
|
||||
})
|
||||
}
|
||||
|
||||
// PersonalQuestionPage list personal questions
|
||||
// @Summary list personal questions
|
||||
// @Description list personal questions
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /personal/question/page [get]
|
||||
func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalQuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetUserIsAdminModerator(ctx)
|
||||
resp, err := qc.questionService.PersonalQuestionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// PersonalAnswerPage list personal answers
|
||||
// @Summary list personal answers
|
||||
// @Description list personal answers
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/answer/page [get]
|
||||
func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalAnswerPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetUserIsAdminModerator(ctx)
|
||||
resp, err := qc.questionService.PersonalAnswerPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// PersonalCollectionPage list personal collections
|
||||
// @Summary list personal collections
|
||||
// @Description list personal collections
|
||||
// @Tags Collection
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/collection/page [get]
|
||||
func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalCollectionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := qc.questionService.PersonalCollectionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AdminQuestionPage admin question page
|
||||
// @Summary AdminQuestionPage admin question page
|
||||
// @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, pending)
|
||||
// @Param query query string false "question id or title"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/question/page [get]
|
||||
func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) {
|
||||
req := &schema.AdminQuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.AdminQuestionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AdminAnswerPage admin answer page
|
||||
// @Summary AdminAnswerPage admin answer page
|
||||
// @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,pending)
|
||||
// @Param query query string false "answer id or question title"
|
||||
// @Param question_id query string false "question id"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/answer/page [get]
|
||||
func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) {
|
||||
req := &schema.AdminAnswerPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.AdminAnswerPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AdminUpdateQuestionStatus update question status
|
||||
// @Summary update question status
|
||||
// @Description update question status
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AdminUpdateQuestionStatusReq true "AdminUpdateQuestionStatusReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/question/status [put]
|
||||
func (qc *QuestionController) AdminUpdateQuestionStatus(ctx *gin.Context) {
|
||||
req := &schema.AdminUpdateQuestionStatusReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := qc.questionService.AdminSetQuestionStatus(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetQuestionLink get question link
|
||||
// @Summary get question link
|
||||
// @Description get question link
|
||||
// @Tags Question
|
||||
// @Param data query schema.GetQuestionLinkReq true "GetQuestionLinkReq"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}
|
||||
// @Router /answer/api/v1/question/link [get]
|
||||
func (qc *QuestionController) GetQuestionLink(ctx *gin.Context) {
|
||||
req := &schema.GetQuestionLinkReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
questions, total, err := qc.questionService.GetQuestionLink(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RankController rank controller
|
||||
type RankController struct {
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewRankController new controller
|
||||
func NewRankController(
|
||||
rankService *rank.RankService) *RankController {
|
||||
return &RankController{rankService: rankService}
|
||||
}
|
||||
|
||||
// GetRankPersonalWithPage user personal rank list
|
||||
// @Summary user personal rank list
|
||||
// @Description user personal rank list
|
||||
// @Tags Rank
|
||||
// @Produce json
|
||||
// @Param page query int false "page"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param username query string false "username"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalPageResp}}
|
||||
// @Router /answer/api/v1/personal/rank/page [get]
|
||||
func (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) {
|
||||
req := &schema.GetRankPersonalWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := cc.rankService.GetRankPersonalPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/reason"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ReasonController answer controller
|
||||
type ReasonController struct {
|
||||
reasonService *reason.ReasonService
|
||||
}
|
||||
|
||||
// NewReasonController new controller
|
||||
func NewReasonController(answerService *reason.ReasonService) *ReasonController {
|
||||
return &ReasonController{reasonService: answerService}
|
||||
}
|
||||
|
||||
// Reasons godoc
|
||||
// @Summary get reasons by object type and action
|
||||
// @Description get reasons by object type and action
|
||||
// @Tags reason
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param object_type query string true "object_type" Enums(question, answer, comment, user)
|
||||
// @Param action query string true "action" Enums(status, close, flag, review)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/reasons [get]
|
||||
// @Router /answer/admin/api/reasons [get]
|
||||
func (rc *ReasonController) Reasons(ctx *gin.Context) {
|
||||
req := &schema.ReasonReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
reasons, err := rc.reasonService.GetReasons(ctx, *req)
|
||||
handler.HandleResponse(ctx, err, reasons)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RenderController struct {
|
||||
}
|
||||
|
||||
func NewRenderController() *RenderController {
|
||||
return &RenderController{}
|
||||
}
|
||||
|
||||
// GetRenderConfig godoc
|
||||
// @Summary GetRenderConfig
|
||||
// @Description GetRenderConfig
|
||||
// @Tags PluginRender
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router /answer/api/v1/render/config [get]
|
||||
// @Success 200 {object} handler.RespBody{data=plugin.RenderConfig}
|
||||
func (c *RenderController) GetRenderConfig(ctx *gin.Context) {
|
||||
var resp *plugin.RenderConfig
|
||||
|
||||
_ = plugin.CallRender(func(render plugin.Render) (err error) {
|
||||
resp = render.GetRenderConfig(ctx)
|
||||
return nil
|
||||
})
|
||||
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/internal/service/report"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// ReportController report controller
|
||||
type ReportController struct {
|
||||
reportService *report.ReportService
|
||||
rankService *rank.RankService
|
||||
actionService *action.CaptchaService
|
||||
}
|
||||
|
||||
// NewReportController new controller
|
||||
func NewReportController(
|
||||
reportService *report.ReportService,
|
||||
rankService *rank.RankService,
|
||||
actionService *action.CaptchaService,
|
||||
) *ReportController {
|
||||
return &ReportController{
|
||||
reportService: reportService,
|
||||
rankService: rankService,
|
||||
actionService: actionService,
|
||||
}
|
||||
}
|
||||
|
||||
// AddReport add report
|
||||
// @Summary add report
|
||||
// @Description add report <br> source (question, answer, comment, user)
|
||||
// @Tags Report
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.AddReportReq true "report"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/report [post]
|
||||
func (rc *ReportController) AddReport(ctx *gin.Context) {
|
||||
req := &schema.AddReportReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := rc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionReport, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = rc.reportService.AddReport(ctx, req)
|
||||
if !isAdmin {
|
||||
rc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionReport, req.UserID)
|
||||
}
|
||||
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
|
||||
// @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)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/internal/service/review"
|
||||
"github.com/apache/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
|
||||
// @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)
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/pkg/obj"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// RevisionController revision controller
|
||||
type RevisionController struct {
|
||||
revisionListService *content.RevisionService
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewRevisionController new controller
|
||||
func NewRevisionController(
|
||||
revisionListService *content.RevisionService,
|
||||
rankService *rank.RankService,
|
||||
) *RevisionController {
|
||||
return &RevisionController{
|
||||
revisionListService: revisionListService,
|
||||
rankService: rankService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRevisionList godoc
|
||||
// @Summary get revision list
|
||||
// @Description get revision list
|
||||
// @Tags Revision
|
||||
// @Produce json
|
||||
// @Param object_id query string true "object id"
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetRevisionResp}
|
||||
// @Router /answer/api/v1/revisions [get]
|
||||
func (rc *RevisionController) GetRevisionList(ctx *gin.Context) {
|
||||
objectID := ctx.Query("object_id")
|
||||
if objectID == "0" || objectID == "" {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
objectID = uid.DeShortID(objectID)
|
||||
req := &schema.GetRevisionListReq{
|
||||
ObjectID: objectID,
|
||||
IsAdmin: middleware.GetUserIsAdminModerator(ctx),
|
||||
UserID: middleware.GetLoginUserIDFromContext(ctx),
|
||||
}
|
||||
|
||||
resp, err := rc.revisionListService.GetRevisionList(ctx, req)
|
||||
list := make([]schema.GetRevisionResp, 0)
|
||||
for _, item := range resp {
|
||||
if item.Status == entity.RevisionNormalStatus || item.Status == entity.RevisionReviewPassStatus {
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
handler.HandleResponse(ctx, err, list)
|
||||
}
|
||||
|
||||
// GetUnreviewedRevisionList godoc
|
||||
// @Summary get unreviewed revision list
|
||||
// @Description get unreviewed revision list
|
||||
// @Tags Revision
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query string true "page id"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}}
|
||||
// @Router /answer/api/v1/revisions/unreviewed [get]
|
||||
func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {
|
||||
req := &schema.RevisionSearch{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
resp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// RevisionAudit godoc
|
||||
// @Summary revision audit
|
||||
// @Description revision audit operation:approve or reject
|
||||
// @Tags Revision
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.RevisionAuditReq true "audit"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/revisions/audit [put]
|
||||
func (rc *RevisionController) RevisionAudit(ctx *gin.Context) {
|
||||
req := &schema.RevisionAuditReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
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]
|
||||
|
||||
err = rc.revisionListService.RevisionAudit(ctx, req)
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
}
|
||||
|
||||
// CheckCanUpdateRevision check can update revision
|
||||
// @Summary check can update revision
|
||||
// @Description check can update revision
|
||||
// @Tags Revision
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id query string true "id" default(string)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/revisions/edit/check [get]
|
||||
func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {
|
||||
req := &schema.CheckCanQuestionUpdate{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
action := ""
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)
|
||||
switch objectTypeStr {
|
||||
case constant.QuestionObjectType:
|
||||
action = permission.QuestionEdit
|
||||
case constant.AnswerObjectType:
|
||||
action = permission.AnswerEdit
|
||||
case constant.TagObjectType:
|
||||
action = permission.TagEdit
|
||||
default:
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil)
|
||||
return
|
||||
}
|
||||
|
||||
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// SearchController tag controller
|
||||
type SearchController struct {
|
||||
searchService *content.SearchService
|
||||
actionService *action.CaptchaService
|
||||
}
|
||||
|
||||
// NewSearchController new controller
|
||||
func NewSearchController(
|
||||
searchService *content.SearchService,
|
||||
actionService *action.CaptchaService,
|
||||
) *SearchController {
|
||||
return &SearchController{
|
||||
searchService: searchService,
|
||||
actionService: actionService,
|
||||
}
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
// @Summary search object
|
||||
// @Description search object
|
||||
// @Tags Search
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param q query string true "query string"
|
||||
// @Param order query string true "order" Enums(newest,active,score,relevance)
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SearchResp}
|
||||
// @Router /answer/api/v1/search [get]
|
||||
func (sc *SearchController) Search(ctx *gin.Context) {
|
||||
dto := schema.SearchDTO{}
|
||||
|
||||
if handler.BindAndCheck(ctx, &dto) {
|
||||
return
|
||||
}
|
||||
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
unit := ctx.ClientIP()
|
||||
if dto.UserID != "" {
|
||||
unit = dto.UserID
|
||||
}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := sc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionSearch, unit, dto.CaptchaID, dto.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
sc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionSearch, unit)
|
||||
}
|
||||
resp, err := sc.searchService.Search(ctx, &dto)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// SearchDesc get search description
|
||||
// @Summary get search description
|
||||
// @Description get search description
|
||||
// @Tags Search
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SearchResp}
|
||||
// @Router /answer/api/v1/search/desc [get]
|
||||
func (sc *SearchController) SearchDesc(ctx *gin.Context) {
|
||||
var finder plugin.Search
|
||||
_ = plugin.CallSearch(func(search plugin.Search) error {
|
||||
finder = search
|
||||
return nil
|
||||
})
|
||||
resp := &schema.SearchDescResp{}
|
||||
if finder != nil {
|
||||
resp.Name = finder.Info().Name.Translate(ctx)
|
||||
resp.Icon = finder.Description().Icon
|
||||
resp.Link = finder.Description().Link
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type SiteInfoController struct {
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewSiteInfoController new site info controller.
|
||||
func NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController {
|
||||
return &SiteInfoController{
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSiteInfo get site info
|
||||
// @Summary get site info
|
||||
// @Description get site info
|
||||
// @Tags site
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp}
|
||||
// @Router /answer/api/v1/siteinfo [get]
|
||||
func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) {
|
||||
var err error
|
||||
resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision}
|
||||
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.UsersSettings, err = sc.siteInfoService.GetSiteUsersSettings(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.Login, err = sc.siteInfoService.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Questions, err = sc.siteInfoService.GetSiteQuestion(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Tags, err = sc.siteInfoService.GetSiteTag(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Advanced, err = sc.siteInfoService.GetSiteAdvanced(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if legal, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil {
|
||||
resp.Legal = &schema.SiteLegalSimpleResp{ExternalContentDisplay: legal.ExternalContentDisplay}
|
||||
}
|
||||
if security, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil {
|
||||
resp.Security = security
|
||||
}
|
||||
if aiConf, err := sc.siteInfoService.GetSiteAI(ctx); err == nil {
|
||||
resp.AIEnabled = aiConf.Enabled
|
||||
}
|
||||
|
||||
if mcpConf, err := sc.siteInfoService.GetSiteMCP(ctx); err == nil {
|
||||
resp.MCPEnabled = mcpConf.Enabled
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetSiteLegalInfo get site legal info
|
||||
// @Summary get site legal info
|
||||
// @Description get site legal info
|
||||
// @Tags site
|
||||
// @Param info_type query string true "legal information type" Enums(tos, privacy)
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}
|
||||
// @Router /answer/api/v1/siteinfo/legal [get]
|
||||
func (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) {
|
||||
req := &schema.GetSiteLegalInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
siteLegal, err := sc.siteInfoService.GetSitePolicies(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
resp := &schema.GetSiteLegalInfoResp{}
|
||||
if req.IsTOS() {
|
||||
resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText
|
||||
resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText
|
||||
} else if req.IsPrivacy() {
|
||||
resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText
|
||||
resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetManifestJson get manifest.json
|
||||
func (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) {
|
||||
favicon := "favicon.ico"
|
||||
resp := &schema.GetManifestJsonResp{
|
||||
ManifestVersion: 3,
|
||||
Version: constant.Version,
|
||||
Revision: constant.Revision,
|
||||
ShortName: "Answer",
|
||||
Name: "answer.apache.org",
|
||||
Icons: schema.CreateManifestJsonIcons(favicon),
|
||||
StartUrl: ".",
|
||||
Display: "standalone",
|
||||
ThemeColor: "#000000",
|
||||
BackgroundColor: "#ffffff",
|
||||
}
|
||||
branding, err := sc.siteInfoService.GetSiteBranding(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else if len(branding.Favicon) > 0 {
|
||||
resp.Icons = schema.CreateManifestJsonIcons(branding.Favicon)
|
||||
}
|
||||
siteGeneral, err := sc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
resp.Name = siteGeneral.Name
|
||||
resp.ShortName = siteGeneral.Name
|
||||
}
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/internal/service/tag"
|
||||
"github.com/apache/answer/internal/service/tag_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// TagController tag controller
|
||||
type TagController struct {
|
||||
tagService *tag.TagService
|
||||
tagCommonService *tag_common.TagCommonService
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewTagController new controller
|
||||
func NewTagController(
|
||||
tagService *tag.TagService,
|
||||
tagCommonService *tag_common.TagCommonService,
|
||||
rankService *rank.RankService,
|
||||
) *TagController {
|
||||
return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService}
|
||||
}
|
||||
|
||||
// SearchTagLike get tag list
|
||||
// @Summary get tag list
|
||||
// @Description get tag list
|
||||
// @Tags Tag
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param tag query string false "tag"
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp}
|
||||
// @Router /answer/api/v1/question/tags [get]
|
||||
func (tc *TagController) SearchTagLike(ctx *gin.Context) {
|
||||
req := &schema.SearchTagLikeReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
resp, err := tc.tagCommonService.SearchTagLike(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetTagsBySlugName get tags list
|
||||
// @Summary get tags list
|
||||
// @Description get tags list by slug name
|
||||
// @Tags Tag
|
||||
// @Produce json
|
||||
// @Param tags query []string false "string collection" collectionFormat(csv)
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp}
|
||||
// @Router /answer/api/v1/tags [get]
|
||||
func (tc *TagController) GetTagsBySlugName(ctx *gin.Context) {
|
||||
req := &schema.SearchTagsBySlugName{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := tc.tagService.GetTagsBySlugName(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// RemoveTag delete tag
|
||||
// @Summary delete tag
|
||||
// @Description delete tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.RemoveTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag [delete]
|
||||
func (tc *TagController) RemoveTag(ctx *gin.Context) {
|
||||
req := &schema.RemoveTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
err = tc.tagService.RemoveTag(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// AddTag add tag
|
||||
// @Summary add tag
|
||||
// @Description add tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.AddTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag [post]
|
||||
func (tc *TagController) AddTag(ctx *gin.Context) {
|
||||
req := &schema.AddTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.TagAdd,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := tc.tagCommonService.AddTag(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateTag update tag
|
||||
// @Summary update tag
|
||||
// @Description update tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UpdateTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag [put]
|
||||
func (tc *TagController) UpdateTag(ctx *gin.Context) {
|
||||
req := &schema.UpdateTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.TagEdit,
|
||||
permission.TagEditWithoutReview,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
req.NoNeedReview = canList[1]
|
||||
|
||||
err = tc.tagService.UpdateTag(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview})
|
||||
}
|
||||
}
|
||||
|
||||
// RecoverTag recover delete tag
|
||||
// @Summary recover delete tag
|
||||
// @Description recover delete tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.RecoverTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag/recover [post]
|
||||
func (tc *TagController) RecoverTag(ctx *gin.Context) {
|
||||
req := &schema.RecoverTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.TagUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = tc.tagService.RecoverTag(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetTagInfo get tag one
|
||||
// @Summary get tag one
|
||||
// @Description get tag one
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tag_id query string true "tag id"
|
||||
// @Param tag_name query string true "tag name"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetTagResp}
|
||||
// @Router /answer/api/v1/tag [get]
|
||||
func (tc *TagController) GetTagInfo(ctx *gin.Context) {
|
||||
req := &schema.GetTagInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.TagEdit,
|
||||
permission.TagDelete,
|
||||
permission.TagUnDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = canList[0]
|
||||
req.CanDelete = canList[1]
|
||||
req.CanRecover = canList[2]
|
||||
req.CanMerge = middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
resp, err := tc.tagService.GetTagInfo(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetTagWithPage get tag page
|
||||
// @Summary get tag page
|
||||
// @Description get tag page
|
||||
// @Tags Tag
|
||||
// @Produce json
|
||||
// @Param page query int false "page size"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param slug_name query string false "slug_name"
|
||||
// @Param query_cond query string false "query condition" Enums(popular, name, newest)
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetTagPageResp}}
|
||||
// @Router /answer/api/v1/tags/page [get]
|
||||
func (tc *TagController) GetTagWithPage(ctx *gin.Context) {
|
||||
req := &schema.GetTagWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := tc.tagService.GetTagWithPage(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if pager.ValPageOutOfRange(resp.Count, req.Page, req.PageSize) {
|
||||
handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetFollowingTags get following tag list
|
||||
// @Summary get following tag list
|
||||
// @Description get following tag list
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetFollowingTagsResp}
|
||||
// @Router /answer/api/v1/tags/following [get]
|
||||
func (tc *TagController) GetFollowingTags(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := tc.tagService.GetFollowingTags(ctx, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetTagSynonyms get tag synonyms
|
||||
// @Summary get tag synonyms
|
||||
// @Description get tag synonyms
|
||||
// @Tags Tag
|
||||
// @Produce json
|
||||
// @Param tag_id query int true "tag id"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp}
|
||||
// @Router /answer/api/v1/tag/synonyms [get]
|
||||
func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
|
||||
req := &schema.GetTagSynonymsReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
req.CanEdit = can
|
||||
|
||||
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateTagSynonym update tag
|
||||
// @Summary update tag
|
||||
// @Description update tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UpdateTagSynonymReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag/synonym [put]
|
||||
func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {
|
||||
req := &schema.UpdateTagSynonymReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = tc.tagService.UpdateTagSynonym(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// MergeTag merge tag
|
||||
// @Summary merge tag
|
||||
// @Description merge tag
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.AddTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag/merge [post]
|
||||
func (tc *TagController) MergeTag(ctx *gin.Context) {
|
||||
req := &schema.MergeTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
isAdminModerator := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdminModerator {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := tc.tagService.MergeTag(ctx, req)
|
||||
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/eventqueue"
|
||||
"github.com/apache/answer/plugin"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
templaterender "github.com/apache/answer/internal/controller/template_render"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
"github.com/apache/answer/pkg/converter"
|
||||
"github.com/apache/answer/pkg/htmltext"
|
||||
"github.com/apache/answer/pkg/obj"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/apache/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
var SiteUrl = ""
|
||||
|
||||
type TemplateController struct {
|
||||
scriptPath []string
|
||||
cssPath string
|
||||
templateRenderController *templaterender.TemplateRenderController
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
eventQueueService eventqueue.Service
|
||||
userService *content.UserService
|
||||
questionService *content.QuestionService
|
||||
}
|
||||
|
||||
// NewTemplateController new controller
|
||||
func NewTemplateController(
|
||||
templateRenderController *templaterender.TemplateRenderController,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
eventQueueService eventqueue.Service,
|
||||
userService *content.UserService,
|
||||
questionService *content.QuestionService,
|
||||
) *TemplateController {
|
||||
script, css := GetStyle()
|
||||
return &TemplateController{
|
||||
scriptPath: script,
|
||||
cssPath: css,
|
||||
templateRenderController: templateRenderController,
|
||||
siteInfoService: siteInfoService,
|
||||
eventQueueService: eventQueueService,
|
||||
userService: userService,
|
||||
questionService: questionService,
|
||||
}
|
||||
}
|
||||
func GetStyle() (script []string, css string) {
|
||||
file, err := ui.Build.ReadFile("build/index.html")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
scriptRegexp := regexp.MustCompile(`<script defer="defer" src="([^"]*)"></script>`)
|
||||
scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1)
|
||||
for _, s := range scriptData {
|
||||
if len(s) == 2 {
|
||||
script = append(script, s[1])
|
||||
}
|
||||
}
|
||||
|
||||
cssRegexp := regexp.MustCompile(`<link href="(.*)" rel="stylesheet">`)
|
||||
cssListData := cssRegexp.FindStringSubmatch(string(file))
|
||||
if len(cssListData) == 2 {
|
||||
css = cssListData[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp {
|
||||
var err error
|
||||
resp := &schema.TemplateSiteInfoResp{}
|
||||
resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
SiteUrl = resp.General.SiteUrl
|
||||
resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Year = fmt.Sprintf("%d", time.Now().Year())
|
||||
return resp
|
||||
}
|
||||
|
||||
// Index question list
|
||||
func (tc *TemplateController) Index(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
var page = req.Page
|
||||
|
||||
data, count, err := tc.templateRenderController.Index(ctx, req)
|
||||
if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
hotQuestionReq := &schema.QuestionPageReq{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
OrderCond: "hot",
|
||||
InDays: 7,
|
||||
}
|
||||
hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = siteInfo.General.SiteUrl
|
||||
|
||||
UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID
|
||||
|
||||
siteInfo.Title = ""
|
||||
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"path": "questions",
|
||||
"hotQuestion": hotQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
var page = req.Page
|
||||
data, count, err := tc.templateRenderController.Index(ctx, req)
|
||||
if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
hotQuestionReq := &schema.QuestionPageReq{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
OrderCond: "hot",
|
||||
InDays: 7,
|
||||
}
|
||||
hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
|
||||
if page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page)
|
||||
}
|
||||
|
||||
UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID
|
||||
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"hotQuestion": hotQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
questionID := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
answerID := uid.DeShortID(title)
|
||||
titleIsAnswerID := false
|
||||
needChangeShortID := false
|
||||
|
||||
objectType, err := obj.GetObjectTypeStrByObjectID(answerID)
|
||||
if err == nil && objectType == constant.AnswerObjectType {
|
||||
titleIsAnswerID = true
|
||||
}
|
||||
|
||||
siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
isShortID := uid.IsShortID(questionID)
|
||||
if siteSeo.IsShortLink() {
|
||||
if !isShortID {
|
||||
questionID = uid.EnShortID(questionID)
|
||||
needChangeShortID = true
|
||||
}
|
||||
if titleIsAnswerID {
|
||||
answerID = uid.EnShortID(answerID)
|
||||
}
|
||||
} else {
|
||||
if isShortID {
|
||||
needChangeShortID = true
|
||||
questionID = uid.DeShortID(questionID)
|
||||
}
|
||||
if titleIsAnswerID {
|
||||
answerID = uid.DeShortID(answerID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil {
|
||||
answerID = ""
|
||||
titleIsAnswerID = false
|
||||
}
|
||||
|
||||
url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, questionID)
|
||||
if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {
|
||||
if len(ctx.Request.URL.Query()) > 0 {
|
||||
url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery)
|
||||
}
|
||||
if needChangeShortID {
|
||||
return true, url
|
||||
}
|
||||
// not have title
|
||||
if titleIsAnswerID || len(title) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, url
|
||||
} else {
|
||||
detail, err := tc.templateRenderController.QuestionDetail(ctx, questionID)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
|
||||
if titleIsAnswerID {
|
||||
url = fmt.Sprintf("%s/%s", url, answerID)
|
||||
}
|
||||
|
||||
if len(ctx.Request.URL.Query()) > 0 {
|
||||
url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery)
|
||||
}
|
||||
// have title
|
||||
if len(title) > 0 && !titleIsAnswerID && correctTitle {
|
||||
if needChangeShortID {
|
||||
return true, url
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
return true, url
|
||||
}
|
||||
}
|
||||
|
||||
// QuestionInfo question and answers info
|
||||
func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
id := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
answerid := ctx.Param("answerid")
|
||||
shareUsername := ctx.Query("share")
|
||||
if checker.IsQuestionsIgnorePath(id) {
|
||||
// if id == "ask" {
|
||||
file, err := ui.Build.ReadFile("build/index.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Header("content-type", "text/html;charset=utf-8")
|
||||
ctx.String(http.StatusOK, string(file))
|
||||
return
|
||||
}
|
||||
|
||||
correctTitle := false
|
||||
|
||||
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
if len(shareUsername) > 0 {
|
||||
userInfo, err := tc.userService.GetOtherUserInfoByUsername(
|
||||
ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername})
|
||||
if err == nil {
|
||||
tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID).
|
||||
QID(id, detail.UserID).AID(answerid, ""))
|
||||
}
|
||||
}
|
||||
encodeTitle := htmltext.UrlTitle(detail.Title)
|
||||
if encodeTitle == title {
|
||||
correctTitle = true
|
||||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
jump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle)
|
||||
if jump {
|
||||
ctx.Redirect(http.StatusFound, jumpurl)
|
||||
return
|
||||
}
|
||||
|
||||
// answers
|
||||
answerReq := &schema.AnswerListReq{
|
||||
QuestionID: id,
|
||||
Order: "",
|
||||
Page: 1,
|
||||
PageSize: 999,
|
||||
UserID: "",
|
||||
}
|
||||
answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// comments
|
||||
objectIDs := []string{uid.DeShortID(id)}
|
||||
for _, answer := range answers {
|
||||
answerID := uid.DeShortID(answer.ID)
|
||||
objectIDs = append(objectIDs, answerID)
|
||||
}
|
||||
comments, err := tc.templateRenderController.CommentList(ctx, objectIDs)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID
|
||||
|
||||
// related question
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
relatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID)
|
||||
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
|
||||
if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
|
||||
}
|
||||
jsonLD := &schema.QAPageJsonLD{}
|
||||
jsonLD.Context = "https://schema.org"
|
||||
jsonLD.Type = "QAPage"
|
||||
jsonLD.MainEntity.Type = "Question"
|
||||
jsonLD.MainEntity.Name = detail.Title
|
||||
jsonLD.MainEntity.Text = detail.HTML
|
||||
jsonLD.MainEntity.AnswerCount = int(answerCount)
|
||||
jsonLD.MainEntity.UpvoteCount = detail.VoteCount
|
||||
jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0)
|
||||
jsonLD.MainEntity.Author.Type = "Person"
|
||||
jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName
|
||||
jsonLD.MainEntity.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, detail.UserInfo.Username)
|
||||
answerList := make([]*schema.SuggestedAnswerItem, 0)
|
||||
for _, answer := range answers {
|
||||
if answer.Accepted == schema.AnswerAcceptedEnable {
|
||||
acceptedAnswerItem := &schema.AcceptedAnswerItem{}
|
||||
acceptedAnswerItem.Type = "Answer"
|
||||
acceptedAnswerItem.Text = answer.HTML
|
||||
acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0)
|
||||
acceptedAnswerItem.UpvoteCount = answer.VoteCount
|
||||
acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
|
||||
acceptedAnswerItem.Author.Type = "Person"
|
||||
acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName
|
||||
acceptedAnswerItem.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username)
|
||||
jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem
|
||||
} else {
|
||||
item := &schema.SuggestedAnswerItem{}
|
||||
item.Type = "Answer"
|
||||
item.Text = answer.HTML
|
||||
item.DateCreated = time.Unix(answer.CreateTime, 0)
|
||||
item.UpvoteCount = answer.VoteCount
|
||||
item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
|
||||
item.Author.Type = "Person"
|
||||
item.Author.Name = answer.UserInfo.DisplayName
|
||||
item.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username)
|
||||
answerList = append(answerList, item)
|
||||
}
|
||||
}
|
||||
jsonLD.MainEntity.SuggestedAnswer = answerList
|
||||
jsonLDStr, err := json.Marshal(jsonLD)
|
||||
if err == nil {
|
||||
siteInfo.JsonLD = `<script data-react-helmet="true" type="application/ld+json">` + string(jsonLDStr) + ` </script>`
|
||||
}
|
||||
|
||||
siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240)
|
||||
tags := make([]string, 0)
|
||||
for _, tag := range detail.Tags {
|
||||
tags = append(tags, tag.DisplayName)
|
||||
}
|
||||
siteInfo.Keywords = strings.ReplaceAll(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",")
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{
|
||||
"id": id,
|
||||
"answerid": answerid,
|
||||
"detail": detail,
|
||||
"answers": answers,
|
||||
"comments": comments,
|
||||
"noindex": detail.Show == entity.QuestionHide,
|
||||
"useTitle": UrlUseTitle,
|
||||
"relatedQuestion": relatedQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
// TagList tags list
|
||||
func (tc *TemplateController) TagList(ctx *gin.Context) {
|
||||
req := &schema.GetTagWithPageReq{
|
||||
PageSize: constant.DefaultPageSize,
|
||||
Page: 1,
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
data, err := tc.templateRenderController.TagList(ctx, req)
|
||||
if err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
page := templaterender.Paginator(req.Page, req.PageSize, data.Count)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page)
|
||||
}
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLangByCtx(ctx), constant.TagsListTitleTrKey), siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
|
||||
"page": page,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// TagInfo taginfo
|
||||
func (tc *TemplateController) TagInfo(ctx *gin.Context) {
|
||||
tag := ctx.Param("tag")
|
||||
req := &schema.GetTamplateTagInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
nowPage := req.Page
|
||||
req.Name = tag
|
||||
tagInfo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
page := templaterender.Paginator(nowPage, req.PageSize, questionCount)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page)
|
||||
}
|
||||
siteInfo.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240)
|
||||
if len(tagInfo.ParsedText) == 0 {
|
||||
siteInfo.Description = translator.Tr(handler.GetLangByCtx(ctx), constant.TagHasNoDescription)
|
||||
}
|
||||
siteInfo.Keywords = tagInfo.DisplayName
|
||||
|
||||
UrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID
|
||||
|
||||
siteInfo.Title = fmt.Sprintf("'%s' %s - %s", tagInfo.DisplayName, translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{
|
||||
"tag": tagInfo,
|
||||
"questionList": questionList,
|
||||
"questionCount": questionCount,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
|
||||
// UserInfo user info
|
||||
func (tc *TemplateController) UserInfo(ctx *gin.Context) {
|
||||
username := ctx.Param("username")
|
||||
if username == "" {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
exist := checker.IsUsersIgnorePath(username)
|
||||
if exist {
|
||||
file, err := ui.Build.ReadFile("build/index.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Header("content-type", "text/html;charset=utf-8")
|
||||
ctx.String(http.StatusOK, string(file))
|
||||
return
|
||||
}
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
req.Username = username
|
||||
userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
questionList, answerList, err := tc.questionService.SearchUserTopList(ctx, req.Username, "")
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
|
||||
"userinfo": userinfo,
|
||||
"bio": template.HTML(userinfo.BioHTML),
|
||||
"topQuestions": questionList,
|
||||
"topAnswers": answerList,
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) Page404(ctx *gin.Context) {
|
||||
tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) {
|
||||
prefix := ""
|
||||
cssPath := ""
|
||||
scriptPath := make([]string, len(tc.scriptPath))
|
||||
|
||||
_ = plugin.CallCDN(func(fn plugin.CDN) error {
|
||||
prefix = fn.GetStaticPrefix()
|
||||
return nil
|
||||
})
|
||||
|
||||
if prefix != "" {
|
||||
if prefix[len(prefix)-1:] == "/" {
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
}
|
||||
cssPath = prefix + tc.cssPath
|
||||
for i, path := range tc.scriptPath {
|
||||
scriptPath[i] = prefix + path
|
||||
}
|
||||
} else {
|
||||
cssPath = tc.cssPath
|
||||
scriptPath = tc.scriptPath
|
||||
}
|
||||
|
||||
data["siteinfo"] = siteInfo
|
||||
data["baseURL"] = ""
|
||||
if parsedUrl, err := url.Parse(siteInfo.General.SiteUrl); err == nil {
|
||||
data["baseURL"] = parsedUrl.Path
|
||||
}
|
||||
data["scriptPath"] = scriptPath
|
||||
data["cssPath"] = cssPath
|
||||
data["keywords"] = siteInfo.Keywords
|
||||
if siteInfo.Description == "" {
|
||||
siteInfo.Description = siteInfo.General.Description
|
||||
}
|
||||
data["title"] = siteInfo.Title
|
||||
if siteInfo.Title == "" {
|
||||
data["title"] = siteInfo.General.Name
|
||||
}
|
||||
data["description"] = siteInfo.Description
|
||||
data["language"] = handler.GetLangByCtx(ctx)
|
||||
data["timezone"] = siteInfo.Interface.TimeZone
|
||||
language := strings.ReplaceAll(siteInfo.Interface.Language, "_", "-")
|
||||
data["lang"] = language
|
||||
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
|
||||
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
|
||||
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
|
||||
data["Version"] = constant.Version
|
||||
data["Revision"] = constant.Revision
|
||||
_, ok := data["path"]
|
||||
if !ok {
|
||||
data["path"] = ""
|
||||
}
|
||||
ctx.Header("X-Frame-Options", "DENY")
|
||||
ctx.HTML(code, tpl, data)
|
||||
}
|
||||
|
||||
func (tc *TemplateController) OpenSearch(ctx *gin.Context) {
|
||||
if tc.checkPrivateMode(ctx) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
tc.templateRenderController.OpenSearch(ctx)
|
||||
}
|
||||
|
||||
func (tc *TemplateController) Sitemap(ctx *gin.Context) {
|
||||
if tc.checkPrivateMode(ctx) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
tc.templateRenderController.Sitemap(ctx)
|
||||
}
|
||||
|
||||
func (tc *TemplateController) SitemapPage(ctx *gin.Context) {
|
||||
if tc.checkPrivateMode(ctx) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
page := 0
|
||||
pageParam := ctx.Param("page")
|
||||
pageRegexp := regexp.MustCompile(`question-(.*).xml`)
|
||||
pageStr := pageRegexp.FindStringSubmatch(pageParam)
|
||||
if len(pageStr) != 2 {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
page = converter.StringToInt(pageStr[1])
|
||||
if page == 0 {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
err := tc.templateRenderController.SitemapPage(ctx, page)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool {
|
||||
resp, err := tc.siteInfoService.GetSiteSecurity(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
}
|
||||
if resp.LoginRequired {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 templaterender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/schema"
|
||||
)
|
||||
|
||||
func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
|
||||
return t.answerService.SearchList(ctx, req)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) AnswerDetail(ctx context.Context, id string) (*schema.AnswerInfo, error) {
|
||||
return t.answerService.GetDetail(ctx, id)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 templaterender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
)
|
||||
|
||||
func (t *TemplateRenderController) CommentList(
|
||||
ctx context.Context,
|
||||
objectIDs []string,
|
||||
) (
|
||||
comments map[string][]*schema.GetCommentResp,
|
||||
err error,
|
||||
) {
|
||||
comments = make(map[string][]*schema.GetCommentResp, len(objectIDs))
|
||||
|
||||
for _, objectID := range objectIDs {
|
||||
var (
|
||||
req = &schema.GetCommentWithPageReq{
|
||||
Page: 1,
|
||||
PageSize: 3,
|
||||
ObjectID: objectID,
|
||||
QueryCond: "vote",
|
||||
UserID: "",
|
||||
}
|
||||
pageModel *pager.PageModel
|
||||
)
|
||||
pageModel, err = t.commentService.GetCommentWithPage(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
li := pageModel.List
|
||||
comments[objectID] = li.([]*schema.GetCommentResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 templaterender
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
|
||||
"github.com/apache/answer/internal/service/comment"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/tag"
|
||||
)
|
||||
|
||||
// ProviderSetTemplateRenderController is template render controller providers.
|
||||
var ProviderSetTemplateRenderController = wire.NewSet(
|
||||
NewTemplateRenderController,
|
||||
)
|
||||
|
||||
type TemplateRenderController struct {
|
||||
questionService *content.QuestionService
|
||||
userService *content.UserService
|
||||
tagService *tag.TagService
|
||||
answerService *content.AnswerService
|
||||
commentService *comment.CommentService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
}
|
||||
|
||||
func NewTemplateRenderController(
|
||||
questionService *content.QuestionService,
|
||||
userService *content.UserService,
|
||||
tagService *tag.TagService,
|
||||
answerService *content.AnswerService,
|
||||
commentService *comment.CommentService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
) *TemplateRenderController {
|
||||
return &TemplateRenderController{
|
||||
questionService: questionService,
|
||||
userService: userService,
|
||||
tagService: tagService,
|
||||
answerService: answerService,
|
||||
commentService: commentService,
|
||||
questionRepo: questionRepo,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// Paginator page
|
||||
// page : now page
|
||||
// pageSize : Number per page
|
||||
// nums : Total
|
||||
// Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned
|
||||
func Paginator(page, pageSize int, nums int64) *schema.Paginator {
|
||||
if pageSize == 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
var prevpage int // Previous page address
|
||||
var nextpage int // Address on the last page
|
||||
// Generate the total number of pages based on the total number of nums and the number of prepage pages
|
||||
totalpages := int(math.Ceil(float64(nums) / float64(pageSize))) // Total number of Pages
|
||||
if page > totalpages {
|
||||
page = totalpages
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
var pages []int
|
||||
switch {
|
||||
case page >= totalpages-5 && totalpages > 5: // The last 5 pages
|
||||
start := totalpages - 5 + 1
|
||||
prevpage = page - 1
|
||||
nextpage = int(math.Min(float64(totalpages), float64(page+1)))
|
||||
pages = make([]int, 5)
|
||||
for i := range pages {
|
||||
pages[i] = start + i
|
||||
}
|
||||
case page >= 3 && totalpages > 5:
|
||||
start := page - 3 + 1
|
||||
pages = make([]int, 5)
|
||||
for i := range pages {
|
||||
pages[i] = start + i
|
||||
}
|
||||
prevpage = page - 1
|
||||
nextpage = page + 1
|
||||
default:
|
||||
pages = make([]int, int(math.Min(5, float64(totalpages))))
|
||||
for i := range pages {
|
||||
pages[i] = i + 1
|
||||
}
|
||||
prevpage = int(math.Max(float64(1), float64(page-1)))
|
||||
nextpage = page + 1
|
||||
}
|
||||
paginator := &schema.Paginator{}
|
||||
paginator.Pages = pages
|
||||
paginator.Totalpages = totalpages
|
||||
paginator.Prevpage = prevpage
|
||||
paginator.Nextpage = nextpage
|
||||
paginator.Currpage = page
|
||||
return paginator
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 templaterender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) {
|
||||
return t.questionService.GetQuestionPage(ctx, req)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfoResp, err error) {
|
||||
return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{})
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
|
||||
general, err := t.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site general failed:", err)
|
||||
return
|
||||
}
|
||||
siteInfo, err := t.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site GetSiteSeo failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
questions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize)
|
||||
if err != nil {
|
||||
log.Errorf("get sitemap questions failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Header("Content-Type", "application/xml")
|
||||
if len(questions) < constant.SitemapMaxSize {
|
||||
ctx.HTML(
|
||||
http.StatusOK, "sitemap.xml", gin.H{
|
||||
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
|
||||
"list": questions,
|
||||
"general": general,
|
||||
"hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
questionNum, err := t.questionRepo.GetQuestionCount(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetQuestionCount error", err)
|
||||
return
|
||||
}
|
||||
var pageList []int
|
||||
totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))
|
||||
for i := 1; i <= totalPages; i++ {
|
||||
pageList = append(pageList, i)
|
||||
}
|
||||
ctx.HTML(
|
||||
http.StatusOK, "sitemap-list.xml", gin.H{
|
||||
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
|
||||
"page": pageList,
|
||||
"general": general,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) OpenSearch(ctx *gin.Context) {
|
||||
general, err := t.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site general failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
favicon := general.SiteUrl + "/favicon.ico"
|
||||
branding, err := t.siteInfoService.GetSiteBranding(ctx)
|
||||
if err == nil && len(branding.Favicon) > 0 {
|
||||
favicon = branding.Favicon
|
||||
}
|
||||
|
||||
ctx.Header("Content-Type", "application/xml")
|
||||
ctx.HTML(
|
||||
http.StatusOK, "opensearch.xml", gin.H{
|
||||
"general": general,
|
||||
"favicon": favicon,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {
|
||||
general, err := t.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site general failed:", err)
|
||||
return err
|
||||
}
|
||||
siteInfo, err := t.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site GetSiteSeo failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
questions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize)
|
||||
if err != nil {
|
||||
log.Errorf("get sitemap questions failed: %s", err)
|
||||
return err
|
||||
}
|
||||
ctx.Header("Content-Type", "application/xml")
|
||||
ctx.HTML(
|
||||
http.StatusOK, "sitemap.xml", gin.H{
|
||||
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
|
||||
"list": questions,
|
||||
"general": general,
|
||||
"hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID,
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -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 templaterender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) {
|
||||
resp, err = q.tagService.GetTagWithPage(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) {
|
||||
dto := &schema.GetTagInfoReq{}
|
||||
_ = copier.Copy(dto, req)
|
||||
resp, err = q.tagService.GetTagInfo(ctx, dto)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
searchQuestion := &schema.QuestionPageReq{}
|
||||
searchQuestion.Page = req.Page
|
||||
searchQuestion.PageSize = req.PageSize
|
||||
searchQuestion.OrderCond = "newest"
|
||||
searchQuestion.Tag = req.Name
|
||||
searchQuestion.LoginUserID = req.UserID
|
||||
questionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return resp, questionList, questionCount, err
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 templaterender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/schema"
|
||||
)
|
||||
|
||||
func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) {
|
||||
return q.userService.GetOtherUserInfoByUsername(ctx, req)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/uploader"
|
||||
"github.com/apache/answer/pkg/converter"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// file is uploaded by markdown(or something else) editor
|
||||
fileFromPost = "post"
|
||||
// file is used to upload the post attachment
|
||||
fileFromPostAttachment = "post_attachment"
|
||||
// file is used to change the user's avatar
|
||||
fileFromAvatar = "avatar"
|
||||
// file is logo/icon images
|
||||
fileFromBranding = "branding"
|
||||
)
|
||||
|
||||
// UploadController upload controller
|
||||
type UploadController struct {
|
||||
uploaderService uploader.UploaderService
|
||||
}
|
||||
|
||||
// NewUploadController new controller
|
||||
func NewUploadController(uploaderService uploader.UploaderService) *UploadController {
|
||||
return &UploadController{
|
||||
uploaderService: uploaderService,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile upload file
|
||||
// @Summary upload file
|
||||
// @Description upload file
|
||||
// @Tags Upload
|
||||
// @Accept multipart/form-data
|
||||
// @Security ApiKeyAuth
|
||||
// @Param source formData string true "identify the source of the file upload" Enums(post, post_attachment, avatar, branding)
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200 {object} handler.RespBody{data=string}
|
||||
// @Router /answer/api/v1/file [post]
|
||||
func (uc *UploadController) UploadFile(ctx *gin.Context) {
|
||||
var (
|
||||
url string
|
||||
err error
|
||||
)
|
||||
|
||||
source := ctx.PostForm("source")
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
switch source {
|
||||
case fileFromAvatar:
|
||||
url, err = uc.uploaderService.UploadAvatarFile(ctx, userID)
|
||||
case fileFromPost:
|
||||
url, err = uc.uploaderService.UploadPostFile(ctx, userID)
|
||||
case fileFromBranding:
|
||||
if !middleware.GetIsAdminFromContext(ctx) {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
|
||||
return
|
||||
}
|
||||
url, err = uc.uploaderService.UploadBrandingFile(ctx, userID)
|
||||
case fileFromPostAttachment:
|
||||
url, err = uc.uploaderService.UploadPostAttachment(ctx, userID)
|
||||
default:
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
}
|
||||
|
||||
// PostRender render post content
|
||||
// @Summary render post content
|
||||
// @Description render post content
|
||||
// @Tags Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.PostRenderReq true "PostRenderReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/post/render [post]
|
||||
func (uc *UploadController) PostRender(ctx *gin.Context) {
|
||||
req := &schema.PostRenderReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content))
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/url"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/auth"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/export"
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
"github.com/apache/answer/internal/service/user_notification_config"
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// UserController user controller
|
||||
type UserController struct {
|
||||
userService *content.UserService
|
||||
authService *auth.AuthService
|
||||
actionService *action.CaptchaService
|
||||
emailService *export.EmailService
|
||||
siteInfoCommonService siteinfo_common.SiteInfoCommonService
|
||||
userNotificationConfigService *user_notification_config.UserNotificationConfigService
|
||||
}
|
||||
|
||||
// NewUserController new controller
|
||||
func NewUserController(
|
||||
authService *auth.AuthService,
|
||||
userService *content.UserService,
|
||||
actionService *action.CaptchaService,
|
||||
emailService *export.EmailService,
|
||||
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
|
||||
userNotificationConfigService *user_notification_config.UserNotificationConfigService,
|
||||
) *UserController {
|
||||
return &UserController{
|
||||
authService: authService,
|
||||
userService: userService,
|
||||
actionService: actionService,
|
||||
emailService: emailService,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
userNotificationConfigService: userNotificationConfigService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserInfoByUserID get user info, if user no login response http code is 200, but user info is null
|
||||
// @Summary GetUserInfoByUserID
|
||||
// @Description get user info, if user no login response http code is 200, but user info is null
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetCurrentLoginUserInfoResp}
|
||||
// @Router /answer/api/v1/user/info [get]
|
||||
func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) {
|
||||
token := middleware.ExtractToken(ctx)
|
||||
if len(token) == 0 {
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// if user is no login return null in data
|
||||
userInfo, _ := uc.authService.GetUserCacheInfo(ctx, token)
|
||||
if userInfo == nil {
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID)
|
||||
uc.setVisitCookies(ctx, userInfo.VisitToken, false)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetOtherUserInfoByUsername godoc
|
||||
// @Summary GetOtherUserInfoByUsername
|
||||
// @Description GetOtherUserInfoByUsername
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}
|
||||
// @Router /answer/api/v1/personal/user/info [get]
|
||||
func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserEmailLogin godoc
|
||||
// @Summary UserEmailLogin
|
||||
// @Description UserEmailLogin
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserEmailLoginReq true "UserEmailLogin"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}
|
||||
// @Router /answer/api/v1/user/login/email [post]
|
||||
func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
||||
req := &schema.UserEmailLoginReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := uc.userService.EmailLogin(ctx, req)
|
||||
if err != nil {
|
||||
uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, ctx.ClientIP())
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailOrPasswordWrong),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP())
|
||||
}
|
||||
if resp.Status == constant.UserSuspended {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})
|
||||
return
|
||||
}
|
||||
uc.setVisitCookies(ctx, resp.VisitToken, true)
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// RetrievePassWord godoc
|
||||
// @Summary RetrievePassWord
|
||||
// @Description RetrievePassWord
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserRetrievePassWordRequest true "UserRetrievePassWordRequest"
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/user/password/reset [post]
|
||||
func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
||||
req := &schema.UserRetrievePassWordRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UseRePassWord godoc
|
||||
// @Summary UseRePassWord
|
||||
// @Description UseRePassWord
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserRePassWordRequest true "UserRePassWordRequest"
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/user/password/replacement [post]
|
||||
func (uc *UserController) UseRePassWord(ctx *gin.Context) {
|
||||
req := &schema.UserRePassWordRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP())
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserLogout user logout
|
||||
// @Summary user logout
|
||||
// @Description user logout
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/logout [get]
|
||||
func (uc *UserController) UserLogout(ctx *gin.Context) {
|
||||
accessToken := middleware.ExtractToken(ctx)
|
||||
if len(accessToken) == 0 {
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
|
||||
_ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken)
|
||||
visitToken, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey)
|
||||
_ = uc.authService.RemoveUserVisitCacheInfo(ctx, visitToken)
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// UserRegisterByEmail godoc
|
||||
// @Summary UserRegisterByEmail
|
||||
// @Description UserRegisterByEmail
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserRegisterReq true "UserRegisterReq"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}
|
||||
// @Router /answer/api/v1/user/register/email [post]
|
||||
func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
||||
// check whether site allow register or not
|
||||
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
|
||||
return
|
||||
}
|
||||
|
||||
req := &schema.UserRegisterReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
|
||||
return
|
||||
}
|
||||
req.RequireEmailVerification = siteInfo.RequireEmailVerification
|
||||
req.IP = ctx.ClientIP()
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, req.IP, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)
|
||||
if len(errFields) > 0 {
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.
|
||||
Tr(handler.GetLangByCtx(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// UserVerifyEmail godoc
|
||||
// @Summary UserVerifyEmail
|
||||
// @Description UserVerifyEmail
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "code" default()
|
||||
// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}
|
||||
// @Router /answer/api/v1/user/email/verification [post]
|
||||
func (uc *UserController) UserVerifyEmail(ctx *gin.Context) {
|
||||
req := &schema.UserVerifyEmailReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UserVerifyEmail(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP())
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserVerifyEmailSend godoc
|
||||
// @Summary UserVerifyEmailSend
|
||||
// @Description UserVerifyEmailSend
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param captcha_id query string false "captcha_id" default()
|
||||
// @Param captcha_code query string false "captcha_code" default()
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/user/email/verification/send [post]
|
||||
func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
||||
req := &schema.UserVerifyEmailSendReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userInfo := middleware.GetUserInfoFromContext(ctx)
|
||||
if userInfo == nil {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserModifyPassWord godoc
|
||||
// @Summary UserModifyPassWord
|
||||
// @Description UserModifyPassWord
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/password [put]
|
||||
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
||||
req := &schema.UserModifyPasswordReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.AccessToken = middleware.ExtractToken(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID,
|
||||
req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID)
|
||||
}
|
||||
|
||||
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !oldPassVerification {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "old_pass",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
|
||||
if req.OldPass == req.Pass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
|
||||
return
|
||||
}
|
||||
err = uc.userService.UserModifyPassword(ctx, req)
|
||||
if err == nil {
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, req.UserID)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserUpdateInfo update user info
|
||||
// @Summary UserUpdateInfo update user info
|
||||
// @Description UserUpdateInfo update user info
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param Authorization header string true "access-token"
|
||||
// @Param data body schema.UpdateInfoRequest true "UpdateInfoRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/info [put]
|
||||
func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
||||
req := &schema.UpdateInfoRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetUserIsAdminModerator(ctx)
|
||||
errFields, err := uc.userService.UpdateInfo(ctx, req)
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.Tr(handler.GetLangByCtx(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface config
|
||||
// @Summary UserUpdateInterface update user interface config
|
||||
// @Description UserUpdateInterface update user interface config
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param Authorization header string true "access-token"
|
||||
// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/interface [put]
|
||||
func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
|
||||
req := &schema.UpdateUserInterfaceRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UserUpdateInterface(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// ActionRecord godoc
|
||||
// @Summary ActionRecord
|
||||
// @Description ActionRecord
|
||||
// @Tags User
|
||||
// @Param action query string true "action" Enums(login, e_mail, find_pass)
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody{data=schema.ActionRecordResp}
|
||||
// @Router /answer/api/v1/user/action/record [get]
|
||||
func (uc *UserController) ActionRecord(ctx *gin.Context) {
|
||||
req := &schema.ActionRecordReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userinfo := middleware.GetUserInfoFromContext(ctx)
|
||||
if userinfo != nil {
|
||||
req.UserID = userinfo.UserID
|
||||
}
|
||||
req.IP = ctx.ClientIP()
|
||||
resp := &schema.ActionRecordResp{}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if isAdmin {
|
||||
resp.Verify = false
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
} else {
|
||||
resp, err := uc.actionService.ActionRecord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserNotificationConfig get user's notification config
|
||||
// @Summary get user's notification config
|
||||
// @Description get user's notification config
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetUserNotificationConfigResp}
|
||||
// @Router /answer/api/v1/user/notification/config [post]
|
||||
func (uc *UserController) GetUserNotificationConfig(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := uc.userNotificationConfigService.GetUserNotificationConfig(ctx, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateUserNotificationConfig update user's notification config
|
||||
// @Summary update user's notification config
|
||||
// @Description update user's notification config
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdateUserNotificationConfigReq true "UpdateUserNotificationConfigReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/notification/config [put]
|
||||
func (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) {
|
||||
req := &schema.UpdateUserNotificationConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserChangeEmailSendCode send email to the user email then change their email
|
||||
// @Summary send email to the user email then change their email
|
||||
// @Description send email to the user email then change their email
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserChangeEmailSendCodeReq true "UserChangeEmailSendCodeReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/email/change/code [post]
|
||||
func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
||||
req := &schema.UserChangeEmailSendCodeReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
// If the user is not logged in, the api cannot be used.
|
||||
// If the user email is not verified, that also can use this api to modify the email.
|
||||
if len(req.UserID) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
// check whether email allow register or not
|
||||
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
|
||||
return
|
||||
}
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
if !isAdmin {
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, ctx.ClientIP())
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserChangeEmailVerify user change email verification
|
||||
// @Summary user change email verification
|
||||
// @Description user change email verification
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UserChangeEmailVerifyReq true "UserChangeEmailVerifyReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/email [put]
|
||||
func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {
|
||||
req := &schema.UserChangeEmailVerifyReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content)
|
||||
uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP())
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserRanking get user ranking
|
||||
// @Summary get user ranking
|
||||
// @Description get user ranking
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.UserRankingResp}
|
||||
// @Router /answer/api/v1/user/ranking [get]
|
||||
func (uc *UserController) UserRanking(ctx *gin.Context) {
|
||||
resp, err := uc.userService.UserRanking(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserStaff get user staff
|
||||
// @Summary get user staff
|
||||
// @Description get user staff
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string true "username"
|
||||
// @Param page_size query string true "page_size"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetUserStaffResp}
|
||||
// @Router /answer/api/v1/user/staff [get]
|
||||
func (uc *UserController) UserStaff(ctx *gin.Context) {
|
||||
req := &schema.GetUserStaffReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.GetUserStaff(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserUnsubscribeNotification unsubscribe notification
|
||||
// @Summary unsubscribe notification
|
||||
// @Description unsubscribe notification
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.UserUnsubscribeNotificationReq true "UserUnsubscribeNotificationReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/notification/unsubscribe [put]
|
||||
func (uc *UserController) UserUnsubscribeNotification(ctx *gin.Context) {
|
||||
req := &schema.UserUnsubscribeNotificationReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
err := uc.userService.UserUnsubscribeNotification(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// SearchUserListByName godoc
|
||||
// @Summary SearchUserListByName
|
||||
// @Description SearchUserListByName
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}
|
||||
// @Router /answer/api/v1/user/info/search [get]
|
||||
func (uc *UserController) SearchUserListByName(ctx *gin.Context) {
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := uc.userService.SearchUserListByName(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
func (uc *UserController) setVisitCookies(ctx *gin.Context, visitToken string, force bool) {
|
||||
if !force {
|
||||
cookie, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey)
|
||||
// If the cookie is the same as the visitToken, no need to set it again
|
||||
if cookie == visitToken {
|
||||
return
|
||||
}
|
||||
}
|
||||
general, err := uc.siteInfoCommonService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general error: %v", err)
|
||||
return
|
||||
}
|
||||
parsedURL, err := url.Parse(general.SiteUrl)
|
||||
if err != nil {
|
||||
log.Errorf("parse url error: %v", err)
|
||||
return
|
||||
}
|
||||
ctx.SetCookie(constant.UserVisitCookiesCacheKey,
|
||||
visitToken, constant.UserVisitCacheTime, "/", parsedURL.Hostname(), true, true)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/plugin_common"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserPluginController role controller
|
||||
type UserPluginController struct {
|
||||
pluginCommonService *plugin_common.PluginCommonService
|
||||
}
|
||||
|
||||
// NewUserPluginController new controller
|
||||
func NewUserPluginController(pluginCommonService *plugin_common.PluginCommonService) *UserPluginController {
|
||||
return &UserPluginController{pluginCommonService: pluginCommonService}
|
||||
}
|
||||
|
||||
// GetUserPluginList get plugin list that used for user.
|
||||
// @Summary get plugin list that used for user.
|
||||
// @Description get plugin list that used for user.
|
||||
// @Tags UserPlugin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetUserPluginListResp}
|
||||
// @Router /answer/api/v1/user/plugin/configs [get]
|
||||
func (pc *UserPluginController) GetUserPluginList(ctx *gin.Context) {
|
||||
resp := make([]*schema.GetUserPluginListResp, 0)
|
||||
_ = plugin.CallUserConfig(func(base plugin.UserConfig) error {
|
||||
info := base.Info()
|
||||
if plugin.StatusManager.IsEnabled(info.SlugName) {
|
||||
resp = append(resp, &schema.GetUserPluginListResp{
|
||||
Name: info.Name.Translate(ctx),
|
||||
SlugName: info.SlugName,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetUserPluginConfig get user plugin config
|
||||
// @Summary get user plugin config
|
||||
// @Description get user plugin config
|
||||
// @Tags UserPlugin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param plugin_slug_name query string true "plugin_slug_name"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}
|
||||
// @Router /answer/api/v1/user/plugin/config [get]
|
||||
func (pc *UserPluginController) GetUserPluginConfig(ctx *gin.Context) {
|
||||
req := &schema.GetUserPluginConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp := &schema.GetUserPluginConfigResp{}
|
||||
_ = plugin.CallUserConfig(func(fn plugin.UserConfig) error {
|
||||
if fn.Info().SlugName != req.PluginSlugName {
|
||||
return nil
|
||||
}
|
||||
info := fn.Info()
|
||||
resp.Name = info.Name.Translate(ctx)
|
||||
resp.SlugName = info.SlugName
|
||||
resp.SetConfigFields(ctx, fn.UserConfigFields())
|
||||
return nil
|
||||
})
|
||||
|
||||
configValue, err := pc.pluginCommonService.GetUserPluginConfig(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if len(configValue) > 0 {
|
||||
configValueMapping := make(map[string]any)
|
||||
_ = json.Unmarshal([]byte(configValue), &configValueMapping)
|
||||
for _, field := range resp.ConfigFields {
|
||||
if value, ok := configValueMapping[field.Name]; ok {
|
||||
field.Value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdatePluginUserConfig update user plugin config
|
||||
// @Summary update user plugin config
|
||||
// @Description update user plugin config
|
||||
// @Tags UserPlugin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdateUserPluginConfigReq true "UpdatePluginConfigReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/plugin/config [put]
|
||||
func (pc *UserPluginController) UpdatePluginUserConfig(ctx *gin.Context) {
|
||||
req := &schema.UpdateUserPluginConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
if !plugin.StatusManager.IsEnabled(req.PluginSlugName) {
|
||||
handler.HandleResponse(ctx, errors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
configFields, _ := json.Marshal(req.ConfigFields)
|
||||
err := plugin.CallUserConfig(func(fn plugin.UserConfig) error {
|
||||
if fn.Info().SlugName == req.PluginSlugName {
|
||||
return fn.UserConfigReceiver(req.UserID, configFields)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = pc.pluginCommonService.UpdatePluginUserConfig(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/action"
|
||||
"github.com/apache/answer/internal/service/content"
|
||||
"github.com/apache/answer/internal/service/rank"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// VoteController activity controller
|
||||
type VoteController struct {
|
||||
VoteService *content.VoteService
|
||||
rankService *rank.RankService
|
||||
actionService *action.CaptchaService
|
||||
}
|
||||
|
||||
// NewVoteController new controller
|
||||
func NewVoteController(
|
||||
voteService *content.VoteService,
|
||||
rankService *rank.RankService,
|
||||
actionService *action.CaptchaService,
|
||||
) *VoteController {
|
||||
return &VoteController{
|
||||
VoteService: voteService,
|
||||
rankService: rankService,
|
||||
actionService: actionService,
|
||||
}
|
||||
}
|
||||
|
||||
// VoteUp godoc
|
||||
// @Summary vote up
|
||||
// @Description add vote
|
||||
// @Tags Activity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.VoteReq true "vote"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.VoteResp}
|
||||
// @Router /answer/api/v1/vote/up [post]
|
||||
func (vc *VoteController) VoteUp(ctx *gin.Context) {
|
||||
req := &schema.VoteReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
if !isAdmin {
|
||||
captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID)
|
||||
}
|
||||
resp, err := vc.VoteService.VoteUp(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// VoteDown godoc
|
||||
// @Summary vote down
|
||||
// @Description add vote
|
||||
// @Tags Activity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.VoteReq true "vote"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.VoteResp}
|
||||
// @Router /answer/api/v1/vote/down [post]
|
||||
func (vc *VoteController) VoteDown(ctx *gin.Context) {
|
||||
req := &schema.VoteReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdmin := middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isAdmin {
|
||||
vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID)
|
||||
}
|
||||
resp, err := vc.VoteService.VoteDown(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// UserVotes user votes
|
||||
// @Summary get user personal votes
|
||||
// @Description get user personal votes
|
||||
// @Tags Activity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query int false "page size"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetVoteWithPageResp}}
|
||||
// @Router /answer/api/v1/personal/vote/page [get]
|
||||
func (vc *VoteController) UserVotes(ctx *gin.Context) {
|
||||
req := schema.GetVoteWithPageReq{}
|
||||
if handler.BindAndCheck(ctx, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := vc.VoteService.ListUserVotes(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user