From f6677e140637cdf0da4a96ea0dde46dfeaecb28e Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 20 Sep 2024 11:11:48 +0800 Subject: [PATCH 01/18] refactor(highlight): Improve useHighlightCode function --- render-markdown-codehighlight/hooks.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/render-markdown-codehighlight/hooks.ts b/render-markdown-codehighlight/hooks.ts index 99bf52af..1cec6808 100644 --- a/render-markdown-codehighlight/hooks.ts +++ b/render-markdown-codehighlight/hooks.ts @@ -19,10 +19,12 @@ import { useEffect, useState } from 'react'; import hljs from 'highlight.js'; -import { themeStyles } from './themeStyles' +import { themeStyles } from './themeStyles'; -const useHighlightCode = (element: HTMLElement | null) => { +const useHighlightCode = (props: HTMLElement | null | { + current: HTMLElement | null; +}) => { const [selectTheme, setSelectTheme] = useState('default'); // Fetch theme from API @@ -39,7 +41,14 @@ const useHighlightCode = (element: HTMLElement | null) => { }, []); useEffect(() => { - if (!element) return; + let element; + if (props instanceof HTMLElement) { + element = props; + } else if (props && props.current instanceof HTMLElement) { + element = props.current; + } else { + return; + } const applyThemeCSS = async (theme: string) => { const existingStyleElement = document.querySelector('style[data-theme-style="highlight"]'); @@ -96,10 +105,9 @@ const useHighlightCode = (element: HTMLElement | null) => { contentObserver.disconnect(); themeObserver.disconnect(); }; - }, [element, selectTheme]); + }, [props, selectTheme]); return null; }; export { useHighlightCode }; - From 581938b9e97df3d232f5885c8bf4b6483ee41aa5 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 20 Sep 2024 11:12:10 +0800 Subject: [PATCH 02/18] refactor(render-markdown-codehighlight): Update version to 0.0.4 --- render-markdown-codehighlight/info.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render-markdown-codehighlight/info.yaml b/render-markdown-codehighlight/info.yaml index 10cdb8f1..b6fa9a96 100644 --- a/render-markdown-codehighlight/info.yaml +++ b/render-markdown-codehighlight/info.yaml @@ -17,6 +17,6 @@ slug_name: render_markdown_codehighlight type: render -version: 0.0.3 +version: 0.0.4 author: Chen Jiaji, Zhu Xuanlyu link: https://github.com/apache/incubator-answer-plugins/tree/main/render-markdown-codehighlight From aeec5165a11fbded7f48bc976cc8f14ca5e5b058 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:12:57 +0800 Subject: [PATCH 03/18] chore: Sync Plugin Info (#226) Sync Plugin Info Co-authored-by: robinv8 --- render-markdown-codehighlight/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render-markdown-codehighlight/package.json b/render-markdown-codehighlight/package.json index 147088ad..1f5d2281 100644 --- a/render-markdown-codehighlight/package.json +++ b/render-markdown-codehighlight/package.json @@ -1,6 +1,6 @@ { "name": "render-markdown-codehighlight", - "version": "0.0.3", + "version": "0.0.4", "description": "", "type": "module", "files": [ From 52b65495085d184fa5bf8adedde3dc7d5d41843d Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 23 Sep 2024 11:24:42 +0800 Subject: [PATCH 04/18] feat(embed): update version --- embed-basic/go.mod | 4 ++-- embed-basic/go.sum | 4 ++-- embed-basic/info.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/embed-basic/go.mod b/embed-basic/go.mod index 6d0bf243..c20f43b1 100644 --- a/embed-basic/go.mod +++ b/embed-basic/go.mod @@ -3,8 +3,9 @@ module github.com/apache/incubator-answer-plugins/embed-basic go 1.19 require ( - github.com/apache/incubator-answer v1.3.6 + github.com/apache/incubator-answer v1.4.0 github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/gin-gonic/gin v1.9.1 ) require ( @@ -14,7 +15,6 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect diff --git a/embed-basic/go.sum b/embed-basic/go.sum index 02955dbb..309afe1a 100644 --- a/embed-basic/go.sum +++ b/embed-basic/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= -github.com/apache/incubator-answer v1.3.6 h1:OddJdWqDrgIKY2wnLOipT3mjNI9h7fLNc4eEyyUp+hs= -github.com/apache/incubator-answer v1.3.6/go.mod h1:YKwpG0rwRC0kHcbILcIyIbPMwsWaZ8j5lHJ34DPIdMI= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= diff --git a/embed-basic/info.yaml b/embed-basic/info.yaml index 94d433e2..054f43ac 100644 --- a/embed-basic/info.yaml +++ b/embed-basic/info.yaml @@ -17,6 +17,6 @@ slug_name: basic_embed type: editor -version: 1.0.4 +version: 1.0.5 author: answerdev link: https://github.com/apache/incubator-answer-plugins/tree/main/embed-basic From 471173e6ddd1689085b15ec566a59a2998dcc033 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Wed, 25 Sep 2024 11:17:45 +0800 Subject: [PATCH 05/18] chore: Optimize error log content --- storage-aliyunoss/README.md | 2 +- storage-aliyunoss/aliyunoss.go | 6 +++--- storage-s3/s3.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/storage-aliyunoss/README.md b/storage-aliyunoss/README.md index f3cde2d1..e98a7d59 100644 --- a/storage-aliyunoss/README.md +++ b/storage-aliyunoss/README.md @@ -9,7 +9,7 @@ ``` ### Configuration -- `Endpoint` - Endpoint of AliCloud OSS storage, such as oss-cn-hangzhou.aliyuncs.com +- `Endpoint` - Endpoint of AliCloud OSS storage, such as oss-cn-hangzhou.aliyuncs.com - `Bucket Name` - Your bucket name - `Object Key Prefix` - Prefix of the object key like 'answer/data/' that ending with '/' - `Access Key Id` - AccessKeyID of the AliCloud OSS storage diff --git a/storage-aliyunoss/aliyunoss.go b/storage-aliyunoss/aliyunoss.go index 66a065ef..af132ca3 100644 --- a/storage-aliyunoss/aliyunoss.go +++ b/storage-aliyunoss/aliyunoss.go @@ -89,14 +89,14 @@ func (s *Storage) UploadFile(ctx *plugin.GinContext, source plugin.UploadSource) bucket, err := client.Bucket(s.Config.BucketName) if err != nil { - resp.OriginalError = fmt.Errorf("create oss client failed: %v", err) + resp.OriginalError = fmt.Errorf("get bucket failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrMisStorageConfig) return resp } file, err := ctx.FormFile("file") if err != nil { - resp.OriginalError = fmt.Errorf("get bucket failed: %v", err) + resp.OriginalError = fmt.Errorf("get upload file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound) return resp } @@ -128,7 +128,7 @@ func (s *Storage) UploadFile(ctx *plugin.GinContext, source plugin.UploadSource) } respBody, err := bucket.DoPutObject(request, nil) if err != nil { - resp.OriginalError = fmt.Errorf("get file failed: %v", err) + resp.OriginalError = fmt.Errorf("upload file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUploadFileFailed) return resp } diff --git a/storage-s3/s3.go b/storage-s3/s3.go index 83b55456..93b0e428 100644 --- a/storage-s3/s3.go +++ b/storage-s3/s3.go @@ -86,7 +86,7 @@ func (s *Storage) UploadFile(ctx *plugin.GinContext, source plugin.UploadSource) file, err := ctx.FormFile("file") if err != nil { - resp.OriginalError = fmt.Errorf("get bucket failed: %v", err) + resp.OriginalError = fmt.Errorf("get upload file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound) return resp } @@ -114,7 +114,7 @@ func (s *Storage) UploadFile(ctx *plugin.GinContext, source plugin.UploadSource) objectKey := s.createObjectKey(file.Filename, source) err = s.Client.PutObject(objectKey, strings.ToLower(filepath.Ext(file.Filename)), openFile) if err != nil { - resp.OriginalError = fmt.Errorf("get file failed: %v", err) + resp.OriginalError = fmt.Errorf("upload file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUploadFileFailed) return resp } From f10caf7ce5b6c60b02e01bf47c2454d09395e992 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Wed, 25 Sep 2024 11:09:53 +0800 Subject: [PATCH 06/18] feat: Support Tencent COS Storage --- storage-tencentyuncos/README.md | 21 ++ storage-tencentyuncos/go.mod | 51 +++++ storage-tencentyuncos/go.sum | 166 ++++++++++++++ storage-tencentyuncos/i18n/en_US.yaml | 72 ++++++ storage-tencentyuncos/i18n/translation.go | 46 ++++ storage-tencentyuncos/i18n/zh_CN.yaml | 72 ++++++ storage-tencentyuncos/info.yaml | 22 ++ storage-tencentyuncos/tencentyuncos.go | 267 ++++++++++++++++++++++ 8 files changed, 717 insertions(+) create mode 100644 storage-tencentyuncos/README.md create mode 100644 storage-tencentyuncos/go.mod create mode 100644 storage-tencentyuncos/go.sum create mode 100644 storage-tencentyuncos/i18n/en_US.yaml create mode 100644 storage-tencentyuncos/i18n/translation.go create mode 100644 storage-tencentyuncos/i18n/zh_CN.yaml create mode 100644 storage-tencentyuncos/info.yaml create mode 100644 storage-tencentyuncos/tencentyuncos.go diff --git a/storage-tencentyuncos/README.md b/storage-tencentyuncos/README.md new file mode 100644 index 00000000..013fc10e --- /dev/null +++ b/storage-tencentyuncos/README.md @@ -0,0 +1,21 @@ +# Tencent COS Storage (preview) + +> This plugin can be used to store attachments and avatars to Tencent COS. + +## How to use + +### Build + +```bash +./answer build --with github.com/apache/incubator-answer-plugins/storage-tencentyuncos +``` + +### Configuration + +- `Region` - Region of Tencent COS storage, like 'ap-beijing' +- `Bucket Name` - Your bucket name +- `Object Key Prefix` - Prefix of the object key like 'answer/data/' that ending with '/' +- `Secret ID` - Secret ID of the Tencent COS storage +- `Secret Key` - Secret Key of the Tencent COS storage +- `Visit Url Prefix` - Prefix of access address for the uploaded file, ending with '/' such as https://example.com/xxx/ +- `Max File Size` - Max file size in MB, default is 10MB diff --git a/storage-tencentyuncos/go.mod b/storage-tencentyuncos/go.mod new file mode 100644 index 00000000..bebd74e1 --- /dev/null +++ b/storage-tencentyuncos/go.mod @@ -0,0 +1,51 @@ +module tencentyuncos + +go 1.21.3 + +require ( + github.com/apache/incubator-answer v1.4.0 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/tencentyun/cos-go-sdk-v5 v0.7.55 +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/go-httpheader v0.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/storage-tencentyuncos/go.sum b/storage-tencentyuncos/go.sum new file mode 100644 index 00000000..2fa1db75 --- /dev/null +++ b/storage-tencentyuncos/go.sum @@ -0,0 +1,166 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.55 h1:9DfH3umWUd0I2jdqcUxrU1kLfUPOydULNy4T9qN5PF8= +github.com/tencentyun/cos-go-sdk-v5 v0.7.55/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/storage-tencentyuncos/i18n/en_US.yaml b/storage-tencentyuncos/i18n/en_US.yaml new file mode 100644 index 00000000..e3ae90dc --- /dev/null +++ b/storage-tencentyuncos/i18n/en_US.yaml @@ -0,0 +1,72 @@ +# 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. + +plugin: + tencentyuncos_storage: + backend: + info: + name: + other: Tencent COS storage + description: + other: Upload files to Tencent COS storage + config: + region: + title: + other: Region + description: + other: Region of Tencent COS storage, like 'ap-beijing' + bucket_name: + title: + other: Bucket name + description: + other: Bucket name of Tencent COS storage + object_key_prefix: + title: + other: Object Key prefix + description: + other: prefix of the object key like 'answer/data/' that ending with '/' + secret_id: + title: + other: SecretID + description: + other: SecretID of the Tencent COS storage + secret_key: + title: + other: SecretKey + description: + other: SecretKey of the Tencent COS storage + visit_url_prefix: + title: + other: Access URL prefix + description: + other: prefix of the final access address of the uploaded file, ending with '/' https://example.com/xxx/ + max_file_size: + title: + other: Maximum file size(MB) + description: + other: Limit the maximum size of uploaded files, in MB, default is 10MB + err: + mis_storage_config: + other: Wrong storage configuration causes upload failure. + file_not_found: + other: File not found. + unsupported_file_type: + other: Unsupported file type. + over_file_size_limit: + other: File size limit exceeded. + upload_file_failed: + other: Failed to upload a file. diff --git a/storage-tencentyuncos/i18n/translation.go b/storage-tencentyuncos/i18n/translation.go new file mode 100644 index 00000000..2664a8c4 --- /dev/null +++ b/storage-tencentyuncos/i18n/translation.go @@ -0,0 +1,46 @@ +/* + * 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 i18n + +const ( + InfoName = "plugin.tencentyuncos_storage.backend.info.name" + InfoDescription = "plugin.tencentyuncos_storage.backend.info.description" + + ConfigRegionTitle = "plugin.tencentyuncos_storage.backend.config.region.title" + ConfigRegionDescription = "plugin.tencentyuncos_storage.backend.config.region.description" + ConfigBucketNameTitle = "plugin.tencentyuncos_storage.backend.config.bucket_name.title" + ConfigBucketNameDescription = "plugin.tencentyuncos_storage.backend.config.bucket_name.description" + ConfigObjectKeyPrefixTitle = "plugin.tencentyuncos_storage.backend.config.object_key_prefix.title" + ConfigObjectKeyPrefixDescription = "plugin.tencentyuncos_storage.backend.config.object_key_prefix.description" + ConfigSecretIdTitle = "plugin.tencentyuncos_storage.backend.config.secret_id.title" + ConfigSecretIdDescription = "plugin.tencentyuncos_storage.backend.config.secret_id.description" + ConfigSecretKeyTitle = "plugin.tencentyuncos_storage.backend.config.secret_key.title" + ConfigSecretKeyDescription = "plugin.tencentyuncos_storage.backend.config.secret_key.description" + ConfigVisitUrlPrefixTitle = "plugin.tencentyuncos_storage.backend.config.visit_url_prefix.title" + ConfigVisitUrlPrefixDescription = "plugin.tencentyuncos_storage.backend.config.visit_url_prefix.description" + ConfigMaxFileSizeTitle = "plugin.tencentyuncos_storage.backend.config.max_file_size.title" + ConfigMaxFileSizeDescription = "plugin.tencentyuncos_storage.backend.config.max_file_size.description" + + ErrMisStorageConfig = "plugin.tencentyuncos_storage.backend.err.mis_storage_config" + ErrFileNotFound = "plugin.tencentyuncos_storage.backend.err.file_not_found" + ErrUnsupportedFileType = "plugin.tencentyuncos_storage.backend.err.unsupported_file_type" + ErrOverFileSizeLimit = "plugin.tencentyuncos_storage.backend.err.over_file_size_limit" + ErrUploadFileFailed = "plugin.tencentyuncos_storage.backend.err.upload_file_failed" +) diff --git a/storage-tencentyuncos/i18n/zh_CN.yaml b/storage-tencentyuncos/i18n/zh_CN.yaml new file mode 100644 index 00000000..280aa19d --- /dev/null +++ b/storage-tencentyuncos/i18n/zh_CN.yaml @@ -0,0 +1,72 @@ +# 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. + +plugin: + tencentyuncos_storage: + backend: + info: + name: + other: 腾讯云COS对象存储 + description: + other: 上传文件到腾讯云COS对象存储 + config: + region: + title: + other: 所属地域 + description: + other: 存储桶的所属地域,如'ap-beijing' + bucket_name: + title: + other: 存储桶名称 + description: + other: 存储桶的名称,如'example-1234567890' + object_key_prefix: + title: + other: 对象Key前缀 + description: + other: 对象键的前缀,如'answer/data/',以'/'结尾 + secret_id: + title: + other: SecretID + description: + other: 腾讯云COS对象存储的SecretID,在 https://console.cloud.tencent.com/cam/capi 获取 + secret_key: + title: + other: SecretKey + description: + other: 腾讯云COS对象存储的SecretKey + visit_url_prefix: + title: + other: 访问URL前缀 + description: + other: 上传文件最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ + max_file_size: + title: + other: 最大文件大小(MB) + description: + other: 限制上传文件的最大大小,单位为MB,默认为 10MB + err: + mis_storage_config: + other: 错误的存储配置导致上传失败 + file_not_found: + other: 文件未找到 + unsupported_file_type: + other: 不支持的文件类型 + over_file_size_limit: + other: 超过文件大小限制 + upload_file_failed: + other: 上传文件失败 diff --git a/storage-tencentyuncos/info.yaml b/storage-tencentyuncos/info.yaml new file mode 100644 index 00000000..5ee9905f --- /dev/null +++ b/storage-tencentyuncos/info.yaml @@ -0,0 +1,22 @@ +# 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. + +slug_name: tencentyuncos_storage +type: storage +version: 1.0.0 +author: Luffy +link: https://github.com/apache/incubator-answer-plugins/tree/main/storage-tencentyuncos diff --git a/storage-tencentyuncos/tencentyuncos.go b/storage-tencentyuncos/tencentyuncos.go new file mode 100644 index 00000000..f2273eef --- /dev/null +++ b/storage-tencentyuncos/tencentyuncos.go @@ -0,0 +1,267 @@ +/* + * 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 tencentyuncos + +import ( + "crypto/rand" + "embed" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/apache/incubator-answer-plugins/util" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/apache/incubator-answer-plugins/storage-tencentyuncos/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/tencentyun/cos-go-sdk-v5" +) + +//go:embed info.yaml +var Info embed.FS + +const ( + // 10MB + defaultMaxFileSize int64 = 10 * 1024 * 1024 +) + +type Storage struct { + Config *StorageConfig +} + +type StorageConfig struct { + Region string `json:"region"` + BucketName string `json:"bucket_name"` + ObjectKeyPrefix string `json:"object_key_prefix"` + SecretID string `json:"secret_id"` + SecretKey string `json:"secret_key"` + VisitUrlPrefix string `json:"visit_url_prefix"` + MaxFileSize string `json:"max_file_size"` +} + +func init() { + plugin.Register(&Storage{ + Config: &StorageConfig{}, + }) +} + +func (s *Storage) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (s *Storage) UploadFile(ctx *plugin.GinContext, source plugin.UploadSource) (resp plugin.UploadFileResponse) { + resp = plugin.UploadFileResponse{} + + BucketURL, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", s.Config.BucketName, s.Config.Region)) + BaseURL := &cos.BaseURL{BucketURL: BucketURL} + client := cos.NewClient(BaseURL, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: s.Config.SecretID, + SecretKey: s.Config.SecretKey, + }, + }) + + _, err := client.Bucket.IsExist(ctx) + if err != nil { + resp.OriginalError = fmt.Errorf("head bucket failed: %v", err) + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrMisStorageConfig) + return resp + } + + file, err := ctx.FormFile("file") + if err != nil { + resp.OriginalError = fmt.Errorf("get upload file failed: %v", err) + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound) + return resp + } + + if !s.CheckFileType(file.Filename, source) { + resp.OriginalError = fmt.Errorf("file type not allowed") + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUnsupportedFileType) + return resp + } + + if file.Size > s.maxFileSizeLimit() { + resp.OriginalError = fmt.Errorf("file size too large") + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrOverFileSizeLimit) + return resp + } + + openFile, err := file.Open() + if err != nil { + resp.OriginalError = fmt.Errorf("get file failed: %v", err) + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound) + return resp + } + defer openFile.Close() + + objectKey := s.createObjectKey(file.Filename, source) + _, err = client.Object.Put(ctx, objectKey, openFile, nil) + if err != nil { + resp.OriginalError = fmt.Errorf("upload file failed: %v", err) + resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUploadFileFailed) + return resp + } + resp.FullURL = s.Config.VisitUrlPrefix + objectKey + return resp +} + +func (s *Storage) createObjectKey(originalFilename string, source plugin.UploadSource) string { + ext := strings.ToLower(filepath.Ext(originalFilename)) + randomString := s.randomObjectKey() + switch source { + case plugin.UserAvatar: + return s.Config.ObjectKeyPrefix + "avatar/" + randomString + ext + case plugin.UserPost: + return s.Config.ObjectKeyPrefix + "post/" + randomString + ext + case plugin.AdminBranding: + return s.Config.ObjectKeyPrefix + "branding/" + randomString + ext + default: + return s.Config.ObjectKeyPrefix + "other/" + randomString + ext + } +} + +func (s *Storage) randomObjectKey() string { + bytes := make([]byte, 4) + _, _ = rand.Read(bytes) + return fmt.Sprintf("%d", time.Now().UnixNano()) + hex.EncodeToString(bytes) +} + +func (s *Storage) CheckFileType(originalFilename string, source plugin.UploadSource) bool { + ext := strings.ToLower(filepath.Ext(originalFilename)) + if _, ok := plugin.DefaultFileTypeCheckMapping[source][ext]; ok { + return true + } + return false +} + +func (s *Storage) maxFileSizeLimit() int64 { + if len(s.Config.MaxFileSize) == 0 { + return defaultMaxFileSize + } + limit, _ := strconv.Atoi(s.Config.MaxFileSize) + if limit <= 0 { + return defaultMaxFileSize + } + return int64(limit) * 1024 * 1024 +} + +func (s *Storage) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "region", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigRegionTitle), + Description: plugin.MakeTranslator(i18n.ConfigRegionDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.Region, + }, + { + Name: "bucket_name", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigBucketNameTitle), + Description: plugin.MakeTranslator(i18n.ConfigBucketNameDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.BucketName, + }, + { + Name: "object_key_prefix", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigObjectKeyPrefixTitle), + Description: plugin.MakeTranslator(i18n.ConfigObjectKeyPrefixDescription), + Required: false, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.ObjectKeyPrefix, + }, + { + Name: "secret_id", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigSecretIdTitle), + Description: plugin.MakeTranslator(i18n.ConfigSecretIdDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.SecretID, + }, + { + Name: "secret_key", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigSecretKeyTitle), + Description: plugin.MakeTranslator(i18n.ConfigSecretKeyDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.SecretKey, + }, + { + Name: "visit_url_prefix", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigVisitUrlPrefixTitle), + Description: plugin.MakeTranslator(i18n.ConfigVisitUrlPrefixDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: s.Config.VisitUrlPrefix, + }, + { + Name: "max_file_size", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigMaxFileSizeTitle), + Description: plugin.MakeTranslator(i18n.ConfigMaxFileSizeDescription), + Required: false, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeNumber, + }, + Value: s.Config.MaxFileSize, + }, + } +} + +func (s *Storage) ConfigReceiver(config []byte) error { + c := &StorageConfig{} + _ = json.Unmarshal(config, c) + s.Config = c + return nil +} From 2886ad51a46c8e217596c0b79c9fe1d2f41a5451 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Sat, 5 Oct 2024 10:37:57 +0800 Subject: [PATCH 07/18] feat: Support Ding talk Notification --- README.md | 5 +- notification-dingtalk/README.md | 20 +++ notification-dingtalk/config.go | 53 ++++++ .../dingtalk_notification.go | 166 ++++++++++++++++++ .../docs/dingtalk-config.png | Bin 0 -> 252802 bytes notification-dingtalk/go.mod | 47 +++++ notification-dingtalk/go.sum | 152 ++++++++++++++++ notification-dingtalk/i18n/en_US.yaml | 112 ++++++++++++ notification-dingtalk/i18n/translation.go | 63 +++++++ notification-dingtalk/i18n/zh_CN.yaml | 112 ++++++++++++ notification-dingtalk/info.yaml | 22 +++ notification-dingtalk/schema.go | 41 +++++ notification-dingtalk/user_config.go | 137 +++++++++++++++ 13 files changed, 929 insertions(+), 1 deletion(-) create mode 100644 notification-dingtalk/README.md create mode 100644 notification-dingtalk/config.go create mode 100644 notification-dingtalk/dingtalk_notification.go create mode 100644 notification-dingtalk/docs/dingtalk-config.png create mode 100644 notification-dingtalk/go.mod create mode 100644 notification-dingtalk/go.sum create mode 100644 notification-dingtalk/i18n/en_US.yaml create mode 100644 notification-dingtalk/i18n/translation.go create mode 100644 notification-dingtalk/i18n/zh_CN.yaml create mode 100644 notification-dingtalk/info.yaml create mode 100644 notification-dingtalk/schema.go create mode 100644 notification-dingtalk/user_config.go diff --git a/README.md b/README.md index e128fe0d..0b8d4c41 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ The Connector plugin helps us to implement third-party login functionality. For The Storage plugin helps us to upload files to third-party storage. For example: Aliyun OSS or AWS S3. -- [x] [Aliyun](https://github.com/apache/incubator-answer-plugins/tree/main/storage-aliyunoss) +- [x] [Aliyun OSS](https://github.com/apache/incubator-answer-plugins/tree/main/storage-aliyunoss) +- [x] [Tencentyun COS](https://github.com/apache/incubator-answer-plugins/tree/main/storage-tencentyuncos) - [x] [S3](https://github.com/apache/incubator-answer-plugins/tree/main/storage-s3) ### Cache @@ -53,6 +54,8 @@ Using the third-party user system to manage users. For example: WeCom The Notification plugin helps us to send messages to third-party notification systems. For example: Slack. - [x] [Slack](https://github.com/apache/incubator-answer-plugins/tree/main/notification-slack) +- [x] [Lark](https://github.com/apache/incubator-answer-plugins/tree/main/notification-lark) +- [x] [Ding talk](https://github.com/apache/incubator-answer-plugins/tree/main/notification-dingtalk) ### Route diff --git a/notification-dingtalk/README.md b/notification-dingtalk/README.md new file mode 100644 index 00000000..3b1a7d8b --- /dev/null +++ b/notification-dingtalk/README.md @@ -0,0 +1,20 @@ +# Ding talk Notification + +## Feature + +- Send message to Ding talk + +## Config + +> Config Webhook URL and open the notification + +- Webhook URL: such as `https://oapi.dingtalk.com/robot/send?access_token=xxxxxx` + +## Preview + +![Ding talk Config](./docs/dingtalk-config.png) + +## Document + +- https://open.dingtalk.com/document/robots/custom-robot-access +- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type \ No newline at end of file diff --git a/notification-dingtalk/config.go b/notification-dingtalk/config.go new file mode 100644 index 00000000..0bad4007 --- /dev/null +++ b/notification-dingtalk/config.go @@ -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 dingtalk + +import ( + "encoding/json" + + "github.com/apache/incubator-answer-plugins/notification-dingtalk/i18n" + "github.com/apache/incubator-answer/plugin" +) + +type NotificationConfig struct { + Notification bool `json:"notification"` +} + +func (n *Notification) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "notification", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), + Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), + }, + Value: n.Config.Notification, + }, + } +} + +func (n *Notification) ConfigReceiver(config []byte) error { + c := &NotificationConfig{} + _ = json.Unmarshal(config, c) + n.Config = c + return nil +} diff --git a/notification-dingtalk/dingtalk_notification.go b/notification-dingtalk/dingtalk_notification.go new file mode 100644 index 00000000..360a941d --- /dev/null +++ b/notification-dingtalk/dingtalk_notification.go @@ -0,0 +1,166 @@ +/* + * 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 dingtalk + +import ( + "embed" + "github.com/apache/incubator-answer-plugins/util" + "github.com/go-resty/resty/v2" + "strings" + + dingtalkI18n "github.com/apache/incubator-answer-plugins/notification-dingtalk/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Notification struct { + Config *NotificationConfig + UserConfigCache *UserConfigCache +} + +func init() { + uc := &Notification{ + Config: &NotificationConfig{}, + UserConfigCache: NewUserConfigCache(), + } + plugin.Register(uc) +} + +func (n *Notification) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(dingtalkI18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(dingtalkI18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +// GetNewQuestionSubscribers returns the subscribers of the new question notification +func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) { + for userID, conf := range n.UserConfigCache.userConfigMapping { + if conf.AllNewQuestions { + userIDs = append(userIDs, userID) + } + } + return userIDs +} + +// Notify sends a notification to the user +func (n *Notification) Notify(msg plugin.NotificationMessage) { + log.Debugf("try to send notification %+v", msg) + + if !n.Config.Notification { + return + } + + // get user config + userConfig, err := n.getUserConfig(msg.ReceiverUserID) + if err != nil { + log.Errorf("get user config failed: %v", err) + return + } + if userConfig == nil { + log.Debugf("user %s has no config", msg.ReceiverUserID) + return + } + + // check if the notification is enabled + switch msg.Type { + case plugin.NotificationNewQuestion: + if !userConfig.AllNewQuestions { + log.Debugf("user %s not config the new question", msg.ReceiverUserID) + return + } + case plugin.NotificationNewQuestionFollowedTag: + if !userConfig.NewQuestionsForFollowingTags { + log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) + return + } + default: + if !userConfig.InboxNotifications { + log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) + return + } + } + + log.Debugf("user %s config the notification", msg.ReceiverUserID) + + if len(userConfig.WebhookURL) == 0 { + log.Errorf("user %s has no webhook url", msg.ReceiverUserID) + return + } + + notificationMsg, notificationTitle := renderNotification(msg) + // no need to send empty message + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + + // Create a Resty Client + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(NewWebhookReq(notificationMsg, notificationTitle)). + Post(userConfig.WebhookURL) + + if err != nil { + log.Errorf("send message failed: %v %v", err, resp) + } else { + log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) + } +} + +func renderNotification(msg plugin.NotificationMessage) (string, string) { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, dingtalkI18n.TplUpdateQuestion, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplUpdateQuestionTitle, nil) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, dingtalkI18n.TplAnswerTheQuestion, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplAnswerTheQuestionTitle, nil) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, dingtalkI18n.TplUpdateAnswer, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplUpdateAnswerTitle, nil) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, dingtalkI18n.TplAcceptAnswer, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplAcceptAnswerTitle, nil) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, dingtalkI18n.TplCommentQuestion, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplCommentQuestionTitle, nil) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, dingtalkI18n.TplCommentAnswer, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplCommentAnswerTitle, nil) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, dingtalkI18n.TplReplyToYou, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplReplyToYouTitle, nil) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, dingtalkI18n.TplMentionYou, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplMentionYouTitle, nil) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, dingtalkI18n.TplInvitedYouToAnswer, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplInvitedYouToAnswerTitle, nil) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") + return plugin.TranslateWithData(lang, dingtalkI18n.TplNewQuestion, msg), plugin.TranslateWithData(lang, dingtalkI18n.TplNewQuestionTitle, nil) + } + return "", "" +} diff --git a/notification-dingtalk/docs/dingtalk-config.png b/notification-dingtalk/docs/dingtalk-config.png new file mode 100644 index 0000000000000000000000000000000000000000..22efeb98e33a03918cbf1168ff09bdbbb4e5359f GIT binary patch literal 252802 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

=AJKE*dN^Wq-GwS*Q+TriA$yvTe~Sle_Nu) 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

t4(K#>l^K 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 p7!B3Yv^tutMhh#dm#YW>`8l9d1e literal 0 HcmV?d00001 diff --git a/notification-dingtalk/go.mod b/notification-dingtalk/go.mod new file mode 100644 index 00000000..9b08b275 --- /dev/null +++ b/notification-dingtalk/go.mod @@ -0,0 +1,47 @@ +module dingtalk + +go 1.21.3 + +require ( + github.com/apache/incubator-answer v1.4.0 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/go-resty/resty/v2 v2.15.3 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/notification-dingtalk/go.sum b/notification-dingtalk/go.sum new file mode 100644 index 00000000..5299099b --- /dev/null +++ b/notification-dingtalk/go.sum @@ -0,0 +1,152 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/notification-dingtalk/i18n/en_US.yaml b/notification-dingtalk/i18n/en_US.yaml new file mode 100644 index 00000000..c4d14497 --- /dev/null +++ b/notification-dingtalk/i18n/en_US.yaml @@ -0,0 +1,112 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + dingtalk_notification: + backend: + info: + name: + other: Ding talk Notification + description: + other: Send notifications to Ding Talk + config: + tip: + title: + other: Push notification service has been turned off. + notification: + label: + other: Turn on push notifications + title: + other: Notifications + description: + other: Users will receive notifications on Ding Talk. + user_config: + webhook_url: + title: + other: Webhook URL + inbox_notifications: + title: + other: Inbox Notifications + label: + other: Turn on inbox notifications + description: + other: Answers to your questions, comments, invites, and more. + all_new_questions: + title: + other: All New Questions + label: + other: Turn on all new questions + description: + other: Get notified of all new questions. Up to 50 questions per week. + new_questions_for_following_tags: + title: + other: New Questions for Following Tags + label: + other: Turn on new questions for following tags + description: + other: Get notified of new questions for following tags. + tpl: + update_question: + title: + other: Question Updated + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated question <{{.QuestionUrl}}|{{.QuestionTitle}}>" + answer_the_question: + title: + other: Answer Received + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> answered the question <{{.AnswerUrl}}|{{.QuestionTitle}}>" + update_answer: + title: + other: Answer Updated + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated answer <{{.AnswerUrl}}|{{.QuestionTitle}}>" + accept_answer: + title: + other: Answer Accepted + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> accepted answer <{{.AnswerUrl}}|{{.QuestionTitle}}>" + comment_question: + title: + other: Comment on Question + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented question <{{.CommentUrl}}|{{.QuestionTitle}}>" + comment_answer: + title: + other: Comment on Answer + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented answer <{{.CommentUrl}}|{{.QuestionTitle}}>" + reply_to_you: + title: + other: Reply to You + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> replied you <{{.CommentUrl}}|{{.QuestionTitle}}>" + mention_you: + title: + other: Mentioned You + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> mentioned you <{{.CommentUrl}}|{{.QuestionTitle}}>" + invited_you_to_answer: + title: + other: Invited to Answer + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> invited you to answer <{{.QuestionUrl}}|{{.QuestionTitle}}>" + new_question: + title: + other: New Question Posted + text: + other: "New question:\n<{{.QuestionUrl}}|{{.QuestionTitle}}>\n{{.QuestionTags}}" diff --git a/notification-dingtalk/i18n/translation.go b/notification-dingtalk/i18n/translation.go new file mode 100644 index 00000000..2d9c5a46 --- /dev/null +++ b/notification-dingtalk/i18n/translation.go @@ -0,0 +1,63 @@ +/* + * 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 i18n + +const ( + InfoName = "plugin.dingtalk_notification.backend.info.name" + InfoDescription = "plugin.dingtalk_notification.backend.info.description" + ConfigTipTitle = "plugin.dingtalk_notification.backend.config.tip.title" + ConfigNotificationLabel = "plugin.dingtalk_notification.backend.config.notification.label" + ConfigNotificationTitle = "plugin.dingtalk_notification.backend.config.notification.title" + ConfigNotificationDescription = "plugin.dingtalk_notification.backend.config.notification.description" + + UserConfigWebhookURLTitle = "plugin.dingtalk_notification.backend.user_config.webhook_url.title" + UserConfigInboxNotificationsTitle = "plugin.dingtalk_notification.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.dingtalk_notification.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.dingtalk_notification.backend.user_config.inbox_notifications.description" + + UserConfigAllNewQuestionsNotificationsTitle = "plugin.dingtalk_notification.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsNotificationsLabel = "plugin.dingtalk_notification.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsNotificationsDescription = "plugin.dingtalk_notification.backend.user_config.all_new_questions.description" + + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.dingtalk_notification.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.dingtalk_notification.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.dingtalk_notification.backend.user_config.new_questions_for_following_tags.description" + + TplUpdateQuestionTitle = "plugin.dingtalk_notification.backend.tpl.update_question.title" + TplUpdateQuestion = "plugin.dingtalk_notification.backend.tpl.update_question.text" + TplAnswerTheQuestionTitle = "plugin.dingtalk_notification.backend.tpl.answer_the_question.title" + TplAnswerTheQuestion = "plugin.dingtalk_notification.backend.tpl.answer_the_question.text" + TplUpdateAnswerTitle = "plugin.dingtalk_notification.backend.tpl.update_answer.title" + TplUpdateAnswer = "plugin.dingtalk_notification.backend.tpl.update_answer.text" + TplAcceptAnswerTitle = "plugin.dingtalk_notification.backend.tpl.accept_answer.title" + TplAcceptAnswer = "plugin.dingtalk_notification.backend.tpl.accept_answer.text" + TplCommentQuestionTitle = "plugin.dingtalk_notification.backend.tpl.comment_question.title" + TplCommentQuestion = "plugin.dingtalk_notification.backend.tpl.comment_question.text" + TplCommentAnswerTitle = "plugin.dingtalk_notification.backend.tpl.comment_answer.title" + TplCommentAnswer = "plugin.dingtalk_notification.backend.tpl.comment_answer.text" + TplReplyToYouTitle = "plugin.dingtalk_notification.backend.tpl.reply_to_you.title" + TplReplyToYou = "plugin.dingtalk_notification.backend.tpl.reply_to_you.text" + TplMentionYouTitle = "plugin.dingtalk_notification.backend.tpl.mention_you.title" + TplMentionYou = "plugin.dingtalk_notification.backend.tpl.mention_you.text" + TplInvitedYouToAnswerTitle = "plugin.dingtalk_notification.backend.tpl.invited_you_to_answer.title" + TplInvitedYouToAnswer = "plugin.dingtalk_notification.backend.tpl.invited_you_to_answer.text" + TplNewQuestionTitle = "plugin.dingtalk_notification.backend.tpl.new_question.title" + TplNewQuestion = "plugin.dingtalk_notification.backend.tpl.new_question.text" +) diff --git a/notification-dingtalk/i18n/zh_CN.yaml b/notification-dingtalk/i18n/zh_CN.yaml new file mode 100644 index 00000000..80a170f4 --- /dev/null +++ b/notification-dingtalk/i18n/zh_CN.yaml @@ -0,0 +1,112 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + dingtalk_notification: + backend: + info: + name: + other: 钉钉通知 + description: + other: 发送通知到钉钉 + config: + tip: + title: + other: 推送通知服务已关闭。 + notification: + label: + other: 打开通知 + title: + other: 通知 + description: + other: 用户将在钉钉上收到通知。 + user_config: + webhook_url: + title: + other: Webhook URL + inbox_notifications: + title: + other: 收件箱通知 + label: + other: 打开收件箱通知 + description: + other: 问题的答案、评论、邀请等。 + all_new_questions: + title: + other: 所有新问题通知 + label: + other: 打开所有新问题通知 + description: + other: 收到所有新问题的通知。每周最多 50 个问题。 + new_questions_for_following_tags: + title: + other: 关注标签的新问题通知 + label: + other: 打开关注标签的新问题通知 + description: + other: 收到以下标签的新问题通知。 + tpl: + update_question: + title: + other: 更新问题 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新问题 <{{.QuestionUrl}}|{{.QuestionTitle}}>" + answer_the_question: + title: + other: 回答了问题 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回答了问题 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + update_answer: + title: + other: 更新答案 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + accept_answer: + title: + other: 接受答案 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 接受答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + comment_question: + title: + other: 评论提问 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论提问 <{{.CommentUrl}}|{{.QuestionTitle}}>" + comment_answer: + title: + other: 评论回答 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论回答 <{{.CommentUrl}}|{{.QuestionTitle}}>" + reply_to_you: + title: + other: 回复了问题 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回复了问题 <{{.CommentUrl}}|{{.QuestionTitle}}>" + mention_you: + title: + other: 提到了你 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 提到了你 <{{.CommentUrl}}|{{.QuestionTitle}}>" + invited_you_to_answer: + title: + other: 邀请你回答 + text: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 邀请你回答 <{{.QuestionUrl}}|{{.QuestionTitle}}>" + new_question: + title: + other: 新问题 + text: + other: "新问题:\n<{{.QuestionUrl}}|{{.QuestionTitle}}>\n{{.QuestionTags}}" \ No newline at end of file diff --git a/notification-dingtalk/info.yaml b/notification-dingtalk/info.yaml new file mode 100644 index 00000000..c5481f20 --- /dev/null +++ b/notification-dingtalk/info.yaml @@ -0,0 +1,22 @@ +# 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. + +slug_name: dingtalk_notification +type: notification +version: 1.0.0 +author: Luffy +link: https://github.com/apache/incubator-answer-plugins/tree/main/notification-dingtalk diff --git a/notification-dingtalk/schema.go b/notification-dingtalk/schema.go new file mode 100644 index 00000000..65d5d0e1 --- /dev/null +++ b/notification-dingtalk/schema.go @@ -0,0 +1,41 @@ +/* + * 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 dingtalk + +type WebhookReq struct { + MsgType string `json:"msgtype"` + Markdown struct { + Title string `json:"title"` + Text string `json:"text"` + } `json:"markdown"` +} + +func NewWebhookReq(content string, title string) *WebhookReq { + return &WebhookReq{ + MsgType: "markdown", + Markdown: struct { + Title string `json:"title"` + Text string `json:"text"` + }{ + Title: title, + Text: content, + }, + } +} diff --git a/notification-dingtalk/user_config.go b/notification-dingtalk/user_config.go new file mode 100644 index 00000000..2dbdba6e --- /dev/null +++ b/notification-dingtalk/user_config.go @@ -0,0 +1,137 @@ +/* + * 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 dingtalk + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/apache/incubator-answer-plugins/notification-dingtalk/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + WebhookURL string `json:"webhook_url"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` +} + +type UserConfigCache struct { + // key: userID value: user config + userConfigMapping map[string]*UserConfig + sync.Mutex +} + +func NewUserConfigCache() *UserConfigCache { + ucc := &UserConfigCache{ + userConfigMapping: make(map[string]*UserConfig), + } + return ucc +} + +func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { + ucc.Lock() + defer ucc.Unlock() + ucc.userConfigMapping[userID] = config +} + +func (n *Notification) UserConfigFields() []plugin.ConfigField { + fields := make([]plugin.ConfigField, 0) + // Show tip for user, if the notification service is disabled + if !n.Config.Notification { + fields = append(fields, plugin.ConfigField{ + Name: "tip", + Type: plugin.ConfigTypeLegend, + Title: plugin.MakeTranslator(i18n.ConfigTipTitle), + Description: plugin.Translator{}, + UIOptions: plugin.ConfigFieldUIOptions{ + ClassName: "mb-3", + FieldClassName: "mb-0 text-danger", + }, + }) + } + fields = append(fields, plugin.ConfigField{ + Name: "webhook_url", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }) + fields = append(fields, createSwitchConfig( + "inbox_notifications", + i18n.UserConfigInboxNotificationsTitle, + i18n.UserConfigInboxNotificationsLabel, + i18n.UserConfigInboxNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "all_new_questions", + i18n.UserConfigAllNewQuestionsNotificationsTitle, + i18n.UserConfigAllNewQuestionsNotificationsLabel, + i18n.UserConfigAllNewQuestionsNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "new_questions_for_following_tags", + i18n.UserConfigNewQuestionsForFollowingTagsTitle, + i18n.UserConfigNewQuestionsForFollowingTagsLabel, + i18n.UserConfigNewQuestionsForFollowingTagsDescription, + )) + return fields +} + +func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(label), + }, + } +} + +func (n *Notification) UserConfigReceiver(userID string, config []byte) error { + log.Debugf("receive user config %s %s", userID, string(config)) + var userConfig UserConfig + err := json.Unmarshal(config, &userConfig) + if err != nil { + return fmt.Errorf("unmarshal user config failed: %w", err) + } + n.UserConfigCache.SetUserConfig(userID, &userConfig) + return nil +} + +func (n *Notification) getUserConfig(userID string) (config *UserConfig, err error) { + userConfig := plugin.GetPluginUserConfig(userID, n.Info().SlugName) + if len(userConfig) == 0 { + return nil, nil + } + config = &UserConfig{} + err = json.Unmarshal(userConfig, config) + if err != nil { + return nil, fmt.Errorf("unmarshal user config failed: %w", err) + } + return config, nil +} From ae317933ef0b74fb3217dbaaca8125ba622fd73e Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Mon, 7 Oct 2024 09:53:59 +0800 Subject: [PATCH 08/18] fix: module name missing link --- notification-dingtalk/go.mod | 2 +- storage-tencentyuncos/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/notification-dingtalk/go.mod b/notification-dingtalk/go.mod index 9b08b275..a0e03e07 100644 --- a/notification-dingtalk/go.mod +++ b/notification-dingtalk/go.mod @@ -1,4 +1,4 @@ -module dingtalk +module github.com/apache/incubator-answer-plugins/dingtalk go 1.21.3 diff --git a/storage-tencentyuncos/go.mod b/storage-tencentyuncos/go.mod index bebd74e1..55f64dcf 100644 --- a/storage-tencentyuncos/go.mod +++ b/storage-tencentyuncos/go.mod @@ -1,4 +1,4 @@ -module tencentyuncos +module github.com/apache/incubator-answer-plugins/tencentyuncos go 1.21.3 From 4f6733f54b0d7cf69f9d46c74bbac88be34d8a74 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Mon, 7 Oct 2024 11:59:49 +0800 Subject: [PATCH 09/18] feat: Support Baidu Content Audit Platform --- reviewer-baidu/README.md | 12 +++ reviewer-baidu/basic.go | 163 +++++++++++++++++++++++++++++ reviewer-baidu/go.mod | 48 +++++++++ reviewer-baidu/go.sum | 150 ++++++++++++++++++++++++++ reviewer-baidu/i18n/en_US.yaml | 49 +++++++++ reviewer-baidu/i18n/translation.go | 36 +++++++ reviewer-baidu/i18n/zh_CN.yaml | 49 +++++++++ reviewer-baidu/info.yaml | 22 ++++ 8 files changed, 529 insertions(+) create mode 100644 reviewer-baidu/README.md create mode 100644 reviewer-baidu/basic.go create mode 100644 reviewer-baidu/go.mod create mode 100644 reviewer-baidu/go.sum create mode 100644 reviewer-baidu/i18n/en_US.yaml create mode 100644 reviewer-baidu/i18n/translation.go create mode 100644 reviewer-baidu/i18n/zh_CN.yaml create mode 100644 reviewer-baidu/info.yaml diff --git a/reviewer-baidu/README.md b/reviewer-baidu/README.md new file mode 100644 index 00000000..401c62f7 --- /dev/null +++ b/reviewer-baidu/README.md @@ -0,0 +1,12 @@ +# Baidu Reviewer + +> Baidu Content Audit Platform is a service platform for intelligent auditing of multimedia content. It can be used to filter spam. + +## Config + +- `api_key`: Baidu App's API key +- `secret_key`: Baidu App's Secret Key + +## Document + +- https://cloud.baidu.com/doc/ANTIPORN/s/Vk3h6xaga diff --git a/reviewer-baidu/basic.go b/reviewer-baidu/basic.go new file mode 100644 index 00000000..a5c3b163 --- /dev/null +++ b/reviewer-baidu/basic.go @@ -0,0 +1,163 @@ +/* + * 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 basic + +import ( + "embed" + "encoding/json" + "github.com/apache/incubator-answer-plugins/util" + + "github.com/apache/incubator-answer-plugins/reviewer-baidu/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/lufei/baidu-golang-sdk/aip/censor" + myI18n "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Reviewer struct { + Config *ReviewerConfig +} + +type ReviewerConfig struct { + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + SpamFiltering string `json:"span_filtering"` +} + +func init() { + plugin.Register(&Reviewer{ + Config: &ReviewerConfig{}, + }) +} + +func (r *Reviewer) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (r *Reviewer) Review(content *plugin.ReviewContent) (result *plugin.ReviewResult) { + result = &plugin.ReviewResult{Approved: true} + if len(r.Config.APIKey) == 0 { + return result + } + // If the author is admin, no need to review + if content.Author.Role > 1 { + return result + } + + client := censor.NewClient(r.Config.APIKey, r.Config.SecretKey) + TextCensorResult, err := client.TextCensor(content.Title+"\n"+content.Content, content.IP) + if err != nil { + log.Errorf("Request baidu to check failed: %v", err) + return handleReviewError(content, plugin.ReviewStatusNeedReview) + } + + var jsonMap map[string]interface{} + err = json.Unmarshal([]byte(TextCensorResult), &jsonMap) + if err != nil { + return handleReviewError(content, plugin.ReviewStatusNeedReview) + } + + if conclusionType, ok := jsonMap["conclusionType"].(float64); ok { + if conclusionType == 1.0 { + return result + } + } + + if r.Config.SpamFiltering == "delete" { + return handleReviewError(content, plugin.ReviewStatusDeleteDirectly) + } + + return handleReviewError(content, plugin.ReviewStatusNeedReview) +} + +func (r *Reviewer) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "api_key", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigAPIKeyLabel), + Description: plugin.MakeTranslator(i18n.ConfigAPIKeyDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + Label: plugin.MakeTranslator(i18n.ConfigAPIKeyLabel), + }, + Value: r.Config.APIKey, + }, + { + Name: "secret_key", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigSecretKeyTitle), + Description: plugin.MakeTranslator(i18n.ConfigSecretKeyDescription), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + Label: plugin.MakeTranslator(i18n.ConfigSecretKeyLabel), + }, + Value: r.Config.SecretKey, + }, + { + Name: "span_filtering", + Type: plugin.ConfigTypeSelect, + Title: plugin.MakeTranslator(i18n.ConfigSpanFilteringTitle), + Required: false, + UIOptions: plugin.ConfigFieldUIOptions{}, + Value: r.Config.SpamFiltering, + Options: []plugin.ConfigFieldOption{ + { + Value: "review", + Label: plugin.MakeTranslator(i18n.ConfigSpanFilteringReview), + }, + { + Value: "delete", + Label: plugin.MakeTranslator(i18n.ConfigSpanFilteringDelete), + }, + }, + }, + } +} + +func handleReviewError(content *plugin.ReviewContent, ReviewStatus plugin.ReviewStatus) *plugin.ReviewResult { + return &plugin.ReviewResult{ + Approved: false, + ReviewStatus: ReviewStatus, + Reason: plugin.TranslateWithData(myI18n.Language(content.Language), i18n.CommentNeedReview, nil), + } +} + +func (r *Reviewer) ConfigReceiver(config []byte) error { + c := &ReviewerConfig{} + _ = json.Unmarshal(config, c) + r.Config = c + return nil +} diff --git a/reviewer-baidu/go.mod b/reviewer-baidu/go.mod new file mode 100644 index 00000000..d1fc5500 --- /dev/null +++ b/reviewer-baidu/go.mod @@ -0,0 +1,48 @@ +module github.com/apache/incubator-answer-plugins/reviewer-baidu + +go 1.21.3 + +require ( + github.com/apache/incubator-answer v1.4.0 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/lufei/baidu-golang-sdk v0.0.0-20241007032158-d85deddc0d61 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.1.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/reviewer-baidu/go.sum b/reviewer-baidu/go.sum new file mode 100644 index 00000000..a22f1ed4 --- /dev/null +++ b/reviewer-baidu/go.sum @@ -0,0 +1,150 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lufei/baidu-golang-sdk v0.0.0-20241007032158-d85deddc0d61 h1:kO3vkpA7fDnLsndVhYxZVcCIQRQqmxYuhINy4rtDGus= +github.com/lufei/baidu-golang-sdk v0.0.0-20241007032158-d85deddc0d61/go.mod h1:UBdQbJ7zwTRkSaKOG8QDRrLjztTPGG+w8uIc8FH0J3I= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/reviewer-baidu/i18n/en_US.yaml b/reviewer-baidu/i18n/en_US.yaml new file mode 100644 index 00000000..aca0dbda --- /dev/null +++ b/reviewer-baidu/i18n/en_US.yaml @@ -0,0 +1,49 @@ +# 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. + +plugin: + baidu_reviewer: + backend: + info: + name: + other: Baidu Anti-Spam + description: + other: Baidu Anti-Spam is used to check the content of posts and comments against the Baidu web service to see if they look like spam. + config: + api_key: + title: + other: API Key + label: + other: Enter your API key + description: + other: Your API key. Manage your API keys. + secret_key: + title: + other: Secret Key + label: + other: Enter your Secret key + description: + other: Your Secret key. Manage your API keys. + span_filtering: + title: + other: Span Filtering + review: + other: Put spam in the review queue + delete: + other: Silently delete the spam + comment: + need_review: Needs approval. diff --git a/reviewer-baidu/i18n/translation.go b/reviewer-baidu/i18n/translation.go new file mode 100644 index 00000000..b2cf1ce1 --- /dev/null +++ b/reviewer-baidu/i18n/translation.go @@ -0,0 +1,36 @@ +/* + * 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 i18n + +const ( + InfoName = "plugin.baidu_reviewer.backend.info.name" + InfoDescription = "plugin.baidu_reviewer.backend.info.description" + ConfigAPIKeyLabel = "plugin.baidu_reviewer.backend.config.api_key.label" + ConfigAPIKeyTitle = "plugin.baidu_reviewer.backend.config.api_key.title" + ConfigAPIKeyDescription = "plugin.baidu_reviewer.backend.config.api_key.description" + ConfigSecretKeyLabel = "plugin.baidu_reviewer.backend.config.secret_key.label" + ConfigSecretKeyTitle = "plugin.baidu_reviewer.backend.config.secret_key.title" + ConfigSecretKeyDescription = "plugin.baidu_reviewer.backend.config.secret_key.description" + CommentNeedReview = "plugin.baidu_reviewer.backend.comment.need_review" + ConfigSpanFilteringTitle = "plugin.baidu_reviewer.backend.config.span_filtering.title" + ConfigSpanFilteringLabel = "plugin.baidu_reviewer.backend.config.span_filtering.label" + ConfigSpanFilteringReview = "plugin.baidu_reviewer.backend.config.span_filtering.review" + ConfigSpanFilteringDelete = "plugin.baidu_reviewer.backend.config.span_filtering.delete" +) diff --git a/reviewer-baidu/i18n/zh_CN.yaml b/reviewer-baidu/i18n/zh_CN.yaml new file mode 100644 index 00000000..1d319845 --- /dev/null +++ b/reviewer-baidu/i18n/zh_CN.yaml @@ -0,0 +1,49 @@ +# 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. + +plugin: + baidu_reviewer: + backend: + info: + name: + other: 百度反垃圾 + description: + other: 百度反垃圾用于检查帖子和评论内容,通过百度内容审核平台查看它们是否像垃圾内容。 + config: + api_key: + title: + other: API Key + label: + other: 输入您的 API Key + description: + other: 您的 API Key。创建应用后查看您的 API 密钥信息。 + secret_key: + title: + other: Secret Key + label: + other: 输入您的 Secret key + description: + other: 您的 Secret key。创建应用后查看您的 API 密钥信息。 + span_filtering: + title: + other: 垃圾内容过滤 + review: + other: 将垃圾内容放入审核队列 + delete: + other: 静默删除垃圾内容 + comment: + need_review: 需要批准。 diff --git a/reviewer-baidu/info.yaml b/reviewer-baidu/info.yaml new file mode 100644 index 00000000..fd5cf7ad --- /dev/null +++ b/reviewer-baidu/info.yaml @@ -0,0 +1,22 @@ +# 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. + +slug_name: baidu_reviewer +type: reviewer +version: 1.0.0 +author: Luffy +link: https://github.com/apache/incubator-answer-plugins/tree/main/reviewer-baidu From 8e42d3d32b952d49a5423d491c2231ed295d2f1c Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Tue, 8 Oct 2024 11:02:11 +0800 Subject: [PATCH 10/18] fix: replace TextCensorResult with textCensorResult --- reviewer-baidu/basic.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reviewer-baidu/basic.go b/reviewer-baidu/basic.go index a5c3b163..1baf3935 100644 --- a/reviewer-baidu/basic.go +++ b/reviewer-baidu/basic.go @@ -75,14 +75,14 @@ func (r *Reviewer) Review(content *plugin.ReviewContent) (result *plugin.ReviewR } client := censor.NewClient(r.Config.APIKey, r.Config.SecretKey) - TextCensorResult, err := client.TextCensor(content.Title+"\n"+content.Content, content.IP) + textCensorResult, err := client.TextCensor(content.Title+"\n"+content.Content, content.IP) if err != nil { log.Errorf("Request baidu to check failed: %v", err) return handleReviewError(content, plugin.ReviewStatusNeedReview) } var jsonMap map[string]interface{} - err = json.Unmarshal([]byte(TextCensorResult), &jsonMap) + err = json.Unmarshal([]byte(textCensorResult), &jsonMap) if err != nil { return handleReviewError(content, plugin.ReviewStatusNeedReview) } From 2b50e8e704796aee04676961616e7339b6394f40 Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Sat, 28 Sep 2024 05:09:04 +0000 Subject: [PATCH 11/18] Create user center slack and integrate with importer --- notification-slack/README.md | 26 +- notification-slack/config.go | 106 +++---- notification-slack/go.mod | 98 ++++--- notification-slack/go.sum | 309 ++++++++++---------- notification-slack/i18n/en_US.yaml | 192 ++++++------ notification-slack/i18n/translation.go | 124 ++++---- notification-slack/i18n/zh_CN.yaml | 162 +++++------ notification-slack/info.yaml | 44 +-- notification-slack/schema.go | 106 +++---- notification-slack/slack_notification.go | 356 ++++++++++++----------- notification-slack/user_config.go | 302 ++++++++++--------- user-center-slack/README.md | 20 ++ user-center-slack/client.go | 179 ++++++++++++ user-center-slack/config.go | 158 ++++++++++ user-center-slack/cron.go | 41 +++ user-center-slack/go.mod | 60 ++++ user-center-slack/go.sum | 153 ++++++++++ user-center-slack/handler.go | 149 ++++++++++ user-center-slack/i18n/en_US.yaml | 121 ++++++++ user-center-slack/i18n/translation.go | 67 +++++ user-center-slack/i18n/zh_CN.yaml | 121 ++++++++ user-center-slack/importer.go | 136 +++++++++ user-center-slack/info.yaml | 22 ++ user-center-slack/notification.go | 137 +++++++++ user-center-slack/schema.go | 68 +++++ user-center-slack/slack_user_center.go | 259 +++++++++++++++++ user-center-slack/user_config.go | 127 ++++++++ 27 files changed, 2772 insertions(+), 871 deletions(-) create mode 100644 user-center-slack/README.md create mode 100644 user-center-slack/client.go create mode 100644 user-center-slack/config.go create mode 100644 user-center-slack/cron.go create mode 100644 user-center-slack/go.mod create mode 100644 user-center-slack/go.sum create mode 100644 user-center-slack/handler.go create mode 100644 user-center-slack/i18n/en_US.yaml create mode 100644 user-center-slack/i18n/translation.go create mode 100644 user-center-slack/i18n/zh_CN.yaml create mode 100644 user-center-slack/importer.go create mode 100644 user-center-slack/info.yaml create mode 100644 user-center-slack/notification.go create mode 100644 user-center-slack/schema.go create mode 100644 user-center-slack/slack_user_center.go create mode 100644 user-center-slack/user_config.go diff --git a/notification-slack/README.md b/notification-slack/README.md index d52696f6..907e8936 100644 --- a/notification-slack/README.md +++ b/notification-slack/README.md @@ -1,14 +1,14 @@ -# Slack Notification -## Feature -- Send message to Slack - -## Config -> Config Webhook URL and open the notification - -- Webhook URL: such as `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX` - -## Preview -![Slack Config](./docs/slack-config.png) - -## Document +# Slack Notification +## Feature +- Send message to Slack + +## Config +> Config Webhook URL and open the notification + +- Webhook URL: such as `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX` + +## Preview +![Slack Config](./docs/slack-config.png) + +## Document - https://api.slack.com/messaging/webhooks \ No newline at end of file diff --git a/notification-slack/config.go b/notification-slack/config.go index aafe828f..8afd16e5 100644 --- a/notification-slack/config.go +++ b/notification-slack/config.go @@ -1,53 +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 slack - -import ( - "encoding/json" - - "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" -) - -type NotificationConfig struct { - Notification bool `json:"notification"` -} - -func (n *Notification) ConfigFields() []plugin.ConfigField { - return []plugin.ConfigField{ - { - Name: "notification", - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), - Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), - }, - Value: n.Config.Notification, - }, - } -} - -func (n *Notification) ConfigReceiver(config []byte) error { - c := &NotificationConfig{} - _ = json.Unmarshal(config, c) - n.Config = c - return nil -} +/* + * 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 slack_notification + +import ( + "encoding/json" + + "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" +) + +type NotificationConfig struct { + Notification bool `json:"notification"` +} + +func (n *Notification) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "notification", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), + Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), + }, + Value: n.Config.Notification, + }, + } +} + +func (n *Notification) ConfigReceiver(config []byte) error { + c := &NotificationConfig{} + _ = json.Unmarshal(config, c) + n.Config = c + return nil +} diff --git a/notification-slack/go.mod b/notification-slack/go.mod index 57943ed1..3adb597b 100644 --- a/notification-slack/go.mod +++ b/notification-slack/go.mod @@ -1,48 +1,50 @@ -module github.com/apache/incubator-answer-plugins/notification-slack - -go 1.19 - -require ( - github.com/apache/incubator-answer v1.3.6 - github.com/apache/incubator-answer-plugins/util v1.0.2 - github.com/go-resty/resty/v2 v2.11.0 - github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f -) - -require ( - github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/google/wire v0.5.0 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect -) +module github.com/Anan1225/incubator-answer-plugins/notification-slack + +go 1.19 + +require ( + github.com/apache/incubator-answer v1.3.6 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/go-resty/resty/v2 v2.15.0 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +replace github.com/Anan1225/incubator-answer-plugins/notification-slack => /root/incubator-answer-plugins/notification-slack diff --git a/notification-slack/go.sum b/notification-slack/go.sum index c1a852a8..03bbbd16 100644 --- a/notification-slack/go.sum +++ b/notification-slack/go.sum @@ -1,163 +1,146 @@ -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= -github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= -github.com/apache/incubator-answer v1.3.6 h1:OddJdWqDrgIKY2wnLOipT3mjNI9h7fLNc4eEyyUp+hs= -github.com/apache/incubator-answer v1.3.6/go.mod h1:YKwpG0rwRC0kHcbILcIyIbPMwsWaZ8j5lHJ34DPIdMI= -github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= -github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= -github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= -github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= -github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer v1.3.6 h1:OddJdWqDrgIKY2wnLOipT3mjNI9h7fLNc4eEyyUp+hs= +github.com/apache/incubator-answer v1.3.6/go.mod h1:YKwpG0rwRC0kHcbILcIyIbPMwsWaZ8j5lHJ34DPIdMI= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= +github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/notification-slack/i18n/en_US.yaml b/notification-slack/i18n/en_US.yaml index 9067b38f..f3149992 100644 --- a/notification-slack/i18n/en_US.yaml +++ b/notification-slack/i18n/en_US.yaml @@ -1,82 +1,110 @@ -# 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. - -plugin: - slack_notification: - backend: - info: - name: - other: Slack Notification - description: - other: Send notifications to Slack - config: - tip: - title: - other: Push notification service has been turned off. - notification: - label: - other: Turn on push notifications - title: - other: Notifications - description: - other: Users will receive notifications on Slack. - user_config: - webhook_url: - title: - other: Webhook URL - inbox_notifications: - title: - other: Inbox Notifications - label: - other: Turn on inbox notifications - description: - other: Answers to your questions, comments, invites, and more. - all_new_questions: - title: - other: All New Questions - label: - other: Turn on all new questions - description: - other: Get notified of all new questions. Up to 50 questions per week. - new_questions_for_following_tags: - title: - other: New Questions for Following Tags - label: - other: Turn on new questions for following tags - description: - other: Get notified of new questions for following tags. - tpl: - update_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated question <{{.QuestionUrl}}|{{.QuestionTitle}}>" - answer_the_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> answered the question <{{.AnswerUrl}}|{{.QuestionTitle}}>" - update_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated answer <{{.AnswerUrl}}|{{.QuestionTitle}}>" - accept_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> accepted answer <{{.AnswerUrl}}|{{.QuestionTitle}}>" - comment_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented question <{{.CommentUrl}}|{{.QuestionTitle}}>" - comment_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented answer <{{.CommentUrl}}|{{.QuestionTitle}}>" - reply_to_you: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> replied you <{{.CommentUrl}}|{{.QuestionTitle}}>" - mention_you: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> mentioned you <{{.CommentUrl}}|{{.QuestionTitle}}>" - invited_you_to_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> invited you to answer <{{.QuestionUrl}}|{{.QuestionTitle}}>" - new_question: - other: "New question:\n<{{.QuestionUrl}}|{{.QuestionTitle}}>\n{{.QuestionTags}}" +# 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. + +plugin: + slack_notification: + backend: + info: + name: + other: Slack Notification + description: + other: Send notifications to Slack + config: + tip: + title: + other: Push notification service has been turned off. + notification: + label: + other: Turn on push notifications + title: + other: Notifications + description: + other: Users will receive notifications on Slack. + user_config: + webhook_url: + title: + other: Webhook URL + inbox_notifications: + title: + other: Inbox Notifications + label: + other: Turn on inbox notifications + description: + other: Answers to your questions, comments, invites, and more. + all_new_questions: + title: + other: All New Questions + label: + other: Turn on all new questions + description: + other: Get notified of all new questions. Up to 50 questions per week. + new_questions_for_following_tags: + title: + other: New Questions for Following Tags + label: + other: Turn on new questions for following tags + description: + other: Get notified of new questions for following tags. + upvoted_answers: + title: + other: UpVoted Answers + label: + other: Turn on notification for upvoted answers + description: + other: Get notified of upvoted answers + downvoted_answers: + title: + other: DownVoted Answers + label: + other: Turn on notification for downvoted answers + description: + other: Get notified of downvoted answers + updated_questions: + title: + other: Updated Questions + label: + other: Turn on notification for updated questions + description: + other: Get notified of updated questions + updated_answers: + title: + other: Updated Answers + label: + other: Turn on notification for updated answers + description: + other: Get notified of updated answers + tpl: + updated_questions: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated questions <{{.QuestionUrl}}|{{.QuestionTitle}}>" + answer_the_question: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> answered the question <{{.AnswerUrl}}|{{.QuestionTitle}}>" + updated_answers: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> updated answers <{{.AnswerUrl}}|{{.QuestionTitle}}>" + accept_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> accepted answer <{{.AnswerUrl}}|{{.QuestionTitle}}>" + comment_question: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented question <{{.CommentUrl}}|{{.QuestionTitle}}>" + comment_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> commented answer <{{.CommentUrl}}|{{.QuestionTitle}}>" + reply_to_you: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> replied you <{{.CommentUrl}}|{{.QuestionTitle}}>" + mention_you: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> mentioned you <{{.CommentUrl}}|{{.QuestionTitle}}>" + invited_you_to_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> invited you to answer <{{.QuestionUrl}}|{{.QuestionTitle}}>" + new_question: + other: "New question:\n<{{.QuestionUrl}}|{{.QuestionTitle}}>\n{{.QuestionTags}}" diff --git a/notification-slack/i18n/translation.go b/notification-slack/i18n/translation.go index 16e8cab5..51660f31 100644 --- a/notification-slack/i18n/translation.go +++ b/notification-slack/i18n/translation.go @@ -1,53 +1,71 @@ -/* - * 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 i18n - -const ( - InfoName = "plugin.slack_notification.backend.info.name" - InfoDescription = "plugin.slack_notification.backend.info.description" - ConfigTipTitle = "plugin.slack_notification.backend.config.tip.title" - ConfigNotificationLabel = "plugin.slack_notification.backend.config.notification.label" - ConfigNotificationTitle = "plugin.slack_notification.backend.config.notification.title" - ConfigNotificationDescription = "plugin.slack_notification.backend.config.notification.description" - - UserConfigWebhookURLTitle = "plugin.slack_notification.backend.user_config.webhook_url.title" - UserConfigInboxNotificationsTitle = "plugin.slack_notification.backend.user_config.inbox_notifications.title" - UserConfigInboxNotificationsLabel = "plugin.slack_notification.backend.user_config.inbox_notifications.label" - UserConfigInboxNotificationsDescription = "plugin.slack_notification.backend.user_config.inbox_notifications.description" - - UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_notification.backend.user_config.all_new_questions.title" - UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_notification.backend.user_config.all_new_questions.label" - UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_notification.backend.user_config.all_new_questions.description" - - UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.title" - UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.label" - UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.description" - - TplUpdateQuestion = "plugin.slack_notification.backend.tpl.update_question" - TplAnswerTheQuestion = "plugin.slack_notification.backend.tpl.answer_the_question" - TplUpdateAnswer = "plugin.slack_notification.backend.tpl.update_answer" - TplAcceptAnswer = "plugin.slack_notification.backend.tpl.accept_answer" - TplCommentQuestion = "plugin.slack_notification.backend.tpl.comment_question" - TplCommentAnswer = "plugin.slack_notification.backend.tpl.comment_answer" - TplReplyToYou = "plugin.slack_notification.backend.tpl.reply_to_you" - TplMentionYou = "plugin.slack_notification.backend.tpl.mention_you" - TplInvitedYouToAnswer = "plugin.slack_notification.backend.tpl.invited_you_to_answer" - TplNewQuestion = "plugin.slack_notification.backend.tpl.new_question" -) +/* + * 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 i18n + +const ( + InfoName = "plugin.slack_notification.backend.info.name" + InfoDescription = "plugin.slack_notification.backend.info.description" + ConfigTipTitle = "plugin.slack_notification.backend.config.tip.title" + ConfigNotificationLabel = "plugin.slack_notification.backend.config.notification.label" + ConfigNotificationTitle = "plugin.slack_notification.backend.config.notification.title" + ConfigNotificationDescription = "plugin.slack_notification.backend.config.notification.description" + + UserConfigWebhookURLTitle = "plugin.slack_notification.backend.user_config.webhook_url.title" + UserConfigInboxNotificationsTitle = "plugin.slack_notification.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.slack_notification.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.slack_notification.backend.user_config.inbox_notifications.description" + + UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_notification.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_notification.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_notification.backend.user_config.all_new_questions.description" + + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.description" + + UserConfigUpvotedAnswersTitle = "plugin.slack_notification.backend.user_config.upvoted_answers.title" + UserConfigUpvotedAnswersLabel = "plugin.slack_notification.backend.user_config.upvoted_answers.label" + UserConfigUpvotedAnswersDescription = "plugin.slack_notification.backend.user_config.upvoted_answers.description" + + UserConfigDownvotedAnswersTitle = "plugin.slack_notification.backend.user_config.downvoted_answers.title" + UserConfigDownvotedAnswersLabel = "plugin.slack_notification.backend.user_config.downvoted_answers.label" + UserConfigDownvotedAnswersDescription = "plugin.slack_notification.backend.user_config.downvoted_answers.description" + + UserConfigUpdatedQuestionsTitle = "plugin.slack_notification.backend.user_config.updated_questions.title" + UserConfigUpdatedQuestionsLabel = "plugin.slack_notification.backend.user_config.updated_questions.label" + UserConfigUpdatedQuestionsDescription = "plugin.slack_notification.backend.user_config.updated_questions.description" + + UserConfigUpdatedAnswersTitle = "plugin.slack_notification.backend.user_config.updated_answers.title" + UserConfigUpdatedAnswersLabel = "plugin.slack_notification.backend.user_config.updated_answers.label" + UserConfigUpdatedAnswersDescription = "plugin.slack_notification.backend.user_config.updated_answers.description" + + TplUpdatedQuestions = "plugin.slack_notification.backend.tpl.updated_questions" + TplAnswerTheQuestion = "plugin.slack_notification.backend.tpl.answer_the_question" + TplUpdatedAnswers = "plugin.slack_notification.backend.tpl.updated_answers" + TplAcceptAnswer = "plugin.slack_notification.backend.tpl.accept_answer" + TplCommentQuestion = "plugin.slack_notification.backend.tpl.comment_question" + TplCommentAnswer = "plugin.slack_notification.backend.tpl.comment_answer" + TplReplyToYou = "plugin.slack_notification.backend.tpl.reply_to_you" + TplMentionYou = "plugin.slack_notification.backend.tpl.mention_you" + TplInvitedYouToAnswer = "plugin.slack_notification.backend.tpl.invited_you_to_answer" + TplNewQuestion = "plugin.slack_notification.backend.tpl.new_question" + TplUpvotedAnswer = "plugin.slack_notification.backend.tpl.upvoted_answer" + TplDownvotedAnswer = "plugin.slack_notification.backend.tpl.downvoted_answer" +) diff --git a/notification-slack/i18n/zh_CN.yaml b/notification-slack/i18n/zh_CN.yaml index 901e2c9b..00ed99b1 100644 --- a/notification-slack/i18n/zh_CN.yaml +++ b/notification-slack/i18n/zh_CN.yaml @@ -1,82 +1,82 @@ -# 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. - -plugin: - slack_notification: - backend: - info: - name: - other: Slack 通知 - description: - other: 发送通知到 Slack - config: - tip: - title: - other: 推送通知服务已关闭。 - notification: - label: - other: 打开通知 - title: - other: 通知 - description: - other: 用户将在 Slack 上收到通知。 - user_config: - webhook_url: - title: - other: Webhook URL - inbox_notifications: - title: - other: 收件箱通知 - label: - other: 打开收件箱通知 - description: - other: 问题的答案、评论、邀请等。 - all_new_questions: - title: - other: 所有新问题通知 - label: - other: 打开所有新问题通知 - description: - other: 收到所有新问题的通知。每周最多 50 个问题。 - new_questions_for_following_tags: - title: - other: 关注标签的新问题通知 - label: - other: 打开关注标签的新问题通知 - description: - other: 收到以下标签的新问题通知。 - tpl: - update_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新问题 <{{.QuestionUrl}}|{{.QuestionTitle}}>" - answer_the_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回答了问题 <{{.AnswerUrl}}|{{.QuestionTitle}}>" - update_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" - accept_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 接受答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" - comment_question: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论提问 <{{.CommentUrl}}|{{.QuestionTitle}}>" - comment_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论回答 <{{.CommentUrl}}|{{.QuestionTitle}}>" - reply_to_you: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回复了问题 <{{.CommentUrl}}|{{.QuestionTitle}}>" - mention_you: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 提到了你 <{{.CommentUrl}}|{{.QuestionTitle}}>" - invited_you_to_answer: - other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 邀请你回答 <{{.QuestionUrl}}|{{.QuestionTitle}}>" - new_question: +# 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. + +plugin: + slack_notification: + backend: + info: + name: + other: Slack 通知 + description: + other: 发送通知到 Slack + config: + tip: + title: + other: 推送通知服务已关闭。 + notification: + label: + other: 打开通知 + title: + other: 通知 + description: + other: 用户将在 Slack 上收到通知。 + user_config: + webhook_url: + title: + other: Webhook URL + inbox_notifications: + title: + other: 收件箱通知 + label: + other: 打开收件箱通知 + description: + other: 问题的答案、评论、邀请等。 + all_new_questions: + title: + other: 所有新问题通知 + label: + other: 打开所有新问题通知 + description: + other: 收到所有新问题的通知。每周最多 50 个问题。 + new_questions_for_following_tags: + title: + other: 关注标签的新问题通知 + label: + other: 打开关注标签的新问题通知 + description: + other: 收到以下标签的新问题通知。 + tpl: + update_question: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新问题 <{{.QuestionUrl}}|{{.QuestionTitle}}>" + answer_the_question: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回答了问题 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + update_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + accept_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 接受答案 <{{.AnswerUrl}}|{{.QuestionTitle}}>" + comment_question: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论提问 <{{.CommentUrl}}|{{.QuestionTitle}}>" + comment_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 评论回答 <{{.CommentUrl}}|{{.QuestionTitle}}>" + reply_to_you: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 回复了问题 <{{.CommentUrl}}|{{.QuestionTitle}}>" + mention_you: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 提到了你 <{{.CommentUrl}}|{{.QuestionTitle}}>" + invited_you_to_answer: + other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 邀请你回答 <{{.QuestionUrl}}|{{.QuestionTitle}}>" + new_question: other: "新问题:\n<{{.QuestionUrl}}|{{.QuestionTitle}}>\n{{.QuestionTags}}" \ No newline at end of file diff --git a/notification-slack/info.yaml b/notification-slack/info.yaml index 6b6e5fe0..92f4897e 100644 --- a/notification-slack/info.yaml +++ b/notification-slack/info.yaml @@ -1,22 +1,22 @@ -# 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. - -slug_name: slack_notification -type: notification -version: 1.0.2 -author: answerdev -link: https://github.com/apache/incubator-answer-plugins/tree/main/notification-slack +# 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. + +slug_name: slack_notification +type: notification +version: 1.0.2 +author: answerdev +link: https://github.com/Anan1225/incubator-answer-plugins/tree/main/notification-slack diff --git a/notification-slack/schema.go b/notification-slack/schema.go index d10581c1..d56c83f0 100644 --- a/notification-slack/schema.go +++ b/notification-slack/schema.go @@ -1,53 +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 slack - -type WebhookReq struct { - Blocks []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - } `json:"blocks"` -} - -func NewWebhookReq(content string) *WebhookReq { - return &WebhookReq{ - Blocks: []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - }{ - { - Type: "section", - Text: struct { - Type string `json:"type"` - Text string `json:"text"` - }{ - Type: "mrkdwn", - Text: content, - }, - }, - }, - } -} +/* + * 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 slack_notification + +type WebhookReq struct { + Blocks []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + } `json:"blocks"` +} + +func NewWebhookReq(content string) *WebhookReq { + return &WebhookReq{ + Blocks: []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + }{ + { + Type: "section", + Text: struct { + Type string `json:"type"` + Text string `json:"text"` + }{ + Type: "mrkdwn", + Text: content, + }, + }, + }, + } +} diff --git a/notification-slack/slack_notification.go b/notification-slack/slack_notification.go index c6b01587..43664817 100644 --- a/notification-slack/slack_notification.go +++ b/notification-slack/slack_notification.go @@ -1,166 +1,190 @@ -/* - * 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 slack - -import ( - "embed" - "github.com/apache/incubator-answer-plugins/util" - "github.com/go-resty/resty/v2" - "strings" - - slackI18n "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/segmentfault/pacman/i18n" - "github.com/segmentfault/pacman/log" -) - -//go:embed info.yaml -var Info embed.FS - -type Notification struct { - Config *NotificationConfig - UserConfigCache *UserConfigCache -} - -func init() { - uc := &Notification{ - Config: &NotificationConfig{}, - UserConfigCache: NewUserConfigCache(), - } - plugin.Register(uc) -} - -func (n *Notification) Info() plugin.Info { - info := &util.Info{} - info.GetInfo(Info) - - return plugin.Info{ - Name: plugin.MakeTranslator(slackI18n.InfoName), - SlugName: info.SlugName, - Description: plugin.MakeTranslator(slackI18n.InfoDescription), - Author: info.Author, - Version: info.Version, - Link: info.Link, - } -} - -// GetNewQuestionSubscribers returns the subscribers of the new question notification -func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) { - for userID, conf := range n.UserConfigCache.userConfigMapping { - if conf.AllNewQuestions { - userIDs = append(userIDs, userID) - } - } - return userIDs -} - -// Notify sends a notification to the user -func (n *Notification) Notify(msg plugin.NotificationMessage) { - log.Debugf("try to send notification %+v", msg) - - if !n.Config.Notification { - return - } - - // get user config - userConfig, err := n.getUserConfig(msg.ReceiverUserID) - if err != nil { - log.Errorf("get user config failed: %v", err) - return - } - if userConfig == nil { - log.Debugf("user %s has no config", msg.ReceiverUserID) - return - } - - // check if the notification is enabled - switch msg.Type { - case plugin.NotificationNewQuestion: - if !userConfig.AllNewQuestions { - log.Debugf("user %s not config the new question", msg.ReceiverUserID) - return - } - case plugin.NotificationNewQuestionFollowedTag: - if !userConfig.NewQuestionsForFollowingTags { - log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) - return - } - default: - if !userConfig.InboxNotifications { - log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) - return - } - } - - log.Debugf("user %s config the notification", msg.ReceiverUserID) - - if len(userConfig.WebhookURL) == 0 { - log.Errorf("user %s has no webhook url", msg.ReceiverUserID) - return - } - - notificationMsg := renderNotification(msg) - // no need to send empty message - if len(notificationMsg) == 0 { - log.Debugf("this type of notification will be drop, the type is %s", msg.Type) - return - } - - // Create a Resty Client - client := resty.New() - resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(NewWebhookReq(notificationMsg)). - Post(userConfig.WebhookURL) - - if err != nil { - log.Errorf("send message failed: %v %v", err, resp) - } else { - log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) - } -} - -func renderNotification(msg plugin.NotificationMessage) string { - lang := i18n.Language(msg.ReceiverLang) - switch msg.Type { - case plugin.NotificationUpdateQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplUpdateQuestion, msg) - case plugin.NotificationAnswerTheQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) - case plugin.NotificationUpdateAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpdateAnswer, msg) - case plugin.NotificationAcceptAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) - case plugin.NotificationCommentQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) - case plugin.NotificationCommentAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) - case plugin.NotificationReplyToYou: - return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) - case plugin.NotificationMentionYou: - return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) - case plugin.NotificationInvitedYouToAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) - case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: - msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") - return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) - } - return "" -} +/* + * 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 slack_notification + +import ( + "embed" + "strings" + + "github.com/apache/incubator-answer-plugins/util" + "github.com/go-resty/resty/v2" + + slackI18n "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Notification struct { + Config *NotificationConfig + UserConfigCache *UserConfigCache +} + +func init() { + uc := &Notification{ + Config: &NotificationConfig{}, + UserConfigCache: NewUserConfigCache(), + } + plugin.Register(uc) +} + +func (n *Notification) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(slackI18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(slackI18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +// GetNewQuestionSubscribers returns the subscribers of the new question notification +func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) { + for userID, conf := range n.UserConfigCache.userConfigMapping { + if conf.AllNewQuestions { + userIDs = append(userIDs, userID) + } + } + return userIDs +} + +// Notify sends a notification to the user +func (n *Notification) Notify(msg plugin.NotificationMessage) { + log.Debugf("try to send notification %+v", msg) + + if !n.Config.Notification { + return + } + + // get user config + userConfig, err := n.getUserConfig(msg.ReceiverUserID) + if err != nil { + log.Errorf("get user config failed: %v", err) + return + } + if userConfig == nil { + log.Debugf("user %s has no config", msg.ReceiverUserID) + return + } + + // check if the notification is enabled + switch msg.Type { + case plugin.NotificationNewQuestion: + if !userConfig.AllNewQuestions { + log.Debugf("user %s not config the new question", msg.ReceiverUserID) + return + } + case plugin.NotificationNewQuestionFollowedTag: + if !userConfig.NewQuestionsForFollowingTags { + log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) + return + } + case plugin.NotificationUpVotedTheAnswer: + if !userConfig.UpvotedAnswers { + log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) + } + case plugin.NotificationDownVotedTheAnswer: + if !userConfig.DownvotedAnswers { + log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) + } + + case plugin.NotificationUpdateQuestion: + if !userConfig.UpdatedQuestions { + log.Debugf("user %s not config the update question", msg.ReceiverUserID) + return + } + case plugin.NotificationUpdateAnswer: + if !userConfig.UpdatedAnswers { + log.Debugf("user %s not config the update answer", msg.ReceiverUserID) + return + } + default: + if !userConfig.InboxNotifications { + log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) + return + } + } + + log.Debugf("user %s config the notification", msg.ReceiverUserID) + + if len(userConfig.WebhookURL) == 0 { + log.Errorf("user %s has no webhook url", msg.ReceiverUserID) + return + } + + notificationMsg := renderNotification(msg) + // no need to send empty message + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + + // Create a Resty Client + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(NewWebhookReq(notificationMsg)). + Post(userConfig.WebhookURL) + + if err != nil { + log.Errorf("send message failed: %v %v", err, resp) + } else { + log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) + } +} + +func renderNotification(msg plugin.NotificationMessage) string { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") + return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) + case plugin.NotificationUpVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) + case plugin.NotificationDownVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) + } + return "" +} diff --git a/notification-slack/user_config.go b/notification-slack/user_config.go index 270df6ee..b7f5ee59 100644 --- a/notification-slack/user_config.go +++ b/notification-slack/user_config.go @@ -1,137 +1,165 @@ -/* - * 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 slack - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/segmentfault/pacman/log" -) - -type UserConfig struct { - WebhookURL string `json:"webhook_url"` - InboxNotifications bool `json:"inbox_notifications"` - AllNewQuestions bool `json:"all_new_questions"` - NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` -} - -type UserConfigCache struct { - // key: userID value: user config - userConfigMapping map[string]*UserConfig - sync.Mutex -} - -func NewUserConfigCache() *UserConfigCache { - ucc := &UserConfigCache{ - userConfigMapping: make(map[string]*UserConfig), - } - return ucc -} - -func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { - ucc.Lock() - defer ucc.Unlock() - ucc.userConfigMapping[userID] = config -} - -func (n *Notification) UserConfigFields() []plugin.ConfigField { - fields := make([]plugin.ConfigField, 0) - // Show tip for user, if the notification service is disabled - if !n.Config.Notification { - fields = append(fields, plugin.ConfigField{ - Name: "tip", - Type: plugin.ConfigTypeLegend, - Title: plugin.MakeTranslator(i18n.ConfigTipTitle), - Description: plugin.Translator{}, - UIOptions: plugin.ConfigFieldUIOptions{ - ClassName: "mb-3", - FieldClassName: "mb-0 text-danger", - }, - }) - } - fields = append(fields, plugin.ConfigField{ - Name: "webhook_url", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - }) - fields = append(fields, createSwitchConfig( - "inbox_notifications", - i18n.UserConfigInboxNotificationsTitle, - i18n.UserConfigInboxNotificationsLabel, - i18n.UserConfigInboxNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "all_new_questions", - i18n.UserConfigAllNewQuestionsNotificationsTitle, - i18n.UserConfigAllNewQuestionsNotificationsLabel, - i18n.UserConfigAllNewQuestionsNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "new_questions_for_following_tags", - i18n.UserConfigNewQuestionsForFollowingTagsTitle, - i18n.UserConfigNewQuestionsForFollowingTagsLabel, - i18n.UserConfigNewQuestionsForFollowingTagsDescription, - )) - return fields -} - -func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { - return plugin.ConfigField{ - Name: name, - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(title), - Description: plugin.MakeTranslator(desc), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(label), - }, - } -} - -func (n *Notification) UserConfigReceiver(userID string, config []byte) error { - log.Debugf("receive user config %s %s", userID, string(config)) - var userConfig UserConfig - err := json.Unmarshal(config, &userConfig) - if err != nil { - return fmt.Errorf("unmarshal user config failed: %w", err) - } - n.UserConfigCache.SetUserConfig(userID, &userConfig) - return nil -} - -func (n *Notification) getUserConfig(userID string) (config *UserConfig, err error) { - userConfig := plugin.GetPluginUserConfig(userID, n.Info().SlugName) - if len(userConfig) == 0 { - return nil, nil - } - config = &UserConfig{} - err = json.Unmarshal(userConfig, config) - if err != nil { - return nil, fmt.Errorf("unmarshal user config failed: %w", err) - } - return config, nil -} +/* + * 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 slack_notification + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + WebhookURL string `json:"webhook_url"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` + UpvotedAnswers bool `json:"upvoted_answers"` + DownvotedAnswers bool `json:"downvoted_answers"` + UpdatedQuestions bool `json:"updated_questions"` + UpdatedAnswers bool `json:"updated_answers"` +} + +type UserConfigCache struct { + // key: userID value: user config + userConfigMapping map[string]*UserConfig + sync.Mutex +} + +func NewUserConfigCache() *UserConfigCache { + ucc := &UserConfigCache{ + userConfigMapping: make(map[string]*UserConfig), + } + return ucc +} + +func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { + ucc.Lock() + defer ucc.Unlock() + ucc.userConfigMapping[userID] = config +} + +func (n *Notification) UserConfigFields() []plugin.ConfigField { + fields := make([]plugin.ConfigField, 0) + // Show tip for user, if the notification service is disabled + if !n.Config.Notification { + fields = append(fields, plugin.ConfigField{ + Name: "tip", + Type: plugin.ConfigTypeLegend, + Title: plugin.MakeTranslator(i18n.ConfigTipTitle), + Description: plugin.Translator{}, + UIOptions: plugin.ConfigFieldUIOptions{ + ClassName: "mb-3", + FieldClassName: "mb-0 text-danger", + }, + }) + } + fields = append(fields, plugin.ConfigField{ + Name: "webhook_url", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }) + fields = append(fields, createSwitchConfig( + "inbox_notifications", + i18n.UserConfigInboxNotificationsTitle, + i18n.UserConfigInboxNotificationsLabel, + i18n.UserConfigInboxNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "all_new_questions", + i18n.UserConfigAllNewQuestionsNotificationsTitle, + i18n.UserConfigAllNewQuestionsNotificationsLabel, + i18n.UserConfigAllNewQuestionsNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "new_questions_for_following_tags", + i18n.UserConfigNewQuestionsForFollowingTagsTitle, + i18n.UserConfigNewQuestionsForFollowingTagsLabel, + i18n.UserConfigNewQuestionsForFollowingTagsDescription, + )) + fields = append(fields, createSwitchConfig( + "upvoted_answers", + i18n.UserConfigUpvotedAnswersTitle, + i18n.UserConfigUpvotedAnswersLabel, + i18n.UserConfigUpvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "downvoted_answers", + i18n.UserConfigDownvotedAnswersTitle, + i18n.UserConfigDownvotedAnswersLabel, + i18n.UserConfigDownvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_questions", + i18n.UserConfigUpdatedQuestionsTitle, + i18n.UserConfigUpdatedQuestionsLabel, + i18n.UserConfigUpdatedQuestionsDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_answers", + i18n.UserConfigUpdatedAnswersTitle, + i18n.UserConfigUpdatedAnswersLabel, + i18n.UserConfigUpdatedAnswersDescription, + )) + return fields +} + +func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(label), + }, + } +} + +func (n *Notification) UserConfigReceiver(userID string, config []byte) error { + log.Debugf("receive user config %s %s", userID, string(config)) + var userConfig UserConfig + err := json.Unmarshal(config, &userConfig) + if err != nil { + return fmt.Errorf("unmarshal user config failed: %w", err) + } + n.UserConfigCache.SetUserConfig(userID, &userConfig) + return nil +} + +func (n *Notification) getUserConfig(userID string) (config *UserConfig, err error) { + userConfig := plugin.GetPluginUserConfig(userID, n.Info().SlugName) + if len(userConfig) == 0 { + return nil, nil + } + config = &UserConfig{} + err = json.Unmarshal(userConfig, config) + if err != nil { + return nil, fmt.Errorf("unmarshal user config failed: %w", err) + } + return config, nil +} diff --git a/user-center-slack/README.md b/user-center-slack/README.md new file mode 100644 index 00000000..753eec9b --- /dev/null +++ b/user-center-slack/README.md @@ -0,0 +1,20 @@ +# WeCom User Center +## Feature +- User login via WeCom QRCode + +## Config +> You need to create a WeCom App first, and then get the `Corp ID`, `Agent ID` and `App Secret` from the App. + +- `Company ID`: WeCom Corp ID +- `App Secret`: WeCom App Secret +- `App Agent ID`: WeCom App Agent ID + +Note: WeCom restricts the ip address of the callback url, so you need to add the ip address of the server where the project is located to the callback url. + +## Preview +![WeCom Config](./docs/wecom-config.png) +![WeCom QRCode](./docs/wecom-qrcode.png) +![WeCom Login](./docs/wecom-login.png) + +## Document +- https://developer.work.weixin.qq.com/document/path/90664 \ No newline at end of file diff --git a/user-center-slack/client.go b/user-center-slack/client.go new file mode 100644 index 00000000..3525fd2b --- /dev/null +++ b/user-center-slack/client.go @@ -0,0 +1,179 @@ +package slack_user_center + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/go-resty/resty/v2" + "github.com/segmentfault/pacman/log" +) + +type SlackClient struct { + AccessToken string + ClientID string + ClientSecret string + RedirectURI string + AuthedUserID string + + Enabled bool // 用户是否启用的标记 + UserInfoMapping map[string]*UserInfo + ChannelMapping string +} + +func NewSlackClient(clientID, clientSecret string) *SlackClient { + return &SlackClient{ + ClientID: clientID, + ClientSecret: clientSecret, + } +} + +// OAuthV2ResponseTeam +type OAuthV2ResponseTeam struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OAuthResponseIncomingWebhook +type OAuthResponseIncomingWebhook struct { + URL string `json:"url"` + Channel string `json:"channel"` + ChannelID string `json:"channel_id,omitempty"` + ConfigurationURL string `json:"configuration_url"` +} + +// OAuthV2ResponseEnterprise +type OAuthV2ResponseEnterprise struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OAuthV2ResponseAuthedUser +type OAuthV2ResponseAuthedUser struct { + ID string `json:"id"` + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + BotUserID string `json:"bot_user_id"` + AppID string `json:"app_id"` + Team OAuthV2ResponseTeam `json:"team"` + IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` + Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` + AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + Error string `json:"error,omitempty"` +} + +type Member struct { + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +// ExchangeCodeForUser through OAuthToken +func (sc *SlackClient) AuthUser(code string) (info *UserInfo, err error) { + clientID := sc.ClientID + clientSecret := sc.ClientSecret + redirectURI := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", "https://as.0vo.lol") + + data := url.Values{} + data.Set("code", code) + data.Set("client_id", clientID) + data.Set("client_secret", clientSecret) + data.Set("redirect_uri", redirectURI) + + resp, err := http.PostForm("https://slack.com/api/oauth.v2.access", data) + if err != nil { + log.Errorf("Failed to exchange code for token: %v", err) + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Failed to read response body: %v", err) + } + + var tokenResp TokenResponse + err = json.Unmarshal([]byte(body), &tokenResp) + if err != nil { + fmt.Println("Error parsing response:", err) + return nil, err + } + + if tokenResp.Error != "" { + return nil, fmt.Errorf("Slack API error in AuthUser: %s", tokenResp.Error) + } + + sc.Enabled = true + // sc.AccessToken = tokenResp.AccessToken + sc.AccessToken = tokenResp.AccessToken + sc.AuthedUserID = tokenResp.AuthedUser.ID + + // Get user detailed information + return sc.GetUserDetailInfo(sc.AuthedUserID) +} + +func (sc *SlackClient) GetUserDetailInfo(userid string) (info *UserInfo, err error) { + getUserInfoResp, err := resty.New().R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", sc.AccessToken)). + SetHeader("Accept", "application/json"). + Get("https://slack.com/api/users.info?user=" + userid) + if err != nil { + log.Errorf("Failed to get user info: %v", err) + return nil, err + } + + var authUserResp *AuthUserResp + err = json.Unmarshal([]byte(getUserInfoResp.String()), &authUserResp) + if err != nil { + log.Errorf("Error unmarshaling user info: %v", err) + return nil, err + } + if !authUserResp.Ok { + log.Errorf("Failed to get valid user info, Slack API error: %s", authUserResp.Errmsg) + return nil, fmt.Errorf("Get user info failed: %s", authUserResp.Errmsg) + } + log.Debugf("Get user info for UserID: %s", userid) + + if authUserResp.User == nil { + log.Errorf("No user data available in the response") + return nil, fmt.Errorf("No user data available in the response") + } + + authUserResp.User.IsAvailable = true + authUserResp.User.Status = 1 + + // Directly returning the user data parsed from the response + return authUserResp.User, nil +} + +func (sc *SlackClient) UpdateUserInfo() (err error) { + log.Debug("Try to update slack client") + + userInfo, err := sc.GetUserDetailInfo(sc.AuthedUserID) + if err != nil { + log.Errorf("Failed to update user info: %v", err) + return err + } + + if sc.UserInfoMapping == nil { + sc.UserInfoMapping = make(map[string]*UserInfo) + } + sc.UserInfoMapping[sc.AuthedUserID] = userInfo + log.Infof("Updated user info for UserID: %s", sc.AuthedUserID) + + return nil +} diff --git a/user-center-slack/config.go b/user-center-slack/config.go new file mode 100644 index 00000000..561fdf09 --- /dev/null +++ b/user-center-slack/config.go @@ -0,0 +1,158 @@ +/* + * 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 slack_user_center + +import ( + "encoding/json" + "time" + + "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" +) + +type UserCenterConfig struct { + ClientID string `json:"client_id"` // Slack Client ID + ClientSecret string `json:"client_secret"` // Slack Client Secret + RedirectURI string `json:"redirect_uri"` // OAuth Redirect URI + SigningSecret string `json:"signing_secret"` // Slack Signing Secret + AutoSync bool `json:"auto_sync"` // Auto sync + Notification bool `json:"notification"` // Notification +} + +func NewSlackClientWithConfig(clientID, clientSecret, redirectURI string) *SlackClient { + return &SlackClient{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURI: redirectURI, + Enabled: true, + } +} + +func (uc *UserCenter) ConfigFields() []plugin.ConfigField { + syncState := plugin.LoadingActionStateNone + lastSuccessfulSyncAt := "None" + if !uc.syncTime.IsZero() { + syncState = plugin.LoadingActionStateComplete + lastSuccessfulSyncAt = uc.syncTime.In(time.FixedZone("GMT", 8*3600)).Format("2006-01-02 15:04:05") + } + t := func(ctx *plugin.GinContext) string { + return plugin.Translate(ctx, i18n.ConfigSyncNowDescription) + ": " + lastSuccessfulSyncAt + } + syncNowDesc := plugin.Translator{Fn: t} + + syncNowLabel := plugin.MakeTranslator(i18n.ConfigSyncNowLabel) + + if uc.syncing { + syncNowLabel = plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing) + syncState = plugin.LoadingActionStatePending + } + + return []plugin.ConfigField{ + { + Name: "auto_sync", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigAutoSyncTitle), + Description: plugin.MakeTranslator(i18n.ConfigAutoSyncDescription), + Required: false, + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigAutoSyncLabel), + }, + Value: uc.Config.AutoSync, + }, + { + Name: "sync_now", + Type: plugin.ConfigTypeButton, + Title: plugin.MakeTranslator(i18n.ConfigSyncNowTitle), + Description: syncNowDesc, + UIOptions: plugin.ConfigFieldUIOptions{ + Text: syncNowLabel, + Action: &plugin.UIOptionAction{ + Url: "/answer/admin/api/slack/sync", // 修改为 Slack 的同步 URL + Method: "get", + Loading: &plugin.LoadingAction{ + Text: plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing), + State: syncState, + }, + OnComplete: &plugin.OnCompleteAction{ + ToastReturnMessage: true, + RefreshFormConfig: true, + }, + }, + Variant: "outline-secondary", + }, + }, + { + Name: "client_id", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigClientIDTitle), // Slack Client ID + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: uc.Config.ClientID, + }, + { + Name: "client_secret", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigClientSecretTitle), // Slack Client Secret + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + }, + Value: uc.Config.ClientSecret, + }, + { + Name: "signing_secret", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigSigningSecretTitle), // Slack Redirect URI + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: uc.Config.SigningSecret, + }, + { + Name: "notification", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), + Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), + }, + Value: uc.Config.Notification, + }, + } +} + +func (uc *UserCenter) ConfigReceiver(config []byte) error { + c := &UserCenterConfig{} + err := json.Unmarshal(config, c) + if err != nil { + return err + } + uc.Config = c + + uc.SlackClient = NewSlackClient(c.ClientID, c.ClientSecret) + + if uc.Config.AutoSync { + uc.CronSyncData() + } + return nil +} diff --git a/user-center-slack/cron.go b/user-center-slack/cron.go new file mode 100644 index 00000000..33306509 --- /dev/null +++ b/user-center-slack/cron.go @@ -0,0 +1,41 @@ +/* + * 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 slack_user_center + +import ( + "time" + + "github.com/segmentfault/pacman/log" +) + +func (uc *UserCenter) CronSyncData() { + go func() { + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Infof("UserCenter is syncing Slack user data...") + uc.syncSlackClient() + } + } + }() +} diff --git a/user-center-slack/go.mod b/user-center-slack/go.mod new file mode 100644 index 00000000..f81ef78d --- /dev/null +++ b/user-center-slack/go.mod @@ -0,0 +1,60 @@ +module github.com/Anan1225/incubator-answer-plugins/user-center-slack + +go 1.22.7 + +toolchain go1.23.1 + +require ( + github.com/apache/incubator-answer v1.4.0 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-resty/resty/v2 v2.15.1 + github.com/jarcoal/httpmock v1.3.1 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +// replace github.com/Anan1225/incubator-answer-plugins/user-center-slack => /root/incubator-answer-plugins/user-center-slack +replace github.com/apache/incubator-answer => ../../incubator-answer diff --git a/user-center-slack/go.sum b/user-center-slack/go.sum new file mode 100644 index 00000000..f235a5a7 --- /dev/null +++ b/user-center-slack/go.sum @@ -0,0 +1,153 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.15.1 h1:vuna8FM2EaQ6IYbtjh+Gjh00uu7xEWuuGyTKeIaYkvE= +github.com/go-resty/resty/v2 v2.15.1/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/user-center-slack/handler.go b/user-center-slack/handler.go new file mode 100644 index 00000000..133ecee7 --- /dev/null +++ b/user-center-slack/handler.go @@ -0,0 +1,149 @@ +/* + * 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 slack_user_center + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +// RespBody response body. +type RespBody struct { + // http code + Code int `json:"code"` + // reason key + Reason string `json:"reason"` + // response message + Message string `json:"msg"` + // response data + Data interface{} `json:"data"` +} + +// NewRespBodyData new response body with data +func NewRespBodyData(code int, reason string, data interface{}) *RespBody { + return &RespBody{ + Code: code, + Reason: reason, + Data: data, + } +} + +func (uc *UserCenter) BuildSlackBaseRedirectURL() string { + clientID := uc.Config.ClientID + log.Debug("Get client ID:", clientID) + scope := "chat:write,commands,groups:write,im:write,incoming-webhook,mpim:write,users:read,users:read.email" // 需要的权限范围 + response_type := "code" + redirect_uri := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) + + base_redirectURL := fmt.Sprintf( + "https://slack.com/oauth/v2/authorize?client_id=%s&scope=%s&response_type=%s&redirect_uri=%s", + clientID, scope, response_type, redirect_uri, + ) + + state := genState() + nonce := genNonce() + uc.Cache.Set("oauth_state_"+state, state, time.Minute*5) + + redirectURL := fmt.Sprintf("%s&state=%s&nonce=%s", base_redirectURL, state, nonce) + log.Debug("RedirectURL from BuildSlackBaseRedirectURL:", redirectURL) + + return redirectURL +} + +func (uc *UserCenter) GetSlackRedirectURL(ctx *gin.Context) { + redirectURL := uc.BuildSlackBaseRedirectURL() + log.Debug("Processing GetSlackRedirectURL") + + ctx.Writer.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(ctx.Writer) + encoder.SetEscapeHTML(false) + + respData := NewRespBodyData(http.StatusOK, "success", map[string]string{ + "redirect_url": redirectURL, + }) + ctx.Writer.WriteHeader(http.StatusOK) + if err := encoder.Encode(respData); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode response"}) + return + } +} + +func genNonce() string { + bytes := make([]byte, 10) + _, _ = rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +func genState() string { + bytes := make([]byte, 32) + _, _ = rand.Read(bytes) + return base64.URLEncoding.EncodeToString(bytes) +} + +func (uc *UserCenter) Sync(ctx *gin.Context) { + uc.syncSlackClient() + + if uc.syncSuccess { + ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", map[string]any{ + "message": "User data synced successfully", + })) + return + } + + errRespBodyData := NewRespBodyData(http.StatusBadRequest, "error", map[string]any{ + "err_type": "toast", + }) + errRespBodyData.Message = "Failed to sync user data" + ctx.JSON(http.StatusBadRequest, errRespBodyData) +} + +func (uc *UserCenter) syncSlackClient() { + if !uc.syncLock.TryLock() { + log.Infof("sync data is running") + return + } + defer func() { + uc.syncing = false + if uc.syncSuccess { + uc.syncTime = time.Now() + } + uc.syncLock.Unlock() + }() + + log.Info("start sync slack data") + uc.syncing = true + uc.syncSuccess = true + + if err := uc.SlackClient.UpdateUserInfo(); err != nil { + log.Errorf("list department error: %s", err) + uc.syncSuccess = false + return + } + log.Info("end sync slack data") +} diff --git a/user-center-slack/i18n/en_US.yaml b/user-center-slack/i18n/en_US.yaml new file mode 100644 index 00000000..90ac7efd --- /dev/null +++ b/user-center-slack/i18n/en_US.yaml @@ -0,0 +1,121 @@ +# 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. + +plugin: + slack_user_center: + backend: + response: + sync_now: + success: + other: Contacts sync successful. + failed: + other: Contacts sync failed. + info: + name: + other: Slack User Center + description: + other: A plugin for integrating Slack user management + config: + tip: + title: + other: Push notification service has been turned off. + auto_sync: + label: + other: Turn on auto sync + title: + other: Auto Sync Contacts + description: + other: Automatically synchronize every hour. + sync_now: + label: + other: Sync now + label_for_doing: + other: Syncing + title: + other: Manual Sync Contacts + description: + other: Last successful sync + authorize_url: + title: + other: Authorize URL + description: + other: Slack authorize URL + client_id: + title: + other: Client ID + description: + other: Slack client ID + client_secret: + title: + other: Client Secret + description: + other: Slack client secret + notification: + label: + other: Turn on push notifications + title: + other: Notifications + description: + other: Users will receive notifications on Slack. + signing_secret: + title: + other: Signing Secret + description: + other: Slack signing secret + user_config: + inbox_notifications: + title: + other: Inbox Notifications + label: + other: Turn on inbox notifications + description: + other: Answers to your questions, comments, invites, and more. + all_new_questions: + title: + other: All New Questions + label: + other: Turn on all new questions + description: + other: Get notified of all new questions. Up to 50 questions per week. + new_questions_for_following_tags: + title: + other: New Questions for Following Tags + label: + other: Turn on new questions for following tags + description: + other: Get notified of new questions for following tags. + tpl: + update_question: + other: "{{.TriggerUserDisplayName}} updated question {{.QuestionTitle}}" + answer_the_question: + other: "{{.TriggerUserDisplayName}} answered the question {{.QuestionTitle}}" + update_answer: + other: "{{.TriggerUserDisplayName}} updated answer {{.QuestionTitle}}" + accept_answer: + other: "{{.TriggerUserDisplayName}} accepted answer {{.QuestionTitle}}" + comment_question: + other: "{{.TriggerUserDisplayName}} commented question {{.QuestionTitle}}" + comment_answer: + other: "{{.TriggerUserDisplayName}} commented answer {{.QuestionTitle}}" + reply_to_you: + other: "{{.TriggerUserDisplayName}} replied you {{.QuestionTitle}}" + mention_you: + other: "{{.TriggerUserDisplayName}} mentioned you {{.QuestionTitle}}" + invited_you_to_answer: + other: "{{.TriggerUserDisplayName}} invited you to answer {{.QuestionTitle}}" + new_question: + other: "New question:\n{{.QuestionTitle}}\n{{.QuestionTags}}" diff --git a/user-center-slack/i18n/translation.go b/user-center-slack/i18n/translation.go new file mode 100644 index 00000000..b2e13eac --- /dev/null +++ b/user-center-slack/i18n/translation.go @@ -0,0 +1,67 @@ +/* + * 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 i18n + +const ( + InfoName = "plugin.slack_user_center.backend.info.name" + InfoDescription = "plugin.slack_user_center.backend.info.description" + ConfigTipTitle = "plugin.slack_user_center.backend.config.tip.title" + ConfigAutoSyncLabel = "plugin.slack_user_center.backend.config.auto_sync.label" + ConfigAutoSyncTitle = "plugin.slack_user_center.backend.config.auto_sync.title" + ConfigAutoSyncDescription = "plugin.slack_user_center.backend.config.auto_sync.description" + ConfigSyncNowLabel = "plugin.slack_user_center.backend.config.sync_now.label" + ConfigSyncNowLabelForDoing = "plugin.slack_user_center.backend.config.sync_now.label_for_doing" + ConfigSyncNowTitle = "plugin.slack_user_center.backend.config.sync_now.title" + ConfigSyncNowDescription = "plugin.slack_user_center.backend.config.sync_now.description" + ConfigClientIDTitle = "plugin.slack_user_center.backend.config.client_id.title" + ConfigClientIDDescription = "plugin.slack_user_center.backend.config.client_id.description" + ConfigClientSecretTitle = "plugin.slack_user_center.backend.config.client_secret.title" + ConfigClientSecretDescription = "plugin.slack_user_center.backend.config.client_secret.description" + ConfigSigningSecretTitle = "plugin.slack_user_center.backend.config.signing_secret.title" + ConfigSigningSecretDescription = "plugin.slack_user_center.backend.config.signing_secret.description" + ConfigSyncNowSuccessResponse = "plugin.slack_user_center.backend.response.sync_now.success" + ConfigSyncNowFailedResponse = "plugin.slack_user_center.backend.response.sync_now.failed" + ConfigNotificationLabel = "plugin.slack_user_center.backend.config.notification.label" + ConfigNotificationTitle = "plugin.slack_user_center.backend.config.notification.title" + ConfigNotificationDescription = "plugin.slack_user_center.backend.config.notification.description" + + UserConfigInboxNotificationsTitle = "plugin.slack_user_center.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.slack_user_center.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.slack_user_center.backend.user_config.inbox_notifications.description" + + UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_user_center.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_user_center.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_user_center.backend.user_config.all_new_questions.description" + + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.description" + + TplUpdateQuestion = "plugin.slack_user_center.backend.tpl.update_question" + TplAnswerTheQuestion = "plugin.slack_user_center.backend.tpl.answer_the_question" + TplUpdateAnswer = "plugin.slack_user_center.backend.tpl.update_answer" + TplAcceptAnswer = "plugin.slack_user_center.backend.tpl.accept_answer" + TplCommentQuestion = "plugin.slack_user_center.backend.tpl.comment_question" + TplCommentAnswer = "plugin.slack_user_center.backend.tpl.comment_answer" + TplReplyToYou = "plugin.slack_user_center.backend.tpl.reply_to_you" + TplMentionYou = "plugin.slack_user_center.backend.tpl.mention_you" + TplInvitedYouToAnswer = "plugin.slack_user_center.backend.tpl.invited_you_to_answer" + TplNewQuestion = "plugin.slack_user_center.backend.tpl.new_question" +) diff --git a/user-center-slack/i18n/zh_CN.yaml b/user-center-slack/i18n/zh_CN.yaml new file mode 100644 index 00000000..9ff875af --- /dev/null +++ b/user-center-slack/i18n/zh_CN.yaml @@ -0,0 +1,121 @@ +# 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. + +plugin: + slack_user_center: + backend: + response: + sync_now: + success: + other: 联系人同步成功。 + failed: + other: 联系人同步失败。 + info: + name: + other: 企业微信 + description: + other: 从企业微信获取用户信息并同步到用户中心 + config: + tip: + title: + other: 推送通知服务已关闭。 + auto_sync: + label: + other: 打开自动同步 + title: + other: 自动同步联系人 + description: + other: 每小时自动同步。 + sync_now: + label: + other: 立即同步 + label_for_doing: + other: 同步中 + title: + other: 手动同步联系人 + description: + other: 上次成功同步于 + authorize_url: + title: + other: 授权网址 + description: + other: 企业微信授权网址 + corp_id: + title: + other: 企业 ID + description: + other: 企业微信企业ID + corp_secret: + title: + other: 应用 Secret + description: + other: 企业微信应用程序密钥 + agent_id: + title: + other: 应用 Agent ID + description: + other: 企业微信应用程序代理ID + notification: + label: + other: 打开通知 + title: + other: 通知 + description: + other: 用户将在企业微信上收到通知。 + user_config: + inbox_notifications: + title: + other: 收件箱通知 + label: + other: 打开收件箱通知 + description: + other: 问题的答案、评论、邀请等。 + all_new_questions: + title: + other: 所有新问题通知 + label: + other: 打开所有新问题通知 + description: + other: 收到所有新问题的通知。每周最多 50 个问题。 + new_questions_for_following_tags: + title: + other: 关注标签的新问题通知 + label: + other: 打开关注标签的新问题通知 + description: + other: 收到以下标签的新问题通知。 + tpl: + update_question: + other: "{{.TriggerUserDisplayName}} 更新问题 {{.QuestionTitle}}" + answer_the_question: + other: "{{.TriggerUserDisplayName}} 回答了问题 {{.QuestionTitle}}" + update_answer: + other: "{{.TriggerUserDisplayName}} 更新答案 {{.QuestionTitle}}" + accept_answer: + other: "{{.TriggerUserDisplayName}} 接受答案 {{.QuestionTitle}}" + comment_question: + other: "{{.TriggerUserDisplayName}} 评论提问 {{.QuestionTitle}}" + comment_answer: + other: "{{.TriggerUserDisplayName}} 评论回答 {{.QuestionTitle}}" + reply_to_you: + other: "{{.TriggerUserDisplayName}} 回复了问题 {{.QuestionTitle}}" + mention_you: + other: "{{.TriggerUserDisplayName}} 提到了你 {{.QuestionTitle}}" + invited_you_to_answer: + other: "{{.TriggerUserDisplayName}} 邀请你回答 {{.QuestionTitle}}" + new_question: + other: "新问题:\n{{.QuestionTitle}}\n{{.QuestionTags}}" \ No newline at end of file diff --git a/user-center-slack/importer.go b/user-center-slack/importer.go new file mode 100644 index 00000000..6277e8a8 --- /dev/null +++ b/user-center-slack/importer.go @@ -0,0 +1,136 @@ +package slack_user_center + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" +) + +func (uc *UserCenter) parseText(text string) (string, string, []string, error) { + re := regexp.MustCompile(`\[(.*?)\]`) + matches := re.FindAllStringSubmatch(text, -1) + + if len(matches) != 3 { + return "", "", nil, fmt.Errorf("text field does not conform to the required format") + } + + part1 := matches[0][1] + part2 := matches[1][1] + rawTags := strings.Split(matches[2][1], ",") + + var tags []string + for _, tag := range rawTags { + if tag != "" { + tags = append(tags, tag) + } + } + + // if part1 or part2 or tags in empty return error + if part1 == "" || part2 == "" || len(tags) == 0 { + return "", "", nil, fmt.Errorf("text field does not be empty") + } + return part1, part2, tags, nil +} +func getSlackUserEmail(userID, token string) (string, error) { + url := fmt.Sprintf("https://slack.com/api/users.info?user=%s", userID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var userResponse SlackUserResponse + if err := json.Unmarshal(body, &userResponse); err != nil { + return "", err + } + + if !userResponse.Ok { + return "", fmt.Errorf("failed to get user info from Slack") + } + + return userResponse.User.Profile.Email, nil +} +func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error { + body, err := io.ReadAll(ctx.Request.Body) + if err != nil { + return fmt.Errorf("could not read request body: %v", err) + } + timestamp := ctx.GetHeader("X-Slack-Request-Timestamp") + slackSignature := ctx.GetHeader("X-Slack-Signature") + + // check the timestamp validity in 5 minutes + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return fmt.Errorf("invalid timestamp: %v", err) + } + if time.Now().Unix()-ts > 60*5 { + return fmt.Errorf("timestamp is too old") + } + // Reset the request body for further processing + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + sigBaseString := fmt.Sprintf("v0:%s:%s", timestamp, string(body)) + + h := hmac.New(sha256.New, []byte(uc.Config.SigningSecret)) + h.Write([]byte(sigBaseString)) + computedSignature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + if !hmac.Equal([]byte(computedSignature), []byte(slackSignature)) { + return fmt.Errorf("invalid signature") + } + + return nil +} +func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo *plugin.QuestionImporterInfo, err error) { + questionInfo = &plugin.QuestionImporterInfo{} + + err = uc.verifySlackRequest(ctx) + if err != nil { + return nil, err + } + + text := ctx.PostForm("text") + part1, part2, tags, err := uc.parseText(text) + if err != nil { + return questionInfo, err + } + + questionInfo.Title = part1 + questionInfo.Content = part2 + questionInfo.Tags = tags + userID := ctx.PostForm("user_id") + + token := uc.SlackClient.AccessToken + email, err := getSlackUserEmail(userID, token) + if err != nil { + return questionInfo, err + } + + questionInfo.UserEmail = email + return questionInfo, nil +} diff --git a/user-center-slack/info.yaml b/user-center-slack/info.yaml new file mode 100644 index 00000000..8e3db487 --- /dev/null +++ b/user-center-slack/info.yaml @@ -0,0 +1,22 @@ +# 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. + +slug_name: slack_user_center +type: user_center +version: 1.0.1 +author: answerdev +link: https://github.com/Anan1225/incubator-answer-plugins/tree/main/user-center-slack diff --git a/user-center-slack/notification.go b/user-center-slack/notification.go new file mode 100644 index 00000000..5e754d01 --- /dev/null +++ b/user-center-slack/notification.go @@ -0,0 +1,137 @@ +package slack_user_center + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + slackI18n "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +// GetNewQuestionSubscribers returns the subscribers of the new question notification +func (uc *UserCenter) GetNewQuestionSubscribers() (userIDs []string) { + for userID, conf := range uc.UserConfigCache.userConfigMapping { + if conf.AllNewQuestions { + userIDs = append(userIDs, userID) + } + } + return userIDs +} + +// Notify sends a notification to the user using Slack +func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { + log.Debugf("try to send notification %+v", msg) + + if !uc.Config.Notification { + return + } + + // get user config + userConfig, err := uc.getUserConfig(msg.ReceiverUserID) + if err != nil { + log.Errorf("get user config failed: %v", err) + return + } + if userConfig == nil { + log.Debugf("user %s has no config", msg.ReceiverUserID) + return + } + + // check if the notification is enabled + switch msg.Type { + case plugin.NotificationNewQuestion: + if !userConfig.AllNewQuestions { + log.Debugf("user %s not config the new question", msg.ReceiverUserID) + return + } + case plugin.NotificationNewQuestionFollowedTag: + if !userConfig.NewQuestionsForFollowingTags { + log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) + return + } + default: + if !userConfig.InboxNotifications { + log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) + return + } + } + + log.Debugf("user %s config the notification", msg.ReceiverUserID) +} + +// renderNotification generates the notification message based on type +func renderNotification(msg plugin.NotificationMessage) string { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplUpdateQuestion, msg) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpdateAnswer, msg) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") + return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) + } + return "" +} + +// SendMessage sends a message to a Slack user using Slack API +func (sc *SlackClient) SendMessage(userID string, message string) error { + data := url.Values{} + data.Set("channel", userID) // Slack user ID or channel ID + data.Set("text", message) + + req, err := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", strings.NewReader(data.Encode())) + if err != nil { + return err + } + + req.Header.Add("Authorization", "Bearer "+sc.AccessToken) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + var result struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return err + } + + if !result.OK { + return fmt.Errorf("Slack API error: %s", result.Error) + } + + return nil +} diff --git a/user-center-slack/schema.go b/user-center-slack/schema.go new file mode 100644 index 00000000..3713895f --- /dev/null +++ b/user-center-slack/schema.go @@ -0,0 +1,68 @@ +/* + * 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 slack_user_center + +type AuthUserResp struct { + Ok bool `json:"ok"` + Errmsg string `json:"error"` + User *UserInfo `json:"user"` +} + +type UserProfile struct { + AvatarHash string `json:"avatar_hash"` + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + RealName string `json:"real_name"` + DisplayName string `json:"display_name"` + Email string `json:"email"` + ImageOriginal string `json:"image_original"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + Image512 string `json:"image_512"` +} + +type UserInfo struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + RealName string `json:"real_name"` + Deleted bool `json:"deleted"` + TimeZone string `json:"tz"` + TimeZoneLabel string `json:"tz_label"` + TimeZoneOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsBot bool `json:"is_bot"` + Updated int64 `json:"updated"` + IsAppUser bool `json:"is_app_user"` + Has2FA bool `json:"has_2fa"` + + LastLogin int64 `json:"last_login,omitempty"` + IsAvailable bool `json:"is_available"` + Enable bool `json:"true"` + Status int `json:"status"` +} diff --git a/user-center-slack/slack_user_center.go b/user-center-slack/slack_user_center.go new file mode 100644 index 00000000..76b3b2af --- /dev/null +++ b/user-center-slack/slack_user_center.go @@ -0,0 +1,259 @@ +/* + * 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 slack_user_center + +import ( + "embed" + "fmt" + "net/http" + "sync" + "time" + + "github.com/apache/incubator-answer-plugins/util" + + "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/patrickmn/go-cache" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Importer struct{} + +type SlackUserResponse struct { + Ok bool `json:"ok"` + User struct { + Profile struct { + Email string `json:"email"` + } `json:"profile"` + } `json:"user"` +} + +type UserCenter struct { + Config *UserCenterConfig + SlackClient *SlackClient + UserConfigCache *UserConfigCache + Cache *cache.Cache + syncLock sync.Mutex + syncing bool + syncSuccess bool + syncTime time.Time +} + +func (uc *UserCenter) RegisterUnAuthRouter(r *gin.RouterGroup) { + r.GET("/slack/login/url", uc.GetSlackRedirectURL) +} + +func (uc *UserCenter) RegisterAuthUserRouter(r *gin.RouterGroup) { +} + +func (uc *UserCenter) RegisterAuthAdminRouter(r *gin.RouterGroup) { + r.GET("/slack/sync", uc.Sync) +} + +func (uc *UserCenter) AfterLogin(externalID, accessToken string) { + log.Debugf("user %s is login", externalID) + uc.Cache.Set(externalID, accessToken, time.Minute*5) +} + +func (uc *UserCenter) UserStatus(externalID string) (userStatus plugin.UserStatus) { + if len(externalID) == 0 { + return plugin.UserStatusAvailable + } + + var err error + userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] + if userDetailInfo == nil { + userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) + if err != nil { + log.Errorf("get user detail info failed: %v", err) + } + } + if userDetailInfo == nil { + return plugin.UserStatusDeleted + } + switch userDetailInfo.Status { + case 1: + return plugin.UserStatusAvailable + case 2: + return plugin.UserStatusSuspended + default: + return plugin.UserStatusDeleted + } +} + +func init() { + uc := &UserCenter{ + Config: &UserCenterConfig{}, + UserConfigCache: NewUserConfigCache(), + SlackClient: NewSlackClient("", ""), + Cache: cache.New(5*time.Minute, 10*time.Minute), + syncLock: sync.Mutex{}, + } + + plugin.Register(uc) + uc.CronSyncData() +} + +func (uc *UserCenter) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (uc *UserCenter) Description() plugin.UserCenterDesc { + redirectURL := uc.BuildSlackBaseRedirectURL() + desc := plugin.UserCenterDesc{ + Name: "Slack", + DisplayName: plugin.MakeTranslator(i18n.InfoName), + Icon: "", + Url: "", + LoginRedirectURL: redirectURL, + SignUpRedirectURL: redirectURL, + RankAgentEnabled: false, + UserStatusAgentEnabled: false, + UserRoleAgentEnabled: false, + MustAuthEmailEnabled: true, + EnabledOriginalUserSystem: true, + } + return desc +} + +func (uc *UserCenter) ControlCenterItems() []plugin.ControlCenter { + var controlCenterItems []plugin.ControlCenter + return controlCenterItems +} + +func (uc *UserCenter) LoginCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + log.Debugf("Processing LoginCallback") + CallbackURL := ctx.Request.URL.String() + log.Debugf("callbackURL in SlackLoginCallback:", CallbackURL) + code := ctx.Query("code") + if len(code) == 0 { + return nil, fmt.Errorf("code is empty") + } + + state := ctx.Query("state") + if len(state) == 0 { + return nil, fmt.Errorf("state is empty") + } + log.Debugf("request code: %s, state: %s", code, state) + + expectedState, exist := uc.Cache.Get("oauth_state_" + state) + if !exist { + fmt.Println("State not found in cache or expired") + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired state"}) + return + } + if state != expectedState { + fmt.Println("State mismatch") + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid state"}) + return + } + log.Debugf("State validated successfully") + + info, err := uc.SlackClient.AuthUser(code) + if err != nil { + return nil, fmt.Errorf("auth user failed: %w", err) + } + if !info.IsAvailable { + return nil, fmt.Errorf("user is not available") + } + //Get Email + if len(info.Profile.Email) == 0 { + ctx.Redirect(http.StatusFound, "/user-center/auth-failed") + ctx.Abort() + return nil, fmt.Errorf("user email is empty") + } + + userInfo = &plugin.UserCenterBasicUserInfo{} + userInfo.ExternalID = info.ID + userInfo.Username = info.ID + userInfo.DisplayName = info.Name + userInfo.Email = info.Profile.Email + userInfo.Rank = 0 + userInfo.Mobile = "" + userInfo.Avatar = info.Profile.Image192 + + uc.Cache.Set(state, userInfo.ExternalID, time.Minute*5) + return userInfo, nil +} + +func (uc *UserCenter) SignUpCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + return uc.LoginCallback(ctx) +} + +func (uc *UserCenter) UserInfo(externalID string) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] + if userDetailInfo == nil { + userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) + if err != nil { + log.Errorf("get user detail info failed: %v", err) + userInfo = &plugin.UserCenterBasicUserInfo{ + ExternalID: externalID, + Status: plugin.UserStatusDeleted, + } + return userInfo, nil + } + } + + userInfo = &plugin.UserCenterBasicUserInfo{ + ExternalID: externalID, + Username: userDetailInfo.ID, + DisplayName: userDetailInfo.Name, + Bio: "", + } + switch userDetailInfo.Status { + case 1: + userInfo.Status = plugin.UserStatusAvailable + case 2: + userInfo.Status = plugin.UserStatusSuspended + default: + userInfo.Status = plugin.UserStatusDeleted + } + return userInfo, nil +} + +func (uc *UserCenter) UserList(externalIDs []string) (userList []*plugin.UserCenterBasicUserInfo, err error) { + userList = make([]*plugin.UserCenterBasicUserInfo, 0) + return userList, nil +} + +func (uc *UserCenter) UserSettings(externalID string) (userSettings *plugin.SettingInfo, err error) { + return &plugin.SettingInfo{ + ProfileSettingRedirectURL: "", + AccountSettingRedirectURL: "", + }, nil +} + +func (uc *UserCenter) PersonalBranding(externalID string) (branding []*plugin.PersonalBranding) { + return branding +} diff --git a/user-center-slack/user_config.go b/user-center-slack/user_config.go new file mode 100644 index 00000000..428253f3 --- /dev/null +++ b/user-center-slack/user_config.go @@ -0,0 +1,127 @@ +/* + * 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 slack_user_center + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` +} + +type UserConfigCache struct { + // key: userID value: user config + userConfigMapping map[string]*UserConfig + sync.Mutex +} + +func NewUserConfigCache() *UserConfigCache { + ucc := &UserConfigCache{ + userConfigMapping: make(map[string]*UserConfig), + } + return ucc +} + +func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { + ucc.Lock() + defer ucc.Unlock() + ucc.userConfigMapping[userID] = config +} + +func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { + fields := make([]plugin.ConfigField, 0) + // Show tip for user, if the notification service is disabled + if !uc.Config.Notification { + fields = append(fields, plugin.ConfigField{ + Name: "tip", + Type: plugin.ConfigTypeLegend, + Title: plugin.MakeTranslator(i18n.ConfigTipTitle), + Description: plugin.Translator{}, + UIOptions: plugin.ConfigFieldUIOptions{ + ClassName: "mb-3", + FieldClassName: "mb-0 text-danger", + }, + }) + } + fields = append(fields, createSwitchConfig( + "inbox_notifications", + i18n.UserConfigInboxNotificationsTitle, + i18n.UserConfigInboxNotificationsLabel, + i18n.UserConfigInboxNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "all_new_questions", + i18n.UserConfigAllNewQuestionsNotificationsTitle, + i18n.UserConfigAllNewQuestionsNotificationsLabel, + i18n.UserConfigAllNewQuestionsNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "new_questions_for_following_tags", + i18n.UserConfigNewQuestionsForFollowingTagsTitle, + i18n.UserConfigNewQuestionsForFollowingTagsLabel, + i18n.UserConfigNewQuestionsForFollowingTagsDescription, + )) + return fields +} + +func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(label), + }, + } +} + +func (uc *UserCenter) UserConfigReceiver(userID string, config []byte) error { + log.Debugf("receive user config %s %s", userID, string(config)) + var userConfig UserConfig + err := json.Unmarshal(config, &userConfig) + if err != nil { + return fmt.Errorf("unmarshal user config failed: %w", err) + } + uc.UserConfigCache.SetUserConfig(userID, &userConfig) + return nil +} + +func (uc *UserCenter) getUserConfig(userID string) (config *UserConfig, err error) { + userConfig := plugin.GetPluginUserConfig(userID, uc.Info().SlugName) + if len(userConfig) == 0 { + return nil, nil + } + config = &UserConfig{} + err = json.Unmarshal(userConfig, config) + if err != nil { + return nil, fmt.Errorf("unmarshal user config failed: %w", err) + } + return config, nil +} From b1e443772503752a64d810d3c452b9b6d62e2cbc Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Fri, 11 Oct 2024 01:59:39 +0000 Subject: [PATCH 12/18] Fix readme and integrate notification --- user-center-slack/README.md | 42 ++++++++--- user-center-slack/client.go | 7 +- user-center-slack/config.go | 3 +- user-center-slack/go.mod | 7 -- user-center-slack/go.sum | 6 +- user-center-slack/i18n/en_US.yaml | 5 -- user-center-slack/i18n/translation.go | 24 +++++- user-center-slack/i18n/zh_CN.yaml | 41 +++++------ user-center-slack/notification.go | 102 +++++++++++++------------- user-center-slack/schema.go | 33 +++++++++ user-center-slack/user_config.go | 44 ++++++++++- 11 files changed, 201 insertions(+), 113 deletions(-) diff --git a/user-center-slack/README.md b/user-center-slack/README.md index 753eec9b..a15e8907 100644 --- a/user-center-slack/README.md +++ b/user-center-slack/README.md @@ -1,20 +1,38 @@ -# WeCom User Center +# Slack User Center + ## Feature -- User login via WeCom QRCode + +- User login via slack Account ## Config -> You need to create a WeCom App first, and then get the `Corp ID`, `Agent ID` and `App Secret` from the App. -- `Company ID`: WeCom Corp ID -- `App Secret`: WeCom App Secret -- `App Agent ID`: WeCom App Agent ID +To use this plugin, you need to create [a Slack App](https://api.slack.com/quickstart) first, set the Scope and Redirect URL correctly, and copy the `Client ID`, `Client Secrect`, `Signing Secret` and `Webhook URL`. To activate the Slash Command function, you also need to set the `slash command` in your app. Here are some examples: + +> Scope: chat:write, commands, groups:write, im:write, incoming-webhook, mpim:write, users:read, users:read.email +> +> RedirectURL: https://Your_Site_URL/answer/api/v1/user-center/login/callback +> +> Slash command: +> +> * Command: /ask +> * Requesti URL: https://Your_Site_URL/answer/api/v1/importer/command +> * Usage Hint: [Title][Content\][Tag1,Tag2...\] + + + +- `Client ID`: Slack App Client ID + +- `Client Secret`: Slack App Secret + +- `Signing Secret`: Slack App Signing Secret + +- `Webhook URL`: find in the `Incoming Webhooks` feature, such as `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX` -Note: WeCom restricts the ip address of the callback url, so you need to add the ip address of the server where the project is located to the callback url. -## Preview -![WeCom Config](./docs/wecom-config.png) -![WeCom QRCode](./docs/wecom-qrcode.png) -![WeCom Login](./docs/wecom-login.png) +Note: A Redirect URL must also use HTTPS. You can configure a Redirect URL and scope in the **App Management** page under **OAuth & Permissions**. ## Document -- https://developer.work.weixin.qq.com/document/path/90664 \ No newline at end of file +- https://api.slack.com/quickstart +- https://api.slack.com/authentication/oauth-v2 +- https://api.slack.com/messaging/webhooks +- https://api.slack.com/interactivity/slash-commands \ No newline at end of file diff --git a/user-center-slack/client.go b/user-center-slack/client.go index 3525fd2b..d6447b32 100644 --- a/user-center-slack/client.go +++ b/user-center-slack/client.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" + "github.com/apache/incubator-answer/plugin" "github.com/go-resty/resty/v2" "github.com/segmentfault/pacman/log" ) @@ -18,7 +19,6 @@ type SlackClient struct { RedirectURI string AuthedUserID string - Enabled bool // 用户是否启用的标记 UserInfoMapping map[string]*UserInfo ChannelMapping string } @@ -86,7 +86,7 @@ type Member struct { func (sc *SlackClient) AuthUser(code string) (info *UserInfo, err error) { clientID := sc.ClientID clientSecret := sc.ClientSecret - redirectURI := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", "https://as.0vo.lol") + redirectURI := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) data := url.Values{} data.Set("code", code) @@ -117,12 +117,9 @@ func (sc *SlackClient) AuthUser(code string) (info *UserInfo, err error) { return nil, fmt.Errorf("Slack API error in AuthUser: %s", tokenResp.Error) } - sc.Enabled = true - // sc.AccessToken = tokenResp.AccessToken sc.AccessToken = tokenResp.AccessToken sc.AuthedUserID = tokenResp.AuthedUser.ID - // Get user detailed information return sc.GetUserDetailInfo(sc.AuthedUserID) } diff --git a/user-center-slack/config.go b/user-center-slack/config.go index 561fdf09..897c7a50 100644 --- a/user-center-slack/config.go +++ b/user-center-slack/config.go @@ -41,7 +41,6 @@ func NewSlackClientWithConfig(clientID, clientSecret, redirectURI string) *Slack ClientID: clientID, ClientSecret: clientSecret, RedirectURI: redirectURI, - Enabled: true, } } @@ -84,7 +83,7 @@ func (uc *UserCenter) ConfigFields() []plugin.ConfigField { UIOptions: plugin.ConfigFieldUIOptions{ Text: syncNowLabel, Action: &plugin.UIOptionAction{ - Url: "/answer/admin/api/slack/sync", // 修改为 Slack 的同步 URL + Url: "/answer/admin/api/slack/sync", Method: "get", Loading: &plugin.LoadingAction{ Text: plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing), diff --git a/user-center-slack/go.mod b/user-center-slack/go.mod index f81ef78d..9543b42c 100644 --- a/user-center-slack/go.mod +++ b/user-center-slack/go.mod @@ -9,10 +9,8 @@ require ( github.com/apache/incubator-answer-plugins/util v1.0.2 github.com/gin-gonic/gin v1.10.0 github.com/go-resty/resty/v2 v2.15.1 - github.com/jarcoal/httpmock v1.3.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f - github.com/stretchr/testify v1.9.0 ) require ( @@ -22,7 +20,6 @@ require ( github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -41,7 +38,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect @@ -55,6 +51,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) - -// replace github.com/Anan1225/incubator-answer-plugins/user-center-slack => /root/incubator-answer-plugins/user-center-slack -replace github.com/apache/incubator-answer => ../../incubator-answer diff --git a/user-center-slack/go.sum b/user-center-slack/go.sum index f235a5a7..99544a61 100644 --- a/user-center-slack/go.sum +++ b/user-center-slack/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -46,8 +48,6 @@ github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= -github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -62,8 +62,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/user-center-slack/i18n/en_US.yaml b/user-center-slack/i18n/en_US.yaml index 90ac7efd..90d76451 100644 --- a/user-center-slack/i18n/en_US.yaml +++ b/user-center-slack/i18n/en_US.yaml @@ -49,11 +49,6 @@ plugin: other: Manual Sync Contacts description: other: Last successful sync - authorize_url: - title: - other: Authorize URL - description: - other: Slack authorize URL client_id: title: other: Client ID diff --git a/user-center-slack/i18n/translation.go b/user-center-slack/i18n/translation.go index b2e13eac..188fad21 100644 --- a/user-center-slack/i18n/translation.go +++ b/user-center-slack/i18n/translation.go @@ -42,6 +42,8 @@ const ( ConfigNotificationTitle = "plugin.slack_user_center.backend.config.notification.title" ConfigNotificationDescription = "plugin.slack_user_center.backend.config.notification.description" + UserConfigWebhookURLTitle = "plugin.slack_user_center.backend.user_config.webhook_url.title" + UserConfigInboxNotificationsTitle = "plugin.slack_user_center.backend.user_config.inbox_notifications.title" UserConfigInboxNotificationsLabel = "plugin.slack_user_center.backend.user_config.inbox_notifications.label" UserConfigInboxNotificationsDescription = "plugin.slack_user_center.backend.user_config.inbox_notifications.description" @@ -54,9 +56,25 @@ const ( UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.label" UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.description" - TplUpdateQuestion = "plugin.slack_user_center.backend.tpl.update_question" + UserConfigUpvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.upvoted_answers.title" + UserConfigUpvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.upvoted_answers.label" + UserConfigUpvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.upvoted_answers.description" + + UserConfigDownvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.downvoted_answers.title" + UserConfigDownvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.downvoted_answers.label" + UserConfigDownvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.downvoted_answers.description" + + UserConfigUpdatedQuestionsTitle = "plugin.slack_user_center.backend.user_config.updated_questions.title" + UserConfigUpdatedQuestionsLabel = "plugin.slack_user_center.backend.user_config.updated_questions.label" + UserConfigUpdatedQuestionsDescription = "plugin.slack_user_center.backend.user_config.updated_questions.description" + + UserConfigUpdatedAnswersTitle = "plugin.slack_user_center.backend.user_config.updated_answers.title" + UserConfigUpdatedAnswersLabel = "plugin.slack_user_center.backend.user_config.updated_answers.label" + UserConfigUpdatedAnswersDescription = "plugin.slack_user_center.backend.user_config.updated_answers.description" + + TplUpdatedQuestions = "plugin.slack_user_center.backend.tpl.update_question" TplAnswerTheQuestion = "plugin.slack_user_center.backend.tpl.answer_the_question" - TplUpdateAnswer = "plugin.slack_user_center.backend.tpl.update_answer" + TplUpdatedAnswers = "plugin.slack_user_center.backend.tpl.update_answer" TplAcceptAnswer = "plugin.slack_user_center.backend.tpl.accept_answer" TplCommentQuestion = "plugin.slack_user_center.backend.tpl.comment_question" TplCommentAnswer = "plugin.slack_user_center.backend.tpl.comment_answer" @@ -64,4 +82,6 @@ const ( TplMentionYou = "plugin.slack_user_center.backend.tpl.mention_you" TplInvitedYouToAnswer = "plugin.slack_user_center.backend.tpl.invited_you_to_answer" TplNewQuestion = "plugin.slack_user_center.backend.tpl.new_question" + TplUpvotedAnswer = "plugin.slack_user_center.backend.tpl.upvoted_answer" + TplDownvotedAnswer = "plugin.slack_user_center.backend.tpl.downvoted_answer" ) diff --git a/user-center-slack/i18n/zh_CN.yaml b/user-center-slack/i18n/zh_CN.yaml index 9ff875af..f369ac8b 100644 --- a/user-center-slack/i18n/zh_CN.yaml +++ b/user-center-slack/i18n/zh_CN.yaml @@ -21,14 +21,14 @@ plugin: response: sync_now: success: - other: 联系人同步成功。 + other: 同步成功。 failed: - other: 联系人同步失败。 + other: 同步失败。 info: name: - other: 企业微信 + other: Slack description: - other: 从企业微信获取用户信息并同步到用户中心 + other: 通过Slack进行登录 config: tip: title: @@ -37,7 +37,7 @@ plugin: label: other: 打开自动同步 title: - other: 自动同步联系人 + other: 自动同步 description: other: 每小时自动同步。 sync_now: @@ -46,36 +46,31 @@ plugin: label_for_doing: other: 同步中 title: - other: 手动同步联系人 + other: 手动同步 description: other: 上次成功同步于 - authorize_url: + client_id: title: - other: 授权网址 + other: Client ID description: - other: 企业微信授权网址 - corp_id: + other: Slack client ID + client_secret: title: - other: 企业 ID + other: Client Secret description: - other: 企业微信企业ID - corp_secret: - title: - other: 应用 Secret - description: - other: 企业微信应用程序密钥 - agent_id: - title: - other: 应用 Agent ID - description: - other: 企业微信应用程序代理ID + other: Slack client secret notification: label: other: 打开通知 title: other: 通知 description: - other: 用户将在企业微信上收到通知。 + other: 用户将在Slack上收到通知。 + signing_secret: + title: + other: Signing Secret + description: + other: Slack signing secret user_config: inbox_notifications: title: diff --git a/user-center-slack/notification.go b/user-center-slack/notification.go index 5e754d01..a08b40d7 100644 --- a/user-center-slack/notification.go +++ b/user-center-slack/notification.go @@ -1,15 +1,11 @@ package slack_user_center import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" "strings" slackI18n "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" "github.com/apache/incubator-answer/plugin" + "github.com/go-resty/resty/v2" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) @@ -24,7 +20,7 @@ func (uc *UserCenter) GetNewQuestionSubscribers() (userIDs []string) { return userIDs } -// Notify sends a notification to the user using Slack +// Notify sends a notification to the user func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { log.Debugf("try to send notification %+v", msg) @@ -55,6 +51,25 @@ func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) return } + case plugin.NotificationUpVotedTheAnswer: + if !userConfig.UpvotedAnswers { + log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) + } + case plugin.NotificationDownVotedTheAnswer: + if !userConfig.DownvotedAnswers { + log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) + } + + case plugin.NotificationUpdateQuestion: + if !userConfig.UpdatedQuestions { + log.Debugf("user %s not config the update question", msg.ReceiverUserID) + return + } + case plugin.NotificationUpdateAnswer: + if !userConfig.UpdatedAnswers { + log.Debugf("user %s not config the update answer", msg.ReceiverUserID) + return + } default: if !userConfig.InboxNotifications { log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) @@ -63,6 +78,31 @@ func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { } log.Debugf("user %s config the notification", msg.ReceiverUserID) + + if len(userConfig.WebhookURL) == 0 { + log.Errorf("user %s has no webhook url", msg.ReceiverUserID) + return + } + + notificationMsg := renderNotification(msg) + // no need to send empty message + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + + // Create a Resty Client + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(NewWebhookReq(notificationMsg)). + Post(userConfig.WebhookURL) + + if err != nil { + log.Errorf("send message failed: %v %v", err, resp) + } else { + log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) + } } // renderNotification generates the notification message based on type @@ -70,11 +110,11 @@ func renderNotification(msg plugin.NotificationMessage) string { lang := i18n.Language(msg.ReceiverLang) switch msg.Type { case plugin.NotificationUpdateQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplUpdateQuestion, msg) + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) case plugin.NotificationAnswerTheQuestion: return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) case plugin.NotificationUpdateAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpdateAnswer, msg) + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) case plugin.NotificationAcceptAnswer: return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) case plugin.NotificationCommentQuestion: @@ -90,48 +130,10 @@ func renderNotification(msg plugin.NotificationMessage) string { case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) + case plugin.NotificationUpVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) + case plugin.NotificationDownVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) } return "" } - -// SendMessage sends a message to a Slack user using Slack API -func (sc *SlackClient) SendMessage(userID string, message string) error { - data := url.Values{} - data.Set("channel", userID) // Slack user ID or channel ID - data.Set("text", message) - - req, err := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", strings.NewReader(data.Encode())) - if err != nil { - return err - } - - req.Header.Add("Authorization", "Bearer "+sc.AccessToken) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - var result struct { - OK bool `json:"ok"` - Error string `json:"error"` - } - - if err := json.Unmarshal(body, &result); err != nil { - return err - } - - if !result.OK { - return fmt.Errorf("Slack API error: %s", result.Error) - } - - return nil -} diff --git a/user-center-slack/schema.go b/user-center-slack/schema.go index 3713895f..defbec26 100644 --- a/user-center-slack/schema.go +++ b/user-center-slack/schema.go @@ -66,3 +66,36 @@ type UserInfo struct { Enable bool `json:"true"` Status int `json:"status"` } + +type WebhookReq struct { + Blocks []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + } `json:"blocks"` +} + +func NewWebhookReq(content string) *WebhookReq { + return &WebhookReq{ + Blocks: []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + }{ + { + Type: "section", + Text: struct { + Type string `json:"type"` + Text string `json:"text"` + }{ + Type: "mrkdwn", + Text: content, + }, + }, + }, + } +} diff --git a/user-center-slack/user_config.go b/user-center-slack/user_config.go index 428253f3..e39c3a8b 100644 --- a/user-center-slack/user_config.go +++ b/user-center-slack/user_config.go @@ -30,9 +30,14 @@ import ( ) type UserConfig struct { - InboxNotifications bool `json:"inbox_notifications"` - AllNewQuestions bool `json:"all_new_questions"` - NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` + WebhookURL string `json:"webhook_url"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` + UpvotedAnswers bool `json:"upvoted_answers"` + DownvotedAnswers bool `json:"downvoted_answers"` + UpdatedQuestions bool `json:"updated_questions"` + UpdatedAnswers bool `json:"updated_answers"` } type UserConfigCache struct { @@ -69,6 +74,15 @@ func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { }, }) } + fields = append(fields, plugin.ConfigField{ + Name: "webhook_url", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }) fields = append(fields, createSwitchConfig( "inbox_notifications", i18n.UserConfigInboxNotificationsTitle, @@ -87,6 +101,30 @@ func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { i18n.UserConfigNewQuestionsForFollowingTagsLabel, i18n.UserConfigNewQuestionsForFollowingTagsDescription, )) + fields = append(fields, createSwitchConfig( + "upvoted_answers", + i18n.UserConfigUpvotedAnswersTitle, + i18n.UserConfigUpvotedAnswersLabel, + i18n.UserConfigUpvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "downvoted_answers", + i18n.UserConfigDownvotedAnswersTitle, + i18n.UserConfigDownvotedAnswersLabel, + i18n.UserConfigDownvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_questions", + i18n.UserConfigUpdatedQuestionsTitle, + i18n.UserConfigUpdatedQuestionsLabel, + i18n.UserConfigUpdatedQuestionsDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_answers", + i18n.UserConfigUpdatedAnswersTitle, + i18n.UserConfigUpdatedAnswersLabel, + i18n.UserConfigUpdatedAnswersDescription, + )) return fields } From 81a8ba03489c4fb99409016c7e7bea7785bcf5f2 Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Fri, 11 Oct 2024 02:01:36 +0000 Subject: [PATCH 13/18] Fix i18n --- notification-slack/i18n/zh_CN.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/notification-slack/i18n/zh_CN.yaml b/notification-slack/i18n/zh_CN.yaml index 00ed99b1..23e61b9b 100644 --- a/notification-slack/i18n/zh_CN.yaml +++ b/notification-slack/i18n/zh_CN.yaml @@ -59,6 +59,34 @@ plugin: other: 打开关注标签的新问题通知 description: other: 收到以下标签的新问题通知。 + upvoted_answers: + title: + other: 收到一个点赞 + label: + other: 打开点赞通知 + description: + other: 收到点赞的通知 + downvoted_answers: + title: + other: 收到一个反对 + label: + other: 打开反对通知 + description: + other: 收到反对的通知 + updated_questions: + title: + other: 问题更新 + label: + other: 打开问题更新通知 + description: + other: 收到问题更新的通知 + updated_answers: + title: + other: 回答更新 + label: + other: 打开回答更新通知 + description: + other: 收到回答更新的通知 tpl: update_question: other: "<{{.TriggerUserUrl}}|{{.TriggerUserDisplayName}}> 更新问题 <{{.QuestionUrl}}|{{.QuestionTitle}}>" From 67bb89a890e0b4742153092f9d2b3336d060b17e Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Tue, 15 Oct 2024 04:36:38 +0000 Subject: [PATCH 14/18] Importer logic change --- user-center-slack/cron.go | 4 +- user-center-slack/go.mod | 108 ++++----- user-center-slack/go.sum | 300 ++++++++++++------------- user-center-slack/handler.go | 2 +- user-center-slack/importer.go | 55 ++++- user-center-slack/slack_user_center.go | 2 + 6 files changed, 261 insertions(+), 210 deletions(-) diff --git a/user-center-slack/cron.go b/user-center-slack/cron.go index 33306509..40c13d95 100644 --- a/user-center-slack/cron.go +++ b/user-center-slack/cron.go @@ -28,12 +28,12 @@ import ( func (uc *UserCenter) CronSyncData() { go func() { ticker := time.NewTicker(time.Hour) - defer ticker.Stop() + // defer ticker.Stop() for { select { case <-ticker.C: - log.Infof("UserCenter is syncing Slack user data...") + log.Infof("UserCenter is syncing Slack user data") uc.syncSlackClient() } } diff --git a/user-center-slack/go.mod b/user-center-slack/go.mod index 9543b42c..e5cb4909 100644 --- a/user-center-slack/go.mod +++ b/user-center-slack/go.mod @@ -1,53 +1,55 @@ -module github.com/Anan1225/incubator-answer-plugins/user-center-slack - -go 1.22.7 - -toolchain go1.23.1 - -require ( - github.com/apache/incubator-answer v1.4.0 - github.com/apache/incubator-answer-plugins/util v1.0.2 - github.com/gin-gonic/gin v1.10.0 - github.com/go-resty/resty/v2 v2.15.1 - github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f -) - -require ( - github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/bytedance/sonic v1.12.2 // indirect - github.com/bytedance/sonic/loader v0.2.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.5 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/wire v0.5.0 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.10.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect -) +module github.com/Anan1225/incubator-answer-plugins/user-center-slack + +go 1.22.7 + +toolchain go1.23.1 + +require ( + github.com/apache/incubator-answer v1.4.0 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-resty/resty/v2 v2.15.1 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f +) + +require ( + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +replace github.com/apache/incubator-answer => /root/incubator-answer diff --git a/user-center-slack/go.sum b/user-center-slack/go.sum index 99544a61..388f607c 100644 --- a/user-center-slack/go.sum +++ b/user-center-slack/go.sum @@ -1,151 +1,149 @@ -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= -github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= -github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= -github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= -github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= -github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= -github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.15.1 h1:vuna8FM2EaQ6IYbtjh+Gjh00uu7xEWuuGyTKeIaYkvE= -github.com/go-resty/resty/v2 v2.15.1/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= -github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= -github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= -golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= +github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.15.1 h1:vuna8FM2EaQ6IYbtjh+Gjh00uu7xEWuuGyTKeIaYkvE= +github.com/go-resty/resty/v2 v2.15.1/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/user-center-slack/handler.go b/user-center-slack/handler.go index 133ecee7..aefc1da0 100644 --- a/user-center-slack/handler.go +++ b/user-center-slack/handler.go @@ -141,7 +141,7 @@ func (uc *UserCenter) syncSlackClient() { uc.syncSuccess = true if err := uc.SlackClient.UpdateUserInfo(); err != nil { - log.Errorf("list department error: %s", err) + log.Errorf("list user error: %s", err) uc.syncSuccess = false return } diff --git a/user-center-slack/importer.go b/user-center-slack/importer.go index 6277e8a8..0e3984df 100644 --- a/user-center-slack/importer.go +++ b/user-center-slack/importer.go @@ -2,6 +2,7 @@ package slack_user_center import ( "bytes" + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" @@ -17,6 +18,7 @@ import ( "github.com/apache/incubator-answer/plugin" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" ) func (uc *UserCenter) parseText(text string) (string, string, []string, error) { @@ -69,12 +71,17 @@ func getSlackUserEmail(userID, token string) (string, error) { return "", err } + fmt.Println("===UserResponse===Begin") + fmt.Println(userResponse) + fmt.Println("===UserResponse===End") + if !userResponse.Ok { return "", fmt.Errorf("failed to get user info from Slack") } return userResponse.User.Profile.Email, nil } + func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error { body, err := io.ReadAll(ctx.Request.Body) if err != nil { @@ -106,12 +113,12 @@ func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error { return nil } -func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo *plugin.QuestionImporterInfo, err error) { - questionInfo = &plugin.QuestionImporterInfo{} +func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo plugin.QuestionImporterInfo, err error) { + questionInfo = plugin.QuestionImporterInfo{} err = uc.verifySlackRequest(ctx) if err != nil { - return nil, err + return questionInfo, err } text := ctx.PostForm("text") @@ -134,3 +141,45 @@ func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo *plugin.Questi questionInfo.UserEmail = email return questionInfo, nil } + +func (uc *UserCenter) SlashCommand(ctx *gin.Context) { + body, _ := io.ReadAll(ctx.Request.Body) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + cmd := ctx.PostForm("command") + // FIXME: Change to /ask + if cmd != "/ask2" { + log.Errorf("error: Invalid command") + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Invalid command"}) + return + } + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + err := uc.verifySlackRequest(ctx) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Slack request verification faild"}) + log.Errorf("error: %v", err) + return + } + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + questionInfo, err := uc.GetQuestion(ctx) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + fmt.Println("===Title===Begin") + fmt.Println(questionInfo.Title) + fmt.Println(questionInfo.Content) + fmt.Println(questionInfo.Tags) + fmt.Println(questionInfo.UserEmail) + fmt.Println("===Title===End") + if uc.importerFunc == nil { + log.Errorf("error: importerFunc is not initialized") + return + } + uc.importerFunc.AddQuestion(ctx, questionInfo) + ctx.JSON(http.StatusOK, gin.H{"text": "Question has been added successfully"}) +} + +func (uc *UserCenter) RegisterImporterFunc(ctx context.Context, importerFunc plugin.ImporterFunc) { + uc.importerFunc = importerFunc +} diff --git a/user-center-slack/slack_user_center.go b/user-center-slack/slack_user_center.go index 76b3b2af..e5f4e44b 100644 --- a/user-center-slack/slack_user_center.go +++ b/user-center-slack/slack_user_center.go @@ -58,10 +58,12 @@ type UserCenter struct { syncing bool syncSuccess bool syncTime time.Time + importerFunc plugin.ImporterFunc } func (uc *UserCenter) RegisterUnAuthRouter(r *gin.RouterGroup) { r.GET("/slack/login/url", uc.GetSlackRedirectURL) + r.POST("/slack/slash", uc.SlashCommand) } func (uc *UserCenter) RegisterAuthUserRouter(r *gin.RouterGroup) { From 90d3920be68fa9dc82ab04ec8381e0b5df3c3a2d Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Wed, 16 Oct 2024 01:36:12 +0000 Subject: [PATCH 15/18] With slash command /ask --- notification-slack/config.go | 2 +- notification-slack/go.mod | 4 +--- notification-slack/info.yaml | 2 +- notification-slack/slack_notification.go | 2 +- notification-slack/user_config.go | 2 +- user-center-slack/config.go | 6 +----- user-center-slack/go.mod | 4 +--- user-center-slack/go.sum | 2 ++ user-center-slack/importer.go | 21 +++++++-------------- user-center-slack/info.yaml | 2 +- user-center-slack/notification.go | 2 +- user-center-slack/slack_user_center.go | 2 +- user-center-slack/user_config.go | 2 +- 13 files changed, 20 insertions(+), 33 deletions(-) diff --git a/notification-slack/config.go b/notification-slack/config.go index 8afd16e5..7f230571 100644 --- a/notification-slack/config.go +++ b/notification-slack/config.go @@ -22,7 +22,7 @@ package slack_notification import ( "encoding/json" - "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer-plugins/notification-slack/i18n" "github.com/apache/incubator-answer/plugin" ) diff --git a/notification-slack/go.mod b/notification-slack/go.mod index 3adb597b..894b18b1 100644 --- a/notification-slack/go.mod +++ b/notification-slack/go.mod @@ -1,4 +1,4 @@ -module github.com/Anan1225/incubator-answer-plugins/notification-slack +module github.com/apache/incubator-answer-plugins/notification-slack go 1.19 @@ -46,5 +46,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) - -replace github.com/Anan1225/incubator-answer-plugins/notification-slack => /root/incubator-answer-plugins/notification-slack diff --git a/notification-slack/info.yaml b/notification-slack/info.yaml index 92f4897e..7c034f04 100644 --- a/notification-slack/info.yaml +++ b/notification-slack/info.yaml @@ -19,4 +19,4 @@ slug_name: slack_notification type: notification version: 1.0.2 author: answerdev -link: https://github.com/Anan1225/incubator-answer-plugins/tree/main/notification-slack +link: https://github.com/apache/incubator-answer-plugins/tree/main/notification-slack diff --git a/notification-slack/slack_notification.go b/notification-slack/slack_notification.go index 43664817..7905bc0a 100644 --- a/notification-slack/slack_notification.go +++ b/notification-slack/slack_notification.go @@ -26,7 +26,7 @@ import ( "github.com/apache/incubator-answer-plugins/util" "github.com/go-resty/resty/v2" - slackI18n "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + slackI18n "github.com/apache/incubator-answer-plugins/notification-slack/i18n" "github.com/apache/incubator-answer/plugin" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" diff --git a/notification-slack/user_config.go b/notification-slack/user_config.go index b7f5ee59..44145da4 100644 --- a/notification-slack/user_config.go +++ b/notification-slack/user_config.go @@ -24,7 +24,7 @@ import ( "fmt" "sync" - "github.com/Anan1225/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer-plugins/notification-slack/i18n" "github.com/apache/incubator-answer/plugin" "github.com/segmentfault/pacman/log" ) diff --git a/user-center-slack/config.go b/user-center-slack/config.go index 897c7a50..2e6420f6 100644 --- a/user-center-slack/config.go +++ b/user-center-slack/config.go @@ -23,7 +23,7 @@ import ( "encoding/json" "time" - "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" "github.com/apache/incubator-answer/plugin" ) @@ -149,9 +149,5 @@ func (uc *UserCenter) ConfigReceiver(config []byte) error { uc.Config = c uc.SlackClient = NewSlackClient(c.ClientID, c.ClientSecret) - - if uc.Config.AutoSync { - uc.CronSyncData() - } return nil } diff --git a/user-center-slack/go.mod b/user-center-slack/go.mod index e5cb4909..50786fa4 100644 --- a/user-center-slack/go.mod +++ b/user-center-slack/go.mod @@ -1,4 +1,4 @@ -module github.com/Anan1225/incubator-answer-plugins/user-center-slack +module github.com/apache/incubator-answer-plugins/user-center-slack go 1.22.7 @@ -51,5 +51,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) - -replace github.com/apache/incubator-answer => /root/incubator-answer diff --git a/user-center-slack/go.sum b/user-center-slack/go.sum index 388f607c..335f91e2 100644 --- a/user-center-slack/go.sum +++ b/user-center-slack/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/apache/incubator-answer v1.4.0 h1:W3y4TAQ4sdzgcqntGqNBPe0BdyeW7+l8FWYBDs9g8+Y= +github.com/apache/incubator-answer v1.4.0/go.mod h1:Q4NkQmBd0sV7t3Cd8NBsWh9w8jFRo/2qjzOw9MlRNwk= github.com/apache/incubator-answer-plugins/util v1.0.2 h1:PontocVaiEm+oTj+4aDonwWDZnxywUeHsaTwlQgclfA= github.com/apache/incubator-answer-plugins/util v1.0.2/go.mod h1:KPMSiM4ec4uEl2njaGINYuSl6zVmHdvPB2nHUxVcQDo= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= diff --git a/user-center-slack/importer.go b/user-center-slack/importer.go index 0e3984df..f4debd0b 100644 --- a/user-center-slack/importer.go +++ b/user-center-slack/importer.go @@ -70,11 +70,6 @@ func getSlackUserEmail(userID, token string) (string, error) { if err := json.Unmarshal(body, &userResponse); err != nil { return "", err } - - fmt.Println("===UserResponse===Begin") - fmt.Println(userResponse) - fmt.Println("===UserResponse===End") - if !userResponse.Ok { return "", fmt.Errorf("failed to get user info from Slack") } @@ -146,8 +141,7 @@ func (uc *UserCenter) SlashCommand(ctx *gin.Context) { body, _ := io.ReadAll(ctx.Request.Body) ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) cmd := ctx.PostForm("command") - // FIXME: Change to /ask - if cmd != "/ask2" { + if cmd != "/ask" { log.Errorf("error: Invalid command") ctx.JSON(http.StatusBadRequest, gin.H{"text": "Invalid command"}) return @@ -166,17 +160,16 @@ func (uc *UserCenter) SlashCommand(ctx *gin.Context) { ctx.JSON(200, gin.H{"text": err.Error()}) return } - fmt.Println("===Title===Begin") - fmt.Println(questionInfo.Title) - fmt.Println(questionInfo.Content) - fmt.Println(questionInfo.Tags) - fmt.Println(questionInfo.UserEmail) - fmt.Println("===Title===End") if uc.importerFunc == nil { log.Errorf("error: importerFunc is not initialized") return } - uc.importerFunc.AddQuestion(ctx, questionInfo) + err = uc.importerFunc.AddQuestion(ctx, questionInfo) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Failed to add question"}) + return + } ctx.JSON(http.StatusOK, gin.H{"text": "Question has been added successfully"}) } diff --git a/user-center-slack/info.yaml b/user-center-slack/info.yaml index 8e3db487..21addf03 100644 --- a/user-center-slack/info.yaml +++ b/user-center-slack/info.yaml @@ -19,4 +19,4 @@ slug_name: slack_user_center type: user_center version: 1.0.1 author: answerdev -link: https://github.com/Anan1225/incubator-answer-plugins/tree/main/user-center-slack +link: https://github.com/apache/incubator-answer-plugins/tree/main/user-center-slack diff --git a/user-center-slack/notification.go b/user-center-slack/notification.go index a08b40d7..799ec91f 100644 --- a/user-center-slack/notification.go +++ b/user-center-slack/notification.go @@ -3,7 +3,7 @@ package slack_user_center import ( "strings" - slackI18n "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + slackI18n "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" "github.com/apache/incubator-answer/plugin" "github.com/go-resty/resty/v2" "github.com/segmentfault/pacman/i18n" diff --git a/user-center-slack/slack_user_center.go b/user-center-slack/slack_user_center.go index e5f4e44b..33f3ce8a 100644 --- a/user-center-slack/slack_user_center.go +++ b/user-center-slack/slack_user_center.go @@ -28,7 +28,7 @@ import ( "github.com/apache/incubator-answer-plugins/util" - "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" "github.com/apache/incubator-answer/plugin" "github.com/gin-gonic/gin" "github.com/patrickmn/go-cache" diff --git a/user-center-slack/user_config.go b/user-center-slack/user_config.go index e39c3a8b..c2885f04 100644 --- a/user-center-slack/user_config.go +++ b/user-center-slack/user_config.go @@ -24,7 +24,7 @@ import ( "fmt" "sync" - "github.com/Anan1225/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" "github.com/apache/incubator-answer/plugin" "github.com/segmentfault/pacman/log" ) From 262cf4305578687bf7be6c427988adc0aaecfed6 Mon Sep 17 00:00:00 2001 From: Anan <1449891717@qq.com> Date: Wed, 16 Oct 2024 03:06:27 +0000 Subject: [PATCH 16/18] Fix slack_user_center --- user-center-slack/README.md | 4 ++-- user-center-slack/handler.go | 2 +- user-center-slack/schema.go | 9 +++++++++ user-center-slack/slack_user_center.go | 10 ---------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/user-center-slack/README.md b/user-center-slack/README.md index a15e8907..ec25cf9d 100644 --- a/user-center-slack/README.md +++ b/user-center-slack/README.md @@ -6,7 +6,7 @@ ## Config -To use this plugin, you need to create [a Slack App](https://api.slack.com/quickstart) first, set the Scope and Redirect URL correctly, and copy the `Client ID`, `Client Secrect`, `Signing Secret` and `Webhook URL`. To activate the Slash Command function, you also need to set the `slash command` in your app. Here are some examples: +To use this plugin, you need to create [a Slack App](https://api.slack.com/quickstart) first, set the Scope and Redirect URL correctly, and copy the `Client ID`, `Client Secrect`, `Signing Secret` and `Webhook URL`. To activate the Slash Command function, you also need to set the `slash command` in your app. Here are default settings you can try: > Scope: chat:write, commands, groups:write, im:write, incoming-webhook, mpim:write, users:read, users:read.email > @@ -15,7 +15,7 @@ To use this plugin, you need to create [a Slack App](https://api.slack.com/quick > Slash command: > > * Command: /ask -> * Requesti URL: https://Your_Site_URL/answer/api/v1/importer/command +> * Requesti URL: https://Your_Site_URL/answer/api/v1/slack/slash > * Usage Hint: [Title][Content\][Tag1,Tag2...\] diff --git a/user-center-slack/handler.go b/user-center-slack/handler.go index aefc1da0..0ad674b0 100644 --- a/user-center-slack/handler.go +++ b/user-center-slack/handler.go @@ -57,7 +57,7 @@ func NewRespBodyData(code int, reason string, data interface{}) *RespBody { func (uc *UserCenter) BuildSlackBaseRedirectURL() string { clientID := uc.Config.ClientID log.Debug("Get client ID:", clientID) - scope := "chat:write,commands,groups:write,im:write,incoming-webhook,mpim:write,users:read,users:read.email" // 需要的权限范围 + scope := "chat:write,commands,groups:write,im:write,incoming-webhook,mpim:write,users:read,users:read.email" response_type := "code" redirect_uri := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) diff --git a/user-center-slack/schema.go b/user-center-slack/schema.go index defbec26..7596fa53 100644 --- a/user-center-slack/schema.go +++ b/user-center-slack/schema.go @@ -99,3 +99,12 @@ func NewWebhookReq(content string) *WebhookReq { }, } } + +type SlackUserResponse struct { + Ok bool `json:"ok"` + User struct { + Profile struct { + Email string `json:"email"` + } `json:"profile"` + } `json:"user"` +} diff --git a/user-center-slack/slack_user_center.go b/user-center-slack/slack_user_center.go index 33f3ce8a..688d61db 100644 --- a/user-center-slack/slack_user_center.go +++ b/user-center-slack/slack_user_center.go @@ -40,15 +40,6 @@ var Info embed.FS type Importer struct{} -type SlackUserResponse struct { - Ok bool `json:"ok"` - User struct { - Profile struct { - Email string `json:"email"` - } `json:"profile"` - } `json:"user"` -} - type UserCenter struct { Config *UserCenterConfig SlackClient *SlackClient @@ -192,7 +183,6 @@ func (uc *UserCenter) LoginCallback(ctx *plugin.GinContext) (userInfo *plugin.Us //Get Email if len(info.Profile.Email) == 0 { ctx.Redirect(http.StatusFound, "/user-center/auth-failed") - ctx.Abort() return nil, fmt.Errorf("user email is empty") } From d296971f9f5e846836c2fd96e339a4858d9e51aa Mon Sep 17 00:00:00 2001 From: AnanChen <57036911+Anan1225@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:15:18 -0400 Subject: [PATCH 17/18] Update info.yaml --- user-center-slack/info.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user-center-slack/info.yaml b/user-center-slack/info.yaml index 21addf03..090e1454 100644 --- a/user-center-slack/info.yaml +++ b/user-center-slack/info.yaml @@ -18,5 +18,5 @@ slug_name: slack_user_center type: user_center version: 1.0.1 -author: answerdev +author: AnanChen link: https://github.com/apache/incubator-answer-plugins/tree/main/user-center-slack From 86579a0e70c0ef57786f3fa19b64beaadf883abc Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 16 Oct 2024 11:47:41 +0800 Subject: [PATCH 18/18] chore(plugins): go fmt code add ASF header --- captcha-google-v2/i18n/translation.go | 14 +- captcha-google-v2/recaptcha.go | 4 +- notification-slack/config.go | 106 ++-- notification-slack/i18n/translation.go | 142 ++--- notification-slack/schema.go | 106 ++-- notification-slack/slack_notification.go | 380 ++++++------- notification-slack/user_config.go | 330 ++++++------ .../i18n/translation.go | 13 +- .../renderMarkdownCodehighlight.go | 119 ++--- render-markdown-codehighlight/theme_list.go | 115 ++-- user-center-slack/client.go | 371 +++++++------ user-center-slack/config.go | 306 +++++------ user-center-slack/cron.go | 82 +-- user-center-slack/handler.go | 298 +++++------ user-center-slack/i18n/translation.go | 174 +++--- user-center-slack/importer.go | 375 ++++++------- user-center-slack/notification.go | 297 ++++++----- user-center-slack/schema.go | 220 ++++---- user-center-slack/slack_user_center.go | 502 +++++++++--------- user-center-slack/user_config.go | 330 ++++++------ 20 files changed, 2169 insertions(+), 2115 deletions(-) diff --git a/captcha-google-v2/i18n/translation.go b/captcha-google-v2/i18n/translation.go index 7a84a0c9..a76c6442 100644 --- a/captcha-google-v2/i18n/translation.go +++ b/captcha-google-v2/i18n/translation.go @@ -20,12 +20,12 @@ package i18n const ( - InfoName = "plugin.google_v2_captcha.backend.info.name" - InfoDescription = "plugin.google_v2_captcha.backend.info.description" - ConfigSiteKeyTitle = "plugin.google_v2_captcha.backend.config.site_key.title" - ConfigSiteKeyDescription = "plugin.google_v2_captcha.backend.config.site_key.description" - ConfigSecretKeyTitle = "plugin.google_v2_captcha.backend.config.secret_key.title" - ConfigSecretKeyDescription = "plugin.google_v2_captcha.backend.config.secret_key.description" - ConfigSiteVerifyEndpointTitle = "plugin.google_v2_captcha.backend.config.site_verify_endpoint.title" + InfoName = "plugin.google_v2_captcha.backend.info.name" + InfoDescription = "plugin.google_v2_captcha.backend.info.description" + ConfigSiteKeyTitle = "plugin.google_v2_captcha.backend.config.site_key.title" + ConfigSiteKeyDescription = "plugin.google_v2_captcha.backend.config.site_key.description" + ConfigSecretKeyTitle = "plugin.google_v2_captcha.backend.config.secret_key.title" + ConfigSecretKeyDescription = "plugin.google_v2_captcha.backend.config.secret_key.description" + ConfigSiteVerifyEndpointTitle = "plugin.google_v2_captcha.backend.config.site_verify_endpoint.title" ConfigSiteVerifyEndpointDescription = "plugin.google_v2_captcha.backend.config.site_verify_endpoint.description" ) diff --git a/captcha-google-v2/recaptcha.go b/captcha-google-v2/recaptcha.go index 7083bdc1..b610a819 100644 --- a/captcha-google-v2/recaptcha.go +++ b/captcha-google-v2/recaptcha.go @@ -42,8 +42,8 @@ type Captcha struct { } type CaptchaConfig struct { - SiteKey string `json:"site_key"` - SecretKey string `json:"secret_key"` + SiteKey string `json:"site_key"` + SecretKey string `json:"secret_key"` SiteVerifyEndpoint string `json:"site_verify_endpoint"` } diff --git a/notification-slack/config.go b/notification-slack/config.go index 7f230571..de3a9cd1 100644 --- a/notification-slack/config.go +++ b/notification-slack/config.go @@ -1,53 +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 slack_notification - -import ( - "encoding/json" - - "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" -) - -type NotificationConfig struct { - Notification bool `json:"notification"` -} - -func (n *Notification) ConfigFields() []plugin.ConfigField { - return []plugin.ConfigField{ - { - Name: "notification", - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), - Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), - }, - Value: n.Config.Notification, - }, - } -} - -func (n *Notification) ConfigReceiver(config []byte) error { - c := &NotificationConfig{} - _ = json.Unmarshal(config, c) - n.Config = c - return nil -} +/* + * 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 slack_notification + +import ( + "encoding/json" + + "github.com/apache/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" +) + +type NotificationConfig struct { + Notification bool `json:"notification"` +} + +func (n *Notification) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "notification", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), + Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), + }, + Value: n.Config.Notification, + }, + } +} + +func (n *Notification) ConfigReceiver(config []byte) error { + c := &NotificationConfig{} + _ = json.Unmarshal(config, c) + n.Config = c + return nil +} diff --git a/notification-slack/i18n/translation.go b/notification-slack/i18n/translation.go index 51660f31..6c83c168 100644 --- a/notification-slack/i18n/translation.go +++ b/notification-slack/i18n/translation.go @@ -1,71 +1,71 @@ -/* - * 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 i18n - -const ( - InfoName = "plugin.slack_notification.backend.info.name" - InfoDescription = "plugin.slack_notification.backend.info.description" - ConfigTipTitle = "plugin.slack_notification.backend.config.tip.title" - ConfigNotificationLabel = "plugin.slack_notification.backend.config.notification.label" - ConfigNotificationTitle = "plugin.slack_notification.backend.config.notification.title" - ConfigNotificationDescription = "plugin.slack_notification.backend.config.notification.description" - - UserConfigWebhookURLTitle = "plugin.slack_notification.backend.user_config.webhook_url.title" - UserConfigInboxNotificationsTitle = "plugin.slack_notification.backend.user_config.inbox_notifications.title" - UserConfigInboxNotificationsLabel = "plugin.slack_notification.backend.user_config.inbox_notifications.label" - UserConfigInboxNotificationsDescription = "plugin.slack_notification.backend.user_config.inbox_notifications.description" - - UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_notification.backend.user_config.all_new_questions.title" - UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_notification.backend.user_config.all_new_questions.label" - UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_notification.backend.user_config.all_new_questions.description" - - UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.title" - UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.label" - UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.description" - - UserConfigUpvotedAnswersTitle = "plugin.slack_notification.backend.user_config.upvoted_answers.title" - UserConfigUpvotedAnswersLabel = "plugin.slack_notification.backend.user_config.upvoted_answers.label" - UserConfigUpvotedAnswersDescription = "plugin.slack_notification.backend.user_config.upvoted_answers.description" - - UserConfigDownvotedAnswersTitle = "plugin.slack_notification.backend.user_config.downvoted_answers.title" - UserConfigDownvotedAnswersLabel = "plugin.slack_notification.backend.user_config.downvoted_answers.label" - UserConfigDownvotedAnswersDescription = "plugin.slack_notification.backend.user_config.downvoted_answers.description" - - UserConfigUpdatedQuestionsTitle = "plugin.slack_notification.backend.user_config.updated_questions.title" - UserConfigUpdatedQuestionsLabel = "plugin.slack_notification.backend.user_config.updated_questions.label" - UserConfigUpdatedQuestionsDescription = "plugin.slack_notification.backend.user_config.updated_questions.description" - - UserConfigUpdatedAnswersTitle = "plugin.slack_notification.backend.user_config.updated_answers.title" - UserConfigUpdatedAnswersLabel = "plugin.slack_notification.backend.user_config.updated_answers.label" - UserConfigUpdatedAnswersDescription = "plugin.slack_notification.backend.user_config.updated_answers.description" - - TplUpdatedQuestions = "plugin.slack_notification.backend.tpl.updated_questions" - TplAnswerTheQuestion = "plugin.slack_notification.backend.tpl.answer_the_question" - TplUpdatedAnswers = "plugin.slack_notification.backend.tpl.updated_answers" - TplAcceptAnswer = "plugin.slack_notification.backend.tpl.accept_answer" - TplCommentQuestion = "plugin.slack_notification.backend.tpl.comment_question" - TplCommentAnswer = "plugin.slack_notification.backend.tpl.comment_answer" - TplReplyToYou = "plugin.slack_notification.backend.tpl.reply_to_you" - TplMentionYou = "plugin.slack_notification.backend.tpl.mention_you" - TplInvitedYouToAnswer = "plugin.slack_notification.backend.tpl.invited_you_to_answer" - TplNewQuestion = "plugin.slack_notification.backend.tpl.new_question" - TplUpvotedAnswer = "plugin.slack_notification.backend.tpl.upvoted_answer" - TplDownvotedAnswer = "plugin.slack_notification.backend.tpl.downvoted_answer" -) +/* + * 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 i18n + +const ( + InfoName = "plugin.slack_notification.backend.info.name" + InfoDescription = "plugin.slack_notification.backend.info.description" + ConfigTipTitle = "plugin.slack_notification.backend.config.tip.title" + ConfigNotificationLabel = "plugin.slack_notification.backend.config.notification.label" + ConfigNotificationTitle = "plugin.slack_notification.backend.config.notification.title" + ConfigNotificationDescription = "plugin.slack_notification.backend.config.notification.description" + + UserConfigWebhookURLTitle = "plugin.slack_notification.backend.user_config.webhook_url.title" + UserConfigInboxNotificationsTitle = "plugin.slack_notification.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.slack_notification.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.slack_notification.backend.user_config.inbox_notifications.description" + + UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_notification.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_notification.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_notification.backend.user_config.all_new_questions.description" + + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_notification.backend.user_config.new_questions_for_following_tags.description" + + UserConfigUpvotedAnswersTitle = "plugin.slack_notification.backend.user_config.upvoted_answers.title" + UserConfigUpvotedAnswersLabel = "plugin.slack_notification.backend.user_config.upvoted_answers.label" + UserConfigUpvotedAnswersDescription = "plugin.slack_notification.backend.user_config.upvoted_answers.description" + + UserConfigDownvotedAnswersTitle = "plugin.slack_notification.backend.user_config.downvoted_answers.title" + UserConfigDownvotedAnswersLabel = "plugin.slack_notification.backend.user_config.downvoted_answers.label" + UserConfigDownvotedAnswersDescription = "plugin.slack_notification.backend.user_config.downvoted_answers.description" + + UserConfigUpdatedQuestionsTitle = "plugin.slack_notification.backend.user_config.updated_questions.title" + UserConfigUpdatedQuestionsLabel = "plugin.slack_notification.backend.user_config.updated_questions.label" + UserConfigUpdatedQuestionsDescription = "plugin.slack_notification.backend.user_config.updated_questions.description" + + UserConfigUpdatedAnswersTitle = "plugin.slack_notification.backend.user_config.updated_answers.title" + UserConfigUpdatedAnswersLabel = "plugin.slack_notification.backend.user_config.updated_answers.label" + UserConfigUpdatedAnswersDescription = "plugin.slack_notification.backend.user_config.updated_answers.description" + + TplUpdatedQuestions = "plugin.slack_notification.backend.tpl.updated_questions" + TplAnswerTheQuestion = "plugin.slack_notification.backend.tpl.answer_the_question" + TplUpdatedAnswers = "plugin.slack_notification.backend.tpl.updated_answers" + TplAcceptAnswer = "plugin.slack_notification.backend.tpl.accept_answer" + TplCommentQuestion = "plugin.slack_notification.backend.tpl.comment_question" + TplCommentAnswer = "plugin.slack_notification.backend.tpl.comment_answer" + TplReplyToYou = "plugin.slack_notification.backend.tpl.reply_to_you" + TplMentionYou = "plugin.slack_notification.backend.tpl.mention_you" + TplInvitedYouToAnswer = "plugin.slack_notification.backend.tpl.invited_you_to_answer" + TplNewQuestion = "plugin.slack_notification.backend.tpl.new_question" + TplUpvotedAnswer = "plugin.slack_notification.backend.tpl.upvoted_answer" + TplDownvotedAnswer = "plugin.slack_notification.backend.tpl.downvoted_answer" +) diff --git a/notification-slack/schema.go b/notification-slack/schema.go index d56c83f0..d1c45788 100644 --- a/notification-slack/schema.go +++ b/notification-slack/schema.go @@ -1,53 +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 slack_notification - -type WebhookReq struct { - Blocks []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - } `json:"blocks"` -} - -func NewWebhookReq(content string) *WebhookReq { - return &WebhookReq{ - Blocks: []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - }{ - { - Type: "section", - Text: struct { - Type string `json:"type"` - Text string `json:"text"` - }{ - Type: "mrkdwn", - Text: content, - }, - }, - }, - } -} +/* + * 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 slack_notification + +type WebhookReq struct { + Blocks []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + } `json:"blocks"` +} + +func NewWebhookReq(content string) *WebhookReq { + return &WebhookReq{ + Blocks: []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + }{ + { + Type: "section", + Text: struct { + Type string `json:"type"` + Text string `json:"text"` + }{ + Type: "mrkdwn", + Text: content, + }, + }, + }, + } +} diff --git a/notification-slack/slack_notification.go b/notification-slack/slack_notification.go index 7905bc0a..0cd28e47 100644 --- a/notification-slack/slack_notification.go +++ b/notification-slack/slack_notification.go @@ -1,190 +1,190 @@ -/* - * 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 slack_notification - -import ( - "embed" - "strings" - - "github.com/apache/incubator-answer-plugins/util" - "github.com/go-resty/resty/v2" - - slackI18n "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/segmentfault/pacman/i18n" - "github.com/segmentfault/pacman/log" -) - -//go:embed info.yaml -var Info embed.FS - -type Notification struct { - Config *NotificationConfig - UserConfigCache *UserConfigCache -} - -func init() { - uc := &Notification{ - Config: &NotificationConfig{}, - UserConfigCache: NewUserConfigCache(), - } - plugin.Register(uc) -} - -func (n *Notification) Info() plugin.Info { - info := &util.Info{} - info.GetInfo(Info) - - return plugin.Info{ - Name: plugin.MakeTranslator(slackI18n.InfoName), - SlugName: info.SlugName, - Description: plugin.MakeTranslator(slackI18n.InfoDescription), - Author: info.Author, - Version: info.Version, - Link: info.Link, - } -} - -// GetNewQuestionSubscribers returns the subscribers of the new question notification -func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) { - for userID, conf := range n.UserConfigCache.userConfigMapping { - if conf.AllNewQuestions { - userIDs = append(userIDs, userID) - } - } - return userIDs -} - -// Notify sends a notification to the user -func (n *Notification) Notify(msg plugin.NotificationMessage) { - log.Debugf("try to send notification %+v", msg) - - if !n.Config.Notification { - return - } - - // get user config - userConfig, err := n.getUserConfig(msg.ReceiverUserID) - if err != nil { - log.Errorf("get user config failed: %v", err) - return - } - if userConfig == nil { - log.Debugf("user %s has no config", msg.ReceiverUserID) - return - } - - // check if the notification is enabled - switch msg.Type { - case plugin.NotificationNewQuestion: - if !userConfig.AllNewQuestions { - log.Debugf("user %s not config the new question", msg.ReceiverUserID) - return - } - case plugin.NotificationNewQuestionFollowedTag: - if !userConfig.NewQuestionsForFollowingTags { - log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) - return - } - case plugin.NotificationUpVotedTheAnswer: - if !userConfig.UpvotedAnswers { - log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) - } - case plugin.NotificationDownVotedTheAnswer: - if !userConfig.DownvotedAnswers { - log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) - } - - case plugin.NotificationUpdateQuestion: - if !userConfig.UpdatedQuestions { - log.Debugf("user %s not config the update question", msg.ReceiverUserID) - return - } - case plugin.NotificationUpdateAnswer: - if !userConfig.UpdatedAnswers { - log.Debugf("user %s not config the update answer", msg.ReceiverUserID) - return - } - default: - if !userConfig.InboxNotifications { - log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) - return - } - } - - log.Debugf("user %s config the notification", msg.ReceiverUserID) - - if len(userConfig.WebhookURL) == 0 { - log.Errorf("user %s has no webhook url", msg.ReceiverUserID) - return - } - - notificationMsg := renderNotification(msg) - // no need to send empty message - if len(notificationMsg) == 0 { - log.Debugf("this type of notification will be drop, the type is %s", msg.Type) - return - } - - // Create a Resty Client - client := resty.New() - resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(NewWebhookReq(notificationMsg)). - Post(userConfig.WebhookURL) - - if err != nil { - log.Errorf("send message failed: %v %v", err, resp) - } else { - log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) - } -} - -func renderNotification(msg plugin.NotificationMessage) string { - lang := i18n.Language(msg.ReceiverLang) - switch msg.Type { - case plugin.NotificationUpdateQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) - case plugin.NotificationAnswerTheQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) - case plugin.NotificationUpdateAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) - case plugin.NotificationAcceptAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) - case plugin.NotificationCommentQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) - case plugin.NotificationCommentAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) - case plugin.NotificationReplyToYou: - return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) - case plugin.NotificationMentionYou: - return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) - case plugin.NotificationInvitedYouToAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) - case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: - msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") - return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) - case plugin.NotificationUpVotedTheAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) - case plugin.NotificationDownVotedTheAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) - } - return "" -} +/* + * 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 slack_notification + +import ( + "embed" + "strings" + + "github.com/apache/incubator-answer-plugins/util" + "github.com/go-resty/resty/v2" + + slackI18n "github.com/apache/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Notification struct { + Config *NotificationConfig + UserConfigCache *UserConfigCache +} + +func init() { + uc := &Notification{ + Config: &NotificationConfig{}, + UserConfigCache: NewUserConfigCache(), + } + plugin.Register(uc) +} + +func (n *Notification) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(slackI18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(slackI18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +// GetNewQuestionSubscribers returns the subscribers of the new question notification +func (n *Notification) GetNewQuestionSubscribers() (userIDs []string) { + for userID, conf := range n.UserConfigCache.userConfigMapping { + if conf.AllNewQuestions { + userIDs = append(userIDs, userID) + } + } + return userIDs +} + +// Notify sends a notification to the user +func (n *Notification) Notify(msg plugin.NotificationMessage) { + log.Debugf("try to send notification %+v", msg) + + if !n.Config.Notification { + return + } + + // get user config + userConfig, err := n.getUserConfig(msg.ReceiverUserID) + if err != nil { + log.Errorf("get user config failed: %v", err) + return + } + if userConfig == nil { + log.Debugf("user %s has no config", msg.ReceiverUserID) + return + } + + // check if the notification is enabled + switch msg.Type { + case plugin.NotificationNewQuestion: + if !userConfig.AllNewQuestions { + log.Debugf("user %s not config the new question", msg.ReceiverUserID) + return + } + case plugin.NotificationNewQuestionFollowedTag: + if !userConfig.NewQuestionsForFollowingTags { + log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) + return + } + case plugin.NotificationUpVotedTheAnswer: + if !userConfig.UpvotedAnswers { + log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) + } + case plugin.NotificationDownVotedTheAnswer: + if !userConfig.DownvotedAnswers { + log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) + } + + case plugin.NotificationUpdateQuestion: + if !userConfig.UpdatedQuestions { + log.Debugf("user %s not config the update question", msg.ReceiverUserID) + return + } + case plugin.NotificationUpdateAnswer: + if !userConfig.UpdatedAnswers { + log.Debugf("user %s not config the update answer", msg.ReceiverUserID) + return + } + default: + if !userConfig.InboxNotifications { + log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) + return + } + } + + log.Debugf("user %s config the notification", msg.ReceiverUserID) + + if len(userConfig.WebhookURL) == 0 { + log.Errorf("user %s has no webhook url", msg.ReceiverUserID) + return + } + + notificationMsg := renderNotification(msg) + // no need to send empty message + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + + // Create a Resty Client + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(NewWebhookReq(notificationMsg)). + Post(userConfig.WebhookURL) + + if err != nil { + log.Errorf("send message failed: %v %v", err, resp) + } else { + log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) + } +} + +func renderNotification(msg plugin.NotificationMessage) string { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") + return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) + case plugin.NotificationUpVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) + case plugin.NotificationDownVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) + } + return "" +} diff --git a/notification-slack/user_config.go b/notification-slack/user_config.go index 44145da4..f657b148 100644 --- a/notification-slack/user_config.go +++ b/notification-slack/user_config.go @@ -1,165 +1,165 @@ -/* - * 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 slack_notification - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/apache/incubator-answer-plugins/notification-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/segmentfault/pacman/log" -) - -type UserConfig struct { - WebhookURL string `json:"webhook_url"` - InboxNotifications bool `json:"inbox_notifications"` - AllNewQuestions bool `json:"all_new_questions"` - NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` - UpvotedAnswers bool `json:"upvoted_answers"` - DownvotedAnswers bool `json:"downvoted_answers"` - UpdatedQuestions bool `json:"updated_questions"` - UpdatedAnswers bool `json:"updated_answers"` -} - -type UserConfigCache struct { - // key: userID value: user config - userConfigMapping map[string]*UserConfig - sync.Mutex -} - -func NewUserConfigCache() *UserConfigCache { - ucc := &UserConfigCache{ - userConfigMapping: make(map[string]*UserConfig), - } - return ucc -} - -func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { - ucc.Lock() - defer ucc.Unlock() - ucc.userConfigMapping[userID] = config -} - -func (n *Notification) UserConfigFields() []plugin.ConfigField { - fields := make([]plugin.ConfigField, 0) - // Show tip for user, if the notification service is disabled - if !n.Config.Notification { - fields = append(fields, plugin.ConfigField{ - Name: "tip", - Type: plugin.ConfigTypeLegend, - Title: plugin.MakeTranslator(i18n.ConfigTipTitle), - Description: plugin.Translator{}, - UIOptions: plugin.ConfigFieldUIOptions{ - ClassName: "mb-3", - FieldClassName: "mb-0 text-danger", - }, - }) - } - fields = append(fields, plugin.ConfigField{ - Name: "webhook_url", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - }) - fields = append(fields, createSwitchConfig( - "inbox_notifications", - i18n.UserConfigInboxNotificationsTitle, - i18n.UserConfigInboxNotificationsLabel, - i18n.UserConfigInboxNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "all_new_questions", - i18n.UserConfigAllNewQuestionsNotificationsTitle, - i18n.UserConfigAllNewQuestionsNotificationsLabel, - i18n.UserConfigAllNewQuestionsNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "new_questions_for_following_tags", - i18n.UserConfigNewQuestionsForFollowingTagsTitle, - i18n.UserConfigNewQuestionsForFollowingTagsLabel, - i18n.UserConfigNewQuestionsForFollowingTagsDescription, - )) - fields = append(fields, createSwitchConfig( - "upvoted_answers", - i18n.UserConfigUpvotedAnswersTitle, - i18n.UserConfigUpvotedAnswersLabel, - i18n.UserConfigUpvotedAnswersDescription, - )) - fields = append(fields, createSwitchConfig( - "downvoted_answers", - i18n.UserConfigDownvotedAnswersTitle, - i18n.UserConfigDownvotedAnswersLabel, - i18n.UserConfigDownvotedAnswersDescription, - )) - fields = append(fields, createSwitchConfig( - "updated_questions", - i18n.UserConfigUpdatedQuestionsTitle, - i18n.UserConfigUpdatedQuestionsLabel, - i18n.UserConfigUpdatedQuestionsDescription, - )) - fields = append(fields, createSwitchConfig( - "updated_answers", - i18n.UserConfigUpdatedAnswersTitle, - i18n.UserConfigUpdatedAnswersLabel, - i18n.UserConfigUpdatedAnswersDescription, - )) - return fields -} - -func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { - return plugin.ConfigField{ - Name: name, - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(title), - Description: plugin.MakeTranslator(desc), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(label), - }, - } -} - -func (n *Notification) UserConfigReceiver(userID string, config []byte) error { - log.Debugf("receive user config %s %s", userID, string(config)) - var userConfig UserConfig - err := json.Unmarshal(config, &userConfig) - if err != nil { - return fmt.Errorf("unmarshal user config failed: %w", err) - } - n.UserConfigCache.SetUserConfig(userID, &userConfig) - return nil -} - -func (n *Notification) getUserConfig(userID string) (config *UserConfig, err error) { - userConfig := plugin.GetPluginUserConfig(userID, n.Info().SlugName) - if len(userConfig) == 0 { - return nil, nil - } - config = &UserConfig{} - err = json.Unmarshal(userConfig, config) - if err != nil { - return nil, fmt.Errorf("unmarshal user config failed: %w", err) - } - return config, nil -} +/* + * 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 slack_notification + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/apache/incubator-answer-plugins/notification-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + WebhookURL string `json:"webhook_url"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` + UpvotedAnswers bool `json:"upvoted_answers"` + DownvotedAnswers bool `json:"downvoted_answers"` + UpdatedQuestions bool `json:"updated_questions"` + UpdatedAnswers bool `json:"updated_answers"` +} + +type UserConfigCache struct { + // key: userID value: user config + userConfigMapping map[string]*UserConfig + sync.Mutex +} + +func NewUserConfigCache() *UserConfigCache { + ucc := &UserConfigCache{ + userConfigMapping: make(map[string]*UserConfig), + } + return ucc +} + +func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { + ucc.Lock() + defer ucc.Unlock() + ucc.userConfigMapping[userID] = config +} + +func (n *Notification) UserConfigFields() []plugin.ConfigField { + fields := make([]plugin.ConfigField, 0) + // Show tip for user, if the notification service is disabled + if !n.Config.Notification { + fields = append(fields, plugin.ConfigField{ + Name: "tip", + Type: plugin.ConfigTypeLegend, + Title: plugin.MakeTranslator(i18n.ConfigTipTitle), + Description: plugin.Translator{}, + UIOptions: plugin.ConfigFieldUIOptions{ + ClassName: "mb-3", + FieldClassName: "mb-0 text-danger", + }, + }) + } + fields = append(fields, plugin.ConfigField{ + Name: "webhook_url", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }) + fields = append(fields, createSwitchConfig( + "inbox_notifications", + i18n.UserConfigInboxNotificationsTitle, + i18n.UserConfigInboxNotificationsLabel, + i18n.UserConfigInboxNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "all_new_questions", + i18n.UserConfigAllNewQuestionsNotificationsTitle, + i18n.UserConfigAllNewQuestionsNotificationsLabel, + i18n.UserConfigAllNewQuestionsNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "new_questions_for_following_tags", + i18n.UserConfigNewQuestionsForFollowingTagsTitle, + i18n.UserConfigNewQuestionsForFollowingTagsLabel, + i18n.UserConfigNewQuestionsForFollowingTagsDescription, + )) + fields = append(fields, createSwitchConfig( + "upvoted_answers", + i18n.UserConfigUpvotedAnswersTitle, + i18n.UserConfigUpvotedAnswersLabel, + i18n.UserConfigUpvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "downvoted_answers", + i18n.UserConfigDownvotedAnswersTitle, + i18n.UserConfigDownvotedAnswersLabel, + i18n.UserConfigDownvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_questions", + i18n.UserConfigUpdatedQuestionsTitle, + i18n.UserConfigUpdatedQuestionsLabel, + i18n.UserConfigUpdatedQuestionsDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_answers", + i18n.UserConfigUpdatedAnswersTitle, + i18n.UserConfigUpdatedAnswersLabel, + i18n.UserConfigUpdatedAnswersDescription, + )) + return fields +} + +func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(label), + }, + } +} + +func (n *Notification) UserConfigReceiver(userID string, config []byte) error { + log.Debugf("receive user config %s %s", userID, string(config)) + var userConfig UserConfig + err := json.Unmarshal(config, &userConfig) + if err != nil { + return fmt.Errorf("unmarshal user config failed: %w", err) + } + n.UserConfigCache.SetUserConfig(userID, &userConfig) + return nil +} + +func (n *Notification) getUserConfig(userID string) (config *UserConfig, err error) { + userConfig := plugin.GetPluginUserConfig(userID, n.Info().SlugName) + if len(userConfig) == 0 { + return nil, nil + } + config = &UserConfig{} + err = json.Unmarshal(userConfig, config) + if err != nil { + return nil, fmt.Errorf("unmarshal user config failed: %w", err) + } + return config, nil +} diff --git a/render-markdown-codehighlight/i18n/translation.go b/render-markdown-codehighlight/i18n/translation.go index ed3059d5..d9a08b54 100644 --- a/render-markdown-codehighlight/i18n/translation.go +++ b/render-markdown-codehighlight/i18n/translation.go @@ -17,11 +17,10 @@ * under the License. */ - package i18n +package i18n - const ( - InfoName = "plugin.render_markdown_codehighlight.backend.info.name" - InfoDescription = "plugin.render_markdown_codehighlight.backend.info.description" - ConfigCssFilteringTitle = "plugin.render_markdown_codehighlight.backend.config.css_filtering.title" - ) - +const ( + InfoName = "plugin.render_markdown_codehighlight.backend.info.name" + InfoDescription = "plugin.render_markdown_codehighlight.backend.info.description" + ConfigCssFilteringTitle = "plugin.render_markdown_codehighlight.backend.config.css_filtering.title" +) diff --git a/render-markdown-codehighlight/renderMarkdownCodehighlight.go b/render-markdown-codehighlight/renderMarkdownCodehighlight.go index 380e0dd4..e97388bd 100644 --- a/render-markdown-codehighlight/renderMarkdownCodehighlight.go +++ b/render-markdown-codehighlight/renderMarkdownCodehighlight.go @@ -17,51 +17,51 @@ * under the License. */ - package render_markdown_codehighlight +package render_markdown_codehighlight - import ( - "embed" - "encoding/json" - "log" - "github.com/gin-gonic/gin" - "strings" - "github.com/apache/incubator-answer-plugins/render-markdown-codehighlight/i18n" - "github.com/apache/incubator-answer-plugins/util" - "github.com/apache/incubator-answer/plugin" - ) - - //go:embed info.yaml - var Info embed.FS - - type Render struct { - Config *RenderConfig - } - - type RenderConfig struct { - SelectTheme string `json:"select_theme"` - } - - func init() { - plugin.Register(&Render{ - Config: &RenderConfig{}, - }) - } - - func (r *Render) Info() plugin.Info { - info := &util.Info{} - info.GetInfo(Info) - - return plugin.Info{ - Name: plugin.MakeTranslator(i18n.InfoName), - SlugName: info.SlugName, - Description: plugin.MakeTranslator(i18n.InfoDescription), - Author: info.Author, - Version: info.Version, - Link: info.Link, - } - } +import ( + "embed" + "encoding/json" + "github.com/apache/incubator-answer-plugins/render-markdown-codehighlight/i18n" + "github.com/apache/incubator-answer-plugins/util" + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "log" + "strings" +) - func (r *Render) ConfigFields() []plugin.ConfigField { +//go:embed info.yaml +var Info embed.FS + +type Render struct { + Config *RenderConfig +} + +type RenderConfig struct { + SelectTheme string `json:"select_theme"` +} + +func init() { + plugin.Register(&Render{ + Config: &RenderConfig{}, + }) +} + +func (r *Render) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (r *Render) ConfigFields() []plugin.ConfigField { themeOptions := make([]plugin.ConfigFieldOption, len(ThemeList)) for i, theme := range ThemeList { @@ -87,20 +87,19 @@ }, } } - - func (r *Render) ConfigReceiver(config []byte) error { - c := &RenderConfig{} - _ = json.Unmarshal(config, c) - r.Config = c - log.Println("Received theme:", r.Config.SelectTheme) - return nil - } - - func (r *Render) GetRenderConfig(ctx *gin.Context) (renderConfig *plugin.RenderConfig) { - log.Println("Current theme:", r.Config.SelectTheme) - renderConfig = &plugin.RenderConfig{ - SelectTheme: r.Config.SelectTheme, - } - return - } - + +func (r *Render) ConfigReceiver(config []byte) error { + c := &RenderConfig{} + _ = json.Unmarshal(config, c) + r.Config = c + log.Println("Received theme:", r.Config.SelectTheme) + return nil +} + +func (r *Render) GetRenderConfig(ctx *gin.Context) (renderConfig *plugin.RenderConfig) { + log.Println("Current theme:", r.Config.SelectTheme) + renderConfig = &plugin.RenderConfig{ + SelectTheme: r.Config.SelectTheme, + } + return +} diff --git a/render-markdown-codehighlight/theme_list.go b/render-markdown-codehighlight/theme_list.go index 6051b673..dc16fa05 100644 --- a/render-markdown-codehighlight/theme_list.go +++ b/render-markdown-codehighlight/theme_list.go @@ -17,65 +17,64 @@ * under the License. */ - package render_markdown_codehighlight var ThemeList = []string{ -"1c-light", - "a11y-all", - "agate-dark", - "an-dark", - "androidstudio-dark", - "arduino-light", - "arta-dark", - "ascetic-light", - "atom-all", - "brown-light", - "codepen-dark", - "color-light", - "dark-dark", - "default-light", - "devibeans-dark", - "docco-light", - "far-dark", - "felipec-dark", - "foundation-light", - "github-all", - "gml-dark", - "googlecode-light", - "gradient-all", - "grayscale-light", - "hybrid-dark", - "idea-light", - "intellij-light", - "ir-dark", - "isbl-all", - "kimbie-all", - "lightfair-light", - "lioshi-dark", - "magula-light", - "mono-light", - "monokai-dark", - "night-dark", - "nnfx-all", - "nord-dark", - "obsidian-dark", - "panda-all", - "paraiso-all", - "pojoaque-light", - "purebasic-light", - "qtcreator-all", - "rainbow-dark", - "routeros-light", - "school-light", - "shades-dark", - "srcery-dark", - "stackoverflow-all", - "sunburst-dark", - "tokyo-all", - "tomorrow-dark", - "vs-light", - "vs2015-dark", - "xcode-light", - "xt256-dark", + "1c-light", + "a11y-all", + "agate-dark", + "an-dark", + "androidstudio-dark", + "arduino-light", + "arta-dark", + "ascetic-light", + "atom-all", + "brown-light", + "codepen-dark", + "color-light", + "dark-dark", + "default-light", + "devibeans-dark", + "docco-light", + "far-dark", + "felipec-dark", + "foundation-light", + "github-all", + "gml-dark", + "googlecode-light", + "gradient-all", + "grayscale-light", + "hybrid-dark", + "idea-light", + "intellij-light", + "ir-dark", + "isbl-all", + "kimbie-all", + "lightfair-light", + "lioshi-dark", + "magula-light", + "mono-light", + "monokai-dark", + "night-dark", + "nnfx-all", + "nord-dark", + "obsidian-dark", + "panda-all", + "paraiso-all", + "pojoaque-light", + "purebasic-light", + "qtcreator-all", + "rainbow-dark", + "routeros-light", + "school-light", + "shades-dark", + "srcery-dark", + "stackoverflow-all", + "sunburst-dark", + "tokyo-all", + "tomorrow-dark", + "vs-light", + "vs2015-dark", + "xcode-light", + "xt256-dark", } diff --git a/user-center-slack/client.go b/user-center-slack/client.go index d6447b32..4ff54c3d 100644 --- a/user-center-slack/client.go +++ b/user-center-slack/client.go @@ -1,176 +1,195 @@ -package slack_user_center - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/apache/incubator-answer/plugin" - "github.com/go-resty/resty/v2" - "github.com/segmentfault/pacman/log" -) - -type SlackClient struct { - AccessToken string - ClientID string - ClientSecret string - RedirectURI string - AuthedUserID string - - UserInfoMapping map[string]*UserInfo - ChannelMapping string -} - -func NewSlackClient(clientID, clientSecret string) *SlackClient { - return &SlackClient{ - ClientID: clientID, - ClientSecret: clientSecret, - } -} - -// OAuthV2ResponseTeam -type OAuthV2ResponseTeam struct { - ID string `json:"id"` - Name string `json:"name"` -} - -// OAuthResponseIncomingWebhook -type OAuthResponseIncomingWebhook struct { - URL string `json:"url"` - Channel string `json:"channel"` - ChannelID string `json:"channel_id,omitempty"` - ConfigurationURL string `json:"configuration_url"` -} - -// OAuthV2ResponseEnterprise -type OAuthV2ResponseEnterprise struct { - ID string `json:"id"` - Name string `json:"name"` -} - -// OAuthV2ResponseAuthedUser -type OAuthV2ResponseAuthedUser struct { - ID string `json:"id"` - Scope string `json:"scope"` - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` -} - -type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - BotUserID string `json:"bot_user_id"` - AppID string `json:"app_id"` - Team OAuthV2ResponseTeam `json:"team"` - IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` - Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` - IsEnterpriseInstall bool `json:"is_enterprise_install"` - AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - Error string `json:"error,omitempty"` -} - -type Member struct { - ID string `json:"id"` - Name string `json:"name"` - TeamID string `json:"team_id"` -} - -// ExchangeCodeForUser through OAuthToken -func (sc *SlackClient) AuthUser(code string) (info *UserInfo, err error) { - clientID := sc.ClientID - clientSecret := sc.ClientSecret - redirectURI := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) - - data := url.Values{} - data.Set("code", code) - data.Set("client_id", clientID) - data.Set("client_secret", clientSecret) - data.Set("redirect_uri", redirectURI) - - resp, err := http.PostForm("https://slack.com/api/oauth.v2.access", data) - if err != nil { - log.Errorf("Failed to exchange code for token: %v", err) - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("Failed to read response body: %v", err) - } - - var tokenResp TokenResponse - err = json.Unmarshal([]byte(body), &tokenResp) - if err != nil { - fmt.Println("Error parsing response:", err) - return nil, err - } - - if tokenResp.Error != "" { - return nil, fmt.Errorf("Slack API error in AuthUser: %s", tokenResp.Error) - } - - sc.AccessToken = tokenResp.AccessToken - sc.AuthedUserID = tokenResp.AuthedUser.ID - - return sc.GetUserDetailInfo(sc.AuthedUserID) -} - -func (sc *SlackClient) GetUserDetailInfo(userid string) (info *UserInfo, err error) { - getUserInfoResp, err := resty.New().R(). - SetHeader("Authorization", fmt.Sprintf("Bearer %s", sc.AccessToken)). - SetHeader("Accept", "application/json"). - Get("https://slack.com/api/users.info?user=" + userid) - if err != nil { - log.Errorf("Failed to get user info: %v", err) - return nil, err - } - - var authUserResp *AuthUserResp - err = json.Unmarshal([]byte(getUserInfoResp.String()), &authUserResp) - if err != nil { - log.Errorf("Error unmarshaling user info: %v", err) - return nil, err - } - if !authUserResp.Ok { - log.Errorf("Failed to get valid user info, Slack API error: %s", authUserResp.Errmsg) - return nil, fmt.Errorf("Get user info failed: %s", authUserResp.Errmsg) - } - log.Debugf("Get user info for UserID: %s", userid) - - if authUserResp.User == nil { - log.Errorf("No user data available in the response") - return nil, fmt.Errorf("No user data available in the response") - } - - authUserResp.User.IsAvailable = true - authUserResp.User.Status = 1 - - // Directly returning the user data parsed from the response - return authUserResp.User, nil -} - -func (sc *SlackClient) UpdateUserInfo() (err error) { - log.Debug("Try to update slack client") - - userInfo, err := sc.GetUserDetailInfo(sc.AuthedUserID) - if err != nil { - log.Errorf("Failed to update user info: %v", err) - return err - } - - if sc.UserInfoMapping == nil { - sc.UserInfoMapping = make(map[string]*UserInfo) - } - sc.UserInfoMapping[sc.AuthedUserID] = userInfo - log.Infof("Updated user info for UserID: %s", sc.AuthedUserID) - - return nil -} +/* + * 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 slack_user_center + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/apache/incubator-answer/plugin" + "github.com/go-resty/resty/v2" + "github.com/segmentfault/pacman/log" +) + +type SlackClient struct { + AccessToken string + ClientID string + ClientSecret string + RedirectURI string + AuthedUserID string + + UserInfoMapping map[string]*UserInfo + ChannelMapping string +} + +func NewSlackClient(clientID, clientSecret string) *SlackClient { + return &SlackClient{ + ClientID: clientID, + ClientSecret: clientSecret, + } +} + +// OAuthV2ResponseTeam +type OAuthV2ResponseTeam struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OAuthResponseIncomingWebhook +type OAuthResponseIncomingWebhook struct { + URL string `json:"url"` + Channel string `json:"channel"` + ChannelID string `json:"channel_id,omitempty"` + ConfigurationURL string `json:"configuration_url"` +} + +// OAuthV2ResponseEnterprise +type OAuthV2ResponseEnterprise struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OAuthV2ResponseAuthedUser +type OAuthV2ResponseAuthedUser struct { + ID string `json:"id"` + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + BotUserID string `json:"bot_user_id"` + AppID string `json:"app_id"` + Team OAuthV2ResponseTeam `json:"team"` + IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` + Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` + AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + Error string `json:"error,omitempty"` +} + +type Member struct { + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +// ExchangeCodeForUser through OAuthToken +func (sc *SlackClient) AuthUser(code string) (info *UserInfo, err error) { + clientID := sc.ClientID + clientSecret := sc.ClientSecret + redirectURI := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) + + data := url.Values{} + data.Set("code", code) + data.Set("client_id", clientID) + data.Set("client_secret", clientSecret) + data.Set("redirect_uri", redirectURI) + + resp, err := http.PostForm("https://slack.com/api/oauth.v2.access", data) + if err != nil { + log.Errorf("Failed to exchange code for token: %v", err) + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Failed to read response body: %v", err) + } + + var tokenResp TokenResponse + err = json.Unmarshal([]byte(body), &tokenResp) + if err != nil { + fmt.Println("Error parsing response:", err) + return nil, err + } + + if tokenResp.Error != "" { + return nil, fmt.Errorf("Slack API error in AuthUser: %s", tokenResp.Error) + } + + sc.AccessToken = tokenResp.AccessToken + sc.AuthedUserID = tokenResp.AuthedUser.ID + + return sc.GetUserDetailInfo(sc.AuthedUserID) +} + +func (sc *SlackClient) GetUserDetailInfo(userid string) (info *UserInfo, err error) { + getUserInfoResp, err := resty.New().R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", sc.AccessToken)). + SetHeader("Accept", "application/json"). + Get("https://slack.com/api/users.info?user=" + userid) + if err != nil { + log.Errorf("Failed to get user info: %v", err) + return nil, err + } + + var authUserResp *AuthUserResp + err = json.Unmarshal([]byte(getUserInfoResp.String()), &authUserResp) + if err != nil { + log.Errorf("Error unmarshaling user info: %v", err) + return nil, err + } + if !authUserResp.Ok { + log.Errorf("Failed to get valid user info, Slack API error: %s", authUserResp.Errmsg) + return nil, fmt.Errorf("Get user info failed: %s", authUserResp.Errmsg) + } + log.Debugf("Get user info for UserID: %s", userid) + + if authUserResp.User == nil { + log.Errorf("No user data available in the response") + return nil, fmt.Errorf("No user data available in the response") + } + + authUserResp.User.IsAvailable = true + authUserResp.User.Status = 1 + + // Directly returning the user data parsed from the response + return authUserResp.User, nil +} + +func (sc *SlackClient) UpdateUserInfo() (err error) { + log.Debug("Try to update slack client") + + userInfo, err := sc.GetUserDetailInfo(sc.AuthedUserID) + if err != nil { + log.Errorf("Failed to update user info: %v", err) + return err + } + + if sc.UserInfoMapping == nil { + sc.UserInfoMapping = make(map[string]*UserInfo) + } + sc.UserInfoMapping[sc.AuthedUserID] = userInfo + log.Infof("Updated user info for UserID: %s", sc.AuthedUserID) + + return nil +} diff --git a/user-center-slack/config.go b/user-center-slack/config.go index 2e6420f6..f1be5e67 100644 --- a/user-center-slack/config.go +++ b/user-center-slack/config.go @@ -1,153 +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 slack_user_center - -import ( - "encoding/json" - "time" - - "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" - "github.com/apache/incubator-answer/plugin" -) - -type UserCenterConfig struct { - ClientID string `json:"client_id"` // Slack Client ID - ClientSecret string `json:"client_secret"` // Slack Client Secret - RedirectURI string `json:"redirect_uri"` // OAuth Redirect URI - SigningSecret string `json:"signing_secret"` // Slack Signing Secret - AutoSync bool `json:"auto_sync"` // Auto sync - Notification bool `json:"notification"` // Notification -} - -func NewSlackClientWithConfig(clientID, clientSecret, redirectURI string) *SlackClient { - return &SlackClient{ - ClientID: clientID, - ClientSecret: clientSecret, - RedirectURI: redirectURI, - } -} - -func (uc *UserCenter) ConfigFields() []plugin.ConfigField { - syncState := plugin.LoadingActionStateNone - lastSuccessfulSyncAt := "None" - if !uc.syncTime.IsZero() { - syncState = plugin.LoadingActionStateComplete - lastSuccessfulSyncAt = uc.syncTime.In(time.FixedZone("GMT", 8*3600)).Format("2006-01-02 15:04:05") - } - t := func(ctx *plugin.GinContext) string { - return plugin.Translate(ctx, i18n.ConfigSyncNowDescription) + ": " + lastSuccessfulSyncAt - } - syncNowDesc := plugin.Translator{Fn: t} - - syncNowLabel := plugin.MakeTranslator(i18n.ConfigSyncNowLabel) - - if uc.syncing { - syncNowLabel = plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing) - syncState = plugin.LoadingActionStatePending - } - - return []plugin.ConfigField{ - { - Name: "auto_sync", - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(i18n.ConfigAutoSyncTitle), - Description: plugin.MakeTranslator(i18n.ConfigAutoSyncDescription), - Required: false, - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(i18n.ConfigAutoSyncLabel), - }, - Value: uc.Config.AutoSync, - }, - { - Name: "sync_now", - Type: plugin.ConfigTypeButton, - Title: plugin.MakeTranslator(i18n.ConfigSyncNowTitle), - Description: syncNowDesc, - UIOptions: plugin.ConfigFieldUIOptions{ - Text: syncNowLabel, - Action: &plugin.UIOptionAction{ - Url: "/answer/admin/api/slack/sync", - Method: "get", - Loading: &plugin.LoadingAction{ - Text: plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing), - State: syncState, - }, - OnComplete: &plugin.OnCompleteAction{ - ToastReturnMessage: true, - RefreshFormConfig: true, - }, - }, - Variant: "outline-secondary", - }, - }, - { - Name: "client_id", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.ConfigClientIDTitle), // Slack Client ID - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - Value: uc.Config.ClientID, - }, - { - Name: "client_secret", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.ConfigClientSecretTitle), // Slack Client Secret - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypePassword, - }, - Value: uc.Config.ClientSecret, - }, - { - Name: "signing_secret", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.ConfigSigningSecretTitle), // Slack Redirect URI - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - Value: uc.Config.SigningSecret, - }, - { - Name: "notification", - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), - Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), - }, - Value: uc.Config.Notification, - }, - } -} - -func (uc *UserCenter) ConfigReceiver(config []byte) error { - c := &UserCenterConfig{} - err := json.Unmarshal(config, c) - if err != nil { - return err - } - uc.Config = c - - uc.SlackClient = NewSlackClient(c.ClientID, c.ClientSecret) - return nil -} +/* + * 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 slack_user_center + +import ( + "encoding/json" + "time" + + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" +) + +type UserCenterConfig struct { + ClientID string `json:"client_id"` // Slack Client ID + ClientSecret string `json:"client_secret"` // Slack Client Secret + RedirectURI string `json:"redirect_uri"` // OAuth Redirect URI + SigningSecret string `json:"signing_secret"` // Slack Signing Secret + AutoSync bool `json:"auto_sync"` // Auto sync + Notification bool `json:"notification"` // Notification +} + +func NewSlackClientWithConfig(clientID, clientSecret, redirectURI string) *SlackClient { + return &SlackClient{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURI: redirectURI, + } +} + +func (uc *UserCenter) ConfigFields() []plugin.ConfigField { + syncState := plugin.LoadingActionStateNone + lastSuccessfulSyncAt := "None" + if !uc.syncTime.IsZero() { + syncState = plugin.LoadingActionStateComplete + lastSuccessfulSyncAt = uc.syncTime.In(time.FixedZone("GMT", 8*3600)).Format("2006-01-02 15:04:05") + } + t := func(ctx *plugin.GinContext) string { + return plugin.Translate(ctx, i18n.ConfigSyncNowDescription) + ": " + lastSuccessfulSyncAt + } + syncNowDesc := plugin.Translator{Fn: t} + + syncNowLabel := plugin.MakeTranslator(i18n.ConfigSyncNowLabel) + + if uc.syncing { + syncNowLabel = plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing) + syncState = plugin.LoadingActionStatePending + } + + return []plugin.ConfigField{ + { + Name: "auto_sync", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigAutoSyncTitle), + Description: plugin.MakeTranslator(i18n.ConfigAutoSyncDescription), + Required: false, + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigAutoSyncLabel), + }, + Value: uc.Config.AutoSync, + }, + { + Name: "sync_now", + Type: plugin.ConfigTypeButton, + Title: plugin.MakeTranslator(i18n.ConfigSyncNowTitle), + Description: syncNowDesc, + UIOptions: plugin.ConfigFieldUIOptions{ + Text: syncNowLabel, + Action: &plugin.UIOptionAction{ + Url: "/answer/admin/api/slack/sync", + Method: "get", + Loading: &plugin.LoadingAction{ + Text: plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing), + State: syncState, + }, + OnComplete: &plugin.OnCompleteAction{ + ToastReturnMessage: true, + RefreshFormConfig: true, + }, + }, + Variant: "outline-secondary", + }, + }, + { + Name: "client_id", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigClientIDTitle), // Slack Client ID + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: uc.Config.ClientID, + }, + { + Name: "client_secret", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigClientSecretTitle), // Slack Client Secret + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + }, + Value: uc.Config.ClientSecret, + }, + { + Name: "signing_secret", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigSigningSecretTitle), // Slack Redirect URI + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: uc.Config.SigningSecret, + }, + { + Name: "notification", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.ConfigNotificationTitle), + Description: plugin.MakeTranslator(i18n.ConfigNotificationDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.ConfigNotificationLabel), + }, + Value: uc.Config.Notification, + }, + } +} + +func (uc *UserCenter) ConfigReceiver(config []byte) error { + c := &UserCenterConfig{} + err := json.Unmarshal(config, c) + if err != nil { + return err + } + uc.Config = c + + uc.SlackClient = NewSlackClient(c.ClientID, c.ClientSecret) + return nil +} diff --git a/user-center-slack/cron.go b/user-center-slack/cron.go index 40c13d95..f1dd392e 100644 --- a/user-center-slack/cron.go +++ b/user-center-slack/cron.go @@ -1,41 +1,41 @@ -/* - * 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 slack_user_center - -import ( - "time" - - "github.com/segmentfault/pacman/log" -) - -func (uc *UserCenter) CronSyncData() { - go func() { - ticker := time.NewTicker(time.Hour) - // defer ticker.Stop() - - for { - select { - case <-ticker.C: - log.Infof("UserCenter is syncing Slack user data") - uc.syncSlackClient() - } - } - }() -} +/* + * 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 slack_user_center + +import ( + "time" + + "github.com/segmentfault/pacman/log" +) + +func (uc *UserCenter) CronSyncData() { + go func() { + ticker := time.NewTicker(time.Hour) + // defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Infof("UserCenter is syncing Slack user data") + uc.syncSlackClient() + } + } + }() +} diff --git a/user-center-slack/handler.go b/user-center-slack/handler.go index 0ad674b0..e4268471 100644 --- a/user-center-slack/handler.go +++ b/user-center-slack/handler.go @@ -1,149 +1,149 @@ -/* - * 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 slack_user_center - -import ( - "crypto/rand" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/apache/incubator-answer/plugin" - "github.com/gin-gonic/gin" - "github.com/segmentfault/pacman/log" -) - -// RespBody response body. -type RespBody struct { - // http code - Code int `json:"code"` - // reason key - Reason string `json:"reason"` - // response message - Message string `json:"msg"` - // response data - Data interface{} `json:"data"` -} - -// NewRespBodyData new response body with data -func NewRespBodyData(code int, reason string, data interface{}) *RespBody { - return &RespBody{ - Code: code, - Reason: reason, - Data: data, - } -} - -func (uc *UserCenter) BuildSlackBaseRedirectURL() string { - clientID := uc.Config.ClientID - log.Debug("Get client ID:", clientID) - scope := "chat:write,commands,groups:write,im:write,incoming-webhook,mpim:write,users:read,users:read.email" - response_type := "code" - redirect_uri := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) - - base_redirectURL := fmt.Sprintf( - "https://slack.com/oauth/v2/authorize?client_id=%s&scope=%s&response_type=%s&redirect_uri=%s", - clientID, scope, response_type, redirect_uri, - ) - - state := genState() - nonce := genNonce() - uc.Cache.Set("oauth_state_"+state, state, time.Minute*5) - - redirectURL := fmt.Sprintf("%s&state=%s&nonce=%s", base_redirectURL, state, nonce) - log.Debug("RedirectURL from BuildSlackBaseRedirectURL:", redirectURL) - - return redirectURL -} - -func (uc *UserCenter) GetSlackRedirectURL(ctx *gin.Context) { - redirectURL := uc.BuildSlackBaseRedirectURL() - log.Debug("Processing GetSlackRedirectURL") - - ctx.Writer.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(ctx.Writer) - encoder.SetEscapeHTML(false) - - respData := NewRespBodyData(http.StatusOK, "success", map[string]string{ - "redirect_url": redirectURL, - }) - ctx.Writer.WriteHeader(http.StatusOK) - if err := encoder.Encode(respData); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode response"}) - return - } -} - -func genNonce() string { - bytes := make([]byte, 10) - _, _ = rand.Read(bytes) - return hex.EncodeToString(bytes) -} - -func genState() string { - bytes := make([]byte, 32) - _, _ = rand.Read(bytes) - return base64.URLEncoding.EncodeToString(bytes) -} - -func (uc *UserCenter) Sync(ctx *gin.Context) { - uc.syncSlackClient() - - if uc.syncSuccess { - ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", map[string]any{ - "message": "User data synced successfully", - })) - return - } - - errRespBodyData := NewRespBodyData(http.StatusBadRequest, "error", map[string]any{ - "err_type": "toast", - }) - errRespBodyData.Message = "Failed to sync user data" - ctx.JSON(http.StatusBadRequest, errRespBodyData) -} - -func (uc *UserCenter) syncSlackClient() { - if !uc.syncLock.TryLock() { - log.Infof("sync data is running") - return - } - defer func() { - uc.syncing = false - if uc.syncSuccess { - uc.syncTime = time.Now() - } - uc.syncLock.Unlock() - }() - - log.Info("start sync slack data") - uc.syncing = true - uc.syncSuccess = true - - if err := uc.SlackClient.UpdateUserInfo(); err != nil { - log.Errorf("list user error: %s", err) - uc.syncSuccess = false - return - } - log.Info("end sync slack data") -} +/* + * 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 slack_user_center + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +// RespBody response body. +type RespBody struct { + // http code + Code int `json:"code"` + // reason key + Reason string `json:"reason"` + // response message + Message string `json:"msg"` + // response data + Data interface{} `json:"data"` +} + +// NewRespBodyData new response body with data +func NewRespBodyData(code int, reason string, data interface{}) *RespBody { + return &RespBody{ + Code: code, + Reason: reason, + Data: data, + } +} + +func (uc *UserCenter) BuildSlackBaseRedirectURL() string { + clientID := uc.Config.ClientID + log.Debug("Get client ID:", clientID) + scope := "chat:write,commands,groups:write,im:write,incoming-webhook,mpim:write,users:read,users:read.email" + response_type := "code" + redirect_uri := fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL()) + + base_redirectURL := fmt.Sprintf( + "https://slack.com/oauth/v2/authorize?client_id=%s&scope=%s&response_type=%s&redirect_uri=%s", + clientID, scope, response_type, redirect_uri, + ) + + state := genState() + nonce := genNonce() + uc.Cache.Set("oauth_state_"+state, state, time.Minute*5) + + redirectURL := fmt.Sprintf("%s&state=%s&nonce=%s", base_redirectURL, state, nonce) + log.Debug("RedirectURL from BuildSlackBaseRedirectURL:", redirectURL) + + return redirectURL +} + +func (uc *UserCenter) GetSlackRedirectURL(ctx *gin.Context) { + redirectURL := uc.BuildSlackBaseRedirectURL() + log.Debug("Processing GetSlackRedirectURL") + + ctx.Writer.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(ctx.Writer) + encoder.SetEscapeHTML(false) + + respData := NewRespBodyData(http.StatusOK, "success", map[string]string{ + "redirect_url": redirectURL, + }) + ctx.Writer.WriteHeader(http.StatusOK) + if err := encoder.Encode(respData); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode response"}) + return + } +} + +func genNonce() string { + bytes := make([]byte, 10) + _, _ = rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +func genState() string { + bytes := make([]byte, 32) + _, _ = rand.Read(bytes) + return base64.URLEncoding.EncodeToString(bytes) +} + +func (uc *UserCenter) Sync(ctx *gin.Context) { + uc.syncSlackClient() + + if uc.syncSuccess { + ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", map[string]any{ + "message": "User data synced successfully", + })) + return + } + + errRespBodyData := NewRespBodyData(http.StatusBadRequest, "error", map[string]any{ + "err_type": "toast", + }) + errRespBodyData.Message = "Failed to sync user data" + ctx.JSON(http.StatusBadRequest, errRespBodyData) +} + +func (uc *UserCenter) syncSlackClient() { + if !uc.syncLock.TryLock() { + log.Infof("sync data is running") + return + } + defer func() { + uc.syncing = false + if uc.syncSuccess { + uc.syncTime = time.Now() + } + uc.syncLock.Unlock() + }() + + log.Info("start sync slack data") + uc.syncing = true + uc.syncSuccess = true + + if err := uc.SlackClient.UpdateUserInfo(); err != nil { + log.Errorf("list user error: %s", err) + uc.syncSuccess = false + return + } + log.Info("end sync slack data") +} diff --git a/user-center-slack/i18n/translation.go b/user-center-slack/i18n/translation.go index 188fad21..ed7c5c97 100644 --- a/user-center-slack/i18n/translation.go +++ b/user-center-slack/i18n/translation.go @@ -1,87 +1,87 @@ -/* - * 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 i18n - -const ( - InfoName = "plugin.slack_user_center.backend.info.name" - InfoDescription = "plugin.slack_user_center.backend.info.description" - ConfigTipTitle = "plugin.slack_user_center.backend.config.tip.title" - ConfigAutoSyncLabel = "plugin.slack_user_center.backend.config.auto_sync.label" - ConfigAutoSyncTitle = "plugin.slack_user_center.backend.config.auto_sync.title" - ConfigAutoSyncDescription = "plugin.slack_user_center.backend.config.auto_sync.description" - ConfigSyncNowLabel = "plugin.slack_user_center.backend.config.sync_now.label" - ConfigSyncNowLabelForDoing = "plugin.slack_user_center.backend.config.sync_now.label_for_doing" - ConfigSyncNowTitle = "plugin.slack_user_center.backend.config.sync_now.title" - ConfigSyncNowDescription = "plugin.slack_user_center.backend.config.sync_now.description" - ConfigClientIDTitle = "plugin.slack_user_center.backend.config.client_id.title" - ConfigClientIDDescription = "plugin.slack_user_center.backend.config.client_id.description" - ConfigClientSecretTitle = "plugin.slack_user_center.backend.config.client_secret.title" - ConfigClientSecretDescription = "plugin.slack_user_center.backend.config.client_secret.description" - ConfigSigningSecretTitle = "plugin.slack_user_center.backend.config.signing_secret.title" - ConfigSigningSecretDescription = "plugin.slack_user_center.backend.config.signing_secret.description" - ConfigSyncNowSuccessResponse = "plugin.slack_user_center.backend.response.sync_now.success" - ConfigSyncNowFailedResponse = "plugin.slack_user_center.backend.response.sync_now.failed" - ConfigNotificationLabel = "plugin.slack_user_center.backend.config.notification.label" - ConfigNotificationTitle = "plugin.slack_user_center.backend.config.notification.title" - ConfigNotificationDescription = "plugin.slack_user_center.backend.config.notification.description" - - UserConfigWebhookURLTitle = "plugin.slack_user_center.backend.user_config.webhook_url.title" - - UserConfigInboxNotificationsTitle = "plugin.slack_user_center.backend.user_config.inbox_notifications.title" - UserConfigInboxNotificationsLabel = "plugin.slack_user_center.backend.user_config.inbox_notifications.label" - UserConfigInboxNotificationsDescription = "plugin.slack_user_center.backend.user_config.inbox_notifications.description" - - UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_user_center.backend.user_config.all_new_questions.title" - UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_user_center.backend.user_config.all_new_questions.label" - UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_user_center.backend.user_config.all_new_questions.description" - - UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.title" - UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.label" - UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.description" - - UserConfigUpvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.upvoted_answers.title" - UserConfigUpvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.upvoted_answers.label" - UserConfigUpvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.upvoted_answers.description" - - UserConfigDownvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.downvoted_answers.title" - UserConfigDownvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.downvoted_answers.label" - UserConfigDownvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.downvoted_answers.description" - - UserConfigUpdatedQuestionsTitle = "plugin.slack_user_center.backend.user_config.updated_questions.title" - UserConfigUpdatedQuestionsLabel = "plugin.slack_user_center.backend.user_config.updated_questions.label" - UserConfigUpdatedQuestionsDescription = "plugin.slack_user_center.backend.user_config.updated_questions.description" - - UserConfigUpdatedAnswersTitle = "plugin.slack_user_center.backend.user_config.updated_answers.title" - UserConfigUpdatedAnswersLabel = "plugin.slack_user_center.backend.user_config.updated_answers.label" - UserConfigUpdatedAnswersDescription = "plugin.slack_user_center.backend.user_config.updated_answers.description" - - TplUpdatedQuestions = "plugin.slack_user_center.backend.tpl.update_question" - TplAnswerTheQuestion = "plugin.slack_user_center.backend.tpl.answer_the_question" - TplUpdatedAnswers = "plugin.slack_user_center.backend.tpl.update_answer" - TplAcceptAnswer = "plugin.slack_user_center.backend.tpl.accept_answer" - TplCommentQuestion = "plugin.slack_user_center.backend.tpl.comment_question" - TplCommentAnswer = "plugin.slack_user_center.backend.tpl.comment_answer" - TplReplyToYou = "plugin.slack_user_center.backend.tpl.reply_to_you" - TplMentionYou = "plugin.slack_user_center.backend.tpl.mention_you" - TplInvitedYouToAnswer = "plugin.slack_user_center.backend.tpl.invited_you_to_answer" - TplNewQuestion = "plugin.slack_user_center.backend.tpl.new_question" - TplUpvotedAnswer = "plugin.slack_user_center.backend.tpl.upvoted_answer" - TplDownvotedAnswer = "plugin.slack_user_center.backend.tpl.downvoted_answer" -) +/* + * 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 i18n + +const ( + InfoName = "plugin.slack_user_center.backend.info.name" + InfoDescription = "plugin.slack_user_center.backend.info.description" + ConfigTipTitle = "plugin.slack_user_center.backend.config.tip.title" + ConfigAutoSyncLabel = "plugin.slack_user_center.backend.config.auto_sync.label" + ConfigAutoSyncTitle = "plugin.slack_user_center.backend.config.auto_sync.title" + ConfigAutoSyncDescription = "plugin.slack_user_center.backend.config.auto_sync.description" + ConfigSyncNowLabel = "plugin.slack_user_center.backend.config.sync_now.label" + ConfigSyncNowLabelForDoing = "plugin.slack_user_center.backend.config.sync_now.label_for_doing" + ConfigSyncNowTitle = "plugin.slack_user_center.backend.config.sync_now.title" + ConfigSyncNowDescription = "plugin.slack_user_center.backend.config.sync_now.description" + ConfigClientIDTitle = "plugin.slack_user_center.backend.config.client_id.title" + ConfigClientIDDescription = "plugin.slack_user_center.backend.config.client_id.description" + ConfigClientSecretTitle = "plugin.slack_user_center.backend.config.client_secret.title" + ConfigClientSecretDescription = "plugin.slack_user_center.backend.config.client_secret.description" + ConfigSigningSecretTitle = "plugin.slack_user_center.backend.config.signing_secret.title" + ConfigSigningSecretDescription = "plugin.slack_user_center.backend.config.signing_secret.description" + ConfigSyncNowSuccessResponse = "plugin.slack_user_center.backend.response.sync_now.success" + ConfigSyncNowFailedResponse = "plugin.slack_user_center.backend.response.sync_now.failed" + ConfigNotificationLabel = "plugin.slack_user_center.backend.config.notification.label" + ConfigNotificationTitle = "plugin.slack_user_center.backend.config.notification.title" + ConfigNotificationDescription = "plugin.slack_user_center.backend.config.notification.description" + + UserConfigWebhookURLTitle = "plugin.slack_user_center.backend.user_config.webhook_url.title" + + UserConfigInboxNotificationsTitle = "plugin.slack_user_center.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.slack_user_center.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.slack_user_center.backend.user_config.inbox_notifications.description" + + UserConfigAllNewQuestionsNotificationsTitle = "plugin.slack_user_center.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsNotificationsLabel = "plugin.slack_user_center.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsNotificationsDescription = "plugin.slack_user_center.backend.user_config.all_new_questions.description" + + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.slack_user_center.backend.user_config.new_questions_for_following_tags.description" + + UserConfigUpvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.upvoted_answers.title" + UserConfigUpvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.upvoted_answers.label" + UserConfigUpvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.upvoted_answers.description" + + UserConfigDownvotedAnswersTitle = "plugin.slack_user_center.backend.user_config.downvoted_answers.title" + UserConfigDownvotedAnswersLabel = "plugin.slack_user_center.backend.user_config.downvoted_answers.label" + UserConfigDownvotedAnswersDescription = "plugin.slack_user_center.backend.user_config.downvoted_answers.description" + + UserConfigUpdatedQuestionsTitle = "plugin.slack_user_center.backend.user_config.updated_questions.title" + UserConfigUpdatedQuestionsLabel = "plugin.slack_user_center.backend.user_config.updated_questions.label" + UserConfigUpdatedQuestionsDescription = "plugin.slack_user_center.backend.user_config.updated_questions.description" + + UserConfigUpdatedAnswersTitle = "plugin.slack_user_center.backend.user_config.updated_answers.title" + UserConfigUpdatedAnswersLabel = "plugin.slack_user_center.backend.user_config.updated_answers.label" + UserConfigUpdatedAnswersDescription = "plugin.slack_user_center.backend.user_config.updated_answers.description" + + TplUpdatedQuestions = "plugin.slack_user_center.backend.tpl.update_question" + TplAnswerTheQuestion = "plugin.slack_user_center.backend.tpl.answer_the_question" + TplUpdatedAnswers = "plugin.slack_user_center.backend.tpl.update_answer" + TplAcceptAnswer = "plugin.slack_user_center.backend.tpl.accept_answer" + TplCommentQuestion = "plugin.slack_user_center.backend.tpl.comment_question" + TplCommentAnswer = "plugin.slack_user_center.backend.tpl.comment_answer" + TplReplyToYou = "plugin.slack_user_center.backend.tpl.reply_to_you" + TplMentionYou = "plugin.slack_user_center.backend.tpl.mention_you" + TplInvitedYouToAnswer = "plugin.slack_user_center.backend.tpl.invited_you_to_answer" + TplNewQuestion = "plugin.slack_user_center.backend.tpl.new_question" + TplUpvotedAnswer = "plugin.slack_user_center.backend.tpl.upvoted_answer" + TplDownvotedAnswer = "plugin.slack_user_center.backend.tpl.downvoted_answer" +) diff --git a/user-center-slack/importer.go b/user-center-slack/importer.go index f4debd0b..8f2d19ca 100644 --- a/user-center-slack/importer.go +++ b/user-center-slack/importer.go @@ -1,178 +1,197 @@ -package slack_user_center - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "regexp" - "strconv" - "strings" - "time" - - "github.com/apache/incubator-answer/plugin" - "github.com/gin-gonic/gin" - "github.com/segmentfault/pacman/log" -) - -func (uc *UserCenter) parseText(text string) (string, string, []string, error) { - re := regexp.MustCompile(`\[(.*?)\]`) - matches := re.FindAllStringSubmatch(text, -1) - - if len(matches) != 3 { - return "", "", nil, fmt.Errorf("text field does not conform to the required format") - } - - part1 := matches[0][1] - part2 := matches[1][1] - rawTags := strings.Split(matches[2][1], ",") - - var tags []string - for _, tag := range rawTags { - if tag != "" { - tags = append(tags, tag) - } - } - - // if part1 or part2 or tags in empty return error - if part1 == "" || part2 == "" || len(tags) == 0 { - return "", "", nil, fmt.Errorf("text field does not be empty") - } - return part1, part2, tags, nil -} -func getSlackUserEmail(userID, token string) (string, error) { - url := fmt.Sprintf("https://slack.com/api/users.info?user=%s", userID) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var userResponse SlackUserResponse - if err := json.Unmarshal(body, &userResponse); err != nil { - return "", err - } - if !userResponse.Ok { - return "", fmt.Errorf("failed to get user info from Slack") - } - - return userResponse.User.Profile.Email, nil -} - -func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error { - body, err := io.ReadAll(ctx.Request.Body) - if err != nil { - return fmt.Errorf("could not read request body: %v", err) - } - timestamp := ctx.GetHeader("X-Slack-Request-Timestamp") - slackSignature := ctx.GetHeader("X-Slack-Signature") - - // check the timestamp validity in 5 minutes - ts, err := strconv.ParseInt(timestamp, 10, 64) - if err != nil { - return fmt.Errorf("invalid timestamp: %v", err) - } - if time.Now().Unix()-ts > 60*5 { - return fmt.Errorf("timestamp is too old") - } - // Reset the request body for further processing - ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) - - sigBaseString := fmt.Sprintf("v0:%s:%s", timestamp, string(body)) - - h := hmac.New(sha256.New, []byte(uc.Config.SigningSecret)) - h.Write([]byte(sigBaseString)) - computedSignature := "v0=" + hex.EncodeToString(h.Sum(nil)) - - if !hmac.Equal([]byte(computedSignature), []byte(slackSignature)) { - return fmt.Errorf("invalid signature") - } - - return nil -} -func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo plugin.QuestionImporterInfo, err error) { - questionInfo = plugin.QuestionImporterInfo{} - - err = uc.verifySlackRequest(ctx) - if err != nil { - return questionInfo, err - } - - text := ctx.PostForm("text") - part1, part2, tags, err := uc.parseText(text) - if err != nil { - return questionInfo, err - } - - questionInfo.Title = part1 - questionInfo.Content = part2 - questionInfo.Tags = tags - userID := ctx.PostForm("user_id") - - token := uc.SlackClient.AccessToken - email, err := getSlackUserEmail(userID, token) - if err != nil { - return questionInfo, err - } - - questionInfo.UserEmail = email - return questionInfo, nil -} - -func (uc *UserCenter) SlashCommand(ctx *gin.Context) { - body, _ := io.ReadAll(ctx.Request.Body) - ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) - cmd := ctx.PostForm("command") - if cmd != "/ask" { - log.Errorf("error: Invalid command") - ctx.JSON(http.StatusBadRequest, gin.H{"text": "Invalid command"}) - return - } - ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) - err := uc.verifySlackRequest(ctx) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"text": "Slack request verification faild"}) - log.Errorf("error: %v", err) - return - } - ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) - questionInfo, err := uc.GetQuestion(ctx) - if err != nil { - log.Errorf("error: %v", err) - ctx.JSON(200, gin.H{"text": err.Error()}) - return - } - if uc.importerFunc == nil { - log.Errorf("error: importerFunc is not initialized") - return - } - err = uc.importerFunc.AddQuestion(ctx, questionInfo) - if err != nil { - log.Errorf("error: %v", err) - ctx.JSON(http.StatusBadRequest, gin.H{"text": "Failed to add question"}) - return - } - ctx.JSON(http.StatusOK, gin.H{"text": "Question has been added successfully"}) -} - -func (uc *UserCenter) RegisterImporterFunc(ctx context.Context, importerFunc plugin.ImporterFunc) { - uc.importerFunc = importerFunc -} +/* + * 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 slack_user_center + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +func (uc *UserCenter) parseText(text string) (string, string, []string, error) { + re := regexp.MustCompile(`\[(.*?)\]`) + matches := re.FindAllStringSubmatch(text, -1) + + if len(matches) != 3 { + return "", "", nil, fmt.Errorf("text field does not conform to the required format") + } + + part1 := matches[0][1] + part2 := matches[1][1] + rawTags := strings.Split(matches[2][1], ",") + + var tags []string + for _, tag := range rawTags { + if tag != "" { + tags = append(tags, tag) + } + } + + // if part1 or part2 or tags in empty return error + if part1 == "" || part2 == "" || len(tags) == 0 { + return "", "", nil, fmt.Errorf("text field does not be empty") + } + return part1, part2, tags, nil +} +func getSlackUserEmail(userID, token string) (string, error) { + url := fmt.Sprintf("https://slack.com/api/users.info?user=%s", userID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var userResponse SlackUserResponse + if err := json.Unmarshal(body, &userResponse); err != nil { + return "", err + } + if !userResponse.Ok { + return "", fmt.Errorf("failed to get user info from Slack") + } + + return userResponse.User.Profile.Email, nil +} + +func (uc *UserCenter) verifySlackRequest(ctx *gin.Context) error { + body, err := io.ReadAll(ctx.Request.Body) + if err != nil { + return fmt.Errorf("could not read request body: %v", err) + } + timestamp := ctx.GetHeader("X-Slack-Request-Timestamp") + slackSignature := ctx.GetHeader("X-Slack-Signature") + + // check the timestamp validity in 5 minutes + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return fmt.Errorf("invalid timestamp: %v", err) + } + if time.Now().Unix()-ts > 60*5 { + return fmt.Errorf("timestamp is too old") + } + // Reset the request body for further processing + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + sigBaseString := fmt.Sprintf("v0:%s:%s", timestamp, string(body)) + + h := hmac.New(sha256.New, []byte(uc.Config.SigningSecret)) + h.Write([]byte(sigBaseString)) + computedSignature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + if !hmac.Equal([]byte(computedSignature), []byte(slackSignature)) { + return fmt.Errorf("invalid signature") + } + + return nil +} +func (uc *UserCenter) GetQuestion(ctx *gin.Context) (questionInfo plugin.QuestionImporterInfo, err error) { + questionInfo = plugin.QuestionImporterInfo{} + + err = uc.verifySlackRequest(ctx) + if err != nil { + return questionInfo, err + } + + text := ctx.PostForm("text") + part1, part2, tags, err := uc.parseText(text) + if err != nil { + return questionInfo, err + } + + questionInfo.Title = part1 + questionInfo.Content = part2 + questionInfo.Tags = tags + userID := ctx.PostForm("user_id") + + token := uc.SlackClient.AccessToken + email, err := getSlackUserEmail(userID, token) + if err != nil { + return questionInfo, err + } + + questionInfo.UserEmail = email + return questionInfo, nil +} + +func (uc *UserCenter) SlashCommand(ctx *gin.Context) { + body, _ := io.ReadAll(ctx.Request.Body) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + cmd := ctx.PostForm("command") + if cmd != "/ask" { + log.Errorf("error: Invalid command") + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Invalid command"}) + return + } + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + err := uc.verifySlackRequest(ctx) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Slack request verification faild"}) + log.Errorf("error: %v", err) + return + } + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + questionInfo, err := uc.GetQuestion(ctx) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(200, gin.H{"text": err.Error()}) + return + } + if uc.importerFunc == nil { + log.Errorf("error: importerFunc is not initialized") + return + } + err = uc.importerFunc.AddQuestion(ctx, questionInfo) + if err != nil { + log.Errorf("error: %v", err) + ctx.JSON(http.StatusBadRequest, gin.H{"text": "Failed to add question"}) + return + } + ctx.JSON(http.StatusOK, gin.H{"text": "Question has been added successfully"}) +} + +func (uc *UserCenter) RegisterImporterFunc(ctx context.Context, importerFunc plugin.ImporterFunc) { + uc.importerFunc = importerFunc +} diff --git a/user-center-slack/notification.go b/user-center-slack/notification.go index 799ec91f..94fa8ac2 100644 --- a/user-center-slack/notification.go +++ b/user-center-slack/notification.go @@ -1,139 +1,158 @@ -package slack_user_center - -import ( - "strings" - - slackI18n "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/go-resty/resty/v2" - "github.com/segmentfault/pacman/i18n" - "github.com/segmentfault/pacman/log" -) - -// GetNewQuestionSubscribers returns the subscribers of the new question notification -func (uc *UserCenter) GetNewQuestionSubscribers() (userIDs []string) { - for userID, conf := range uc.UserConfigCache.userConfigMapping { - if conf.AllNewQuestions { - userIDs = append(userIDs, userID) - } - } - return userIDs -} - -// Notify sends a notification to the user -func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { - log.Debugf("try to send notification %+v", msg) - - if !uc.Config.Notification { - return - } - - // get user config - userConfig, err := uc.getUserConfig(msg.ReceiverUserID) - if err != nil { - log.Errorf("get user config failed: %v", err) - return - } - if userConfig == nil { - log.Debugf("user %s has no config", msg.ReceiverUserID) - return - } - - // check if the notification is enabled - switch msg.Type { - case plugin.NotificationNewQuestion: - if !userConfig.AllNewQuestions { - log.Debugf("user %s not config the new question", msg.ReceiverUserID) - return - } - case plugin.NotificationNewQuestionFollowedTag: - if !userConfig.NewQuestionsForFollowingTags { - log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) - return - } - case plugin.NotificationUpVotedTheAnswer: - if !userConfig.UpvotedAnswers { - log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) - } - case plugin.NotificationDownVotedTheAnswer: - if !userConfig.DownvotedAnswers { - log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) - } - - case plugin.NotificationUpdateQuestion: - if !userConfig.UpdatedQuestions { - log.Debugf("user %s not config the update question", msg.ReceiverUserID) - return - } - case plugin.NotificationUpdateAnswer: - if !userConfig.UpdatedAnswers { - log.Debugf("user %s not config the update answer", msg.ReceiverUserID) - return - } - default: - if !userConfig.InboxNotifications { - log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) - return - } - } - - log.Debugf("user %s config the notification", msg.ReceiverUserID) - - if len(userConfig.WebhookURL) == 0 { - log.Errorf("user %s has no webhook url", msg.ReceiverUserID) - return - } - - notificationMsg := renderNotification(msg) - // no need to send empty message - if len(notificationMsg) == 0 { - log.Debugf("this type of notification will be drop, the type is %s", msg.Type) - return - } - - // Create a Resty Client - client := resty.New() - resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(NewWebhookReq(notificationMsg)). - Post(userConfig.WebhookURL) - - if err != nil { - log.Errorf("send message failed: %v %v", err, resp) - } else { - log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) - } -} - -// renderNotification generates the notification message based on type -func renderNotification(msg plugin.NotificationMessage) string { - lang := i18n.Language(msg.ReceiverLang) - switch msg.Type { - case plugin.NotificationUpdateQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) - case plugin.NotificationAnswerTheQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) - case plugin.NotificationUpdateAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) - case plugin.NotificationAcceptAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) - case plugin.NotificationCommentQuestion: - return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) - case plugin.NotificationCommentAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) - case plugin.NotificationReplyToYou: - return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) - case plugin.NotificationMentionYou: - return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) - case plugin.NotificationInvitedYouToAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) - case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: - msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") - return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) - case plugin.NotificationUpVotedTheAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) - case plugin.NotificationDownVotedTheAnswer: - return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) - } - return "" -} +/* + * 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 slack_user_center + +import ( + "strings" + + slackI18n "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/go-resty/resty/v2" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +// GetNewQuestionSubscribers returns the subscribers of the new question notification +func (uc *UserCenter) GetNewQuestionSubscribers() (userIDs []string) { + for userID, conf := range uc.UserConfigCache.userConfigMapping { + if conf.AllNewQuestions { + userIDs = append(userIDs, userID) + } + } + return userIDs +} + +// Notify sends a notification to the user +func (uc *UserCenter) Notify(msg plugin.NotificationMessage) { + log.Debugf("try to send notification %+v", msg) + + if !uc.Config.Notification { + return + } + + // get user config + userConfig, err := uc.getUserConfig(msg.ReceiverUserID) + if err != nil { + log.Errorf("get user config failed: %v", err) + return + } + if userConfig == nil { + log.Debugf("user %s has no config", msg.ReceiverUserID) + return + } + + // check if the notification is enabled + switch msg.Type { + case plugin.NotificationNewQuestion: + if !userConfig.AllNewQuestions { + log.Debugf("user %s not config the new question", msg.ReceiverUserID) + return + } + case plugin.NotificationNewQuestionFollowedTag: + if !userConfig.NewQuestionsForFollowingTags { + log.Debugf("user %s not config the new question followed tag", msg.ReceiverUserID) + return + } + case plugin.NotificationUpVotedTheAnswer: + if !userConfig.UpvotedAnswers { + log.Debugf("user %s not config the new upvoted answers", msg.ReceiverUserID) + } + case plugin.NotificationDownVotedTheAnswer: + if !userConfig.DownvotedAnswers { + log.Debugf("user %s not config the new downvoted answers", msg.ReceiverUserID) + } + + case plugin.NotificationUpdateQuestion: + if !userConfig.UpdatedQuestions { + log.Debugf("user %s not config the update question", msg.ReceiverUserID) + return + } + case plugin.NotificationUpdateAnswer: + if !userConfig.UpdatedAnswers { + log.Debugf("user %s not config the update answer", msg.ReceiverUserID) + return + } + default: + if !userConfig.InboxNotifications { + log.Debugf("user %s not config the inbox notification", msg.ReceiverUserID) + return + } + } + + log.Debugf("user %s config the notification", msg.ReceiverUserID) + + if len(userConfig.WebhookURL) == 0 { + log.Errorf("user %s has no webhook url", msg.ReceiverUserID) + return + } + + notificationMsg := renderNotification(msg) + // no need to send empty message + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + + // Create a Resty Client + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(NewWebhookReq(notificationMsg)). + Post(userConfig.WebhookURL) + + if err != nil { + log.Errorf("send message failed: %v %v", err, resp) + } else { + log.Infof("send message to %s success, resp: %s", msg.ReceiverUserID, resp.String()) + } +} + +// renderNotification generates the notification message based on type +func renderNotification(msg plugin.NotificationMessage) string { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedQuestions, msg) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplAnswerTheQuestion, msg) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpdatedAnswers, msg) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplAcceptAnswer, msg) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, slackI18n.TplCommentQuestion, msg) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplCommentAnswer, msg) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, slackI18n.TplReplyToYou, msg) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, slackI18n.TplMentionYou, msg) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplInvitedYouToAnswer, msg) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, ","), ", ") + return plugin.TranslateWithData(lang, slackI18n.TplNewQuestion, msg) + case plugin.NotificationUpVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplUpvotedAnswer, msg) + case plugin.NotificationDownVotedTheAnswer: + return plugin.TranslateWithData(lang, slackI18n.TplDownvotedAnswer, msg) + } + return "" +} diff --git a/user-center-slack/schema.go b/user-center-slack/schema.go index 7596fa53..743cabb7 100644 --- a/user-center-slack/schema.go +++ b/user-center-slack/schema.go @@ -1,110 +1,110 @@ -/* - * 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 slack_user_center - -type AuthUserResp struct { - Ok bool `json:"ok"` - Errmsg string `json:"error"` - User *UserInfo `json:"user"` -} - -type UserProfile struct { - AvatarHash string `json:"avatar_hash"` - StatusText string `json:"status_text"` - StatusEmoji string `json:"status_emoji"` - RealName string `json:"real_name"` - DisplayName string `json:"display_name"` - Email string `json:"email"` - ImageOriginal string `json:"image_original"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - Image512 string `json:"image_512"` -} - -type UserInfo struct { - ID string `json:"id"` - TeamID string `json:"team_id"` - Name string `json:"name"` - RealName string `json:"real_name"` - Deleted bool `json:"deleted"` - TimeZone string `json:"tz"` - TimeZoneLabel string `json:"tz_label"` - TimeZoneOffset int `json:"tz_offset"` - Profile UserProfile `json:"profile"` - IsAdmin bool `json:"is_admin"` - IsOwner bool `json:"is_owner"` - IsPrimaryOwner bool `json:"is_primary_owner"` - IsRestricted bool `json:"is_restricted"` - IsUltraRestricted bool `json:"is_ultra_restricted"` - IsBot bool `json:"is_bot"` - Updated int64 `json:"updated"` - IsAppUser bool `json:"is_app_user"` - Has2FA bool `json:"has_2fa"` - - LastLogin int64 `json:"last_login,omitempty"` - IsAvailable bool `json:"is_available"` - Enable bool `json:"true"` - Status int `json:"status"` -} - -type WebhookReq struct { - Blocks []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - } `json:"blocks"` -} - -func NewWebhookReq(content string) *WebhookReq { - return &WebhookReq{ - Blocks: []struct { - Type string `json:"type"` - Text struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"text"` - }{ - { - Type: "section", - Text: struct { - Type string `json:"type"` - Text string `json:"text"` - }{ - Type: "mrkdwn", - Text: content, - }, - }, - }, - } -} - -type SlackUserResponse struct { - Ok bool `json:"ok"` - User struct { - Profile struct { - Email string `json:"email"` - } `json:"profile"` - } `json:"user"` -} +/* + * 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 slack_user_center + +type AuthUserResp struct { + Ok bool `json:"ok"` + Errmsg string `json:"error"` + User *UserInfo `json:"user"` +} + +type UserProfile struct { + AvatarHash string `json:"avatar_hash"` + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + RealName string `json:"real_name"` + DisplayName string `json:"display_name"` + Email string `json:"email"` + ImageOriginal string `json:"image_original"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + Image512 string `json:"image_512"` +} + +type UserInfo struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + RealName string `json:"real_name"` + Deleted bool `json:"deleted"` + TimeZone string `json:"tz"` + TimeZoneLabel string `json:"tz_label"` + TimeZoneOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsBot bool `json:"is_bot"` + Updated int64 `json:"updated"` + IsAppUser bool `json:"is_app_user"` + Has2FA bool `json:"has_2fa"` + + LastLogin int64 `json:"last_login,omitempty"` + IsAvailable bool `json:"is_available"` + Enable bool `json:"true"` + Status int `json:"status"` +} + +type WebhookReq struct { + Blocks []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + } `json:"blocks"` +} + +func NewWebhookReq(content string) *WebhookReq { + return &WebhookReq{ + Blocks: []struct { + Type string `json:"type"` + Text struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"text"` + }{ + { + Type: "section", + Text: struct { + Type string `json:"type"` + Text string `json:"text"` + }{ + Type: "mrkdwn", + Text: content, + }, + }, + }, + } +} + +type SlackUserResponse struct { + Ok bool `json:"ok"` + User struct { + Profile struct { + Email string `json:"email"` + } `json:"profile"` + } `json:"user"` +} diff --git a/user-center-slack/slack_user_center.go b/user-center-slack/slack_user_center.go index 688d61db..63da5c44 100644 --- a/user-center-slack/slack_user_center.go +++ b/user-center-slack/slack_user_center.go @@ -1,251 +1,251 @@ -/* - * 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 slack_user_center - -import ( - "embed" - "fmt" - "net/http" - "sync" - "time" - - "github.com/apache/incubator-answer-plugins/util" - - "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/gin-gonic/gin" - "github.com/patrickmn/go-cache" - "github.com/segmentfault/pacman/log" -) - -//go:embed info.yaml -var Info embed.FS - -type Importer struct{} - -type UserCenter struct { - Config *UserCenterConfig - SlackClient *SlackClient - UserConfigCache *UserConfigCache - Cache *cache.Cache - syncLock sync.Mutex - syncing bool - syncSuccess bool - syncTime time.Time - importerFunc plugin.ImporterFunc -} - -func (uc *UserCenter) RegisterUnAuthRouter(r *gin.RouterGroup) { - r.GET("/slack/login/url", uc.GetSlackRedirectURL) - r.POST("/slack/slash", uc.SlashCommand) -} - -func (uc *UserCenter) RegisterAuthUserRouter(r *gin.RouterGroup) { -} - -func (uc *UserCenter) RegisterAuthAdminRouter(r *gin.RouterGroup) { - r.GET("/slack/sync", uc.Sync) -} - -func (uc *UserCenter) AfterLogin(externalID, accessToken string) { - log.Debugf("user %s is login", externalID) - uc.Cache.Set(externalID, accessToken, time.Minute*5) -} - -func (uc *UserCenter) UserStatus(externalID string) (userStatus plugin.UserStatus) { - if len(externalID) == 0 { - return plugin.UserStatusAvailable - } - - var err error - userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] - if userDetailInfo == nil { - userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) - if err != nil { - log.Errorf("get user detail info failed: %v", err) - } - } - if userDetailInfo == nil { - return plugin.UserStatusDeleted - } - switch userDetailInfo.Status { - case 1: - return plugin.UserStatusAvailable - case 2: - return plugin.UserStatusSuspended - default: - return plugin.UserStatusDeleted - } -} - -func init() { - uc := &UserCenter{ - Config: &UserCenterConfig{}, - UserConfigCache: NewUserConfigCache(), - SlackClient: NewSlackClient("", ""), - Cache: cache.New(5*time.Minute, 10*time.Minute), - syncLock: sync.Mutex{}, - } - - plugin.Register(uc) - uc.CronSyncData() -} - -func (uc *UserCenter) Info() plugin.Info { - info := &util.Info{} - info.GetInfo(Info) - - return plugin.Info{ - Name: plugin.MakeTranslator(i18n.InfoName), - SlugName: info.SlugName, - Description: plugin.MakeTranslator(i18n.InfoDescription), - Author: info.Author, - Version: info.Version, - Link: info.Link, - } -} - -func (uc *UserCenter) Description() plugin.UserCenterDesc { - redirectURL := uc.BuildSlackBaseRedirectURL() - desc := plugin.UserCenterDesc{ - Name: "Slack", - DisplayName: plugin.MakeTranslator(i18n.InfoName), - Icon: "", - Url: "", - LoginRedirectURL: redirectURL, - SignUpRedirectURL: redirectURL, - RankAgentEnabled: false, - UserStatusAgentEnabled: false, - UserRoleAgentEnabled: false, - MustAuthEmailEnabled: true, - EnabledOriginalUserSystem: true, - } - return desc -} - -func (uc *UserCenter) ControlCenterItems() []plugin.ControlCenter { - var controlCenterItems []plugin.ControlCenter - return controlCenterItems -} - -func (uc *UserCenter) LoginCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { - log.Debugf("Processing LoginCallback") - CallbackURL := ctx.Request.URL.String() - log.Debugf("callbackURL in SlackLoginCallback:", CallbackURL) - code := ctx.Query("code") - if len(code) == 0 { - return nil, fmt.Errorf("code is empty") - } - - state := ctx.Query("state") - if len(state) == 0 { - return nil, fmt.Errorf("state is empty") - } - log.Debugf("request code: %s, state: %s", code, state) - - expectedState, exist := uc.Cache.Get("oauth_state_" + state) - if !exist { - fmt.Println("State not found in cache or expired") - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired state"}) - return - } - if state != expectedState { - fmt.Println("State mismatch") - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid state"}) - return - } - log.Debugf("State validated successfully") - - info, err := uc.SlackClient.AuthUser(code) - if err != nil { - return nil, fmt.Errorf("auth user failed: %w", err) - } - if !info.IsAvailable { - return nil, fmt.Errorf("user is not available") - } - //Get Email - if len(info.Profile.Email) == 0 { - ctx.Redirect(http.StatusFound, "/user-center/auth-failed") - return nil, fmt.Errorf("user email is empty") - } - - userInfo = &plugin.UserCenterBasicUserInfo{} - userInfo.ExternalID = info.ID - userInfo.Username = info.ID - userInfo.DisplayName = info.Name - userInfo.Email = info.Profile.Email - userInfo.Rank = 0 - userInfo.Mobile = "" - userInfo.Avatar = info.Profile.Image192 - - uc.Cache.Set(state, userInfo.ExternalID, time.Minute*5) - return userInfo, nil -} - -func (uc *UserCenter) SignUpCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { - return uc.LoginCallback(ctx) -} - -func (uc *UserCenter) UserInfo(externalID string) (userInfo *plugin.UserCenterBasicUserInfo, err error) { - userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] - if userDetailInfo == nil { - userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) - if err != nil { - log.Errorf("get user detail info failed: %v", err) - userInfo = &plugin.UserCenterBasicUserInfo{ - ExternalID: externalID, - Status: plugin.UserStatusDeleted, - } - return userInfo, nil - } - } - - userInfo = &plugin.UserCenterBasicUserInfo{ - ExternalID: externalID, - Username: userDetailInfo.ID, - DisplayName: userDetailInfo.Name, - Bio: "", - } - switch userDetailInfo.Status { - case 1: - userInfo.Status = plugin.UserStatusAvailable - case 2: - userInfo.Status = plugin.UserStatusSuspended - default: - userInfo.Status = plugin.UserStatusDeleted - } - return userInfo, nil -} - -func (uc *UserCenter) UserList(externalIDs []string) (userList []*plugin.UserCenterBasicUserInfo, err error) { - userList = make([]*plugin.UserCenterBasicUserInfo, 0) - return userList, nil -} - -func (uc *UserCenter) UserSettings(externalID string) (userSettings *plugin.SettingInfo, err error) { - return &plugin.SettingInfo{ - ProfileSettingRedirectURL: "", - AccountSettingRedirectURL: "", - }, nil -} - -func (uc *UserCenter) PersonalBranding(externalID string) (branding []*plugin.PersonalBranding) { - return branding -} +/* + * 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 slack_user_center + +import ( + "embed" + "fmt" + "net/http" + "sync" + "time" + + "github.com/apache/incubator-answer-plugins/util" + + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/gin-gonic/gin" + "github.com/patrickmn/go-cache" + "github.com/segmentfault/pacman/log" +) + +//go:embed info.yaml +var Info embed.FS + +type Importer struct{} + +type UserCenter struct { + Config *UserCenterConfig + SlackClient *SlackClient + UserConfigCache *UserConfigCache + Cache *cache.Cache + syncLock sync.Mutex + syncing bool + syncSuccess bool + syncTime time.Time + importerFunc plugin.ImporterFunc +} + +func (uc *UserCenter) RegisterUnAuthRouter(r *gin.RouterGroup) { + r.GET("/slack/login/url", uc.GetSlackRedirectURL) + r.POST("/slack/slash", uc.SlashCommand) +} + +func (uc *UserCenter) RegisterAuthUserRouter(r *gin.RouterGroup) { +} + +func (uc *UserCenter) RegisterAuthAdminRouter(r *gin.RouterGroup) { + r.GET("/slack/sync", uc.Sync) +} + +func (uc *UserCenter) AfterLogin(externalID, accessToken string) { + log.Debugf("user %s is login", externalID) + uc.Cache.Set(externalID, accessToken, time.Minute*5) +} + +func (uc *UserCenter) UserStatus(externalID string) (userStatus plugin.UserStatus) { + if len(externalID) == 0 { + return plugin.UserStatusAvailable + } + + var err error + userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] + if userDetailInfo == nil { + userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) + if err != nil { + log.Errorf("get user detail info failed: %v", err) + } + } + if userDetailInfo == nil { + return plugin.UserStatusDeleted + } + switch userDetailInfo.Status { + case 1: + return plugin.UserStatusAvailable + case 2: + return plugin.UserStatusSuspended + default: + return plugin.UserStatusDeleted + } +} + +func init() { + uc := &UserCenter{ + Config: &UserCenterConfig{}, + UserConfigCache: NewUserConfigCache(), + SlackClient: NewSlackClient("", ""), + Cache: cache.New(5*time.Minute, 10*time.Minute), + syncLock: sync.Mutex{}, + } + + plugin.Register(uc) + uc.CronSyncData() +} + +func (uc *UserCenter) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (uc *UserCenter) Description() plugin.UserCenterDesc { + redirectURL := uc.BuildSlackBaseRedirectURL() + desc := plugin.UserCenterDesc{ + Name: "Slack", + DisplayName: plugin.MakeTranslator(i18n.InfoName), + Icon: "", + Url: "", + LoginRedirectURL: redirectURL, + SignUpRedirectURL: redirectURL, + RankAgentEnabled: false, + UserStatusAgentEnabled: false, + UserRoleAgentEnabled: false, + MustAuthEmailEnabled: true, + EnabledOriginalUserSystem: true, + } + return desc +} + +func (uc *UserCenter) ControlCenterItems() []plugin.ControlCenter { + var controlCenterItems []plugin.ControlCenter + return controlCenterItems +} + +func (uc *UserCenter) LoginCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + log.Debugf("Processing LoginCallback") + CallbackURL := ctx.Request.URL.String() + log.Debugf("callbackURL in SlackLoginCallback:", CallbackURL) + code := ctx.Query("code") + if len(code) == 0 { + return nil, fmt.Errorf("code is empty") + } + + state := ctx.Query("state") + if len(state) == 0 { + return nil, fmt.Errorf("state is empty") + } + log.Debugf("request code: %s, state: %s", code, state) + + expectedState, exist := uc.Cache.Get("oauth_state_" + state) + if !exist { + fmt.Println("State not found in cache or expired") + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired state"}) + return + } + if state != expectedState { + fmt.Println("State mismatch") + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid state"}) + return + } + log.Debugf("State validated successfully") + + info, err := uc.SlackClient.AuthUser(code) + if err != nil { + return nil, fmt.Errorf("auth user failed: %w", err) + } + if !info.IsAvailable { + return nil, fmt.Errorf("user is not available") + } + //Get Email + if len(info.Profile.Email) == 0 { + ctx.Redirect(http.StatusFound, "/user-center/auth-failed") + return nil, fmt.Errorf("user email is empty") + } + + userInfo = &plugin.UserCenterBasicUserInfo{} + userInfo.ExternalID = info.ID + userInfo.Username = info.ID + userInfo.DisplayName = info.Name + userInfo.Email = info.Profile.Email + userInfo.Rank = 0 + userInfo.Mobile = "" + userInfo.Avatar = info.Profile.Image192 + + uc.Cache.Set(state, userInfo.ExternalID, time.Minute*5) + return userInfo, nil +} + +func (uc *UserCenter) SignUpCallback(ctx *plugin.GinContext) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + return uc.LoginCallback(ctx) +} + +func (uc *UserCenter) UserInfo(externalID string) (userInfo *plugin.UserCenterBasicUserInfo, err error) { + userDetailInfo := uc.SlackClient.UserInfoMapping[externalID] + if userDetailInfo == nil { + userDetailInfo, err = uc.SlackClient.GetUserDetailInfo(externalID) + if err != nil { + log.Errorf("get user detail info failed: %v", err) + userInfo = &plugin.UserCenterBasicUserInfo{ + ExternalID: externalID, + Status: plugin.UserStatusDeleted, + } + return userInfo, nil + } + } + + userInfo = &plugin.UserCenterBasicUserInfo{ + ExternalID: externalID, + Username: userDetailInfo.ID, + DisplayName: userDetailInfo.Name, + Bio: "", + } + switch userDetailInfo.Status { + case 1: + userInfo.Status = plugin.UserStatusAvailable + case 2: + userInfo.Status = plugin.UserStatusSuspended + default: + userInfo.Status = plugin.UserStatusDeleted + } + return userInfo, nil +} + +func (uc *UserCenter) UserList(externalIDs []string) (userList []*plugin.UserCenterBasicUserInfo, err error) { + userList = make([]*plugin.UserCenterBasicUserInfo, 0) + return userList, nil +} + +func (uc *UserCenter) UserSettings(externalID string) (userSettings *plugin.SettingInfo, err error) { + return &plugin.SettingInfo{ + ProfileSettingRedirectURL: "", + AccountSettingRedirectURL: "", + }, nil +} + +func (uc *UserCenter) PersonalBranding(externalID string) (branding []*plugin.PersonalBranding) { + return branding +} diff --git a/user-center-slack/user_config.go b/user-center-slack/user_config.go index c2885f04..b39eebc4 100644 --- a/user-center-slack/user_config.go +++ b/user-center-slack/user_config.go @@ -1,165 +1,165 @@ -/* - * 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 slack_user_center - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" - "github.com/apache/incubator-answer/plugin" - "github.com/segmentfault/pacman/log" -) - -type UserConfig struct { - WebhookURL string `json:"webhook_url"` - InboxNotifications bool `json:"inbox_notifications"` - AllNewQuestions bool `json:"all_new_questions"` - NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` - UpvotedAnswers bool `json:"upvoted_answers"` - DownvotedAnswers bool `json:"downvoted_answers"` - UpdatedQuestions bool `json:"updated_questions"` - UpdatedAnswers bool `json:"updated_answers"` -} - -type UserConfigCache struct { - // key: userID value: user config - userConfigMapping map[string]*UserConfig - sync.Mutex -} - -func NewUserConfigCache() *UserConfigCache { - ucc := &UserConfigCache{ - userConfigMapping: make(map[string]*UserConfig), - } - return ucc -} - -func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { - ucc.Lock() - defer ucc.Unlock() - ucc.userConfigMapping[userID] = config -} - -func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { - fields := make([]plugin.ConfigField, 0) - // Show tip for user, if the notification service is disabled - if !uc.Config.Notification { - fields = append(fields, plugin.ConfigField{ - Name: "tip", - Type: plugin.ConfigTypeLegend, - Title: plugin.MakeTranslator(i18n.ConfigTipTitle), - Description: plugin.Translator{}, - UIOptions: plugin.ConfigFieldUIOptions{ - ClassName: "mb-3", - FieldClassName: "mb-0 text-danger", - }, - }) - } - fields = append(fields, plugin.ConfigField{ - Name: "webhook_url", - Type: plugin.ConfigTypeInput, - Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), - Required: true, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - }) - fields = append(fields, createSwitchConfig( - "inbox_notifications", - i18n.UserConfigInboxNotificationsTitle, - i18n.UserConfigInboxNotificationsLabel, - i18n.UserConfigInboxNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "all_new_questions", - i18n.UserConfigAllNewQuestionsNotificationsTitle, - i18n.UserConfigAllNewQuestionsNotificationsLabel, - i18n.UserConfigAllNewQuestionsNotificationsDescription, - )) - fields = append(fields, createSwitchConfig( - "new_questions_for_following_tags", - i18n.UserConfigNewQuestionsForFollowingTagsTitle, - i18n.UserConfigNewQuestionsForFollowingTagsLabel, - i18n.UserConfigNewQuestionsForFollowingTagsDescription, - )) - fields = append(fields, createSwitchConfig( - "upvoted_answers", - i18n.UserConfigUpvotedAnswersTitle, - i18n.UserConfigUpvotedAnswersLabel, - i18n.UserConfigUpvotedAnswersDescription, - )) - fields = append(fields, createSwitchConfig( - "downvoted_answers", - i18n.UserConfigDownvotedAnswersTitle, - i18n.UserConfigDownvotedAnswersLabel, - i18n.UserConfigDownvotedAnswersDescription, - )) - fields = append(fields, createSwitchConfig( - "updated_questions", - i18n.UserConfigUpdatedQuestionsTitle, - i18n.UserConfigUpdatedQuestionsLabel, - i18n.UserConfigUpdatedQuestionsDescription, - )) - fields = append(fields, createSwitchConfig( - "updated_answers", - i18n.UserConfigUpdatedAnswersTitle, - i18n.UserConfigUpdatedAnswersLabel, - i18n.UserConfigUpdatedAnswersDescription, - )) - return fields -} - -func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { - return plugin.ConfigField{ - Name: name, - Type: plugin.ConfigTypeSwitch, - Title: plugin.MakeTranslator(title), - Description: plugin.MakeTranslator(desc), - UIOptions: plugin.ConfigFieldUIOptions{ - Label: plugin.MakeTranslator(label), - }, - } -} - -func (uc *UserCenter) UserConfigReceiver(userID string, config []byte) error { - log.Debugf("receive user config %s %s", userID, string(config)) - var userConfig UserConfig - err := json.Unmarshal(config, &userConfig) - if err != nil { - return fmt.Errorf("unmarshal user config failed: %w", err) - } - uc.UserConfigCache.SetUserConfig(userID, &userConfig) - return nil -} - -func (uc *UserCenter) getUserConfig(userID string) (config *UserConfig, err error) { - userConfig := plugin.GetPluginUserConfig(userID, uc.Info().SlugName) - if len(userConfig) == 0 { - return nil, nil - } - config = &UserConfig{} - err = json.Unmarshal(userConfig, config) - if err != nil { - return nil, fmt.Errorf("unmarshal user config failed: %w", err) - } - return config, nil -} +/* + * 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 slack_user_center + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/apache/incubator-answer-plugins/user-center-slack/i18n" + "github.com/apache/incubator-answer/plugin" + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + WebhookURL string `json:"webhook_url"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` + UpvotedAnswers bool `json:"upvoted_answers"` + DownvotedAnswers bool `json:"downvoted_answers"` + UpdatedQuestions bool `json:"updated_questions"` + UpdatedAnswers bool `json:"updated_answers"` +} + +type UserConfigCache struct { + // key: userID value: user config + userConfigMapping map[string]*UserConfig + sync.Mutex +} + +func NewUserConfigCache() *UserConfigCache { + ucc := &UserConfigCache{ + userConfigMapping: make(map[string]*UserConfig), + } + return ucc +} + +func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) { + ucc.Lock() + defer ucc.Unlock() + ucc.userConfigMapping[userID] = config +} + +func (uc *UserCenter) UserConfigFields() []plugin.ConfigField { + fields := make([]plugin.ConfigField, 0) + // Show tip for user, if the notification service is disabled + if !uc.Config.Notification { + fields = append(fields, plugin.ConfigField{ + Name: "tip", + Type: plugin.ConfigTypeLegend, + Title: plugin.MakeTranslator(i18n.ConfigTipTitle), + Description: plugin.Translator{}, + UIOptions: plugin.ConfigFieldUIOptions{ + ClassName: "mb-3", + FieldClassName: "mb-0 text-danger", + }, + }) + } + fields = append(fields, plugin.ConfigField{ + Name: "webhook_url", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigWebhookURLTitle), + Required: true, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }) + fields = append(fields, createSwitchConfig( + "inbox_notifications", + i18n.UserConfigInboxNotificationsTitle, + i18n.UserConfigInboxNotificationsLabel, + i18n.UserConfigInboxNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "all_new_questions", + i18n.UserConfigAllNewQuestionsNotificationsTitle, + i18n.UserConfigAllNewQuestionsNotificationsLabel, + i18n.UserConfigAllNewQuestionsNotificationsDescription, + )) + fields = append(fields, createSwitchConfig( + "new_questions_for_following_tags", + i18n.UserConfigNewQuestionsForFollowingTagsTitle, + i18n.UserConfigNewQuestionsForFollowingTagsLabel, + i18n.UserConfigNewQuestionsForFollowingTagsDescription, + )) + fields = append(fields, createSwitchConfig( + "upvoted_answers", + i18n.UserConfigUpvotedAnswersTitle, + i18n.UserConfigUpvotedAnswersLabel, + i18n.UserConfigUpvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "downvoted_answers", + i18n.UserConfigDownvotedAnswersTitle, + i18n.UserConfigDownvotedAnswersLabel, + i18n.UserConfigDownvotedAnswersDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_questions", + i18n.UserConfigUpdatedQuestionsTitle, + i18n.UserConfigUpdatedQuestionsLabel, + i18n.UserConfigUpdatedQuestionsDescription, + )) + fields = append(fields, createSwitchConfig( + "updated_answers", + i18n.UserConfigUpdatedAnswersTitle, + i18n.UserConfigUpdatedAnswersLabel, + i18n.UserConfigUpdatedAnswersDescription, + )) + return fields +} + +func createSwitchConfig(name, title, label, desc string) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(label), + }, + } +} + +func (uc *UserCenter) UserConfigReceiver(userID string, config []byte) error { + log.Debugf("receive user config %s %s", userID, string(config)) + var userConfig UserConfig + err := json.Unmarshal(config, &userConfig) + if err != nil { + return fmt.Errorf("unmarshal user config failed: %w", err) + } + uc.UserConfigCache.SetUserConfig(userID, &userConfig) + return nil +} + +func (uc *UserCenter) getUserConfig(userID string) (config *UserConfig, err error) { + userConfig := plugin.GetPluginUserConfig(userID, uc.Info().SlugName) + if len(userConfig) == 0 { + return nil, nil + } + config = &UserConfig{} + err = json.Unmarshal(userConfig, config) + if err != nil { + return nil, fmt.Errorf("unmarshal user config failed: %w", err) + } + return config, nil +}