This is an automated email from the ASF dual-hosted git repository. linkinstar pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/incubator-answer-plugins.git
commit 464951c812d1b58b7c1037b8e4c36de960cb68ba Author: Sonui <[email protected]> AuthorDate: Mon Aug 12 00:58:38 2024 +0800 feat: add support for Lark notifications --- notification-lark/README.md | 50 ++++++ notification-lark/README_CN.md | 49 +++++ notification-lark/config.go | 190 ++++++++++++++++++++ notification-lark/docs/1.png | Bin 0 -> 385099 bytes notification-lark/docs/2.png | Bin 0 -> 453527 bytes notification-lark/docs/3.png | Bin 0 -> 351335 bytes notification-lark/docs/4.png | Bin 0 -> 364168 bytes notification-lark/go.mod | 51 ++++++ notification-lark/go.sum | 194 ++++++++++++++++++++ notification-lark/i18n/en_US.yaml | 104 +++++++++++ notification-lark/i18n/translation.go | 61 +++++++ notification-lark/i18n/zh_CN.yaml | 104 +++++++++++ notification-lark/info.yaml | 22 +++ notification-lark/lark_card.go | 268 +++++++++++++++++++++++++++ notification-lark/lark_card_test.go | 178 ++++++++++++++++++ notification-lark/notification.go | 329 ++++++++++++++++++++++++++++++++++ notification-lark/user_config.go | 121 +++++++++++++ notification-lark/utils.go | 56 ++++++ notification-lark/utils_test.go | 43 +++++ 19 files changed, 1820 insertions(+) diff --git a/notification-lark/README.md b/notification-lark/README.md new file mode 100644 index 0000000..250ffec --- /dev/null +++ b/notification-lark/README.md @@ -0,0 +1,50 @@ +# NotificationLark Plugin + +[English](./README.md) | [中文](./README_CN.md) + +## How to use + +To use the NotificationLark plugin with your application, install it using the following command: + +```bash +./answer build --with github.com/apache/incubator-answer-plugins/notification-lark +``` + +## How to config + +### For Administrators + +#### Creating a Bot + +1. Create a Bot in Lark or Feishu: + * Visit [Lark](https://open.larksuite.com) or [Feishu](https://open.feishu.cn) to create a new bot. + * In the bot settings, enable the Custom Bot Menu and set the action type to Push Event. + * Configure the menu event with `10001` as the event code. +  + +2. Configure Events and Callbacks: + * Navigate to the Events & Callbacks tab. + * Set the `Mode of event subscription` to `Receive events through persistent connection`. + * Add the `application.bot.menu_v6` event ID to your Event Configuration. +  + +3. Release the Bot Version: Once the above settings are configured, proceed to release your bot version. + +#### Website Configuration + +Set the following parameters based on your requirements: + +* Brand: Choose between Lark and Feishu as they are separate brands. +* App ID: Your bot's App ID, typically formatted as `cli_xxx`. +* App Secret: Your bot's secret key. +* Verification Token: (Optional) `Verification token` from the bot `Events & Callbacks` => `Encryption Strategy`. +* Encrypt Key: (Optional) `Encrypt key` from the bot `Events & Callbacks` => `Encryption Strategy`. + +### For User + +1. Interact with the Bot: + * Click on the bot menu in the chat interface to trigger interactions. + * Upon interaction, you will receive your open ID from the bot. +2. User Settings: + * Enter your Open ID in the user settings. + * Select which notifications you wish to receive under Notifications Choices. diff --git a/notification-lark/README_CN.md b/notification-lark/README_CN.md new file mode 100644 index 0000000..34a3b93 --- /dev/null +++ b/notification-lark/README_CN.md @@ -0,0 +1,49 @@ +# NotificationLark Plugin + +[English](./README.md) | [中文](./README_CN.md) + +## 如何使用 + +要在应用程序中使用飞书通知插件,请使用以下命令进行安装: + +```bash +./answer build --with github.com/apache/incubator-answer-plugins/notification-lark +``` + +## 如何配置 + +### 对于管理员 + +#### 创建机器人 + +1. 在 Lark 或飞书中创建机器人: + * 访问 [Lark](https://open.larksuite.com) 或 [飞书](https://open.feishu.cn) 创建一个新的机器人。 + * 在机器人设置中,启用自定义机器人菜单,并将操作类型设置为 `推送事件`。 + * 使用`10001`作为配置菜单事件代码。 +  +2. 配置事件和回调: + * 导航到事件和回调选项卡。 + * 设置 `配置订阅方式` 为 `使用长连接接收事件`。 + * 将 `application.bot.menu_v6` 事件ID添加到您的事件配置中。 +  + +3. 发布机器人版本:配置完上述设置后,发布机器人版本。 + +#### 网站配置 + +根据您的需求设置以下参数: + +* 品牌:选择 `Lark` 或 `飞书` 作为通知渠道。 +* App ID:您的机器人AppID,通常格式为 `cli_xxx`。 +* App Secret:您的机器人密钥。 +* Verification Token:(可选)机器人 `事件与回调` => `加密策略` 中的 `Verification Token`。 +* Encrypt Key:(可选)机器人 `事件与回调` => `加密策略` 中的 `Encrypt Key`。 + +### 对于用户 + +1. 与机器人互动: + * 在聊天界面中点击机器人菜单以触发互动。 + * 互动后,您将从机器人收到您的OpenID。 +2. 用户设置: + * 在用户设置中输入您的`OpenID`。 + * 在通知选项下选择您希望接收的通知。 diff --git a/notification-lark/config.go b/notification-lark/config.go new file mode 100644 index 0000000..b1646d0 --- /dev/null +++ b/notification-lark/config.go @@ -0,0 +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 lark + +import ( + "context" + "encoding/json" + + "github.com/apache/incubator-answer-plugins/notification-lark/i18n" + "github.com/apache/incubator-answer/plugin" + lark "github.com/larksuite/oapi-sdk-go/v3" + larkCore "github.com/larksuite/oapi-sdk-go/v3/core" + larkWebSocket "github.com/larksuite/oapi-sdk-go/v3/ws" + "github.com/segmentfault/pacman/log" +) + +type NotificationConfig struct { + Version string `json:"version"` + AppID string `json:"app_id"` + AppSecret string `json:"app_secret"` + VerificationToken string `json:"verification_token"` + EventEncryptKey string `json:"event_encrypt_key"` +} + +func (n *NotificationConfig) GetVersion() string { + if n == nil { + return "" + } + + return n.Version +} + +func (n *NotificationConfig) GetAppID() string { + if n == nil { + return "" + } + + return n.AppID +} + +func (n *NotificationConfig) GetAppSecret() string { + if n == nil { + return "" + } + + return n.AppSecret +} + +func (n *NotificationConfig) GetVerificationToken() string { + if n == nil { + return "" + } + + return n.VerificationToken +} + +func (n *NotificationConfig) GetEventEncryptKey() string { + if n == nil { + return "" + } + + return n.EventEncryptKey +} + +func (n *Notification) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + { + Name: "version", + Type: plugin.ConfigTypeSelect, + Title: plugin.MakeTranslator(i18n.ConfigVersionTitle), + Description: plugin.MakeTranslator(i18n.ConfigVersionDescription), + Required: true, + Value: n.config.GetVersion(), + Options: []plugin.ConfigFieldOption{ + { + Label: plugin.MakeTranslator(i18n.ConfigVersionOptionsFeishu), + Value: i18n.ConfigVersionOptionsFeishu, + }, + { + Label: plugin.MakeTranslator(i18n.ConfigVersionOptionsLark), + Value: i18n.ConfigVersionOptionsLark, + }, + }, + }, + { + Name: "app_id", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigAppIdTitle), + Description: plugin.MakeTranslator(i18n.ConfigAppIdDescription), + Required: true, + Value: n.config.GetAppID(), + }, + { + Name: "app_secret", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigAppSecretTitle), + Description: plugin.MakeTranslator(i18n.ConfigAppSecretDescription), + Required: true, + Value: n.config.GetAppSecret(), + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + }, + }, + { + Name: "event_encrypt_key", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigEventEncryptKeyTitle), + Description: plugin.MakeTranslator(i18n.ConfigEventEncryptKeyDescription), + Required: false, + Value: n.config.GetEventEncryptKey(), + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + }, + }, + { + Name: "verification_token", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.ConfigVerificationTokenTitle), + Description: plugin.MakeTranslator(i18n.ConfigVerificationTokenDescription), + Required: false, + Value: n.config.GetVerificationToken(), + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + }, + }, + } +} + +type LarkLogger struct{} + +func (l LarkLogger) Debug(ctx context.Context, args ...interface{}) { + log.Debug(args...) +} + +func (l LarkLogger) Info(ctx context.Context, args ...interface{}) { + log.Info(args...) +} + +func (l LarkLogger) Warn(ctx context.Context, args ...interface{}) { + log.Warn(args...) +} + +func (l LarkLogger) Error(ctx context.Context, args ...interface{}) { + log.Error(args...) +} + +func (n *Notification) ConfigReceiver(config []byte) error { + c := &NotificationConfig{} + if err := json.Unmarshal(config, c); err != nil { + return err + } + + n.config = c + + larkDomain := lark.FeishuBaseUrl + if c.Version == i18n.ConfigVersionOptionsLark { + larkDomain = lark.LarkBaseUrl + } + + n.client = &LarkClient{ + ws: larkWebSocket.NewClient( + n.config.GetAppID(), + n.config.GetAppSecret(), + larkWebSocket.WithDomain(larkDomain), + larkWebSocket.WithLogger(LarkLogger{}), + larkWebSocket.WithLogLevel(larkCore.LogLevelDebug), + larkWebSocket.WithEventHandler(n.LarkWsEventHub()), + ), + http: lark.NewClient(n.config.GetAppID(), n.config.GetAppSecret()), + } + go n.client.Start() + return nil +} diff --git a/notification-lark/docs/1.png b/notification-lark/docs/1.png new file mode 100644 index 0000000..6ce2682 Binary files /dev/null and b/notification-lark/docs/1.png differ diff --git a/notification-lark/docs/2.png b/notification-lark/docs/2.png new file mode 100644 index 0000000..e930058 Binary files /dev/null and b/notification-lark/docs/2.png differ diff --git a/notification-lark/docs/3.png b/notification-lark/docs/3.png new file mode 100644 index 0000000..0a8fbed Binary files /dev/null and b/notification-lark/docs/3.png differ diff --git a/notification-lark/docs/4.png b/notification-lark/docs/4.png new file mode 100644 index 0000000..af07773 Binary files /dev/null and b/notification-lark/docs/4.png differ diff --git a/notification-lark/go.mod b/notification-lark/go.mod new file mode 100644 index 0000000..2d94c09 --- /dev/null +++ b/notification-lark/go.mod @@ -0,0 +1,51 @@ +module github.com/apache/incubator-answer-plugins/notification-lark + +go 1.18 + +require ( + github.com/apache/incubator-answer v1.3.6 + github.com/apache/incubator-answer-plugins/util v1.0.2 + github.com/larksuite/oapi-sdk-go/v3 v3.3.1 + 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.1 // 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/gin-gonic/gin v1.10.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.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/wire v0.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/websocket v1.5.3 // 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.27 // 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.2 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.9.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/notification-lark/go.sum b/notification-lark/go.sum new file mode 100644 index 0000000..6d151d2 --- /dev/null +++ b/notification-lark/go.sum @@ -0,0 +1,194 @@ +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.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24= +github.com/bytedance/sonic v1.12.1/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/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.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc= +github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= +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.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +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.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k= +golang.org/x/arch v0.9.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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/notification-lark/i18n/en_US.yaml b/notification-lark/i18n/en_US.yaml new file mode 100644 index 0000000..3153dd3 --- /dev/null +++ b/notification-lark/i18n/en_US.yaml @@ -0,0 +1,104 @@ +# 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: + notification_lark: + backend: + info: + name: + other: Lark Notification + description: + other: Send notifications to Lark + config: + version: + title: + other: Brand + description: + other: Feishu or Lark + options: + feishu: + other: Feishu + lark: + other: Lark + app_id: + title: + other: App ID + description: + other: Feishu App ID + app_secret: + title: + other: App Secret + description: + other: Feishu App Secret + verification_token: + title: + other: Verification Token + description: + other: Verification Token of the Lark bot + event_encrypt_key: + title: + other: Encrypt Key + description: + other: Event Encrypt Key of the Lark bot + user_config: + open_id: + title: + other: User Open ID + description: + other: User's Open ID in the app, can be obtained through Lark bot + inbox_notifications: + title: + other: Inbox Notifications + label: + other: Enable Inbox Notifications + description: + other: Answers, comments, invitations, etc. + all_new_questions: + title: + other: All New Questions Notifications + label: + other: Enable All New Questions Notifications + description: + other: Receive notifications for all new questions. Up to 50 questions per week. + new_questions_for_following_tags: + title: + other: New Questions for Following Tags Notifications + label: + other: Enable New Questions for Following Tags Notifications + description: + other: Receive notifications for new questions in the following tags. + tpl: + update_question: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) updated question [{{.QuestionTitle}}]({{.QuestionUrl}})" + answer_the_question: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) answered the question [{{.QuestionTitle}}]({{.AnswerUrl}})" + update_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) updated answer [{{.QuestionTitle}}]({{.AnswerUrl}})" + accept_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) accepted answer [{{.QuestionTitle}}]({{.AnswerUrl}})" + comment_question: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) commented question [{{.QuestionTitle}}]({{.CommentUrl}})" + comment_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) commented answer [{{.QuestionTitle}}]({{.CommentUrl}})" + reply_to_you: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) replied you [{{.QuestionTitle}}]({{.CommentUrl}})" + mention_you: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) mentioned you [{{.QuestionTitle}}]({{.CommentUrl}})" + invited_you_to_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) invited you to answer [{{.QuestionTitle}}]({{.QuestionUrl}})" + new_question: + other: "New question:\n[{{.QuestionTitle}}]({{.QuestionUrl}}) {{.QuestionTags}}" diff --git a/notification-lark/i18n/translation.go b/notification-lark/i18n/translation.go new file mode 100644 index 0000000..1e76b0b --- /dev/null +++ b/notification-lark/i18n/translation.go @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package i18n + +const ( + InfoName = "plugin.notification_lark.backend.info.name" + InfoDescription = "plugin.notification_lark.backend.info.description" + + ConfigVersionTitle = "plugin.notification_lark.backend.config.version.title" + ConfigVersionDescription = "plugin.notification_lark.backend.config.version.description" + ConfigVersionOptionsFeishu = "plugin.notification_lark.backend.config.version.options.feishu" + ConfigVersionOptionsLark = "plugin.notification_lark.backend.config.version.options.lark" + ConfigAppIdTitle = "plugin.notification_lark.backend.config.app_id.title" + ConfigAppIdDescription = "plugin.notification_lark.backend.config.app_id.description" + ConfigAppSecretTitle = "plugin.notification_lark.backend.config.app_secret.title" + ConfigAppSecretDescription = "plugin.notification_lark.backend.config.app_secret.description" + ConfigVerificationTokenTitle = "plugin.notification_lark.backend.config.verification_token.title" + ConfigVerificationTokenDescription = "plugin.notification_lark.backend.config.verification_token.description" + ConfigEventEncryptKeyTitle = "plugin.notification_lark.backend.config.event_encrypt_key.title" + ConfigEventEncryptKeyDescription = "plugin.notification_lark.backend.config.event_encrypt_key.description" + + UserConfigOpenIdTitle = "plugin.notification_lark.backend.user_config.open_id.title" + UserConfigOpenIdDescription = "plugin.notification_lark.backend.user_config.open_id.description" + UserConfigInboxNotificationsTitle = "plugin.notification_lark.backend.user_config.inbox_notifications.title" + UserConfigInboxNotificationsLabel = "plugin.notification_lark.backend.user_config.inbox_notifications.label" + UserConfigInboxNotificationsDescription = "plugin.notification_lark.backend.user_config.inbox_notifications.description" + UserConfigAllNewQuestionsTitle = "plugin.notification_lark.backend.user_config.all_new_questions.title" + UserConfigAllNewQuestionsLabel = "plugin.notification_lark.backend.user_config.all_new_questions.label" + UserConfigAllNewQuestionsDescription = "plugin.notification_lark.backend.user_config.all_new_questions.description" + UserConfigNewQuestionsForFollowingTagsTitle = "plugin.notification_lark.backend.user_config.new_questions_for_following_tags.title" + UserConfigNewQuestionsForFollowingTagsLabel = "plugin.notification_lark.backend.user_config.new_questions_for_following_tags.label" + UserConfigNewQuestionsForFollowingTagsDescription = "plugin.notification_lark.backend.user_config.new_questions_for_following_tags.description" + + TplUpdateQuestion = "plugin.notification_lark.backend.tpl.update_question" + TplAnswerTheQuestion = "plugin.notification_lark.backend.tpl.answer_the_question" + TplUpdateAnswer = "plugin.notification_lark.backend.tpl.update_answer" + TplAcceptAnswer = "plugin.notification_lark.backend.tpl.accept_answer" + TplCommentQuestion = "plugin.notification_lark.backend.tpl.comment_question" + TplCommentAnswer = "plugin.notification_lark.backend.tpl.comment_answer" + TplReplyToYou = "plugin.notification_lark.backend.tpl.reply_to_you" + TplMentionYou = "plugin.notification_lark.backend.tpl.mention_you" + TplInvitedYouToAnswer = "plugin.notification_lark.backend.tpl.invited_you_to_answer" + TplNewQuestion = "plugin.notification_lark.backend.tpl.new_question" +) diff --git a/notification-lark/i18n/zh_CN.yaml b/notification-lark/i18n/zh_CN.yaml new file mode 100644 index 0000000..2a1cad3 --- /dev/null +++ b/notification-lark/i18n/zh_CN.yaml @@ -0,0 +1,104 @@ +# 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: + notification_lark: + backend: + info: + name: + other: 飞书通知 + description: + other: 发送通知到飞书 + config: + version: + title: + other: 品牌 + description: + other: 飞书或者Lark + options: + feishu: + other: 飞书 + lark: + other: Lark + app_id: + title: + other: App ID + description: + other: 飞书 App ID + app_secret: + title: + other: App Secret + description: + other: 飞书 App Secret + verification_token: + title: + other: Verification Token + description: + other: 飞书机器人的验证 Token + event_encrypt_key: + title: + other: Encrypt Key + description: + other: 飞书机器人的事件加密 Key + user_config: + open_id: + title: + other: 用户 Open ID + description: + other: 用户在应用中的 Open ID,可以通过飞书机器人获取 + 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}}]({{.TriggerUserUrl}}) 更新问题 [{{.QuestionTitle}}]({{.QuestionUrl}})" + answer_the_question: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 回答了问题 [{{.QuestionTitle}}]({{.AnswerUrl}})" + update_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 更新答案 [{{.QuestionTitle}}]({{.AnswerUrl}})" + accept_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 接受答案 [{{.QuestionTitle}}]({{.AnswerUrl}})" + comment_question: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 评论提问 [{{.QuestionTitle}}]({{.CommentUrl}})" + comment_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 评论回答 [{{.QuestionTitle}}]({{.CommentUrl}})" + reply_to_you: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 回复了问题 [{{.QuestionTitle}}]({{.CommentUrl}})" + mention_you: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 提到了你 [{{.QuestionTitle}}]({{.CommentUrl}})" + invited_you_to_answer: + other: "[@{{.TriggerUserDisplayName}}]({{.TriggerUserUrl}}) 邀请你回答 [{{.QuestionTitle}}]({{.QuestionUrl}})" + new_question: + other: "新问题:\n[{{.QuestionTitle}}]({{.QuestionUrl}}) {{.QuestionTags}}" diff --git a/notification-lark/info.yaml b/notification-lark/info.yaml new file mode 100644 index 0000000..2f370d5 --- /dev/null +++ b/notification-lark/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: lark_notification +type: notification +version: 1.0.0 +author: sonui +link: https://apache.com/apache/incubator-answer-plugins/tree/main/notification-lark diff --git a/notification-lark/lark_card.go b/notification-lark/lark_card.go new file mode 100644 index 0000000..43ec5e0 --- /dev/null +++ b/notification-lark/lark_card.go @@ -0,0 +1,268 @@ +/* + * 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 lark + +import "encoding/json" + +// CardLink represents the URLs for different platforms. +type CardLink struct { + URL string `json:"url,omitempty"` + PCURL string `json:"pc_url,omitempty"` + IOSURL string `json:"ios_url,omitempty"` + AndroidURL string `json:"android_url,omitempty"` +} + +// Text represents a text component with optional internationalization. +type Text struct { + Tag string `json:"tag,omitempty"` + Content string `json:"content,omitempty"` + I18n *I18n `json:"i18n,omitempty"` + TextSize string `json:"text_size,omitempty"` + TextAlign string `json:"text_align,omitempty"` + TextColor string `json:"text_color,omitempty"` + Icon *Icon `json:"icon,omitempty"` +} + +// I18n represents internationalized text. +type I18n struct { + ZhCn string `json:"zh_cn,omitempty"` + EnUs string `json:"en_us,omitempty"` +} + +// TextTag represents a text tag component. +type TextTag struct { + Tag string `json:"tag,omitempty"` + Text *Text `json:"text,omitempty"` + Color string `json:"color,omitempty"` +} + +// MarshalJSON customizes the JSON encoding for TextTag. +func (tt *TextTag) MarshalJSON() ([]byte, error) { + tt.Tag = "text_tag" + return json.Marshal(*tt) +} + +// Icon represents an icon component. +type Icon struct { + Tag string `json:"tag,omitempty"` + Token string `json:"token,omitempty"` + Color string `json:"color,omitempty"` + ImgKey string `json:"img_key,omitempty"` + Style *struct { + Color Color `json:"color,omitempty"` + } `json:"style,omitempty"` +} + +// Header represents the header component of a card. +type Header struct { + Title *Text `json:"title,omitempty"` + Subtitle *Text `json:"subtitle,omitempty"` + TextTagList []TextTag `json:"text_tag_list,omitempty"` + Template Template `json:"template,omitempty"` + UdIcon *Icon `json:"ud_icon,omitempty"` +} + +// Element represents a generic element in a card. +type Element struct { + *PlainText + *Button +} + +// MarshalJSON customizes the JSON encoding for Element. +func (e *Element) MarshalJSON() ([]byte, error) { + if e.PlainText != nil { + return json.Marshal(e.PlainText) + } + return json.Marshal(e.Button) +} + +// Column represents a column in a ColumnSet. +type Column struct { + Tag string `json:"tag,omitempty"` + Elements []Element `json:"elements,omitempty"` + Width string `json:"width,omitempty"` + Weight int `json:"weight,omitempty"` + BackgroundStyle string `json:"background_style,omitempty"` + VerticalAlign string `json:"vertical_align,omitempty"` + VerticalSpacing string `json:"vertical_spacing,omitempty"` + Padding string `json:"padding,omitempty"` +} + +// MarshalJSON customizes the JSON encoding for Column. +func (c *Column) MarshalJSON() ([]byte, error) { + c.Tag = "column" + return json.Marshal(*c) +} + +// ColumnSet represents a set of columns. +type ColumnSet struct { + *Show + *Action +} + +// MarshalJSON customizes the JSON encoding for ColumnSet. +func (cs *ColumnSet) MarshalJSON() ([]byte, error) { + if cs.Show != nil { + cs.Show.Tag = "column_set" + return json.Marshal(*cs.Show) + } + if cs.Action != nil { + cs.Action.Tag = "action" + return json.Marshal(*cs.Action) + } + return nil, nil +} + +// Show represents the display properties of a ColumnSet. +type Show struct { + Tag string `json:"tag"` + FlexMode string `json:"flex_mode,omitempty"` + HorizontalSpacing string `json:"horizontal_spacing,omitempty"` + BackgroundStyle string `json:"background_style,omitempty"` + Columns []Column `json:"columns,omitempty"` +} + +// Action represents actions in a ColumnSet. +type Action struct { + Tag string `json:"tag"` + Actions []*Button `json:"actions,omitempty"` +} + +// I18nElements represents internationalized elements. +type I18nElements struct { + ZhCn []ColumnSet `json:"zh_cn,omitempty"` + EnUs []ColumnSet `json:"en_us,omitempty"` +} + +// Behavior represents the behavior of a button. +type Behavior struct { + Type string `json:"type"` + DefaultURL string `json:"default_url"` + AndroidURL string `json:"android_url"` + IOSURL string `json:"ios_url"` + PCURL string `json:"pc_url"` +} + +// Button represents a button component. +type Button struct { + Tag string `json:"tag,omitempty"` + Width string `json:"width,omitempty"` + Text *Text `json:"text,omitempty"` + Behaviors []Behavior `json:"behaviors,omitempty"` + Type string `json:"type,omitempty"` + HoverTips *Text `json:"hover_tips,omitempty"` + Value map[string]any `json:"value,omitempty"` +} + +// MarshalJSON customizes the JSON encoding for Button. +func (b *Button) MarshalJSON() ([]byte, error) { + b.Tag = "button" + return json.Marshal(*b) +} + +// PlainText represents plain text component. +type PlainText struct { + Tag string `json:"tag,omitempty"` + Text *Text `json:"text,omitempty"` + Icon *Icon `json:"icon,omitempty"` +} + +// Summary represents the summary information of a card. +type Summary struct { + Content string `json:"content,omitempty"` + I18nContent map[string]string `json:"i18n_content,omitempty"` +} + +// TextSize represents the custom text size configuration. +type TextSize struct { + Default string `json:"default,omitempty"` + PC string `json:"pc,omitempty"` + Mobile string `json:"mobile,omitempty"` +} + +// ConfigColor represents the custom color configuration. +type ConfigColor struct { + LightMode string `json:"light_mode,omitempty"` + DarkMode string `json:"dark_mode,omitempty"` +} + +// Style represents the custom font size and color configuration. +type Style struct { + TextSize map[string]TextSize `json:"text_size,omitempty"` + Color map[string]ConfigColor `json:"color,omitempty"` +} + +// Config represents the configuration of a card. +type Config struct { + StreamingMode *bool `json:"streaming_mode,omitempty"` + Summary *Summary `json:"summary,omitempty"` + EnableForward *bool `json:"enable_forward,omitempty"` + UpdateMulti *bool `json:"update_multi,omitempty"` + WidthMode string `json:"width_mode,omitempty"` + UseCustomTranslation *bool `json:"use_custom_translation,omitempty"` + EnableForwardInteraction *bool `json:"enable_forward_interaction,omitempty"` + Style Style `json:"style,omitempty"` +} + +// Card represents the entire JSON structure of a card. +type Card struct { + Config *Config `json:"config"` + CardLink *CardLink `json:"card_link,omitempty"` + I18nElements *I18nElements `json:"i18n_elements,omitempty"` + Header *Header `json:"header,omitempty"` +} + +// Template represents theme styles enumeration. +type Template string + +const ( + ThemeBlue Template = "blue" + ThemeWathet Template = "wathet" + ThemeTurquoise Template = "turquoise" + ThemeGreen Template = "green" + ThemeYellow Template = "yellow" + ThemeOrange Template = "orange" + ThemeRed Template = "red" + ThemeCarmine Template = "carmine" + ThemeViolet Template = "violet" + ThemePurple Template = "purple" + ThemeIndigo Template = "indigo" + ThemeGrey Template = "grey" + ThemeDefault Template = "default" +) + +// Color represents color effects enumeration. +type Color string + +const ( + ColorNeutral Color = "neutral" + ColorBlue Color = "blue" + ColorTurquoise Color = "turquoise" + ColorLime Color = "lime" + ColorOrange Color = "orange" + ColorViolet Color = "violet" + ColorIndigo Color = "indigo" + ColorWathet Color = "wathet" + ColorGreen Color = "green" + ColorYellow Color = "yellow" + ColorRed Color = "red" + ColorPurple Color = "purple" + ColorCarmine Color = "carmine" +) diff --git a/notification-lark/lark_card_test.go b/notification-lark/lark_card_test.go new file mode 100644 index 0000000..0c8dd05 --- /dev/null +++ b/notification-lark/lark_card_test.go @@ -0,0 +1,178 @@ +/* + * 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 lark_test + +import ( + "context" + "encoding/json" + "os" + "testing" + + answer "github.com/apache/incubator-answer-plugins/notification-lark" + lark "github.com/larksuite/oapi-sdk-go/v3" + larkIM "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" +) + +func TestLarkCardMessage(t *testing.T) { + appId := os.Getenv("LARK_APP_ID") + appSecret := os.Getenv("LARK_APP_SECRET") + openId := os.Getenv("LARK_OPEN_ID") + if appId == "" || appSecret == "" || openId == "" { + t.Skip("LARK_APP_ID, LARK_APP_SECRET, LARK_OPEN_ID are required") + } + + larkClient := lark.NewClient(appId, appSecret) + + contentData, _ := json.Marshal(answer.Card{ + Config: &answer.Config{ + WidthMode: "compact", + UseCustomTranslation: answer.PtrBool(true), + EnableForward: answer.PtrBool(false), + }, + Header: &answer.Header{ + Title: &answer.Text{ + Tag: "plain_text", + I18n: &answer.I18n{ + ZhCn: "新通知", + EnUs: "New Notification", + }, + }, + UdIcon: &answer.Icon{ + Tag: "icon", + Token: "bell_outlined", + Color: "blue", + }, + Template: answer.ThemeGreen, + }, + I18nElements: &answer.I18nElements{ + ZhCn: []answer.ColumnSet{ + { + Show: &answer.Show{ + Tag: "column_set", + FlexMode: "flex_mode", + Columns: []answer.Column{ + { + Elements: []answer.Element{ + { + PlainText: &answer.PlainText{ + Tag: "div", + Text: &answer.Text{ + Tag: "lark_md", + Content: "[@Answer](https://answer.apache.org/) 创建了问题 [如何使用 Answer?](https://answer.apache.org/docs/)", + }, + }, + }, + }, + }, + }, + }, + }, + { + Action: &answer.Action{ + Tag: "action", + Actions: []*answer.Button{ + { + Width: "fill", + Text: &answer.Text{ + Tag: "plain_text", + Content: "查看详情", + Icon: &answer.Icon{ + Tag: "standard_icon", + Token: "link-copy_outlined", + }, + }, + Behaviors: []answer.Behavior{ + { + Type: "open_url", + DefaultURL: "https://answer.apache.org/docs/", + }, + }, + }, + }, + }, + }, + }, + EnUs: []answer.ColumnSet{ + { + Show: &answer.Show{ + FlexMode: "flex_mode", + Columns: []answer.Column{ + { + Elements: []answer.Element{ + { + PlainText: &answer.PlainText{ + Tag: "div", + Text: &answer.Text{ + Tag: "lark_md", + Content: "[@Answer](https://answer.apache.org/) created a question [How to use Answer?](https://answer.apache.org/docs/)", + }, + }, + }, + }, + }, + }, + }, + }, + { + Action: &answer.Action{ + Tag: "action", + Actions: []*answer.Button{ + { + Width: "fill", + Text: &answer.Text{ + Tag: "plain_text", + Content: "View details", + Icon: &answer.Icon{ + Tag: "standard_icon", + Token: "link-copy_outlined", + }, + }, + Behaviors: []answer.Behavior{ + { + Type: "open_url", + DefaultURL: "https://answer.apache.org/docs/", + }, + }, + }, + }, + }, + }, + }, + }, + }) + + t.Logf("Content: %s", string(contentData)) + req := larkIM.NewCreateMessageReqBuilder(). + ReceiveIdType("open_id"). + Body(larkIM.NewCreateMessageReqBodyBuilder(). + ReceiveId(openId). + MsgType("interactive"). + Content(string(contentData)). + Build()). + Build() + + resp, err := larkClient.Im.Message.Create(context.Background(), req) + if err != nil { + t.Errorf("Failed to send message: %v", err) + } + if !resp.Success() { + t.Errorf("Failed to send message: %v", resp.Error()) + } +} diff --git a/notification-lark/notification.go b/notification-lark/notification.go new file mode 100644 index 0000000..c3fd84b --- /dev/null +++ b/notification-lark/notification.go @@ -0,0 +1,329 @@ +/* + * 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 lark + +import ( + "context" + "embed" + "encoding/json" + "fmt" + "strings" + + lark_i18n "github.com/apache/incubator-answer-plugins/notification-lark/i18n" + "github.com/apache/incubator-answer-plugins/util" + "github.com/apache/incubator-answer/plugin" + + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" + + lark "github.com/larksuite/oapi-sdk-go/v3" + "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher" + larkApplication "github.com/larksuite/oapi-sdk-go/v3/service/application/v6" + larkIM "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" + larkWebSocket "github.com/larksuite/oapi-sdk-go/v3/ws" +) + +//go:embed info.yaml +var Info embed.FS + +type LarkClient struct { + ws *larkWebSocket.Client + http *lark.Client +} + +type Notification struct { + info *util.Info + config *NotificationConfig + client *LarkClient + userConfigCache *UserConfigCache +} + +const ( + LarkBindAccountMenuEventKey = "10001" + NotificationTypeInteractive = "interactive" + MsgTypeText = "text" + ReceiveIdTypeOpenId = "open_id" +) + +var ( + TagColor = []string{"neutral", "blue", "turquoise", "lime", "orange", "violet", "indigo", "wathet", "green", "yellow", "red", "purple", "carmine"} +) + +func init() { + plugin.Register(&Notification{ + userConfigCache: NewUserConfigCache(), + }) +} + +func (n Notification) Info() plugin.Info { + if n.info == nil { + info := &util.Info{} + info.GetInfo(Info) + n.info = info + } + + return plugin.Info{ + Name: plugin.MakeTranslator("NotificationLark"), + SlugName: n.info.SlugName, + Description: plugin.MakeTranslator(""), + Author: n.info.Author, + Version: n.info.Version, + Link: n.info.Link, + } +} + +func renderTag(tags []string) string { + + var builder strings.Builder + for _, tag := range tags { + idx := RandomInt(0, int64(len(TagColor))) + builder.WriteString(fmt.Sprintf(`<text_tag color='%s'> %s </text_tag>`, TagColor[idx], tag)) + } + return builder.String() +} + +func renderNotification(msg plugin.NotificationMessage) string { + lang := i18n.Language(msg.ReceiverLang) + switch msg.Type { + case plugin.NotificationUpdateQuestion: + return plugin.TranslateWithData(lang, lark_i18n.TplUpdateQuestion, msg) + case plugin.NotificationAnswerTheQuestion: + return plugin.TranslateWithData(lang, lark_i18n.TplAnswerTheQuestion, msg) + case plugin.NotificationUpdateAnswer: + return plugin.TranslateWithData(lang, lark_i18n.TplUpdateAnswer, msg) + case plugin.NotificationAcceptAnswer: + return plugin.TranslateWithData(lang, lark_i18n.TplAcceptAnswer, msg) + case plugin.NotificationCommentQuestion: + return plugin.TranslateWithData(lang, lark_i18n.TplCommentQuestion, msg) + case plugin.NotificationCommentAnswer: + return plugin.TranslateWithData(lang, lark_i18n.TplCommentAnswer, msg) + case plugin.NotificationReplyToYou: + return plugin.TranslateWithData(lang, lark_i18n.TplReplyToYou, msg) + case plugin.NotificationMentionYou: + return plugin.TranslateWithData(lang, lark_i18n.TplMentionYou, msg) + case plugin.NotificationInvitedYouToAnswer: + return plugin.TranslateWithData(lang, lark_i18n.TplInvitedYouToAnswer, msg) + case plugin.NotificationNewQuestion, plugin.NotificationNewQuestionFollowedTag: + msg.QuestionTags = renderTag(strings.Split(msg.QuestionTags, ",")) + return plugin.TranslateWithData(lang, lark_i18n.TplNewQuestion, msg) + } + return "" +} + +func makeCardMsg(args plugin.NotificationMessage) Card { + action := &Action{ + Tag: "action", + Actions: []*Button{ + { + Width: "fill", + Text: &Text{ + Tag: "plain_text", + Content: "查看详情", + Icon: &Icon{ + Tag: "standard_icon", + Token: "link-copy_outlined", + }, + }, + Behaviors: []Behavior{ + { + Type: "open_url", + DefaultURL: args.QuestionUrl, + }, + }, + }, + }, + } + + columnSet := func(content string) ColumnSet { + return ColumnSet{ + Show: &Show{ + Tag: "column_set", + FlexMode: "flex_mode", + Columns: []Column{ + { + Elements: []Element{ + { + PlainText: &PlainText{ + Tag: "div", + Text: &Text{ + Tag: "lark_md", + Content: content, + }, + }, + }, + }, + }, + }, + }, + Action: action, + } + } + + card := Card{ + Config: &Config{ + WidthMode: "compact", + UseCustomTranslation: PtrBool(true), + EnableForward: PtrBool(false), + }, + Header: &Header{ + Title: &Text{ + Tag: "plain_text", + I18n: &I18n{ + ZhCn: "新通知", + EnUs: "New Notification", + }, + }, + UdIcon: &Icon{ + Tag: "icon", + Token: "bell_outlined", + Color: "blue", + }, + Template: ThemeGreen, + }, + I18nElements: &I18nElements{ + ZhCn: []ColumnSet{}, + EnUs: []ColumnSet{}, + }, + } + + args.ReceiverLang = string(i18n.LanguageChinese) + card.I18nElements.ZhCn = append(card.I18nElements.ZhCn, columnSet(renderNotification(args))) + args.ReceiverLang = string(i18n.LanguageEnglish) + card.I18nElements.EnUs = append(card.I18nElements.EnUs, columnSet(renderNotification(args))) + return card +} + +// 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) { + ctx := context.TODO() + log.Debugf("Attempting to send notification to user %s: %+v", msg.ReceiverUserID, msg) + + // 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 + } + if userConfig.OpenId == "" { + log.Debugf("user %s not set the open id", 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) + + cardMsg := makeCardMsg(msg) + notificationMsg, err := json.Marshal(cardMsg) + if err != nil { + log.Errorf("marshal notification message failed: %v", err) + return + } + log.Debugf("card message: %s", notificationMsg) + + if len(notificationMsg) == 0 { + log.Debugf("this type of notification will be drop, the type is %s", msg.Type) + return + } + req := larkIM.NewCreateMessageReqBuilder(). + ReceiveIdType(ReceiveIdTypeOpenId). + Body(larkIM.NewCreateMessageReqBodyBuilder(). + ReceiveId(userConfig.OpenId). + MsgType(NotificationTypeInteractive). + Content(string(notificationMsg)). + Build()). + Build() + + resp, err := n.client.http.Im.Message.Create(ctx, req) + if err != nil || !resp.Success() { + log.Errorf("Failed to send message to user %s: %v", userConfig.OpenId, err) + } +} + +// LarkWsEventMenuClick is the event handler for the menu click event +func (n *Notification) LarkWsEventMenuClick(ctx context.Context, event *larkApplication.P2BotMenuV6) error { + switch *event.Event.EventKey { + case LarkBindAccountMenuEventKey: + contentData, _ := json.Marshal(map[string]interface{}{ + "text": *event.Event.Operator.OperatorId.OpenId, + }) + + req := larkIM.NewCreateMessageReqBuilder(). + ReceiveIdType(ReceiveIdTypeOpenId). + Body(larkIM.NewCreateMessageReqBodyBuilder(). + ReceiveId(*event.Event.Operator.OperatorId.OpenId). + MsgType(MsgTypeText). + Content(string(contentData)). + Build()). + Build() + + resp, err := n.client.http.Im.Message.Create(context.Background(), req) + if err != nil || !resp.Success() { + fmt.Printf("Failed to send message: %v\n", err) + return nil + } + } + + return nil +} + +func (n *Notification) LarkWsEventHub() *dispatcher.EventDispatcher { + return dispatcher.NewEventDispatcher(n.config.VerificationToken, n.config.EventEncryptKey). + OnP2BotMenuV6(n.LarkWsEventMenuClick) +} + +func (n *LarkClient) Start() error { + // TODO: wait feishu sdk fix the cancel not work issue + // https://github.com/larksuite/oapi-sdk-go/issues/141 + // ctx, cancel := context.WithCancel(context.TODO()) + n.ws.Start(context.TODO()) + return nil +} diff --git a/notification-lark/user_config.go b/notification-lark/user_config.go new file mode 100644 index 0000000..62d4ca0 --- /dev/null +++ b/notification-lark/user_config.go @@ -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. + */ + +package lark + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/apache/incubator-answer-plugins/notification-lark/i18n" + "github.com/apache/incubator-answer/plugin" + + "github.com/segmentfault/pacman/log" +) + +type UserConfig struct { + OpenId string `json:"open_id"` + InboxNotifications bool `json:"inbox_notifications"` + AllNewQuestions bool `json:"all_new_questions"` + NewQuestionsForFollowingTags bool `json:"new_questions_for_following_tags"` +} + +type UserConfigCache struct { + 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 { + return []plugin.ConfigField{ + { + Name: "open_id", + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(i18n.UserConfigOpenIdTitle), + Description: plugin.MakeTranslator(i18n.UserConfigOpenIdDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + }, + { + Name: "inbox_notifications", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.UserConfigInboxNotificationsTitle), + Description: plugin.MakeTranslator(i18n.UserConfigInboxNotificationsDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.UserConfigInboxNotificationsLabel), + }, + }, + { + Name: "all_new_questions", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.UserConfigAllNewQuestionsTitle), + Description: plugin.MakeTranslator(i18n.UserConfigAllNewQuestionsDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.UserConfigAllNewQuestionsLabel), + }, + }, + { + Name: "new_questions_for_following_tags", + Type: plugin.ConfigTypeSwitch, + Title: plugin.MakeTranslator(i18n.UserConfigNewQuestionsForFollowingTagsTitle), + Description: plugin.MakeTranslator(i18n.UserConfigNewQuestionsForFollowingTagsDescription), + UIOptions: plugin.ConfigFieldUIOptions{ + Label: plugin.MakeTranslator(i18n.UserConfigNewQuestionsForFollowingTagsLabel), + }, + }, + } +} + +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/notification-lark/utils.go b/notification-lark/utils.go new file mode 100644 index 0000000..55c29c4 --- /dev/null +++ b/notification-lark/utils.go @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package lark + +import ( + "crypto/rand" + "math/big" +) + +type GenerateRandomStringArgs struct { + Length uint64 + StringPool string +} + +// GenerateRandomString use crypto to generate a random string +func GenerateRandomString(args *GenerateRandomStringArgs) string { + // check args + if args.Length <= 0 || args.StringPool == "" { + return "" + } + + // generate random string + b := make([]byte, args.Length) + for i := uint64(0); i < args.Length; i++ { + idx := RandomInt(0, int64(len(args.StringPool))) + b[i] = args.StringPool[idx] + } + + return string(b) +} + +func RandomInt(min, max int64) int64 { + result, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min))) + return result.Int64() + min +} + +func PtrBool(b bool) *bool { + return &b +} diff --git a/notification-lark/utils_test.go b/notification-lark/utils_test.go new file mode 100644 index 0000000..c0ac618 --- /dev/null +++ b/notification-lark/utils_test.go @@ -0,0 +1,43 @@ +/* + * 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 lark_test + +import ( + "testing" + + answer "github.com/apache/incubator-answer-plugins/notification-lark" +) + +func TestGenRandomStr(t *testing.T) { + rs := answer.GenerateRandomString(&answer.GenerateRandomStringArgs{ + Length: 10, + StringPool: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + }) + + t.Logf("Random string: %s", rs) +} + +func TestPtrBool(t *testing.T) { + b := true + bp := answer.PtrBool(b) + if *bp != b { + t.Errorf("PtrBool failed") + } +}
