Merge branch 'feat/1.2.5/notification' into test

# Conflicts:
#	i18n/en_US.yaml
#	i18n/zh_CN.yaml
This commit is contained in:
LinkinStars
2024-01-02 15:31:54 +08:00
45 changed files with 1397 additions and 238 deletions
+6 -2
View File
@@ -47,7 +47,9 @@ RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
FROM alpine
LABEL maintainer="linkinstar@apache.org"
ENV TZ "Asia/Shanghai"
ARG TIMEZONE
ENV TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
RUN apk update \
&& apk --no-cache add \
bash \
@@ -58,7 +60,9 @@ RUN apk update \
openssh \
sqlite \
gnupg \
&& echo "Asia/Shanghai" > /etc/timezone
tzdata \
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
&& echo "${TIMEZONE}" > /etc/timezone
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
COPY --from=golang-builder /data /data
+7 -5
View File
@@ -201,8 +201,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
collectionController := controller.NewCollectionController(collectionService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService)
externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService)
externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware)
answerController := controller.NewAnswerController(answerService, rankService, captchaService, siteInfoCommonService, rateLimitMiddleware)
@@ -227,7 +227,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService)
notificationRepo := notification2.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService, userExternalLoginRepo, siteInfoCommonService)
notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo)
notificationController := controller.NewNotificationController(notificationService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData)
@@ -241,10 +241,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
activityController := controller.NewActivityController(activityService)
roleController := controller_admin.NewRoleController(roleService)
pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configService, dataData)
pluginUserConfigRepo := plugin_config.NewPluginUserConfigRepo(dataData)
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, pluginUserConfigRepo, configService, dataData)
pluginController := controller_admin.NewPluginController(pluginCommonService)
permissionController := controller.NewPermissionController(rankService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController)
userPluginController := controller.NewUserPluginController(pluginCommonService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
+159 -24
View File
@@ -5876,6 +5876,132 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/plugin/config": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get user plugin config",
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "get user plugin config",
"parameters": [
{
"type": "string",
"description": "plugin_slug_name",
"name": "plugin_slug_name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetPluginConfigResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update user plugin config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "update user plugin config",
"parameters": [
{
"description": "UpdatePluginConfigReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserPluginConfigReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/plugin/configs": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get plugin list that used for user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "get plugin list that used for user.",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetUserPluginListResp"
}
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/ranking": {
"get": {
"security": [
@@ -7803,22 +7929,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"all_new_question": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"all_new_question_for_following_tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"inbox": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
}
},
@@ -7875,6 +7992,17 @@ const docTemplate = `{
}
}
},
"schema.GetUserPluginListResp": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"slug_name": {
"type": "string"
}
}
},
"schema.GetVoteWithPageResp": {
"type": "object",
"properties": {
@@ -9409,22 +9537,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"all_new_question": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"all_new_question_for_following_tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"inbox": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
}
},
@@ -9445,6 +9564,22 @@ const docTemplate = `{
}
}
},
"schema.UpdateUserPluginConfigReq": {
"type": "object",
"required": [
"plugin_slug_name"
],
"properties": {
"config_fields": {
"type": "object",
"additionalProperties": {}
},
"plugin_slug_name": {
"type": "string",
"maxLength": 100
}
}
},
"schema.UpdateUserRoleReq": {
"type": "object",
"required": [
+159 -24
View File
@@ -5864,6 +5864,132 @@
}
}
},
"/answer/api/v1/user/plugin/config": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get user plugin config",
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "get user plugin config",
"parameters": [
{
"type": "string",
"description": "plugin_slug_name",
"name": "plugin_slug_name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetPluginConfigResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update user plugin config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "update user plugin config",
"parameters": [
{
"description": "UpdatePluginConfigReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserPluginConfigReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/plugin/configs": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get plugin list that used for user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"UserPlugin"
],
"summary": "get plugin list that used for user.",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetUserPluginListResp"
}
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/ranking": {
"get": {
"security": [
@@ -7791,22 +7917,13 @@
"type": "object",
"properties": {
"all_new_question": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"all_new_question_for_following_tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"inbox": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
}
},
@@ -7863,6 +7980,17 @@
}
}
},
"schema.GetUserPluginListResp": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"slug_name": {
"type": "string"
}
}
},
"schema.GetVoteWithPageResp": {
"type": "object",
"properties": {
@@ -9397,22 +9525,13 @@
"type": "object",
"properties": {
"all_new_question": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"all_new_question_for_following_tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
},
"inbox": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
"$ref": "#/definitions/schema.NotificationChannelConfig"
}
}
},
@@ -9433,6 +9552,22 @@
}
}
},
"schema.UpdateUserPluginConfigReq": {
"type": "object",
"required": [
"plugin_slug_name"
],
"properties": {
"config_fields": {
"type": "object",
"additionalProperties": {}
},
"plugin_slug_name": {
"type": "string",
"maxLength": 100
}
}
},
"schema.UpdateUserRoleReq": {
"type": "object",
"required": [
+97 -18
View File
@@ -991,17 +991,11 @@ definitions:
schema.GetUserNotificationConfigResp:
properties:
all_new_question:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
all_new_question_for_following_tags:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
inbox:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
type: object
schema.GetUserPageResp:
properties:
@@ -1042,6 +1036,13 @@ definitions:
description: username
type: string
type: object
schema.GetUserPluginListResp:
properties:
name:
type: string
slug_name:
type: string
type: object
schema.GetVoteWithPageResp:
properties:
answer_id:
@@ -2099,17 +2100,11 @@ definitions:
schema.UpdateUserNotificationConfigReq:
properties:
all_new_question:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
all_new_question_for_following_tags:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
inbox:
items:
$ref: '#/definitions/schema.NotificationChannelConfig'
type: array
$ref: '#/definitions/schema.NotificationChannelConfig'
type: object
schema.UpdateUserPasswordReq:
properties:
@@ -2123,6 +2118,17 @@ definitions:
- password
- user_id
type: object
schema.UpdateUserPluginConfigReq:
properties:
config_fields:
additionalProperties: {}
type: object
plugin_slug_name:
maxLength: 100
type: string
required:
- plugin_slug_name
type: object
schema.UpdateUserRoleReq:
properties:
role_id:
@@ -5993,6 +5999,79 @@ paths:
summary: RetrievePassWord
tags:
- User
/answer/api/v1/user/plugin/config:
get:
description: get user plugin config
parameters:
- description: plugin_slug_name
in: query
name: plugin_slug_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetPluginConfigResp'
type: object
security:
- ApiKeyAuth: []
summary: get user plugin config
tags:
- UserPlugin
put:
consumes:
- application/json
description: update user plugin config
parameters:
- description: UpdatePluginConfigReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdateUserPluginConfigReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update user plugin config
tags:
- UserPlugin
/answer/api/v1/user/plugin/configs:
get:
consumes:
- application/json
description: get plugin list that used for user.
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.GetUserPluginListResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get plugin list that used for user.
tags:
- UserPlugin
/answer/api/v1/user/ranking:
get:
consumes:
+1
View File
@@ -46,4 +46,5 @@ var ProviderSetController = wire.NewSet(
NewConnectorController,
NewUserCenterController,
NewPermissionController,
NewUserPluginController,
)
@@ -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 (
"encoding/json"
"github.com/apache/incubator-answer/internal/base/middleware"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/segmentfault/pacman/errors"
"net/http"
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/schema"
"github.com/apache/incubator-answer/internal/service/plugin_common"
"github.com/apache/incubator-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)
}
@@ -31,12 +31,12 @@ import (
// PluginController role controller
type PluginController struct {
PluginCommonService *plugin_common.PluginCommonService
pluginCommonService *plugin_common.PluginCommonService
}
// NewPluginController new controller
func NewPluginController(PluginCommonService *plugin_common.PluginCommonService) *PluginController {
return &PluginController{PluginCommonService: PluginCommonService}
func NewPluginController(pluginCommonService *plugin_common.PluginCommonService) *PluginController {
return &PluginController{pluginCommonService: pluginCommonService}
}
// GetAllPluginStatus get all plugins status
@@ -150,7 +150,7 @@ func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) {
}
plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled)
err := pc.PluginCommonService.UpdatePluginStatus(ctx)
err := pc.pluginCommonService.UpdatePluginStatus(ctx)
handler.HandleResponse(ctx, err, nil)
}
@@ -220,6 +220,6 @@ func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) {
return
}
err = pc.PluginCommonService.UpdatePluginConfig(ctx, req)
err = pc.pluginCommonService.UpdatePluginConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package entity
// PluginUserConfig plugin config
type PluginUserConfig struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
UserID string `xorm:"not null default 0 BIGINT(20) UNIQUE(uk_up) user_id"`
PluginSlugName string `xorm:"VARCHAR(128) UNIQUE(uk_up) plugin_slug_name"`
Value string `xorm:"TEXT value"`
}
// TableName config table name
func (PluginUserConfig) TableName() string {
return "plugin_user_config"
}
+1
View File
@@ -67,6 +67,7 @@ var (
&entity.PluginConfig{},
&entity.UserExternalLogin{},
&entity.UserNotificationConfig{},
&entity.PluginUserConfig{},
}
roles = []*entity.Role{
+1
View File
@@ -94,6 +94,7 @@ var migrations = []Migration{
NewMigration("v1.1.3", "set default user notification config", setDefaultUserNotificationConfig, false),
NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true),
NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true),
NewMigration("v1.2.5", "add notification plugin", addNotificationPlugin, true),
}
func GetMigrations() []Migration {
+30
View File
@@ -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 migrations
import (
"context"
"github.com/apache/incubator-answer/internal/entity"
"xorm.io/xorm"
)
func addNotificationPlugin(ctx context.Context, x *xorm.Engine) error {
return x.Context(ctx).Sync(new(entity.PluginUserConfig))
}
@@ -50,7 +50,7 @@ func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName
old.Value = configValue
_, err = ur.data.DB.Context(ctx).ID(old.ID).Update(old)
} else {
_, err = ur.data.DB.Context(ctx).InsertOne(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
_, err = ur.data.DB.Context(ctx).Insert(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package plugin_config
import (
"context"
"xorm.io/xorm"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/entity"
"github.com/apache/incubator-answer/internal/service/plugin_common"
"github.com/segmentfault/pacman/errors"
)
type pluginUserConfigRepo struct {
data *data.Data
}
// NewPluginUserConfigRepo new repository
func NewPluginUserConfigRepo(data *data.Data) plugin_common.PluginUserConfigRepo {
return &pluginUserConfigRepo{
data: data,
}
}
func (ur *pluginUserConfigRepo) SaveUserPluginConfig(ctx context.Context, userID string,
pluginSlugName, configValue string) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
session = session.Context(ctx)
old := &entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
}
exist, err := session.Get(old)
if err != nil {
return nil, err
}
if exist {
old.Value = configValue
_, err = session.ID(old.ID).Update(old)
} else {
_, err = session.Insert(&entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
Value: configValue,
})
}
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *pluginUserConfigRepo) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) (
pluginUserConfig *entity.PluginUserConfig, exist bool, err error) {
pluginUserConfig = &entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
}
exist, err = ur.data.DB.Context(ctx).Get(pluginUserConfig)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return pluginUserConfig, exist, err
}
+1
View File
@@ -96,4 +96,5 @@ var ProviderSetRepo = wire.NewSet(
plugin_config.NewPluginConfigRepo,
user_notification_config.NewUserNotificationConfigRepo,
limit.NewRateLimitRepo,
plugin_config.NewPluginUserConfigRepo,
)
+22 -22
View File
@@ -23,18 +23,13 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/apache/incubator-answer/plugin"
"github.com/segmentfault/pacman/log"
"strings"
"time"
"unicode"
"xorm.io/xorm"
"github.com/apache/incubator-answer/internal/base/handler"
"xorm.io/builder"
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/base/pager"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/entity"
@@ -43,8 +38,11 @@ import (
"github.com/apache/incubator-answer/internal/service/unique"
"github.com/apache/incubator-answer/pkg/htmltext"
"github.com/apache/incubator-answer/pkg/uid"
"github.com/apache/incubator-answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// questionRepo question repository
@@ -77,7 +75,6 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
_ = qr.updateSearch(ctx, question.ID)
return
}
@@ -101,7 +98,7 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return
}
@@ -112,7 +109,7 @@ func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (e
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
@@ -124,7 +121,7 @@ func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
@@ -156,7 +153,7 @@ func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, questionID str
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, questionID)
_ = qr.UpdateSearch(ctx, questionID)
return nil
}
@@ -166,7 +163,7 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
@@ -176,7 +173,7 @@ func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, questionID)
_ = qr.UpdateSearch(ctx, questionID)
return nil
}
@@ -195,7 +192,7 @@ func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Que
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
@@ -205,7 +202,7 @@ func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Q
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.updateSearch(ctx, question.ID)
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
@@ -347,19 +344,22 @@ func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int
}
// GetQuestionPage query question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden bool) (
questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
session := qr.data.DB.Context(ctx).Where("question.status = ? OR question.status = ?",
entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
if len(tagID) > 0 {
if len(tagIDs) > 0 {
session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
session.And("tag_rel.tag_id = ?", tagID)
session.In("tag_rel.tag_id", tagIDs)
session.And("tag_rel.status = ?", entity.TagRelStatusAvailable)
}
if len(userID) > 0 {
session.And("question.user_id = ?", userID)
if !showHidden {
session.And("question.show = ?", entity.QuestionShow)
}
} else {
session.And("question.show = ?", entity.QuestionShow)
}
@@ -462,8 +462,8 @@ func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.Ad
return rows, count, nil
}
// updateSearch update search, if search plugin not enable, do nothing
func (qr *questionRepo) updateSearch(ctx context.Context, questionID string) (err error) {
// UpdateSearch update search, if search plugin not enable, do nothing
func (qr *questionRepo) UpdateSearch(ctx context.Context, questionID string) (err error) {
// check search plugin
var s plugin.Search
_ = plugin.CallSearch(func(search plugin.Search) error {
@@ -544,7 +544,7 @@ func (qr *questionRepo) RemoveAllUserQuestion(ctx context.Context, userID string
// update search content
for _, id := range questionIDs {
_ = qr.updateSearch(ctx, id)
_ = qr.UpdateSearch(ctx, id)
}
return nil
}
+25 -15
View File
@@ -98,7 +98,7 @@ func NewSearchRepo(
}
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
@@ -152,16 +152,20 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
And(builder.Eq{
ast + ".tag_id": tagID,
ast + ".status": entity.TagRelStatusAvailable,
})
}).
And(builder.In(ast+".tag_id", tagID))
ub.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
And(builder.Eq{
ast + ".tag_id": tagID,
ast + ".status": entity.TagRelStatusAvailable,
})
argsQ = append(argsQ, entity.TagRelStatusAvailable, tagID)
argsA = append(argsA, entity.TagRelStatusAvailable, tagID)
}).
And(builder.In(ast+".tag_id", tagID))
argsQ = append(argsQ, entity.TagRelStatusAvailable)
argsA = append(argsA, entity.TagRelStatusAvailable)
for _, t := range tagID {
argsQ = append(argsQ, t)
argsA = append(argsA, t)
}
}
// check user
@@ -236,7 +240,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
}
// SearchQuestions search question data
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
qfs = qFields
@@ -269,10 +273,13 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
And(builder.Eq{
ast + ".tag_id": tagID,
ast + ".status": entity.TagRelStatusAvailable,
})
args = append(args, entity.TagRelStatusAvailable, tagID)
}).
And(builder.In(ast+".tag_id", tagID))
args = append(args, entity.TagRelStatusAvailable)
for _, t := range tagID {
args = append(args, t)
}
}
// check need filter has not accepted
@@ -343,7 +350,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
}
// SearchAnswers search answer data
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
@@ -378,10 +385,13 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
And(builder.Eq{
ast + ".tag_id": tagID,
ast + ".status": entity.TagRelStatusAvailable,
})
args = append(args, entity.TagRelStatusAvailable, tagID)
}).
And(builder.In(ast+".tag_id", tagID))
args = append(args, entity.TagRelStatusAvailable)
for _, t := range tagID {
args = append(args, t)
}
}
// check limit accepted
+10
View File
@@ -21,6 +21,7 @@ package tag
import (
"context"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/entity"
@@ -118,6 +119,15 @@ func (tr *tagRepo) GetTagSynonymCount(ctx context.Context, tagID string) (count
return
}
func (tr *tagRepo) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) {
session := tr.data.DB.Context(ctx).Table(entity.Tag{}.TableName()).Where(builder.Eq{"status": entity.TagStatusAvailable, "main_tag_id": converter.StringToInt64(mainTagID)}).Cols("id")
err = session.Find(&tagIDs)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagList get tag list all
func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
@@ -72,6 +72,17 @@ func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider,
return
}
// GetByUserID get by user ID
func (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, userID string) (
userInfo *entity.UserExternalLogin, exist bool, err error) {
userInfo = &entity.UserExternalLogin{}
exist, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Where("provider = ?", provider).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserExternalLoginList get by external ID
func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (
resp []*entity.UserExternalLogin, err error) {
+7
View File
@@ -53,6 +53,7 @@ type AnswerAPIRouter struct {
roleController *controller_admin.RoleController
pluginController *controller_admin.PluginController
permissionController *controller.PermissionController
userPluginController *controller.UserPluginController
}
func NewAnswerAPIRouter(
@@ -82,6 +83,7 @@ func NewAnswerAPIRouter(
roleController *controller_admin.RoleController,
pluginController *controller_admin.PluginController,
permissionController *controller.PermissionController,
userPluginController *controller.UserPluginController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -110,6 +112,7 @@ func NewAnswerAPIRouter(
roleController: roleController,
pluginController: pluginController,
permissionController: permissionController,
userPluginController: userPluginController,
}
}
@@ -268,6 +271,10 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/activity/timeline", a.activityController.GetObjectTimeline)
r.GET("/activity/timeline/detail", a.activityController.GetObjectTimelineDetail)
// plugin
r.GET("/user/plugin/configs", a.userPluginController.GetUserPluginList)
r.GET("/user/plugin/config", a.userPluginController.GetUserPluginConfig)
r.PUT("/user/plugin/config", a.userPluginController.UpdatePluginUserConfig)
}
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
+100
View File
@@ -0,0 +1,100 @@
/*
* 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 schema
import (
"github.com/apache/incubator-answer/plugin"
"github.com/gin-gonic/gin"
)
type GetUserPluginListResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
}
type UpdateUserPluginReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
UserID string `json:"-"`
}
type GetUserPluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"`
UserID string `json:"-"`
}
type GetUserPluginConfigResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
ConfigFields []*ConfigField `json:"config_fields"`
}
func (g *GetUserPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {
for _, field := range fields {
configField := &ConfigField{
Name: field.Name,
Type: string(field.Type),
Title: field.Title.Translate(ctx),
Description: field.Description.Translate(ctx),
Required: field.Required,
Value: field.Value,
UIOptions: ConfigFieldUIOptions{
Rows: field.UIOptions.Rows,
InputType: string(field.UIOptions.InputType),
Variant: field.UIOptions.Variant,
},
}
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
if field.UIOptions.Action != nil {
uiOptionAction := &UIOptionAction{
Url: field.UIOptions.Action.Url,
Method: field.UIOptions.Action.Method,
}
if field.UIOptions.Action.Loading != nil {
uiOptionAction.Loading = &LoadingAction{
Text: field.UIOptions.Action.Loading.Text.Translate(ctx),
State: string(field.UIOptions.Action.Loading.State),
}
}
if field.UIOptions.Action.OnComplete != nil {
uiOptionAction.OnCompleteAction = &OnCompleteAction{
ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,
RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig,
}
}
configField.UIOptions.Action = uiOptionAction
}
for _, option := range field.Options {
configField.Options = append(configField.Options, ConfigFieldOption{
Label: option.Label.Translate(ctx),
Value: option.Value,
})
}
g.ConfigFields = append(g.ConfigFields, configField)
}
}
type UpdateUserPluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
ConfigFields map[string]any `json:"config_fields"`
UserID string `json:"-"`
}
+1 -1
View File
@@ -64,7 +64,7 @@ type SearchCondition struct {
// only show this question's answer
QuestionID string
// search query tags
Tags []string
Tags [][]string
// search query keywords
Words []string
}
+23 -74
View File
@@ -38,41 +38,13 @@ func NewNotificationChannelsFormJson(jsonStr string) NotificationChannels {
return list
}
func (n *NotificationChannels) Format(sequences []constant.NotificationChannelKey) {
if n == nil {
*n = make([]*NotificationChannelConfig, 0)
return
func NewNotificationChannelConfigFormJson(jsonStr string) NotificationChannelConfig {
var list NotificationChannels
_ = json.Unmarshal([]byte(jsonStr), &list)
if len(list) > 0 {
return *list[0]
}
mapping := make(map[constant.NotificationChannelKey]*NotificationChannelConfig)
for _, item := range *n {
mapping[item.Key] = &NotificationChannelConfig{
Key: item.Key,
Enable: item.Enable,
}
}
newList := make([]*NotificationChannelConfig, 0)
for _, ch := range sequences {
if c, ok := mapping[ch]; ok {
newList = append(newList, c)
} else {
newList = append(newList, &NotificationChannelConfig{
Key: ch,
})
}
}
*n = newList
}
func (n *NotificationChannels) CheckEnable(ch constant.NotificationChannelKey) bool {
if n == nil {
return false
}
for _, item := range *n {
if item.Key == ch {
return item.Enable
}
}
return false
return NotificationChannelConfig{}
}
func (n *NotificationChannels) ToJsonString() string {
@@ -81,62 +53,39 @@ func (n *NotificationChannels) ToJsonString() string {
}
type NotificationConfig struct {
Inbox NotificationChannels `json:"inbox"`
AllNewQuestion NotificationChannels `json:"all_new_question"`
AllNewQuestionForFollowingTags NotificationChannels `json:"all_new_question_for_following_tags"`
}
func (n *NotificationConfig) ToJsonString() string {
data, _ := json.Marshal(n)
return string(data)
Inbox NotificationChannelConfig `json:"inbox"`
AllNewQuestion NotificationChannelConfig `json:"all_new_question"`
AllNewQuestionForFollowingTags NotificationChannelConfig `json:"all_new_question_for_following_tags"`
}
func NewNotificationConfig(configs []*entity.UserNotificationConfig) NotificationConfig {
nc := NotificationConfig{}
nc.Inbox = make([]*NotificationChannelConfig, 0)
nc.AllNewQuestion = make([]*NotificationChannelConfig, 0)
nc.AllNewQuestionForFollowingTags = make([]*NotificationChannelConfig, 0)
for _, item := range configs {
switch item.Source {
case string(constant.InboxSource):
nc.Inbox = NewNotificationChannelsFormJson(item.Channels)
nc.Inbox = NewNotificationChannelConfigFormJson(item.Channels)
case string(constant.AllNewQuestionSource):
nc.AllNewQuestion = NewNotificationChannelsFormJson(item.Channels)
nc.AllNewQuestion = NewNotificationChannelConfigFormJson(item.Channels)
case string(constant.AllNewQuestionForFollowingTagsSource):
nc.AllNewQuestionForFollowingTags = NewNotificationChannelsFormJson(item.Channels)
nc.AllNewQuestionForFollowingTags = NewNotificationChannelConfigFormJson(item.Channels)
}
}
return nc
}
func (n *NotificationConfig) FromJsonString(data string) {
if len(data) > 0 {
_ = json.Unmarshal([]byte(data), n)
return
}
n.Inbox = make([]*NotificationChannelConfig, 0)
n.AllNewQuestion = make([]*NotificationChannelConfig, 0)
n.AllNewQuestionForFollowingTags = make([]*NotificationChannelConfig, 0)
return
}
func (n *NotificationConfig) Format() {
n.Inbox.Format([]constant.NotificationChannelKey{constant.EmailChannel})
n.AllNewQuestion.Format([]constant.NotificationChannelKey{constant.EmailChannel})
n.AllNewQuestionForFollowingTags.Format([]constant.NotificationChannelKey{constant.EmailChannel})
}
func (n *NotificationConfig) CheckEnable(
source constant.NotificationSource, channel constant.NotificationChannelKey) bool {
switch source {
case constant.InboxSource:
return n.Inbox.CheckEnable(channel)
case constant.AllNewQuestionSource:
return n.AllNewQuestion.CheckEnable(channel)
case constant.AllNewQuestionForFollowingTagsSource:
return n.AllNewQuestionForFollowingTags.CheckEnable(channel)
if n.Inbox.Key == "" {
n.Inbox.Key = constant.EmailChannel
n.Inbox.Enable = false
}
if n.AllNewQuestion.Key == "" {
n.AllNewQuestion.Key = constant.EmailChannel
n.AllNewQuestion.Enable = false
}
if n.AllNewQuestionForFollowingTags.Key == "" {
n.AllNewQuestionForFollowingTags.Key = constant.EmailChannel
n.AllNewQuestionForFollowingTags.Enable = false
}
return false
}
// UpdateUserNotificationConfigReq update user notification config request
+1
View File
@@ -325,6 +325,7 @@ type UserBasicInfo struct {
Avatar string `json:"avatar"`
Website string `json:"website"`
Location string `json:"location"`
Language string `json:"language"`
Status string `json:"status"`
}
@@ -21,14 +21,20 @@ package notification
import (
"context"
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/schema"
"github.com/apache/incubator-answer/internal/service/activity_common"
"github.com/apache/incubator-answer/internal/service/export"
"github.com/apache/incubator-answer/internal/service/notice_queue"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
usercommon "github.com/apache/incubator-answer/internal/service/user_common"
"github.com/apache/incubator-answer/internal/service/user_external_login"
"github.com/apache/incubator-answer/internal/service/user_notification_config"
"github.com/apache/incubator-answer/pkg/display"
"github.com/apache/incubator-answer/plugin"
"github.com/segmentfault/pacman/log"
"strings"
)
type ExternalNotificationService struct {
@@ -38,6 +44,8 @@ type ExternalNotificationService struct {
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.ExternalNotificationQueueService
userExternalLoginRepo user_external_login.UserExternalLoginRepo
siteInfoService siteinfo_common.SiteInfoCommonService
}
func NewExternalNotificationService(
@@ -47,6 +55,8 @@ func NewExternalNotificationService(
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.ExternalNotificationQueueService,
userExternalLoginRepo user_external_login.UserExternalLoginRepo,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *ExternalNotificationService {
n := &ExternalNotificationService{
data: data,
@@ -55,6 +65,8 @@ func NewExternalNotificationService(
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
userExternalLoginRepo: userExternalLoginRepo,
siteInfoService: siteInfoService,
}
notificationQueueService.RegisterHandler(n.Handler)
return n
@@ -78,3 +90,64 @@ func (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.
log.Errorf("unknown notification message: %+v", msg)
return nil
}
func (ns *ExternalNotificationService) syncNotificationToPlugin(ctx context.Context,
source constant.NotificationSource, msg *schema.ExternalNotificationMsg) {
pluginNotificationMsg := &plugin.NotificationMessage{
ReceiverUserID: msg.ReceiverUserID,
ReceiverLang: msg.ReceiverLang,
}
switch source {
case constant.InboxSource:
return
case constant.AllNewQuestionSource:
pluginNotificationMsg.Type = plugin.NotificationNewQuestion
pluginNotificationMsg = ns.newPluginQuestionNotification(ctx, msg)
case constant.AllNewQuestionForFollowingTagsSource:
pluginNotificationMsg.Type = plugin.NotificationNewQuestionFollowedTag
pluginNotificationMsg = ns.newPluginQuestionNotification(ctx, msg)
}
if len(msg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 {
userInfo, _, _ := ns.userRepo.GetByUserID(ctx, msg.ReceiverUserID)
if userInfo != nil {
pluginNotificationMsg.ReceiverLang = userInfo.Language
}
}
_ = plugin.CallNotification(func(fn plugin.Notification) error {
userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID)
if err != nil {
log.Errorf("get user external login info failed: %v", err)
return nil
}
if exist {
pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID
}
fn.Notify(pluginNotificationMsg)
return nil
})
}
func (ns *ExternalNotificationService) newPluginQuestionNotification(
ctx context.Context, msg *schema.ExternalNotificationMsg) (raw *plugin.NotificationMessage) {
raw = &plugin.NotificationMessage{
ReceiverUserID: msg.ReceiverUserID,
ReceiverLang: msg.ReceiverLang,
QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle,
QuestionTags: strings.Join(msg.NewQuestionTemplateRawData.Tags, ","),
}
siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return raw
}
seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)
if err != nil {
return raw
}
raw.QuestionUrl = display.QuestionURL(
seoInfo.Permalink, siteInfo.SiteUrl,
msg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle)
return raw
}
@@ -49,6 +49,7 @@ func (ns *ExternalNotificationService) handleInviteAnswerNotification(ctx contex
ns.sendInviteAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewInviteAnswerTemplateRawData)
}
}
ns.syncNotificationToPlugin(ctx, constant.InboxSource, msg)
return nil
}
@@ -49,6 +49,7 @@ func (ns *ExternalNotificationService) handleNewAnswerNotification(ctx context.C
ns.sendNewAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewAnswerTemplateRawData)
}
}
ns.syncNotificationToPlugin(ctx, constant.InboxSource, msg)
return nil
}
@@ -49,7 +49,7 @@ func (ns *ExternalNotificationService) handleNewCommentNotification(ctx context.
ns.sendNewCommentNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewCommentTemplateRawData)
}
}
ns.syncNotificationToPlugin(ctx, constant.InboxSource, msg)
return nil
}
@@ -24,14 +24,16 @@ import (
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/schema"
"github.com/apache/incubator-answer/pkg/token"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"time"
)
type NewQuestionSubscriber struct {
UserID string `json:"user_id"`
Channels schema.NotificationChannels `json:"channels"`
UserID string `json:"user_id"`
Channels schema.NotificationChannels `json:"channels"`
NotificationSource constant.NotificationSource `json:"notification_source"`
}
func (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context.Context,
@@ -59,6 +61,10 @@ func (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context
})
}
}
pluginMsg := &schema.ExternalNotificationMsg{}
_ = copier.Copy(pluginMsg, msg)
pluginMsg.ReceiverUserID = subscriber.UserID
ns.syncNotificationToPlugin(ctx, subscriber.NotificationSource, pluginMsg)
}
return nil
}
@@ -94,8 +100,9 @@ func (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Con
continue
}
subscribersMapping[userNotificationConfig.UserID] = &NewQuestionSubscriber{
UserID: userNotificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels),
UserID: userNotificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels),
NotificationSource: constant.AllNewQuestionForFollowingTagsSource,
}
}
log.Debugf("get %d subscribers from tags", len(subscribersMapping))
@@ -113,8 +120,9 @@ func (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Con
continue
}
subscribersMapping[notificationConfig.UserID] = &NewQuestionSubscriber{
UserID: notificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(notificationConfig.Channels),
UserID: notificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(notificationConfig.Channels),
NotificationSource: constant.AllNewQuestionSource,
}
}
@@ -22,6 +22,9 @@ package notificationcommon
import (
"context"
"fmt"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
"github.com/apache/incubator-answer/internal/service/user_external_login"
"github.com/apache/incubator-answer/pkg/display"
"time"
"github.com/apache/incubator-answer/internal/base/constant"
@@ -59,6 +62,8 @@ type NotificationCommon struct {
userCommon *usercommon.UserCommon
objectInfoService *object_info.ObjService
notificationQueueService notice_queue.NotificationQueueService
userExternalLoginRepo user_external_login.UserExternalLoginRepo
siteInfoService siteinfo_common.SiteInfoCommonService
}
func NewNotificationCommon(
@@ -69,6 +74,8 @@ func NewNotificationCommon(
followRepo activity_common.FollowRepo,
objectInfoService *object_info.ObjService,
notificationQueueService notice_queue.NotificationQueueService,
userExternalLoginRepo user_external_login.UserExternalLoginRepo,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *NotificationCommon {
notification := &NotificationCommon{
data: data,
@@ -78,6 +85,8 @@ func NewNotificationCommon(
userCommon: userCommon,
objectInfoService: objectInfoService,
notificationQueueService: notificationQueueService,
userExternalLoginRepo: userExternalLoginRepo,
siteInfoService: siteInfoService,
}
notificationQueueService.RegisterHandler(notification.AddNotification)
return notification
@@ -183,6 +192,10 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N
}
go ns.SendNotificationToAllFollower(ctx, msg, questionID)
if msg.Type == schema.NotificationTypeInbox {
ns.syncNotificationToPlugin(ctx, objInfo, msg)
}
return nil
}
@@ -226,3 +239,72 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
ns.notificationQueueService.Send(ctx, t)
}
}
func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo,
msg *schema.NotificationMsg) {
siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site general info failed: %v", err)
return
}
seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Errorf("get site seo info failed: %v", err)
return
}
objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
pluginNotificationMsg := &plugin.NotificationMessage{
Type: plugin.NotificationType(msg.NotificationAction),
ReceiverUserID: msg.ReceiverUserID,
TriggerUserID: msg.TriggerUserID,
QuestionTitle: objInfo.Title,
}
if len(objInfo.QuestionID) > 0 {
pluginNotificationMsg.QuestionUrl =
display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title)
}
if len(objInfo.AnswerID) > 0 {
pluginNotificationMsg.AnswerUrl =
display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID)
}
if len(objInfo.CommentID) > 0 {
pluginNotificationMsg.CommentUrl =
display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID)
}
if len(msg.TriggerUserID) > 0 {
triggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID)
if err != nil {
log.Errorf("get trigger user basic info failed: %v", err)
return
}
if exist {
pluginNotificationMsg.TriggerUserID = triggerUser.ID
pluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName
pluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)
}
}
if len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 {
userInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID)
if userInfo != nil {
pluginNotificationMsg.ReceiverLang = userInfo.Language
}
}
_ = plugin.CallNotification(func(fn plugin.Notification) error {
userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID)
if err != nil {
log.Errorf("get user external login info failed: %v", err)
return nil
}
if exist {
pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID
}
fn.Notify(pluginNotificationMsg)
return nil
})
}
@@ -22,12 +22,11 @@ package plugin_common
import (
"context"
"encoding/json"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/repo/search_sync"
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/entity"
"github.com/apache/incubator-answer/internal/repo/search_sync"
"github.com/apache/incubator-answer/internal/schema"
"github.com/apache/incubator-answer/internal/service/config"
"github.com/apache/incubator-answer/plugin"
@@ -40,16 +39,24 @@ type PluginConfigRepo interface {
GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error)
}
type PluginUserConfigRepo interface {
SaveUserPluginConfig(ctx context.Context, userID string, pluginSlugName, configValue string) (err error)
GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) (
pluginUserConfig *entity.PluginUserConfig, exist bool, err error)
}
// PluginCommonService user service
type PluginCommonService struct {
configService *config.ConfigService
pluginConfigRepo PluginConfigRepo
data *data.Data
configService *config.ConfigService
pluginConfigRepo PluginConfigRepo
pluginUserConfigRepo PluginUserConfigRepo
data *data.Data
}
// NewPluginCommonService new report service
func NewPluginCommonService(
pluginConfigRepo PluginConfigRepo,
pluginUserConfigRepo PluginUserConfigRepo,
configService *config.ConfigService,
data *data.Data,
) *PluginCommonService {
@@ -82,10 +89,24 @@ func NewPluginCommonService(
}
}
// init plugin user config
plugin.RegisterGetPluginUserConfigFunc(func(userID, pluginSlugName string) []byte {
pluginUserConfig, exist, err := pluginUserConfigRepo.GetPluginUserConfig(context.Background(), userID, pluginSlugName)
if err != nil {
log.Error(err)
return nil
}
if !exist {
return nil
}
return []byte(pluginUserConfig.Value)
})
return &PluginCommonService{
configService: configService,
pluginConfigRepo: pluginConfigRepo,
data: data,
configService: configService,
pluginConfigRepo: pluginConfigRepo,
pluginUserConfigRepo: pluginUserConfigRepo,
data: data,
}
}
@@ -114,3 +135,26 @@ func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *sche
})
return nil
}
// UpdatePluginUserConfig update plugin config
func (ps *PluginCommonService) UpdatePluginUserConfig(ctx context.Context, req *schema.UpdateUserPluginConfigReq) (err error) {
configValue, _ := json.Marshal(req.ConfigFields)
err = ps.pluginUserConfigRepo.SaveUserPluginConfig(ctx, req.UserID, req.PluginSlugName, string(configValue))
if err != nil {
return err
}
return nil
}
// GetUserPluginConfig get user plugin config
func (ps *PluginCommonService) GetUserPluginConfig(ctx context.Context, req *schema.GetUserPluginConfigReq) (
configValue string, err error) {
pluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(ctx, req.UserID, req.PluginSlugName)
if err != nil {
return "", err
}
if !exist {
return "", nil
}
return pluginUserConfig.Value, nil
}
+2 -1
View File
@@ -54,7 +54,7 @@ type QuestionRepo interface {
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden bool) (
questionList []*entity.Question, total int64, err error)
UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error)
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
@@ -72,6 +72,7 @@ type QuestionRepo interface {
GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error)
SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
RemoveAllUserQuestion(ctx context.Context, userID string) (err error)
UpdateSearch(ctx context.Context, questionID string) (err error)
}
// QuestionCommon user service
+27 -6
View File
@@ -22,9 +22,6 @@ package service
import (
"encoding/json"
"fmt"
"github.com/apache/incubator-answer/internal/service/notification"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
"github.com/apache/incubator-answer/pkg/token"
"strings"
"time"
@@ -42,13 +39,17 @@ import (
"github.com/apache/incubator-answer/internal/service/export"
"github.com/apache/incubator-answer/internal/service/meta"
"github.com/apache/incubator-answer/internal/service/notice_queue"
"github.com/apache/incubator-answer/internal/service/notification"
"github.com/apache/incubator-answer/internal/service/permission"
questioncommon "github.com/apache/incubator-answer/internal/service/question_common"
"github.com/apache/incubator-answer/internal/service/revision_common"
"github.com/apache/incubator-answer/internal/service/role"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
tagcommon "github.com/apache/incubator-answer/internal/service/tag_common"
usercommon "github.com/apache/incubator-answer/internal/service/user_common"
"github.com/apache/incubator-answer/pkg/converter"
"github.com/apache/incubator-answer/pkg/htmltext"
"github.com/apache/incubator-answer/pkg/token"
"github.com/apache/incubator-answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
@@ -65,6 +66,7 @@ type QuestionService struct {
questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
userRoleRelService *role.UserRoleRelService
revisionService *revision_common.RevisionService
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
@@ -83,6 +85,7 @@ func NewQuestionService(
questioncommon *questioncommon.QuestionCommon,
userCommon *usercommon.UserCommon,
userRepo usercommon.UserRepo,
userRoleRelService *role.UserRoleRelService,
revisionService *revision_common.RevisionService,
metaService *meta.MetaService,
collectionCommon *collectioncommon.CollectionCommon,
@@ -100,6 +103,7 @@ func NewQuestionService(
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
userRoleRelService: userRoleRelService,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
@@ -314,6 +318,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
if err != nil {
return
}
_ = qs.questionRepo.UpdateSearch(ctx, question.ID)
revisionDTO := &schema.AddRevisionDTO{
UserID: question.UserID,
@@ -1237,15 +1242,31 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin
func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (
questions []*schema.QuestionPageResp, total int64, err error) {
questions = make([]*schema.QuestionPageResp, 0)
// query by user role
showHidden := false
if req.LoginUserID != "" && req.UserIDBeSearched != "" {
showHidden = req.LoginUserID == req.UserIDBeSearched
if !showHidden {
userRole, err := qs.userRoleRelService.GetUserRole(ctx, req.LoginUserID)
if err != nil {
return nil, 0, err
}
showHidden = userRole == role.RoleAdminID || userRole == role.RoleModeratorID
}
}
// query by tag condition
var tagIDs = make([]string, 0)
if len(req.Tag) > 0 {
tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
if err != nil {
return nil, 0, err
}
if exist {
req.TagID = tagInfo.ID
synTagIds, err := qs.tagCommon.GetTagIDsByMainTagID(ctx, tagInfo.ID)
if err != nil {
return nil, 0, err
}
tagIDs = append(synTagIds, tagInfo.ID)
}
}
@@ -1262,7 +1283,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
}
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
req.UserIDBeSearched, req.TagID, req.OrderCond, req.InDays)
tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden)
if err != nil {
return nil, 0, err
}
+3 -3
View File
@@ -26,8 +26,8 @@ import (
)
type SearchRepo interface {
SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)
ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []*schema.SearchResult, err error)
}
@@ -105,7 +105,7 @@ func (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDT
}
// parseTags parse search tags, return tag ids array
func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags []string) {
func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][]string) {
var (
// expire tag pattern
exprTag = `\[(.*?)\]`
@@ -119,17 +119,25 @@ func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags []st
return
}
tags = []string{}
tags = make([][]string, 0)
for _, item := range res {
tagGroup := make([]string, 0)
tag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1])
if err != nil || !exists {
continue
}
tagGroup = append(tagGroup, tag.ID)
if tag.MainTagID > 0 {
tags = append(tags, fmt.Sprintf("%d", tag.MainTagID))
} else {
tags = append(tags, tag.ID)
tagGroup = append(tagGroup, fmt.Sprintf("%d", tag.MainTagID))
}
synIDs, err := sp.tagCommonService.GetTagIDsByMainTagID(ctx, tag.ID)
if err != nil || !exists {
continue
}
tagGroup = append(tagGroup, tag.ID)
tagGroup = append(tagGroup, synIDs...)
tagGroup = converter.UniqueArray(tagGroup)
tags = append(tags, tagGroup)
}
// limit maximum 5 tags
@@ -60,6 +60,7 @@ type TagRepo interface {
MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (tag *entity.Tag, exist bool, err error)
UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)
GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error)
GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error)
GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)
}
@@ -364,6 +365,12 @@ func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *
return
}
// GetTagIDsByMainTagID get object tag
func (ts *TagCommonService) GetTagIDsByMainTagID(ctx context.Context, tagID string) (tagIDs []string, err error) {
tagIDs, err = ts.tagRepo.GetIDsByMainTagId(ctx, tagID)
return
}
// GetTagBySlugName get object tag
func (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) {
tag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName)
+1
View File
@@ -153,6 +153,7 @@ func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.
userBasicInfo.DisplayName = userInfo.DisplayName
userBasicInfo.Website = userInfo.Website
userBasicInfo.Location = userInfo.Location
userBasicInfo.Language = userInfo.Language
userBasicInfo.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)
if userBasicInfo.Status == constant.UserDeleted {
userBasicInfo.Avatar = ""
@@ -49,6 +49,7 @@ type UserExternalLoginRepo interface {
AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error)
UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)
GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
GetByUserID(ctx context.Context, provider, userID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)
DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error)
SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error)
@@ -96,7 +96,9 @@ func (us *UserNotificationConfigService) SetDefaultUserNotificationConfig(ctx co
}
func (us *UserNotificationConfigService) convertToEntity(ctx context.Context, userID string,
source constant.NotificationSource, channels schema.NotificationChannels) (c *entity.UserNotificationConfig) {
source constant.NotificationSource, channel schema.NotificationChannelConfig) (c *entity.UserNotificationConfig) {
var channels schema.NotificationChannels
channels = append(channels, &channel)
c = &entity.UserNotificationConfig{
UserID: userID,
Source: string(source),
@@ -110,17 +112,3 @@ func (us *UserNotificationConfigService) convertToEntity(ctx context.Context, us
}
return c
}
func (us *UserNotificationConfigService) CheckEnable(
ctx context.Context, userID string, source constant.NotificationSource,
channel constant.NotificationChannelKey) (enable bool, err error) {
conf, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource(ctx, userID, source)
if err != nil {
return false, err
}
if !exist {
return false, nil
}
notificationChannels := schema.NewNotificationChannelsFormJson(conf.Channels)
return notificationChannels.CheckEnable(channel), nil
}
+12
View File
@@ -32,3 +32,15 @@ func ArrayNotInArray(original []string, search []string) []string {
}
return result
}
func UniqueArray[T comparable](input []T) []T {
result := make([]T, 0, len(input))
seen := make(map[T]bool, len(input))
for _, element := range input {
if !seen[element] {
result = append(result, element)
seen[element] = true
}
}
return result
}
+5
View File
@@ -58,3 +58,8 @@ func CommentURL(permalink int, siteUrl, questionID, title, answerID, commentID s
}
return QuestionURL(permalink, siteUrl, questionID, title) + "?commentId=" + commentID
}
// UserURL get user url
func UserURL(siteUrl, username string) string {
return siteUrl + "/users/" + username
}
+89
View File
@@ -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 plugin
// NotificationType is the type of the notification
type NotificationType string
const (
NotificationUpdateQuestion NotificationType = "notification.action.update_question"
NotificationAnswerTheQuestion NotificationType = "notification.action.answer_the_question"
NotificationUpVotedTheQuestion NotificationType = "notification.action.up_voted_question"
NotificationDownVotedTheQuestion NotificationType = "notification.action.down_voted_question"
NotificationUpdateAnswer NotificationType = "notification.action.update_answer"
NotificationAcceptAnswer NotificationType = "notification.action.accept_answer"
NotificationUpVotedTheAnswer NotificationType = "notification.action.up_voted_answer"
NotificationDownVotedTheAnswer NotificationType = "notification.action.down_voted_answer"
NotificationCommentQuestion NotificationType = "notification.action.comment_question"
NotificationCommentAnswer NotificationType = "notification.action.comment_answer"
NotificationUpVotedTheComment NotificationType = "notification.action.up_voted_comment"
NotificationReplyToYou NotificationType = "notification.action.reply_to_you"
NotificationMentionYou NotificationType = "notification.action.mention_you"
NotificationYourQuestionIsClosed NotificationType = "notification.action.your_question_is_closed"
NotificationYourQuestionWasDeleted NotificationType = "notification.action.your_question_was_deleted"
NotificationYourAnswerWasDeleted NotificationType = "notification.action.your_answer_was_deleted"
NotificationYourCommentWasDeleted NotificationType = "notification.action.your_comment_was_deleted"
NotificationInvitedYouToAnswer NotificationType = "notification.action.invited_you_to_answer"
NotificationNewQuestion NotificationType = "notification.action.new_question"
NotificationNewQuestionFollowedTag NotificationType = "notification.action.new_question_followed_tag"
)
type Notification interface {
Base
// Notify sends a notification to the user
Notify(msg *NotificationMessage)
}
type NotificationMessage struct {
// the type of the notification
Type NotificationType `json:"notification_type"`
// the receiver user id
ReceiverUserID string `json:"receiver_user_id"`
// the receiver user using language
ReceiverLang string `json:"receiver_lang"`
// the receiver user external id (optional)
ReceiverExternalID string `json:"receiver_external_id"`
// Who triggered the notification (optional, admin or system operation will not have this field)
TriggerUserID string `json:"trigger_user_id"`
// The trigger user's display name (optional, admin or system operation will not have this field)
TriggerUserDisplayName string `json:"trigger_user_display_name"`
// The trigger user's url (optional, admin or system operation will not have this field)
TriggerUserUrl string `json:"trigger_user_url"`
// the question title
QuestionTitle string `json:"question_title"`
// the question url
QuestionUrl string `json:"question_url"`
// the question tags (comma separated, optional, only for new question notification)
QuestionTags string `json:"tags"`
// the answer url (optional, only for new answer notification)
AnswerUrl string `json:"answer_url"`
// the comment url (optional, only for new comment notification)
CommentUrl string `json:"comment_url"`
}
var (
// CallNotification is a function that calls all registered notification plugins
CallNotification,
registerNotification = MakePlugin[Notification](false)
)
+14
View File
@@ -21,6 +21,7 @@ package plugin
import (
"encoding/json"
"github.com/segmentfault/pacman/i18n"
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/base/translator"
@@ -48,6 +49,10 @@ func Register(p Base) {
registerConfig(p.(Config))
}
if _, ok := p.(UserConfig); ok {
registerUserConfig(p.(UserConfig))
}
if _, ok := p.(Connector); ok {
registerConnector(p.(Connector))
}
@@ -79,6 +84,10 @@ func Register(p Base) {
if _, ok := p.(Search); ok {
registerSearch(p.(Search))
}
if _, ok := p.(Notification); ok {
registerNotification(p.(Notification))
}
}
type Stack[T Base] struct {
@@ -153,6 +162,11 @@ func Translate(ctx *GinContext, key string) string {
return translator.Tr(handler.GetLang(ctx), key)
}
// TranslateWithData translates the key to the language with data
func TranslateWithData(lang i18n.Language, key string, data any) string {
return translator.TrWithData(lang, key, data)
}
// TranslateFn presents a generator of translated string.
// We use it to delegate the translation work outside the plugin.
type TranslateFn func(ctx *GinContext) string
+1 -1
View File
@@ -56,7 +56,7 @@ type SearchBasicCond struct {
// The keywords for search.
Words []string
// TagIDs is a list of tag IDs.
TagIDs []string
TagIDs [][]string
// The object's owner user ID.
UserID string
// The order of the search result.
+52
View File
@@ -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 plugin
type UserConfig interface {
Base
// UserConfigFields returns the list of config fields
UserConfigFields() []ConfigField
// UserConfigReceiver receives the config data, it calls when the config is saved or initialized.
// We recommend to unmarshal the data to a struct, and then use the struct to do something.
// The config is encoded in JSON format.
// It depends on the definition of ConfigFields.
UserConfigReceiver(userID string, config []byte) error
}
var (
// CallUserConfig is a function that calls all registered config plugins
CallUserConfig,
registerUserConfig = MakePlugin[UserConfig](false)
getPluginUserConfigFn func(userID, pluginSlugName string) []byte
)
// GetPluginUserConfig returns the user config of the given user id
func GetPluginUserConfig(userID, pluginSlugName string) []byte {
if getPluginUserConfigFn != nil {
return getPluginUserConfigFn(userID, pluginSlugName)
}
return nil
}
// RegisterGetPluginUserConfigFunc registers a function to get the user config of the given user id
func RegisterGetPluginUserConfigFunc(fn func(userID, pluginSlugName string) []byte) {
getPluginUserConfigFn = fn
}