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.
+    ![bot menu set](./docs/1.png)
+
+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.
+    ![event config](./docs/2.png)
+
+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`作为配置菜单事件代码。
+     ![bot menu set](./docs/3.png)
+2. 配置事件和回调:
+    * 导航到事件和回调选项卡。
+    * 设置 `配置订阅方式` 为 `使用长连接接收事件`。
+    * 将 `application.bot.menu_v6` 事件ID添加到您的事件配置中。
+     ![event config](./docs/4.png)
+
+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")
+       }
+}

Reply via email to