You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by li...@apache.org on 2024/08/15 08:19:06 UTC
(incubator-answer-plugins) 01/04: feat: add support for Lark notifications
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 <me...@sonui.cn>
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")
+ }
+}