This is an automated email from the ASF dual-hosted git repository.

linkinstar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-answer-plugins.git

commit 4cb41d874a7bfb67e6f9ae7daea6e9549e887e3f
Author: LinkinStars <[email protected]>
AuthorDate: Thu Jan 18 12:03:21 2024 +0800

    feat(wecom): add wecom plugin
---
 user-center-wecom/README.md             |  20 +++
 user-center-wecom/company.go            | 235 ++++++++++++++++++++++++++
 user-center-wecom/config.go             | 122 +++++++++++++
 user-center-wecom/cron.go               |  20 +++
 user-center-wecom/docs/wecom-config.png | Bin 0 -> 46290 bytes
 user-center-wecom/docs/wecom-login.png  | Bin 0 -> 12314 bytes
 user-center-wecom/docs/wecom-qrcode.png | Bin 0 -> 25478 bytes
 user-center-wecom/go.mod                |  69 ++++++++
 user-center-wecom/go.sum                | 291 ++++++++++++++++++++++++++++++++
 user-center-wecom/handler.go            |  96 +++++++++++
 user-center-wecom/i18n/en_US.yaml       | 101 +++++++++++
 user-center-wecom/i18n/translation.go   |  49 ++++++
 user-center-wecom/i18n/zh_CN.yaml       | 101 +++++++++++
 user-center-wecom/notification.go       | 103 +++++++++++
 user-center-wecom/schema.go             |  59 +++++++
 user-center-wecom/user_config.go        |  94 +++++++++++
 user-center-wecom/wecom_user_center.go  | 242 ++++++++++++++++++++++++++
 17 files changed, 1602 insertions(+)

diff --git a/user-center-wecom/README.md b/user-center-wecom/README.md
new file mode 100644
index 0000000..bfaeb2e
--- /dev/null
+++ b/user-center-wecom/README.md
@@ -0,0 +1,20 @@
+# WeCom User Center
+## Feature
+- User login via WeCom QRCode
+
+## Config
+> You need to create a WeCom App first, and then get the `Corp ID`, `Agent ID` 
and `App Secret` from the App. 
+
+- `Company ID`: WeCom Corp ID
+- `App Secret`: WeCom App Secret
+- `App Agent ID`: WeCom App Agent ID
+
+Note: WeCom restricts the ip address of the callback url, so you need to add 
the ip address of the server where the project is located to the callback url.
+
+## Preview
+![WeCom Config](./docs/wecom-config.png)
+![WeCom QRCode](./docs/wecom-qrcode.png)
+![WeCom Login](./docs/wecom-login.png)
+
+## Document
+- https://developer.work.weixin.qq.com/document/path/90664
\ No newline at end of file
diff --git a/user-center-wecom/company.go b/user-center-wecom/company.go
new file mode 100644
index 0000000..f953276
--- /dev/null
+++ b/user-center-wecom/company.go
@@ -0,0 +1,235 @@
+package wecom
+
+import (
+       "encoding/json"
+       "fmt"
+       "strings"
+
+       "github.com/imroc/req/v3"
+       "github.com/segmentfault/pacman/log"
+       "github.com/silenceper/wechat/v2/cache"
+       "github.com/silenceper/wechat/v2/work"
+       workConfig "github.com/silenceper/wechat/v2/work/config"
+       "github.com/tidwall/gjson"
+)
+
+type Company struct {
+       CorpID      string
+       CorpSecret  string
+       AgentID     string
+       CallbackURL string
+
+       Work                  *work.Work
+       DepartmentMapping     map[int]*Department
+       EmployeeMapping       map[string]*Employee
+       UserDetailInfoMapping map[string]*UserDetailInfo
+}
+
+func NewCompany(corpID, corpSecret, agentID string) *Company {
+       memory := cache.NewMemory()
+       cfg := &workConfig.Config{
+               CorpID:     corpID,
+               CorpSecret: corpSecret,
+               AgentID:    agentID,
+               Cache:      memory,
+       }
+       newWork := work.NewWork(cfg)
+       return &Company{
+               CorpID:                corpID,
+               CorpSecret:            corpSecret,
+               AgentID:               agentID,
+               Work:                  newWork,
+               DepartmentMapping:     make(map[int]*Department),
+               EmployeeMapping:       make(map[string]*Employee),
+               UserDetailInfoMapping: make(map[string]*UserDetailInfo),
+       }
+}
+
+func (c *Company) ListDepartmentAll() (err error) {
+       log.Debugf("try to list department all")
+       token, err := c.Work.GetOauth().GetAccessToken()
+       if err != nil {
+               return fmt.Errorf("get access token failed: %w", err)
+       }
+       log.Debugf("get access token success")
+
+       r, err := 
req.Get("https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token="; + 
token)
+       if err != nil {
+               return fmt.Errorf("get department list failed: %w", err)
+       }
+
+       log.Debugf("get department success: %s", r.String())
+
+       department := gjson.Get(r.String(), "department").String()
+       var departments []*Department
+       err = json.Unmarshal([]byte(department), &departments)
+       if err != nil {
+               return fmt.Errorf("unmarshal department failed: %w", err)
+       }
+
+       departmentMapping := make(map[int]*Department)
+       for _, d := range departments {
+               departmentMapping[d.Id] = d
+       }
+       c.DepartmentMapping = departmentMapping
+       log.Debugf("get department list: %d", len(departments))
+       return nil
+}
+
+func (c *Company) ListUser() (err error) {
+       token, err := c.Work.GetOauth().GetAccessToken()
+       if err != nil {
+               return fmt.Errorf("get access token failed: %w", err)
+       }
+       log.Debugf("get access token success")
+
+       for _, department := range c.DepartmentMapping {
+               log.Debugf("try to get department user list: %d %s", 
department.Id, department.Name)
+               resp, err := 
req.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?department_id=%d&access_token=%s";,
+                       department.Id, token))
+               if err != nil {
+                       log.Errorf("get department user list failed: %v", err)
+                       continue
+               }
+               if gjson.Get(resp.String(), "errcode").Int() != 0 {
+                       log.Errorf("get department user list failed: %v", 
resp.String())
+                       continue
+               }
+
+               userList := gjson.Get(resp.String(), "userlist").String()
+               var employees []*Employee
+               err = json.Unmarshal([]byte(userList), &employees)
+               if err != nil {
+                       log.Errorf("unmarshal userList failed: %w", err)
+                       continue
+               }
+               log.Debugf("get department user list: %d", len(employees))
+               for _, employee := range employees {
+                       c.EmployeeMapping[employee.Userid] = employee
+                       log.Debugf(employee.Userid)
+               }
+       }
+
+       log.Debugf("all user amount: %d", len(c.EmployeeMapping))
+       return nil
+}
+
+func (c *Company) AuthUser(code string) (info *UserInfo, err error) {
+       token, err := c.Work.GetOauth().GetAccessToken()
+       if err != nil {
+               return nil, fmt.Errorf("get access token failed: %w", err)
+       }
+
+       getUserInfoResp, err := 
req.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=";
 + token + "&code=" + code))
+       if err != nil {
+               log.Errorf("get user info failed: %v", err)
+               return nil, err
+       }
+       log.Debugf("get user info: %s", getUserInfoResp.String())
+
+       userTicket := gjson.Get(getUserInfoResp.String(), 
"user_ticket").String()
+       request := req.SetBody(map[string]string{"user_ticket": userTicket})
+       getUserDetailResp, err := 
request.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=";
 + token))
+       if err != nil {
+               log.Errorf("get user info failed: %v", err)
+               return nil, err
+       }
+
+       var userInfoResp *AuthUserInfoResp
+       err = json.Unmarshal([]byte(getUserDetailResp.String()), &userInfoResp)
+       if err != nil {
+               log.Errorf("unmarshal user info failed: %s", err)
+               return nil, err
+       }
+       if userInfoResp.Errcode != 0 {
+               log.Errorf("get user info failed: %v", 
getUserDetailResp.String())
+               return nil, fmt.Errorf("get user info failed")
+       }
+       log.Debugf("get user info: %s", getUserDetailResp.String())
+
+       employee := c.EmployeeMapping[userInfoResp.Userid]
+       if employee == nil {
+               return nil, fmt.Errorf("user %s not found in employee list", 
userInfoResp.Userid)
+       }
+
+       userDetailInfo, err := c.GetUserDetailInfo(userInfoResp.Userid)
+       if err != nil {
+               return nil, err
+       }
+
+       userInfo := &UserInfo{
+               Userid:        userInfoResp.Userid,
+               Mobile:        userInfoResp.Mobile,
+               Gender:        userInfoResp.Gender,
+               Email:         userInfoResp.Email,
+               Avatar:        userInfoResp.Avatar,
+               QrCode:        userInfoResp.QrCode,
+               Address:       userInfoResp.Address,
+               Name:          employee.Name,
+               Position:      userDetailInfo.Position,
+               IsAvailable:   userDetailInfo.Status == 1,
+               DepartmentIDs: employee.Department,
+       }
+       return userInfo, nil
+}
+
+func (c *Company) GetRedirectURL(callbackURl string) (redirectURL string) {
+       return c.Work.GetOauth().GetTargetPrivateURL(callbackURl, c.AgentID)
+}
+
+func (c *Company) GetUserDetailInfo(userid string) (info *UserDetailInfo, err 
error) {
+       token, err := c.Work.GetOauth().GetAccessToken()
+       if err != nil {
+               return nil, fmt.Errorf("get access token failed: %w", err)
+       }
+
+       userDetailInfoResp, err := 
req.Get(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=";
 + token + "&userid=" + userid))
+       if err != nil {
+               log.Errorf("get user info failed: %v", err)
+               return nil, err
+       }
+       var userDetailInfo *UserDetailInfo
+       _ = json.Unmarshal([]byte(userDetailInfoResp.String()), &userDetailInfo)
+       if userDetailInfo.Errcode != 0 {
+               log.Errorf("get user info failed: %v", 
userDetailInfoResp.String())
+               return nil, fmt.Errorf("get user info failed")
+       }
+       log.Debugf("get user detail info: %s", userDetailInfoResp.String())
+       c.UserDetailInfoMapping[userid] = userDetailInfo
+       return userDetailInfo, nil
+}
+
+func (c *Company) formatDepartmentAndPosition(departmentIDs []int, position 
string) string {
+       var departmentName []string
+       for _, t := range departmentIDs {
+               name := c.fullDepartmentName(t)
+               if len(name) == 0 {
+                       continue
+               }
+               departmentName = append(departmentName, name)
+       }
+       var desc []string
+       if dep := strings.Join(departmentName, ","); len(dep) > 0 {
+               desc = append(desc, fmt.Sprintf("部门:%s", dep))
+       }
+       if len(position) > 0 {
+               desc = append(desc, fmt.Sprintf("职位:%s", position))
+       }
+       return strings.Join(desc, "\n")
+}
+
+func (c *Company) fullDepartmentName(departmentID int) string {
+       departmentNames := make([]string, 0)
+       for {
+               department := c.DepartmentMapping[departmentID]
+               if department == nil {
+                       break
+               }
+               departmentNames = append([]string{department.Name}, 
departmentNames...)
+               if department.ParentID == 0 {
+                       break
+               }
+               departmentID = department.ParentID
+       }
+       return strings.Join(departmentNames, "/")
+}
diff --git a/user-center-wecom/config.go b/user-center-wecom/config.go
new file mode 100644
index 0000000..9c8364f
--- /dev/null
+++ b/user-center-wecom/config.go
@@ -0,0 +1,122 @@
+package wecom
+
+import (
+       "encoding/json"
+       "time"
+
+       "github.com/apache/incubator-answer-plugins/user-center-wecom/i18n"
+       "github.com/apache/incubator-answer/plugin"
+)
+
+type UserCenterConfig struct {
+       CorpID       string `json:"corp_id"`
+       CorpSecret   string `json:"corp_secret"`
+       AgentID      string `json:"agent_id"`
+       AutoSync     bool   `json:"auto_sync"`
+       Notification bool   `json:"notification"`
+}
+
+func (uc *UserCenter) ConfigFields() []plugin.ConfigField {
+       syncState := plugin.LoadingActionStateNone
+       lastSuccessfulSyncAt := "None"
+       if !uc.syncTime.IsZero() {
+               syncState = plugin.LoadingActionStateComplete
+               lastSuccessfulSyncAt = uc.syncTime.In(time.FixedZone("GMT", 
8*3600)).Format("2006-01-02 15:04:05")
+       }
+       t := func(ctx *plugin.GinContext) string {
+               return plugin.Translate(ctx, i18n.ConfigSyncNowDescription) + 
": " + lastSuccessfulSyncAt
+       }
+       syncNowDesc := plugin.Translator{Fn: t}
+
+       syncNowLabel := plugin.MakeTranslator(i18n.ConfigSyncNowLabel)
+
+       if uc.syncing {
+               syncNowLabel = 
plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing)
+               syncState = plugin.LoadingActionStatePending
+       }
+
+       return []plugin.ConfigField{
+               {
+                       Name:        "auto_sync",
+                       Type:        plugin.ConfigTypeSwitch,
+                       Title:       
plugin.MakeTranslator(i18n.ConfigAutoSyncTitle),
+                       Description: 
plugin.MakeTranslator(i18n.ConfigAutoSyncDescription),
+                       Required:    false,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               Label: 
plugin.MakeTranslator(i18n.ConfigAutoSyncLabel),
+                       },
+                       Value: uc.Config.AutoSync,
+               },
+               {
+                       Name:        "sync_now",
+                       Type:        plugin.ConfigTypeButton,
+                       Title:       
plugin.MakeTranslator(i18n.ConfigSyncNowTitle),
+                       Description: syncNowDesc,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               Text: syncNowLabel,
+                               Action: &plugin.UIOptionAction{
+                                       Url:    "/answer/admin/api/wecom/sync",
+                                       Method: "get",
+                                       Loading: &plugin.LoadingAction{
+                                               Text:  
plugin.MakeTranslator(i18n.ConfigSyncNowLabelForDoing),
+                                               State: syncState,
+                                       },
+                                       OnComplete: &plugin.OnCompleteAction{
+                                               ToastReturnMessage: true,
+                                               RefreshFormConfig:  true,
+                                       },
+                               },
+                               Variant: "outline-secondary",
+                       },
+               },
+               {
+                       Name:     "corp_id",
+                       Type:     plugin.ConfigTypeInput,
+                       Title:    plugin.MakeTranslator(i18n.ConfigCorpIdTitle),
+                       Required: true,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypeText,
+                       },
+                       Value: uc.Config.CorpID,
+               },
+               {
+                       Name:     "corp_secret",
+                       Type:     plugin.ConfigTypeInput,
+                       Title:    
plugin.MakeTranslator(i18n.ConfigCorpSecretTitle),
+                       Required: true,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypePassword,
+                       },
+                       Value: uc.Config.CorpSecret,
+               },
+               {
+                       Name:     "agent_id",
+                       Type:     plugin.ConfigTypeInput,
+                       Title:    
plugin.MakeTranslator(i18n.ConfigAgentIDTitle),
+                       Required: true,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypeText,
+                       },
+                       Value: uc.Config.AgentID,
+               },
+               {
+                       Name:        "notification",
+                       Type:        plugin.ConfigTypeSwitch,
+                       Title:       
plugin.MakeTranslator(i18n.ConfigNotificationTitle),
+                       Description: 
plugin.MakeTranslator(i18n.ConfigNotificationDescription),
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               Label: 
plugin.MakeTranslator(i18n.ConfigNotificationLabel),
+                       },
+                       Value: uc.Config.Notification,
+               },
+       }
+}
+
+func (uc *UserCenter) ConfigReceiver(config []byte) error {
+       c := &UserCenterConfig{}
+       _ = json.Unmarshal(config, c)
+       uc.Config = c
+       uc.Company = NewCompany(c.CorpID, c.CorpSecret, c.AgentID)
+       uc.asyncCompany()
+       return nil
+}
diff --git a/user-center-wecom/cron.go b/user-center-wecom/cron.go
new file mode 100644
index 0000000..fcb96b8
--- /dev/null
+++ b/user-center-wecom/cron.go
@@ -0,0 +1,20 @@
+package wecom
+
+import (
+       "time"
+
+       "github.com/segmentfault/pacman/log"
+)
+
+func (uc *UserCenter) CronSyncData() {
+       go func() {
+               ticker := time.NewTicker(time.Hour)
+               for {
+                       select {
+                       case <-ticker.C:
+                               log.Infof("user center try to sync data")
+                               uc.syncCompany()
+                       }
+               }
+       }()
+}
diff --git a/user-center-wecom/docs/wecom-config.png 
b/user-center-wecom/docs/wecom-config.png
new file mode 100644
index 0000000..3799c75
Binary files /dev/null and b/user-center-wecom/docs/wecom-config.png differ
diff --git a/user-center-wecom/docs/wecom-login.png 
b/user-center-wecom/docs/wecom-login.png
new file mode 100644
index 0000000..4997b48
Binary files /dev/null and b/user-center-wecom/docs/wecom-login.png differ
diff --git a/user-center-wecom/docs/wecom-qrcode.png 
b/user-center-wecom/docs/wecom-qrcode.png
new file mode 100644
index 0000000..31227f3
Binary files /dev/null and b/user-center-wecom/docs/wecom-qrcode.png differ
diff --git a/user-center-wecom/go.mod b/user-center-wecom/go.mod
new file mode 100644
index 0000000..c561c34
--- /dev/null
+++ b/user-center-wecom/go.mod
@@ -0,0 +1,69 @@
+module github.com/apache/incubator-answer-plugins/user-center-wecom
+
+go 1.19
+
+require (
+       github.com/apache/incubator-answer v1.2.2-0.20240117083035-5418bdb2fad1
+       github.com/gin-gonic/gin v1.9.1
+       github.com/imroc/req/v3 v3.33.1
+       github.com/patrickmn/go-cache v2.1.0+incompatible
+       github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f
+       github.com/silenceper/wechat/v2 v2.1.6
+       github.com/tidwall/gjson v1.14.4
+)
+
+require (
+       github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect
+       github.com/aymerick/douceur v0.2.0 // indirect
+       github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // 
indirect
+       github.com/bytedance/sonic v1.9.1 // indirect
+       github.com/cespare/xxhash/v2 v2.2.0 // indirect
+       github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 
indirect
+       github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 
indirect
+       github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+       github.com/gin-contrib/sse v0.1.0 // indirect
+       github.com/go-playground/locales v0.14.1 // indirect
+       github.com/go-playground/universal-translator v0.18.1 // indirect
+       github.com/go-playground/validator/v10 v10.14.0 // indirect
+       github.com/go-redis/redis/v8 v8.11.5 // indirect
+       github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // 
indirect
+       github.com/goccy/go-json v0.10.2 // indirect
+       github.com/golang/mock v1.6.0 // indirect
+       github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
+       github.com/google/wire v0.5.0 // indirect
+       github.com/gorilla/css v1.0.0 // indirect
+       github.com/hashicorp/errwrap v1.1.0 // indirect
+       github.com/hashicorp/go-multierror v1.1.1 // indirect
+       github.com/json-iterator/go v1.1.12 // indirect
+       github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+       github.com/kr/text v0.2.0 // indirect
+       github.com/leodido/go-urn v1.2.4 // indirect
+       github.com/mattn/go-isatty v0.0.19 // indirect
+       github.com/microcosm-cc/bluemonday v1.0.21 // indirect
+       github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 
indirect
+       github.com/modern-go/reflect2 v1.0.2 // indirect
+       github.com/onsi/ginkgo/v2 v2.2.0 // indirect
+       github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+       github.com/quic-go/qpack v0.4.0 // indirect
+       github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
+       github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
+       github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
+       github.com/quic-go/quic-go v0.32.0 // indirect
+       github.com/segmentfault/pacman/contrib/i18n 
v0.0.0-20230516093754-b76aef1c1150 // indirect
+       github.com/tidwall/match v1.1.1 // indirect
+       github.com/tidwall/pretty v1.2.0 // indirect
+       github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+       github.com/ugorji/go/codec v1.2.11 // indirect
+       golang.org/x/arch v0.3.0 // indirect
+       golang.org/x/crypto v0.13.0 // indirect
+       golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
+       golang.org/x/mod v0.12.0 // indirect
+       golang.org/x/net v0.15.0 // indirect
+       golang.org/x/sys v0.12.0 // indirect
+       golang.org/x/text v0.13.0 // indirect
+       golang.org/x/tools v0.13.0 // indirect
+       google.golang.org/protobuf v1.30.0 // indirect
+       gopkg.in/yaml.v2 v2.4.0 // indirect
+       gopkg.in/yaml.v3 v3.0.1 // indirect
+       sigs.k8s.io/yaml v1.3.0 // indirect
+)
diff --git a/user-center-wecom/go.sum b/user-center-wecom/go.sum
new file mode 100644
index 0000000..55eaba8
--- /dev/null
+++ b/user-center-wecom/go.sum
@@ -0,0 +1,291 @@
+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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a 
h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod 
h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis/v2 v2.30.0 
h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
+github.com/alicebob/miniredis/v2 v2.30.0/go.mod 
h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
+github.com/apache/incubator-answer v1.2.2-0.20240117083035-5418bdb2fad1 
h1:h1QQt3WaZ3IlTIH2JdZQP17kiU5XJ0NGUsFHODnJaWg=
+github.com/apache/incubator-answer v1.2.2-0.20240117083035-5418bdb2fad1/go.mod 
h1:yoYETRAnY3Bng3wEo+B6R9nXjZ1O3brs2DKWGpKYPcA=
+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/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d 
h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
+github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod 
h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
+github.com/bytedance/sonic v1.5.0/go.mod 
h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 
h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod 
h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 
h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod 
h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 
h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod 
h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chzyer/logex v1.1.10/go.mod 
h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod 
h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod 
h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f 
h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod 
h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fatih/structs v1.1.0/go.mod 
h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fsnotify/fsnotify v1.4.7/go.mod 
h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod 
h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.6.0 
h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/gabriel-vasile/mimetype v1.4.2 
h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod 
h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 
h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod 
h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod 
h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-playground/assert/v2 v2.2.0 
h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 
h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod 
h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 
h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod 
h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 
h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod 
h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-redis/redis/v8 v8.11.5 
h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod 
h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 
h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod 
h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/goccy/go-json v0.10.2 
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod 
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod 
h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod 
h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod 
h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod 
h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod 
h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod 
h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod 
h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod 
h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 
h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod 
h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod 
h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod 
h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod 
h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod 
h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 
h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod 
h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/subcommands v1.0.1/go.mod 
h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
+github.com/google/wire v0.5.0/go.mod 
h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
+github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
+github.com/gorilla/css v1.0.0/go.mod 
h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 
h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod 
h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
+github.com/hashicorp/errwrap v1.0.0/go.mod 
h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 
h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod 
h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 
h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod 
h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hpcloud/tail v1.0.0/go.mod 
h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod 
h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imroc/req/v3 v3.33.1 h1:BZnyl+K0hXcJlZBHY2CqbPgmVc1pPJDzjn6aJfB6shI=
+github.com/imroc/req/v3 v3.33.1/go.mod 
h1:cZ+7C3L/AYOr4tLGG16hZF90F1WzAdAdzt1xFSlizXY=
+github.com/json-iterator/go v1.1.12 
h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod 
h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod 
h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 
h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod 
h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod 
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.4 
h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod 
h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-isatty v0.0.19 
h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod 
h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/microcosm-cc/bluemonday v1.0.21 
h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
+github.com/microcosm-cc/bluemonday v1.0.21/go.mod 
h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod 
h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd 
h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod 
h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 
h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod 
h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod 
h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
+github.com/nxadm/tail v1.4.4/go.mod 
h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod 
h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod 
h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod 
h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod 
h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod 
h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.0.0/go.mod 
h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/ginkgo/v2 v2.2.0 
h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
+github.com/onsi/ginkgo/v2 v2.2.0/go.mod 
h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
+github.com/onsi/gomega v1.7.1/go.mod 
h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod 
h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod 
h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.18.1/go.mod 
h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
+github.com/patrickmn/go-cache v2.1.0+incompatible 
h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod 
h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml/v2 v2.0.8 
h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod 
h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
+github.com/quic-go/qpack v0.4.0/go.mod 
h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+github.com/quic-go/qtls-go1-18 v0.2.0 
h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
+github.com/quic-go/qtls-go1-18 v0.2.0/go.mod 
h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
+github.com/quic-go/qtls-go1-19 v0.2.0 
h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
+github.com/quic-go/qtls-go1-19 v0.2.0/go.mod 
h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
+github.com/quic-go/qtls-go1-20 v0.1.0 
h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
+github.com/quic-go/qtls-go1-20 v0.1.0/go.mod 
h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
+github.com/quic-go/quic-go v0.32.0 
h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
+github.com/quic-go/quic-go v0.32.0/go.mod 
h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
+github.com/rogpeppe/go-internal v1.8.0 
h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f 
h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0=
+github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod 
h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
+github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 
h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk=
+github.com/segmentfault/pacman/contrib/i18n 
v0.0.0-20230516093754-b76aef1c1150/go.mod 
h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
+github.com/silenceper/wechat/v2 v2.1.6 
h1:2br2DxNzhksmvIBJ+PfMqjqsvoZmd/5BnMIfjKYUBgc=
+github.com/silenceper/wechat/v2 v2.1.6/go.mod 
h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
+github.com/sirupsen/logrus v1.9.0/go.mod 
h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cast v1.4.1/go.mod 
h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod 
h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod 
h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod 
h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod 
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod 
h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod 
h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod 
h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod 
h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3/go.mod 
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4 
h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/tidwall/gjson v1.14.1/go.mod 
h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.4 
h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod 
h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod 
h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 
h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod 
h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/twitchyliquid64/golang-asm v0.15.1 
h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod 
h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 
h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod 
h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yuin/goldmark v1.2.1/go.mod 
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod 
h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod 
h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 
h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
+github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod 
h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod 
h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod 
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-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.0.0-20220622213112-05595931fe9d/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
+golang.org/x/crypto v0.13.0/go.mod 
h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db 
h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod 
h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod 
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod 
h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+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-20200520004742-59133d7f0dd7/go.mod 
h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+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-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod 
h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20210220032951-036812b2e83c/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+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-20190904154756-749cb33beabd/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20210112080510-489259a85091/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod 
h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod 
h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod 
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod 
h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod 
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod 
h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod 
h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+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 v0.0.0-20200109180630-ec00e32a8dfd/go.mod 
h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod 
h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod 
h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod 
h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod 
h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod 
h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod 
h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod 
h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 
h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod 
h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 
h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod 
h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
+gopkg.in/h2non/gock.v1 v1.1.2/go.mod 
h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 
h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod 
h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod 
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/user-center-wecom/handler.go b/user-center-wecom/handler.go
new file mode 100644
index 0000000..7828d66
--- /dev/null
+++ b/user-center-wecom/handler.go
@@ -0,0 +1,96 @@
+package wecom
+
+import (
+       "crypto/rand"
+       "encoding/hex"
+       "fmt"
+       "net/http"
+       "strings"
+
+       "github.com/apache/incubator-answer-plugins/user-center-wecom/i18n"
+       "github.com/apache/incubator-answer/plugin"
+       "github.com/gin-gonic/gin"
+)
+
+// RespBody response body.
+type RespBody struct {
+       // http code
+       Code int `json:"code"`
+       // reason key
+       Reason string `json:"reason"`
+       // response message
+       Message string `json:"msg"`
+       // response data
+       Data interface{} `json:"data"`
+}
+
+// NewRespBodyData new response body with data
+func NewRespBodyData(code int, reason string, data interface{}) *RespBody {
+       return &RespBody{
+               Code:   code,
+               Reason: reason,
+               Data:   data,
+       }
+}
+
+func (uc *UserCenter) GetRedirectURL(ctx *gin.Context) {
+       authorizeUrl := 
fmt.Sprintf("%s/answer/api/v1/user-center/login/callback", plugin.SiteURL())
+       redirectURL := uc.Company.GetRedirectURL(authorizeUrl)
+       state := genNonce()
+       redirectURL = strings.ReplaceAll(redirectURL, "STATE", state)
+       ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", 
map[string]string{
+               "redirect_url": redirectURL,
+               "key":          state,
+       }))
+}
+
+func (uc *UserCenter) Sync(ctx *gin.Context) {
+       uc.syncCompany()
+       if uc.syncSuccess {
+               ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, 
"success", map[string]any{
+                       "message": plugin.Translate(ctx, 
i18n.ConfigSyncNowSuccessResponse),
+               }))
+               return
+       }
+       errRespBodyData := NewRespBodyData(http.StatusBadRequest, "error", 
map[string]any{
+               "err_type": "toast",
+       })
+       errRespBodyData.Message = plugin.Translate(ctx, 
i18n.ConfigSyncNowFailedResponse)
+       ctx.JSON(http.StatusBadRequest, errRespBodyData)
+}
+
+func (uc *UserCenter) Data(ctx *gin.Context) {
+       ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", 
map[string]any{
+               "dep":  uc.Company.DepartmentMapping,
+               "user": uc.Company.EmployeeMapping,
+       }))
+}
+
+func (uc *UserCenter) CheckUserLogin(ctx *gin.Context) {
+       key := ctx.Query("key")
+       val, exist := uc.Cache.Get(key)
+       if !exist {
+               ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, 
"success", map[string]any{
+                       "is_login": false,
+                       "token":    "",
+               }))
+               return
+       }
+       token := ""
+       externalID, _ := val.(string)
+       tokenStr, exist := uc.Cache.Get(externalID)
+       if exist {
+               token, _ = tokenStr.(string)
+       }
+       ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, "success", 
map[string]any{
+               "is_login": len(token) > 0,
+               "token":    token,
+       }))
+}
+
+// 随机生成 nonce
+func genNonce() string {
+       bytes := make([]byte, 10)
+       _, _ = rand.Read(bytes)
+       return hex.EncodeToString(bytes)
+}
diff --git a/user-center-wecom/i18n/en_US.yaml 
b/user-center-wecom/i18n/en_US.yaml
new file mode 100644
index 0000000..cb5b527
--- /dev/null
+++ b/user-center-wecom/i18n/en_US.yaml
@@ -0,0 +1,101 @@
+plugin:
+  wecom_user_center:
+    backend:
+      response:
+        sync_now:
+          success:
+            other: Contacts sync successful.
+          failed:
+            other: Contacts sync failed.
+      info:
+        name:
+          other: WeCom
+        description:
+          other: Get user info from WeCom and sync to User Center
+      config:
+        auto_sync:
+          label:
+            other: Turn on auto sync
+          title:
+            other: Auto Sync Contacts
+          description:
+            other: Automatically synchronize every hour.
+        sync_now:
+          label:
+            other: Sync now
+          label_for_doing:
+            other: Syncing
+          title:
+            other: Manual Sync Contacts
+          description:
+            other: Last successful sync
+        authorize_url:
+          title:
+            other: Authorize URL
+          description:
+            other: WeCom authorize URL
+        corp_id:
+          title:
+            other: Company ID
+          description:
+            other: WeCom corp ID
+        corp_secret:
+          title:
+            other: App Secret
+          description:
+            other: WeCom corp secret
+        agent_id:
+          title:
+            other: App Agent ID
+          description:
+            other: WeCom agent ID
+        notification:
+          label:
+            other: Turn on push notifications
+          title:
+            other: Notifications
+          description:
+            other: Users will receive notifications on WeCom.
+      user_config:
+        inbox_notifications:
+          title:
+            other: Inbox Notifications
+          label:
+            other: Turn on inbox notifications
+          description:
+            other: Answers to your questions, comments, invites, and more.
+        all_new_questions:
+          title:
+            other: All New Questions
+          label:
+            other: Turn on all new questions
+          description:
+            other: Get notified of all new questions. Up to 50 questions per 
week.
+        new_questions_for_following_tags:
+          title:
+            other: New Questions for Following Tags
+          label:
+            other: Turn on new questions for following tags
+          description:
+            other: Get notified of new questions for following tags.
+      tpl:
+        update_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> updated question 
<a href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>"
+        answer_the_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> answered the 
question <a href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        update_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> updated answer <a 
href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        accept_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> accepted answer <a 
href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        comment_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> commented question 
<a href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        comment_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> commented answer 
<a href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        reply_to_you:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> replied you <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        mention_you:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> mentioned you <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        invited_you_to_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> invited you to 
answer <a href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>"
+        new_question:
+          other: "New question:\n<a 
href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>\n{{.QuestionTags}}"
diff --git a/user-center-wecom/i18n/translation.go 
b/user-center-wecom/i18n/translation.go
new file mode 100644
index 0000000..0624d1b
--- /dev/null
+++ b/user-center-wecom/i18n/translation.go
@@ -0,0 +1,49 @@
+package i18n
+
+const (
+       InfoName                      = 
"plugin.wecom_user_center.backend.info.name"
+       InfoDescription               = 
"plugin.wecom_user_center.backend.info.description"
+       ConfigAutoSyncLabel           = 
"plugin.wecom_user_center.backend.config.auto_sync.label"
+       ConfigAutoSyncTitle           = 
"plugin.wecom_user_center.backend.config.auto_sync.title"
+       ConfigAutoSyncDescription     = 
"plugin.wecom_user_center.backend.config.auto_sync.description"
+       ConfigSyncNowLabel            = 
"plugin.wecom_user_center.backend.config.sync_now.label"
+       ConfigSyncNowLabelForDoing    = 
"plugin.wecom_user_center.backend.config.sync_now.label_for_doing"
+       ConfigSyncNowTitle            = 
"plugin.wecom_user_center.backend.config.sync_now.title"
+       ConfigSyncNowDescription      = 
"plugin.wecom_user_center.backend.config.sync_now.description"
+       ConfigAuthorizeUrlTitle       = 
"plugin.wecom_user_center.backend.config.authorize_url.title"
+       ConfigAuthorizeUrlDescription = 
"plugin.wecom_user_center.backend.config.authorize_url.description"
+       ConfigCorpIdTitle             = 
"plugin.wecom_user_center.backend.config.corp_id.title"
+       ConfigCorpIdDescription       = 
"plugin.wecom_user_center.backend.config.corp_id.description"
+       ConfigCorpSecretTitle         = 
"plugin.wecom_user_center.backend.config.corp_secret.title"
+       ConfigCorpSecretDescription   = 
"plugin.wecom_user_center.backend.config.corp_secret.description"
+       ConfigAgentIDTitle            = 
"plugin.wecom_user_center.backend.config.agent_id.title"
+       ConfigAgentIDDescription      = 
"plugin.wecom_user_center.backend.config.agent_id.description"
+       ConfigSyncNowSuccessResponse  = 
"plugin.wecom_user_center.backend.response.sync_now.success"
+       ConfigSyncNowFailedResponse   = 
"plugin.wecom_user_center.backend.response.sync_now.failed"
+       ConfigNotificationLabel       = 
"plugin.wecom_user_center.backend.config.notification.label"
+       ConfigNotificationTitle       = 
"plugin.wecom_user_center.backend.config.notification.title"
+       ConfigNotificationDescription = 
"plugin.wecom_user_center.backend.config.notification.description"
+
+       UserConfigInboxNotificationsTitle       = 
"plugin.wecom_user_center.backend.user_config.inbox_notifications.title"
+       UserConfigInboxNotificationsLabel       = 
"plugin.wecom_user_center.backend.user_config.inbox_notifications.label"
+       UserConfigInboxNotificationsDescription = 
"plugin.wecom_user_center.backend.user_config.inbox_notifications.description"
+
+       UserConfigAllNewQuestionsNotificationsTitle       = 
"plugin.wecom_user_center.backend.user_config.all_new_questions.title"
+       UserConfigAllNewQuestionsNotificationsLabel       = 
"plugin.wecom_user_center.backend.user_config.all_new_questions.label"
+       UserConfigAllNewQuestionsNotificationsDescription = 
"plugin.wecom_user_center.backend.user_config.all_new_questions.description"
+
+       UserConfigNewQuestionsForFollowingTagsTitle       = 
"plugin.wecom_user_center.backend.user_config.new_questions_for_following_tags.title"
+       UserConfigNewQuestionsForFollowingTagsLabel       = 
"plugin.wecom_user_center.backend.user_config.new_questions_for_following_tags.label"
+       UserConfigNewQuestionsForFollowingTagsDescription = 
"plugin.wecom_user_center.backend.user_config.new_questions_for_following_tags.description"
+
+       TplUpdateQuestion     = 
"plugin.wecom_user_center.backend.tpl.update_question"
+       TplAnswerTheQuestion  = 
"plugin.wecom_user_center.backend.tpl.answer_the_question"
+       TplUpdateAnswer       = 
"plugin.wecom_user_center.backend.tpl.update_answer"
+       TplAcceptAnswer       = 
"plugin.wecom_user_center.backend.tpl.accept_answer"
+       TplCommentQuestion    = 
"plugin.wecom_user_center.backend.tpl.comment_question"
+       TplCommentAnswer      = 
"plugin.wecom_user_center.backend.tpl.comment_answer"
+       TplReplyToYou         = 
"plugin.wecom_user_center.backend.tpl.reply_to_you"
+       TplMentionYou         = 
"plugin.wecom_user_center.backend.tpl.mention_you"
+       TplInvitedYouToAnswer = 
"plugin.wecom_user_center.backend.tpl.invited_you_to_answer"
+       TplNewQuestion        = 
"plugin.wecom_user_center.backend.tpl.new_question"
+)
diff --git a/user-center-wecom/i18n/zh_CN.yaml 
b/user-center-wecom/i18n/zh_CN.yaml
new file mode 100644
index 0000000..1decdca
--- /dev/null
+++ b/user-center-wecom/i18n/zh_CN.yaml
@@ -0,0 +1,101 @@
+plugin:
+  wecom_user_center:
+    backend:
+      response:
+        sync_now:
+          success:
+            other: 联系人同步成功。
+          failed:
+            other: 联系人同步失败。
+      info:
+        name:
+          other: 企业微信
+        description:
+          other: 从企业微信获取用户信息并同步到用户中心
+      config:
+        auto_sync:
+          label:
+            other: 打开自动同步
+          title:
+            other: 自动同步联系人
+          description:
+            other: 每小时自动同步。
+        sync_now:
+          label:
+            other: 立即同步
+          label_for_doing:
+            other: 同步中
+          title:
+            other: 手动同步联系人
+          description:
+            other: 上次成功同步于
+        authorize_url:
+          title:
+            other: 授权网址
+          description:
+            other: 企业微信授权网址
+        corp_id:
+          title:
+            other: 企业 ID
+          description:
+            other: 企业微信企业ID
+        corp_secret:
+          title:
+            other: 应用 Secret
+          description:
+            other: 企业微信应用程序密钥
+        agent_id:
+          title:
+            other: 应用 Agent ID
+          description:
+            other: 企业微信应用程序代理ID
+        notification:
+          label:
+            other: 打开通知
+          title:
+            other: 通知
+          description:
+            other: 用户将在企业微信上收到通知。
+      user_config:
+        inbox_notifications:
+          title:
+            other: 收件箱通知
+          label:
+            other: 打开收件箱通知
+          description:
+            other: 问题的答案、评论、邀请等。
+        all_new_questions:
+          title:
+            other: 所有新问题通知
+          label:
+            other: 打开所有新问题通知
+          description:
+            other: 收到所有新问题的通知。每周最多 50 个问题。
+        new_questions_for_following_tags:
+          title:
+            other: 关注标签的新问题通知
+          label:
+            other: 打开关注标签的新问题通知
+          description:
+            other: 收到以下标签的新问题通知。
+      tpl:
+        update_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 更新问题 <a 
href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>"
+        answer_the_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 回答了问题 <a 
href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        update_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 更新答案 <a 
href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        accept_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 接受答案 <a 
href=\"{{.AnswerUrl}}\">{{.QuestionTitle}}</a>"
+        comment_question:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 评论提问 <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        comment_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 评论回答 <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        reply_to_you:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 回复了问题 <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        mention_you:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 提到了你 <a 
href=\"{{.CommentUrl}}\">{{.QuestionTitle}}</a>"
+        invited_you_to_answer:
+          other: "<a 
href=\"{{.TriggerUserUrl}}\">{{.TriggerUserDisplayName}}</a> 邀请你回答 <a 
href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>"
+        new_question:
+          other: "新问题:\n<a 
href=\"{{.QuestionUrl}}\">{{.QuestionTitle}}</a>\n{{.QuestionTags}}"
\ No newline at end of file
diff --git a/user-center-wecom/notification.go 
b/user-center-wecom/notification.go
new file mode 100644
index 0000000..b8debcc
--- /dev/null
+++ b/user-center-wecom/notification.go
@@ -0,0 +1,103 @@
+package wecom
+
+import (
+       wecomI18n 
"github.com/apache/incubator-answer-plugins/user-center-wecom/i18n"
+       "github.com/apache/incubator-answer/plugin"
+       "github.com/segmentfault/pacman/i18n"
+       "github.com/segmentfault/pacman/log"
+       "github.com/silenceper/wechat/v2/work/message"
+       "strings"
+)
+
+// GetNewQuestionSubscribers returns the subscribers of the new question 
notification
+func (uc *UserCenter) GetNewQuestionSubscribers() (userIDs []string) {
+       for userID, conf := range uc.UserConfigCache.userConfigMapping {
+               if conf.AllNewQuestions {
+                       userIDs = append(userIDs, userID)
+               }
+       }
+       return userIDs
+}
+
+// Notify sends a notification to the user
+func (uc *UserCenter) Notify(msg *plugin.NotificationMessage) {
+       log.Debugf("try to send notification %+v", msg)
+
+       if !uc.Config.Notification {
+               return
+       }
+
+       // get user config
+       userConfig, err := uc.getUserConfig(msg.ReceiverUserID)
+       if err != nil {
+               log.Errorf("get user config failed: %v", err)
+               return
+       }
+       if userConfig == nil {
+               return
+       }
+
+       // check if the notification is enabled
+       if msg.Type == plugin.NotificationNewQuestion && 
!userConfig.AllNewQuestions {
+               return
+       } else if msg.Type == plugin.NotificationNewQuestionFollowedTag && 
!userConfig.NewQuestionsForFollowingTags {
+               return
+       } else {
+               if !userConfig.InboxNotifications {
+                       return
+               }
+       }
+
+       userDetail := uc.Company.UserDetailInfoMapping[msg.ReceiverExternalID]
+       if userDetail == nil {
+               log.Infof("user [%s] not found", msg.ReceiverExternalID)
+               return
+       }
+
+       notificationMsg := renderNotification(msg)
+       // no need to send empty message
+       if len(notificationMsg) == 0 {
+               return
+       }
+       resp, err := 
uc.Company.Work.GetMessage().SendText(message.SendTextRequest{
+               SendRequestCommon: &message.SendRequestCommon{
+                       ToUser:  userDetail.Userid,
+                       MsgType: "text",
+                       AgentID: uc.Config.AgentID,
+               },
+               Text: message.TextField{
+                       Content: notificationMsg,
+               },
+       })
+       if err != nil {
+               log.Errorf("send message failed: %v %v", err, resp)
+       }
+}
+
+func renderNotification(msg *plugin.NotificationMessage) string {
+       lang := i18n.Language(msg.ReceiverLang)
+       switch msg.Type {
+       case plugin.NotificationUpdateQuestion:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplUpdateQuestion, msg)
+       case plugin.NotificationAnswerTheQuestion:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplAnswerTheQuestion, msg)
+       case plugin.NotificationUpdateAnswer:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplUpdateAnswer, msg)
+       case plugin.NotificationAcceptAnswer:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplAcceptAnswer, msg)
+       case plugin.NotificationCommentQuestion:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplCommentQuestion, msg)
+       case plugin.NotificationCommentAnswer:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplCommentAnswer, msg)
+       case plugin.NotificationReplyToYou:
+               return plugin.TranslateWithData(lang, wecomI18n.TplReplyToYou, 
msg)
+       case plugin.NotificationMentionYou:
+               return plugin.TranslateWithData(lang, wecomI18n.TplMentionYou, 
msg)
+       case plugin.NotificationInvitedYouToAnswer:
+               return plugin.TranslateWithData(lang, 
wecomI18n.TplInvitedYouToAnswer, msg)
+       case plugin.NotificationNewQuestion, 
plugin.NotificationNewQuestionFollowedTag:
+               msg.QuestionTags = strings.Join(strings.Split(msg.QuestionTags, 
","), ", ")
+               return plugin.TranslateWithData(lang, wecomI18n.TplNewQuestion, 
msg)
+       }
+       return ""
+}
diff --git a/user-center-wecom/schema.go b/user-center-wecom/schema.go
new file mode 100644
index 0000000..3944f03
--- /dev/null
+++ b/user-center-wecom/schema.go
@@ -0,0 +1,59 @@
+package wecom
+
+type Department struct {
+       Id               int      `json:"id"`
+       Name             string   `json:"name"`
+       ParentID         int      `json:"parentid"`
+       Order            int      `json:"order"`
+       DepartmentLeader []string `json:"department_leader"`
+}
+
+type Employee struct {
+       Name       string `json:"name"`
+       Department []int  `json:"department"`
+       Userid     string `json:"userid"`
+}
+
+type AuthUserInfoResp struct {
+       Errcode int    `json:"errcode"`
+       Errmsg  string `json:"errmsg"`
+       Userid  string `json:"userid"`
+       Mobile  string `json:"mobile"`
+       Gender  string `json:"gender"`
+       Email   string `json:"email"`
+       Avatar  string `json:"avatar"`
+       QrCode  string `json:"qr_code"`
+       Address string `json:"address"`
+}
+
+type UserInfo struct {
+       Userid        string `json:"userid"`
+       Mobile        string `json:"mobile"`
+       Gender        string `json:"gender"`
+       Email         string `json:"email"`
+       Avatar        string `json:"avatar"`
+       QrCode        string `json:"qr_code"`
+       Address       string `json:"address"`
+       Name          string `json:"name"`
+       DepartmentIDs []int  `json:"department"`
+       Position      string `json:"position"`
+       IsAvailable   bool   `json:"is_available"`
+}
+
+type UserDetailInfo struct {
+       Errcode        int    `json:"errcode"`
+       Errmsg         string `json:"errmsg"`
+       Userid         string `json:"userid"`
+       Name           string `json:"name"`
+       Department     []int  `json:"department"`
+       Position       string `json:"position"`
+       Status         int    `json:"status"`
+       Isleader       int    `json:"isleader"`
+       EnglishName    string `json:"english_name"`
+       Telephone      string `json:"telephone"`
+       Enable         int    `json:"enable"`
+       HideMobile     int    `json:"hide_mobile"`
+       Order          []int  `json:"order"`
+       MainDepartment int    `json:"main_department"`
+       Alias          string `json:"alias"`
+}
diff --git a/user-center-wecom/user_config.go b/user-center-wecom/user_config.go
new file mode 100644
index 0000000..1ea7d81
--- /dev/null
+++ b/user-center-wecom/user_config.go
@@ -0,0 +1,94 @@
+package wecom
+
+import (
+       "encoding/json"
+       "fmt"
+       "github.com/apache/incubator-answer-plugins/user-center-wecom/i18n"
+       "github.com/apache/incubator-answer/plugin"
+       "github.com/segmentfault/pacman/log"
+       "sync"
+)
+
+type UserConfig struct {
+       InboxNotifications           bool `json:"inbox_notifications"`
+       AllNewQuestions              bool `json:"all_new_questions"`
+       NewQuestionsForFollowingTags bool 
`json:"new_questions_for_following_tags"`
+}
+
+type UserConfigCache struct {
+       // key: userID value: user config
+       userConfigMapping map[string]*UserConfig
+       sync.Mutex
+}
+
+func NewUserConfigCache() *UserConfigCache {
+       ucc := &UserConfigCache{
+               userConfigMapping: make(map[string]*UserConfig),
+       }
+       return ucc
+}
+
+func (ucc *UserConfigCache) SetUserConfig(userID string, config *UserConfig) {
+       ucc.Lock()
+       defer ucc.Unlock()
+       ucc.userConfigMapping[userID] = config
+}
+
+func (uc *UserCenter) UserConfigFields() []plugin.ConfigField {
+       fields := make([]plugin.ConfigField, 0)
+       fields = append(fields, createSwitchConfig(
+               "inbox_notifications",
+               i18n.UserConfigInboxNotificationsTitle,
+               i18n.UserConfigInboxNotificationsLabel,
+               i18n.UserConfigInboxNotificationsDescription,
+       ))
+       fields = append(fields, createSwitchConfig(
+               "all_new_questions",
+               i18n.UserConfigAllNewQuestionsNotificationsTitle,
+               i18n.UserConfigAllNewQuestionsNotificationsLabel,
+               i18n.UserConfigAllNewQuestionsNotificationsDescription,
+       ))
+       fields = append(fields, createSwitchConfig(
+               "new_questions_for_following_tags",
+               i18n.UserConfigNewQuestionsForFollowingTagsTitle,
+               i18n.UserConfigNewQuestionsForFollowingTagsLabel,
+               i18n.UserConfigNewQuestionsForFollowingTagsDescription,
+       ))
+       return fields
+}
+
+func createSwitchConfig(name, title, label, desc string) plugin.ConfigField {
+       return plugin.ConfigField{
+               Name:        name,
+               Type:        plugin.ConfigTypeSwitch,
+               Title:       plugin.MakeTranslator(title),
+               Description: plugin.MakeTranslator(desc),
+               UIOptions: plugin.ConfigFieldUIOptions{
+                       Label: plugin.MakeTranslator(label),
+               },
+       }
+}
+
+func (uc *UserCenter) UserConfigReceiver(userID string, config []byte) error {
+       log.Debugf("receive user config %s %s", userID, string(config))
+       var userConfig UserConfig
+       err := json.Unmarshal(config, &userConfig)
+       if err != nil {
+               return fmt.Errorf("unmarshal user config failed: %w", err)
+       }
+       uc.UserConfigCache.SetUserConfig(userID, &userConfig)
+       return nil
+}
+
+func (uc *UserCenter) getUserConfig(userID string) (config *UserConfig, err 
error) {
+       userConfig := plugin.GetPluginUserConfig(userID, uc.Info().SlugName)
+       if len(userConfig) == 0 {
+               return nil, nil
+       }
+       config = &UserConfig{}
+       err = json.Unmarshal(userConfig, config)
+       if err != nil {
+               return nil, fmt.Errorf("unmarshal user config failed: %w", err)
+       }
+       return config, nil
+}
diff --git a/user-center-wecom/wecom_user_center.go 
b/user-center-wecom/wecom_user_center.go
new file mode 100644
index 0000000..96926a8
--- /dev/null
+++ b/user-center-wecom/wecom_user_center.go
@@ -0,0 +1,242 @@
+package wecom
+
+import (
+       "fmt"
+       "net/http"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-answer-plugins/user-center-wecom/i18n"
+       "github.com/apache/incubator-answer/plugin"
+       "github.com/gin-gonic/gin"
+       "github.com/patrickmn/go-cache"
+       "github.com/segmentfault/pacman/log"
+)
+
+type UserCenter struct {
+       Config          *UserCenterConfig
+       Company         *Company
+       UserConfigCache *UserConfigCache
+       Cache           *cache.Cache
+       syncLock        sync.Mutex
+       syncing         bool
+       syncSuccess     bool
+       syncTime        time.Time
+}
+
+func (uc *UserCenter) RegisterUnAuthRouter(r *gin.RouterGroup) {
+       r.GET("/wecom/login/url", uc.GetRedirectURL)
+       r.GET("/wecom/login/check", uc.CheckUserLogin)
+}
+
+func (uc *UserCenter) RegisterAuthUserRouter(r *gin.RouterGroup) {
+}
+
+func (uc *UserCenter) RegisterAuthAdminRouter(r *gin.RouterGroup) {
+       r.GET("/wecom/sync", uc.Sync)
+       r.GET("/wecom/data", uc.Data)
+}
+
+func (uc *UserCenter) AfterLogin(externalID, accessToken string) {
+       log.Debugf("user %s is login", externalID)
+       uc.Cache.Set(externalID, accessToken, time.Minute*5)
+}
+
+func (uc *UserCenter) UserStatus(externalID string) (userStatus 
plugin.UserStatus) {
+       if len(externalID) == 0 {
+               return plugin.UserStatusAvailable
+       }
+       var err error
+       userDetailInfo := uc.Company.UserDetailInfoMapping[externalID]
+       if userDetailInfo == nil {
+               userDetailInfo, err = uc.Company.GetUserDetailInfo(externalID)
+               if err != nil {
+                       log.Errorf("get user detail info failed: %v", err)
+               }
+       }
+       if userDetailInfo == nil {
+               return plugin.UserStatusDeleted
+       }
+       switch userDetailInfo.Status {
+       case 1:
+               return plugin.UserStatusAvailable
+       case 2:
+               return plugin.UserStatusSuspended
+       default:
+               return plugin.UserStatusDeleted
+       }
+}
+
+func init() {
+       uc := &UserCenter{
+               Config:          &UserCenterConfig{},
+               UserConfigCache: NewUserConfigCache(),
+               Company:         NewCompany("", "", ""),
+               Cache:           cache.New(5*time.Minute, 10*time.Minute),
+               syncLock:        sync.Mutex{},
+       }
+       plugin.Register(uc)
+       uc.CronSyncData()
+}
+
+func (uc *UserCenter) Info() plugin.Info {
+       return plugin.Info{
+               Name:        plugin.MakeTranslator(i18n.InfoName),
+               SlugName:    "wecom_user_center",
+               Description: plugin.MakeTranslator(i18n.InfoDescription),
+               Author:      "answerdev",
+               Version:     "0.0.1",
+               Link:        "",
+       }
+}
+
+func (uc *UserCenter) Description() plugin.UserCenterDesc {
+       redirectURL := "/user-center/auth"
+       desc := plugin.UserCenterDesc{
+               Name:                      "WeCom",
+               DisplayName:               plugin.MakeTranslator(i18n.InfoName),
+               Icon:                      
"PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNiAxNCIgZmlsbD0iY3VycmVudENvbG9yIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTkuMDY4NTkgMTAuOTkwMkM4Ljc1NDMxIDEwLjgzMjcgOC41OTQ2MiAxMC41NzQ0IDguNTczODYgMTAuMjI3NUM4LjU2NjE5IDEwLjA5OTggOC41MDkwMiAxMC4wODU0IDguMzk0NjggMTAuMTE4OUM3Ljg0NTY2IDEwLjI3OTIgNy4yODQ4MiAxMC4zNTUzIDYuNzExNTIgMTAuMzY0OEM2LjEyMTI5IDEwLjM3NDcgNS41NDA2NSAxMC4zMTE4ID
 [...]
+               Url:                       "",
+               LoginRedirectURL:          redirectURL,
+               SignUpRedirectURL:         redirectURL,
+               RankAgentEnabled:          false,
+               UserStatusAgentEnabled:    false,
+               UserRoleAgentEnabled:      false,
+               MustAuthEmailEnabled:      true,
+               EnabledOriginalUserSystem: true,
+       }
+       return desc
+}
+
+func (uc *UserCenter) ControlCenterItems() []plugin.ControlCenter {
+       var controlCenterItems []plugin.ControlCenter
+       return controlCenterItems
+}
+
+func (uc *UserCenter) LoginCallback(ctx *plugin.GinContext) (userInfo 
*plugin.UserCenterBasicUserInfo, err error) {
+       code := ctx.Query("code")
+       if len(code) == 0 {
+               return nil, fmt.Errorf("code is empty")
+       }
+       state := ctx.Query("state")
+       if len(state) == 0 {
+               return nil, fmt.Errorf("state is empty")
+       }
+       log.Debugf("request code: %s, state: %s", code, state)
+
+       info, err := uc.Company.AuthUser(code)
+       if err != nil {
+               return nil, fmt.Errorf("auth user failed: %w", err)
+       }
+       if !info.IsAvailable {
+               return nil, fmt.Errorf("user is not available")
+       }
+       if len(info.Email) == 0 {
+               ctx.Redirect(http.StatusFound, "/user-center/auth-failed")
+               ctx.Abort()
+               return nil, fmt.Errorf("user email is empty")
+       }
+
+       userInfo = &plugin.UserCenterBasicUserInfo{}
+       userInfo.ExternalID = info.Userid
+       userInfo.Username = info.Userid
+       userInfo.DisplayName = info.Name
+       userInfo.Email = info.Email
+       userInfo.Rank = 0
+       userInfo.Avatar = info.Avatar
+       userInfo.Mobile = info.Mobile
+
+       uc.Cache.Set(state, userInfo.ExternalID, time.Minute*5)
+       return userInfo, nil
+}
+
+func (uc *UserCenter) SignUpCallback(ctx *plugin.GinContext) (userInfo 
*plugin.UserCenterBasicUserInfo, err error) {
+       return uc.LoginCallback(ctx)
+}
+
+func (uc *UserCenter) UserInfo(externalID string) (userInfo 
*plugin.UserCenterBasicUserInfo, err error) {
+       userDetailInfo := uc.Company.UserDetailInfoMapping[externalID]
+       if userDetailInfo == nil {
+               userDetailInfo, err = uc.Company.GetUserDetailInfo(externalID)
+               if err != nil {
+                       log.Errorf("get user detail info failed: %v", err)
+                       userInfo = &plugin.UserCenterBasicUserInfo{
+                               ExternalID: externalID,
+                               Status:     plugin.UserStatusDeleted,
+                       }
+                       return userInfo, nil
+               }
+       }
+       userInfo = &plugin.UserCenterBasicUserInfo{
+               ExternalID:  externalID,
+               Username:    userDetailInfo.Userid,
+               DisplayName: userDetailInfo.Name,
+               Bio:         
uc.Company.formatDepartmentAndPosition(userDetailInfo.Department, 
userDetailInfo.Position),
+       }
+       switch userDetailInfo.Status {
+       case 1:
+               userInfo.Status = plugin.UserStatusAvailable
+       case 2:
+               userInfo.Status = plugin.UserStatusSuspended
+       default:
+               userInfo.Status = plugin.UserStatusDeleted
+       }
+       return userInfo, nil
+}
+
+func (uc *UserCenter) UserList(externalIDs []string) (userList 
[]*plugin.UserCenterBasicUserInfo, err error) {
+       userList = make([]*plugin.UserCenterBasicUserInfo, 0)
+       return userList, nil
+}
+
+func (uc *UserCenter) UserSettings(externalID string) (userSettings 
*plugin.SettingInfo, err error) {
+       return &plugin.SettingInfo{
+               ProfileSettingRedirectURL: "",
+               AccountSettingRedirectURL: "",
+       }, nil
+}
+
+func (uc *UserCenter) PersonalBranding(externalID string) (branding 
[]*plugin.PersonalBranding) {
+       return branding
+}
+
+func (uc *UserCenter) asyncCompany() {
+       go func() {
+               defer func() {
+                       if err := recover(); err != nil {
+                               log.Errorf("sync data panic: %s", err)
+                       }
+               }()
+               uc.syncCompany()
+       }()
+}
+
+func (uc *UserCenter) syncCompany() {
+       if !uc.syncLock.TryLock() {
+               log.Infof("sync data is running")
+               return
+       }
+       defer func() {
+               uc.syncing = false
+               if uc.syncSuccess {
+                       uc.syncTime = time.Now()
+               }
+               uc.syncLock.Unlock()
+       }()
+
+       log.Info("start sync company data")
+       uc.syncing = true
+       uc.syncSuccess = true
+
+       if err := uc.Company.ListDepartmentAll(); err != nil {
+               log.Errorf("list department error: %s", err)
+               uc.syncSuccess = false
+               return
+       }
+       if err := uc.Company.ListUser(); err != nil {
+               log.Errorf("list user error: %s", err)
+               uc.syncSuccess = false
+               return
+       }
+       log.Info("end sync company data")
+}

Reply via email to