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 + + + + +## 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") +}
