From 6e42f11024bdc1b8a07d44798ae2809d6036a70e Mon Sep 17 00:00:00 2001 From: mhf Date: Wed, 7 Dec 2022 16:30:49 +0800 Subject: [PATCH] fix notify --- cmd/climc/shell/notifyv2/notification.go | 13 +- go.mod | 5 +- go.sum | 6 + pkg/apis/notify/config.go | 138 +- pkg/apis/notify/const.go | 13 +- pkg/apis/notify/event.go | 8 +- pkg/apis/notify/notification.go | 14 +- pkg/apis/notify/receiver.go | 13 +- pkg/apis/notify/robot.go | 1 + pkg/apis/websocket/const.go | 5 + pkg/{notify/rpc => apis/websocket}/doc.go | 2 +- pkg/cloudcommon/db/keystonecache.go | 1 + pkg/cloudcommon/db/usercache.go | 11 +- pkg/cloudcommon/notifyclient/events.go | 7 +- pkg/cloudcommon/notifyclient/notify.go | 4 + pkg/compute/models/guests.go | 1 - pkg/mcclient/modules/websocket/doc.go | 1 + pkg/mcclient/modules/websocket/managers.go | 17 + .../modules/websocket/mod_websockets.go} | 25 +- pkg/mcclient/modules/websocket/register.go | 7 + pkg/notify/interface.go | 85 -- pkg/notify/models/config.go | 460 ++----- pkg/notify/models/emailqueue.go | 16 +- pkg/notify/models/event.go | 22 +- pkg/notify/models/event_template.go | 16 +- pkg/notify/models/notification.go | 478 +++---- pkg/notify/models/plugindriver.go | 73 + pkg/notify/models/receiver.go | 881 +++++------- pkg/notify/models/receiver_notification.go | 48 +- pkg/notify/models/robot.go | 204 +-- pkg/notify/models/smsdriver.go | 41 + pkg/notify/models/subscriber.go | 17 + pkg/notify/models/template.go | 10 +- pkg/notify/models/topic.go | 98 +- pkg/notify/{sender => models}/worker.go | 2 +- pkg/notify/oldmodels/base.go | 98 -- pkg/notify/oldmodels/mod_config.go | 44 - pkg/notify/oldmodels/mod_contact.go | 53 - pkg/notify/oldmodels/mod_notification.go | 52 - pkg/notify/oldmodels/mod_template.go | 51 - pkg/notify/oldmodels/standalone.go | 103 -- pkg/notify/oldmodels/statusstandalone.go | 82 -- pkg/notify/oldmodels/usercache.go | 127 -- pkg/notify/options/options.go | 4 +- pkg/notify/rpc/apis/send_client.go | 90 -- pkg/notify/rpc/apis/send_server.pb.go | 1218 ----------------- pkg/notify/rpc/apis/send_server.proto | 108 -- pkg/notify/rpc/send.go | 480 ------- pkg/notify/rpc/service_map.go | 97 -- pkg/notify/sender/const.go | 53 + pkg/notify/sender/dingtalk.go | 162 +++ pkg/notify/sender/dingtalk_robot.go | 110 ++ pkg/notify/sender/email.go | 199 ++- pkg/notify/sender/feishu.go | 163 +++ pkg/notify/sender/feishu_robot.go | 112 ++ pkg/notify/sender/mobile.go | 113 ++ pkg/notify/sender/smsdriver/aliyun.go | 130 ++ pkg/notify/sender/smsdriver/const.go | 40 + .../{rpc/apis => sender/smsdriver}/doc.go | 2 +- pkg/notify/sender/smsdriver/huawei.go | 102 ++ pkg/notify/sender/smsdriver/utils.go | 78 ++ pkg/notify/sender/utils.go | 55 + pkg/notify/sender/webconsole.go | 118 ++ pkg/notify/sender/webhook.go | 103 ++ pkg/notify/sender/websocket.go | 120 ++ pkg/notify/sender/workwx.go | 140 ++ pkg/notify/sender/workwx_robot.go | 95 ++ pkg/notify/service/handlers.go | 9 +- pkg/notify/service/service.go | 17 +- pkg/notify/tasks/notifications_send_task.go | 181 +-- pkg/notify/tasks/repull_subcontact_task.go | 30 +- pkg/notify/tasks/subcontact_pull_task.go | 61 +- pkg/notify/tasks/topic_message_send_task.go | 302 ++++ pkg/notify/tasks/verification_send_task.go | 16 +- pkg/util/logclient/consts.go | 2 + .../github.com/hugozhu/godingtalk/.gitignore | 24 + vendor/github.com/hugozhu/godingtalk/LICENSE | 21 + .../github.com/hugozhu/godingtalk/README.md | 108 ++ .../hugozhu/godingtalk/api_attendance.go | 104 ++ .../hugozhu/godingtalk/api_calendar.go | 59 + .../hugozhu/godingtalk/api_callback.go | 49 + .../hugozhu/godingtalk/api_contact.go | 146 ++ .../hugozhu/godingtalk/api_encryption.go | 34 + .../github.com/hugozhu/godingtalk/api_file.go | 38 + .../hugozhu/godingtalk/api_media.go | 44 + .../hugozhu/godingtalk/api_message.go | 259 ++++ .../hugozhu/godingtalk/api_robot.go | 75 + .../github.com/hugozhu/godingtalk/api_sns.go | 109 ++ .../github.com/hugozhu/godingtalk/crypto.go | 164 +++ .../hugozhu/godingtalk/godingtalk.go | 189 +++ .../hugozhu/godingtalk/top_api_approval.go | 187 +++ .../hugozhu/godingtalk/top_api_message.go | 124 ++ .../hugozhu/godingtalk/top_api_request.go | 88 ++ .../hugozhu/godingtalk/transport.go | 119 ++ vendor/github.com/hugozhu/godingtalk/util.go | 102 ++ vendor/modules.txt | 3 + 96 files changed, 5606 insertions(+), 4303 deletions(-) create mode 100644 pkg/apis/websocket/const.go rename pkg/{notify/rpc => apis/websocket}/doc.go (90%) create mode 100644 pkg/mcclient/modules/websocket/doc.go create mode 100644 pkg/mcclient/modules/websocket/managers.go rename pkg/{notify/oldmodels/doc.go => mcclient/modules/websocket/mod_websockets.go} (61%) create mode 100755 pkg/mcclient/modules/websocket/register.go delete mode 100644 pkg/notify/interface.go create mode 100644 pkg/notify/models/plugindriver.go create mode 100644 pkg/notify/models/smsdriver.go rename pkg/notify/{sender => models}/worker.go (98%) delete mode 100644 pkg/notify/oldmodels/base.go delete mode 100644 pkg/notify/oldmodels/mod_config.go delete mode 100644 pkg/notify/oldmodels/mod_contact.go delete mode 100644 pkg/notify/oldmodels/mod_notification.go delete mode 100644 pkg/notify/oldmodels/mod_template.go delete mode 100644 pkg/notify/oldmodels/standalone.go delete mode 100644 pkg/notify/oldmodels/statusstandalone.go delete mode 100644 pkg/notify/oldmodels/usercache.go delete mode 100644 pkg/notify/rpc/apis/send_client.go delete mode 100644 pkg/notify/rpc/apis/send_server.pb.go delete mode 100644 pkg/notify/rpc/apis/send_server.proto delete mode 100644 pkg/notify/rpc/send.go delete mode 100644 pkg/notify/rpc/service_map.go create mode 100644 pkg/notify/sender/const.go create mode 100644 pkg/notify/sender/dingtalk.go create mode 100644 pkg/notify/sender/dingtalk_robot.go create mode 100644 pkg/notify/sender/feishu.go create mode 100644 pkg/notify/sender/feishu_robot.go create mode 100644 pkg/notify/sender/mobile.go create mode 100644 pkg/notify/sender/smsdriver/aliyun.go create mode 100644 pkg/notify/sender/smsdriver/const.go rename pkg/notify/{rpc/apis => sender/smsdriver}/doc.go (89%) create mode 100644 pkg/notify/sender/smsdriver/huawei.go create mode 100644 pkg/notify/sender/smsdriver/utils.go create mode 100644 pkg/notify/sender/utils.go create mode 100644 pkg/notify/sender/webconsole.go create mode 100644 pkg/notify/sender/webhook.go create mode 100644 pkg/notify/sender/websocket.go create mode 100644 pkg/notify/sender/workwx.go create mode 100644 pkg/notify/sender/workwx_robot.go create mode 100644 pkg/notify/tasks/topic_message_send_task.go create mode 100644 vendor/github.com/hugozhu/godingtalk/.gitignore create mode 100644 vendor/github.com/hugozhu/godingtalk/LICENSE create mode 100644 vendor/github.com/hugozhu/godingtalk/README.md create mode 100644 vendor/github.com/hugozhu/godingtalk/api_attendance.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_calendar.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_callback.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_contact.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_encryption.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_file.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_media.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_message.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_robot.go create mode 100644 vendor/github.com/hugozhu/godingtalk/api_sns.go create mode 100644 vendor/github.com/hugozhu/godingtalk/crypto.go create mode 100644 vendor/github.com/hugozhu/godingtalk/godingtalk.go create mode 100644 vendor/github.com/hugozhu/godingtalk/top_api_approval.go create mode 100644 vendor/github.com/hugozhu/godingtalk/top_api_message.go create mode 100644 vendor/github.com/hugozhu/godingtalk/top_api_request.go create mode 100644 vendor/github.com/hugozhu/godingtalk/transport.go create mode 100644 vendor/github.com/hugozhu/godingtalk/util.go diff --git a/cmd/climc/shell/notifyv2/notification.go b/cmd/climc/shell/notifyv2/notification.go index 1870204122..71718735be 100644 --- a/cmd/climc/shell/notifyv2/notification.go +++ b/cmd/climc/shell/notifyv2/notification.go @@ -104,9 +104,13 @@ func init() { return nil }) type NotificationEventInput struct { - Event string - Priority string - MsgBody string + Event string + Priority string + MsgBody string + ResourceType string + Action string + Contacts string + IsFailed string } R(&NotificationEventInput{}, "notify-event-send", "Send notify event message", func(s *mcclient.ClientSession, args *NotificationEventInput) error { body, err := jsonutils.ParseString(args.MsgBody) @@ -122,6 +126,9 @@ func init() { ResourceDetails: dict, Event: args.Event, Priority: args.Priority, + ResourceType: args.ResourceType, + Action: api.SAction(args.Action), + IsFailed: api.SResult(args.IsFailed), } _, err = modules.Notification.PerformClassAction(s, "event-notify", jsonutils.Marshal(params)) if err != nil { diff --git a/go.mod b/go.mod index aa6f6d07ba..4d1ad49e2b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/golang-plus/errors v1.0.0 github.com/golang-plus/uuid v1.0.0 github.com/golang/mock v1.4.4 - github.com/golang/protobuf v1.5.2 github.com/google/gopacket v1.1.17 github.com/google/uuid v1.3.0 github.com/googollee/go-socket.io v0.0.0-20181214084611-0ad7206c347a @@ -31,6 +30,7 @@ require ( github.com/gorilla/websocket v1.4.1 github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282 github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead + github.com/hugozhu/godingtalk v1.0.6 github.com/jaypipes/ghw v0.9.1 github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c github.com/lestrrat-go/jwx v1.0.2 @@ -46,6 +46,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.15 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.2.0 + github.com/satori/go.uuid v1.2.0 github.com/sergi/go-diff v1.2.0 github.com/serialx/hashring v0.0.0-20180504054112-49a4782e9908 github.com/sevlyar/go-daemon v0.1.5 @@ -164,6 +165,7 @@ require ( github.com/gofrs/uuid v4.1.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/google/btree v1.0.0 // indirect github.com/google/go-querystring v1.0.0 // indirect @@ -221,7 +223,6 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect - github.com/satori/go.uuid v1.2.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/smartystreets/assertions v1.2.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 20004cba79..ea02e54fac 100644 --- a/go.sum +++ b/go.sum @@ -359,6 +359,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -419,11 +420,14 @@ github.com/huaweicloud/huaweicloud-sdk-go v1.0.26 h1:aWTl4Ng9lZUW1DupYyqYiqkTwuT github.com/huaweicloud/huaweicloud-sdk-go v1.0.26/go.mod h1:YHXxw/bm7AohI4jTY8Z43JYw+DPCDLqjxoLTqmcyja4= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible h1:tANYIteuFrosKbRYUk1Yo/OGJjbt4x3OVg211Qc60M0= github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= +github.com/hugozhu/godingtalk v1.0.6 h1:K280OQGbYVS/UQuVWJz5cHhv/Wvfj1FHNo8lbbLGsNQ= +github.com/hugozhu/godingtalk v1.0.6/go.mod h1:Je6PSjUH7IKCPpFq5ti2I9ak1szzSNgCIMlP0s+8pbQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/ipandtcp/godingtalk v0.0.0-20180410032244-ca3d6ac197fb/go.mod h1:3umLciE1jBnh2LgrL15R2zbEe+HVPRZXnlSjjb/dYiw= github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk= github.com/jdcloud-api/jdcloud-sdk-go v1.55.0 h1:mzVj8r6fluEwjn8ogqtGfYW2qSIVUaEq0JAsvjCav3A= github.com/jdcloud-api/jdcloud-sdk-go v1.55.0/go.mod h1:UrKjuULIWLjHFlG6aSPunArE5QX57LftMmStAZJBEX8= @@ -1002,6 +1006,7 @@ google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEt google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= @@ -1019,6 +1024,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.4/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/pkg/apis/notify/config.go b/pkg/apis/notify/config.go index 2dc7f95994..3a93b1297e 100644 --- a/pkg/apis/notify/config.go +++ b/pkg/apis/notify/config.go @@ -15,14 +15,16 @@ package notify import ( + "reflect" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/gotypes" "yunion.io/x/onecloud/pkg/apis" ) type ConfigCreateInput struct { - apis.StandaloneResourceCreateInput - apis.DomainizedResourceInput + apis.DomainLevelResourceCreateInput // description: config type // required: true @@ -32,7 +34,7 @@ type ConfigCreateInput struct { // description: config content // required: true // example: {"app_id": "123456", "app_secret": "feishu_nihao"} - Content jsonutils.JSONObject `json:"content"` + Content *SNotifyConfigContent `json:"content"` // description: attribution // required: true @@ -45,19 +47,17 @@ type ConfigUpdateInput struct { // description: config content // required: true // example: {"app_id": "123456", "app_secret": "feishu_nihao"} - Content jsonutils.JSONObject `json:"content"` + Content *SNotifyConfigContent `json:"content"` } type ConfigDetails struct { - apis.StandaloneResourceDetails - apis.DomainizedResourceInfo + apis.DomainLevelResourceDetails SConfig } type ConfigListInput struct { - apis.StandaloneResourceListInput - apis.DomainizedResourceListInput + apis.DomainLevelResourceListInput Type string `json:"type"` Attribution string `json:"attribution"` } @@ -71,7 +71,7 @@ type ConfigValidateInput struct { // description: config content // required: true // example: {"app_id": "123456", "app_secret": "feishu_nihao"} - Content jsonutils.JSONObject `json:"content"` + Content *SNotifyConfigContent `json:"content"` } type ConfigValidateOutput struct { @@ -95,3 +95,123 @@ type ConfigManagerGetTypesInput struct { type ConfigManagerGetTypesOutput struct { Types []string `json:"types"` } + +type SsNotification struct { + ContactType string + Topic string + Message string + Event SNotifyEvent + AdvanceDays int +} + +type SBatchSendParams struct { + ContactType string + Contacts []string + Topic string + Message string + Priority string + Lang string +} + +type SNotifyReceiver struct { + Contact string + DomainId string + Enabled bool + Lang string + + callback func(error) +} + +func (self *SNotifyReceiver) Callback(err error) { + if self.callback != nil && err != nil { + self.callback(err) + } +} + +type SSendParams struct { + ContactType string + Contact string + Topic string + Message string + Priority string + Title string + RemoteTemplate string + Lang string + Receiver SNotifyReceiver +} + +type SendParams struct { + Title string + Message string + Priority string + RemoteTemplate string + Topic string + Event string + Receivers SNotifyReceiver + EmailMsg *SEmailMessage +} + +type SSMSSendParams struct { + AppKey string + AppSecret string + From string + To string + TemplateId string + TemplateParas string + Signature string +} + +type NotifyConfig struct { + SNotifyConfigContent + Attribution string + DomainId string +} + +func (self *NotifyConfig) GetDomainId() string { + if self.Attribution == CONFIG_ATTRIBUTION_SYSTEM { + return CONFIG_ATTRIBUTION_SYSTEM + } + return self.DomainId +} + +type SNotifyConfigContent struct { + // Email + Hostname string + Hostport int + Password string + SslGlobal bool + Username string + SenderAddress string + //Lark + AppId string + AppSecret string + AccessToken string + // workwx + AgentId string + CorpId string + Secret string + // dingtalk + //AgentId string + //AppSecret string + AppKey string + // sms + AccessKeyId string + AccessKeySecret string + ServiceUrl string + Signature string + SmsDriver string +} + +func (self SNotifyConfigContent) String() string { + return jsonutils.Marshal(self).String() +} + +func (self SNotifyConfigContent) IsZero() bool { + return jsonutils.Marshal(self).Equals(jsonutils.Marshal(SNotifyConfigContent{})) +} + +func init() { + gotypes.RegisterSerializable(reflect.TypeOf(&SNotifyConfigContent{}), func() gotypes.ISerializable { + return &SNotifyConfigContent{} + }) +} diff --git a/pkg/apis/notify/const.go b/pkg/apis/notify/const.go index c2024e1b73..f92ba5828f 100644 --- a/pkg/apis/notify/const.go +++ b/pkg/apis/notify/const.go @@ -14,7 +14,11 @@ package notify -import "yunion.io/x/onecloud/pkg/apis" +import ( + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/apis" +) const ( SERVICE_TYPE = apis.SERVICE_TYPE_NOTIFY @@ -30,6 +34,8 @@ const ( DINGTALK_ROBOT = "dingtalk-robot" WORKWX_ROBOT = "workwx-robot" WEBHOOK = "webhook" + WEBHOOK_ROBOT = "webhook-robot" + WEBSOCKET = "websocket" ROBOT = "robot" @@ -139,3 +145,8 @@ const ( SUBSCRIBER_SCOPE_DOMAIN = "domain" SUBSCRIBER_SCOPE_PROJECT = "project" ) + +var ( + ErrNoSuchMobile = errors.Error("no such mobile") + ErrIncompleteConfig = errors.Error("incomplete config") +) diff --git a/pkg/apis/notify/event.go b/pkg/apis/notify/event.go index 693e9feaf1..34cdc5b551 100644 --- a/pkg/apis/notify/event.go +++ b/pkg/apis/notify/event.go @@ -40,9 +40,10 @@ var ( ActionCreateBackupServer SAction = "add_backup_server" ActionDelBackupServer SAction = "delete_backup_server" - ActionSyncCreate SAction = "sync_create" - ActionSyncUpdate SAction = "sync_update" - ActionSyncDelete SAction = "sync_delete" + ActionSyncCreate SAction = "sync_create" + ActionSyncUpdate SAction = "sync_update" + ActionSyncDelete SAction = "sync_delete" + ActionSyncAccountStatus SAction = "sync_account_status" ActionOffline SAction = "offline" ActionSystemPanic SAction = "panic" @@ -53,7 +54,6 @@ var ( ActionLock SAction = "lock" ActionExceedCount SAction = "exceed_count" - ActionSyncAccountStatus SAction = "sync_account_status" ActionPasswordExpireSoon SAction = "password_expire_soon" ActionWorkerBlock SAction = "woker_block" ActionNetOutOfSync SAction = "net_out_of_sync" diff --git a/pkg/apis/notify/notification.go b/pkg/apis/notify/notification.go index b2543deeea..67c926b3b5 100644 --- a/pkg/apis/notify/notification.go +++ b/pkg/apis/notify/notification.go @@ -54,9 +54,8 @@ type NotificationCreateInput struct { // description: notification tag // required: false // example: alert - Tag string `json:"tag"` - Metadata map[string]interface{} `json:"metadata"` - IgnoreNonexistentReceiver bool `json:"ignore_nonexistent_receiver"` + Tag string `json:"tag"` + IgnoreNonexistentReceiver bool `json:"ignore_nonexistent_receiver"` } type ReceiveDetail struct { @@ -118,7 +117,13 @@ type NotificationManagerEventNotifyInput struct { // description: event trigger sending notification // required: true // example: SERVER_DELETE - Event string `json:"event"` + Event string + ResourceType string + + CloudAccountName string + Action SAction + // failed,succeed + Result SResult // description: day left before the event // required: false // example: 0 @@ -131,6 +136,7 @@ type NotificationManagerEventNotifyInput struct { // required: false // example: f627e09f038645f08ce6880c8d9cb8fd ProjectId string `json:"project_id"` + IsFailed SResult } type NotificationManagerEventNotifyOutput struct { diff --git a/pkg/apis/notify/receiver.go b/pkg/apis/notify/receiver.go index 067a7adc8a..ecf20a58a4 100644 --- a/pkg/apis/notify/receiver.go +++ b/pkg/apis/notify/receiver.go @@ -22,9 +22,7 @@ import ( ) type ReceiverCreateInput struct { - apis.StatusStandaloneResourceCreateInput - apis.DomainizedResourceCreateInput - apis.EnabledBaseResourceCreateInput + apis.EnabledStatusDomainLevelResourceCreateInput // description: user id in keystone // example: adfb720ccdd34c638346ea4fa7a713a8 @@ -84,8 +82,7 @@ func (im SInternationalMobile) String() string { } type ReceiverDetails struct { - apis.StatusStandaloneResourceDetails - apis.DomainizedResourceInfo + apis.EnabledStatusDomainLevelResourceDetails SReceiver InternationalMobile SInternationalMobile `json:"international_mobile"` @@ -105,9 +102,7 @@ type VerifiedInfo struct { } type ReceiverListInput struct { - apis.StatusStandaloneResourceListInput - apis.DomainizedResourceListInput - apis.EnabledResourceBaseListInput + apis.EnabledStatusDomainLevelResourceListInput UID string `json:"uid"` @@ -121,7 +116,7 @@ type ReceiverListInput struct { } type ReceiverUpdateInput struct { - apis.StatusStandaloneResourceBaseUpdateInput + apis.EnabledStatusDomainLevelResourceBaseUpdateInput // description: user email // example: example@gmail.com diff --git a/pkg/apis/notify/robot.go b/pkg/apis/notify/robot.go index 5af17b29d7..e3cc0dcf1f 100644 --- a/pkg/apis/notify/robot.go +++ b/pkg/apis/notify/robot.go @@ -18,6 +18,7 @@ import "yunion.io/x/onecloud/pkg/apis" type RobotCreateInput struct { apis.SharableVirtualResourceCreateInput + apis.EnabledBaseResourceCreateInput // description: robot type // enum: feishu,dingtalk,workwx,webhook // example: webhook diff --git a/pkg/apis/websocket/const.go b/pkg/apis/websocket/const.go new file mode 100644 index 0000000000..e20d3f6a91 --- /dev/null +++ b/pkg/apis/websocket/const.go @@ -0,0 +1,5 @@ +package websocket + +const ( + SERVICE_TYPE_WEBSOCKET = "websocket" +) diff --git a/pkg/notify/rpc/doc.go b/pkg/apis/websocket/doc.go similarity index 90% rename from pkg/notify/rpc/doc.go rename to pkg/apis/websocket/doc.go index b60fcc9105..370f678e1a 100644 --- a/pkg/notify/rpc/doc.go +++ b/pkg/apis/websocket/doc.go @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -package rpc // import "yunion.io/x/onecloud/pkg/notify/rpc" +package websocket // import "yunion.io/x/onecloud/pkg/apis" diff --git a/pkg/cloudcommon/db/keystonecache.go b/pkg/cloudcommon/db/keystonecache.go index 3535a6f4ed..474640766e 100644 --- a/pkg/cloudcommon/db/keystonecache.go +++ b/pkg/cloudcommon/db/keystonecache.go @@ -31,6 +31,7 @@ type SKeystoneCacheObject struct { Domain string `width:"128" charset:"utf8" nullable:"true"` LastCheck time.Time `nullable:"true"` + Lang string `width:"8" charset:"ascii" nullable:"true" list:"domain" update:"domain" create:"domain_optional"` } func NewKeystoneCacheObjectManager(dt interface{}, tableName string, keyword string, keywordPlural string) SKeystoneCacheObjectManager { diff --git a/pkg/cloudcommon/db/usercache.go b/pkg/cloudcommon/db/usercache.go index f1b8cbb74b..ac63423962 100644 --- a/pkg/cloudcommon/db/usercache.go +++ b/pkg/cloudcommon/db/usercache.go @@ -64,7 +64,7 @@ func init() { func (manager *SUserCacheManager) updateUserCache(ctx context.Context, userCred mcclient.TokenCredential) { manager.Save(ctx, userCred.GetUserId(), userCred.GetUserName(), - userCred.GetDomainId(), userCred.GetDomainName()) + userCred.GetDomainId(), userCred.GetDomainName(), "") } func (manager *SUserCacheManager) FetchUserByIdOrName(ctx context.Context, idStr string) (*SUser, error) { @@ -143,10 +143,11 @@ func (manager *SUserCacheManager) FetchUserFromKeystone(ctx context.Context, idS name, _ := user.GetString("name") domainId, _ := user.GetString("domain_id") domainNmae, _ := user.GetString("project_domain") - return manager.Save(ctx, id, name, domainId, domainNmae) + lang, _ := user.GetString("lang") + return manager.Save(ctx, id, name, domainId, domainNmae, lang) } -func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string, domain string) (*SUser, error) { +func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string, domain, lang string) (*SUser, error) { lockman.LockRawObject(ctx, manager.KeywordPlural(), idStr) defer lockman.ReleaseRawObject(ctx, manager.KeywordPlural(), idStr) @@ -163,6 +164,9 @@ func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name s obj.Domain = domain obj.DomainId = domainId obj.LastCheck = time.Now().UTC() + if len(lang) > 0 { + obj.Lang = lang + } return nil }) if err != nil { @@ -178,6 +182,7 @@ func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name s obj.Domain = domain obj.DomainId = domainId obj.LastCheck = time.Now().UTC() + obj.Lang = lang err = manager.TableSpec().InsertOrUpdate(ctx, obj) if err != nil { return nil, err diff --git a/pkg/cloudcommon/notifyclient/events.go b/pkg/cloudcommon/notifyclient/events.go index 7f0dc15364..7a641d9bf2 100644 --- a/pkg/cloudcommon/notifyclient/events.go +++ b/pkg/cloudcommon/notifyclient/events.go @@ -63,9 +63,10 @@ var ( ActionPendingDelete = api.ActionPendingDelete - ActionSyncCreate = api.ActionSyncCreate - ActionSyncUpdate = api.ActionSyncUpdate - ActionSyncDelete = api.ActionSyncDelete + ActionSyncCreate = api.ActionSyncCreate + ActionSyncUpdate = api.ActionSyncUpdate + ActionSyncDelete = api.ActionSyncDelete + ActionSyncAccountStatus = api.ActionSyncAccountStatus ) type SEvent struct { diff --git a/pkg/cloudcommon/notifyclient/notify.go b/pkg/cloudcommon/notifyclient/notify.go index 03b69bf5b5..57cb21b461 100644 --- a/pkg/cloudcommon/notifyclient/notify.go +++ b/pkg/cloudcommon/notifyclient/notify.go @@ -313,6 +313,8 @@ func EventNotify(ctx context.Context, userCred mcclient.TokenCredential, ep SEve Priority: string(npk.NotifyPriorityNormal), ProjectId: projectId, ProjectDomainId: projectDomainId, + ResourceType: ep.ResourceType, + Action: ep.Action, } t := eventTask{ params: params, @@ -336,6 +338,8 @@ func EventNotifyServiceAbnormal(ctx context.Context, userCred mcclient.TokenCred Event: event.String(), AdvanceDays: 0, Priority: string(npk.NotifyPriorityNormal), + ResourceType: api.TOPIC_RESOURCE_SERVICE, + Action: api.ActionServiceAbnormal, } t := eventTask{ params: params, diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index c9924897e5..01e2b434ef 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -215,7 +215,6 @@ func (manager *SGuestManager) ListItemFilter( query api.ServerListInput, ) (*sqlchemy.SQuery, error) { var err error - q, err = manager.SHostResourceBaseManager.ListItemFilter(ctx, q, userCred, query.HostFilterListInput) if err != nil { return nil, errors.Wrap(err, "SHostResourceBaseManager.ListItemFilter") diff --git a/pkg/mcclient/modules/websocket/doc.go b/pkg/mcclient/modules/websocket/doc.go new file mode 100644 index 0000000000..e2ffc254cb --- /dev/null +++ b/pkg/mcclient/modules/websocket/doc.go @@ -0,0 +1 @@ +package websocket // import "yunion.io/x/cloudpods/pkg/mcclient/modules/websocket" diff --git a/pkg/mcclient/modules/websocket/managers.go b/pkg/mcclient/modules/websocket/managers.go new file mode 100644 index 0000000000..281aabfbf7 --- /dev/null +++ b/pkg/mcclient/modules/websocket/managers.go @@ -0,0 +1,17 @@ +package websocket + +import ( + apis "yunion.io/x/onecloud/pkg/apis/websocket" + "yunion.io/x/onecloud/pkg/mcclient/modulebase" +) + +/* +添加新manager注意事项: +1. version字段 -- 在endpoint中注册的url如果携带版本。例如http://x.x.x.x/api/v1,那么必须标注对应version字段。否者可能导致yunionapi报资源not found的错误。 +*/ + +func newWebsocketManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager { + return modulebase.ResourceManager{ + BaseManager: *modulebase.NewBaseManager(apis.SERVICE_TYPE_WEBSOCKET, "", "", columns, adminColumns), + Keyword: keyword, KeywordPlural: keywordPlural} +} diff --git a/pkg/notify/oldmodels/doc.go b/pkg/mcclient/modules/websocket/mod_websockets.go similarity index 61% rename from pkg/notify/oldmodels/doc.go rename to pkg/mcclient/modules/websocket/mod_websockets.go index 1953161f39..255c8588cc 100644 --- a/pkg/notify/oldmodels/doc.go +++ b/pkg/mcclient/modules/websocket/mod_websockets.go @@ -12,4 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -package oldmodels // import "yunion.io/x/onecloud/pkg/notify/oldmodels" +package websocket + +import ( + "yunion.io/x/onecloud/pkg/mcclient/modulebase" +) + +type SWebsocketManager struct { + modulebase.ResourceManager +} + +var ( + Websockets SWebsocketManager +) + +func init() { + + Websockets = SWebsocketManager{newWebsocketManager("websocket", "websockets", + // user view + []string{}, + []string{}, // admin view + )} + + register(&Websockets) +} diff --git a/pkg/mcclient/modules/websocket/register.go b/pkg/mcclient/modules/websocket/register.go new file mode 100755 index 0000000000..cb07c81d2e --- /dev/null +++ b/pkg/mcclient/modules/websocket/register.go @@ -0,0 +1,7 @@ +package websocket + +import "yunion.io/x/onecloud/pkg/mcclient/modulebase" + +func register(mod modulebase.IBaseManager) { + modulebase.Register(mod) +} diff --git a/pkg/notify/interface.go b/pkg/notify/interface.go deleted file mode 100644 index 8cc7e2968c..0000000000 --- a/pkg/notify/interface.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package notify - -import ( - "context" - - "yunion.io/x/pkg/errors" - - notify_apis "yunion.io/x/onecloud/pkg/apis/notify" - "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/notify/rpc/apis" -) - -type INotifyService interface { - InitAll() error - StopAll() - UpdateServices(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) - UpdateConfig(ctx context.Context, service string, config SConfig) error - Send(ctx context.Context, contactType string, args apis.SendParams) error - ContactByMobile(ctx context.Context, mobile, serviceName, domainId string) (string, error) - BatchSend(ctx context.Context, contactType string, args apis.BatchSendParams) ([]*apis.FailedRecord, error) - SendRobotMessage(ctx context.Context, rType string, receivers []*apis.SReceiver, title string, message string) ([]*apis.FailedRecord, error) - AddConfig(ctx context.Context, service string, config SConfig) error - DeleteConfig(ctx context.Context, service, domainId string) error - ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error) -} - -type SSendParams struct { - ContactType string - Contact string - Topic string - Message string - Priority string - Lang string -} - -type SBatchSendParams struct { - ContactType string - Contacts []string - Topic string - Message string - Priority string - Lang string -} - -type IServiceConfigStore interface { - GetConfigs(service string) ([]SConfig, error) - SetConfig(service string, config SConfig) error -} - -type SNotification struct { - ContactType string - Topic string - Message string - Event notify_apis.SNotifyEvent - AdvanceDays int -} - -type ITemplateStore interface { - // NotifyFilter(contactType, topic, msg, lang string) (params apis.SendParams, err error) - FillWithTemplate(ctx context.Context, lang string, notification SNotification) (params apis.SendParams, err error) -} - -type SConfig struct { - Config map[string]string - DomainId string -} - -var ( - ErrNoSuchMobile = errors.Error("no such mobile") - ErrIncompleteConfig = errors.Error("incomplete config") -) diff --git a/pkg/notify/models/config.go b/pkg/notify/models/config.go index 19cd62ee27..714f010d04 100644 --- a/pkg/notify/models/config.go +++ b/pkg/notify/models/config.go @@ -26,30 +26,28 @@ import ( "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" + "yunion.io/x/onecloud/pkg/apis/notify" api "yunion.io/x/onecloud/pkg/apis/notify" - "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" - "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" - notifyv2 "yunion.io/x/onecloud/pkg/notify" - "yunion.io/x/onecloud/pkg/notify/oldmodels" "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/util/logclient" "yunion.io/x/onecloud/pkg/util/stringutils2" ) type SConfigManager struct { - db.SStandaloneResourceBaseManager - db.SDomainizedResourceBaseManager + db.SDomainLevelResourceBaseManager } var ConfigManager *SConfigManager +var ConfigMap map[string]SConfig func init() { ConfigManager = &SConfigManager{ - SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager( + SDomainLevelResourceBaseManager: db.NewDomainLevelResourceBaseManager( SConfig{}, "configs_tbl", "notifyconfig", @@ -60,63 +58,45 @@ func init() { } type SConfig struct { - db.SStandaloneResourceBase - db.SDomainizedResourceBase + db.SDomainLevelResourceBase - Type string `width:"15" nullable:"false" create:"required" get:"domain" list:"domain" index:"true"` - Content jsonutils.JSONObject `nullable:"false" create:"required" update:"domain" get:"domain" list:"domain"` - Attribution string `width:"8" nullable:"false" default:"system" get:"domain" list:"domain" create:"optional"` + Type string `width:"15" nullable:"false" create:"required" get:"domain" list:"domain" index:"true"` + Content *api.SNotifyConfigContent `nullable:"false" create:"required" update:"domain" get:"domain" list:"domain"` + Attribution string `width:"8" nullable:"false" default:"system" get:"domain" list:"domain" create:"optional"` } func (cm *SConfigManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ConfigCreateInput) (api.ConfigCreateInput, error) { var err error - input.StandaloneResourceCreateInput, err = cm.SStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StandaloneResourceCreateInput) + input.DomainLevelResourceCreateInput, err = cm.SDomainLevelResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.DomainLevelResourceCreateInput) if err != nil { return input, err } - if len(input.ProjectDomainId) > 0 { - _, input.DomainizedResourceInput, err = db.ValidateDomainizedResourceInput(ctx, input.DomainizedResourceInput) - if err != nil { - return input, err - } - } - if !utils.IsInStringArray(input.Type, []string{api.EMAIL, api.MOBILE, api.DINGTALK, api.FEISHU, api.WEBCONSOLE, api.WORKWX}) { - return input, httperrors.NewInputParameterError("unkown type %q", input.Type) + if !utils.IsInStringArray(input.Type, GetSenderTypes()) { + return input, httperrors.NewInputParameterError("unkown type %s allow: %s", input.Type, GetSenderTypes()) } if !utils.IsInStringArray(input.Attribution, []string{api.CONFIG_ATTRIBUTION_SYSTEM, api.CONFIG_ATTRIBUTION_DOMAIN}) { return input, httperrors.NewInputParameterError("invalid attribution, need %q or %q", api.CONFIG_ATTRIBUTION_SYSTEM, api.CONFIG_ATTRIBUTION_DOMAIN) } - if input.Attribution == api.CONFIG_ATTRIBUTION_SYSTEM { - allowScope, _ := policy.PolicyManager.AllowScope(userCred, consts.GetServiceType(), ConfigManager.KeywordPlural(), policy.PolicyActionCreate) - if allowScope != rbacscope.ScopeSystem { - return input, httperrors.NewInputParameterError("No permission to set %q attribution", api.CONFIG_ATTRIBUTION_SYSTEM) - } - } if input.Content == nil { return input, httperrors.NewMissingParameterError("content") } config, err := cm.Config(input.Type, input.ProjectDomainId, input.Attribution) - if err == nil && config != nil { + if config != nil { return input, httperrors.NewDuplicateResourceError("duplicate type %q", input.Type) } if err != nil && errors.Cause(err) != sql.ErrNoRows { return input, err } + driver := GetDriver(input.Type) + // validate - configs := make(map[string]string) - err = input.Content.Unmarshal(&configs) + message, err := driver.ValidateConfig(api.NotifyConfig{ + SNotifyConfigContent: *input.Content, + Attribution: input.Attribution, + DomainId: input.ProjectDomainId, + }) if err != nil { - return input, err - } - isValid, message, err := NotifyService.ValidateConfig(ctx, input.Type, configs) - if err != nil { - if errors.Cause(err) == errors.ErrNotImplemented { - return input, httperrors.NewNotImplementedError("validating config of %s", input.Type) - } - return input, err - } - if !isValid { - return input, httperrors.NewInputParameterError(message) + return input, errors.Wrapf(err, message) } if len(input.Name) == 0 { input.Name = input.Type @@ -124,73 +104,57 @@ func (cm *SConfigManager) ValidateCreateData(ctx context.Context, userCred mccli return input, nil } -func (c *SConfig) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { - err := c.SStandaloneResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) +func (c *SConfig) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + c.SDomainLevelResourceBase.PostCreate(ctx, userCred, ownerId, query, data) + err := c.StartRepullSubcontactTask(ctx, userCred, false) if err != nil { - return err + log.Errorf("unable to StartRepullSubcontactTask: %v", err) } - if c.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN || c.Attribution == "" { - c.Attribution = api.CONFIG_ATTRIBUTION_DOMAIN - c.DomainId, _ = data.GetString("project_domain_id") - if c.DomainId == "" { - c.DomainId = userCred.GetProjectDomainId() - } + ConfigMap[c.Type] = *c +} + +func (c *SConfig) GetNotifyConfig() api.NotifyConfig { + return api.NotifyConfig{ + SNotifyConfigContent: *c.Content, + Attribution: c.Attribution, + DomainId: c.DomainId, } - return nil } func (c *SConfig) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ConfigUpdateInput) (api.ConfigUpdateInput, error) { - // validate - configs := make(map[string]string) - err := input.Content.Unmarshal(&configs) + q := ConfigManager.Query() + q = q.Equals("type", c.Type) + confs := []SConfig{} + err := db.FetchModelObjects(ConfigManager, q, &confs) if err != nil { - return input, err + return input, errors.Wrapf(err, "config type:%s", c.Type) } + if len(confs) == 0 { + return input, errors.Wrapf(errors.ErrNotFound, "config type:%s", c.Type) + } + // check if changed - if c.Content.Equals(input.Content) { - return input, nil - } - isValid, message, err := NotifyService.ValidateConfig(ctx, c.Type, configs) - if err != nil { - if errors.Cause(err) == errors.ErrNotImplemented { - return input, httperrors.NewNotImplementedError("validating config of %s", c.Type) + if input.Content != nil { + driver := GetDriver(c.Type) + message, err := driver.ValidateConfig(api.NotifyConfig{ + DomainId: c.DomainId, + Attribution: c.Attribution, + SNotifyConfigContent: *input.Content, + }) + if err != nil { + return input, errors.Wrapf(err, message) } - return input, err - } - if !isValid { - return input, httperrors.NewInputParameterError(message) } return input, nil } -func (c *SConfig) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { - c.SStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data) - configMap := make(map[string]string) - err := c.Content.Unmarshal(&configMap) - if err != nil { - log.Errorf("unable to unmarshal: %v", err) - return - } - NotifyService.AddConfig(ctx, c.Type, c.Config()) - err = c.StartRepullSubcontactTask(ctx, userCred, false) - if err != nil { - log.Errorf("unable to StartRepullSubcontactTask: %v", err) - } -} - func (c *SConfig) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { c.SStandaloneResourceBase.PostUpdate(ctx, userCred, query, data) - configMap := make(map[string]string) - err := c.Content.Unmarshal(&configMap) - if err != nil { - log.Errorf("unable to unmarshal: %v", err) - return + config := c.GetNotifyConfig() + ConfigMap[c.Type] = SConfig{ + Content: &config.SNotifyConfigContent, } - NotifyService.UpdateConfig(ctx, c.Type, notifyv2.SConfig{ - Config: configMap, - DomainId: c.DomainId, - }) - err = c.StartRepullSubcontactTask(ctx, userCred, false) + err := c.StartRepullSubcontactTask(ctx, userCred, false) if err != nil { log.Errorf("unable to StartRepullSubcontactTask: %v", err) } @@ -198,7 +162,7 @@ func (c *SConfig) PostUpdate(ctx context.Context, userCred mcclient.TokenCredent func (c *SConfig) PreDelete(ctx context.Context, userCred mcclient.TokenCredential) { c.SStandaloneResourceBase.PreDelete(ctx, userCred) - NotifyService.DeleteConfig(ctx, c.Type, c.Config().DomainId) + delete(ConfigMap, c.Type) } func (c *SConfig) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { @@ -206,12 +170,7 @@ func (c *SConfig) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCr if err != nil { return err } - NotifyService.DeleteConfig(ctx, c.Type, c.Config().DomainId) - err = c.StartRepullSubcontactTask(ctx, userCred, true) - if err != nil { - return errors.Wrap(err, "unable to start repull subcontact") - } - return err + return c.StartRepullSubcontactTask(ctx, userCred, true) } func (c *SConfig) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { @@ -219,7 +178,24 @@ func (c *SConfig) Delete(ctx context.Context, userCred mcclient.TokenCredential) } func (c *SConfig) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error { - return c.SStandaloneResourceBase.Delete(ctx, userCred) + delete(ConfigMap, c.Type) + return c.SDomainLevelResourceBase.Delete(ctx, userCred) +} + +func (self *SConfig) GetReceivers() ([]SReceiver, error) { + subq := SubContactManager.Query("receiver_id").Equals("type", self.Type).SubQuery() + q := ReceiverManager.Query() + if self.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN { + q = q.Equals("domain_id", self.DomainId) + } else { + // The system-level config update should not affect the receiver under the domain with config + configq := ConfigManager.Query("domain_id").Equals("type", self.Type).Equals("attribution", api.CONFIG_ATTRIBUTION_DOMAIN).SubQuery() + q = q.NotIn("domain_id", configq) + } + q = q.Join(subq, sqlchemy.Equals(q.Field("id"), subq.Field("receiver_id"))) + ret := []SReceiver{} + err := db.FetchModelObjects(ReceiverManager, q, &ret) + return ret, err } func (c *SConfig) StartRepullSubcontactTask(ctx context.Context, userCred mcclient.TokenCredential, del bool) error { @@ -296,11 +272,7 @@ func (cm *SConfigManager) allContactType() ([]string, error) { } func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ConfigListInput) (*sqlchemy.SQuery, error) { - q, err := self.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput) - if err != nil { - return nil, err - } - q, err = self.SDomainizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainizedResourceListInput) + q, err := self.SDomainLevelResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainLevelResourceListInput) if err != nil { return nil, err } @@ -311,12 +283,11 @@ func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQue if len(input.Attribution) > 0 { q = q.Equals("attribution", input.Attribution) } - return q, nil } func (manager *SConfigManager) ListItemExportKeys(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, keys stringutils2.SSortedStrings) (*sqlchemy.SQuery, error) { - return manager.SStandaloneResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + return manager.SDomainLevelResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) } func (cm *SConfigManager) FetchCustomizeColumns( @@ -327,22 +298,16 @@ func (cm *SConfigManager) FetchCustomizeColumns( fields stringutils2.SSortedStrings, isList bool, ) []api.ConfigDetails { - sRows := cm.SStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) - dRows := cm.SDomainizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + sRows := cm.SDomainLevelResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) rows := make([]api.ConfigDetails, len(objs)) for i := range rows { - rows[i].StandaloneResourceDetails = sRows[i] - rows[i].DomainizedResourceInfo = dRows[i] + rows[i].DomainLevelResourceDetails = sRows[i] } return rows } func (cm *SConfigManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { - q, err := cm.SStandaloneResourceBaseManager.QueryDistinctExtraField(q, field) - if err != nil { - return q, nil - } - q, err = cm.SDomainizedResourceBaseManager.QueryDistinctExtraField(q, field) + q, err := cm.SDomainLevelResourceBaseManager.QueryDistinctExtraField(q, field) if err != nil { return q, nil } @@ -350,11 +315,7 @@ func (cm *SConfigManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field stri } func (cm *SConfigManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.ConfigListInput) (*sqlchemy.SQuery, error) { - q, err := cm.SStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StandaloneResourceListInput) - if err != nil { - return nil, err - } - q, err = cm.SDomainizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainizedResourceListInput) + q, err := cm.SDomainLevelResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainLevelResourceListInput) if err != nil { return nil, err } @@ -362,155 +323,49 @@ func (cm *SConfigManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQ } func (cm *SConfigManager) PerformValidate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ConfigValidateInput) (api.ConfigValidateOutput, error) { - var ( - output api.ConfigValidateOutput - err error - ) - if !utils.IsInStringArray(input.Type, []string{api.EMAIL, api.MOBILE, api.DINGTALK, api.FEISHU, api.WEBCONSOLE, api.WORKWX, api.FEISHU_ROBOT, api.DINGTALK_ROBOT, api.WORKWX_ROBOT}) { + output := api.ConfigValidateOutput{} + if !utils.IsInStringArray(input.Type, GetSenderTypes()) { return output, httperrors.NewInputParameterError("unkown type %q", input.Type) } if input.Content == nil { return output, httperrors.NewMissingParameterError("content") } // validate - configs := make(map[string]string) - err = input.Content.Unmarshal(&configs) + driver := GetDriver(input.Type) + message, err := driver.ValidateConfig(api.NotifyConfig{ + SNotifyConfigContent: *input.Content, + }) if err != nil { - return output, err - } - isValid, message, err := NotifyService.ValidateConfig(ctx, input.Type, configs) - if err != nil { - if errors.Cause(err) == errors.ErrNotImplemented { - return output, httperrors.NewNotImplementedError("validating config of %s", input.Type) - } - return output, err - } - if !isValid { - output.IsValid = false - output.Message = message - } else { - output.IsValid = true + return output, errors.Wrapf(err, message) } + output.IsValid = true + output.Message = message return output, nil } -func (self *SConfigManager) InitializeData() error { - ctx := context.Background() - userCred := auth.AdminCredential() - // fetch all configs - configs := make([]oldmodels.SConfig, 0, 5) - q := oldmodels.ConfigManager.Query() - err := db.FetchModelObjects(oldmodels.ConfigManager, q, &configs) +func (confManager *SConfigManager) InitializeData() error { + q := confManager.Query() + res := []SConfig{} + err := db.FetchModelObjects(confManager, q, &res) if err != nil { - return errors.Wrap(err, "db.FetchModelObjects") + return errors.Wrap(err, "init configMap err") } - - // build type==>config map - tcMap := make(map[string][]*oldmodels.SConfig) - for i := range configs { - t := configs[i].Type - if _, ok := tcMap[t]; !ok { - tcMap[t] = make([]*oldmodels.SConfig, 0, 3) + ConfigMap = make(map[string]SConfig) + for _, config := range res { + ConfigMap[config.Type] = config + driver := GetDriver(config.Type) + if config.Type == notify.EMAIL || config.Type == notify.MOBILE { + continue } - tcMap[t] = append(tcMap[t], &configs[i]) - } - - for t, configs := range tcMap { - cMap := make(map[string]string) - if t == api.EMAIL { - for _, config := range configs { - switch config.KeyText { - case "mail.username": - cMap["username"] = config.ValueText - case "mail.password": - cMap["password"] = config.ValueText - case "mail.smtp.hostname": - cMap["hostname"] = config.ValueText - case "mail.smtp.hostport": - cMap["hostport"] = config.ValueText - } - } - } else { - for _, config := range configs { - cMap[config.KeyText] = config.ValueText - } - } - newConfig := SConfig{ - Type: t, - Content: jsonutils.Marshal(cMap), - } - err := self.TableSpec().Insert(ctx, &newConfig) + err := driver.GetAccessToken() if err != nil { - return errors.Wrap(err, "TableSpec().Insert") - } - for _, config := range configs { - err := config.Delete(ctx, userCred) - if err != nil { - return errors.Wrap(err, "Delete") - } - } - } - - // init webconsole's config - q = self.Query().Equals("type", api.WEBCONSOLE) - wsConfigs := make([]SConfig, 0, 2) - err = db.FetchModelObjects(self, q, &wsConfigs) - if err != nil && errors.Cause(err) != sql.ErrNoRows { - return errors.Wrap(err, "db.FetchModelObjects") - } - if len(wsConfigs) > 1 { - for i := 1; i < len(wsConfigs); i++ { - err := wsConfigs[i].Delete(ctx, userCred) - if err != nil { - return errors.Wrap(err, "Delete redundant") - } - } - } - - var config *SConfig - if len(wsConfigs) > 0 { - config = &wsConfigs[0] - } else { - config = &SConfig{ - Type: api.WEBCONSOLE, - } - } - config.Content = jsonutils.Marshal(map[string]string{ - "auth_uri": options.Options.AuthURL, - "admin_user": options.Options.AdminUser, - "admin_password": options.Options.AdminPassword, - "admin_tenant_name": options.Options.AdminProject, - }) - err = self.TableSpec().InsertOrUpdate(context.TODO(), config) - - // init config name - q = self.Query().IsNullOrEmpty("name") - enConfigs := make([]SConfig, 0) - err = db.FetchModelObjects(self, q, &enConfigs) - if err != nil { - return errors.Wrap(err, "unable to get configs with empty name") - } - for i := range enConfigs { - c := &enConfigs[i] - name, err := db.GenerateAlterName(c, enConfigs[i].Type) - if err != nil { - return errors.Wrap(err, "unable to generate alter name") - } - _, err = db.Update(c, func() error { - c.Name = name - return nil - }) - if err != nil { - return errors.Wrap(err, "unable to update name") + session := auth.GetAdminSession(context.Background(), options.Options.Region) + logclient.AddSimpleActionLog(&config, logclient.ACT_INIT_NOTIFY_CONFIGMAP, err, session.GetToken(), false) } } return nil } -func (cm *SConfigManager) ResourceScope() rbacscope.TRbacScope { - return rbacscope.ScopeDomain -} - func (cm *SConfigManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery { switch scope { case rbacscope.ScopeDomain, rbacscope.ScopeProject: @@ -536,11 +391,9 @@ func (self *SConfigManager) Configs(contactType string) ([]SConfig, error) { func (self *SConfigManager) Config(contactType, domainId string, attribution string) (*SConfig, error) { q := self.Query() - q = q.Equals("type", contactType) - if attribution == api.CONFIG_ATTRIBUTION_SYSTEM { - q = q.Equals("attribution", api.CONFIG_ATTRIBUTION_SYSTEM) - } else { - q = q.Equals("domain_id", domainId).Equals("attribution", api.CONFIG_ATTRIBUTION_DOMAIN) + q = q.Equals("type", contactType).Equals("attribution", attribution) + if attribution == api.CONFIG_ATTRIBUTION_DOMAIN { + q = q.Equals("domain_id", domainId) } var config SConfig err := q.First(&config) @@ -551,107 +404,10 @@ func (self *SConfigManager) Config(contactType, domainId string, attribution str } func (self *SConfigManager) HasSystemConfig(contactType string) (bool, error) { - q := self.Query().Equals("type", contactType).Equals("attribution", "system") + q := self.Query().Equals("type", contactType).Equals("attribution", api.CONFIG_ATTRIBUTION_SYSTEM) c, err := q.CountWithError() if err != nil { return false, err } return c > 0, nil } - -func (self *SConfigManager) BatchCheckConfig(contactType string, domainIds []string) ([]bool, error) { - domainIdSet := sets.NewString(domainIds...) - var configs = make([]SConfig, 0, 2) - q := self.Query().Equals("type", contactType).Equals("attribution", "domain").In("domain_id", domainIdSet.UnsortedList()) - err := q.All(&configs) - if err != nil { - return nil, errors.Wrapf(err, "fail to fetch SConfigs by type %s", contactType) - } - for i := range configs { - if domainIdSet.Has(configs[i].DomainId) { - domainIdSet.Delete(configs[i].DomainId) - } - } - ret := make([]bool, len(domainIds)) - for i := range domainIds { - if domainIdSet.Has(domainIds[i]) { - // no config of domainId, use default one - ret[i] = false - } - ret[i] = true - } - return ret, nil -} - -func (self *SConfigManager) GetConfigs(contactType string) ([]notifyv2.SConfig, error) { - configs, err := self.Configs(contactType) - if err != nil { - return nil, err - } - ret := make([]notifyv2.SConfig, 0, len(configs)) - for i := range configs { - ret = append(ret, configs[i].Config()) - } - return ret, nil -} - -func (manager *SConfigManager) getEmailConfig() (*api.SEmailConfig, error) { - confs, err := manager.GetConfigs(api.EMAIL) - if err != nil { - return nil, errors.Wrap(err, "GetConfigs") - } - if len(confs) == 0 { - return nil, errors.Wrap(errors.ErrNotSupported, "email not supported") - } - conf := api.SEmailConfig{} - err = jsonutils.Marshal(confs[0].Config).Unmarshal(&conf) - if err != nil { - return nil, errors.Wrap(err, "Unmarshal") - } - return &conf, nil -} - -func (self *SConfigManager) SetConfig(contactType string, config notifyv2.SConfig) error { - content := jsonutils.Marshal(config.Config) - sConfig := &SConfig{ - Type: contactType, - Content: content, - } - sConfig.DomainId = config.DomainId - if sConfig.DomainId == "" { - sConfig.Attribution = api.CONFIG_ATTRIBUTION_SYSTEM - } else { - sConfig.Attribution = api.CONFIG_ATTRIBUTION_DOMAIN - } - return self.TableSpec().InsertOrUpdate(context.Background(), sConfig) -} - -func (self *SConfig) Config() notifyv2.SConfig { - c := make(map[string]string) - _ = self.Content.Unmarshal(&c) - sc := notifyv2.SConfig{ - Config: c, - } - if self.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN { - sc.DomainId = self.DomainId - } - return sc -} - -func intersection(sa1, sa2 []string) []string { - set1 := sets.NewString(sa1...) - set2 := sets.NewString(sa2...) - return set1.Intersection(set2).UnsortedList() -} - -func difference(sa1, sa2 []string) []string { - set1 := sets.NewString(sa1...) - set2 := sets.NewString(sa2...) - return set1.Difference(set2).UnsortedList() -} - -func union(sa1, sa2 []string) []string { - set1 := sets.NewString(sa1...) - set2 := sets.NewString(sa2...) - return set1.Union(set2).UnsortedList() -} diff --git a/pkg/notify/models/emailqueue.go b/pkg/notify/models/emailqueue.go index 485688a02b..5f4292e29a 100644 --- a/pkg/notify/models/emailqueue.go +++ b/pkg/notify/models/emailqueue.go @@ -31,7 +31,6 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/notify/sender" "yunion.io/x/onecloud/pkg/util/stringutils2" ) @@ -134,7 +133,7 @@ func (eq *SEmailQueue) PostCreate( } func (eq *SEmailQueue) doSendAsync() { - sender.Worker.Run(eq, nil, nil) + Worker.Run(eq, nil, nil) } func (eq *SEmailQueue) Dump() string { @@ -147,26 +146,21 @@ func (eq *SEmailQueue) Run() { } func (eq *SEmailQueue) doSend(ctx context.Context) { - conf, err := ConfigManager.getEmailConfig() - if err != nil { - eq.setStatus(ctx, api.EmailFail, err) - return - } - log.Debugf("conf: %s", jsonutils.Marshal(conf)) msg, err := eq.getMessage() if err != nil { eq.setStatus(ctx, api.EmailFail, err) return } - log.Debugf("msg: %s", jsonutils.Marshal(msg)) eq.setStatus(ctx, api.EmailSending, nil) - err = sender.SendEmail(conf, msg) + driver := GetDriver(api.EMAIL) + driver.Send(api.SendParams{ + EmailMsg: msg, + }) if err != nil { eq.setStatus(ctx, api.EmailFail, err) return } eq.setStatus(ctx, api.EmailSuccess, nil) - return } func (eq *SEmailQueue) getMessage() (*api.SEmailMessage, error) { diff --git a/pkg/notify/models/event.go b/pkg/notify/models/event.go index b7ebe23fd9..af278bebcb 100644 --- a/pkg/notify/models/event.go +++ b/pkg/notify/models/event.go @@ -41,18 +41,22 @@ type SEvent struct { // 资源创建时间 CreatedAt time.Time `nullable:"false" created_at:"true" index:"true" get:"user" list:"user" json:"created_at"` - Message string - Event string `width:"64" nullable:"true"` - AdvanceDays int - TopicId string `width:"128" nullable:"true" index:"true"` + Message string + Event string `width:"64" nullable:"true"` + ResourceType string `width:"64" nullable:"true"` + Action string `width:"64" nullable:"true"` + AdvanceDays int + TopicId string `width:"128" nullable:"true" index:"true"` } -func (e *SEventManager) CreateEvent(ctx context.Context, event, topicId, message string, advanceDays int) (*SEvent, error) { +func (e *SEventManager) CreateEvent(ctx context.Context, event, topicId, message, action, resourceType string, advanceDays int) (*SEvent, error) { eve := &SEvent{ - Message: message, - Event: event, - AdvanceDays: advanceDays, - TopicId: topicId, + Message: message, + Event: event, + AdvanceDays: advanceDays, + TopicId: topicId, + Action: action, + ResourceType: resourceType, } err := e.TableSpec().Insert(ctx, eve) if err != nil { diff --git a/pkg/notify/models/event_template.go b/pkg/notify/models/event_template.go index 0985a9b98a..9c1b97e860 100644 --- a/pkg/notify/models/event_template.go +++ b/pkg/notify/models/event_template.go @@ -36,8 +36,6 @@ import ( api "yunion.io/x/onecloud/pkg/apis/notify" schapi "yunion.io/x/onecloud/pkg/apis/scheduledtask" "yunion.io/x/onecloud/pkg/i18n" - notifyv2 "yunion.io/x/onecloud/pkg/notify" - rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis" ) type SEventDisplay struct { @@ -94,8 +92,9 @@ func (lt *SLocalTemplateManager) detailsDisplay(resourceType string, details *js } } -func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang string, no notifyv2.SNotification) (params rpcapi.SendParams, err error) { - out, event := rpcapi.SendParams{}, no.Event +func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) { + // return api.SendParams{}, nil + out, event := api.SendParams{}, no.Event rtStr, aStr, resultStr := event.ResourceType(), string(event.Action()), string(event.Result()) msgObj, err := jsonutils.ParseString(no.Message) if err != nil { @@ -111,7 +110,7 @@ func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang stri webhookMsg.Set("result", jsonutils.NewString(resultStr)) webhookMsg.Set("resource_details", msg) if no.ContactType == api.WEBHOOK { - return rpcapi.SendParams{ + return api.SendParams{ Title: no.Event.StringWithDeli("_"), Message: webhookMsg.String(), }, nil @@ -152,7 +151,6 @@ func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang stri return out, err } } - // get content content, err := lt.fillWithTemplate(ctx, "content", no.ContactType, lang, event, templateParams) if err != nil { @@ -259,7 +257,6 @@ func init() { func (lt *SLocalTemplateManager) getTemplate(ctx context.Context, titleOrContent string, contactType string, topic string, lang string) (*template.Template, error) { key := fmt.Sprintf("%s.%s@%s", topic, titleOrContent, lang) - obj, ok := lt.templatesTable.Load(key) var elem sTemplateElem if !ok { @@ -527,6 +524,11 @@ func init() { "created", "创建", }, + sI18nElme{ + string(api.ActionUpdate), + "update", + "更新", + }, sI18nElme{ string(api.ActionDelete), "deleted", diff --git a/pkg/notify/models/notification.go b/pkg/notify/models/notification.go index a6382512af..d7ed5cd816 100644 --- a/pkg/notify/models/notification.go +++ b/pkg/notify/models/notification.go @@ -18,6 +18,7 @@ import ( "context" "database/sql" "fmt" + "strings" "time" "yunion.io/x/jsonutils" @@ -31,11 +32,9 @@ import ( api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/validators" "yunion.io/x/onecloud/pkg/httperrors" - "yunion.io/x/onecloud/pkg/image/policy" "yunion.io/x/onecloud/pkg/mcclient" - notifyv2 "yunion.io/x/onecloud/pkg/notify" - "yunion.io/x/onecloud/pkg/notify/oldmodels" "yunion.io/x/onecloud/pkg/notify/options" "yunion.io/x/onecloud/pkg/util/stringutils2" ) @@ -45,7 +44,6 @@ type SNotificationManager struct { } var NotificationManager *SNotificationManager -var NotifyService notifyv2.INotifyService func init() { NotificationManager = &SNotificationManager{ @@ -60,22 +58,22 @@ func init() { NotificationManager.TableSpec().AddIndex(false, "deleted", "contact_type", "topic_type") } +// 站内信 type SNotification struct { db.SStatusStandaloneResourceBase - ContactType string `width:"16" nullable:"false" create:"required" list:"user" get:"user"` + ContactType string `width:"128" nullable:"true" create:"optional" list:"user" get:"user"` // swagger:ignore - Topic string `width:"128" nullable:"true" create:"required" search:"user"` + Topic string `width:"128" nullable:"true" create:"required" list:"user" get:"user"` Priority string `width:"16" nullable:"true" create:"optional" list:"user" get:"user"` // swagger:ignore - Message string `create:"required"` + Message string `create:"required"` + // swagger:ignore + TopicType string `json:"topic_type" width:"20" nullable:"true" create:"required" update:"user" list:"user"` ReceivedAt time.Time `nullable:"true" list:"user" get:"user"` EventId string `width:"128" nullable:"true"` - TopicType string `json:"topic_type" width:"20" nullable:"true" create:"required" update:"user" list:"user"` - SendTimes int - Tag string `width:"16" nullable:"true" index:"true" create:"optional"` } const ( @@ -83,72 +81,61 @@ const ( ) func (nm *SNotificationManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.NotificationCreateInput) (api.NotificationCreateInput, error) { - if len(input.Tag) > 0 && !utils.IsInStringArray(input.Tag, []string{api.NOTIFICATION_TAG_ALERT}) { - return input, httperrors.NewInputParameterError("invalid tag") - } - if len(input.Contacts) > 0 { - if userCred.IsAllow(rbacscope.ScopeSystem, api.SERVICE_TYPE, nm.KeywordPlural(), policy.PolicyActionPerform, SendByContact).Result.IsDeny() { - return input, httperrors.NewForbiddenError("only admin can send notification by contact") - } - if len(input.Contacts) == 0 { - input.Contacts = []string{""} - } + cTypes := []string{} + if len(input.Contacts) > 0 && !userCred.HasSystemAdminPrivilege() { + return input, httperrors.NewForbiddenError("only admin can send notification by contact") } // check robot - if len(input.Robots) > 0 { - input.ContactType = api.ROBOT - robots, err := RobotManager.FetchByIdOrNames(ctx, input.Robots...) - if err != nil { - return input, errors.Wrap(err, "RobotManager.FetchByIdOrNames") + robots := []string{} + for i := range input.Robots { + _robot, err := validators.ValidateModel(userCred, RobotManager, &input.Robots[i]) + if err != nil && !input.IgnoreNonexistentReceiver { + return input, err } - idSet := sets.NewString() - nameSet := sets.NewString() - for i := range robots { - idSet.Insert(robots[i].Id) - nameSet.Insert(robots[i].Name) - } - for _, re := range input.Receivers { - if idSet.Has(re) || nameSet.Has(re) { - continue + if _robot != nil { + robot := _robot.(*SRobot) + if !utils.IsInStringArray(robot.GetId(), robots) { + robots = append(robots, robot.GetId()) } - if !input.IgnoreNonexistentReceiver { - return input, httperrors.NewInputParameterError("no such robot whose id is %q", re) + if !utils.IsInStringArray(robot.Type, cTypes) { + cTypes = append(cTypes, robot.Type) } } - input.Robots = idSet.UnsortedList() - if len(input.Robots) == 0 { - return input, httperrors.NewInputParameterError("no valid receiver or contact") - } } + input.Robots = robots + // check receivers - if len(input.Receivers) > 0 { - receivers, err := ReceiverManager.FetchByIdOrNames(ctx, input.Receivers...) - if err != nil { - return input, errors.Wrap(err, "ReceiverManager.FetchByIDs") + receivers, err := ReceiverManager.FetchByIdOrNames(ctx, input.Receivers...) + if err != nil { + return input, errors.Wrap(err, "ReceiverManager.FetchByIDs") + } + idSet := sets.NewString() + nameSet := sets.NewString() + for i := range receivers { + idSet.Insert(receivers[i].Id) + nameSet.Insert(receivers[i].Name) + } + for _, re := range input.Receivers { + if idSet.Has(re) || nameSet.Has(re) { + continue } - idSet := sets.NewString() - nameSet := sets.NewString() - for i := range receivers { - idSet.Insert(receivers[i].Id) - nameSet.Insert(receivers[i].Name) + if input.ContactType == api.WEBCONSOLE { + input.Contacts = append(input.Contacts, re) } - for _, re := range input.Receivers { - if idSet.Has(re) || nameSet.Has(re) { - continue - } - if input.ContactType == api.WEBCONSOLE { - input.Contacts = append(input.Contacts, re) - } - if !input.IgnoreNonexistentReceiver { - return input, httperrors.NewInputParameterError("no such receiver whose uid is %q", re) - } - } - input.Receivers = idSet.UnsortedList() - if len(input.Receivers)+len(input.Contacts) == 0 { - return input, httperrors.NewInputParameterError("no valid receiver or contact") + if !input.IgnoreNonexistentReceiver { + return input, httperrors.NewInputParameterError("no such receiver whose uid is %q", re) } } + input.Receivers = idSet.UnsortedList() + if len(input.Receivers)+len(input.Contacts) == 0 { + return input, httperrors.NewInputParameterError("no valid receiver or contact") + } + + if len(input.Receivers)+len(input.Contacts)+len(input.Robots) == 0 { + return input, httperrors.NewInputParameterError("no valid receiver or contact") + } + input.ContactType = strings.Join(cTypes, ",") nowStr := time.Now().Format("2006-01-02 15:04:05") if len(input.Priority) == 0 { input.Priority = api.NOTIFICATION_PRIORITY_NORMAL @@ -159,12 +146,7 @@ func (nm *SNotificationManager) ValidateCreateData(ctx context.Context, userCred if len(topicRunes) < 10 { length = len(topicRunes) } - name := fmt.Sprintf("%s-%s-%s", string(topicRunes[:length]), input.ContactType, nowStr) - var err error - input.Name, err = db.GenerateName(ctx, nm, ownerId, name) - if err != nil { - return input, errors.Wrapf(err, "unable to generate name for %s", name) - } + input.GenerateName = fmt.Sprintf("%s-%s-%s", string(topicRunes[:length]), input.ContactType, nowStr) return input, nil } @@ -198,32 +180,19 @@ func (n *SNotification) CustomizeCreate(ctx context.Context, userCred mcclient.T } func (n *SNotification) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { - if data.Contains("metadata") { - metadata := make(map[string]interface{}) - err := data.Unmarshal(&metadata, "metadata") - if err != nil { - log.Errorf("unable to unmarshal to metadata: %v", err) - } else { - n.SetAllMetadata(ctx, metadata, userCred) - } - } + n.SStatusStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data) n.SetStatus(userCred, api.NOTIFICATION_STATUS_RECEIVED, "") task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") if err != nil { - log.Errorf("NotificationSendTask newTask error %v", err) - } else { - task.ScheduleRun(nil) + n.SetStatus(userCred, api.NOTIFICATION_STATUS_FAILED, "NewTask") + return } + task.ScheduleRun(nil) } // TODO: support project and domain func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.NotificationManagerEventNotifyInput) (api.NotificationManagerEventNotifyOutput, error) { var output api.NotificationManagerEventNotifyOutput - // check event - _, err := parseEvent(input.Event) - if err != nil { - return output, httperrors.NewInputParameterError("unable to parse event %q", input.Event) - } // contact type contactTypes := input.ContactTypes cts, err := ConfigManager.allContactType() @@ -231,11 +200,9 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred return output, errors.Wrap(err, "unable to fetch allContactType") } if len(contactTypes) == 0 { - contactTypes = intersection(cts, PersonalConfigContactTypes) + contactTypes = append(contactTypes, cts...) } - // receiver - topic, err := TopicManager.TopicByEvent(input.Event, input.AdvanceDays) if err != nil { return output, errors.Wrapf(err, "unable fetch subscriptions by event %q", input.Event) @@ -301,7 +268,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred receiverIds = idSet.UnsortedList() // create event - event, err := EventManager.CreateEvent(ctx, input.Event, topic.Id, message, input.AdvanceDays) + event, err := EventManager.CreateEvent(ctx, input.Event, topic.Id, message, string(input.Action), input.ResourceType, input.AdvanceDays) if err != nil { return output, errors.Wrap(err, "unable to create Event") } @@ -356,70 +323,6 @@ func (nm *SNotificationManager) needWebconsole(topics []STopic) bool { return false } -func (nm *SNotificationManager) createWithWebhookRobots(ctx context.Context, userCred mcclient.TokenCredential, webhookRobotIds []string, priority, eventId string, topicType string) error { - if len(webhookRobotIds) == 0 { - return nil - } - n := &SNotification{ - ContactType: api.WEBHOOK, - Priority: priority, - ReceivedAt: time.Now(), - EventId: eventId, - TopicType: topicType, - } - n.Id = db.DefaultUUIDGenerator() - for i := range webhookRobotIds { - _, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, webhookRobotIds[i], n.Id) - if err != nil { - return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot") - } - } - err := nm.TableSpec().Insert(ctx, n) - if err != nil { - return errors.Wrap(err, "unable to insert Notification") - } - n.SetModelManager(nm, n) - task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") - if err != nil { - log.Errorf("NotificationSendTask newTask error %v", err) - } else { - task.ScheduleRun(nil) - } - return nil -} - -func (nm *SNotificationManager) createWithRobots(ctx context.Context, userCred mcclient.TokenCredential, robotIds []string, priority, eventId string, topicType string) error { - if len(robotIds) == 0 { - return nil - } - n := &SNotification{ - ContactType: api.ROBOT, - Priority: priority, - ReceivedAt: time.Now(), - EventId: eventId, - TopicType: topicType, - } - n.Id = db.DefaultUUIDGenerator() - for i := range robotIds { - _, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, robotIds[i], n.Id) - if err != nil { - return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot") - } - } - err := nm.TableSpec().Insert(ctx, n) - if err != nil { - return errors.Wrap(err, "unable to insert Notification") - } - n.SetModelManager(nm, n) - task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") - if err != nil { - log.Errorf("NotificationSendTask newTask error %v", err) - } else { - task.ScheduleRun(nil) - } - return nil -} - func (nm *SNotificationManager) create(ctx context.Context, userCred mcclient.TokenCredential, contactType string, receiverIds, contacts []string, priority, eventId string, topicType string) error { if len(receiverIds)+len(contacts) == 0 { return nil @@ -459,6 +362,99 @@ func (nm *SNotificationManager) create(ctx context.Context, userCred mcclient.To return nil } +func (nm *SNotificationManager) createWithWebhookRobots(ctx context.Context, userCred mcclient.TokenCredential, webhookRobotIds []string, priority, eventId string, topicType string) error { + if len(webhookRobotIds) == 0 { + return nil + } + n := &SNotification{ + ContactType: api.WEBHOOK, + Priority: priority, + ReceivedAt: time.Now(), + EventId: eventId, + TopicType: topicType, + } + n.Id = db.DefaultUUIDGenerator() + for i := range webhookRobotIds { + _, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, webhookRobotIds[i], n.Id) + if err != nil { + return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot") + } + } + err := nm.TableSpec().Insert(ctx, n) + if err != nil { + return errors.Wrap(err, "unable to insert Notification") + } + n.SetModelManager(nm, n) + task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") + if err != nil { + return errors.Wrapf(err, "NewTask") + } + return task.ScheduleRun(nil) +} + +func (nm *SNotificationManager) createWithRobots(ctx context.Context, userCred mcclient.TokenCredential, robotIds []string, priority, eventId string, topicType string) error { + if len(robotIds) == 0 { + return nil + } + n := &SNotification{ + ContactType: api.ROBOT, + Priority: priority, + ReceivedAt: time.Now(), + EventId: eventId, + TopicType: topicType, + } + n.Id = db.DefaultUUIDGenerator() + for i := range robotIds { + _, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, robotIds[i], n.Id) + if err != nil { + return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot") + } + } + err := nm.TableSpec().Insert(ctx, n) + if err != nil { + return errors.Wrap(err, "unable to insert Notification") + } + n.SetModelManager(nm, n) + task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") + if err != nil { + log.Errorf("NotificationSendTask newTask error %v", err) + } else { + task.ScheduleRun(nil) + } + return nil +} + +func (n *SNotification) Create(ctx context.Context, userCred mcclient.TokenCredential, receiverIds, contacts []string) error { + if len(receiverIds)+len(contacts) == 0 { + return nil + } + + n.Id = db.DefaultUUIDGenerator() + err := NotificationManager.TableSpec().Insert(ctx, n) + if err != nil { + return errors.Wrap(err, "unable to insert Notification") + } + for i := range receiverIds { + _, err := ReceiverNotificationManager.Create(ctx, userCred, receiverIds[i], n.Id) + if err != nil { + return errors.Wrap(err, "ReceiverNotificationManager.Create") + } + } + for i := range contacts { + _, err := ReceiverNotificationManager.CreateContact(ctx, userCred, contacts[i], n.Id) + if err != nil { + return errors.Wrap(err, "ReceiverNotificationManager.CreateContact") + } + } + task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "") + if err != nil { + log.Errorf("NotificationSendTask newTask error %v", err) + } else { + task.ScheduleRun(nil) + } + return nil +} + func (nm *SNotificationManager) FetchCustomizeColumns( ctx context.Context, userCred mcclient.TokenCredential, @@ -467,8 +463,8 @@ func (nm *SNotificationManager) FetchCustomizeColumns( fields stringutils2.SSortedStrings, isList bool, ) []api.NotificationDetails { + log.Infoln("this is objs:", objs) rows := make([]api.NotificationDetails, len(objs)) - resRows := nm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) var err error @@ -535,9 +531,10 @@ func (n *SNotification) getMoreDetails(ctx context.Context, userCred mcclient.To if err != nil { return out, err } - p, err := n.TemplateStore().FillWithTemplate(ctx, lang, nn) + // p, err := n.TemplateStore().FillWithTemplate(ctx, lang, nn) + p, _ := LocalTemplateManager.FillWithTemplate(ctx, lang, nn) if err != nil { - return out, errors.Wrap(err, "TemplateStore().FillWithTemplate") + return out, err } out.Title = p.Title out.Content = p.Message @@ -546,14 +543,14 @@ func (n *SNotification) getMoreDetails(ctx context.Context, userCred mcclient.To // get receive details out.ReceiveDetails, err = n.receiveDetails(userCred, scope) if err != nil { - return out, errors.Wrap(err, "receiveDetails") + return out, err } return out, nil } -func (n *SNotification) Notification() (notifyv2.SNotification, error) { +func (n *SNotification) Notification() (api.SsNotification, error) { if n.EventId == "" { - return notifyv2.SNotification{ + return api.SsNotification{ ContactType: n.ContactType, Topic: n.Topic, Message: n.Message, @@ -561,10 +558,10 @@ func (n *SNotification) Notification() (notifyv2.SNotification, error) { } event, err := EventManager.GetEvent(n.EventId) if err != nil { - return notifyv2.SNotification{}, err + return api.SsNotification{}, err } e, _ := parseEvent(event.Event) - return notifyv2.SNotification{ + return api.SsNotification{ ContactType: n.ContactType, Topic: n.Topic, Message: event.Message, @@ -617,31 +614,6 @@ func (n *SNotification) AddOne() error { return err } -const ( - NOTIFY_RECEIVED = "received" // Received a task about sending a notification - NOTIFY_SENT = "sent" // Nofity module has sent notification, but result unkown - NOTIFY_OK = "sent_ok" // Notification was sent successfully - NOTIFY_FAIL = "sent_fail" // That sent a notification is failed - NOTIFY_REMOVED = "removed" -) - -func (self *SNotificationManager) singleRowLineQuery(sqlStr string, dest ...interface{}) error { - q := sqlchemy.NewRawQuery(sqlStr) - rows, err := q.Rows() - if err != nil { - return errors.Wrap(err, "q.Rows") - } - defer rows.Close() - for rows.Next() { - err := rows.Scan(dest...) - if err != nil { - return errors.Wrap(err, "rows.Scan") - } - return nil - } - return sql.ErrNoRows -} - func (self *SNotificationManager) InitializeData() error { return dataCleaning(self.TableSpec().Name()) } @@ -650,7 +622,7 @@ func dataCleaning(tableName string) error { now := time.Now() monthsDaysAgo := now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05") sqlStr := fmt.Sprintf( - "delete from %s where created_at < '%s'", + "delete from %s where deleted = 0 and created_at < '%s'", tableName, monthsDaysAgo, ) @@ -664,117 +636,6 @@ func dataCleaning(tableName string) error { return nil } -func (self *SNotificationManager) dataMigration() error { - // check - sqlStr := fmt.Sprintf( - "select count(*) as total from (select cluster_id from %s where status='%s' and contact_type='webconsole' group by cluster_id) as cluster", - oldmodels.NotificationManager.TableSpec().Name(), - NOTIFY_REMOVED, - ) - var count int - err := self.singleRowLineQuery(sqlStr, &count) - if err != nil { - return err - } - if count >= options.Options.MaxSyncNotification { - return nil - } - - limitTimeStr := time.Now().Add(time.Duration(-30) * time.Hour * 24).Format("2006-01-02 15:04:05") - - // get min received_at - var minReceivedAt time.Time - sqlStr = fmt.Sprintf( - "select min(received_at) as min_received_at from (select received_at from %s where received_at > '%s' and contact_type = 'webconsole' group by cluster_id order by received_at desc limit %d) as cluster", - oldmodels.NotificationManager.TableSpec().Name(), - limitTimeStr, - options.Options.MaxSyncNotification, - ) - err = self.singleRowLineQuery(sqlStr, &minReceivedAt) - if err != nil { - return err - } - log.Infof("minReceivedAt: %s", minReceivedAt) - - ctx := context.Background() - q := oldmodels.NotificationManager.Query().Equals("contact_type", api.WEBCONSOLE).GT("received_at", minReceivedAt).NotEquals("status", NOTIFY_REMOVED) - n := q.Count() - log.Infof("total %d notifications to sync", n) - oldNotifications := make([]oldmodels.SNotification, 0, n) - err = db.FetchModelObjects(oldmodels.NotificationManager, q, &oldNotifications) - if err != nil { - return errors.Wrap(err, "db.FetchModelObjects") - } - - // build cluster=>Notification - cnMap := make(map[string][]*oldmodels.SNotification) - for i := range oldNotifications { - clusterId := oldNotifications[i].ClusterID - if _, ok := cnMap[clusterId]; !ok { - cnMap[clusterId] = make([]*oldmodels.SNotification, 0, 2) - } - cnMap[clusterId] = append(cnMap[clusterId], &oldNotifications[i]) - } - - for _, oldNotifications := range cnMap { - oldNotificaion := oldNotifications[0] - newNotification := SNotification{ - ContactType: oldNotificaion.ContactType, - Topic: oldNotificaion.Topic, - Priority: oldNotificaion.Priority, - Message: oldNotificaion.Msg, - ReceivedAt: oldNotificaion.ReceivedAt, - } - newNotification.Id = db.DefaultUUIDGenerator() - statusMap := make(map[string]int, 4) - for _, oldNotificaion := range oldNotifications { - rn := SReceiverNotification{ - ReceiverID: oldNotificaion.UID, - NotificationID: newNotification.Id, - SendAt: oldNotificaion.SendAt, - SendBy: oldNotificaion.SendBy, - Status: oldNotificaion.Status, - } - if rn.Status == NOTIFY_SENT { - rn.Status = api.NOTIFICATION_STATUS_SENDING - } - statusMap[rn.Status] += 1 - err := ReceiverNotificationManager.TableSpec().Insert(ctx, &rn) - if err != nil { - return errors.Wrap(err, "TableSpec().Insert") - } - } - switch { - case statusMap[api.RECEIVER_NOTIFICATION_OK] == len(oldNotifications): - newNotification.Status = api.NOTIFICATION_STATUS_OK - case statusMap[api.RECEIVER_NOTIFICATION_RECEIVED] == len(oldNotifications): - newNotification.Status = api.NOTIFICATION_STATUS_RECEIVED - case statusMap[api.RECEIVER_NOTIFICATION_FAIL] == len(oldNotifications): - newNotification.Status = api.NOTIFICATION_STATUS_FAILED - case statusMap[api.RECEIVER_NOTIFICATION_FAIL] == 0 && statusMap[api.RECEIVER_NOTIFICATION_SENT] > 0: - newNotification.Status = api.NOTIFICATION_STATUS_SENDING - default: - newNotification.Status = api.NOTIFICATION_STATUS_PART_OK - } - err := self.TableSpec().InsertOrUpdate(ctx, &newNotification) - if err != nil { - return errors.Wrap(err, "TableSpec().InsertOrUpdate") - } - - // mark removed - for _, oldNotificaion := range oldNotifications { - _, err := db.Update(oldNotificaion, func() error { - oldNotificaion.Status = NOTIFY_REMOVED - return nil - }) - if err != nil { - return errors.Wrap(err, "Delete") - } - } - } - return nil -} - // 通知消息列表 func (nm *SNotificationManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.NotificationListInput) (*sqlchemy.SQuery, error) { q, err := nm.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput) @@ -817,9 +678,22 @@ func (nm *SNotificationManager) ReSend(ctx context.Context, userCred mcclient.To } } -func (n *SNotification) TemplateStore() notifyv2.ITemplateStore { +func (n *SNotification) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (api.SendParams, error) { if len(n.EventId) == 0 || n.ContactType == api.MOBILE { - return TemplateManager + return TemplateManager.FillWithTemplate(ctx, lang, no) } - return LocalTemplateManager + return LocalTemplateManager.FillWithTemplate(ctx, lang, no) +} + +func (n *SNotification) GetNotOKReceivers() ([]SReceiver, error) { + ret := []SReceiver{} + q := ReceiverManager.Query().IsTrue("enabled") + sq := ReceiverNotificationManager.Query().Equals("notification_id", n.Id).NotEquals("status", api.RECEIVER_NOTIFICATION_OK).Equals("receiver_type", api.RECEIVER_TYPE_USER).SubQuery() + q = q.Join(sq, sqlchemy.Equals(q.Field("id"), sq.Field("receiver_id"))) + err := db.FetchModelObjects(ReceiverManager, q, &ret) + return ret, err +} + +func (n *SNotification) TaskInsert() error { + return NotificationManager.TableSpec().Insert(context.Background(), n) } diff --git a/pkg/notify/models/plugindriver.go b/pkg/notify/models/plugindriver.go new file mode 100644 index 0000000000..36f5c476c1 --- /dev/null +++ b/pkg/notify/models/plugindriver.go @@ -0,0 +1,73 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + api "yunion.io/x/onecloud/pkg/apis/notify" +) + +type ISenderDriver interface { + GetSenderType() string + Send(args api.SendParams) error + ValidateConfig(api.NotifyConfig) (string, error) + ContactByMobile(mobile, domainId string) (string, error) + IsRobot() bool + IsPersonal() bool + IsSystemConfigContactType() bool + IsValid() bool + IsPullType() bool + GetAccessToken() error +} + +var ( + driverTable = make(map[string]ISenderDriver) +) + +func Register(driver ISenderDriver) { + driverTable[driver.GetSenderType()] = driver +} + +func GetSenderTypes() []string { + ret := []string{} + for k := range driverTable { + ret = append(ret, k) + } + return ret +} + +func GetRobotTypes() []string { + ret := []string{} + for k := range driverTable { + if driverTable[k].IsRobot() { + ret = append(ret, k) + } + } + return ret +} + +func GetValidPersonalSenderTypes() []string { + ret := []string{} + for k := range driverTable { + if driverTable[k].IsValid() && driverTable[k].IsPersonal() { + ret = append(ret, k) + } + } + return ret +} + +func GetDriver(sendType string) ISenderDriver { + driver, _ := driverTable[sendType] + return driver +} diff --git a/pkg/notify/models/receiver.go b/pkg/notify/models/receiver.go index 641b6ca76a..4ca169e180 100644 --- a/pkg/notify/models/receiver.go +++ b/pkg/notify/models/receiver.go @@ -45,7 +45,6 @@ import ( "yunion.io/x/onecloud/pkg/mcclient/informer" identity_modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity" notify_modules "yunion.io/x/onecloud/pkg/mcclient/modules/notify" - "yunion.io/x/onecloud/pkg/notify/oldmodels" "yunion.io/x/onecloud/pkg/notify/options" "yunion.io/x/onecloud/pkg/util/logclient" "yunion.io/x/onecloud/pkg/util/stringutils2" @@ -72,16 +71,14 @@ var ( ) type SReceiverManager struct { - db.SStatusStandaloneResourceBaseManager - db.SDomainizedResourceBaseManager - db.SEnabledResourceBaseManager + db.SEnabledStatusDomainLevelResourceBaseManager } var ReceiverManager *SReceiverManager func init() { ReceiverManager = &SReceiverManager{ - SStatusStandaloneResourceBaseManager: db.NewStatusStandaloneResourceBaseManager( + SEnabledStatusDomainLevelResourceBaseManager: db.NewEnabledStatusDomainLevelResourceBaseManager( SReceiver{}, "receivers_tbl", "receiver", @@ -91,10 +88,9 @@ func init() { ReceiverManager.SetVirtualObject(ReceiverManager) } +// 接收人 type SReceiver struct { - db.SStatusStandaloneResourceBase - db.SDomainizedResourceBase - db.SEnabledResourceBase + db.SEnabledStatusDomainLevelResourceBase Email string `width:"128" nullable:"false" create:"optional" update:"user" get:"user" list:"user"` // swagger:ignore @@ -112,120 +108,7 @@ type SReceiver struct { VerifiedMobile tristate.TriState `default:"false" update:"user"` // swagger:ignore - subContactCache map[string]*SSubContact `json:"-"` -} - -func (rm *SReceiverManager) InitializeData() error { - ctx := context.Background() - userCred := auth.AdminCredential() - log.Infof("Init Receiver...") - // Fetch all old SContact - q := oldmodels.ContactManager.Query() - contacts := make([]oldmodels.SContact, 0, 50) - err := db.FetchModelObjects(oldmodels.ContactManager, q, &contacts) - if err != nil { - return errors.Wrap(err, "db.FetchModelObjects") - } - if len(contacts) == 0 { - return nil - } - - // build uid map - uids := make([]string, 0, 10) - contactMap := make(map[string][]*oldmodels.SContact, 10) - for i := range contacts { - uid := contacts[i].UID - if _, ok := contactMap[uid]; !ok { - contactMap[uid] = make([]*oldmodels.SContact, 0, 4) - uids = append(uids, uid) - } - contactMap[uid] = append(contactMap[uid], &contacts[i]) - } - - // build uid->uname map - userMap, err := oldmodels.UserCacheManager.FetchUsersByIDs(context.Background(), uids) - if err != nil { - return errors.Wrap(err, "oldmodels.UserCacheManager.FetchUsersByIDs") - } - - // build Receivers - for uid, contacts := range contactMap { - var receiver SReceiver - receiver.subContactCache = make(map[string]*SSubContact) - receiver.Enabled = tristate.True - receiver.Status = api.RECEIVER_STATUS_READY - receiver.Id = uid - user, ok := userMap[uid] - if !ok { - log.Errorf("no user %q in usercache", uid) - } else { - receiver.Name = user.Name - receiver.DomainId = user.DomainId - } - for _, contact := range contacts { - switch contact.ContactType { - case api.EMAIL: - receiver.Email = contact.Contact - if contact.Enabled == "1" { - receiver.EnabledEmail = tristate.True - } else { - receiver.EnabledEmail = tristate.False - } - if contact.Status == oldmodels.CONTACT_VERIFIED { - receiver.VerifiedEmail = tristate.True - } else { - receiver.VerifiedEmail = tristate.False - } - case api.MOBILE: - receiver.Mobile = contact.Contact - if contact.Enabled == "1" { - receiver.EnabledMobile = tristate.True - } else { - receiver.EnabledMobile = tristate.False - } - if contact.Status == oldmodels.CONTACT_VERIFIED { - receiver.VerifiedMobile = tristate.True - } else { - receiver.VerifiedMobile = tristate.False - } - case api.WEBCONSOLE: - default: - var subContact SSubContact - subContact.Type = contact.ContactType - subContact.ParentContactType = api.MOBILE - subContact.Contact = contact.Contact - subContact.ReceiverID = uid - subContact.ParentContactType = api.MOBILE - if contact.Enabled == "1" { - subContact.Enabled = tristate.True - } else { - subContact.Enabled = tristate.False - } - if contact.Status == oldmodels.CONTACT_VERIFIED && len(contact.Contact) > 0 { - subContact.Verified = tristate.True - } else { - subContact.Verified = tristate.False - } - receiver.subContactCache[contact.ContactType] = &subContact - } - } - err := rm.TableSpec().InsertOrUpdate(ctx, &receiver) - if err != nil { - return errors.Wrap(err, "InsertOrUpdate") - } - err = receiver.PushCache(ctx) - if err != nil { - return errors.Wrap(err, "PushCache") - } - //delete old one - for _, contact := range contacts { - err := contact.Delete(ctx, userCred) - if err != nil { - return errors.Wrap(err, "Delete") - } - } - } - return nil + // subContactCache map[string]*SSubContact `json:"-"` } func (rm *SReceiverManager) CreateByInsertOrUpdate() bool { @@ -234,49 +117,25 @@ func (rm *SReceiverManager) CreateByInsertOrUpdate() bool { func (rm *SReceiverManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ReceiverCreateInput) (api.ReceiverCreateInput, error) { var err error - input.StatusStandaloneResourceCreateInput, err = rm.SStatusStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StatusStandaloneResourceCreateInput) + log.Infoln("this is validate SReceiverManager") + input.EnabledStatusDomainLevelResourceCreateInput, err = rm.SEnabledStatusDomainLevelResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.EnabledStatusDomainLevelResourceCreateInput) if err != nil { return input, err } - // check uid - session := auth.GetAdminSession(ctx, "") - if len(input.UID) > 0 { - userObj, err := identity_modules.UsersV3.GetById(session, input.UID, nil) - if err != nil { - if jErr, ok := err.(*httputils.JSONClientError); ok { - if jErr.Code == 404 { - return input, httperrors.NewInputParameterError("no such user") - } - } - return input, err - } - uname, _ := userObj.GetString("name") - uid, _ := userObj.GetString("id") - input.UID = uid - input.UName = uname - domainId, _ := userObj.GetString("domain_id") - input.ProjectDomainId = domainId - } else { - if len(input.UName) == 0 { - return input, httperrors.NewMissingParameterError("uid or uname") - } else { - userObj, err := identity_modules.UsersV3.GetByName(session, input.UName, nil) - if err != nil { - if jErr, ok := err.(*httputils.JSONClientError); ok { - if jErr.Code == 404 { - return input, httperrors.NewInputParameterError("no such user") - } - } - return input, err - } - uid, _ := userObj.GetString("id") - uname, _ := userObj.GetString("name") - input.UID = uid - input.UName = uname - domainId, _ := userObj.GetString("domain_id") - input.ProjectDomainId = domainId - } + if len(input.UID) == 0 && len(input.UName) == 0 { + return input, httperrors.NewMissingParameterError("uid or uname") } + uid := input.UID + if len(input.UID) == 0 { + uid = input.UName + } + user, err := db.UserCacheManager.FetchUserByIdOrName(ctx, uid) + if err != nil { + return input, err + } + input.UID = user.Id + input.UName = user.Name + input.ProjectDomainId = user.DomainId // hack input.Name = input.UName // validate email @@ -287,6 +146,13 @@ func (rm *SReceiverManager) ValidateCreateData(ctx context.Context, userCred mcc if ok := LaxMobileRegexp.MatchString(input.InternationalMobile.Mobile); len(input.InternationalMobile.Mobile) > 0 && !ok { return input, httperrors.NewInputParameterError("invalid mobile") } + for _, cType := range input.EnabledContactTypes { + driver := GetDriver(cType) + if driver == nil { + return input, httperrors.NewInputParameterError("invalid enabled contact type %s", cType) + } + } + return input, nil } @@ -319,9 +185,6 @@ func (r *SReceiver) IsVerifiedContactType(ct string) (bool, error) { } func (r *SReceiver) GetEnabledContactTypes() ([]string, error) { - if err := r.PullCache(false); err != nil { - return nil, err - } ret := make([]string, 0, 1) // for email and mobile if r.EnabledEmail.IsTrue() { @@ -330,144 +193,65 @@ func (r *SReceiver) GetEnabledContactTypes() ([]string, error) { if r.EnabledMobile.IsTrue() { ret = append(ret, api.MOBILE) } - for subct, subc := range r.subContactCache { - if subc.Enabled.IsTrue() { - ret = append(ret, subct) + subs, _ := r.GetSubContacts() + for _, sub := range subs { + if sub.Enabled.IsTrue() { + ret = append(ret, sub.Type) } } ret = append(ret, api.WEBCONSOLE) return ret, nil } -func (r *SReceiver) setEnabledContactType(contactType string, enabled bool) { - switch contactType { - case api.EMAIL: - r.EnabledEmail = tristate.NewFromBool(enabled) - case api.MOBILE: - r.EnabledMobile = tristate.NewFromBool(enabled) - default: - if sc, ok := r.subContactCache[contactType]; ok { - sc.Enabled = tristate.NewFromBool(enabled) - } else { - subContact := &SSubContact{ - Type: contactType, - ReceiverID: r.Id, - Enabled: tristate.NewFromBool(enabled), - } - subContact.ParentContactType = api.MOBILE - r.subContactCache[contactType] = subContact - } - } -} - -func (r *SReceiver) SetEnabledContactTypes(contactTypes []string) error { - if err := r.PullCache(false); err != nil { - return err - } - ctSet := sets.NewString(contactTypes...) - for _, ct := range PersonalConfigContactTypes { - if ctSet.Has(ct) { - r.setEnabledContactType(ct, true) - } else { - r.setEnabledContactType(ct, false) - } - } - return nil -} - -func (r *SReceiver) MarkContactTypeVerified(contactType string) error { - if err := r.PullCache(false); err != nil { - return err - } - if sc, ok := r.subContactCache[contactType]; ok { - sc.Verified = tristate.True - sc.VerifiedNote = "" - } else { - subContact := &SSubContact{ - Type: contactType, - ReceiverID: r.Id, - Verified: tristate.True, - } - subContact.ParentContactType = api.MOBILE - subContact.VerifiedNote = "" - r.subContactCache[contactType] = subContact - } - return nil -} - -func (r *SReceiver) MarkContactTypeUnVerified(contactType string, note string) error { - if err := r.PullCache(false); err != nil { - return err - } - if sc, ok := r.subContactCache[contactType]; ok { - sc.Verified = tristate.False - sc.VerifiedNote = note - } else { - subContact := &SSubContact{ - Type: contactType, - ReceiverID: r.Id, - VerifiedNote: note, - Verified: tristate.False, - } - subContact.ParentContactType = api.MOBILE - r.subContactCache[contactType] = subContact - } - return nil -} - -func (r *SReceiver) setVerifiedContactType(contactType string, enabled bool) { - switch contactType { - case api.EMAIL: - r.VerifiedEmail = tristate.NewFromBool(enabled) - case api.MOBILE: - r.VerifiedMobile = tristate.NewFromBool(enabled) - default: - if sc, ok := r.subContactCache[contactType]; ok { - sc.Verified = tristate.NewFromBool(enabled) - } else { - subContact := &SSubContact{ - Type: contactType, - ReceiverID: r.Id, - Verified: tristate.NewFromBool(enabled), - } - subContact.ParentContactType = api.MOBILE - r.subContactCache[contactType] = subContact - } - } -} - -func (r *SReceiver) getVerifiedInfos() ([]api.VerifiedInfo, error) { - if err := r.PullCache(false); err != nil { - return nil, err - } - infos := []api.VerifiedInfo{ - { - ContactType: api.EMAIL, - Verified: r.VerifiedEmail.Bool(), - }, - { - ContactType: api.MOBILE, - Verified: r.VerifiedMobile.Bool(), - }, - { - ContactType: api.WEBCONSOLE, - Verified: true, - }, - } - for subct, subc := range r.subContactCache { - infos = append(infos, api.VerifiedInfo{ - ContactType: subct, - Verified: subc.Verified.Bool(), - Note: subc.VerifiedNote, +func (r *SReceiver) markContactType(ctx context.Context, contactType string, isVerified bool, note string) error { + if contactType == api.MOBILE { + _, err := db.Update(r, func() error { + r.VerifiedMobile = tristate.NewFromBool(isVerified) + return nil }) + return err } - return infos, nil + if contactType == api.EMAIL { + _, err := db.Update(r, func() error { + r.VerifiedEmail = tristate.NewFromBool(isVerified) + return nil + }) + return err + } + subs, err := r.GetSubContacts() + if err != nil { + return err + } + for i := range subs { + if subs[i].Type == contactType { + _, err := db.Update(&subs[i], func() error { + subs[i].Verified = tristate.NewFromBool(isVerified) + subs[i].VerifiedNote = note + return nil + }) + return err + } + } + sub := &SSubContact{ + Type: contactType, + ReceiverID: r.Id, + Verified: tristate.NewFromBool(isVerified), + VerifiedNote: note, + ParentContactType: api.MOBILE, + } + sub.SetModelManager(SubContactManager, sub) + return SubContactManager.TableSpec().Insert(ctx, sub) +} + +func (r *SReceiver) MarkContactTypeVerified(ctx context.Context, contactType string) error { + return r.markContactType(ctx, contactType, true, "") +} + +func (r *SReceiver) MarkContactTypeUnVerified(ctx context.Context, contactType string, note string) error { + return r.markContactType(ctx, contactType, false, note) } func (r *SReceiver) GetVerifiedContactTypes() ([]string, error) { - if err := r.PullCache(false); err != nil { - return nil, err - } ret := make([]string, 0, 1) // for email and mobile if r.VerifiedEmail.IsTrue() { @@ -476,76 +260,38 @@ func (r *SReceiver) GetVerifiedContactTypes() ([]string, error) { if r.VerifiedMobile.IsTrue() { ret = append(ret, api.MOBILE) } - for subct, subc := range r.subContactCache { - if subc.Verified.IsTrue() { - ret = append(ret, subct) + subs, _ := r.GetSubContacts() + for _, sub := range subs { + if sub.Verified.IsTrue() { + ret = append(ret, sub.Type) } } return ret, nil } -func (r *SReceiver) SetVerifiedContactTypes(contactTypes []string) error { - if err := r.PullCache(false); err != nil { - return err - } - ctSet := sets.NewString(contactTypes...) - for _, ct := range PersonalConfigContactTypes { - if ctSet.Has(ct) { - r.setVerifiedContactType(ct, true) - } else { - r.setVerifiedContactType(ct, false) - } - } - return nil +func (self *SReceiver) GetSubContacts() ([]SSubContact, error) { + ret := []SSubContact{} + q := SubContactManager.Query().Equals("receiver_id", self.Id) + err := db.FetchModelObjects(SubContactManager, q, &ret) + return ret, err } -func (r *SReceiver) PullCache(force bool) error { - if !force && r.subContactCache != nil { - return nil - } - cache, err := SubContactManager.fetchMapByReceiverID(r.Id) +func (rm *SReceiverManager) FetchSubContacts(ids []string) (map[string][]SSubContact, error) { + ret := map[string][]SSubContact{} + q := SubContactManager.Query().In("receiver_id", ids) + subContacts := []SSubContact{} + err := db.FetchModelObjects(SubContactManager, q, &subContacts) if err != nil { - return err + return ret, err } - r.subContactCache = cache - return nil -} - -func (r *SReceiver) PushCache(ctx context.Context) error { - for subct, subc := range r.subContactCache { - err := SubContactManager.TableSpec().InsertOrUpdate(ctx, subc) - if err != nil { - return errors.Wrapf(err, "fail to save subcontact %q to db", subct) + for i := range subContacts { + _, ok := ret[subContacts[i].ReceiverID] + if !ok { + ret[subContacts[i].ReceiverID] = []SSubContact{} } + ret[subContacts[i].ReceiverID] = append(ret[subContacts[i].ReceiverID], subContacts[i]) } - return nil -} - -func (rm *SReceiverManager) EnabledContactFilter(contactType string, q *sqlchemy.SQuery) *sqlchemy.SQuery { - switch contactType { - case api.MOBILE: - q = q.IsTrue("enabled_mobile") - case api.EMAIL: - q = q.IsTrue("enabled_email") - default: - subQuery := SubContactManager.Query("receiver_id").Equals("type", contactType).IsTrue("enabled").SubQuery() - q = q.Join(subQuery, sqlchemy.Equals(subQuery.Field("receiver_id"), q.Field("id"))) - } - return q -} - -func (rm *SReceiverManager) VerifiedContactFilter(contactType string, q *sqlchemy.SQuery) *sqlchemy.SQuery { - switch contactType { - case api.MOBILE: - q = q.IsTrue("verified_mobile") - case api.EMAIL: - q = q.IsTrue("verified_email") - default: - subQuery := SubContactManager.Query("receiver_id").Equals("type", contactType).IsTrue("verified").SubQuery() - q = q.Join(subQuery, sqlchemy.Equals(subQuery.Field("receiver_id"), q.Field("id"))) - - } - return q + return ret, nil } func (rm *SReceiverManager) ResourceScope() rbacscope.TRbacScope { @@ -631,15 +377,7 @@ func (rm *SReceiverManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IId } func (rm *SReceiverManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ReceiverListInput) (*sqlchemy.SQuery, error) { - q, err := rm.SStatusStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StatusStandaloneResourceListInput) - if err != nil { - return nil, err - } - q, err = rm.SDomainizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainizedResourceListInput) - if err != nil { - return nil, err - } - q, err = rm.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledResourceBaseListInput) + q, err := rm.SEnabledStatusDomainLevelResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledStatusDomainLevelResourceListInput) if err != nil { return nil, err } @@ -650,10 +388,26 @@ func (rm *SReceiverManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQue q = q.Equals("name", input.UName) } if len(input.EnabledContactType) > 0 { - q = rm.EnabledContactFilter(input.EnabledContactType, q) + switch input.EnabledContactType { + case api.MOBILE: + q = q.IsTrue("enabled_mobile") + case api.EMAIL: + q = q.IsTrue("enabled_email") + default: + sq := SubContactManager.Query("receiver_id").Equals("type", input.EnabledContactType).IsTrue("enabled").SubQuery() + q = q.Join(sq, sqlchemy.Equals(sq.Field("receiver_id"), q.Field("id"))) + } } if len(input.VerifiedContactType) > 0 { - q = rm.VerifiedContactFilter(input.VerifiedContactType, q) + switch input.VerifiedContactType { + case api.MOBILE: + q = q.IsTrue("verified_mobile") + case api.EMAIL: + q = q.IsTrue("verified_email") + default: + sq := SubContactManager.Query("receiver_id").Equals("type", input.VerifiedContactType).IsTrue("verified").SubQuery() + q = q.Join(sq, sqlchemy.Equals(sq.Field("receiver_id"), q.Field("id"))) + } } ownerId, queryScope, err, _ := db.FetchCheckQueryOwnerScope(ctx, userCred, jsonutils.Marshal(input), rm, policy.PolicyActionList, true) if err != nil { @@ -751,7 +505,6 @@ func (rm *SReceiverManager) PerformGetTypes(ctx context.Context, userCred mcclie } } q := reduce(qs) - q.DebugQuery() allTypes := make([]struct { Type string }, 0, 3) @@ -768,33 +521,59 @@ func (rm *SReceiverManager) PerformGetTypes(ctx context.Context, userCred mcclie } func (rm *SReceiverManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.ReceiverDetails { - sRows := rm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) - dRows := rm.SDomainizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + sRows := rm.SEnabledStatusDomainLevelResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) rows := make([]api.ReceiverDetails, len(objs)) - var err error + recvIds := []string{} for i := range rows { - rows[i].StatusStandaloneResourceDetails = sRows[i] - rows[i].DomainizedResourceInfo = dRows[i] + rows[i].EnabledStatusDomainLevelResourceDetails = sRows[i] user := objs[i].(*SReceiver) + recvIds = append(recvIds, user.Id) rows[i].InternationalMobile = api.ParseInternationalMobile(user.Mobile) - if enabledCTs, err := user.GetEnabledContactTypes(); err != nil { - log.Errorf("GetEnabledContactTypes: %v", err) - } else { - rows[i].EnabledContactTypes = sortContactType(enabledCTs) + rows[i].EnabledContactTypes = []string{} + if user.EnabledEmail.Bool() { + rows[i].EnabledContactTypes = append(rows[i].EnabledContactTypes, api.EMAIL) } - if rows[i].VerifiedInfos, err = user.getVerifiedInfos(); err != nil { - log.Errorf("GetVerifiedContactTypes: %v", err) + if user.EnabledMobile.Bool() { + rows[i].EnabledContactTypes = append(rows[i].EnabledContactTypes, api.MOBILE) + } + rows[i].VerifiedInfos = []api.VerifiedInfo{ + { + ContactType: api.EMAIL, + Verified: user.VerifiedEmail.Bool(), + }, + { + ContactType: api.MOBILE, + Verified: user.VerifiedMobile.Bool(), + }, + { + ContactType: api.WEBCONSOLE, + Verified: true, + }, } } + subContacts, err := rm.FetchSubContacts(recvIds) + if err != nil { + return rows + } + for i := range rows { + for _, contact := range subContacts[recvIds[i]] { + if contact.Enabled.Bool() { + rows[i].EnabledContactTypes = append(rows[i].EnabledContactTypes, contact.Type) + } + rows[i].VerifiedInfos = append(rows[i].VerifiedInfos, api.VerifiedInfo{ + ContactType: contact.Type, + Verified: contact.Verified.Bool(), + Note: contact.VerifiedNote, + }) + } + rows[i].EnabledContactTypes = sortContactType(rows[i].EnabledContactTypes) + } + return rows } func (rm *SReceiverManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { - q, err := rm.SStatusStandaloneResourceBaseManager.QueryDistinctExtraField(q, field) - if err == nil { - return q, nil - } - q, err = rm.SDomainizedResourceBaseManager.QueryDistinctExtraField(q, field) + q, err := rm.SEnabledStatusDomainLevelResourceBaseManager.QueryDistinctExtraField(q, field) if err == nil { return q, nil } @@ -802,27 +581,26 @@ func (rm *SReceiverManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field st } func (rm *SReceiverManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.ReceiverListInput) (*sqlchemy.SQuery, error) { - q, err := rm.SStatusStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StatusStandaloneResourceListInput) + q, err := rm.SEnabledStatusDomainLevelResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.EnabledStatusDomainLevelResourceListInput) if err != nil { return nil, err } - q, err = rm.SDomainizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainizedResourceListInput) return q, nil } func (r *SReceiver) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { - r.SStatusStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data) - // set status - r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "") - logclient.AddActionLogWithContext(ctx, r, logclient.ACT_CREATE, nil, userCred, true) - err := r.StartSubcontactPullTask(ctx, userCred, nil, "") + r.SEnabledStatusDomainLevelResourceBase.PostCreate(ctx, userCred, ownerId, query, data) + cTypes := jsonutils.GetQueryStringArray(data, "enabled_contact_types") + err := r.StartSubcontactPullTask(ctx, userCred, cTypes, "") if err != nil { - log.Errorf("unable to StartSubcontactPullTask: %v", err) + logclient.AddActionLogWithContext(ctx, r, logclient.ACT_CREATE, err, userCred, false) + return } + logclient.AddActionLogWithContext(ctx, r, logclient.ACT_CREATE, err, userCred, true) } func (r *SReceiver) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { - err := r.SStatusStandaloneResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) + err := r.SEnabledStatusDomainLevelResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) if err != nil { return nil } @@ -831,41 +609,27 @@ func (r *SReceiver) CustomizeCreate(ctx context.Context, userCred mcclient.Token if err != nil { return err } - // set id and name r.Id = input.UID - r.Name = input.UName - r.DomainId = input.ProjectDomainId - if input.Enabled == nil { - r.Enabled = tristate.True - } r.Mobile = input.InternationalMobile.String() - err = r.SetEnabledContactTypes(input.EnabledContactTypes) - if err != nil { - return errors.Wrap(err, "SetEnabledContactTypes") - } - err = r.PushCache(ctx) - if err != nil { - return errors.Wrap(err, "PushCache") - } + // 需求:管理后台新建的联系人,手机号和邮箱无需进行校验 // 方案:检查请求者对于创建联系人 是否具有system scope - if input.ForceVerified { - allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) - if allowScope == rbacscope.ScopeSystem { - if r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True - } + allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) + if allowScope == rbacscope.ScopeSystem { + if r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True } } + return nil } func (r *SReceiver) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverUpdateInput) (api.ReceiverUpdateInput, error) { var err error - input.StatusStandaloneResourceBaseUpdateInput, err = r.SStatusStandaloneResourceBase.ValidateUpdateData(ctx, userCred, query, input.StatusStandaloneResourceBaseUpdateInput) + input.EnabledStatusDomainLevelResourceBaseUpdateInput, err = r.SEnabledStatusDomainLevelResourceBase.ValidateUpdateData(ctx, userCred, query, input.EnabledStatusDomainLevelResourceBaseUpdateInput) if err != nil { return input, err } @@ -877,126 +641,134 @@ func (r *SReceiver) ValidateUpdateData(ctx context.Context, userCred mcclient.To if ok := len(input.InternationalMobile.Mobile) == 0 || LaxMobileRegexp.MatchString(input.InternationalMobile.Mobile); !ok { return input, httperrors.NewInputParameterError("invalid mobile") } + + for _, cType := range input.EnabledContactTypes { + driver := GetDriver(cType) + if driver == nil { + return input, httperrors.NewInputParameterError("invalid enabled contact type %s", cType) + } + } + return input, nil } func (r *SReceiver) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { - r.SStatusStandaloneResourceBase.PreUpdate(ctx, userCred, query, data) + r.SEnabledStatusDomainLevelResourceBase.PreUpdate(ctx, userCred, query, data) originEmailEnable, originMobileEnable := r.EnabledEmail, r.EnabledMobile var input api.ReceiverUpdateInput - err := data.Unmarshal(&input) - if err != nil { - log.Errorf("fail to unmarshal to ContactUpdateInput: %v", err) - } - err = r.PullCache(false) - if err != nil { - log.Errorf("PullCache: %v", err) - } - err = r.SetEnabledContactTypes(input.EnabledContactTypes) - if err != nil { - log.Errorf("unable to SetEnabledContactTypes") - } + data.Unmarshal(&input) if len(input.Email) != 0 && input.Email != r.Email { + db.Update(r, func() error { + r.VerifiedEmail = tristate.False + return nil + }) r.VerifiedEmail = tristate.False - for _, c := range r.subContactCache { - if c.ParentContactType == api.EMAIL { - c.Verified = tristate.False - c.VerifiedNote = "email changed, re-verify" + subs, _ := r.GetSubContacts() + for i := range subs { + if subs[i].ParentContactType == api.EMAIL { + db.Update(&subs[i], func() error { + subs[i].Verified = tristate.False + subs[i].VerifiedNote = "email changed, re-verify" + return nil + }) } } } mobile := input.InternationalMobile.String() + log.Infof("this is r.Mobile:%s,this is mobile:%s", r.Mobile, mobile) if len(mobile) != 0 && mobile != r.Mobile { - r.VerifiedMobile = tristate.False - for _, c := range r.subContactCache { - if c.ParentContactType == api.MOBILE { - c.Verified = tristate.False - c.VerifiedNote = "mobile changed, re-verify" + log.Infoln("this is update v.VerifiedMobile") + db.Update(r, func() error { + r.VerifiedMobile = tristate.False + return nil + }) + subs, _ := r.GetSubContacts() + log.Infoln("this is subs:", jsonutils.Marshal(subs)) + for i := range subs { + if subs[i].ParentContactType == api.MOBILE { + db.Update(&subs[i], func() error { + subs[i].Verified = tristate.False + subs[i].VerifiedNote = "mobile changed, re-verify" + return nil + }) } } } - err = r.PushCache(ctx) - if err != nil { - log.Errorf("PushCache: %v", err) - } // 管理后台修改联系人,如果修改或者启用手机号和邮箱,无需进行校验 if input.ForceVerified { allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) if allowScope == rbacscope.ScopeSystem { - // 修改并启用 - if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True - } - // 从禁用变启用 - if !originEmailEnable.Bool() && r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if !originMobileEnable.Bool() && r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True - } + db.Update(r, func() error { + // 修改并启用 + if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True + } + // 从禁用变启用 + if !originEmailEnable.Bool() && r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if !originMobileEnable.Bool() && r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True + } + return nil + }) } } - r.Mobile = mobile - err = ReceiverManager.TableSpec().InsertOrUpdate(ctx, r) - if err != nil { - log.Errorf("InsertOrUpdate: %v", err) - } } func (r *SReceiver) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { - r.SStatusStandaloneResourceBase.PostUpdate(ctx, userCred, query, data) - // set status - r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "") - logclient.AddActionLogWithContext(ctx, r, logclient.ACT_UPDATE, nil, userCred, true) - err := r.StartSubcontactPullTask(ctx, userCred, nil, "") + r.SEnabledStatusDomainLevelResourceBase.PostUpdate(ctx, userCred, query, data) + cTypes := jsonutils.GetQueryStringArray(data, "enabled_contact_types") + err := r.StartSubcontactPullTask(ctx, userCred, cTypes, "") if err != nil { - log.Errorf("unable to StartSubcontactPullTask: %v", err) + logclient.AddActionLogWithContext(ctx, r, logclient.ACT_UPDATE, err, userCred, false) + return } + logclient.AddActionLogWithContext(ctx, r, logclient.ACT_UPDATE, err, userCred, true) } -func (r *SReceiver) StartSubcontactPullTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error { +func (r *SReceiver) StartSubcontactPullTask(ctx context.Context, userCred mcclient.TokenCredential, contactTypes []string, parentTaskId string) error { + if len(r.Mobile) == 0 { + return nil + } + r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "") + params := jsonutils.NewDict() + if len(contactTypes) > 0 { + params.Set("contact_types", jsonutils.NewStringArray(contactTypes)) + } task, err := taskman.TaskManager.NewTask(ctx, "SubcontactPullTask", r, userCred, params, parentTaskId, "") if err != nil { return err } - task.ScheduleRun(nil) - return nil + return task.ScheduleRun(nil) } func (r *SReceiver) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { - err := r.PullCache(false) - if err != nil { - return err - } - for _, sc := range r.subContactCache { + subs, _ := r.GetSubContacts() + for _, sc := range subs { err := sc.Delete(ctx, userCred) if err != nil { return err } } r.deleteReceiverInSubscriber(ctx) - return r.SStatusStandaloneResourceBase.Delete(ctx, userCred) + return r.SEnabledStatusDomainLevelResourceBase.Delete(ctx, userCred) } -func (r *SReceiver) deleteReceiverInSubscriber(ctx context.Context) { +func (r *SReceiver) deleteReceiverInSubscriber(ctx context.Context) error { q := SubscriberReceiverManager.Query().Equals("receiver_id", r.Id) srs := make([]SSubscriberReceiver, 0, 2) err := db.FetchModelObjects(SubscriberReceiverManager, q, &srs) if err != nil { - log.Infof("unable to get SubscriberReceiver: %s", err.Error()) + return errors.Wrapf(err, "db.FetchModelObjects") } for i := range srs { - sr := &srs[i] - _, err := db.Update(sr, func() error { - return sr.MarkDelete() - }) - if err != nil { - log.Errorf("unable to delete subscriber receiver for receiver %q", r.Id) - } + srs[i].Delete(ctx, nil) } + return nil } func (r *SReceiver) IsOwner(userCred mcclient.TokenCredential) bool { @@ -1056,11 +828,9 @@ func (r *SReceiver) PerformTriggerVerify(ctx context.Context, userCred mcclient. if !utils.IsInStringArray(input.ContactType, []string{api.EMAIL, api.MOBILE, api.DINGTALK, api.FEISHU, api.WORKWX}) { return nil, httperrors.NewInputParameterError("not support such contact type %q", input.ContactType) } - if utils.IsInStringArray(input.ContactType, []string{api.DINGTALK, api.FEISHU, api.WORKWX}) { - r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "") - params := jsonutils.NewDict() - params.Set("contact_types", jsonutils.NewArray(jsonutils.NewString(input.ContactType))) - return nil, r.StartSubcontactPullTask(ctx, userCred, params, "") + driver := GetDriver(input.ContactType) + if driver.IsPullType() { + return nil, r.StartSubcontactPullTask(ctx, userCred, []string{input.ContactType}, "") } _, err := VerificationManager.Create(ctx, r.Id, input.ContactType) /*if err == ErrVerifyFrequently { @@ -1073,11 +843,9 @@ func (r *SReceiver) PerformTriggerVerify(ctx context.Context, userCred mcclient. params := jsonutils.Marshal(input).(*jsonutils.JSONDict) task, err := taskman.TaskManager.NewTask(ctx, "VerificationSendTask", r, userCred, params, "", "") if err != nil { - log.Errorf("ContactPullTask newTask error %v", err) - } else { - task.ScheduleRun(nil) + return nil, err } - return nil, nil + return nil, task.ScheduleRun(nil) } func (r *SReceiver) PerformVerify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverVerifyInput) (jsonutils.JSONObject, error) { @@ -1128,29 +896,16 @@ func (r *SReceiver) PerformDisable(ctx context.Context, userCred mcclient.TokenC } func (r *SReceiver) Sync(ctx context.Context) error { - session := auth.GetAdminSessionWithInternal(ctx, "") - params := jsonutils.NewDict() - params.Set("scope", jsonutils.NewString("system")) - params.Set("system", jsonutils.JSONTrue) - data, err := identity_modules.UsersV3.GetById(session, r.Id, params) + user, err := db.UserCacheManager.FetchUserById(ctx, r.Id) if err != nil { - jerr := err.(*httputils.JSONClientError) - if jerr.Code == 404 { - err := r.Delete(ctx, session.GetToken()) - if err != nil { - return errors.Wrapf(err, "unable to delete receiver %s", r.Id) - } - return errors.Wrapf(errors.ErrNotFound, "no such receiver %s", r.Id) - } return err } - uname, _ := data.GetString("name") - domainId, _ := data.GetString("domain_id") - lang, _ := data.GetString("lang") _, err = db.Update(r, func() error { - r.Name = uname - r.DomainId = domainId - r.Lang = lang + r.Name = user.Name + r.DomainId = user.DomainId + if len(user.Lang) > 0 { + r.Lang = user.Lang + } return nil }) return errors.Wrap(err, "unable to update") @@ -1224,6 +979,7 @@ func (rm *SReceiverManager) OnDelete(obj *jsonutils.JSONDict) { } } +// 监听User变化 func (rm *SReceiverManager) StartWatchUserInKeystone() error { adminSession := auth.GetAdminSession(context.Background(), "") watchMan, err := informer.NewWatchManagerBySession(adminSession) @@ -1286,67 +1042,90 @@ func (rm *SReceiverManager) FetchByIdOrNames(ctx context.Context, idOrNames ...s return receivers, nil } -func (r *SReceiver) SetContact(cType string, contact string) error { - if err := r.PullCache(false); err != nil { - return err +func (rm *SReceiverManager) FetchEnableReceiversByIdOrNames(ctx context.Context, idOrNames ...string) ([]SReceiver, error) { + if len(idOrNames) == 0 { + return nil, nil } + var err error + q := idOrNameFilter(rm.Query(), idOrNames...) + q.Equals("enabled", true) + receivers := make([]SReceiver, 0, len(idOrNames)) + err = db.FetchModelObjects(rm, q, &receivers) + if err != nil { + return nil, err + } + return receivers, nil +} + +func (r *SReceiver) SetContact(cType string, contact string) error { + var err error switch cType { case api.EMAIL: - r.Email = contact + _, err = db.Update(r, func() error { + r.Email = contact + return nil + }) case api.MOBILE: + _, err = db.Update(r, func() error { + r.Mobile = contact + return nil + }) r.Mobile = contact default: - if sc, ok := r.subContactCache[cType]; ok { - sc.Contact = contact + subs, _ := r.GetSubContacts() + for i := range subs { + if subs[i].Type == cType { + _, err = db.Update(&subs[i], func() error { + subs[i].Contact = contact + return nil + }) + } } } - return nil + return err } func (r *SReceiver) GetContact(cType string) (string, error) { - if err := r.PullCache(false); err != nil { - return "", err - } - switch { - case cType == api.EMAIL: + switch cType { + case api.EMAIL: return r.Email, nil - case cType == api.MOBILE: + case api.MOBILE: return r.Mobile, nil - case cType == api.WEBCONSOLE: + case api.WEBCONSOLE: return r.Id, nil - case utils.IsInStringArray(cType, RobotContactTypes): + case api.FEISHU_ROBOT, api.DINGTALK_ROBOT, api.WORKWX_ROBOT: return r.Mobile, nil default: - if sc, ok := r.subContactCache[cType]; ok { - return sc.Contact, nil + subs, _ := r.GetSubContacts() + for _, sub := range subs { + if sub.Type == cType { + return sub.Contact, nil + } } } return "", nil } -func (r *SReceiver) GetDomainId() string { - return r.DomainId +func (r *SReceiver) PerformEnableContactType(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverEnableContactTypeInput) (jsonutils.JSONObject, error) { + for _, cType := range input.EnabledContactTypes { + driver := GetDriver(cType) + if driver == nil { + return nil, httperrors.NewInputParameterError("invalid enabled contact type %s", cType) + } + } + return nil, r.StartSubcontactPullTask(ctx, userCred, input.EnabledContactTypes, "") } -func (r *SReceiver) PerformEnableContactType(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverEnableContactTypeInput) (jsonutils.JSONObject, error) { - err := r.PullCache(false) - if err != nil { - return nil, errors.Wrap(err, "unable to pull cache") +func (rm *SReceiverManager) InitializeData() error { + return nil +} + +func (self *SReceiver) GetNotifyReceiver() api.SNotifyReceiver { + ret := api.SNotifyReceiver{ + DomainId: self.DomainId, + Enabled: self.Enabled.Bool(), } - err = r.SetEnabledContactTypes(input.EnabledContactTypes) - if err != nil { - return nil, errors.Wrap(err, "unable to set enabled contact types") - } - err = r.PushCache(ctx) - if err != nil { - return nil, errors.Wrap(err, "unable to push cache") - } - r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "") - err = r.StartSubcontactPullTask(ctx, userCred, nil, "") - if err != nil { - log.Errorf("unable to StartSubcontactPullTask: %v", err) - } - return nil, nil + return ret } func (manager *SReceiverManager) SyncUserFromKeystone(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { @@ -1449,3 +1228,15 @@ func (manager *SReceiverManager) syncUser(ctx context.Context, userCred mcclient } return nil } + +func (r *SReceiver) GetDomainId() string { + return r.DomainId +} + +func (r *SReceiver) IsRobot() bool { + return false +} + +func (r *SReceiver) IsReceiver() bool { + return true +} diff --git a/pkg/notify/models/receiver_notification.go b/pkg/notify/models/receiver_notification.go index aa81aef105..d6bc414bd2 100644 --- a/pkg/notify/models/receiver_notification.go +++ b/pkg/notify/models/receiver_notification.go @@ -19,6 +19,8 @@ import ( "net/http" "time" + "yunion.io/x/pkg/errors" + api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/cloudcommon/db" @@ -71,6 +73,14 @@ func (self *SReceiverNotificationManager) InitializeData() error { return dataCleaning(self.TableSpec().Name()) } +func (self *SReceiverNotification) GetReceiver() (*SReceiver, error) { + recv, err := ReceiverManager.FetchById(self.ReceiverID) + if err != nil { + return nil, errors.Wrapf(err, "with id %s", self.ReceiverID) + } + return recv.(*SReceiver), nil +} + func (rnm *SReceiverNotificationManager) Create(ctx context.Context, userCred mcclient.TokenCredential, receiverID, notificationID string) (*SReceiverNotification, error) { rn := &SReceiverNotification{ ReceiverID: receiverID, @@ -119,16 +129,22 @@ func (rnm *SReceiverNotificationManager) SetHandlerProcessTimeout(info *appsrv.S return -time.Second } -// func (rn *SReceiverNotification) Receiver() (*SReceiver, error) { -// q := ReceiverManager.Query().Equals("id", rn.ReceiverID) -// var receiver SReceiver -// err := q.First(&receiver) -// if err != nil { -// return nil, err -// } -// receiver.SetModelManager(ReceiverManager, &receiver) -// return &receiver, nil -// } +func (rn *SReceiverNotification) Receiver() (IReceiver, error) { + switch rn.ReceiverType { + case api.RECEIVER_TYPE_USER: + return rn.receiver() + case api.RECEIVER_TYPE_CONTACT: + return &SContact{contact: rn.Contact}, nil + case api.RECEIVER_TYPE_ROBOT: + return rn.robot() + default: + // compatible + if rn.ReceiverID != "" && rn.ReceiverID != ReceiverIdDefault { + return rn.receiver() + } + return &SContact{contact: rn.Contact}, nil + } +} func (rn *SReceiverNotification) receiver() (*SReceiver, error) { q := ReceiverManager.Query().Equals("id", rn.ReceiverID) @@ -152,6 +168,7 @@ func (rn *SReceiverNotification) robot() (*SRobot, error) { return &robot, nil } +/* func (rn *SReceiverNotification) Receiver() (IReceiver, error) { switch rn.ReceiverType { case api.RECEIVER_TYPE_USER: @@ -168,6 +185,7 @@ func (rn *SReceiverNotification) Receiver() (IReceiver, error) { return &SContact{contact: rn.Contact}, nil } } +*/ func (rn *SReceiverNotification) BeforeSend(ctx context.Context, sendTime time.Time) error { if sendTime.IsZero() { @@ -195,6 +213,8 @@ func (rn *SReceiverNotification) AfterSend(ctx context.Context, success bool, re } type IReceiver interface { + IsRobot() bool + IsReceiver() bool IsEnabled() bool GetDomainId() string IsEnabledContactType(string) (bool, error) @@ -234,3 +254,11 @@ type SContact struct { func (s *SContact) GetContact(_ string) (string, error) { return s.contact, nil } + +func (s *SContact) IsRobot() bool { + return false +} + +func (s *SContact) IsReceiver() bool { + return false +} diff --git a/pkg/notify/models/robot.go b/pkg/notify/models/robot.go index 43d205e7eb..5950af996e 100644 --- a/pkg/notify/models/robot.go +++ b/pkg/notify/models/robot.go @@ -16,7 +16,7 @@ package models import ( "context" - "strings" + "fmt" "golang.org/x/text/language" @@ -24,18 +24,15 @@ import ( "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/tristate" - "yunion.io/x/pkg/util/rbacscope" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/apis" - idenapi "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/apis/notify" api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" - rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis" "yunion.io/x/onecloud/pkg/util/stringutils2" ) @@ -67,135 +64,7 @@ type SRobot struct { Lang string `width:"16" nullable:"false" create:"required" update:"user" get:"user" list:"user"` } -func (rm *SRobotManager) fetchSystemProjectId(ctx context.Context) (string, error) { - tenant, err := db.TenantCacheManager.FetchTenantByNameInDomain(ctx, "system", idenapi.DEFAULT_DOMAIN_ID) - if err != nil { - return "", err - } - return tenant.Id, nil -} - -func (rm *SRobotManager) InitializeData() error { - log.Infof("start to init data for notify robot") - // init empty projectId robot - ctx := context.WithValue(context.Background(), "from", "init") - systemId, err := rm.fetchSystemProjectId(ctx) - if err != nil { - return errors.Wrap(err, "unable to fetch system project id") - } - rq := RobotManager.Query() - rq = rq.Filter(sqlchemy.OR(sqlchemy.IsEmpty(rq.Field("tenant_id")), sqlchemy.Equals(rq.Field("tenant_id"), "system"))) - npRobots := make([]SRobot, 0) - err = db.FetchModelObjects(RobotManager, rq, &npRobots) - if err != nil { - return errors.Wrap(err, "unable to fetch robots without project") - } - for i := range npRobots { - robot := &npRobots[i] - _, err := db.Update(robot, func() error { - robot.ProjectId = systemId - robot.ProjectSrc = "local" - return nil - }) - if err != nil { - return errors.Wrapf(err, "unable to update robot %s", robot.Id) - } - } - // convert robot config - q := ConfigManager.Query().In("type", append(RobotContactTypes, api.WEBHOOK)) - var configs []SConfig - err = db.FetchModelObjects(ConfigManager, q, &configs) - if err != nil { - return err - } - if len(configs) == 0 { - return nil - } - robots := make([]SRobot, 0, len(configs)) - for i := range configs { - webhook, _ := configs[i].Content.GetString("webhook") - robot := SRobot{ - Address: webhook, - Lang: "zh_CN", - } - robot.IsPublic = true - robot.PublicScope = string(rbacscope.ScopeSystem) - robot.DomainId = idenapi.DEFAULT_DOMAIN_ID - robot.ProjectId = systemId - robot.ProjectSrc = "local" - robot.Status = api.RECEIVER_STATUS_READY - switch configs[i].Type { - case api.FEISHU_ROBOT: - robot.Type = api.ROBOT_TYPE_FEISHU - robot.Name = "Feishu Robot" - case api.DINGTALK_ROBOT: - robot.Type = api.ROBOT_TYPE_DINGTALK - robot.Name = "Dingtalk Robot" - case api.WORKWX_ROBOT: - robot.Type = api.ROBOT_TYPE_WORKWX - robot.Name = "Workwx Robot" - case api.WEBHOOK: - robot.Type = api.ROBOT_TYPE_WEBHOOK - robot.Name = "Webhook" - addresses := strings.Split(robot.Address, ",") - for i := 1; i < len(addresses); i++ { - robotn := robot - robotn.Address = strings.TrimSpace(addresses[i]) - robots = append(robots, robotn) - } - robot.Address = addresses[0] - default: - continue - } - robots = append(robots, robot) - } - var webhookRobotId string - // insert new robot - for i := range robots { - err := rm.TableSpec().Insert(ctx, &robots[i]) - if err != nil { - return err - } - if robots[i].Type == api.ROBOT_TYPE_WEBHOOK { - webhookRobotId = robots[i].Id - } - } - // delete old configs - for i := range configs { - config := &configs[i] - _, err := db.Update(config, func() error { - return config.MarkDelete() - }) - if err != nil { - return err - } - } - // add webhook Robot to subscriber - if len(webhookRobotId) > 0 { - topics := make([]STopic, 0, 3) - q = TopicManager.Query().In("name", []string{DefaultResourceCreateDelete, DefaultResourceChangeConfig, DefaultResourceUpdate}) - err := db.FetchModelObjects(TopicManager, q, &topics) - if err != nil { - return errors.Wrap(err, "unable to fetch topics") - } - // create subscribers - for i := range topics { - subscriber := SSubscriber{ - TopicID: topics[i].Id, - Type: api.SUBSCRIBER_TYPE_ROBOT, - Identification: webhookRobotId, - ResourceScope: "system", - Scope: "system", - } - subscriber.Enabled = tristate.True - err := SubscriberManager.TableSpec().Insert(ctx, &subscriber) - if err != nil { - return errors.Wrapf(err, "unable to create subscriber %s", jsonutils.Marshal(subscriber)) - } - } - } - return nil -} +var RobotList = []string{api.FEISHU_ROBOT, api.DINGTALK_ROBOT, api.WORKWX_ROBOT, api.WEBHOOK, api.WEBHOOK_ROBOT} func (rm *SRobotManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.RobotCreateInput) (api.RobotCreateInput, error) { var err error @@ -204,41 +73,35 @@ func (rm *SRobotManager) ValidateCreateData(ctx context.Context, userCred mcclie return input, errors.Wrap(err, "SSharableVirtualResourceBaseManager.ValidateCreateData") } // check type - if !utils.IsInStringArray(input.Type, []string{api.ROBOT_TYPE_FEISHU, api.ROBOT_TYPE_WORKWX, api.ROBOT_TYPE_DINGTALK, api.ROBOT_TYPE_WEBHOOK}) { - return input, httperrors.NewInputParameterError("unkown type %q", input.Type) + if !utils.IsInStringArray(fmt.Sprintf("%s-robot", input.Type), GetRobotTypes()) { + return input, httperrors.NewInputParameterError("unkown type %s support: %s", input.Type, GetRobotTypes()) } // check lang if input.Lang == "" { input.Lang = "zh_CN" - } else { - _, err = language.Parse(input.Lang) - if err != nil { - return input, httperrors.NewInputParameterError("invalid lang %q: %s", input.Lang, err.Error()) - } } // check Address - records, err := NotifyService.SendRobotMessage(ctx, input.Type, []*rpcapi.SReceiver{ - { - Contact: input.Address, - DomainId: input.ProjectDomainId, - }, - }, "Validate", "This is a verification message, please ignore.") + _, err = language.Parse(input.Lang) if err != nil { return input, errors.Wrap(err, "unable to validate address") } - if len(records) > 0 { - return input, httperrors.NewInputParameterError("invalid address: %s", records[0].Reason) + input.SetEnabled() + input.Status = api.ROBOT_STATUS_READY + driver := GetDriver(fmt.Sprintf("%s-robot", input.Type)) + err = driver.Send(api.SendParams{ + Receivers: api.SNotifyReceiver{ + Contact: input.Address, + DomainId: input.ProjectDomainId, + }, + Title: "Validate", + Message: "This is a verification message, please ignore.", + }) + if err != nil { + return input, err } return input, nil } -func (r *SRobot) Receiver() *rpcapi.SReceiver { - return &rpcapi.SReceiver{ - Contact: r.Address, - DomainId: r.DomainId, - } -} - func (rm *SRobotManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.RobotDetails { sRows := rm.SSharableVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) rows := make([]api.RobotDetails, len(objs)) @@ -281,18 +144,11 @@ func (r *SRobot) ValidateUpdateData(ctx context.Context, userCred mcclient.Token } if len(input.Address) > 0 { // check Address - records, err := NotifyService.SendRobotMessage(ctx, r.Type, []*rpcapi.SReceiver{ - { - Contact: input.Address, - DomainId: r.DomainId, - }, - }, "Validate", "This is a verification message, please ignore.") + dirver := GetDriver(r.Type) + err := dirver.Send(api.SendParams{}) if err != nil { return input, errors.Wrap(err, "unable to validate address") } - if len(records) > 0 { - return input, httperrors.NewInputParameterError("invalid address: %s", records[0].Reason) - } } return input, nil } @@ -330,7 +186,15 @@ func (r *SRobot) IsEnabledContactType(ctype string) (bool, error) { } func (r *SRobot) IsVerifiedContactType(ctype string) (bool, error) { - return ctype == api.ROBOT || ctype == api.WEBHOOK, nil + return utils.IsInStringArray(ctype, RobotList), nil +} + +func (r *SRobot) IsRobot() bool { + return true +} + +func (r *SRobot) IsReceiver() bool { + return false } func (r *SRobot) GetContact(ctype string) (string, error) { @@ -384,3 +248,13 @@ func (r *SRobot) PostDelete(ctx context.Context, userCred mcclient.TokenCredenti } } } + +func GetRobotTypeById(id string) (string, error) { + imode, err := RobotManager.FetchById(id) + if err != nil { + return "", errors.Wrap(err, "FetchById") + } + log.Infoln("this is robot:", jsonutils.Marshal(imode)) + robot := imode.(*SRobot) + return robot.Type, nil +} diff --git a/pkg/notify/models/smsdriver.go b/pkg/notify/models/smsdriver.go new file mode 100644 index 0000000000..181c36f05e --- /dev/null +++ b/pkg/notify/models/smsdriver.go @@ -0,0 +1,41 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + api "yunion.io/x/onecloud/pkg/apis/notify" +) + +type ISmsDriver interface { + Name() string + Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error + Verify(config *api.NotifyConfig) error +} + +var ( + smsdriverTable = make(map[string]ISmsDriver) +) + +func SMSRegister(driver ISmsDriver) { + smsdriverTable[driver.Name()] = driver +} + +func GetSMSDriver(smsDriver string) ISmsDriver { + driver, isExist := smsdriverTable[smsDriver] + if isExist { + return driver + } + return nil +} diff --git a/pkg/notify/models/subscriber.go b/pkg/notify/models/subscriber.go index f000947fd4..deda94eaef 100644 --- a/pkg/notify/models/subscriber.go +++ b/pkg/notify/models/subscriber.go @@ -75,6 +75,7 @@ type SSubscriberManager struct { db.SEnabledResourceBaseManager } +// 消息订阅接收人 type SSubscriber struct { db.SStandaloneAnonResourceBase db.SEnabledResourceBase @@ -108,6 +109,22 @@ func (sm *SSubscriberManager) validateReceivers(ctx context.Context, receivers [ return reIds, nil } +func (self *SSubscriber) GetEnabledReceivers() ([]SReceiver, error) { + q := ReceiverManager.Query().IsTrue("enabled") + sq := SubscriberReceiverManager.Query().SubQuery() + q = q.Join(sq, sqlchemy.Equals(q.Field("id"), sq.Field("receiver_id"))).Filter(sqlchemy.Equals(sq.Field("subscriber_id"), self.Id)) + ret := []SReceiver{} + return ret, db.FetchModelObjects(ReceiverManager, q, &ret) +} + +func (self *SSubscriber) GetRobot() (*SRobot, error) { + robot, err := RobotManager.FetchById(self.Identification) + if err != nil { + return nil, errors.Wrapf(err, "RobotManager.FetchById(%s)", self.Identification) + } + return robot.(*SRobot), nil +} + func (sm *SSubscriberManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.SubscriberCreateInput) (api.SubscriberCreateInput, error) { var err error // permission check diff --git a/pkg/notify/models/template.go b/pkg/notify/models/template.go index 65bd720419..a7b0894512 100644 --- a/pkg/notify/models/template.go +++ b/pkg/notify/models/template.go @@ -20,6 +20,7 @@ import ( "database/sql" "encoding/json" "fmt" + "html" "io/ioutil" "path/filepath" "strings" @@ -37,9 +38,7 @@ import ( "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" - notifyv2 "yunion.io/x/onecloud/pkg/notify" "yunion.io/x/onecloud/pkg/notify/options" - "yunion.io/x/onecloud/pkg/notify/rpc/apis" ) type STemplateManager struct { @@ -68,11 +67,11 @@ type STemplate struct { db.SStandaloneAnonResourceBase ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"` - Topic string `width:"20" nullable:"false" create:"required" update:"user" list:"user"` + Topic string `width:"128" nullable:"false" create:"required" update:"user" list:"user"` // title | content | remote TemplateType string `width:"10" nullable:"false" create:"required" update:"user" list:"user"` - Content string `length:"text" nullable:"false" create:"required" get:"user" list:"user" update:"user"` + Content string `length:"text" nullable:"true" create:"required" get:"user" list:"user" update:"user"` Lang string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user" create:"optional"` Example string `nullable:"true" create:"optional" get:"user" list:"user" update:"user"` } @@ -215,7 +214,7 @@ func (tm *STemplateManager) InitializeData() error { // FillWithTemplate will return the title and content generated by corresponding template. // Local cache about common template will be considered in case of performance issues. -func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, no notifyv2.SNotification) (params apis.SendParams, err error) { +func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) { if len(lang) == 0 { params.Title = no.Topic params.Message = no.Message @@ -259,6 +258,7 @@ func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, n return } } + params.Message = html.UnescapeString(params.Message) return } diff --git a/pkg/notify/models/topic.go b/pkg/notify/models/topic.go index c55679e66d..5c4c3046a7 100644 --- a/pkg/notify/models/topic.go +++ b/pkg/notify/models/topic.go @@ -31,6 +31,7 @@ import ( "yunion.io/x/onecloud/pkg/apis/notify" api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/bitmap" @@ -68,6 +69,7 @@ func init() { TopicManager.SetVirtualObject(TopicManager) } +// 消息订阅 type STopic struct { db.SEnabledStatusStandaloneResourceBase @@ -197,6 +199,7 @@ func (sm *STopicManager) InitializeData() error { ) t.addAction(notify.ActionChangeConfig) t.Type = notify.TOPIC_TYPE_RESOURCE + t.Results = tristate.True case DefaultResourceUpdate: t.addResources( notify.TOPIC_RESOURCE_SERVER, @@ -251,6 +254,7 @@ func (sm *STopicManager) InitializeData() error { t.addResources(notify.TOPIC_RESOURCE_SCHEDULEDTASK) t.addAction(notify.ActionExecute) t.Type = notify.TOPIC_TYPE_AUTOMATED_PROCESS + t.Results = tristate.True case DefaultScalingPolicyExecute: t.addResources(notify.TOPIC_RESOURCE_SCALINGPOLICY) t.addAction(notify.ActionExecute) @@ -423,6 +427,7 @@ func (sm *STopicManager) InitializeData() error { t.Results = tristate.False t.Type = notify.TOPIC_TYPE_RESOURCE } + if topic == nil { err := sm.TableSpec().Insert(ctx, t) if err != nil { @@ -542,18 +547,42 @@ func (s *STopic) getActions() []notify.SAction { return actions } -func (sm *STopicManager) TopicByEvent(eventStr string, advanceDays int) (*STopic, error) { - topics, err := sm.TopicsByEvent(eventStr, advanceDays) +func (sm *STopicManager) GetTopicByEvent(resourceType string, action notify.SAction, isFailed notify.SResult, advanceDays int) (*STopic, error) { + topics, err := sm.GetTopicsByEvent(resourceType, action, isFailed, advanceDays) if err != nil { - return nil, err - } - if len(topics) == 1 { - return &topics[0], nil + return nil, errors.Wrapf(err, "GetTopicsByEvent") } if len(topics) == 0 { - return nil, errors.Wrapf(cloudprovider.ErrNotFound, "eventStr:%s,advanceDays:%d", eventStr, advanceDays) + return nil, httperrors.NewResourceNotFoundError("no available topic found by %s %s", action, resourceType) } - return nil, errors.Wrapf(cloudprovider.ErrDuplicateId, "eventStr:%s,advanceDays:%d", eventStr, advanceDays) + // free memory in time + if len(topics) > 1 { + return nil, httperrors.NewResourceNotFoundError("duplicates %d topics found by %s %s", len(topics), action, resourceType) + } + return &topics[0], nil +} + +func (sm *STopicManager) GetTopicsByEvent(resourceType string, action notify.SAction, isFailed notify.SResult, advanceDays int) ([]STopic, error) { + resourceV := converter.resourceValue(resourceType) + if resourceV < 0 { + return nil, fmt.Errorf("unknow resource type %s", resourceType) + } + actionV := converter.actionValue(action) + if actionV < 0 { + return nil, fmt.Errorf("unkonwn action %s", action) + } + q := sm.Query().Equals("advance_days", advanceDays) + if isFailed == api.ResultSucceed { + q = q.Equals("results", true) + } else { + q = q.Equals("results", false) + } + q = q.Equals("enabled", true) + q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("resources"), 1< 1 { - return nil, sqlchemy.ErrDuplicateEntry - } else { - return nil, sql.ErrNoRows - } -} - -func (model *SStandaloneResourceBase) StandaloneModelManager() db.IStandaloneModelManager { - return model.GetModelManager().(db.IStandaloneModelManager) -} - -func (model *SStandaloneResourceBase) GetId() string { - return model.ID -} - -func (model *SStandaloneResourceBase) GetIStandaloneModel() db.IStandaloneModel { - return model.GetVirtualObject().(db.IStandaloneModel) -} diff --git a/pkg/notify/oldmodels/statusstandalone.go b/pkg/notify/oldmodels/statusstandalone.go deleted file mode 100644 index a809b6d76c..0000000000 --- a/pkg/notify/oldmodels/statusstandalone.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oldmodels - -import ( - "context" - "fmt" - - "yunion.io/x/jsonutils" - "yunion.io/x/pkg/utils" - - "yunion.io/x/onecloud/pkg/cloudcommon/db" - "yunion.io/x/onecloud/pkg/mcclient" -) - -type SStatusStandaloneResourceBase struct { - SStandaloneResourceBase - - Status string `width:"36" charset:"ascii" nullable:"false" default:"init" create:"optional" update:"user"` -} - -type SStatusStandaloneResourceBaseManager struct { - SStandaloneResourceBaseManager -} - -func NewStatusStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStatusStandaloneResourceBaseManager { - return SStatusStandaloneResourceBaseManager{SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(dt, tableName, keyword, keywordPlural)} -} - -func (model *SStatusStandaloneResourceBase) SetStatus(userCred mcclient.TokenCredential, status string, reason string) error { - if model.Status == status { - return nil - } - oldStatus := model.Status - _, err := db.Update(model, func() error { - model.Status = status - return nil - }) - if err != nil { - return err - } - if userCred != nil { - notes := fmt.Sprintf("%s=>%s", oldStatus, status) - if len(reason) > 0 { - notes = fmt.Sprintf("%s: %s", notes, reason) - } - db.OpsLog.LogEvent(model, db.ACT_UPDATE_STATUS, notes, userCred) - } - return nil -} - -func (model *SStatusStandaloneResourceBase) PerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - status, err := data.GetString("status") - if err != nil { - return nil, err - } - reason, _ := data.GetString("reason") - err = model.SetStatus(userCred, status, reason) - return nil, err -} - -func (model *SStatusStandaloneResourceBase) IsInStatus(status ...string) bool { - return utils.IsInStringArray(model.Status, status) -} - -func (model *SStatusStandaloneResourceBase) GetDetailsStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { - ret := jsonutils.NewDict() - ret.Add(jsonutils.NewString(model.Status), "status") - return ret, nil -} diff --git a/pkg/notify/oldmodels/usercache.go b/pkg/notify/oldmodels/usercache.go deleted file mode 100644 index 0dd3e9c84f..0000000000 --- a/pkg/notify/oldmodels/usercache.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oldmodels - -import ( - "context" - "fmt" - - "yunion.io/x/sqlchemy" - - "yunion.io/x/onecloud/pkg/cloudcommon/db" - "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/mcclient/auth" -) - -type SUserCacheManager struct { - db.SUserCacheManager -} - -type SUser struct { - db.SUser -} - -func (user *SUser) GetModelManager() db.IModelManager { - return UserCacheManager -} - -var UserCacheManager *SUserCacheManager - -func init() { - dbUserCacheManager := db.SUserCacheManager{ - SKeystoneCacheObjectManager: db.NewKeystoneCacheObjectManager( - db.SUser{}, "users_cache_tbl", "olduser", "oldusers"), - } - UserCacheManager = &SUserCacheManager{ - dbUserCacheManager, - } - UserCacheManager.SetVirtualObject(&dbUserCacheManager) -} - -func RegistUserCredCacheUpdater() { - auth.RegisterAuthHook(onAuthCompleteUpdateCache) -} - -func onAuthCompleteUpdateCache(ctx context.Context, userCred mcclient.TokenCredential) { - UserCacheManager.updateUserCache(ctx, userCred) -} - -func (ucm *SUserCacheManager) updateUserCache(ctx context.Context, userCred mcclient.TokenCredential) { - ucm.Save(ctx, userCred.GetUserId(), userCred.GetUserName(), - userCred.GetDomainId(), userCred.GetDomainName()) -} - -func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) (map[string]SUser, error) { - q := ucm.Query().In("id", ids) - users := make([]db.SUser, 0) - err := db.FetchModelObjects(ucm, q, &users) - if err != nil { - return nil, err - } - ret := make(map[string]SUser) - - for i := range users { - ret[users[i].Id] = SUser{users[i]} - } - - // check that id is exist - for _, id := range ids { - if _, ok := ret[id]; ok { - continue - } - user, err := ucm.FetchUserFromKeystone(ctx, id) - if err != nil { - continue - } - ret[id] = SUser{*user} - } - return ret, nil -} - -func (ucm *SUserCacheManager) FetchUserByIDOrName(ctx context.Context, idStr string) (*SUser, error) { - user, err := ucm.SUserCacheManager.FetchUserByIdOrName(ctx, idStr) - if err != nil { - return nil, err - } - return &SUser{*user}, nil -} - -func (ucm *SUserCacheManager) FetchUserLikeName(ctx context.Context, name string, noExpireCheck bool) ([]SUser, - error) { - - if !noExpireCheck { - // todo - return nil, fmt.Errorf("FetchUserLikeName with check Not Implement") - } - q := ucm.Query().Contains("name", name) - return ucm.FetchUserFromLoaclCache(ctx, q) -} - -func (ucm *SUserCacheManager) FetchUserFromLoaclCache(ctx context.Context, q *sqlchemy.SQuery) ([]SUser, error) { - dbUsers := make([]db.SUser, 0, 1) - err := db.FetchModelObjects(ucm, q, &dbUsers) - if err != nil { - return nil, err - } - users := make([]SUser, len(dbUsers)) - for i := range dbUsers { - users[i] = SUser{dbUsers[i]} - } - return users, nil -} - -func (u *SUser) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { - return u.SUser.Delete(ctx, userCred) -} diff --git a/pkg/notify/options/options.go b/pkg/notify/options/options.go index 67aafeda39..4cec0e752a 100644 --- a/pkg/notify/options/options.go +++ b/pkg/notify/options/options.go @@ -22,6 +22,7 @@ type NotifyOption struct { common_options.CommonOptions common_options.DBOptions + DebugRequest bool `help:"Debug Send request"` SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/socket"` UpdateInterval int `help:"Update send services interval(unit:min)" default:"30"` @@ -34,7 +35,8 @@ type NotifyOption struct { VerifyExpireInterval int `help:"expire interval of verify message; minutes" default:"2"` VerifyValidInterval int `help:"valid interval of verify message; miniutes" default:"20"` - SyncReceiverIntervalMinutes int `help:"interval to sync receivers from keystone, in minutes" default:"30"` + SyncReceiverIntervalMinutes int `help:"interval to sync receivers from keystone, in minutes" default:"30"` + EnableWatchUser bool `help:"use etcd to watch user" default:"false"` } var Options NotifyOption diff --git a/pkg/notify/rpc/apis/send_client.go b/pkg/notify/rpc/apis/send_client.go deleted file mode 100644 index 71248f9f95..0000000000 --- a/pkg/notify/rpc/apis/send_client.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package apis - -import ( - "context" - "time" - - "google.golang.org/grpc" -) - -type SendNotificationClient struct { - sendAgentClient - Conn *grpc.ClientConn - CallTimeout time.Duration -} - -func NewSendNotificationClient(cc *grpc.ClientConn) *SendNotificationClient { - return &SendNotificationClient{ - sendAgentClient: sendAgentClient{cc}, - Conn: cc, - CallTimeout: 30 * time.Second, - } -} - -func (c *SendNotificationClient) Send(ctx context.Context, in *SendParams, opts ...grpc.CallOption) (*Empty, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.Send(ctx, in, opts...) -} - -func (c *SendNotificationClient) AddConfig(ctx context.Context, in *AddConfigInput, opts ...grpc.CallOption) (*Empty, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.AddConfig(ctx, in, opts...) -} - -func (c *SendNotificationClient) DeleteConfig(ctx context.Context, in *DeleteConfigInput, opts ...grpc.CallOption) (*Empty, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.DeleteConfig(ctx, in, opts...) -} - -func (c *SendNotificationClient) UpdateConfig(ctx context.Context, in *UpdateConfigInput, opts ...grpc.CallOption) (*Empty, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.UpdateConfig(ctx, in, opts...) -} - -func (c *SendNotificationClient) CompleteConfig(ctx context.Context, in *CompleteConfigInput, opts ...grpc.CallOption) (*Empty, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.CompleteConfig(ctx, in, opts...) -} - -func (c *SendNotificationClient) ValidateConfig(ctx context.Context, in *ValidateConfigInput, opts ...grpc.CallOption) (*ValidateConfigReply, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.ValidateConfig(ctx, in, opts...) -} - -func (c *SendNotificationClient) UseridByMobile(ctx context.Context, in *UseridByMobileParams, opts ...grpc.CallOption) (*UseridByMobileReply, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.UseridByMobile(ctx, in, opts...) -} - -func (c *SendNotificationClient) Ready(ctx context.Context, in *ReadyInput, opts ...grpc.CallOption) (*ReadyOutput, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.Ready(ctx, in, opts...) -} - -func (c *SendNotificationClient) BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error) { - ctx, cancel := context.WithTimeout(ctx, c.CallTimeout) - defer cancel() - return c.sendAgentClient.BatchSend(ctx, in, opts...) -} diff --git a/pkg/notify/rpc/apis/send_server.pb.go b/pkg/notify/rpc/apis/send_server.pb.go deleted file mode 100644 index a147ecbee0..0000000000 --- a/pkg/notify/rpc/apis/send_server.pb.go +++ /dev/null @@ -1,1218 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: send_server.proto - -package apis - -import ( - context "context" - fmt "fmt" - math "math" - - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package - -type SendParams struct { - Receiver *SReceiver `protobuf:"bytes,1,opt,name=Receiver,proto3" json:"Receiver,omitempty"` - Topic string `protobuf:"bytes,2,opt,name=Topic,proto3" json:"Topic,omitempty"` - Title string `protobuf:"bytes,3,opt,name=Title,proto3" json:"Title,omitempty"` - Message string `protobuf:"bytes,4,opt,name=Message,proto3" json:"Message,omitempty"` - Priority string `protobuf:"bytes,5,opt,name=Priority,proto3" json:"Priority,omitempty"` - RemoteTemplate string `protobuf:"bytes,6,opt,name=RemoteTemplate,proto3" json:"RemoteTemplate,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *SendParams) Reset() { *m = SendParams{} } -func (m *SendParams) String() string { return proto.CompactTextString(m) } -func (*SendParams) ProtoMessage() {} -func (*SendParams) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{0} -} - -func (m *SendParams) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_SendParams.Unmarshal(m, b) -} -func (m *SendParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_SendParams.Marshal(b, m, deterministic) -} -func (m *SendParams) XXX_Merge(src proto.Message) { - xxx_messageInfo_SendParams.Merge(m, src) -} -func (m *SendParams) XXX_Size() int { - return xxx_messageInfo_SendParams.Size(m) -} -func (m *SendParams) XXX_DiscardUnknown() { - xxx_messageInfo_SendParams.DiscardUnknown(m) -} - -var xxx_messageInfo_SendParams proto.InternalMessageInfo - -func (m *SendParams) GetReceiver() *SReceiver { - if m != nil { - return m.Receiver - } - return nil -} - -func (m *SendParams) GetTopic() string { - if m != nil { - return m.Topic - } - return "" -} - -func (m *SendParams) GetTitle() string { - if m != nil { - return m.Title - } - return "" -} - -func (m *SendParams) GetMessage() string { - if m != nil { - return m.Message - } - return "" -} - -func (m *SendParams) GetPriority() string { - if m != nil { - return m.Priority - } - return "" -} - -func (m *SendParams) GetRemoteTemplate() string { - if m != nil { - return m.RemoteTemplate - } - return "" -} - -type ValidateConfigInput struct { - Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ValidateConfigInput) Reset() { *m = ValidateConfigInput{} } -func (m *ValidateConfigInput) String() string { return proto.CompactTextString(m) } -func (*ValidateConfigInput) ProtoMessage() {} -func (*ValidateConfigInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{1} -} - -func (m *ValidateConfigInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ValidateConfigInput.Unmarshal(m, b) -} -func (m *ValidateConfigInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ValidateConfigInput.Marshal(b, m, deterministic) -} -func (m *ValidateConfigInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_ValidateConfigInput.Merge(m, src) -} -func (m *ValidateConfigInput) XXX_Size() int { - return xxx_messageInfo_ValidateConfigInput.Size(m) -} -func (m *ValidateConfigInput) XXX_DiscardUnknown() { - xxx_messageInfo_ValidateConfigInput.DiscardUnknown(m) -} - -var xxx_messageInfo_ValidateConfigInput proto.InternalMessageInfo - -func (m *ValidateConfigInput) GetConfigs() map[string]string { - if m != nil { - return m.Configs - } - return nil -} - -type AddConfigInput struct { - Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - DomainId string `protobuf:"bytes,2,opt,name=domainId,proto3" json:"domainId,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *AddConfigInput) Reset() { *m = AddConfigInput{} } -func (m *AddConfigInput) String() string { return proto.CompactTextString(m) } -func (*AddConfigInput) ProtoMessage() {} -func (*AddConfigInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{2} -} - -func (m *AddConfigInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_AddConfigInput.Unmarshal(m, b) -} -func (m *AddConfigInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_AddConfigInput.Marshal(b, m, deterministic) -} -func (m *AddConfigInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_AddConfigInput.Merge(m, src) -} -func (m *AddConfigInput) XXX_Size() int { - return xxx_messageInfo_AddConfigInput.Size(m) -} -func (m *AddConfigInput) XXX_DiscardUnknown() { - xxx_messageInfo_AddConfigInput.DiscardUnknown(m) -} - -var xxx_messageInfo_AddConfigInput proto.InternalMessageInfo - -func (m *AddConfigInput) GetConfigs() map[string]string { - if m != nil { - return m.Configs - } - return nil -} - -func (m *AddConfigInput) GetDomainId() string { - if m != nil { - return m.DomainId - } - return "" -} - -type UpdateConfigInput struct { - Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - DomainId string `protobuf:"bytes,2,opt,name=domainId,proto3" json:"domainId,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *UpdateConfigInput) Reset() { *m = UpdateConfigInput{} } -func (m *UpdateConfigInput) String() string { return proto.CompactTextString(m) } -func (*UpdateConfigInput) ProtoMessage() {} -func (*UpdateConfigInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{3} -} - -func (m *UpdateConfigInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_UpdateConfigInput.Unmarshal(m, b) -} -func (m *UpdateConfigInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_UpdateConfigInput.Marshal(b, m, deterministic) -} -func (m *UpdateConfigInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_UpdateConfigInput.Merge(m, src) -} -func (m *UpdateConfigInput) XXX_Size() int { - return xxx_messageInfo_UpdateConfigInput.Size(m) -} -func (m *UpdateConfigInput) XXX_DiscardUnknown() { - xxx_messageInfo_UpdateConfigInput.DiscardUnknown(m) -} - -var xxx_messageInfo_UpdateConfigInput proto.InternalMessageInfo - -func (m *UpdateConfigInput) GetConfigs() map[string]string { - if m != nil { - return m.Configs - } - return nil -} - -func (m *UpdateConfigInput) GetDomainId() string { - if m != nil { - return m.DomainId - } - return "" -} - -type DeleteConfigInput struct { - DomainId string `protobuf:"bytes,1,opt,name=domainId,proto3" json:"domainId,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *DeleteConfigInput) Reset() { *m = DeleteConfigInput{} } -func (m *DeleteConfigInput) String() string { return proto.CompactTextString(m) } -func (*DeleteConfigInput) ProtoMessage() {} -func (*DeleteConfigInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{4} -} - -func (m *DeleteConfigInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_DeleteConfigInput.Unmarshal(m, b) -} -func (m *DeleteConfigInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_DeleteConfigInput.Marshal(b, m, deterministic) -} -func (m *DeleteConfigInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_DeleteConfigInput.Merge(m, src) -} -func (m *DeleteConfigInput) XXX_Size() int { - return xxx_messageInfo_DeleteConfigInput.Size(m) -} -func (m *DeleteConfigInput) XXX_DiscardUnknown() { - xxx_messageInfo_DeleteConfigInput.DiscardUnknown(m) -} - -var xxx_messageInfo_DeleteConfigInput proto.InternalMessageInfo - -func (m *DeleteConfigInput) GetDomainId() string { - if m != nil { - return m.DomainId - } - return "" -} - -type CompleteConfigInput struct { - ConfigInput []*AddConfigInput `protobuf:"bytes,1,rep,name=ConfigInput,proto3" json:"ConfigInput,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CompleteConfigInput) Reset() { *m = CompleteConfigInput{} } -func (m *CompleteConfigInput) String() string { return proto.CompactTextString(m) } -func (*CompleteConfigInput) ProtoMessage() {} -func (*CompleteConfigInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{5} -} - -func (m *CompleteConfigInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CompleteConfigInput.Unmarshal(m, b) -} -func (m *CompleteConfigInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CompleteConfigInput.Marshal(b, m, deterministic) -} -func (m *CompleteConfigInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_CompleteConfigInput.Merge(m, src) -} -func (m *CompleteConfigInput) XXX_Size() int { - return xxx_messageInfo_CompleteConfigInput.Size(m) -} -func (m *CompleteConfigInput) XXX_DiscardUnknown() { - xxx_messageInfo_CompleteConfigInput.DiscardUnknown(m) -} - -var xxx_messageInfo_CompleteConfigInput proto.InternalMessageInfo - -func (m *CompleteConfigInput) GetConfigInput() []*AddConfigInput { - if m != nil { - return m.ConfigInput - } - return nil -} - -type UseridByMobileParams struct { - Mobile string `protobuf:"bytes,1,opt,name=mobile,proto3" json:"mobile,omitempty"` - DomainId string `protobuf:"bytes,2,opt,name=domainId,proto3" json:"domainId,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *UseridByMobileParams) Reset() { *m = UseridByMobileParams{} } -func (m *UseridByMobileParams) String() string { return proto.CompactTextString(m) } -func (*UseridByMobileParams) ProtoMessage() {} -func (*UseridByMobileParams) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{6} -} - -func (m *UseridByMobileParams) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_UseridByMobileParams.Unmarshal(m, b) -} -func (m *UseridByMobileParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_UseridByMobileParams.Marshal(b, m, deterministic) -} -func (m *UseridByMobileParams) XXX_Merge(src proto.Message) { - xxx_messageInfo_UseridByMobileParams.Merge(m, src) -} -func (m *UseridByMobileParams) XXX_Size() int { - return xxx_messageInfo_UseridByMobileParams.Size(m) -} -func (m *UseridByMobileParams) XXX_DiscardUnknown() { - xxx_messageInfo_UseridByMobileParams.DiscardUnknown(m) -} - -var xxx_messageInfo_UseridByMobileParams proto.InternalMessageInfo - -func (m *UseridByMobileParams) GetMobile() string { - if m != nil { - return m.Mobile - } - return "" -} - -func (m *UseridByMobileParams) GetDomainId() string { - if m != nil { - return m.DomainId - } - return "" -} - -type Empty struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Empty) Reset() { *m = Empty{} } -func (m *Empty) String() string { return proto.CompactTextString(m) } -func (*Empty) ProtoMessage() {} -func (*Empty) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{7} -} - -func (m *Empty) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Empty.Unmarshal(m, b) -} -func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Empty.Marshal(b, m, deterministic) -} -func (m *Empty) XXX_Merge(src proto.Message) { - xxx_messageInfo_Empty.Merge(m, src) -} -func (m *Empty) XXX_Size() int { - return xxx_messageInfo_Empty.Size(m) -} -func (m *Empty) XXX_DiscardUnknown() { - xxx_messageInfo_Empty.DiscardUnknown(m) -} - -var xxx_messageInfo_Empty proto.InternalMessageInfo - -type UseridByMobileReply struct { - Userid string `protobuf:"bytes,1,opt,name=userid,proto3" json:"userid,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *UseridByMobileReply) Reset() { *m = UseridByMobileReply{} } -func (m *UseridByMobileReply) String() string { return proto.CompactTextString(m) } -func (*UseridByMobileReply) ProtoMessage() {} -func (*UseridByMobileReply) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{8} -} - -func (m *UseridByMobileReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_UseridByMobileReply.Unmarshal(m, b) -} -func (m *UseridByMobileReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_UseridByMobileReply.Marshal(b, m, deterministic) -} -func (m *UseridByMobileReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_UseridByMobileReply.Merge(m, src) -} -func (m *UseridByMobileReply) XXX_Size() int { - return xxx_messageInfo_UseridByMobileReply.Size(m) -} -func (m *UseridByMobileReply) XXX_DiscardUnknown() { - xxx_messageInfo_UseridByMobileReply.DiscardUnknown(m) -} - -var xxx_messageInfo_UseridByMobileReply proto.InternalMessageInfo - -func (m *UseridByMobileReply) GetUserid() string { - if m != nil { - return m.Userid - } - return "" -} - -type ValidateConfigReply struct { - IsValid bool `protobuf:"varint,1,opt,name=isValid,proto3" json:"isValid,omitempty"` - Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ValidateConfigReply) Reset() { *m = ValidateConfigReply{} } -func (m *ValidateConfigReply) String() string { return proto.CompactTextString(m) } -func (*ValidateConfigReply) ProtoMessage() {} -func (*ValidateConfigReply) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{9} -} - -func (m *ValidateConfigReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ValidateConfigReply.Unmarshal(m, b) -} -func (m *ValidateConfigReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ValidateConfigReply.Marshal(b, m, deterministic) -} -func (m *ValidateConfigReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_ValidateConfigReply.Merge(m, src) -} -func (m *ValidateConfigReply) XXX_Size() int { - return xxx_messageInfo_ValidateConfigReply.Size(m) -} -func (m *ValidateConfigReply) XXX_DiscardUnknown() { - xxx_messageInfo_ValidateConfigReply.DiscardUnknown(m) -} - -var xxx_messageInfo_ValidateConfigReply proto.InternalMessageInfo - -func (m *ValidateConfigReply) GetIsValid() bool { - if m != nil { - return m.IsValid - } - return false -} - -func (m *ValidateConfigReply) GetMsg() string { - if m != nil { - return m.Msg - } - return "" -} - -type SReceiver struct { - Contact string `protobuf:"bytes,1,opt,name=Contact,proto3" json:"Contact,omitempty"` - DomainId string `protobuf:"bytes,2,opt,name=DomainId,proto3" json:"DomainId,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *SReceiver) Reset() { *m = SReceiver{} } -func (m *SReceiver) String() string { return proto.CompactTextString(m) } -func (*SReceiver) ProtoMessage() {} -func (*SReceiver) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{10} -} - -func (m *SReceiver) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_SReceiver.Unmarshal(m, b) -} -func (m *SReceiver) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_SReceiver.Marshal(b, m, deterministic) -} -func (m *SReceiver) XXX_Merge(src proto.Message) { - xxx_messageInfo_SReceiver.Merge(m, src) -} -func (m *SReceiver) XXX_Size() int { - return xxx_messageInfo_SReceiver.Size(m) -} -func (m *SReceiver) XXX_DiscardUnknown() { - xxx_messageInfo_SReceiver.DiscardUnknown(m) -} - -var xxx_messageInfo_SReceiver proto.InternalMessageInfo - -func (m *SReceiver) GetContact() string { - if m != nil { - return m.Contact - } - return "" -} - -func (m *SReceiver) GetDomainId() string { - if m != nil { - return m.DomainId - } - return "" -} - -type BatchSendParams struct { - Receivers []*SReceiver `protobuf:"bytes,1,rep,name=Receivers,proto3" json:"Receivers,omitempty"` - Title string `protobuf:"bytes,2,opt,name=Title,proto3" json:"Title,omitempty"` - Message string `protobuf:"bytes,3,opt,name=Message,proto3" json:"Message,omitempty"` - Priority string `protobuf:"bytes,4,opt,name=Priority,proto3" json:"Priority,omitempty"` - RemoteTemplate string `protobuf:"bytes,5,opt,name=RemoteTemplate,proto3" json:"RemoteTemplate,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *BatchSendParams) Reset() { *m = BatchSendParams{} } -func (m *BatchSendParams) String() string { return proto.CompactTextString(m) } -func (*BatchSendParams) ProtoMessage() {} -func (*BatchSendParams) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{11} -} - -func (m *BatchSendParams) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_BatchSendParams.Unmarshal(m, b) -} -func (m *BatchSendParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_BatchSendParams.Marshal(b, m, deterministic) -} -func (m *BatchSendParams) XXX_Merge(src proto.Message) { - xxx_messageInfo_BatchSendParams.Merge(m, src) -} -func (m *BatchSendParams) XXX_Size() int { - return xxx_messageInfo_BatchSendParams.Size(m) -} -func (m *BatchSendParams) XXX_DiscardUnknown() { - xxx_messageInfo_BatchSendParams.DiscardUnknown(m) -} - -var xxx_messageInfo_BatchSendParams proto.InternalMessageInfo - -func (m *BatchSendParams) GetReceivers() []*SReceiver { - if m != nil { - return m.Receivers - } - return nil -} - -func (m *BatchSendParams) GetTitle() string { - if m != nil { - return m.Title - } - return "" -} - -func (m *BatchSendParams) GetMessage() string { - if m != nil { - return m.Message - } - return "" -} - -func (m *BatchSendParams) GetPriority() string { - if m != nil { - return m.Priority - } - return "" -} - -func (m *BatchSendParams) GetRemoteTemplate() string { - if m != nil { - return m.RemoteTemplate - } - return "" -} - -type FailedRecord struct { - Receiver *SReceiver `protobuf:"bytes,1,opt,name=Receiver,proto3" json:"Receiver,omitempty"` - Reason string `protobuf:"bytes,2,opt,name=Reason,proto3" json:"Reason,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *FailedRecord) Reset() { *m = FailedRecord{} } -func (m *FailedRecord) String() string { return proto.CompactTextString(m) } -func (*FailedRecord) ProtoMessage() {} -func (*FailedRecord) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{12} -} - -func (m *FailedRecord) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_FailedRecord.Unmarshal(m, b) -} -func (m *FailedRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_FailedRecord.Marshal(b, m, deterministic) -} -func (m *FailedRecord) XXX_Merge(src proto.Message) { - xxx_messageInfo_FailedRecord.Merge(m, src) -} -func (m *FailedRecord) XXX_Size() int { - return xxx_messageInfo_FailedRecord.Size(m) -} -func (m *FailedRecord) XXX_DiscardUnknown() { - xxx_messageInfo_FailedRecord.DiscardUnknown(m) -} - -var xxx_messageInfo_FailedRecord proto.InternalMessageInfo - -func (m *FailedRecord) GetReceiver() *SReceiver { - if m != nil { - return m.Receiver - } - return nil -} - -func (m *FailedRecord) GetReason() string { - if m != nil { - return m.Reason - } - return "" -} - -type BatchSendReply struct { - FailedRecords []*FailedRecord `protobuf:"bytes,1,rep,name=FailedRecords,proto3" json:"FailedRecords,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *BatchSendReply) Reset() { *m = BatchSendReply{} } -func (m *BatchSendReply) String() string { return proto.CompactTextString(m) } -func (*BatchSendReply) ProtoMessage() {} -func (*BatchSendReply) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{13} -} - -func (m *BatchSendReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_BatchSendReply.Unmarshal(m, b) -} -func (m *BatchSendReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_BatchSendReply.Marshal(b, m, deterministic) -} -func (m *BatchSendReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_BatchSendReply.Merge(m, src) -} -func (m *BatchSendReply) XXX_Size() int { - return xxx_messageInfo_BatchSendReply.Size(m) -} -func (m *BatchSendReply) XXX_DiscardUnknown() { - xxx_messageInfo_BatchSendReply.DiscardUnknown(m) -} - -var xxx_messageInfo_BatchSendReply proto.InternalMessageInfo - -func (m *BatchSendReply) GetFailedRecords() []*FailedRecord { - if m != nil { - return m.FailedRecords - } - return nil -} - -type ReadyInput struct { - DomainIds []string `protobuf:"bytes,1,rep,name=DomainIds,proto3" json:"DomainIds,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ReadyInput) Reset() { *m = ReadyInput{} } -func (m *ReadyInput) String() string { return proto.CompactTextString(m) } -func (*ReadyInput) ProtoMessage() {} -func (*ReadyInput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{14} -} - -func (m *ReadyInput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ReadyInput.Unmarshal(m, b) -} -func (m *ReadyInput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ReadyInput.Marshal(b, m, deterministic) -} -func (m *ReadyInput) XXX_Merge(src proto.Message) { - xxx_messageInfo_ReadyInput.Merge(m, src) -} -func (m *ReadyInput) XXX_Size() int { - return xxx_messageInfo_ReadyInput.Size(m) -} -func (m *ReadyInput) XXX_DiscardUnknown() { - xxx_messageInfo_ReadyInput.DiscardUnknown(m) -} - -var xxx_messageInfo_ReadyInput proto.InternalMessageInfo - -func (m *ReadyInput) GetDomainIds() []string { - if m != nil { - return m.DomainIds - } - return nil -} - -type ReadyOutput struct { - Ok bool `protobuf:"varint,1,opt,name=Ok,proto3" json:"Ok,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ReadyOutput) Reset() { *m = ReadyOutput{} } -func (m *ReadyOutput) String() string { return proto.CompactTextString(m) } -func (*ReadyOutput) ProtoMessage() {} -func (*ReadyOutput) Descriptor() ([]byte, []int) { - return fileDescriptor_63fdd68f7eb311f9, []int{15} -} - -func (m *ReadyOutput) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ReadyOutput.Unmarshal(m, b) -} -func (m *ReadyOutput) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ReadyOutput.Marshal(b, m, deterministic) -} -func (m *ReadyOutput) XXX_Merge(src proto.Message) { - xxx_messageInfo_ReadyOutput.Merge(m, src) -} -func (m *ReadyOutput) XXX_Size() int { - return xxx_messageInfo_ReadyOutput.Size(m) -} -func (m *ReadyOutput) XXX_DiscardUnknown() { - xxx_messageInfo_ReadyOutput.DiscardUnknown(m) -} - -var xxx_messageInfo_ReadyOutput proto.InternalMessageInfo - -func (m *ReadyOutput) GetOk() bool { - if m != nil { - return m.Ok - } - return false -} - -func init() { - proto.RegisterType((*SendParams)(nil), "apis.SendParams") - proto.RegisterType((*ValidateConfigInput)(nil), "apis.ValidateConfigInput") - proto.RegisterMapType((map[string]string)(nil), "apis.ValidateConfigInput.ConfigsEntry") - proto.RegisterType((*AddConfigInput)(nil), "apis.AddConfigInput") - proto.RegisterMapType((map[string]string)(nil), "apis.AddConfigInput.ConfigsEntry") - proto.RegisterType((*UpdateConfigInput)(nil), "apis.UpdateConfigInput") - proto.RegisterMapType((map[string]string)(nil), "apis.UpdateConfigInput.ConfigsEntry") - proto.RegisterType((*DeleteConfigInput)(nil), "apis.DeleteConfigInput") - proto.RegisterType((*CompleteConfigInput)(nil), "apis.CompleteConfigInput") - proto.RegisterType((*UseridByMobileParams)(nil), "apis.UseridByMobileParams") - proto.RegisterType((*Empty)(nil), "apis.Empty") - proto.RegisterType((*UseridByMobileReply)(nil), "apis.UseridByMobileReply") - proto.RegisterType((*ValidateConfigReply)(nil), "apis.ValidateConfigReply") - proto.RegisterType((*SReceiver)(nil), "apis.SReceiver") - proto.RegisterType((*BatchSendParams)(nil), "apis.BatchSendParams") - proto.RegisterType((*FailedRecord)(nil), "apis.FailedRecord") - proto.RegisterType((*BatchSendReply)(nil), "apis.BatchSendReply") - proto.RegisterType((*ReadyInput)(nil), "apis.ReadyInput") - proto.RegisterType((*ReadyOutput)(nil), "apis.ReadyOutput") -} - -func init() { proto.RegisterFile("send_server.proto", fileDescriptor_63fdd68f7eb311f9) } - -var fileDescriptor_63fdd68f7eb311f9 = []byte{ - // 722 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0x5d, 0x6b, 0xd4, 0x4c, - 0x14, 0x26, 0xfb, 0xd9, 0x9c, 0xed, 0xbb, 0xed, 0x4e, 0xf7, 0xed, 0x9b, 0x77, 0x51, 0xa8, 0x41, - 0x4b, 0x51, 0xbb, 0x42, 0x15, 0x59, 0x2a, 0x88, 0xdb, 0x2f, 0x69, 0xa1, 0xb4, 0x4c, 0x5b, 0x6f, - 0x65, 0xba, 0x39, 0xae, 0xa1, 0xf9, 0x22, 0x99, 0x2d, 0xe4, 0xd2, 0xdf, 0xe0, 0x6f, 0xf0, 0xca, - 0x5b, 0xff, 0x81, 0x7f, 0x4c, 0x32, 0x93, 0xc9, 0x26, 0xd9, 0x54, 0x11, 0xc1, 0xbb, 0x3c, 0x67, - 0x9e, 0x73, 0xce, 0x93, 0x93, 0x39, 0x4f, 0xa0, 0x17, 0xa1, 0x67, 0xbd, 0x8f, 0x30, 0xbc, 0xc5, - 0x70, 0x18, 0x84, 0x3e, 0xf7, 0x49, 0x83, 0x05, 0x76, 0x64, 0x7e, 0xd7, 0x00, 0x2e, 0xd0, 0xb3, - 0xce, 0x59, 0xc8, 0xdc, 0x88, 0x3c, 0x81, 0x25, 0x8a, 0x13, 0xb4, 0x6f, 0x31, 0x34, 0xb4, 0x0d, - 0x6d, 0xab, 0xb3, 0xb3, 0x32, 0x4c, 0x78, 0xc3, 0x0b, 0x15, 0xa6, 0x19, 0x81, 0xf4, 0xa1, 0x79, - 0xe9, 0x07, 0xf6, 0xc4, 0xa8, 0x6d, 0x68, 0x5b, 0x3a, 0x95, 0x40, 0x44, 0x6d, 0xee, 0xa0, 0x51, - 0x4f, 0xa3, 0x09, 0x20, 0x06, 0xb4, 0x4f, 0x31, 0x8a, 0xd8, 0x14, 0x8d, 0x86, 0x88, 0x2b, 0x48, - 0x06, 0xb0, 0x74, 0x1e, 0xda, 0x7e, 0x68, 0xf3, 0xd8, 0x68, 0x8a, 0xa3, 0x0c, 0x93, 0x4d, 0xe8, - 0x52, 0x74, 0x7d, 0x8e, 0x97, 0xe8, 0x06, 0x0e, 0xe3, 0x68, 0xb4, 0x04, 0xa3, 0x14, 0x35, 0x3f, - 0x6b, 0xb0, 0xf6, 0x8e, 0x39, 0xb6, 0xc5, 0x38, 0xee, 0xfb, 0xde, 0x07, 0x7b, 0x7a, 0xec, 0x05, - 0x33, 0x4e, 0xde, 0x40, 0x7b, 0x22, 0x60, 0x64, 0x68, 0x1b, 0xf5, 0xad, 0xce, 0xce, 0xa6, 0x7c, - 0x9b, 0x0a, 0xee, 0x50, 0x3e, 0x47, 0x87, 0x1e, 0x0f, 0x63, 0xaa, 0xd2, 0x06, 0xbb, 0xb0, 0x9c, - 0x3f, 0x20, 0xab, 0x50, 0xbf, 0xc1, 0x58, 0xcc, 0x46, 0xa7, 0xc9, 0x63, 0xf2, 0xbe, 0xb7, 0xcc, - 0x99, 0xa1, 0x9a, 0x82, 0x00, 0xbb, 0xb5, 0x91, 0x66, 0x7e, 0xd1, 0xa0, 0x3b, 0xb6, 0xac, 0xbc, - 0xa0, 0x57, 0x65, 0x41, 0x0f, 0xa4, 0xa0, 0x22, 0xad, 0x5a, 0x4b, 0x32, 0x29, 0xcb, 0x77, 0x99, - 0xed, 0x1d, 0x5b, 0x69, 0xb3, 0x0c, 0xff, 0x91, 0xce, 0xaf, 0x1a, 0xf4, 0xae, 0x82, 0xf2, 0xec, - 0x5e, 0x97, 0xa5, 0x3e, 0x94, 0x52, 0x17, 0x98, 0x7f, 0x59, 0xed, 0x33, 0xe8, 0x1d, 0xa0, 0x83, - 0x45, 0xb1, 0xf9, 0x66, 0x5a, 0xb1, 0x99, 0x79, 0x0a, 0x6b, 0xfb, 0xbe, 0x1b, 0x94, 0x53, 0x5e, - 0x42, 0x27, 0x07, 0xd3, 0x77, 0xec, 0x57, 0x7d, 0x0e, 0x9a, 0x27, 0x9a, 0x27, 0xd0, 0xbf, 0x8a, - 0x30, 0xb4, 0xad, 0xbd, 0xf8, 0xd4, 0xbf, 0xb6, 0x1d, 0x4c, 0x57, 0x67, 0x1d, 0x5a, 0xae, 0xc0, - 0xa9, 0x80, 0x14, 0xfd, 0x6c, 0x0e, 0x66, 0x1b, 0x9a, 0x87, 0x6e, 0xc0, 0x63, 0x73, 0x1b, 0xd6, - 0x8a, 0x45, 0x29, 0x06, 0x4e, 0x9c, 0xd4, 0x9c, 0x89, 0xb0, 0xaa, 0x29, 0x91, 0x39, 0x2e, 0x5f, - 0x77, 0x49, 0x37, 0xa0, 0x6d, 0x47, 0xe2, 0x40, 0xf0, 0x97, 0xa8, 0x82, 0xc9, 0x80, 0xdd, 0x68, - 0x9a, 0xf6, 0x4f, 0x1e, 0xcd, 0x31, 0xe8, 0xd9, 0x4e, 0x27, 0x89, 0xfb, 0xbe, 0xc7, 0xd9, 0x84, - 0xa7, 0x8d, 0x14, 0x4c, 0xd4, 0x1f, 0x94, 0xd4, 0x2b, 0x6c, 0x7e, 0xd3, 0x60, 0x65, 0x8f, 0xf1, - 0xc9, 0xc7, 0x9c, 0x81, 0x6c, 0x83, 0xae, 0xaa, 0xaa, 0x7b, 0xb3, 0xe0, 0x20, 0x73, 0xc6, 0xdc, - 0x2c, 0x6a, 0x77, 0x98, 0x45, 0xfd, 0x6e, 0xb3, 0x68, 0xfc, 0xd2, 0x2c, 0x9a, 0x95, 0x66, 0x71, - 0x01, 0xcb, 0x47, 0xcc, 0x76, 0xd0, 0xa2, 0x38, 0xf1, 0x43, 0xeb, 0xf7, 0x3c, 0x6f, 0x1d, 0x5a, - 0x14, 0x59, 0xe4, 0x7b, 0xa9, 0xe2, 0x14, 0x99, 0x27, 0xd0, 0xcd, 0x46, 0x21, 0x3f, 0xc6, 0x08, - 0xfe, 0xc9, 0xb7, 0x51, 0xd3, 0x20, 0xb2, 0x76, 0xfe, 0x88, 0x16, 0x89, 0xe6, 0x63, 0x00, 0x8a, - 0xcc, 0x8a, 0xe5, 0x3d, 0xbd, 0x07, 0xba, 0x9a, 0xb8, 0xac, 0xa1, 0xd3, 0x79, 0xc0, 0xbc, 0x0f, - 0x1d, 0xc1, 0x3d, 0x9b, 0xf1, 0x84, 0xdc, 0x85, 0xda, 0xd9, 0x4d, 0xfa, 0xf1, 0x6b, 0x67, 0x37, - 0x3b, 0x9f, 0x1a, 0xa0, 0x27, 0x92, 0xc6, 0x53, 0xf4, 0x38, 0x79, 0x0a, 0x4d, 0x41, 0x26, 0xab, - 0x52, 0xc4, 0xbc, 0xcb, 0xa0, 0x97, 0x8b, 0xa4, 0xb5, 0x1e, 0x41, 0x23, 0x49, 0x55, 0xe4, 0xf9, - 0x47, 0x1e, 0x74, 0x64, 0x44, 0x5c, 0x5d, 0x32, 0x04, 0x3d, 0x5b, 0x17, 0x52, 0xb9, 0x3f, 0x45, - 0xfe, 0x08, 0xba, 0xc5, 0x75, 0x24, 0xff, 0xcb, 0xe3, 0x8a, 0x25, 0x2d, 0x66, 0xbe, 0x80, 0xe5, - 0xbc, 0xf9, 0x90, 0xff, 0xee, 0x30, 0xa4, 0x85, 0xac, 0xbc, 0x5f, 0xa8, 0xac, 0x05, 0x0f, 0x29, - 0x66, 0x1d, 0x41, 0xb7, 0xb8, 0x61, 0x4a, 0x65, 0xc5, 0xaf, 0x63, 0x50, 0x79, 0x24, 0x6f, 0xc1, - 0x5b, 0xe8, 0x16, 0x17, 0x9b, 0x0c, 0x52, 0xd5, 0x15, 0x1e, 0xa2, 0x0a, 0x55, 0x59, 0xc1, 0x08, - 0xf4, 0xec, 0x82, 0x91, 0x7f, 0x25, 0xaf, 0xb4, 0x7c, 0x83, 0x7e, 0x29, 0x2c, 0x32, 0xaf, 0x5b, - 0xe2, 0x7f, 0xff, 0xfc, 0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x44, 0xad, 0xbd, 0x04, 0x08, - 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// SendAgentClient is the client API for SendAgent service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type SendAgentClient interface { - Ready(ctx context.Context, in *ReadyInput, opts ...grpc.CallOption) (*ReadyOutput, error) - Send(ctx context.Context, in *SendParams, opts ...grpc.CallOption) (*Empty, error) - AddConfig(ctx context.Context, in *AddConfigInput, opts ...grpc.CallOption) (*Empty, error) - CompleteConfig(ctx context.Context, in *CompleteConfigInput, opts ...grpc.CallOption) (*Empty, error) - UpdateConfig(ctx context.Context, in *UpdateConfigInput, opts ...grpc.CallOption) (*Empty, error) - DeleteConfig(ctx context.Context, in *DeleteConfigInput, opts ...grpc.CallOption) (*Empty, error) - ValidateConfig(ctx context.Context, in *ValidateConfigInput, opts ...grpc.CallOption) (*ValidateConfigReply, error) - UseridByMobile(ctx context.Context, in *UseridByMobileParams, opts ...grpc.CallOption) (*UseridByMobileReply, error) - BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error) -} - -type sendAgentClient struct { - cc *grpc.ClientConn -} - -func NewSendAgentClient(cc *grpc.ClientConn) SendAgentClient { - return &sendAgentClient{cc} -} - -func (c *sendAgentClient) Ready(ctx context.Context, in *ReadyInput, opts ...grpc.CallOption) (*ReadyOutput, error) { - out := new(ReadyOutput) - err := c.cc.Invoke(ctx, "/apis.SendAgent/Ready", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) Send(ctx context.Context, in *SendParams, opts ...grpc.CallOption) (*Empty, error) { - out := new(Empty) - err := c.cc.Invoke(ctx, "/apis.SendAgent/Send", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) AddConfig(ctx context.Context, in *AddConfigInput, opts ...grpc.CallOption) (*Empty, error) { - out := new(Empty) - err := c.cc.Invoke(ctx, "/apis.SendAgent/AddConfig", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) CompleteConfig(ctx context.Context, in *CompleteConfigInput, opts ...grpc.CallOption) (*Empty, error) { - out := new(Empty) - err := c.cc.Invoke(ctx, "/apis.SendAgent/CompleteConfig", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) UpdateConfig(ctx context.Context, in *UpdateConfigInput, opts ...grpc.CallOption) (*Empty, error) { - out := new(Empty) - err := c.cc.Invoke(ctx, "/apis.SendAgent/UpdateConfig", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) DeleteConfig(ctx context.Context, in *DeleteConfigInput, opts ...grpc.CallOption) (*Empty, error) { - out := new(Empty) - err := c.cc.Invoke(ctx, "/apis.SendAgent/DeleteConfig", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) ValidateConfig(ctx context.Context, in *ValidateConfigInput, opts ...grpc.CallOption) (*ValidateConfigReply, error) { - out := new(ValidateConfigReply) - err := c.cc.Invoke(ctx, "/apis.SendAgent/ValidateConfig", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) UseridByMobile(ctx context.Context, in *UseridByMobileParams, opts ...grpc.CallOption) (*UseridByMobileReply, error) { - out := new(UseridByMobileReply) - err := c.cc.Invoke(ctx, "/apis.SendAgent/UseridByMobile", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *sendAgentClient) BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error) { - out := new(BatchSendReply) - err := c.cc.Invoke(ctx, "/apis.SendAgent/BatchSend", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// SendAgentServer is the server API for SendAgent service. -type SendAgentServer interface { - Ready(context.Context, *ReadyInput) (*ReadyOutput, error) - Send(context.Context, *SendParams) (*Empty, error) - AddConfig(context.Context, *AddConfigInput) (*Empty, error) - CompleteConfig(context.Context, *CompleteConfigInput) (*Empty, error) - UpdateConfig(context.Context, *UpdateConfigInput) (*Empty, error) - DeleteConfig(context.Context, *DeleteConfigInput) (*Empty, error) - ValidateConfig(context.Context, *ValidateConfigInput) (*ValidateConfigReply, error) - UseridByMobile(context.Context, *UseridByMobileParams) (*UseridByMobileReply, error) - BatchSend(context.Context, *BatchSendParams) (*BatchSendReply, error) -} - -// UnimplementedSendAgentServer can be embedded to have forward compatible implementations. -type UnimplementedSendAgentServer struct { -} - -func (*UnimplementedSendAgentServer) Ready(ctx context.Context, req *ReadyInput) (*ReadyOutput, error) { - return nil, status.Errorf(codes.Unimplemented, "method Ready not implemented") -} -func (*UnimplementedSendAgentServer) Send(ctx context.Context, req *SendParams) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Send not implemented") -} -func (*UnimplementedSendAgentServer) AddConfig(ctx context.Context, req *AddConfigInput) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method AddConfig not implemented") -} -func (*UnimplementedSendAgentServer) CompleteConfig(ctx context.Context, req *CompleteConfigInput) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method CompleteConfig not implemented") -} -func (*UnimplementedSendAgentServer) UpdateConfig(ctx context.Context, req *UpdateConfigInput) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateConfig not implemented") -} -func (*UnimplementedSendAgentServer) DeleteConfig(ctx context.Context, req *DeleteConfigInput) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteConfig not implemented") -} -func (*UnimplementedSendAgentServer) ValidateConfig(ctx context.Context, req *ValidateConfigInput) (*ValidateConfigReply, error) { - return nil, status.Errorf(codes.Unimplemented, "method ValidateConfig not implemented") -} -func (*UnimplementedSendAgentServer) UseridByMobile(ctx context.Context, req *UseridByMobileParams) (*UseridByMobileReply, error) { - return nil, status.Errorf(codes.Unimplemented, "method UseridByMobile not implemented") -} -func (*UnimplementedSendAgentServer) BatchSend(ctx context.Context, req *BatchSendParams) (*BatchSendReply, error) { - return nil, status.Errorf(codes.Unimplemented, "method BatchSend not implemented") -} - -func RegisterSendAgentServer(s *grpc.Server, srv SendAgentServer) { - s.RegisterService(&_SendAgent_serviceDesc, srv) -} - -func _SendAgent_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ReadyInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).Ready(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/Ready", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).Ready(ctx, req.(*ReadyInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SendParams) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).Send(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/Send", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).Send(ctx, req.(*SendParams)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_AddConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(AddConfigInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).AddConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/AddConfig", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).AddConfig(ctx, req.(*AddConfigInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_CompleteConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CompleteConfigInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).CompleteConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/CompleteConfig", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).CompleteConfig(ctx, req.(*CompleteConfigInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_UpdateConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateConfigInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).UpdateConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/UpdateConfig", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).UpdateConfig(ctx, req.(*UpdateConfigInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_DeleteConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteConfigInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).DeleteConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/DeleteConfig", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).DeleteConfig(ctx, req.(*DeleteConfigInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_ValidateConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ValidateConfigInput) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).ValidateConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/ValidateConfig", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).ValidateConfig(ctx, req.(*ValidateConfigInput)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_UseridByMobile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UseridByMobileParams) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).UseridByMobile(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/UseridByMobile", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).UseridByMobile(ctx, req.(*UseridByMobileParams)) - } - return interceptor(ctx, in, info, handler) -} - -func _SendAgent_BatchSend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(BatchSendParams) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SendAgentServer).BatchSend(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/apis.SendAgent/BatchSend", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SendAgentServer).BatchSend(ctx, req.(*BatchSendParams)) - } - return interceptor(ctx, in, info, handler) -} - -var _SendAgent_serviceDesc = grpc.ServiceDesc{ - ServiceName: "apis.SendAgent", - HandlerType: (*SendAgentServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Ready", - Handler: _SendAgent_Ready_Handler, - }, - { - MethodName: "Send", - Handler: _SendAgent_Send_Handler, - }, - { - MethodName: "AddConfig", - Handler: _SendAgent_AddConfig_Handler, - }, - { - MethodName: "CompleteConfig", - Handler: _SendAgent_CompleteConfig_Handler, - }, - { - MethodName: "UpdateConfig", - Handler: _SendAgent_UpdateConfig_Handler, - }, - { - MethodName: "DeleteConfig", - Handler: _SendAgent_DeleteConfig_Handler, - }, - { - MethodName: "ValidateConfig", - Handler: _SendAgent_ValidateConfig_Handler, - }, - { - MethodName: "UseridByMobile", - Handler: _SendAgent_UseridByMobile_Handler, - }, - { - MethodName: "BatchSend", - Handler: _SendAgent_BatchSend_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "send_server.proto", -} diff --git a/pkg/notify/rpc/apis/send_server.proto b/pkg/notify/rpc/apis/send_server.proto deleted file mode 100644 index 79d351182c..0000000000 --- a/pkg/notify/rpc/apis/send_server.proto +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package apis; - -message SendParams { - SReceiver Receiver = 1; - string Topic = 2; - string Title = 3; - string Message = 4; - string Priority = 5; - string RemoteTemplate = 6; -} - -message ValidateConfigInput { - map configs = 1; -} - -message AddConfigInput { - map configs = 1; - string domainId = 2; -} - -message UpdateConfigInput { - map configs = 1; - string domainId = 2; -} - -message DeleteConfigInput { - string domainId = 1; -} - -message CompleteConfigInput { - repeated AddConfigInput ConfigInput = 1; -} - -message UseridByMobileParams { - string mobile = 1; - string domainId = 2; -} - -message Empty { -} - -message UseridByMobileReply { - string userid = 1; -} - -message ValidateConfigReply { - bool isValid = 1; - string msg = 2; -} - -message SReceiver { - string Contact = 1; - string DomainId = 2; -} - -message BatchSendParams { - repeated SReceiver Receivers = 1; - string Title = 2; - string Message = 3; - string Priority = 4; - string RemoteTemplate = 5; -} - -message FailedRecord { - SReceiver Receiver = 1; - string Reason = 2; -} - -message BatchSendReply { - repeated FailedRecord FailedRecords = 1; -} - -message ReadyInput { - repeated string DomainIds = 1; -} - -message ReadyOutput { - bool Ok = 1; -} - -service SendAgent { - rpc Ready(ReadyInput) returns (ReadyOutput); - rpc Send (SendParams) returns (Empty); - rpc AddConfig (AddConfigInput) returns (Empty); - rpc CompleteConfig (CompleteConfigInput) returns (Empty); - rpc UpdateConfig (UpdateConfigInput) returns (Empty); - rpc DeleteConfig (DeleteConfigInput) returns (Empty); - rpc ValidateConfig (ValidateConfigInput) returns (ValidateConfigReply); - rpc UseridByMobile (UseridByMobileParams) returns (UseridByMobileReply); - rpc BatchSend (BatchSendParams) returns (BatchSendReply); -} - diff --git a/pkg/notify/rpc/send.go b/pkg/notify/rpc/send.go deleted file mode 100644 index 3fbb87a2f9..0000000000 --- a/pkg/notify/rpc/send.go +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package rpc - -import ( - "context" - "fmt" - "io/ioutil" - "net" - "os" - "path/filepath" - "strings" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "yunion.io/x/log" - "yunion.io/x/pkg/errors" - - api "yunion.io/x/onecloud/pkg/apis/notify" - "yunion.io/x/onecloud/pkg/mcclient" - notifyv2 "yunion.io/x/onecloud/pkg/notify" - "yunion.io/x/onecloud/pkg/notify/rpc/apis" - "yunion.io/x/onecloud/pkg/util/fileutils2" -) - -const ( - // ErrSendServiceNotFound means SRpcService's SendSerivces hasn't this Send Service. - ErrSendServiceNotFound = errors.Error("No such send service") - // ErrSendServiceNotInit = errors.Error("Send service hasn't been init") -) - -// SRpcService provide rpc service about sending message for notify module and manage these services. -// SendServices storage all send service. -type SRpcService struct { - SendServices *ServiceMap - socketFileDir string - configStore notifyv2.IServiceConfigStore - templateStore notifyv2.ITemplateStore -} - -// NewSRpcService create a SRpcService -func NewSRpcService(socketFileDir string, configStore notifyv2.IServiceConfigStore, - tempalteStore notifyv2.ITemplateStore) *SRpcService { - return &SRpcService{ - SendServices: NewServiceMap(), - socketFileDir: socketFileDir, - configStore: configStore, - templateStore: tempalteStore, - } -} - -// InitAll init all Send Services, the init process is that: -// find all socket file in directory 'self.socketFileDir', if wrong return error; -// the name of file is the service's name; then try to dial to this rpc service -// through corresponding socket file, if failed, only print log but not return error. -func (self *SRpcService) InitAll() error { - files, err := ioutil.ReadDir(self.socketFileDir) - if err != nil { - return errors.Wrapf(err, "read dir %s failed", self.socketFileDir) - } - ctx := context.Background() - for _, file := range files { - filename := file.Name() - if !file.IsDir() && strings.Contains(filename, ".sock") { - serviceName := filename[:len(filename)-5] - self.startNewService(ctx, serviceName, true) - } - } - if self.SendServices.Len() == 0 { - log.Infof("No available send service.") - } else { - log.Infof("Total %d send service init successful", self.SendServices.Len()) - } - return nil -} - -// UpdateServices will detect the self.sockFileDir, add new service and -// delete disappeared one from self.SendServices. -func (self *SRpcService) UpdateServices(ctx context.Context, usreCred mcclient.TokenCredential, isStart bool) { - err := self.updateService(ctx) - if err != nil { - log.Errorf("update services failed because that %s.", err.Error()) - } -} - -// StopAll stop all send service in self.SenderServices normally which can delete the socket file. -func (self *SRpcService) StopAll() { - f := func(client *apis.SendNotificationClient) { - client.Conn.Close() - } - self.SendServices.Map(f) -} - -// Send call the corresponding rpc server to send messager. -func (self *SRpcService) Send(ctx context.Context, contactType string, args apis.SendParams) error { - // Stop sending that must fail early - if len(args.RemoteTemplate) == 0 && contactType == api.MOBILE { - return fmt.Errorf("empty remote template for mobile type notification") - } - var err error - f := func(service *apis.SendNotificationClient) (interface{}, error) { - log.Debugf("send one") - return service.Send(ctx, &args) - } - - _, err = self.execute(ctx, f, contactType) - if err != nil { - s, ok := status.FromError(err) - if !ok { - return err - } - return errors.Error(s.Message()) - } - return nil -} - -func (self *SRpcService) BatchSend(ctx context.Context, contactType string, args apis.BatchSendParams) ([]*apis.FailedRecord, error) { - // Stop sending that must fail early - if len(args.RemoteTemplate) == 0 && contactType == api.MOBILE { - return nil, fmt.Errorf("empty remote template for mobile type notification") - } - ret := make([]*apis.FailedRecord, 0) - f := func(service *apis.SendNotificationClient) (interface{}, error) { - return service.BatchSend(ctx, &args) - } - - i, err := self.execute(ctx, f, contactType) - if err != nil { - s, ok := status.FromError(err) - if !ok { - return nil, err - } - return nil, errors.Error(s.Message()) - } - reply := i.(*apis.BatchSendReply) - return append(ret, reply.FailedRecords...), nil -} - -// UpdateConfig can update config for rpc service with domainId -func (self *SRpcService) UpdateConfig(ctx context.Context, service string, config notifyv2.SConfig) error { - var ( - sendService *apis.SendNotificationClient - err error - ) - - sendService, ok := self.SendServices.Get(service) - if !ok { - return fmt.Errorf("no such service %s", service) - } - - args := apis.UpdateConfigInput{ - Configs: config.Config, - DomainId: config.DomainId, - } - _, err = sendService.UpdateConfig(ctx, &args) - if err != nil { - st := status.Convert(err) - if st.Code() != codes.NotFound { - return errors.Error(st.Message()) - } - _, err = sendService.AddConfig(ctx, &apis.AddConfigInput{ - Configs: config.Config, - DomainId: config.DomainId, - }) - if err != nil { - return errors.Wrap(err, "try to add config but failed") - } - } - return nil -} - -func (self *SRpcService) ContactByMobile(ctx context.Context, mobile, serviceName string, domainId string) (string, error) { - - iMobile := api.ParseInternationalMobile(mobile) - // compatible - if iMobile.AreaCode == "86" { - mobile = iMobile.Mobile - } - args := apis.UseridByMobileParams{ - Mobile: mobile, - DomainId: domainId, - } - - f := func(service *apis.SendNotificationClient) (interface{}, error) { - return service.UseridByMobile(ctx, &args) - } - - ret, err := self.execute(ctx, f, serviceName) - if err == nil { - reply := ret.(*apis.UseridByMobileReply) - return reply.Userid, nil - } - s, ok := status.FromError(err) - if !ok { - return "", err - } - if s.Code() == codes.NotFound { - return "", errors.Wrap(notifyv2.ErrNoSuchMobile, s.Message()) - } - if s.Code() == codes.FailedPrecondition { - return "", errors.Wrap(notifyv2.ErrIncompleteConfig, s.Message()) - } - return "", err -} - -// Wrap function to execute function call rpc server -func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNotificationClient) (interface{}, error), - serviceName string) (interface{}, error) { - - sendService, ok := self.SendServices.Get(serviceName) - - log.Debugf("get service %s", serviceName) - var err error - if !ok { - log.Debugf("get service first time failed") - sendService, err = self.startNewService(ctx, serviceName, true) - - if err != nil { - return nil, errors.Wrap(err, "start new service failed") - } - } - - ret, err := f(sendService) - - if err != nil { - // hander error - st := status.Convert(err) - if st.Code() == codes.Unavailable { - // sock is bad - self.closeService(ctx, serviceName) - return nil, ErrSendServiceNotFound - } - return nil, err - } - return ret, nil -} - -var ErrGetConfig = errors.Error("Get Config Failed") - -func (self *SRpcService) completeConfig(ctx context.Context, serviceName string, sendService *apis.SendNotificationClient) error { - // get config - configs, err := self.configStore.GetConfigs(serviceName) - if err != nil { - log.Errorf("getConfig of serveice %s from database error", serviceName) - return ErrGetConfig - } - - // update config for service - configInput := make([]*apis.AddConfigInput, len(configs)) - for i := range configInput { - configInput[i] = &apis.AddConfigInput{ - Configs: configs[i].Config, - DomainId: configs[i].DomainId, - } - } - _, err = sendService.CompleteConfig(ctx, &apis.CompleteConfigInput{ - ConfigInput: configInput, - }) - if err != nil { - st := status.Convert(err) - if st.Code() == codes.FailedPrecondition { - // no such rpc serve - err = fmt.Errorf(st.Message()) - } - if st.Code() == codes.Unavailable { - err = fmt.Errorf("service is unavailable for now: %s", st.Message()) - } - return errors.Wrap(err, "UpdateConfig") - } - return nil -} - -func (self *SRpcService) restartService(ctx context.Context, service string) (*apis.SendNotificationClient, error) { - sendService, ok := self.SendServices.Get(service) - if !ok { - return nil, fmt.Errorf("no such service, please start new service") - } - return sendService, self.completeConfig(ctx, service, sendService) -} - -// startNewService try to start a new rpc service named serviceName -// passConfig means if pass config to send service -func (self *SRpcService) startNewService(ctx context.Context, serviceName string, passConfig bool) (*apis.SendNotificationClient, error) { - - var ( - sendService *apis.SendNotificationClient - err error - ) - - filename := filepath.Join(self.socketFileDir, serviceName+".sock") - if !fileutils2.Exists(filename) { - return nil, errors.Error(fmt.Sprintf("no such socket file '%s'", filename)) - } - - grpcConn, err := grpcDialWithUnixSocket(ctx, filename) - if err != nil { - return nil, err - } - sendService = apis.NewSendNotificationClient(grpcConn) - - self.SendServices.Set(sendService, serviceName) - - if !passConfig { - return sendService, nil - } - - return sendService, self.completeConfig(ctx, serviceName, sendService) -} - -// closeService will remove service record from self.SendServices and try to remove sock file -func (self *SRpcService) closeService(ctx context.Context, serviceName string) { - filename := filepath.Join(self.socketFileDir, serviceName+".sock") - self.SendServices.Remove(serviceName) - os.Remove(filename) -} - -func (self *SRpcService) updateService(ctx context.Context) error { - files, err := ioutil.ReadDir(self.socketFileDir) - if err != nil { - return errors.Wrapf(err, "read dir %s failed", self.socketFileDir) - } - - serviceNames := self.SendServices.ServiceNames() - serviceNameSet := make(map[string]struct{}) - for _, name := range serviceNames { - serviceNameSet[name] = struct{}{} - } - - for _, file := range files { - filename := file.Name() - if !file.IsDir() && strings.Contains(filename, ".sock") { - serviceName := filename[:len(filename)-5] - if self.SendServices.IsExist(serviceName) { - delete(serviceNameSet, serviceName) - continue - } - self.startNewService(ctx, serviceName, true) - } - } - - serviceNames = serviceNames[:0] - for serviceName := range serviceNameSet { - serviceNames = append(serviceNames, serviceName) - } - - self.SendServices.BatchRemove(serviceNames) - return nil -} - -func (self *SRpcService) AddConfig(ctx context.Context, service string, config notifyv2.SConfig) error { - var ( - sendService *apis.SendNotificationClient - err error - ) - - sendService, ok := self.SendServices.Get(service) - if !ok { - return fmt.Errorf("no such service %s", service) - } - args := apis.AddConfigInput{ - DomainId: config.DomainId, - Configs: config.Config, - } - _, err = sendService.AddConfig(ctx, &args) - if err != nil { - return err - } - return nil -} - -func (self *SRpcService) DeleteConfig(ctx context.Context, service, domainId string) error { - var ( - sendService *apis.SendNotificationClient - err error - ) - - sendService, ok := self.SendServices.Get(service) - if !ok { - return fmt.Errorf("no such service %s", service) - } - args := apis.DeleteConfigInput{ - DomainId: domainId, - } - _, err = sendService.DeleteConfig(ctx, &args) - if err != nil { - return err - } - return nil -} - -func (self *SRpcService) ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error) { - - sendService, ok := self.SendServices.Get(cType) - - log.Debugf("get service %s", cType) - if !ok { - log.Debugf("get service first time failed") - sendService, err = self.startNewService(ctx, cType, false) - - if err != nil { - err = errors.Wrap(err, "start new service failed") - return - } - } - param := apis.ValidateConfigInput{ - Configs: configs, - } - rep, err := sendService.ValidateConfig(ctx, ¶m) - if err != nil { - st := status.Convert(err) - if st.Code() == codes.Unimplemented { - err = errors.ErrNotImplemented - return - } - err = fmt.Errorf(st.Message()) - return - } - return rep.IsValid, rep.Msg, nil -} - -func robotType2ContactType(rType string) string { - switch rType { - case api.ROBOT_TYPE_FEISHU: - return api.FEISHU_ROBOT - case api.ROBOT_TYPE_DINGTALK: - return api.DINGTALK_ROBOT - case api.ROBOT_TYPE_WORKWX: - return api.WORKWX_ROBOT - case api.ROBOT_TYPE_WEBHOOK: - return api.WEBHOOK - } - return rType -} - -func (self *SRpcService) SendRobotMessage(ctx context.Context, rType string, receivers []*apis.SReceiver, title string, message string) ([]*apis.FailedRecord, error) { - log.Infof("rType: %s", rType) - contactType := robotType2ContactType(rType) - args := apis.BatchSendParams{ - Receivers: receivers, - Title: title, - Message: message, - } - f := func(service *apis.SendNotificationClient) (interface{}, error) { - return service.BatchSend(ctx, &args) - } - - ret, err := self.execute(ctx, f, contactType) - if err != nil { - s, ok := status.FromError(err) - if !ok { - return nil, err - } - return nil, errors.Error(s.Message()) - } - reply := ret.(*apis.BatchSendReply) - return reply.FailedRecords, nil -} - -func grpcDialWithUnixSocket(ctx context.Context, socketPath string) (*grpc.ClientConn, error) { - return grpc.DialContext(ctx, socketPath, grpc.WithInsecure(), grpc.WithTimeout(time.Second*5), grpc.WithDialer( - func(addr string, timeout time.Duration) (net.Conn, error) { - return net.DialTimeout("unix", addr, timeout) - }), - ) -} diff --git a/pkg/notify/rpc/service_map.go b/pkg/notify/rpc/service_map.go deleted file mode 100644 index 951d076e53..0000000000 --- a/pkg/notify/rpc/service_map.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package rpc - -import ( - "sync" - - "yunion.io/x/onecloud/pkg/notify/rpc/apis" -) - -// ServiceMap has a map of string and apis.SendNotification's pointer, and a RWMutex lock protect map. -type ServiceMap struct { - serviceMap map[string]*apis.SendNotificationClient - lock sync.RWMutex -} - -func NewServiceMap() *ServiceMap { - return &ServiceMap{serviceMap: make(map[string]*apis.SendNotificationClient)} -} - -func (sm *ServiceMap) Get(serviceName string) (*apis.SendNotificationClient, bool) { - sm.lock.RLock() - defer sm.lock.RUnlock() - client, ok := sm.serviceMap[serviceName] - return client, ok -} - -func (sm *ServiceMap) Set(service *apis.SendNotificationClient, serviceName string) { - sm.lock.Lock() - defer sm.lock.Unlock() - sm.serviceMap[serviceName] = service -} - -func (sm *ServiceMap) Remove(serviceName string) { - sm.lock.Lock() - defer sm.lock.Unlock() - service, ok := sm.serviceMap[serviceName] - if !ok { - return - } - service.Conn.Close() - delete(sm.serviceMap, serviceName) -} - -func (sm *ServiceMap) BatchRemove(serviceNames []string) { - sm.lock.Lock() - defer sm.lock.Unlock() - for _, serviceName := range serviceNames { - service, ok := sm.serviceMap[serviceName] - if !ok { - continue - } - service.Conn.Close() - delete(sm.serviceMap, serviceName) - } -} - -func (sm *ServiceMap) ServiceNames() []string { - sm.lock.RLock() - defer sm.lock.RUnlock() - serviceNames := make([]string, 0, len(sm.serviceMap)) - for serviceName := range sm.serviceMap { - serviceNames = append(serviceNames, serviceName) - } - return serviceNames -} - -func (sm *ServiceMap) IsExist(serviceName string) bool { - sm.lock.RLock() - defer sm.lock.RUnlock() - _, ok := sm.serviceMap[serviceName] - return ok -} - -func (sm *ServiceMap) Len() int { - return len(sm.serviceMap) -} - -func (sm *ServiceMap) Map(f func(*apis.SendNotificationClient)) { - sm.lock.Lock() - defer sm.lock.Unlock() - for _, service := range sm.serviceMap { - f(service) - } -} diff --git a/pkg/notify/sender/const.go b/pkg/notify/sender/const.go new file mode 100644 index 0000000000..08a2038c95 --- /dev/null +++ b/pkg/notify/sender/const.go @@ -0,0 +1,53 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "yunion.io/x/pkg/errors" +) + +var ( + ErrIPWhiteList = errors.Error("Need to add ip to whtelist") + ErrNoSupportSecSetting = errors.Error("Only the IP address option in the security Settings is supported") + ErrNoSuchMobile = errors.Error("No such mobile") + ErrIncompleteConfig = errors.Error("Incomplete config") + ErrDuplicateConfig = errors.Error("Duplicate config for a domain") +) + +const ( + ApiWebhookRobotV2SendMessage = "https://open.feishu.cn/open-apis/bot/v2/hook/" + // 钉钉发送消息 + ApiDingtalkSendMessage = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?" + // 钉钉获取token + ApiDingtalkGetToken = "https://oapi.dingtalk.com/gettoken?" + // 钉钉使用手机号获取用户ID + ApiDingtalkGetUserByMobile = "https://oapi.dingtalk.com/topapi/v2/user/getbymobile?" + // 钉钉获取消息发送结果 + ApiDingtalkGetSendResult = "https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult?" + // 企业微信获取token + ApiWorkwxGetToken = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" + // 企业微信使用手机号获取用户ID + ApiWorkwxGetUserByMobile = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?" + // 企业微信发送消息 + ApiWorkwxSendMessage = "https://qyapi.weixin.qq.com/cgi-bin/message/send?" + // 飞书使用手机号或邮箱获取用户ID + ApiFetchUserID = "https://open.feishu.cn/open-apis/user/v1/batch_get_id?" +) + +var ( + FAIL_KEY = []string{"失败", "fail ", "failed"} +) + +const EVENT_HEADER = "X-Yunion-Event" diff --git a/pkg/notify/sender/dingtalk.go b/pkg/notify/sender/dingtalk.go new file mode 100644 index 0000000000..9b1f0f3dad --- /dev/null +++ b/pkg/notify/sender/dingtalk.go @@ -0,0 +1,162 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "fmt" + "net/url" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SDingTalkSender struct { + config map[string]api.SNotifyConfigContent +} + +func (dingSender *SDingTalkSender) GetSenderType() string { + return api.DINGTALK +} + +func (dingSender *SDingTalkSender) Send(args api.SendParams) error { + body := map[string]interface{}{ + "agent_id": models.ConfigMap[api.DINGTALK].Content.AgentId, + "msg": map[string]interface{}{ + "msgtype": "markdown", + "markdown": map[string]interface{}{ + "title": args.Title, + "text": fmt.Sprintf("%s (%s)", args.Message, time.Now().Format("2006-01-02 15:04")), + }, + }, + "userid_list": args.Receivers.Contact, + } + params := url.Values{} + params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken) + req, err := sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body)) + if err != nil { + subCode, _ := req.GetString("sub_code") + switch subCode { + // token失效或不合法 + case "40014": + // 尝试重新获取token + err = dingSender.GetAccessToken() + if err != nil { + return errors.Wrap(err, "reset token") + } + // 重新发送通知 + params = url.Values{} + params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken) + req, err = sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body)) + if err != nil { + return errors.Wrap(err, "dingtalk resend message") + } + } + if err != nil { + return errors.Wrap(err, "dingtalk send message") + } + } + + // 获取消息通知发送结果 + task_id, _ := req.GetString("task_id") + body = map[string]interface{}{ + "agent_id": models.ConfigMap[api.DINGTALK].Content.AgentId, + "task_id": task_id, + } + _, err = sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body)) + return err +} + +func (dingSender *SDingTalkSender) ValidateConfig(config api.NotifyConfig) (string, error) { + // 校验accesstoken + _, err := dingSender.getAccessToken(config.AppKey, config.AppSecret) + if err != nil { + if strings.Contains(err.Error(), "40089") { + return "invalid AppKey or AppSecret", nil + } + return "", err + } + models.ConfigMap[api.DINGTALK].Content.AppKey, models.ConfigMap[api.DINGTALK].Content.AppSecret = config.AppKey, config.AppSecret + return "", nil +} + +func (dingSender *SDingTalkSender) ContactByMobile(mobile, domainId string) (string, error) { + err := dingSender.GetAccessToken() + if err != nil { + return "", err + } + body := jsonutils.Marshal(map[string]interface{}{ + "mobile": mobile, + }) + params := url.Values{} + params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken) + res, err := sendRequest(ApiDingtalkGetUserByMobile, httputils.POST, nil, params, body) + if err != nil { + return "", errors.Wrap(err, "get user by mobile") + } + return res.GetString("result", "userid") +} + +func (dingSender *SDingTalkSender) IsPersonal() bool { + return true +} + +func (dingSender *SDingTalkSender) IsRobot() bool { + return false +} + +func (dingSender *SDingTalkSender) IsValid() bool { + return len(dingSender.config) > 0 +} + +func (dingSender *SDingTalkSender) IsPullType() bool { + return true +} + +func (dingSender *SDingTalkSender) IsSystemConfigContactType() bool { + return true +} + +func (dingSender *SDingTalkSender) GetAccessToken() error { + appKey, appSecret := models.ConfigMap[api.DINGTALK].Content.AppKey, models.ConfigMap[api.DINGTALK].Content.AppSecret + token, err := dingSender.getAccessToken(appKey, appSecret) + if err != nil { + return errors.Wrap(err, "dingtalk getAccessToken") + } + models.ConfigMap[api.DINGTALK].Content.AccessToken = token + return nil +} + +func (dingSender *SDingTalkSender) getAccessToken(appKey, appSecret string) (string, error) { + params := url.Values{} + params.Set("appkey", appKey) + params.Set("appsecret", appSecret) + res, err := sendRequest(ApiDingtalkGetToken, httputils.GET, nil, params, nil) + if err != nil { + return "", errors.Wrap(err, "get dingtalk token") + } + return res.GetString("access_token") +} + +func init() { + models.Register(&SDingTalkSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/dingtalk_robot.go b/pkg/notify/sender/dingtalk_robot.go new file mode 100644 index 0000000000..d381fc795d --- /dev/null +++ b/pkg/notify/sender/dingtalk_robot.go @@ -0,0 +1,110 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "fmt" + "strings" + + "github.com/hugozhu/godingtalk" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +const ( + WEBHOOK_PREFIX = "https://oapi.dingtalk.com/robot/send?access_token=" +) + +type SDingTalkRobotSender struct { + config map[string]api.SNotifyConfigContent +} + +func (dingRobotSender *SDingTalkRobotSender) GetSenderType() string { + return api.DINGTALK_ROBOT +} + +func (dingRobotSender *SDingTalkRobotSender) Send(args api.SendParams) error { + var token string + var atStr strings.Builder + title, msg := args.Title, args.Message + + webhook := args.Receivers.Contact + if strings.HasPrefix(webhook, WEBHOOK_PREFIX) { + token = webhook[len(WEBHOOK_PREFIX):] + } else { + return errors.Wrap(InvalidWebhook, webhook) + } + processText := fmt.Sprintf("### %s\n%s%s", title, msg, atStr.String()) + // atList := &godingtalk.RobotAtList{} + client := godingtalk.NewDingTalkClient("", "") + rep, err := client.SendRobotMarkdownMessage(token, title, processText) + if err == nil { + return nil + } + if rep.ErrCode == 310000 { + if strings.Contains(rep.ErrMsg, "whitelist") { + return errors.Wrap(ErrIPWhiteList, rep.ErrMsg) + } else { + return errors.Wrap(err, jsonutils.Marshal(rep).PrettyString()) + } + } + if rep.ErrCode == 300001 && strings.Contains(rep.ErrMsg, "token") { + return ErrNoSuchWebhook + } + return errors.Wrap(err, "this is res err") +} + +func (dingRobotSender *SDingTalkRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (dingRobotSender *SDingTalkRobotSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (dingRobotSender *SDingTalkRobotSender) IsPersonal() bool { + return true +} + +func (dingRobotSender *SDingTalkRobotSender) IsRobot() bool { + return true +} + +func (dingRobotSender *SDingTalkRobotSender) IsValid() bool { + return len(dingRobotSender.config) > 0 +} + +func (dingRobotSender *SDingTalkRobotSender) IsPullType() bool { + return true +} + +func (dingRobotSender *SDingTalkRobotSender) IsSystemConfigContactType() bool { + return true +} + +func (dingRobotSender *SDingTalkRobotSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SDingTalkRobotSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/email.go b/pkg/notify/sender/email.go index 1662503012..4c87de26bf 100644 --- a/pkg/notify/sender/email.go +++ b/pkg/notify/sender/email.go @@ -18,15 +18,18 @@ import ( "crypto/tls" "encoding/base64" "io" + "net/http" + "net/url" "time" - "gopkg.in/mail.v2" + gomail "gopkg.in/mail.v2" "yunion.io/x/jsonutils" - "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" ) type errorMap map[string]error @@ -39,8 +42,189 @@ func (em errorMap) Error() string { return jsonutils.Marshal(msg).String() } +type SEmailSender struct { + config map[string]api.SNotifyConfigContent +} + +func (emailSender *SEmailSender) GetSenderType() string { + return api.EMAIL +} + +func (emailSender *SEmailSender) Send(args api.SendParams) error { + // 初始化emaliClient + hostNmae, hostPort, userName, password := models.ConfigMap[api.EMAIL].Content.Hostname, models.ConfigMap[api.EMAIL].Content.Hostport, models.ConfigMap[api.EMAIL].Content.Username, models.ConfigMap[api.EMAIL].Content.Password + dialer := gomail.NewDialer(hostNmae, hostPort, userName, password) + + // 是否支持ssl + if models.ConfigMap[api.EMAIL].Content.SslGlobal { + dialer.SSL = true + } else { + dialer.SSL = false + dialer.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + + if len(args.EmailMsg.To) > 0 { + // emailMsg不为空时 + sender, err := dialer.Dial() + if err != nil { + return errors.Wrap(err, "dialer.Dial") + } + retErr := errorMap{} + for _, to := range args.EmailMsg.To { + gmsg := gomail.NewMessage() + gmsg.SetHeader("From", models.ConfigMap[api.EMAIL].Content.SenderAddress) + gmsg.SetHeader("To", to) + gmsg.SetHeader("Subject", args.EmailMsg.Subject) + gmsg.SetBody("text/html", args.EmailMsg.Body) + + for i := range args.EmailMsg.Attachments { + attach := args.EmailMsg.Attachments[i] + gmsg.Attach(attach.Filename, + gomail.SetCopyFunc(func(w io.Writer) error { + mime := attach.Mime + if len(mime) == 0 { + mime = "application/octet-stream" + } + _, err := w.Write([]byte("Content-Type: " + attach.Mime)) + return errors.Wrap(err, "WriteMime") + }), + gomail.SetCopyFunc(func(w io.Writer) error { + contBytes, err := base64.StdEncoding.DecodeString(attach.Base64Content) + if err != nil { + return errors.Wrap(err, "base64.StdEncoding.DecodeString") + } + _, err = w.Write(contBytes) + return errors.Wrap(err, "WriteContent") + }), + ) + + errs := make([]error, 0) + for tryTime := 3; tryTime > 0; tryTime-- { + err = gomail.Send(sender, gmsg) + if err != nil { + errs = append(errs, err) + time.Sleep(time.Second * 10) + continue + } + errs = errs[0:0] + break + } + + if len(errs) > 0 { + retErr[to] = errors.NewAggregate(errs) + } + } + } + } + + // 构造email发送请求 + gmsg := gomail.NewMessage() + gmsg.SetHeader("From", models.ConfigMap[api.EMAIL].Content.SenderAddress) + gmsg.SetHeader("To", args.Receivers.Contact) + gmsg.SetHeader("Subject", args.Title) + gmsg.SetBody("text/html", args.Message) + dialer.StartTLSPolicy = gomail.MandatoryStartTLS + if err := dialer.DialAndSend(gmsg); err != nil { + return errors.Wrap(err, "send email") + } + return nil +} + +func (emailSender *SEmailSender) ValidateConfig(config api.NotifyConfig) (string, error) { + errChan := make(chan error, 1) + go func() { + dialer := gomail.NewDialer(config.Hostname, config.Hostport, config.Username, config.Password) + if config.SslGlobal { + dialer.SSL = true + } else { + dialer.SSL = false + // StartLSConfig + dialer.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + sender, err := dialer.Dial() + if err != nil { + errChan <- err + return + } + sender.Close() + errChan <- nil + }() + + ticker := time.Tick(10 * time.Second) + select { + case <-ticker: + return "", errors.Error("timeout") + case err := <-errChan: + return "", err + } +} + +func (emailSender *SEmailSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", nil +} + +func (emailSender *SEmailSender) IsPersonal() bool { + return true +} + +func (emailSender *SEmailSender) IsRobot() bool { + return false +} + +func (emailSender *SEmailSender) IsValid() bool { + return len(emailSender.config) > 0 +} + +func (emailSender *SEmailSender) IsPullType() bool { + return false +} + +func (emailSender *SEmailSender) IsSystemConfigContactType() bool { + return true +} + +func (emailSender *SEmailSender) GetAccessToken() error { + corpId, secret := models.ConfigMap[api.WORKWX].Content.CorpId, models.ConfigMap[api.WORKWX].Content.Secret + token, err := emailSender.getAccessToken(corpId, secret) + if err != nil { + return errors.Wrap(err, "workwx getAccessToken") + } + models.ConfigMap[api.WORKWX].Content.AccessToken = token + return nil +} + +func (emailSender *SEmailSender) getAccessToken(corpId, secret string) (string, error) { + // url := ApiWorkwxGetToken + fmt.Sprintf("?corpid=%s&corpsecret=%s", corpId, secret) + params := url.Values{} + params.Set("corpid", corpId) + params.Set("corpsecret", secret) + res, err := sendRequest(ApiWorkwxGetToken, httputils.GET, nil, params, nil) + if err != nil { + return "", errors.Wrap(err, "get workwx token") + } + return res.GetString("access_token") +} + +func (emailSender *SEmailSender) sendMessageWithToken(uri string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if params == nil { + params = url.Values{} + } + params.Set("access_token", models.ConfigMap[api.WORKWX].Content.AccessToken) + return sendRequest(uri, httputils.POST, nil, params, jsonutils.Marshal(body)) +} + +func init() { + models.Register(&SEmailSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} + func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { - dialer := mail.NewDialer(conf.Hostname, conf.Hostport, conf.Username, conf.Password) + dialer := gomail.NewDialer(conf.Hostname, conf.Hostport, conf.Username, conf.Password) if conf.SslGlobal { dialer.SSL = true @@ -59,7 +243,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { retErr := errorMap{} for _, to := range msg.To { - gmsg := mail.NewMessage() + gmsg := gomail.NewMessage() gmsg.SetHeader("From", conf.SenderAddress) gmsg.SetHeader("To", to) gmsg.SetHeader("Subject", msg.Subject) @@ -68,7 +252,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { for i := range msg.Attachments { attach := msg.Attachments[i] gmsg.Attach(attach.Filename, - mail.SetCopyFunc(func(w io.Writer) error { + gomail.SetCopyFunc(func(w io.Writer) error { mime := attach.Mime if len(mime) == 0 { mime = "application/octet-stream" @@ -76,7 +260,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { _, err := w.Write([]byte("Content-Type: " + attach.Mime)) return errors.Wrap(err, "WriteMime") }), - mail.SetCopyFunc(func(w io.Writer) error { + gomail.SetCopyFunc(func(w io.Writer) error { contBytes, err := base64.StdEncoding.DecodeString(attach.Base64Content) if err != nil { return errors.Wrap(err, "base64.StdEncoding.DecodeString") @@ -89,8 +273,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { errs := make([]error, 0) for tryTime := 3; tryTime > 0; tryTime-- { - err = mail.Send(sender, gmsg) - log.Debugf("send email ...") + err = gomail.Send(sender, gmsg) if err != nil { errs = append(errs, err) time.Sleep(time.Second * 10) diff --git a/pkg/notify/sender/feishu.go b/pkg/notify/sender/feishu.go new file mode 100644 index 0000000000..28de7eeb59 --- /dev/null +++ b/pkg/notify/sender/feishu.go @@ -0,0 +1,163 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "fmt" + "net/http" + "net/url" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SFeishuSender struct { + config map[string]api.SNotifyConfigContent +} + +func (self *SFeishuSender) GetSenderType() string { + return api.FEISHU +} + +const ApiSendMessageForFeishuByOpenId = feishu.ApiRobotSendMessage + "?receive_id_type=open_id" + +// 发送飞书消息 +func (feishuSender *SFeishuSender) Send(args api.SendParams) error { + // 发送通知消息体 + body := map[string]interface{}{ + "open_id": args.Receivers.Contact, + "msg_type": "text", + "content": map[string]interface{}{ + "text": args.Message, + }, + } // 添加bearer token请求头 + header := http.Header{} + header.Add("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken)) + req, err := sendRequest(ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body)) + if err == nil { + return nil + } + // 发送通知失败的情况 + code, _ := req.GetString("code") + switch code { + case "99991663": //token过期 + err = feishuSender.GetAccessToken() + if err != nil { + return errors.Wrap(err, "tenant token invalid && getToken err") + } + header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken)) + _, err = sendRequest(ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body)) + if err == nil { + return nil + } + case "99991672": // 未开放发送消息通知的权限 + err = errors.Wrap(ErrIncompleteConfig, err.Error()) + } + + return err +} + +// 校验appId与appSecret +func (feishuSender *SFeishuSender) ValidateConfig(config api.NotifyConfig) (string, error) { + rep, err := feishuSender.getAccessToken(config.AppId, config.AppSecret) + if err == nil { + return "", err + } + var msg string + switch rep.Code { + case 10003: + msg = "invalid AppId" + case 10014: + msg = "invalid AppSecret" + } + return msg, nil +} + +// 根据用户手机号获取用户的open_id +func (feishuSender *SFeishuSender) ContactByMobile(mobile, domainId string) (string, error) { + body := jsonutils.NewDict() + body.Set("mobiles", jsonutils.NewArray(jsonutils.NewString(mobile))) + header := http.Header{} + // 考虑到获取用户id需求较少,可通过直接更新token来避免token失效 + err := feishuSender.GetAccessToken() + if err != nil { + return "", errors.Wrap(err, "GetAccessToken") + } + + params := url.Values{} + params.Set("mobiles", mobile) + + header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken)) + resp, err := sendRequest(ApiFetchUserID, httputils.GET, header, params, body) + if err != nil { + return "", err + } + mobileNotExist, _ := resp.GetArray("data", "mobiles_not_exist") + if len(mobileNotExist) != 0 { + return "", errors.Wrapf(errors.ErrNotFound, "no such user whose mobile is %s", mobile) + } + list, err := resp.GetArray("data", "mobile_users", mobile) + if err != nil { + return "", errors.Wrap(err, "jsonutils.JSONObject.GetArray") + } + // len(list) must be positive + return list[0].GetString("open_id") +} + +func (feishuSender *SFeishuSender) IsPersonal() bool { + return true +} + +func (feishuSender *SFeishuSender) IsRobot() bool { + return false +} + +func (feishuSender *SFeishuSender) IsValid() bool { + return len(feishuSender.config) > 0 +} + +func (feishuSender *SFeishuSender) IsPullType() bool { + return true +} + +func (feishuSender *SFeishuSender) IsSystemConfigContactType() bool { + return true +} + +// 获取token +func (feishuSender *SFeishuSender) GetAccessToken() error { + appId, appSecret := models.ConfigMap[api.FEISHU].Content.AppId, models.ConfigMap[api.FEISHU].Content.AppSecret + resp, err := feishuSender.getAccessToken(appId, appSecret) + models.ConfigMap[api.FEISHU].Content.AccessToken = resp.TenantAccessToken + return err +} + +func (feishuSender *SFeishuSender) getAccessToken(appId, appSecret string) (*feishu.TenantAccesstokenResp, error) { + resp, err := feishu.GetTenantAccessTokenInternal(appId, appSecret) + if err != nil { + return resp, errors.Wrap(err, "feishu.GetTenantAccessTokenInternal") + } + return resp, nil +} + +func init() { + models.Register(&SFeishuSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/feishu_robot.go b/pkg/notify/sender/feishu_robot.go new file mode 100644 index 0000000000..731767ee61 --- /dev/null +++ b/pkg/notify/sender/feishu_robot.go @@ -0,0 +1,112 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "fmt" + "strings" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu" + "yunion.io/x/onecloud/pkg/notify/models" +) + +var ErrNoSuchWebhook = errors.Error("No such webhook") +var InvalidWebhook = errors.Error("Invalid webhook") + +type SFeishuRobotSender struct { + config map[string]api.SNotifyConfigContent +} + +func (feishuRobotSender *SFeishuRobotSender) GetSenderType() string { + return api.FEISHU_ROBOT +} + +func (feishuRobotSender *SFeishuRobotSender) Send(args api.SendParams) error { + var token string + var errs []error + title, msg := args.Title, args.Message + webhook := args.Receivers.Contact + switch { + case strings.HasPrefix(webhook, ApiWebhookRobotV2SendMessage): + token = webhook[len(ApiWebhookRobotV2SendMessage):] + case strings.HasPrefix(webhook, feishu.ApiWebhookRobotSendMessage): + token = webhook[len(feishu.ApiWebhookRobotSendMessage):] + default: + return errors.Wrap(InvalidWebhook, webhook) + } + req := feishu.WebhookRobotMsgReq{ + Title: title, + Text: msg, + } + rep, err := feishu.SendWebhookRobotMessage(token, req) + if err != nil { + return errors.Wrap(err, "SendWebhookRobotMessage") + } + if !rep.Ok { + if strings.Contains(rep.Error, "token") { + return ErrNoSuchWebhook + } else { + return fmt.Errorf("SendWebhookRobotMessage failed: %s", rep.Error) + } + } + if err != nil { + if errs == nil { + errs = []error{} + } + errs = append(errs, err) + } + return errors.NewAggregate(errs) +} + +func (feishuRobotSender *SFeishuRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (feishuRobotSender *SFeishuRobotSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (feishuRobotSender *SFeishuRobotSender) IsPersonal() bool { + return true +} + +func (feishuRobotSender *SFeishuRobotSender) IsRobot() bool { + return true +} + +func (feishuRobotSender *SFeishuRobotSender) IsValid() bool { + return len(feishuRobotSender.config) > 0 +} + +func (feishuRobotSender *SFeishuRobotSender) IsPullType() bool { + return true +} + +func (feishuRobotSender *SFeishuRobotSender) IsSystemConfigContactType() bool { + return true +} + +func (feishuRobotSender *SFeishuRobotSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SFeishuRobotSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/mobile.go b/pkg/notify/sender/mobile.go new file mode 100644 index 0000000000..f3ff839b2f --- /dev/null +++ b/pkg/notify/sender/mobile.go @@ -0,0 +1,113 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SMobileSender struct { + config map[string]api.SNotifyConfigContent +} + +func (smsSender *SMobileSender) GetSenderType() string { + return api.MOBILE +} + +func (smsSender *SMobileSender) Send(args api.SendParams) error { + smsSendParams := api.SSMSSendParams{ + From: "", + To: args.Receivers.Contact, + } + smsdriver := models.GetSMSDriver(models.ConfigMap[api.MOBILE].Content.SmsDriver) + return smsdriver.Send(smsSendParams, false, &api.NotifyConfig{ + SNotifyConfigContent: *models.ConfigMap[api.MOBILE].Content, + Attribution: models.ConfigMap[api.MOBILE].Attribution, + DomainId: models.ConfigMap[api.MOBILE].DomainId, + }) +} + +func (smsSender *SMobileSender) ValidateConfig(config api.NotifyConfig) (string, error) { + driver := models.GetSMSDriver(config.SmsDriver) + return "", driver.Verify(&config) +} + +func (smsSender *SMobileSender) UpdateConfig(config api.NotifyConfig) error { + q := models.ConfigManager.Query() + q = q.Equals("type", api.MOBILE) + confs := []models.SConfig{} + db.FetchModelObjects(models.ConfigManager, q, &confs) + if len(confs) == 0 { + return errors.Wrapf(errors.ErrNotFound, "config type:%s", api.MOBILE) + } + _, err := db.Update(&confs[0], func() error { + confs[0].Content = &config.SNotifyConfigContent + return nil + }) + if err != nil { + return errors.Wrap(err, "update config") + } + models.ConfigMap[api.MOBILE] = models.SConfig{ + Content: &config.SNotifyConfigContent, + } + return nil +} + +func (smsSender *SMobileSender) AddConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (smsSender *SMobileSender) DeleteConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (smsSender *SMobileSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", nil +} + +func (smsSender *SMobileSender) IsPersonal() bool { + return true +} + +func (smsSender *SMobileSender) IsRobot() bool { + return false +} + +func (smsSender *SMobileSender) IsValid() bool { + return len(smsSender.config) > 0 +} + +func (smsSender *SMobileSender) IsPullType() bool { + return false +} + +func (smsSender *SMobileSender) IsSystemConfigContactType() bool { + return true +} + +func (smsSender *SMobileSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SMobileSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/smsdriver/aliyun.go b/pkg/notify/sender/smsdriver/aliyun.go new file mode 100644 index 0000000000..2bffe2a958 --- /dev/null +++ b/pkg/notify/sender/smsdriver/aliyun.go @@ -0,0 +1,130 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package smsdriver + +import ( + "encoding/json" + "regexp" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk" + sdkerrors "github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses" + + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SAliyunSMSDriver struct{} + +var parser = regexp.MustCompile(`\+(\d*) (.*)`) + +func (d *SAliyunSMSDriver) Name() string { + return DriverAliyun +} + +func (d *SAliyunSMSDriver) Verify(config *api.NotifyConfig) error { + err := d.Send(api.SSMSSendParams{}, true, config) + if err == ErrSignnameInvalid || err == ErrSignatureDoesNotMatch || err == ErrAccessKeyIdNotFound { + return nil + } + return errors.Wrap(err, "Verify") +} + +func (d *SAliyunSMSDriver) Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error { + if isVerify { + args.AppKey = config.AccessKeyId + args.AppSecret = config.AccessKeySecret + args.Signature = config.Signature + } else { + args.AppKey = models.ConfigMap[api.MOBILE].Content.AccessKeyId + args.AppSecret = models.ConfigMap[api.MOBILE].Content.AccessKeySecret + args.Signature = models.ConfigMap[api.MOBILE].Content.Signature + } + return d.sendSms(args) +} + +func (d *SAliyunSMSDriver) sendSms(args api.SSMSSendParams) error { + // lock and update + client, err := sdk.NewClientWithAccessKey("default", args.AppKey, args.AppSecret) + if err != nil { + return err + } + + m := parser.FindStringSubmatch(args.To) + if len(m) > 0 { + if m[1] == "86" { + args.To = m[2] + } else { + args.To = m[1] + m[2] + } + } + request := requests.NewCommonRequest() + request.Method = "POST" + request.Scheme = "https" // https | http + request.Domain = "dysmsapi.aliyuncs.com" + request.Version = "2017-05-25" + request.ApiName = "SendSms" + request.QueryParams["RegionId"] = "default" + request.QueryParams["PhoneNumbers"] = args.To + request.QueryParams["SignName"] = args.Signature + + request.QueryParams["TemplateCode"] = args.TemplateId + request.QueryParams["TemplateParam"] = args.TemplateParas + + return d.checkResponseAndError(client.ProcessCommonRequest(request)) +} + +func (d *SAliyunSMSDriver) checkResponseAndError(rep *responses.CommonResponse, err error) error { + if err != nil { + serr, ok := err.(*sdkerrors.ServerError) + if !ok { + return err + } + if serr.ErrorCode() == ACCESSKEYID_NOTFOUND { + return ErrAccessKeyIdNotFound + } + if serr.ErrorCode() == SIGN_DOESNOTMATCH { + return ErrSignatureDoesNotMatch + } + return err + } + + type RepContent struct { + Message string + Code string + } + respContent := rep.GetHttpContentBytes() + rc := RepContent{} + err = json.Unmarshal(respContent, &rc) + if err != nil { + return errors.Wrap(err, "json.Unmarshal") + } + if rc.Code == "OK" { + return nil + } + if rc.Code == SIGHNTURE_ILLEGAL { + return ErrSignnameInvalid + } else if rc.Code == TEMPLATE_ILLGAL { + return ErrSignnameInvalid + } + return errors.Error(rc.Message) +} + +func init() { + models.SMSRegister(&SAliyunSMSDriver{}) +} diff --git a/pkg/notify/sender/smsdriver/const.go b/pkg/notify/sender/smsdriver/const.go new file mode 100644 index 0000000000..cb51c83806 --- /dev/null +++ b/pkg/notify/sender/smsdriver/const.go @@ -0,0 +1,40 @@ +package smsdriver + +import "yunion.io/x/pkg/errors" + +const ( + DriverKey = "sms_driver" + + DriverAliyun = "smsaliyun" + DriverHuawei = "smshuawei" +) + +const ( + ACCESS_KEY_ID = "access_key_id" + ACCESS_KEY_SECRET = "access_key_secret" + SIGNATURE = "signature" + SERVICE_URL = "service_url" +) + +var ( + ErrAccessKeyIdNotFound = errors.Error("AccessKeyId not found") + ErrSignatureDoesNotMatch = errors.Error("AccessKeySecret does not match with the accessKeyId") + ErrSignnameInvalid = errors.Error("Invalid signature (does not exist or is blackened)") + ErrDriverNotFound = errors.Error("Driver not found") +) + +const ( + ACESS_KEY_ID_BP = "accessKeyId" + ACESS_KEY_SECRET_BP = "accessKeySecret" + + NEED_REMOTE_TEMPLATE = "remote template is needed in aliyun sms" + + ACCESSKEYID_NOTFOUND = "InvalidAccessKeyId.NotFound" + SIGN_DOESNOTMATCH = "SignatureDoesNotMatch" + SIGHNTURE_ILLEGAL = "isv.SMS_SIGNATURE_ILLEGAL" + TEMPLATE_ILLGAL = "isv.SMS_TEMPLATE_ILLEGAL" +) + +const ( + HuaweiSendUri = "/sms/batchSendSms/v1" +) diff --git a/pkg/notify/rpc/apis/doc.go b/pkg/notify/sender/smsdriver/doc.go similarity index 89% rename from pkg/notify/rpc/apis/doc.go rename to pkg/notify/sender/smsdriver/doc.go index a1089f153f..e5e3704cdc 100644 --- a/pkg/notify/rpc/apis/doc.go +++ b/pkg/notify/sender/smsdriver/doc.go @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -package apis // import "yunion.io/x/onecloud/pkg/notify/rpc/apis" +package smsdriver // import "yunion.io/x/onecloud/pkg/notify/sender" diff --git a/pkg/notify/sender/smsdriver/huawei.go b/pkg/notify/sender/smsdriver/huawei.go new file mode 100644 index 0000000000..dd67cbe973 --- /dev/null +++ b/pkg/notify/sender/smsdriver/huawei.go @@ -0,0 +1,102 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package smsdriver + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + uuid "github.com/satori/go.uuid" + + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +//无需修改,用于格式化鉴权头域,给"X-WSSE"参数赋值 +const WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\"" + +//无需修改,用于格式化鉴权头域,给"Authorization"参数赋值 +const AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"" + +type SHuaweiSMSDriver struct{} + +func (d *SHuaweiSMSDriver) Name() string { + return DriverHuawei +} + +func (d *SHuaweiSMSDriver) Verify(config *api.NotifyConfig) error { + err := d.Send(api.SSMSSendParams{}, true, config) + if err == ErrSignnameInvalid || err == ErrSignatureDoesNotMatch || err == ErrAccessKeyIdNotFound { + return nil + } + return errors.Wrap(err, "Verify") +} + +func (d *SHuaweiSMSDriver) Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error { + if isVerify { + args.AppKey = config.AccessKeyId + args.AppSecret = config.AccessKeySecret + args.Signature = config.Signature + } else { + args.AppKey = models.ConfigMap[api.MOBILE].Content.AccessKeyId + args.AppSecret = models.ConfigMap[api.MOBILE].Content.AccessKeySecret + args.Signature = models.ConfigMap[api.MOBILE].Content.Signature + } + + return d.sendSms(args) +} + +func (d *SHuaweiSMSDriver) sendSms(args api.SSMSSendParams) error { + uri := models.ConfigMap[api.MOBILE].Content.ServiceUrl + HuaweiSendUri + "?" + header := http.Header{} + header.Set("Content-Type", "application/x-www-form-urlencoded") + header.Set("Authorization", AUTH_HEADER_VALUE) + header.Set("X-WSSE", buildWsseHeader(args.AppKey, args.AppSecret)) + params := url.Values{} + params.Set("from", args.From) + params.Set("to", args.To) + params.Set("templateId", args.TemplateId) + params.Set("templateParas", args.TemplateParas) + params.Set("signature", args.Signature) + _, err := sendRequest(uri, httputils.POST, header, params, nil) + if err != nil { + return errors.Wrap(err, "huawei sendRequest") + } + return nil +} + +func buildWsseHeader(appKey, appSecret string) string { + var cTime = time.Now().Format("2006-01-02T15:04:05Z") + var nonce = uuid.NewV4().String() + nonce = strings.ReplaceAll(nonce, "-", "") + + h := sha256.New() + h.Write([]byte(nonce + cTime + appSecret)) + passwordDigestBase64Str := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + return fmt.Sprintf(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, cTime) +} + +func init() { + models.SMSRegister(&SHuaweiSMSDriver{}) +} diff --git a/pkg/notify/sender/smsdriver/utils.go b/pkg/notify/sender/smsdriver/utils.go new file mode 100644 index 0000000000..0e1a60eb9f --- /dev/null +++ b/pkg/notify/sender/smsdriver/utils.go @@ -0,0 +1,78 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package smsdriver + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + utilerr "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/gotypes" + "yunion.io/x/pkg/util/httputils" +) + +const ( + LARK_MESSAGE_SUCCESS = "success" +) + +var ( + cli = &http.Client{ + Transport: httputils.GetTransport(true), + } + ctx = context.Background() +) + +// 通知请求 +func sendRequest(url string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + var bodystr string + if !gotypes.IsNil(body) { + bodystr = body.String() + } + jbody := strings.NewReader(bodystr) + if header == nil { + header = http.Header{} + } + if header == nil { + header = http.Header{} + } + if len(header.Values("Content-Type")) == 0 { + header.Set("Content-Type", "application/json") + } + if params != nil { + url += params.Encode() + } + resp, err := httputils.Request(cli, ctx, method, url, header, jbody, true) + if err != nil { + return nil, utilerr.Wrap(err, "http request") + } + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + rbody = bytes.TrimSpace(rbody) + + var jrbody jsonutils.JSONObject = nil + if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') { + jrbody, _ = jsonutils.Parse(rbody) + } else { + return nil, errors.Wrap(err, "resource not json") + } + return jrbody, nil +} diff --git a/pkg/notify/sender/utils.go b/pkg/notify/sender/utils.go new file mode 100644 index 0000000000..5110600fb9 --- /dev/null +++ b/pkg/notify/sender/utils.go @@ -0,0 +1,55 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "context" + "net/http" + "net/url" + + "yunion.io/x/jsonutils" + utilerr "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + "yunion.io/x/onecloud/pkg/notify/options" +) + +const ( + LARK_MESSAGE_SUCCESS = "success" +) + +var ( + cli = &http.Client{ + Transport: httputils.GetTransport(true), + } + ctx = context.Background() +) + +// 通知请求 +func sendRequest(url string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if header == nil { + header = http.Header{} + } + if len(header.Values("Content-Type")) == 0 { + header.Set("Content-Type", "application/json") + } + if params != nil { + url += params.Encode() + } + _, req, err := httputils.JSONRequest(cli, ctx, method, url, header, body, options.Options.DebugRequest) + if err != nil { + return nil, utilerr.Wrap(err, "http request") + } + return req, nil +} diff --git a/pkg/notify/sender/webconsole.go b/pkg/notify/sender/webconsole.go new file mode 100644 index 0000000000..eaa2646a22 --- /dev/null +++ b/pkg/notify/sender/webconsole.go @@ -0,0 +1,118 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "yunion.io/x/cloudmux/pkg/cloudprovider" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SWebconsoleSender struct { + config map[string]api.SNotifyConfigContent +} + +func (self *SWebconsoleSender) GetSenderType() string { + return api.WEBCONSOLE +} + +func (self *SWebconsoleSender) Send(args api.SendParams) error { + // var token string + // var errs []error + // title, msg := args.Title, args.Message + // // for _, recevier := range args.Receivers { + // webhook := args.Receivers.Contact + // switch { + // case strings.HasPrefix(webhook, ApiWebhookRobotV2SendMessage): + // token = webhook[len(ApiWebhookRobotV2SendMessage):] + // case strings.HasPrefix(webhook, feishu.ApiWebhookRobotSendMessage): + // token = webhook[len(feishu.ApiWebhookRobotSendMessage):] + // default: + // return errors.Wrap(InvalidWebhook, webhook) + // } + // req := feishu.WebhookRobotMsgReq{ + // Title: title, + // Text: msg, + // } + // rep, err := feishu.SendWebhookRobotMessage(token, req) + // if err != nil { + // return errors.Wrap(err, "SendWebhookRobotMessage") + // } + // if !rep.Ok { + // if strings.Contains(rep.Error, "token") { + // return ErrNoSuchWebhook + // } else { + // return fmt.Errorf("SendWebhookRobotMessage failed: %s", rep.Error) + // } + // } + // if err != nil { + // if errs == nil { + // errs = []error{} + // } + // errs = append(errs, err) + // } + // return errors.NewAggregate(errs) + return nil +} + +func (websender *SWebconsoleSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (websender *SWebconsoleSender) UpdateConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebconsoleSender) AddConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebconsoleSender) DeleteConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebconsoleSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (websender *SWebconsoleSender) IsPersonal() bool { + return true +} + +func (websender *SWebconsoleSender) IsRobot() bool { + return true +} + +func (websender *SWebconsoleSender) IsValid() bool { + return len(websender.config) > 0 +} + +func (websender *SWebconsoleSender) IsPullType() bool { + return true +} + +func (websender *SWebconsoleSender) IsSystemConfigContactType() bool { + return true +} + +func (websender *SWebconsoleSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SWebconsoleSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/webhook.go b/pkg/notify/sender/webhook.go new file mode 100644 index 0000000000..80a8869614 --- /dev/null +++ b/pkg/notify/sender/webhook.go @@ -0,0 +1,103 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "net/http" + "strings" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SWebhookSender struct { + config map[string]api.SNotifyConfigContent +} + +func (self *SWebhookSender) GetSenderType() string { + return api.WEBHOOK_ROBOT +} + +func (self *SWebhookSender) Send(args api.SendParams) error { + log.Infoln("this is in webhookSend ") + body, err := jsonutils.ParseString(args.Message) + if err != nil { + log.Errorf("unable to parse %q: %v", args.Message, err) + } + if _, ok := body.(*jsonutils.JSONString); err != nil || ok { + dict := jsonutils.NewDict() + dict.Set("Msg", jsonutils.NewString(args.Message)) + body = dict + } + event := strings.ToUpper(args.Event) + header := http.Header{} + header.Set(EVENT_HEADER, event) + _, _, err = httputils.JSONRequest(cli, ctx, httputils.POST, args.Receivers.Contact, header, body, true) + return err +} + +func (websender *SWebhookSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (websender *SWebhookSender) UpdateConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebhookSender) AddConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebhookSender) DeleteConfig(config api.NotifyConfig) error { + return cloudprovider.ErrNotImplemented +} + +func (websender *SWebhookSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (websender *SWebhookSender) IsPersonal() bool { + return true +} + +func (websender *SWebhookSender) IsRobot() bool { + return true +} + +func (websender *SWebhookSender) IsValid() bool { + return len(websender.config) > 0 +} + +func (websender *SWebhookSender) IsPullType() bool { + return true +} + +func (websender *SWebhookSender) IsSystemConfigContactType() bool { + return true +} + +func (websender *SWebhookSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SWebhookSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/websocket.go b/pkg/notify/sender/websocket.go new file mode 100644 index 0000000000..c88510ee75 --- /dev/null +++ b/pkg/notify/sender/websocket.go @@ -0,0 +1,120 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "context" + "fmt" + "strings" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/ansibleserver/options" + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/mcclient/auth" + modules "yunion.io/x/onecloud/pkg/mcclient/modules/websocket" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SWebsocketSender struct { + config map[string]api.SNotifyConfigContent +} + +func (websocket *SWebsocketSender) GetSenderType() string { + return api.WEBSOCKET +} + +func (websocket *SWebsocketSender) Send(params api.SendParams) error { + return websocket.send(params) +} + +func (websocket *SWebsocketSender) send(args api.SendParams) error { + params := jsonutils.NewDict() + params.Add(jsonutils.NewString("notify"), "obj_type") + params.Add(jsonutils.NewString(""), "obj_id") + params.Add(jsonutils.NewString(""), "obj_name") + params.Add(jsonutils.JSONTrue, "success") + // component request body + body := jsonutils.DeepCopy(params).(*jsonutils.JSONDict) + body.Add(jsonutils.NewString(args.Title), "action") + body.Add(jsonutils.NewString(fmt.Sprintf("priority=%s; content=%s", args.Priority, args.Message)), "notes") + body.Add(jsonutils.NewString(args.Receivers.Contact), "user_id") + body.Add(jsonutils.NewString(args.Receivers.Contact), "user") + if len(args.Receivers.Contact) == 0 { + body.Add(jsonutils.JSONTrue, "broadcast") + } + if websocket.isFailed(args.Title, args.Message) { + body.Add(jsonutils.JSONFalse, "success") + } + session := auth.GetAdminSession(context.Background(), options.Options.Region) + _, err := modules.Websockets.Create(session, body) + if err != nil { + // failed + _, err = modules.Websockets.Create(session, body) + + return err + } + return nil +} + +func (websocket *SWebsocketSender) isFailed(title, message string) bool { + for _, c := range []string{title, message} { + for _, k := range FAIL_KEY { + if strings.Contains(c, k) { + return true + } + } + } + return false +} + +func (websocket *SWebsocketSender) IsPersonal() bool { + return true +} + +func (websocket *SWebsocketSender) IsRobot() bool { + return false +} + +func (websocket *SWebsocketSender) IsValid() bool { + return true +} + +func (websocket *SWebsocketSender) IsPullType() bool { + return true +} + +func (websocket *SWebsocketSender) IsSystemConfigContactType() bool { + return true +} + +func (websocket *SWebsocketSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", nil +} + +func (websocket *SWebsocketSender) GetAccessToken() error { + return nil +} + +func (websocket *SWebsocketSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func init() { + models.Register(&SWebsocketSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/workwx.go b/pkg/notify/sender/workwx.go new file mode 100644 index 0000000000..5d175be6bb --- /dev/null +++ b/pkg/notify/sender/workwx.go @@ -0,0 +1,140 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SWorkwxSender struct { + config map[string]api.SNotifyConfigContent +} + +func (workwxSender *SWorkwxSender) GetSenderType() string { + return api.WORKWX +} + +func (workwxSender *SWorkwxSender) Send(args api.SendParams) error { + body := map[string]interface{}{ + "agentid": models.ConfigMap[api.WORKWX].Content.AgentId, + "msgtype": "markdown", + "markdown": map[string]interface{}{ + "content": fmt.Sprintf("# %s\n\n%s", args.Title, args.Message), + }, + "touser": args.Receivers.Contact, + } + _, err := workwxSender.sendMessageWithToken(ApiWorkwxSendMessage, httputils.POST, nil, nil, jsonutils.Marshal(body)) + if err != nil { + return errors.Wrap(err, "workwx send message") + } + + return err +} + +func (workwxSender *SWorkwxSender) ValidateConfig(config api.NotifyConfig) (string, error) { + // 校验accesstoken + _, err := workwxSender.getAccessToken(config.CorpId, config.Secret) + if err != nil { + switch { + case strings.Contains(err.Error(), "40013"): + return "invalid corpid", nil + case strings.Contains(err.Error(), "40001"): + return "invalid corpsecret", nil + } + return "", err + } + return "", nil +} + +func (workwxSender *SWorkwxSender) ContactByMobile(mobile, domainId string) (string, error) { + err := workwxSender.GetAccessToken() + if err != nil { + return "", err + } + body := jsonutils.Marshal(map[string]interface{}{ + "mobile": mobile, + }) + res, err := workwxSender.sendMessageWithToken(ApiWorkwxGetUserByMobile, httputils.POST, nil, nil, jsonutils.Marshal(body)) + if err != nil { + return "", errors.Wrap(err, "get user by mobile") + } + return res.GetString("userid") +} + +func (workwxSender *SWorkwxSender) IsPersonal() bool { + return true +} + +func (workwxSender *SWorkwxSender) IsRobot() bool { + return false +} + +func (workwxSender *SWorkwxSender) IsValid() bool { + return len(workwxSender.config) > 0 +} + +func (workwxSender *SWorkwxSender) IsPullType() bool { + return true +} + +func (workwxSender *SWorkwxSender) IsSystemConfigContactType() bool { + return true +} + +func (workwxSender *SWorkwxSender) GetAccessToken() error { + corpId, secret := models.ConfigMap[api.WORKWX].Content.CorpId, models.ConfigMap[api.WORKWX].Content.Secret + token, err := workwxSender.getAccessToken(corpId, secret) + if err != nil { + return errors.Wrap(err, "workwx getAccessToken") + } + models.ConfigMap[api.WORKWX].Content.AccessToken = token + return nil +} + +func (workwxSender *SWorkwxSender) getAccessToken(corpId, secret string) (string, error) { + // url := ApiWorkwxGetToken + fmt.Sprintf("?corpid=%s&corpsecret=%s", corpId, secret) + params := url.Values{} + params.Set("corpid", corpId) + params.Set("corpsecret", secret) + res, err := sendRequest(ApiWorkwxGetToken, httputils.GET, nil, params, nil) + if err != nil { + return "", errors.Wrap(err, "get workwx token") + } + return res.GetString("access_token") +} + +func (workwxSender *SWorkwxSender) sendMessageWithToken(uri string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if params == nil { + params = url.Values{} + } + params.Set("access_token", models.ConfigMap[api.WORKWX].Content.AccessToken) + return sendRequest(uri, httputils.POST, nil, params, jsonutils.Marshal(body)) +} + +func init() { + models.Register(&SWorkwxSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/sender/workwx_robot.go b/pkg/notify/sender/workwx_robot.go new file mode 100644 index 0000000000..de4c174e75 --- /dev/null +++ b/pkg/notify/sender/workwx_robot.go @@ -0,0 +1,95 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sender + +import ( + "fmt" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/notify/models" +) + +type SWorkwxRobotSender struct { + config map[string]api.SNotifyConfigContent +} + +func (workwxRobotSender *SWorkwxRobotSender) GetSenderType() string { + return api.WORKWX_ROBOT +} + +func (workwxRobotSender *SWorkwxRobotSender) Send(args api.SendParams) error { + errs := []error{} + content := fmt.Sprintf("# %s\n\n%s", args.Title, args.Message) + mid := map[string]interface{}{ + "msgtype": "markdown", + "markdown": map[string]interface{}{ + "content": content, + }, + } + req, err := sendRequest(args.Receivers.Contact, httputils.POST, nil, nil, jsonutils.Marshal(mid)) + if err != nil { + return errors.Wrap(err, "sendRequest") + } + errCode, err := req.GetString("errcode") + if err != nil { + return errors.Wrap(err, "req.GetString") + } + if errCode != "0" { + errs = append(errs, errors.Errorf(req.PrettyString())) + } + return errors.NewAggregate(errs) +} + +func (workwxRobotSender *SWorkwxRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (workwxRobotSender *SWorkwxRobotSender) ContactByMobile(mobile, domainId string) (string, error) { + return "", cloudprovider.ErrNotImplemented +} + +func (workwxRobotSender *SWorkwxRobotSender) IsPersonal() bool { + return true +} + +func (workwxRobotSender *SWorkwxRobotSender) IsRobot() bool { + return true +} + +func (workwxRobotSender *SWorkwxRobotSender) IsValid() bool { + return len(workwxRobotSender.config) > 0 +} + +func (workwxRobotSender *SWorkwxRobotSender) IsPullType() bool { + return true +} + +func (workwxRobotSender *SWorkwxRobotSender) IsSystemConfigContactType() bool { + return true +} + +func (workwxRobotSender *SWorkwxRobotSender) GetAccessToken() error { + return nil +} + +func init() { + models.Register(&SWorkwxRobotSender{ + config: map[string]api.SNotifyConfigContent{}, + }) +} diff --git a/pkg/notify/service/handlers.go b/pkg/notify/service/handlers.go index c13e486653..fa35a4e177 100644 --- a/pkg/notify/service/handlers.go +++ b/pkg/notify/service/handlers.go @@ -20,7 +20,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/notify/models" - "yunion.io/x/onecloud/pkg/notify/oldmodels" + _ "yunion.io/x/onecloud/pkg/notify/sender" ) const ( @@ -37,13 +37,6 @@ func InitHandlers(app *appsrv.Application) { db.AddScopeResourceCountHandler(API_VERSION, app) - // Data migration - db.RegisterModelManager(oldmodels.NotificationManager) - db.RegisterModelManager(oldmodels.ContactManager) - db.RegisterModelManager(oldmodels.ConfigManager) - db.RegisterModelManager(oldmodels.TemplateManager) - db.RegisterModelManager(oldmodels.UserCacheManager) - taskman.AddTaskHandler(API_VERSION, app) for _, manager := range []db.IModelManager{ taskman.TaskManager, diff --git a/pkg/notify/service/service.go b/pkg/notify/service/service.go index 71f750010f..9388035c9e 100644 --- a/pkg/notify/service/service.go +++ b/pkg/notify/service/service.go @@ -19,6 +19,7 @@ import ( "time" "yunion.io/x/log" + "yunion.io/x/pkg/errors" _ "yunion.io/x/sqlchemy/backends" api "yunion.io/x/onecloud/pkg/apis/notify" @@ -30,7 +31,7 @@ import ( "yunion.io/x/onecloud/pkg/notify/models" "yunion.io/x/onecloud/pkg/notify/options" _ "yunion.io/x/onecloud/pkg/notify/policy" - "yunion.io/x/onecloud/pkg/notify/rpc" + _ "yunion.io/x/onecloud/pkg/notify/sender/smsdriver" _ "yunion.io/x/onecloud/pkg/notify/tasks" ) @@ -60,19 +61,15 @@ func StartService() { db.EnsureAppSyncDB(applicaion, dbOpts, models.InitDB) defer cloudcommon.CloseDB() - err := models.ReceiverManager.StartWatchUserInKeystone() - if err != nil { - log.Logger().Panic(err.Error()) + if options.Options.EnableWatchUser { + err := models.ReceiverManager.StartWatchUserInKeystone() + if err != nil { + log.Errorln(errors.Wrap(err, "StartWatchUserInKeystone")) + } } - // init notify service - models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager, models.TemplateManager) - models.NotifyService.InitAll() - defer models.NotifyService.StopAll() - cron := cronman.InitCronJobManager(true, 2) // update service - cron.AddJobAtIntervals("UpdateServices", time.Duration(opts.UpdateInterval)*time.Minute, models.NotifyService.UpdateServices) cron.AddJobAtIntervalsWithStartRun("syncReciverFromKeystone", time.Duration(opts.SyncReceiverIntervalMinutes)*time.Minute, models.ReceiverManager.SyncUserFromKeystone, true) // wrapped func to resend notifications diff --git a/pkg/notify/tasks/notifications_send_task.go b/pkg/notify/tasks/notifications_send_task.go index 8c5a5ed03b..7707f24606 100644 --- a/pkg/notify/tasks/notifications_send_task.go +++ b/pkg/notify/tasks/notifications_send_task.go @@ -22,13 +22,13 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" + "yunion.io/x/pkg/errors" apis "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/notify/models" "yunion.io/x/onecloud/pkg/notify/options" - rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis" "yunion.io/x/onecloud/pkg/util/logclient" ) @@ -70,21 +70,27 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone } rns, err := notification.ReceiverNotificationsNotOK() if err != nil { - self.taskFailed(ctx, notification, "fail to fetch ReceiverNotifications", true) + self.taskFailed(ctx, notification, errors.Wrapf(err, "ReceiverNotificationsNotOK").Error(), true) + return + } + event, err := models.EventManager.GetEvent(notification.EventId) + if err != nil { + self.taskFailed(ctx, notification, errors.Wrapf(err, "GetEvent").Error(), true) return } notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_SENDING, "") + // build contactMap + receivers := make([]ReceiverSpec, 0) + receiversEn := make([]ReceiverSpec, 0, len(rns)/2) + receiversCn := make([]ReceiverSpec, 0, len(rns)/2) + failedRecord := make([]string, 0) sendFail := func(rn *models.SReceiverNotification, reason string) { rn.AfterSend(ctx, false, reason) failedRecord = append(failedRecord, fmt.Sprintf("%s: %s", rn.ReceiverID, reason)) } - // build contactMap - receivers := make([]ReceiverSpec, 0, 0) - receiversEn := make([]ReceiverSpec, 0, len(rns)/2) - receiversCn := make([]ReceiverSpec, 0, len(rns)/2) for i := range rns { receiver, err := rns[i].Receiver() if err != nil { @@ -99,10 +105,18 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone // check contact enabled enabled, err := receiver.IsEnabledContactType(notification.ContactType) if err != nil { - sendFail(&rns[i], fmt.Sprintf("IsEnabledContactType error for receiver: %s", err.Error())) + logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetEnabledContactTypes"), self.GetUserCred(), false) continue } - if !enabled { + if notification.ContactType == apis.WEBHOOK { + notification.ContactType = apis.WEBHOOK_ROBOT + } + if receiver.IsRobot() { + robot := receiver.(*models.SRobot) + notification.ContactType = fmt.Sprintf("%s-robot", robot.Type) + } + driver := models.GetDriver(notification.ContactType) + if driver == nil || !enabled { sendFail(&rns[i], fmt.Sprintf("disabled contactType %q", notification.ContactType)) continue } @@ -120,10 +134,32 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone // contact, err := receiver.GetContact(notification.ContactType) // if err != nil { - // reason := fmt.Sprintf("fail to fetch contact: %s", err.Error()) + // logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetContact(%s)", notification.ContactType), self.GetUserCred(), false) + // continue + // } + + // notifyRecv := receiver.GetNotifyReceiver() + // notifyRecv.Lang, _ = receiver.GetTemplateLang(ctx) + // notifyRecv.Contact = contact + // cv := ReceiverSpec{ + // receiver: notifyRecv, + // rNotificaion: &rns[i], + // } + // switch notifyRecv.Lang { + // case "": + // receivers = append(receivers, cv) + // case apis.TEMPLATE_LANG_EN: + // receiversEn = append(receiversEn, cv) + // case apis.TEMPLATE_LANG_CN: + // receiversCn = append(receiversCn, cv) + // } + // lang, err := receiver.GetTemplateLang(ctx) + // if err != nil { + // reason := fmt.Sprintf("fail to GetTemplateLang: %s", err.Error()) // sendFail(&rns[i], reason) // continue // } + lang, err := receiver.GetTemplateLang(ctx) if err != nil { reason := fmt.Sprintf("fail to GetTemplateLang: %s", err.Error()) @@ -148,7 +184,12 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone }) } } - var contactLen int + + nn, err := notification.Notification() + if err != nil { + self.taskFailed(ctx, notification, errors.Wrapf(err, "Notification").Error(), true) + return + } for lang, receivers := range map[string][]ReceiverSpec{ "": receivers, apis.TEMPLATE_LANG_CN: receiversCn, @@ -158,17 +199,13 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone log.Warningf("no receiver to send, skip ...") continue } - // send - nn, err := notification.Notification() + p, err := notification.FillWithTemplate(ctx, lang, nn) if err != nil { - self.taskFailed(ctx, notification, err.Error(), false) + logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "FillWithTemplate(%s)", lang), self.GetUserCred(), false) + continue } - p, err := notification.TemplateStore().FillWithTemplate(ctx, lang, nn) - if err != nil { - self.taskFailed(ctx, notification, err.Error(), false) - } - + p.Event = event.Event switch lang { case apis.TEMPLATE_LANG_CN: p.Message += "\n来自 " + options.Options.ApiServer @@ -181,11 +218,8 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone for _, rn := range receivers { rn.rNotificaion.BeforeSend(ctx, now) } - - contactLen += len(receivers) - // send - fds, err := self.batchSend(ctx, notification.ContactType, receivers, p) + fds, err := self.batchSend(ctx, notification, receivers, p) if err != nil { for _, r := range receivers { sendFail(r.rNotificaion, err.Error()) @@ -224,77 +258,44 @@ type FailedReceiverSpec struct { Reason string } -func (self *NotificationSendTask) batchSend(ctx context.Context, contactType string, receivers []ReceiverSpec, params rpcapi.SendParams) (fails []FailedReceiverSpec, err error) { - log.Debugf("contactType: %s, receivers: %s, params: %s", contactType, jsonutils.Marshal(receivers), jsonutils.Marshal(params)) - if contactType != apis.ROBOT && contactType != apis.WEBHOOK { - return self._batchSend(ctx, contactType, receivers, func(res []*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error) { - return models.NotifyService.BatchSend(ctx, contactType, rpcapi.BatchSendParams{ - Receivers: res, - Title: params.Title, - Message: params.Message, - Priority: params.Priority, - RemoteTemplate: params.RemoteTemplate, - }) - }) - } - robots := make(map[string][]ReceiverSpec) +func (notificationSendTask *NotificationSendTask) batchSend(ctx context.Context, notification *models.SNotification, receivers []ReceiverSpec, params apis.SendParams) (fails []FailedReceiverSpec, err error) { for i := range receivers { - robot := receivers[i].receiver.(*models.SRobot) - robots[robot.Type] = append(robots[robot.Type], receivers[i]) - } - for rType, robots := range robots { - _fails, err := self._batchSend(ctx, contactType, robots, func(res []*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error) { - return models.NotifyService.SendRobotMessage(ctx, rType, res, params.Title, params.Message) - }) - if err != nil { - for i := range robots { - fails = append(fails, FailedReceiverSpec{ - ReceiverSpec: robots[i], - Reason: err.Error(), - }) + if receivers[i].receiver.IsRobot() { + robot := receivers[i].receiver.(*models.SRobot) + driver := models.GetDriver(fmt.Sprintf("%s-robot", robot.Type)) + params.Receivers.Contact = robot.Address + err = driver.Send(params) + if err != nil { + fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()}) + } + } else if receivers[i].receiver.IsReceiver() { + receiver := receivers[i].receiver.(*models.SReceiver) + params.Receivers.Contact, _ = receiver.GetContact(notification.ContactType) + driver := models.GetDriver(notification.ContactType) + if notification.ContactType == apis.EMAIL { + params.EmailMsg = &apis.SEmailMessage{ + To: []string{receiver.Email}, + Subject: params.Title, + Body: params.Message, + } + } + if notification.ContactType == apis.MOBILE { + params.Receivers.Contact = receiver.Mobile + } + err = driver.Send(params) + if err != nil { + fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()}) + } + } else { + receiver := receivers[i].receiver.(*models.SContact) + params.Receivers.Contact, _ = receiver.GetContact(notification.ContactType) + driver := models.GetDriver(notification.ContactType) + err = driver.Send(params) + if err != nil { + fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()}) } } - fails = append(fails, _fails...) } + return fails, nil } - -func (self *NotificationSendTask) _batchSend(ctx context.Context, contactType string, receivers []ReceiverSpec, send func([]*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error)) (fails []FailedReceiverSpec, err error) { - rpcReceivers := make([]*rpcapi.SReceiver, len(receivers)) - rpc2Receiver := make(map[DomainContact]ReceiverSpec, len(receivers)) - for i := range receivers { - contact, err := receivers[i].receiver.GetContact(contactType) - if err != nil { - fails = append(fails, FailedReceiverSpec{ - ReceiverSpec: receivers[i], - Reason: fmt.Sprintf("fail to fetch contact: %s", err.Error()), - }) - continue - } - rpcReceivers[i] = &rpcapi.SReceiver{ - DomainId: receivers[i].receiver.GetDomainId(), - Contact: contact, - } - rpc2Receiver[DomainContact{ - DomainId: receivers[i].receiver.GetDomainId(), - Contact: contact, - }] = receivers[i] - } - fds, err := send(rpcReceivers) - if err != nil { - return nil, err - } - // check result - for _, fd := range fds { - dc := DomainContact{ - DomainId: fd.Receiver.DomainId, - Contact: fd.Receiver.Contact, - } - receiver := rpc2Receiver[dc] - fails = append(fails, FailedReceiverSpec{ - ReceiverSpec: receiver, - Reason: fd.Reason, - }) - } - return -} diff --git a/pkg/notify/tasks/repull_subcontact_task.go b/pkg/notify/tasks/repull_subcontact_task.go index f0e66dfe7f..9d57edb1db 100644 --- a/pkg/notify/tasks/repull_subcontact_task.go +++ b/pkg/notify/tasks/repull_subcontact_task.go @@ -104,25 +104,25 @@ func (self *RepullSuncontactTask) OnInit(ctx context.Context, obj db.IStandalone ctSets := sets.NewString(cts...) if ctSets.Has(config.Type) { ctSets.Delete(config.Type) - err = r.SetVerifiedContactTypes(ctSets.UnsortedList()) - if err != nil { - reasons = append(reasons, repullFailedReason{ - ReceiverId: r.Id, - Reason: fmt.Sprintf("unable to SetVerifiedContactTypes: %v", err), - }.String()) - return - } + // err = r.SetVerifiedContactTypes(ctSets.UnsortedList()) + // if err != nil { + // reasons = append(reasons, repullFailedReason{ + // ReceiverId: r.Id, + // Reason: fmt.Sprintf("unable to SetVerifiedContactTypes: %v", err), + // }.String()) + // return + // } } // pull params := jsonutils.NewDict() params.Set("contact_types", jsonutils.NewArray(jsonutils.NewString(config.Type))) - err = r.StartSubcontactPullTask(ctx, self.UserCred, params, self.Id) - if err != nil { - reasons = append(reasons, repullFailedReason{ - ReceiverId: r.Id, - Reason: fmt.Sprintf("unable to StartSubcontactPullTask: %v", err), - }.String()) - } + // err = r.StartSubcontactPullTask(ctx, self.UserCred, params, self.Id) + // if err != nil { + // reasons = append(reasons, repullFailedReason{ + // ReceiverId: r.Id, + // Reason: fmt.Sprintf("unable to StartSubcontactPullTask: %v", err), + // }.String()) + // } }() } if len(reasons) > 0 { diff --git a/pkg/notify/tasks/subcontact_pull_task.go b/pkg/notify/tasks/subcontact_pull_task.go index 8a3a6015b7..89cc1b3a7a 100644 --- a/pkg/notify/tasks/subcontact_pull_task.go +++ b/pkg/notify/tasks/subcontact_pull_task.go @@ -22,14 +22,15 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" "yunion.io/x/pkg/utils" + "yunion.io/x/sqlchemy" apis "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/mcclient/modules/identity" - "yunion.io/x/onecloud/pkg/notify" "yunion.io/x/onecloud/pkg/notify/models" "yunion.io/x/onecloud/pkg/util/logclient" ) @@ -84,35 +85,63 @@ func (self *SubcontactPullTask) OnInit(ctx context.Context, obj db.IStandaloneMo } else { contactTypes, _ = receiver.GetEnabledContactTypes() } + for _, cType := range contactTypes { if !utils.IsInStringArray(cType, PullContactType) { continue } - userid, err := models.NotifyService.ContactByMobile(ctx, receiver.Mobile, cType, receiver.GetDomainId()) + content := "" + switch cType { + case apis.EMAIL: + content = receiver.Email + default: + driver := models.GetDriver(cType) + content, err = driver.ContactByMobile(mobile, "") + } if err != nil { var reason string - if errors.Cause(err) == notify.ErrNoSuchMobile { - receiver.MarkContactTypeUnVerified(cType, notify.ErrNoSuchMobile.Error()) + if errors.Cause(err) == apis.ErrNoSuchMobile { + receiver.MarkContactTypeUnVerified(ctx, cType, apis.ErrNoSuchMobile.Error()) reason = fmt.Sprintf("%q: no such mobile %s", cType, receiver.Mobile) - } else if errors.Cause(err) == notify.ErrIncompleteConfig { - receiver.MarkContactTypeUnVerified(cType, notify.ErrIncompleteConfig.Error()) + } else if errors.Cause(err) == apis.ErrIncompleteConfig { + receiver.MarkContactTypeUnVerified(ctx, cType, apis.ErrIncompleteConfig.Error()) reason = fmt.Sprintf("%q: %v", cType, err) } else { - receiver.MarkContactTypeUnVerified(cType, "service exceptions") + receiver.MarkContactTypeUnVerified(ctx, cType, "service exceptions") reason = fmt.Sprintf("%q: %v", cType, err) } failedReasons = append(failedReasons, reason) continue + } else { + q := models.SubContactManager.Query() + cond := sqlchemy.AND(sqlchemy.Equals(q.Field("receiver_id"), receiver.Id), sqlchemy.Equals(q.Field("type"), cType)) + q.Filter(cond) + subcontact := []models.SSubContact{} + err := db.FetchModelObjects(models.SubContactManager, q, &subcontact) + if err != nil { + failedReasons = append(failedReasons, err.Error()) + continue + } + subid := "" + if len(subcontact) > 0 { + subid = subcontact[0].Id + } + err = models.SubContactManager.TableSpec().InsertOrUpdate(ctx, &models.SSubContact{ + SStandaloneResourceBase: db.SStandaloneResourceBase{ + SStandaloneAnonResourceBase: db.SStandaloneAnonResourceBase{Id: subid}, + }, + ReceiverID: receiver.Id, + Type: cType, + Contact: content, + ParentContactType: "mobile", + Enabled: tristate.True, + }) + if err != nil { + log.Errorln("this is err:", err) + } } - receiver.SetContact(cType, userid) - receiver.MarkContactTypeVerified(cType) - } - // push cache - err = receiver.PushCache(ctx) - if err != nil { - reason := fmt.Sprintf("PushCache: %v", err) - self.taskFailed(ctx, receiver, reason) - return + receiver.SetContact(cType, content) + receiver.MarkContactTypeVerified(ctx, cType) } if len(failedReasons) > 0 { reason := strings.Join(failedReasons, "; ") diff --git a/pkg/notify/tasks/topic_message_send_task.go b/pkg/notify/tasks/topic_message_send_task.go new file mode 100644 index 0000000000..b39cde3217 --- /dev/null +++ b/pkg/notify/tasks/topic_message_send_task.go @@ -0,0 +1,302 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/utils" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/mcclient/auth" + identity "yunion.io/x/onecloud/pkg/mcclient/modules/identity" + "yunion.io/x/onecloud/pkg/notify/models" + "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type TopicMessageSendTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(TopicMessageSendTask{}) +} + +func (topicMessageSendTask *TopicMessageSendTask) taskFailed(ctx context.Context, topic *models.STopic, err error) { + logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, err, topicMessageSendTask.UserCred, false) + topicMessageSendTask.SetStageFailed(ctx, jsonutils.NewString(err.Error())) +} + +func (topicMessageSendTask *TopicMessageSendTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) { + failedReasons := []string{} + topic := obj.(*models.STopic) + input := api.NotificationManagerEventNotifyInput{} + topicMessageSendTask.GetParams().Unmarshal(&input) + + message := jsonutils.Marshal(input.ResourceDetails).String() + sevent := api.Event.WithAction(input.Action).WithResourceType(input.ResourceType) + event, err := models.EventManager.CreateEvent(ctx, sevent.String(), topic.Id, message, string(input.Action), string(input.ResourceType), input.AdvanceDays) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "unable to create Event")) + return + } + snotification := api.SsNotification{ + Topic: topic.Id, + Message: event.Message, + Event: sevent, + AdvanceDays: event.AdvanceDays, + } + n := models.SNotification{ + Priority: input.Priority, + EventId: event.GetId(), + TopicType: topic.Type, + } + n.Id = db.DefaultUUIDGenerator() + for _, contact := range input.ContactTypes { + n.ContactType = contact + err = models.NotificationManager.TableSpec().Insert(ctx, &n) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "notifications insert")) + return + } + } + + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "unable to fetch receivers by ids")) + return + } + + needWebconsole := false + + // 本地模板 + send, _ := models.LocalTemplateManager.FillWithTemplate(ctx, api.TEMPLATE_LANG_CN, snotification) + // 远程模板(短信) + remoteSend, _ := models.TemplateManager.FillWithTemplate(ctx, api.TEMPLATE_LANG_CN, snotification) + send.Event = event.Event + remoteSend.Event = event.Event + websocketDriver := models.GetDriver(api.WEBSOCKET) + err = websocketDriver.Send(send) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrapf(err, "websocket send")) + } + scribers, err := topic.GetEnabledSubscribers(input.ProjectDomainId, input.ProjectId) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrapf(err, "GetSubscribers")) + return + } + + robots := map[string]*models.SRobot{} + userIds := []string{} + for i := range scribers { + switch scribers[i].Type { + case api.SUBSCRIBER_TYPE_RECEIVER: + recvs, err := scribers[i].GetEnabledReceivers() + if err != nil { + log.Errorf("scribers[%d].GetEnabledReceivers err :%s", i, err) + return + } + // ids := []string{} + err = sendByReceivers(recvs, []string{}, n, send, remoteSend, needWebconsole) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "sendByReceivers")) + return + } + case api.SUBSCRIBER_TYPE_ROLE: + query := jsonutils.NewDict() + query.Set("roles", jsonutils.NewStringArray([]string{scribers[i].Identification})) + query.Set("effective", jsonutils.JSONTrue) + if scribers[i].RoleScope == api.SUBSCRIBER_SCOPE_DOMAIN { + query.Set("project_domain_id", jsonutils.NewString(scribers[i].ResourceAttributionId)) + } else if scribers[i].RoleScope == api.SUBSCRIBER_SCOPE_PROJECT { + query.Add(jsonutils.NewString(scribers[i].ResourceAttributionId), "scope", "project", "id") + } + s := auth.GetAdminSession(ctx, options.Options.Region) + ret, err := identity.RoleAssignments.List(s, query) + if err != nil { + logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "RoleAssignments.List"), topicMessageSendTask.UserCred, false) + continue + } + users := []struct { + User struct { + Id string + } + }{} + jsonutils.Update(&users, ret.Data) + for _, user := range users { + userIds = append(userIds, user.User.Id) + } + recvs, err := models.ReceiverManager.FetchEnableReceiversByIdOrNames(ctx, userIds...) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "FetchEnableReceiversByIdOrNames")) + return + } + err = sendByReceivers(recvs, []string{}, n, send, remoteSend, false) + if err != nil { + topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "sendByReceivers")) + return + } + case api.SUBSCRIBER_TYPE_ROBOT: + robot, err := scribers[i].GetRobot() + if err != nil { + logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetRobot"), topicMessageSendTask.UserCred, false) + continue + } + if !robot.Enabled.Bool() { + continue + } + robots[robot.Id] = robot + robotType := "" + switch robot.Type { + case api.FEISHU: + robotType = api.FEISHU_ROBOT + case api.DINGTALK: + robotType = api.DINGTALK_ROBOT + case api.WORKWX: + robotType = api.WORKWX_ROBOT + case api.WEBHOOK: + robotType = api.WEBHOOK_ROBOT + } + n.ContactType = robotType + err = models.NotificationManager.TableSpec().Insert(ctx, &n) + if err != nil { + failedReasons = append(failedReasons, err.Error()) + } + session := auth.GetAdminSession(ctx, options.Options.Region) + rn := models.SReceiverNotification{ + NotificationID: n.Id, + ReceiverType: api.RECEIVER_TYPE_ROBOT, + ReceiverID: robot.Id, + Status: api.RECEIVER_NOTIFICATION_RECEIVED, + SendBy: session.GetUserId(), + } + + models.ReceiverNotificationManager.TableSpec().Insert(ctx, &rn) + driver := models.GetDriver(robotType) + if driver == nil { + log.Errorln(robotType) + } + send.Receivers = api.SNotifyReceiver{Contact: robot.Address} + err = driver.Send(send) + if err != nil { + log.Errorln("this is err:", err) + } + } + } + if len(failedReasons) > 0 { + reason := strings.Join(failedReasons, "; ") + topicMessageSendTask.taskFailed(ctx, topic, errors.Error(reason)) + return + } + logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, jsonutils.Marshal(input), topicMessageSendTask.UserCred, true) + topicMessageSendTask.SetStageComplete(ctx, nil) +} + +func sendByReceivers(recvs []models.SReceiver, receiverIds []string, n models.SNotification, send, remoteSend api.SendParams, needWebconsole bool) error { + ctx := context.Background() + failedReasons := []string{} + ids := []string{} + idFailedMap := make(map[string][]string) + for i, recv := range recvs { + // 检查是否发送email + if recvs[i].EnabledEmail == tristate.True { + if recvs[i].VerifiedEmail == tristate.True { + if recvs[i].EnabledEmail == tristate.True { + driver := models.GetDriver(api.EMAIL) + send.Receivers.Contact = recvs[i].Email + err := driver.Send(send) + if err != nil { + failedReasons = append(failedReasons, errors.Wrapf(err, "email send").Error()) + } + } + } else { + log.Errorln(errors.Errorf("email has no verified: %s,receiver name: %s", recvs[i].Email, recvs[i].Name)) + } + } + // 检查是否发送短信 + if recvs[i].EnabledMobile == tristate.True { + if recvs[i].VerifiedMobile == tristate.True { + if recvs[i].EnabledMobile == tristate.True { + driver := models.GetDriver(api.MOBILE) + send.Receivers.Contact = recvs[i].Mobile + err := driver.Send(remoteSend) + if err != nil { + failedReasons = append(failedReasons, errors.Wrapf(err, "mobile send").Error()) + } + } + } else { + log.Errorln(errors.Errorf("sms has no verified: %s,receiver name: %s", recvs[i].Mobile, recvs[i].Name)) + } + } + idFailedMap[recv.Id] = append(idFailedMap[recv.Id], failedReasons...) + ids = append(ids, recvs[i].Id) + } + session := auth.GetAdminSession(ctx, options.Options.Region) + for _, receiverId := range receiverIds { + rn := models.SReceiverNotification{ + NotificationID: n.Id, + Status: api.RECEIVER_NOTIFICATION_RECEIVED, + SendBy: session.GetUserId(), + } + if utils.IsInStringArray(receiverId, ids) { + rn.ReceiverType = api.RECEIVER_TYPE_USER + rn.ReceiverID = receiverId + } else { + rn.ReceiverType = api.RECEIVER_TYPE_CONTACT + rn.Contact = receiverId + } + models.ReceiverNotificationManager.TableSpec().Insert(ctx, &rn) + } + rm := &models.SReceiverManager{} + // 从subcontacts表中获取数据并发送 + subContactsMap, err := rm.FetchSubContacts(ids) + if err != nil { + return errors.Wrap(err, "rm.FetchSubContacts") + } + for id, subContacts := range subContactsMap { + for _, subContact := range subContacts { + n.Topic = send.Topic + n.Status = api.NOTIFICATION_STATUS_SENDING + if subContact.Enabled == tristate.False { + continue + } + n.ContactType = subContact.Type + models.NotificationManager.TableSpec().Insert(context.Background(), &n) + driver := models.GetDriver(subContact.Type) + send.Receivers = api.SNotifyReceiver{ + Contact: subContact.Contact, + } + err = driver.Send(send) + if err != nil { + failedReasons = append(failedReasons, errors.Wrapf(err, "content type:%s,receiver:%s", subContact.Type, subContact.ReceiverID).Error()) + } + } + if len(idFailedMap[id]) == 0 { + + } + } + return nil +} + +func createReceiverNotification(receiverIds []string, recvs []models.SReceiver, n models.SNotification) { + +} diff --git a/pkg/notify/tasks/verification_send_task.go b/pkg/notify/tasks/verification_send_task.go index d997343288..5437d880fa 100644 --- a/pkg/notify/tasks/verification_send_task.go +++ b/pkg/notify/tasks/verification_send_task.go @@ -24,9 +24,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" - notifyv2 "yunion.io/x/onecloud/pkg/notify" "yunion.io/x/onecloud/pkg/notify/models" - "yunion.io/x/onecloud/pkg/notify/rpc/apis" "yunion.io/x/onecloud/pkg/util/logclient" ) @@ -52,7 +50,6 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone self.taskFailed(ctx, receiver, fmt.Sprintf("VerificationManager.Get for receiver_id %q and contact_type %q: %s", receiver.GetId(), contactType, err.Error())) return } - contact, err := receiver.GetContact(contactType) if err != nil { self.taskFailed(ctx, receiver, fmt.Sprintf("fail to get contact(type: %s): %s", contactType, err.Error())) return @@ -78,7 +75,7 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone } message = jsonutils.Marshal(data).String() case api.MOBILE: - message = fmt.Sprintf(`[["code", "%s"]]`, verification.Token) + message = fmt.Sprintf("[\"%s\"]", verification.Token) default: // no way } @@ -86,8 +83,7 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone if err != nil { self.taskFailed(ctx, receiver, fmt.Sprintf("unable to GetTemplateLang for receiver %q: %v", receiver.Id, err)) } - - param, err := models.TemplateManager.FillWithTemplate(ctx, tLang, notifyv2.SNotification{ + param, err := models.TemplateManager.FillWithTemplate(ctx, tLang, api.SsNotification{ ContactType: contactType, Topic: "VERIFY", Message: message, @@ -96,11 +92,13 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone self.taskFailed(ctx, receiver, err.Error()) return } - param.Receiver = &apis.SReceiver{ - Contact: contact, + param.Receivers = api.SNotifyReceiver{ + Contact: receiver.Mobile, DomainId: receiver.DomainId, } - err = models.NotifyService.Send(ctx, contactType, param) + driver := models.GetDriver(contactType) + err = driver.Send(param) + // err = models.NotifyService.Send(ctx, contactType, param) if err != nil { self.taskFailed(ctx, receiver, err.Error()) return diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 8ce4c8e948..443f0c7255 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -240,4 +240,6 @@ const ( ACT_PANIC = "panic" ACT_IP_MAC_BIND = "ip_mac_bind" + // 程序内初始化notifyconfigmap错误 + ACT_INIT_NOTIFY_CONFIGMAP = "init_notify_configmap" ) diff --git a/vendor/github.com/hugozhu/godingtalk/.gitignore b/vendor/github.com/hugozhu/godingtalk/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/hugozhu/godingtalk/LICENSE b/vendor/github.com/hugozhu/godingtalk/LICENSE new file mode 100644 index 0000000000..11e9f51d7e --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Hugo Zhu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/hugozhu/godingtalk/README.md b/vendor/github.com/hugozhu/godingtalk/README.md new file mode 100644 index 0000000000..abc99c9e5a --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/README.md @@ -0,0 +1,108 @@ +# DingTalk Open API golang SDK + +![image](http://static.dingtalk.com/media/lALOAQ6nfSvM5Q_229_43.png) + +Check out DingTalk Open API document at: https://ding-doc.dingtalk.com/ + +## Usage + +Fetch the SDK +``` +export GOPATH=`pwd` +go get github.com/hugozhu/godingtalk +``` + +### Example code to send a micro app message + +``` +package main + +import ( + "github.com/hugozhu/godingtalk" + "log" + "os" +) + +func main() { + c := godingtalk.NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret")) + c.RefreshAccessToken() + err := c.SendAppMessage(os.Args[1], os.Args[2], os.Args[3]) + if err != nil { + log.Println(err) + } +} +``` + + +## Guide + +Step-by-step Guide to use this SDK + +http://hugozhu.myalert.info/2016/05/02/66-use-dingtalk-golang-sdk-to-send-message-on-pi.html + +## Tools + +**ding_alert** : Command line tool to send app/text/oa ... messages + +``` +export GOPATH=`pwd` +go get github.com/hugozhu/godingtalk/demo/ding_alert + +export corpid=<组织的corpid 通过 https://oa.dingtalk.com 获取> +export corpsecret=<组织的corpsecret 通过 https://oa.dingtalk.com 获取> + +./bin/ding_alert +Usage of ./bin/ding_alert: + -agent string + agent Id (default "22194403") + -chat string + chat id (default "chat6a93bc1ee3b7d660d372b1b877a9de62") + -file string + file path for media message + -link string + link url (default "http://hugozhu.myalert.info/dingtalk") + -sender string + sender id (default "011217462940") + -text string + text for link message (default "This is link text") + -title string + title for link message (default "This is link title") + -touser string + touser id (default "0420506555") + -type string + message type (app, text, image, voice, link, oa) (default "app") + +``` + +**github**: Deliver Github webhook events to DingTalk, which can be deployed on Google AppEngine. + +more info at: http://hugozhu.myalert.info/2016/05/15/67-use-free-google-cloud-service-to-deliver-github-webhook-events-to-dingtalk.html + +``` +export GOPATH=`pwd` +go get github.com/hugozhu/godingtalk/demo/github/appengine +``` + +Modify `app.yaml` + +``` +cd src/github.com/hugozhu/godingtalk/demo/github/appengine +cat app.yaml +application: github-alert- +version: 1 +runtime: go +api_version: go1 +env_variables: + CORP_ID: '<从 http://oa.dingtalk.com 获取>' + CORP_SECRET: '<从 http://oa.dingtalk.com 获取>' + GITHUB_WEBHOOK_SECRET: '<从 http://github.com/ 获取>' + SENDER_ID: '<从 http://open.dingtalk.com 调用api获取>' + CHAT_ID: '<从 http://open.dingtalk.com 调用api获取>' +handlers: +- url: /.* + script: _go_app + +``` + + + diff --git a/vendor/github.com/hugozhu/godingtalk/api_attendance.go b/vendor/github.com/hugozhu/godingtalk/api_attendance.go new file mode 100644 index 0000000000..3954b29685 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_attendance.go @@ -0,0 +1,104 @@ +/* + * Author Kevin Zhu + * + * Direct questions, comments to + */ + +package godingtalk + +import ( + "errors" + "time" +) + +type Attendance struct { + GmtModifed int64 `json:"gmtModified"` //: 1492594486000, + IsLegal string `json:"isLegal"` //: "N", + BaseCheckTime int64 `json:"baseCheckTime"` //: 1492568460000, + ID int64 `json:"id"` //: 933202551, + UserAddress string `json:"userAddress"` //: "北京市朝阳区崔各庄镇阿里中心.望京A座阿里巴巴绿地中心", + UID string `json:"userId"` //: "manager7078", + CheckType string `json:"checkType"` //: "OnDuty", + TimeResult string `json:"timeResult"` //: "Normal", + DeviceID string `json:"deviceId"` // :"cb7ace07d52fe9be14f4d8bec5e1ba79" + CorpID string `json:"corpId"` //: "ding7536bfee6fb1fa5a35c2f4657eb6378f", + SourceType string `json:"sourceType"` //: "USER", + WorkDate int64 `json:"workDate"` //: 1492531200000, + PlanCheckTime int64 `json:"planCheckTime"` //: 1492568497000, + GmtCreate int64 `json:"gmtCreate"` //: 1492594486000, + LocaltionMethod string `json:"locationMethod"` //: "MAP", + LocationResult string `json:"locationResult"` //: "Outside", + UserLongitude float64 `json:"userLongitude"` //: 116.486888, + PlanID int `json:"planId"` //: 4550269081, + GroupID int `json:"groupId"` //: 121325603, + UserAccuracy int `json:"userAccuracy"` //: 65, + UserCheckTime int64 `json:"userCheckTime"` //: 1492568497000, + UserLatitude float64 `json:"userLatitude"` //: 39.999946, + ProcInstID string `json:"procInstId"` //: "cb992267-9b70" + ApproveID int `json:"approveId"` // string, `json:""`//关联的审批id + ClassId int `json:"classId"` //考勤班次id,没有的话表示该次打卡不在排班内 + UserSsid string `json:"userSsid"` //用户打卡wifi SSID + UserMacAddr string `json:"userMacAddr"` //用户打卡wifi Mac地址 + BaseAddress string `json:"baseAddress"` //基准地址 + BaseLongitude float32 `json:"baseLongitude"` // 基准经度 + BaseLatitude float32 `json:"baseLatitude"` // 基准纬度 + BaseAccuracy int `json:"baseAccuracy"` // 基准定位精度 + BaseSsid string `json:"baseSsid"` //基准wifi ssid + BaseMacAddr string `json:"baseMacAddr"` //基准 Mac 地址 + OutsideRemark string `json:"outsideRemark"` //打卡备注 +} + +type listAttendanceRecordResp struct { + OAPIResponse + Records []Attendance `json:"recordresult"` +} + +// 获取所有的打卡记录,该员工当天如果打卡10条,那么10条都将返回 +func (c *DingTalkClient) ListAttendanceRecord(ulist []string, dateFrom time.Time, dateTo time.Time) ([]Attendance, error) { + var resp listAttendanceRecordResp + if len(ulist) > 50 || len(ulist) < 1 { + return nil, errors.New("Users can't more than 50 or less than 1") + } + + if !dateFrom.Before(dateTo) { + return nil, errors.New("FromDate must before ToDate") + } + if time.Duration(dateTo.UnixNano()-dateFrom.UnixNano()).Hours() > float64(7*24) { + return nil, errors.New("Can't more than 6 days at once") + } + + request := map[string]interface{}{ + "checkDateFrom": dateFrom.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss", + "checkDateTo": dateTo.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss", + "userIds": ulist, // 企业内的员工id列表,最多不能超过50个 + } + return resp.Records, c.httpRPC("/attendance/listRecord", nil, request, &resp) +} + +type listAttendanceResultResp struct { + OAPIResponse + HasMore bool `json:"hasMore"` + Records []Attendance `json:"recordresult"` +} + +// 即使员工在这期间打了多次,该接口也只会返回两条记录,包括上午的打卡结果和下午的打卡结果 +// 用户如果为空则获取所有用户 +func (c *DingTalkClient) ListAttendanceResult(ulist []string, dateFrom, dateTo time.Time, offset, lmt int64) (listAttendanceResultResp, error) { + var resp listAttendanceResultResp + if time.Duration(dateTo.UnixNano()-dateFrom.UnixNano()).Hours() > float64(7*24) { + return resp, errors.New("Can't more than 7 days at once") + } + + if !dateFrom.Before(dateTo) { + return resp, errors.New("FromDate must before ToDate") + } + + request := map[string]interface{}{ + "workDateFrom": dateFrom.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss", + "workDateTo": dateTo.Format("2006-01-02 15:04:05"), // "yyyy-MM-dd hh:mm:ss", + "userIdList": ulist, // ["员工UserId列表"], 必填,与offset和limit配合使用,不传表示分页获取全员的数据 + "offset": offset, // 必填,第一次传0,如果还有多余数据,下次传之前的offset加上limit的值 + "limit": lmt, // 最多50 + } + return resp, c.httpRPC("/attendance/list", nil, request, &resp) +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_calendar.go b/vendor/github.com/hugozhu/godingtalk/api_calendar.go new file mode 100644 index 0000000000..1f32f1d8fd --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_calendar.go @@ -0,0 +1,59 @@ +package godingtalk + +import "time" + +type Event struct { + OAPIResponse + Id string + Location string + Summary string + Description string + Start struct { + DateTime string `json:"date_time"` + } + End struct { + DateTime string `json:"date_time"` + } +} + +type ListEventsResponse struct { + OAPIResponse + Success bool `json:"success"` + Result struct { + Events []Event `json:"items"` + Summary string `json:"summary"` + NextPageToken string `json:"next_page_token"` + } `json:"result"` +} +type CalendarTime struct { + TimeZone string `json:"time_zone"` + Date string `json:"date_time"` +} + +type CalendarRequest struct { + TimeMax CalendarTime `json:"time_max"` + TimeMin CalendarTime `json:"time_min"` + StaffId string `json:"user_id"` +} + +func (c *DingTalkClient) ListEvents(staffid string, from time.Time, to time.Time) (events []Event, err error) { + location := time.Now().Location().String() + timeMin := CalendarTime{ + TimeZone: location, + Date: from.Format("2006-01-02T15:04:05Z0700"), + } + timeMax := CalendarTime{ + TimeZone: location, + Date: to.Format("2006-01-02T15:04:05Z0700"), + } + + data := CalendarRequest{ + TimeMax: timeMax, + TimeMin: timeMin, + StaffId: staffid, + } + var resp ListEventsResponse + err = c.httpRPC("topapi/calendar/list", nil, data, &resp) + events = resp.Result.Events + return events, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_callback.go b/vendor/github.com/hugozhu/godingtalk/api_callback.go new file mode 100644 index 0000000000..85f6a44506 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_callback.go @@ -0,0 +1,49 @@ +package godingtalk + +type Callback struct { + OAPIResponse + Token string + AES_KEY string `json:"aes_key"` + URL string + Callbacks []string `json:"call_back_tag"` +} + +//RegisterCallback is 注册事件回调接口 +func (c *DingTalkClient) RegisterCallback(callbacks []string, token string, aes_key string, callbackURL string) error { + var data OAPIResponse + request := map[string]interface{}{ + "call_back_tag": callbacks, + "token": token, + "aes_key": aes_key, + "url": callbackURL, + } + err := c.httpRPC("call_back/register_call_back", nil, request, &data) + return err +} + +//UpdateCallback is 更新事件回调接口 +func (c *DingTalkClient) UpdateCallback(callbacks []string, token string, aes_key string, callbackURL string) error { + var data OAPIResponse + request := map[string]interface{}{ + "call_back_tag": callbacks, + "token": token, + "aes_key": aes_key, + "url": callbackURL, + } + err := c.httpRPC("call_back/update_call_back", nil, request, &data) + return err +} + +//DeleteCallback is 删除事件回调接口 +func (c *DingTalkClient) DeleteCallback() error { + var data OAPIResponse + err := c.httpRPC("call_back/delete_call_back", nil, nil, &data) + return err +} + +//ListCallback is 查询事件回调接口 +func (c *DingTalkClient) ListCallback() (Callback, error) { + var data Callback + err := c.httpRPC("call_back/get_call_back", nil, nil, &data) + return data, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_contact.go b/vendor/github.com/hugozhu/godingtalk/api_contact.go new file mode 100644 index 0000000000..39aacd49c3 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_contact.go @@ -0,0 +1,146 @@ +package godingtalk + +import ( + "fmt" + "net/url" +) + +type User struct { + OAPIResponse + Userid string + Name string + Mobile string + Tel string + Remark string + Order int + IsAdmin bool + IsBoss bool + IsLeader bool + Active bool + Department []int + Position string + Email string + OrgEmail string + Avatar string + Extattr interface{} +} + +type UserList struct { + OAPIResponse + HasMore bool + Userlist []User +} + +type Department struct { + OAPIResponse + Id int + Name string + ParentId int + Order int + DeptPerimits string + UserPerimits string + OuterDept bool + OuterPermitDepts string + OuterPermitUsers string + OrgDeptOwner string + DeptManagerUseridList string +} + +type DepartmentList struct { + OAPIResponse + Departments []Department `json:"department"` +} + +// DepartmentList is 获取部门列表 +func (c *DingTalkClient) DepartmentList() (DepartmentList, error) { + var data DepartmentList + err := c.httpRPC("department/list", nil, nil, &data) + return data, err +} + +//DepartmentDetail is 获取部门详情 +func (c *DingTalkClient) DepartmentDetail(id int) (Department, error) { + var data Department + params := url.Values{} + params.Add("id", fmt.Sprintf("%d", id)) + err := c.httpRPC("department/get", params, nil, &data) + return data, err +} + +//UserList is 获取部门成员 +func (c *DingTalkClient) UserList(departmentID, offset, size int) (UserList, error) { + var data UserList + if size > 100 { + return data, fmt.Errorf("size 最大100") + } + + params := url.Values{} + params.Add("department_id", fmt.Sprintf("%d", departmentID)) + params.Add("offset", fmt.Sprintf("%d", offset)) + params.Add("size", fmt.Sprintf("%d", size)) + err := c.httpRPC("user/list", params, nil, &data) + return data, err +} + +//CreateChat is +func (c *DingTalkClient) CreateChat(name string, owner string, useridlist []string) (string, error) { + var data struct { + OAPIResponse + Chatid string + } + request := map[string]interface{}{ + "name": name, + "owner": owner, + "useridlist": useridlist, + } + err := c.httpRPC("chat/create", nil, request, &data) + return data.Chatid, err +} + +//UserInfoByCode 校验免登录码并换取用户身份 +func (c *DingTalkClient) UserInfoByCode(code string) (User, error) { + var data User + params := url.Values{} + params.Add("code", code) + err := c.httpRPC("user/getuserinfo", params, nil, &data) + return data, err +} + +//UserInfoByUserId 获取用户详情 +func (c *DingTalkClient) UserInfoByUserId(userid string) (User, error) { + var data User + params := url.Values{} + params.Add("userid", userid) + err := c.httpRPC("user/get", params, nil, &data) + return data, err +} + +//UseridByUnionId 通过UnionId获取玩家Userid +func (c *DingTalkClient) UseridByUnionId(unionid string) (string, error) { + var data struct { + OAPIResponse + UserID string `json:"userid"` + } + + params := url.Values{} + params.Add("unionid", unionid) + err := c.httpRPC("user/getUseridByUnionid", params, nil, &data) + if err != nil { + return "", err + } + + return data.UserID, err +} + +//UseridByMobile 通过手机号获取Userid +func (c *DingTalkClient) UseridByMobile(mobile string) (string, error) { + var data struct { + OAPIResponse + UserID string `json:"userid"` + } + + params := url.Values{} + params.Add("mobile", mobile) + err := c.httpRPC("user/get_by_mobile", params, nil, &data) + return data.UserID, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_encryption.go b/vendor/github.com/hugozhu/godingtalk/api_encryption.go new file mode 100644 index 0000000000..712b019a3e --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_encryption.go @@ -0,0 +1,34 @@ +package godingtalk + +//DataMessage 服务端加密、解密消息 +type DataMessage struct { + OAPIResponse + Data string +} + + +//Encrypt is 服务端加密 +func (c *DingTalkClient) Encrypt(str string) (string, error) { + var data DataMessage + request := map[string]interface{}{ + "data": str, + } + err := c.httpRPC("encryption/encrypt", nil, request, &data) + if err!=nil { + return "", err + } + return data.Data, nil +} + +//Decrypt is 服务端解密 +func (c *DingTalkClient) Decrypt(str string) (string, error) { + var data DataMessage + request := map[string]interface{}{ + "data": str, + } + err := c.httpRPC("encryption/decrypt", nil, request, &data) + if err!=nil { + return "", err + } + return data.Data, nil +} \ No newline at end of file diff --git a/vendor/github.com/hugozhu/godingtalk/api_file.go b/vendor/github.com/hugozhu/godingtalk/api_file.go new file mode 100644 index 0000000000..04ac63cd02 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_file.go @@ -0,0 +1,38 @@ +package godingtalk + +import ( + "bytes" + "fmt" + "io" + "net/url" +) + +/** + * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.UeYQVr&treeId=172&articleId=104970&docType=1 + * TODO: not completed yet + **/ + +//FileResponse is +type FileResponse struct { + OAPIResponse + Code int + Msg string + UploadID string `json:"uploadid"` + Writer io.Writer +} + +func (f *FileResponse) getWriter() io.Writer { + return f.Writer +} + +//CreateFile is to create a new file in Ding Space +func (c *DingTalkClient) CreateFile(size int64) (file FileResponse, err error) { + buf := bytes.Buffer{} + file = FileResponse{ + Writer: &buf, + } + params := url.Values{} + params.Add("size", fmt.Sprintf("%d", size)) + err = c.httpRPC("file/upload/create", params, nil, &file) + return file, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_media.go b/vendor/github.com/hugozhu/godingtalk/api_media.go new file mode 100644 index 0000000000..633ee4fb0c --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_media.go @@ -0,0 +1,44 @@ +package godingtalk + +import ( + "io" + "net/url" + "time" +) + +//MediaResponse is +type MediaResponse struct { + OAPIResponse + Type string + MediaID string `json:"media_id"` + Writer io.Writer +} + +func (m *MediaResponse) getWriter() io.Writer { + return m.Writer +} + +//UploadMedia is to upload media file to DingTalk +func (c *DingTalkClient) UploadMedia(mediaType string, filename string, reader io.Reader) (media MediaResponse, err error) { + upload := UploadFile{ + FieldName: "media", + FileName: filename, + Reader: reader, + } + params := url.Values{} + params.Add("type", mediaType) + c.HTTPClient.Timeout = 120 * time.Second + err = c.httpRPC("media/upload", params, upload, &media) + return media, err +} + +//DownloadMedia is to download a media file from DingTalk +func (c *DingTalkClient) DownloadMedia(mediaID string, write io.Writer) error { + var data MediaResponse + data.Writer = write + params := url.Values{} + params.Add("media_id", mediaID) + c.HTTPClient.Timeout = 120 * time.Second + err := c.httpRPC("media/get", params, nil, &data) + return err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_message.go b/vendor/github.com/hugozhu/godingtalk/api_message.go new file mode 100644 index 0000000000..6882209822 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_message.go @@ -0,0 +1,259 @@ +package godingtalk + +import ( + "net/url" + "strconv" +) + +//SendAppMessage is 发送企业会话消息 +func (c *DingTalkClient) SendAppMessage(agentID string, touser string, msg string) error { + if agentID == "" { + agentID = c.AgentID + } + var data OAPIResponse + request := map[string]interface{}{ + "touser": touser, + "agentid": agentID, + "msgtype": "text", + "text": map[string]interface{}{ + "content": msg, + }, + } + err := c.httpRPC("message/send", nil, request, &data) + return err +} + +//SendAppOAMessage is 发送OA消息 +func (c *DingTalkClient) SendAppOAMessage(agentID string, touser string, msg OAMessage) error { + if agentID == "" { + agentID = c.AgentID + } + var data OAPIResponse + request := map[string]interface{}{ + "touser": touser, + "agentid": agentID, + "msgtype": "oa", + "oa": msg, + } + err := c.httpRPC("message/send", nil, request, &data) + return err +} + +// ActionCardMessage +func (c *DingTalkClient) SendOverAllActionCardMessage(agentID string, touser string, msg OverAllActionCardMessage) error { + if agentID == "" { + agentID = c.AgentID + } + var data OAPIResponse + request := map[string]interface{}{ + "touser": touser, + "agentid": agentID, + "msgtype": "action_card", + "action_card": msg, + } + err := c.httpRPC("message/send", nil, request, &data) + return err +} + +func (c *DingTalkClient) SendIndependentActionCardMessage(agentID string, touser string, msg IndependentActionCardMessage) error { + if agentID == "" { + agentID = c.AgentID + } + var data OAPIResponse + request := map[string]interface{}{ + "touser": touser, + "agentid": agentID, + "msgtype": "action_card", + "action_card": msg, + } + err := c.httpRPC("message/send", nil, request, &data) + return err +} + +//SendAppLinkMessage is 发送企业会话链接消息 +func (c *DingTalkClient) SendAppLinkMessage(agentID, touser string, title, text string, picUrl, url string) error { + if agentID == "" { + agentID = c.AgentID + } + var data OAPIResponse + request := map[string]interface{}{ + "touser": touser, + "agentid": agentID, + "msgtype": "link", + "link": map[string]string{ + "messageUrl": url, + "picUrl": picUrl, + "title": title, + "text": text, + }, + } + err := c.httpRPC("message/send", nil, request, &data) + return err +} + +//SendTextMessage is 发送普通文本消息 +func (c *DingTalkClient) SendTextMessage(sender string, cid string, msg string) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "text", + "text": map[string]interface{}{ + "content": msg, + }, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + +//SendImageMessage is 发送图片消息 +func (c *DingTalkClient) SendImageMessage(sender string, cid string, mediaID string) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "image", + "image": map[string]string{ + "media_id": mediaID, + }, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + +//SendVoiceMessage is 发送语音消息 +func (c *DingTalkClient) SendVoiceMessage(sender string, cid string, mediaID string, duration string) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "voice", + "voice": map[string]string{ + "media_id": mediaID, + "duration": duration, + }, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + +//SendFileMessage is 发送文件消息 +func (c *DingTalkClient) SendFileMessage(sender string, cid string, mediaID string) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "file", + "file": map[string]string{ + "media_id": mediaID, + }, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + +//SendLinkMessage is 发送链接消息 +func (c *DingTalkClient) SendLinkMessage(sender string, cid string, mediaID string, url string, title string, text string) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "link", + "link": map[string]string{ + "messageUrl": url, + "picUrl": mediaID, + "title": title, + "text": text, + }, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + + +// OverAllActionCardMessage 整体跳转ActionCard +type OverAllActionCardMessage struct { + Title string `json:"title"` + MarkDown string `json:"markdown"` + SingleTitle string `json:"single_title"` + SingleUrl string `json:"single_url"` +} + +// IndependentActionCardMessage 独立跳转ActionCard +type IndependentActionCardMessage struct { + Title string `json:"title"` + MarkDown string `json:"markdown"` + BtnOrientation string `json:"btn_orientation"` + BtnJsonList []ActionCardMessageBtnList `json:"btn_json_list"` +} + +type ActionCardMessageBtnList struct { + Title string `json:"title,omitempty"` + ActionUrl string `json:"action_url,omitempty"` +} + +func (m *IndependentActionCardMessage) AppendBtnItem(title string, action_url string) { + f := ActionCardMessageBtnList{Title: title, ActionUrl: action_url} + + if m.BtnJsonList == nil { + m.BtnJsonList = []ActionCardMessageBtnList{} + } + + m.BtnJsonList = append(m.BtnJsonList, f) +} + +//OAMessage is the Message for OA +type OAMessage struct { + URL string `json:"message_url"` + PcURL string `json:"pc_message_url"` + Head struct { + BgColor string `json:"bgcolor,omitempty"` + Text string `json:"text,omitempty"` + } `json:"head,omitempty"` + Body struct { + Title string `json:"title,omitempty"` + Form []OAMessageForm `json:"form,omitempty"` + Rich OAMessageRich `json:"rich,omitempty"` + Content string `json:"content,omitempty"` + Image string `json:"image,omitempty"` + FileCount int `json:"file_count,omitempty"` + Author string `json:"author,omitempty"` + } `json:"body,omitempty"` +} + +type OAMessageForm struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` +} + +type OAMessageRich struct { + Num string `json:"num,omitempty"` + Unit string `json:"body,omitempty"` +} + +func (m *OAMessage) AppendFormItem(key string, value string) { + f := OAMessageForm{Key: key, Value: value} + + if m.Body.Form == nil { + m.Body.Form = []OAMessageForm{} + } + + m.Body.Form = append(m.Body.Form, f) +} + +//SendOAMessage is 发送OA消息 +func (c *DingTalkClient) SendOAMessage(sender string, cid string, msg OAMessage) (data MessageResponse, err error) { + request := map[string]interface{}{ + "chatid": cid, + "sender": sender, + "msgtype": "oa", + "oa": msg, + } + err = c.httpRPC("chat/send", nil, request, &data) + return data, err +} + +//GetMessageReadList is 获取已读列表 +func (c *DingTalkClient) GetMessageReadList(messageID string, cursor int, size int) (data MessageReadListResponse, err error) { + params := url.Values{} + params.Add("messageId", messageID) + params.Add("cursor", strconv.Itoa(cursor)) + params.Add("size", strconv.Itoa(size)) + err = c.httpRPC("chat/getReadList", params, nil, &data) + return data, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_robot.go b/vendor/github.com/hugozhu/godingtalk/api_robot.go new file mode 100644 index 0000000000..76845cb5d8 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_robot.go @@ -0,0 +1,75 @@ +package godingtalk + +import ( + "net/url" +) + +type RobotAtList struct { + AtMobiles []string `json:"atMobiles"` + IsAtAll bool `json:"isAtAll"` +} + +type RobotOutgoingMessage struct { + MessageType string `json:"msgtype"` + Text struct { + Content string `json:"content,omitempty"` + } `json:"text,omitempty"` + MessageID string `json:"msgId"` + CreatedAt int64 `json:"createAt"` + ConversationID string `json:"conversationId"` + ConversationType string `json:"conversationType"` + ConversationTitle string `json:"conversationTitle"` + SenderID string `json:"senderId"` + SenderNick string `json:"senderNick"` + SenderCorpID string `json:"senderCorpId"` + SenderStaffID string `json:"senderStaffId"` + ChatbotUserID string `json:"chatbotUserId"` + AtUsers []struct { + DingTalkID string `json:"dingtalkId,omitempty"` + StaffID string `json:"staffId,omitempty"` + } `json:"atUsers,omitempty"` +} + +//SendRobotTextMessage can send a text message to a group chat +func (c *DingTalkClient) SendRobotTextMessage(accessToken string, msg string) (data MessageResponse, err error) { + params := url.Values{} + params.Add("access_token", accessToken) + request := map[string]interface{}{ + "msgtype": "text", + "text": map[string]interface{}{ + "content": msg, + }, + } + err = c.httpRPC("robot/send", params, request, &data) + return data, err +} + +//SendRobotMarkdownMessage can send a text message to a group chat +func (c *DingTalkClient) SendRobotMarkdownMessage(accessToken string, title string, msg string) (data MessageResponse, err error) { + params := url.Values{} + params.Add("access_token", accessToken) + request := map[string]interface{}{ + "msgtype": "markdown", + "markdown": map[string]interface{}{ + "title": title, + "text": msg, + }, + } + err = c.httpRPC("robot/send", params, request, &data) + return data, err +} + +// SendRobotTextAtMessage can send a text message and at user to a group chat +func (c *DingTalkClient) SendRobotTextAtMessage(accessToken string, msg string, at *RobotAtList) (data OAPIResponse, err error) { + params := url.Values{} + params.Add("access_token", accessToken) + request := map[string]interface{}{ + "msgtype": "text", + "text": map[string]interface{}{ + "content": msg, + }, + "at": at, + } + err = c.httpRPC("robot/send", params, request, &data) + return data, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/api_sns.go b/vendor/github.com/hugozhu/godingtalk/api_sns.go new file mode 100644 index 0000000000..502d62af5d --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/api_sns.go @@ -0,0 +1,109 @@ +//普通钉钉用户账号开放相关接口 +package godingtalk + +import( + "net/url" +) + +//获取钉钉开放应用ACCESS_TOKEN +//TODO: +// 根据和赤司(钉钉开发者)的沟通,ACCESS_TOKEN只有两个小时的有效期 +// 但是目前接口貌似没有返回过期时间相关的信息,因此所有相关的调用都需要强制刷新 +func (c *DingTalkClient) RefreshSnsAccessToken() error { + var data AccessTokenResponse + + params := url.Values{} + params.Add("appid", c.SnsAppID) + params.Add("appsecret", c.SnsAppSecret) + + err := c.httpRPC("sns/gettoken", params, nil, &data) + if err==nil { + c.SnsAccessToken = data.AccessToken + } + return err +} + +//获取用户授权的持久授权码返回信息 +type SnsPersistentCodeResponse struct { + OAPIResponse + UnionID string `json:"unionid"` + OpenID string `json:"openid"` + PersistentCode string `json:"persistent_code"` +} + +//获取用户授权的持久授权码 +func (c *DingTalkClient) GetSnsPersistentCode(tmpAuthCode string) (string, string, string, error) { + c.RefreshSnsAccessToken() + + params := url.Values{} + params.Add("access_token", c.SnsAccessToken) + + request := map[string]interface{}{ + "tmp_auth_code": tmpAuthCode, + } + + var data SnsPersistentCodeResponse + err := c.httpRequest("sns/get_persistent_code", params, request, &data) + if err!=nil { + return "","","",err + } + return data.UnionID, data.OpenID, data.PersistentCode, nil +} + + +type SnsTokenResponse struct { + OAPIResponse + Expires int `json:"expires_in"` + SnsToken string `json:"sns_token"` +} + +//获取用户授权的SNS_TOKEN +func (c *DingTalkClient) GetSnsToken(openid, persistentCode string) (string, error) { + c.RefreshSnsAccessToken() + + params := url.Values{} + params.Add("access_token", c.SnsAccessToken) + + request := map[string]interface{}{ + "openid": openid, + "persistent_code": persistentCode, + } + + var data SnsTokenResponse + err := c.httpRequest("sns/get_sns_token", params, request, &data) + if err!=nil { + return "", err + } + return data.SnsToken, err +} + +type SnsUserInfoResponse struct { + OAPIResponse + + CorpInfo []struct{ + CorpName string `json:"corp_name"` + IsAuth bool `json:"is_auth"` + IsManager bool `json:"is_manager"` + RightsLevel int `json:"rights_level"` + } `json:"corp_info"` + + UserInfo struct { + MaskedMobile string `json:"marskedMobile"` + Nick string `json:"nick"` + OpenID string `json:"openid"` + UnionID string `json:"unionid"` + DingID string `json:"dingId"` + } `json:"user_info"` +} + +//获取用户授权的个人信息 +func (c *DingTalkClient) GetSnsUserInfo(snsToken string) (SnsUserInfoResponse, error) { + c.RefreshSnsAccessToken() + + params := url.Values{} + params.Add("sns_token", snsToken) + + var data SnsUserInfoResponse + err := c.httpRequest("sns/getuserinfo", params, nil, &data) + return data, err +} diff --git a/vendor/github.com/hugozhu/godingtalk/crypto.go b/vendor/github.com/hugozhu/godingtalk/crypto.go new file mode 100644 index 0000000000..260a6448a7 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/crypto.go @@ -0,0 +1,164 @@ +package godingtalk + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/binary" + "errors" + "math/rand" + r "math/rand" + "sort" + "time" +) + +const ( + AES_ENCODE_KEY_LENGTH = 43 +) + +var DefaultDingtalkCrypto *Crypto + +type Crypto struct { + Token string + AesKey string + SuiteKey string + block cipher.Block + bkey []byte +} + +/* + token 数据签名需要用到的token,ISV(服务提供商)推荐使用注册套件时填写的token,普通企业可以随机填写 + aesKey 数据加密密钥。用于回调数据的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成,ISV(服务提供商)推荐使用注册套件时填写的EncodingAESKey + suiteKey 一般使用corpID +*/ +func NewCrypto(token, aesKey, suiteKey string) (c *Crypto) { + c = &Crypto{ + Token: token, + AesKey: aesKey, + SuiteKey: suiteKey, + } + if len(c.AesKey) != AES_ENCODE_KEY_LENGTH { + panic("不合法的aeskey") + } + var err error + c.bkey, err = base64.StdEncoding.DecodeString(aesKey + "=") + if err != nil { + panic(err.Error()) + } + c.block, err = aes.NewCipher(c.bkey) + if err != nil { + panic(err.Error()) + } + return c +} + +/* + signature: 签名字符串 + timeStamp: 时间戳 + nonce: 随机字符串 + secretStr: 密文 + 返回: 解密后的明文 +*/ +func (c *Crypto) DecryptMsg(signature, timeStamp, nonce, secretStr string) (string, error) { + if !c.VerifySignature(c.Token, timeStamp, nonce, secretStr, signature) { + return "", errors.New("签名不匹配") + } + decode, err := base64.StdEncoding.DecodeString(secretStr) + if err != nil { + return "", err + } + if len(decode) < aes.BlockSize { + return "", errors.New("密文太短啦") + } + blockMode := cipher.NewCBCDecrypter(c.block, c.bkey[:c.block.BlockSize()]) + plantText := make([]byte, len(decode)) + blockMode.CryptBlocks(plantText, decode) + plantText = PKCS7UnPadding(plantText) + size := binary.BigEndian.Uint32(plantText[16 : 16+4]) + plantText = plantText[16+4:] + cropid := plantText[size:] + if string(cropid) != c.SuiteKey { + return "", errors.New("CropID不正确") + } + return string(plantText[:size]), nil +} + +func PKCS7UnPadding(plantText []byte) []byte { + length := len(plantText) + unpadding := int(plantText[length-1]) + return plantText[:(length - unpadding)] +} + +/* + replyMsg: 明文字符串 + timeStamp: 时间戳 + nonce: 随机字符串 + 返回: 密文,签名字符串 +*/ +func (c *Crypto) EncryptMsg(replyMsg, timeStamp, nonce string) (string, string, error) { + //原生消息体长度 + size := make([]byte, 4) + binary.BigEndian.PutUint32(size, uint32(len(replyMsg))) + replyMsg = c.RandomString(16) + string(size) + replyMsg + c.SuiteKey + plantText := PKCS7Padding([]byte(replyMsg), c.block.BlockSize()) + if len(plantText)%aes.BlockSize != 0 { + return "", "", errors.New("消息体大小不为16的倍数") + } + + blockMode := cipher.NewCBCEncrypter(c.block, c.bkey[:c.block.BlockSize()]) + ciphertext := make([]byte, len(plantText)) + blockMode.CryptBlocks(ciphertext, plantText) + outStr := base64.StdEncoding.EncodeToString(ciphertext) + sigStr := c.GenerateSignature(c.Token, timeStamp, nonce, string(outStr)) + return string(outStr), sigStr, nil +} + +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// 数据签名 +func (c *Crypto) GenerateSignature(token, timeStamp, nonce, secretStr string) string { + // 先将参数值进行排序 + params := make([]string, 0) + params = append(params, token) + params = append(params, secretStr) + params = append(params, timeStamp) + params = append(params, nonce) + sort.Strings(params) + return sha1Sign(params[0] + params[1] + params[2] + params[3]) +} + +// 校验数据签名 +func (c *Crypto) VerifySignature(token, timeStamp, nonce, secretStr, sigture string) bool { + return c.GenerateSignature(token, timeStamp, nonce, secretStr) == sigture +} + +func (c *Crypto) RandomString(n int, alphabets ...byte) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + var randby bool + if num, err := rand.Read(bytes); num != n || err != nil { + r.Seed(time.Now().UnixNano()) + randby = true + } + for i, b := range bytes { + if len(alphabets) == 0 { + if randby { + bytes[i] = alphanum[r.Intn(len(alphanum))] + } else { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + } else { + if randby { + bytes[i] = alphabets[r.Intn(len(alphabets))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } + } + } + return string(bytes) +} diff --git a/vendor/github.com/hugozhu/godingtalk/godingtalk.go b/vendor/github.com/hugozhu/godingtalk/godingtalk.go new file mode 100644 index 0000000000..7bd65ec79b --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/godingtalk.go @@ -0,0 +1,189 @@ +package godingtalk + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +const ( + //VERSION is SDK version + VERSION = "0.3" +) + +//DingTalkClient is the Client to access DingTalk Open API +type DingTalkClient struct { + CorpID string + CorpSecret string + + AgentID string + PartnerID string + AccessToken string + HTTPClient *http.Client + Cache Cache + + //社交相关的属性 + SnsAppID string + SnsAppSecret string + SnsAccessToken string +} + +//Unmarshallable is +type Unmarshallable interface { + checkError() error + getWriter() io.Writer +} + +//OAPIResponse is +type OAPIResponse struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +func (data *OAPIResponse) checkError() (err error) { + if data.ErrCode != 0 { + err = fmt.Errorf("%d: %s", data.ErrCode, data.ErrMsg) + } + return err +} + +func (data *OAPIResponse) getWriter() io.Writer { + return nil +} + +//MessageResponse is +type MessageResponse struct { + OAPIResponse + MessageID string `json:"messageId"` +} + +//MessageResponse is +type MessageReadListResponse struct { + OAPIResponse + NextCursor int64 `json:"next_cursor"` + ReadUserIdList []string `json:"readUserIdList"` +} + +//AccessTokenResponse is +type AccessTokenResponse struct { + OAPIResponse + AccessToken string `json:"access_token"` + Expires int `json:"expires_in"` + Created int64 +} + +//CreatedAt is when the access token is generated +func (e *AccessTokenResponse) CreatedAt() int64 { + return e.Created +} + +//ExpiresIn is how soon the access token is expired +func (e *AccessTokenResponse) ExpiresIn() int { + return e.Expires +} + +//JsAPITicketResponse is +type JsAPITicketResponse struct { + OAPIResponse + Ticket string + Expires int `json:"expires_in"` + Created int64 +} + +//CreatedAt is when the ticket is generated +func (e *JsAPITicketResponse) CreatedAt() int64 { + return e.Created +} + +//ExpiresIn is how soon the ticket is expired +func (e *JsAPITicketResponse) ExpiresIn() int { + return e.Expires +} + +//NewDingTalkClient creates a DingTalkClient instance +func NewDingTalkClient(corpID string, corpSecret string) *DingTalkClient { + c := new(DingTalkClient) + c.CorpID = corpID + c.CorpSecret = corpSecret + c.HTTPClient = &http.Client{ + Timeout: 30 * time.Second, + } + c.Cache = NewFileCache(".auth_file") + return c +} + +//RefreshAccessToken is to get a valid access token +func (c *DingTalkClient) RefreshAccessToken(p ...interface{}) error { + var data AccessTokenResponse + err := c.Cache.Get(&data) + if err == nil { + c.AccessToken = data.AccessToken + return nil + } + + params := url.Values{} + + useAppKey := false + + if len(p) > 0 { + useAppKey = p[0].(bool) + } + + if useAppKey { + params.Add("corpid", c.CorpID) + params.Add("corpsecret", c.CorpSecret) + } else { + params.Add("appkey", c.CorpID) + params.Add("appsecret", c.CorpSecret) + } + + err = c.httpRPC("gettoken", params, nil, &data) + if err == nil { + c.AccessToken = data.AccessToken + data.Expires = data.Expires | 7200 + data.Created = time.Now().Unix() + err = c.Cache.Set(&data) + } + return err +} + +//GetJsAPITicket is to get a valid ticket for JS API +func (c *DingTalkClient) GetJsAPITicket() (ticket string, err error) { + var data JsAPITicketResponse + cache := NewFileCache(".jsapi_ticket") + err = cache.Get(&data) + if err == nil { + return data.Ticket, err + } + err = c.httpRPC("get_jsapi_ticket", nil, nil, &data) + if err == nil { + ticket = data.Ticket + cache.Set(&data) + } + return ticket, err +} + +//GetConfig is to return config in json +func (c *DingTalkClient) GetConfig(nonceStr string, timestamp string, url string) string { + ticket, _ := c.GetJsAPITicket() + config := map[string]string{ + "url": url, + "nonceStr": nonceStr, + "agentId": c.AgentID, + "timeStamp": timestamp, + "corpId": c.CorpID, + "ticket": ticket, + "signature": Sign(ticket, nonceStr, timestamp, url), + } + bytes, _ := json.Marshal(&config) + return string(bytes) +} + +//Sign is 签名 +func Sign(ticket string, nonceStr string, timeStamp string, url string) string { + s := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s", ticket, nonceStr, timeStamp, url) + return sha1Sign(s) +} diff --git a/vendor/github.com/hugozhu/godingtalk/top_api_approval.go b/vendor/github.com/hugozhu/godingtalk/top_api_approval.go new file mode 100644 index 0000000000..6d101cbd50 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/top_api_approval.go @@ -0,0 +1,187 @@ +/* + * Author Kevin Zhu + * + * Direct questions, comments to + */ + +package godingtalk + +import ( + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + topAPICreateProcInstMethod = "dingtalk.smartwork.bpms.processinstance.create" + topAPIGetProcInstMethod = "dingtalk.smartwork.bpms.processinstance.get" + topAPIListProcInstMethod = "dingtalk.smartwork.bpms.processinstance.list" +) + +type TopAPICreateProcInst struct { + // 审批模板code + ProcessCode string `json:"process_code"` + // 发起人UID + OriginatorUID string `json:"originator_user_id"` + // 发起人所在部门 + DeptID int `json:"dept_id"` + // 审批人列表 + Approvers []string `json:"approvers"` + // 抄送人列表 + CCList []string `json:"cc_list"` + //抄送时间,分为(START,FINISH,START_FINISH + CCPosition string `json:"cc_position"` + // 审批单内容, Name为审批模板中的列名, value 为该列的值 + FormCompntValues []ProcInstCompntValues `json:"form_component_values"` +} + +type ProcInst struct { + ProcInstID string `json:"process_instance_id"` + Title string `json:"title"` + CreateTime string `json:"create_time"` + FinishTime string `json:"finish_time"` + OriginatorUID string `json:"originator_userid"` + Status string `json:"status"` + ApproverUIDS []string `json:"approver_userids"` + CCUIDS []string `json:"cc_userids"` + Result string `json:"result"` + BusinessID string `json:"business_id"` + FormCompntValues []ProcInstCompntValues `json:"form_component_values"` // 表单详情列表 + Tasks []_ProcInstTasks `json:"tasks"` // 任务列表 + OperationRecords []_ProcInstOperationRecords `json:"operation_records"` // 操作记录列表 + OriginatorDeptID string `json:"originator_dept_id"` + OriginatorDeptName string `json:"originator_dept_name"` +} + +type ProcInstCompntValues struct { + Name string `json:"name"` + Value string `json:"value"` + ExtValue string `json:"ext_value"` +} + +type _ProcInstOperationRecords struct { + UID string `json:"userid"` + Date string `json:"date"` + Type string `json:"operation_type"` + Result string `json:"operation_result"` + Remark string `json:"remark"` +} + +type _ProcInstTasks struct { + UID string `json:"userid"` + Status string `json:"task_status"` + Result string `json:"task_result"` + CreateTime string `json:"create_time"` + FinishTime string `json:"finish_time"` +} + +type topAPICreateProcInstResp struct { + topAPIErrResponse + OK struct { + Errcode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + IsSuccess bool `jons:"is_success"` + ProcInstID string `json:"process_instance_id"` + } `json:"result"` + RequestID string `json:"request_id"` +} + +// 发起审批 +func (c *DingTalkClient) TopAPICreateProcInst(data TopAPICreateProcInst) (string, error) { + var resp topAPICreateProcInstResp + values, err := json.Marshal(data.FormCompntValues) + if err != nil { + return "", err + } + + form := url.Values{} + form.Add("method", topAPICreateProcInstMethod) + form.Add("cc_list", strings.Join(data.CCList, ",")) + form.Add("dept_id", strconv.Itoa(data.DeptID)) + form.Add("approvers", strings.Join(data.Approvers, ",")) + form.Add("cc_position", data.CCPosition) + form.Add("process_code", data.ProcessCode) + form.Add("originator_user_id", data.OriginatorUID) + form.Add("form_component_values", string(values)) + if c.AgentID != "" { + form.Add("agent_id", c.AgentID) + } + + return resp.OK.ProcInstID, c.topAPIRequest(form, &resp) +} + +type topAPIGetProcInstResp struct { + Ok struct { + ErrCode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + Success bool `json:"success"` + ProcInst ProcInst `json:"process_instance"` + } `json:"result"` + RequestID string `json:"request_id"` + topAPIErrResponse +} + +// 根据审批实例id获取单条审批实例详情 +func (c *DingTalkClient) TopAPIGetProcInst(pid string) (ProcInst, error) { + var resp topAPIGetProcInstResp + reqForm := url.Values{} + reqForm.Add("process_instance_id", pid) + reqForm.Add("method", topAPIGetProcInstMethod) + err := c.topAPIRequest(reqForm, &resp) + if err != nil { + return resp.Ok.ProcInst, err + } + resp.Ok.ProcInst.ProcInstID = pid + return resp.Ok.ProcInst, err +} + +type ListProcInst struct { + ApproverUIDS []string `json:"approver_userid_list"` + CCUIDS []string `json:"cc_userid_list"` + FormCompntValues []ProcInstCompntValues `json:"form_component_values"` + ProcInstID string `json:"process_instance_id"` + Title string `json:"title"` + CreateTime string `json:"create_time"` + FinishTime string `json:"finish_time"` + OriginatorUID string `json:"originator_userid"` + Status string `json:"status"` + BusinessID string `json:"business_id"` + OriginatorDeptID string `json:"originator_dept_id"` + ProcInstResult string `json:"process_instance_result"` // "agree", +} + +type TopAPIListProcInstResp struct { + topAPIErrResponse + OK struct { + ErrCode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + Success bool `json:"success"` + Result struct { + List []ListProcInst `json:"list"` + NextCursor int `json:"next_cursor"` + } `json:"result"` + } `json:"result"` + RequestID string `json:"request_id"` +} + +// 获取审批实例列表 +// Note: processCode 官方不会检查错误,请保证processCode正确 +func (c *DingTalkClient) TopAPIListProcInst(processCode string, startTime, endTime time.Time, size, cursor int, useridList []string) (TopAPIListProcInstResp, error) { + var resp TopAPIListProcInstResp + if size > 10 { + return resp, errors.New("Max size is 10") + } + + reqForm := url.Values{} + reqForm.Add("process_code", processCode) + reqForm.Add("start_time", strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10)) + reqForm.Add("end_time", strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)) + reqForm.Add("size", strconv.Itoa(size)) + reqForm.Add("cursor", strconv.Itoa(cursor)) + reqForm.Add("userid_list", strings.Join(useridList, ",")) + reqForm.Add("method", topAPIListProcInstMethod) + return resp, c.topAPIRequest(reqForm, &resp) +} diff --git a/vendor/github.com/hugozhu/godingtalk/top_api_message.go b/vendor/github.com/hugozhu/godingtalk/top_api_message.go new file mode 100644 index 0000000000..18788457ca --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/top_api_message.go @@ -0,0 +1,124 @@ +/* + * Author Kevin Zhu + * + * Direct questions, comments to + */ + +package godingtalk + +import ( + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" +) + +const ( + topAPIMsgAsyncSendMethod = "dingtalk.corp.message.corpconversation.asyncsend" + topAPIMsgGetResultMethod = "dingtalk.corp.message.corpconversation.getsendresult" + topAPIMsgGetprogressMethod = "dingtalk.corp.message.corpconversation.getsendprogress" +) + +type topAPIMsgSendResponse struct { + topAPIErrResponse + OK struct { + ErrCode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + Success bool `json:"success"` + TaskID int `json:"task_id"` + } `json:"result"` +} + +// mgType 消息类型:text;iamge;voice;file;link;oa;markdown;action_card +// userList 接收推送的UID 列表 +// deptList 接收推送的部门ID列表 +// toAll 是否发送给所有用户 +// msgContent 消息内容 +// If success return task_id, or is error is not nil when errored +func (c *DingTalkClient) TopAPIMsgSend(msgType string, userList []string, deptList []int, toAll bool, msgContent interface{}) (int, error) { + var resp topAPIMsgSendResponse + if len(userList) > 20 || len(deptList) > 20 { + return 0, errors.New("Can't more than 20 users or departments at once") + } + + mcontent, err := json.Marshal(msgContent) + if err != nil { + return 0, err + } + + toAllStr := "false" + if toAll { + toAllStr = "true" + } + + form := url.Values{ + "method": {topAPIMsgAsyncSendMethod}, + "agent_id": {c.AgentID}, + "userid_list": {strings.Join(userList, ",")}, + "to_all_user": {toAllStr}, + "msgtype": {msgType}, + "msgcontent": {string(mcontent)}, + } + + if len(deptList) > 0 { + var deptListStr string + for _, dept := range deptList { + deptListStr = strconv.Itoa(dept) + "," + } + deptListStr = string([]uint8(deptListStr)[0 : len(deptListStr)-1]) + form.Set("dept_id_list", deptListStr) + } + + return resp.OK.TaskID, c.topAPIRequest(form, &resp) +} + +type TopAPIMsgGetSendResult struct { + topAPIErrResponse + OK struct { + ErrCode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + Success bool `json:"success"` + SendResult struct { + InvalidUserIDList []string `json:"invalid_user_id_list"` + ForbiddenUserIDList []string `json:"forbidden_user_id_list"` + FaildedUserIDList []string `json:"failed_user_id_list"` + ReadUserIDLIst []string `json:"read_user_id_list"` + UnreadUserIDList []string `json:"unread_user_id_list"` + InvalidDeptIDList []int `json:"invalid_dept_id_list"` + } `json:"send_result"` + } `json:"result"` +} + +func (c *DingTalkClient) TopAPIMsgGetSendResult(taskID int) (TopAPIMsgGetSendResult, error) { + var resp TopAPIMsgGetSendResult + form := url.Values{ + "method": {topAPIMsgGetResultMethod}, + "agent_id": {c.AgentID}, + "task_id": {strconv.Itoa(taskID)}, + } + return resp, c.topAPIRequest(form, &resp) +} + +type TopAPIMsgGetSendProgress struct { + topAPIErrResponse + OK struct { + ErrCode int `json:"ding_open_errcode"` + ErrMsg string `json:"error_msg"` + Success bool `json:"success"` + Progress struct { + Percent int `json:"progress_in_percent"` + Status int `json:"status"` + } `json:"progress"` + } `json:"result"` +} + +func (c *DingTalkClient) TopAPIMsgGetSendProgress(taskID int) (TopAPIMsgGetSendProgress, error) { + var resp TopAPIMsgGetSendProgress + form := url.Values{ + "method": {topAPIMsgGetprogressMethod}, + "agent_id": {c.AgentID}, + "task_id": {strconv.Itoa(taskID)}, + } + return resp, c.topAPIRequest(form, &resp) +} diff --git a/vendor/github.com/hugozhu/godingtalk/top_api_request.go b/vendor/github.com/hugozhu/godingtalk/top_api_request.go new file mode 100644 index 0000000000..40e80e165e --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/top_api_request.go @@ -0,0 +1,88 @@ +/* + * Author Kevin Zhu + * + * Direct questions, comments to + */ + +package godingtalk + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "time" +) + +const ( + topAPIRootURL = "https://eco.taobao.com/router/rest" + formDataType = "application/x-www-form-urlencoded;charset=utf-8" +) + +type TopAPIResponse interface { + checkError() error +} + +type topAPIErrResponse struct { + ERR struct { + Code int `json:"code"` + Msg string `json:"msg"` + SubCode string `json:"sub_code"` + SubMsg string `json:"sub_msg"` + RequestID string `json:"request_id"` + } `json:"error_response"` +} + +func (data *topAPIErrResponse) checkError() (err error) { + if data.ERR.Code != 0 || len(data.ERR.SubCode) != 0 { + err = fmt.Errorf("%#v", data.ERR) + } + return err +} + +func (c *DingTalkClient) topAPIRequest(requestForm url.Values, respData TopAPIResponse) error { + requestForm.Set("v", "2.0") + requestForm.Set("format", "json") + requestForm.Set("simplify", "true") + + err := c.RefreshAccessToken() + if err != nil { + return err + } + requestForm.Set("session", c.AccessToken) + if requestForm.Get("timestamp") == "" { + requestForm.Set("timestamp", time.Now().Format("2006-01-02 15:04:05")) + } + if c.PartnerID != "" { + requestForm.Set("partner_id", c.PartnerID) + } + + v := bytes.NewBuffer([]byte(requestForm.Encode())) + + req, _ := http.NewRequest("POST", topAPIRootURL, v) + req.Header.Set("Content-Type", formDataType) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New("Server error: " + resp.Status) + } + + defer resp.Body.Close() + buf, err := ioutil.ReadAll(resp.Body) + + if err == nil { + err := json.Unmarshal(buf, &respData) + if err != nil { + return err + } + return respData.checkError() + } + return err +} diff --git a/vendor/github.com/hugozhu/godingtalk/transport.go b/vendor/github.com/hugozhu/godingtalk/transport.go new file mode 100644 index 0000000000..afbc594bbd --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/transport.go @@ -0,0 +1,119 @@ +package godingtalk + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "net/url" + "os" +) + +const typeJSON = "application/json" + +//UploadFile is for uploading a single file to DingTalk +type UploadFile struct { + FieldName string + FileName string + Reader io.Reader +} + +//DownloadFile is for downloading a single file from DingTalk +type DownloadFile struct { + MediaID string + FileName string + Reader io.Reader +} + +func (c *DingTalkClient) httpRPC(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error { + if c.AccessToken != "" { + if params == nil { + params = url.Values{} + } + if params.Get("access_token") == "" { + params.Set("access_token", c.AccessToken) + } + } + return c.httpRequest(path, params, requestData, responseData) +} + +func (c *DingTalkClient) httpRequest(path string, params url.Values, requestData interface{}, responseData Unmarshallable) error { + client := c.HTTPClient + var request *http.Request + ROOT := os.Getenv("oapi_server") + if ROOT == "" { + ROOT = "oapi.dingtalk.com" + } + DEBUG := os.Getenv("debug") != "" + url2 := "https://" + ROOT + "/" + path + "?" + params.Encode() + // log.Println(url2) + if requestData != nil { + switch requestData.(type) { + case UploadFile: + var b bytes.Buffer + w := multipart.NewWriter(&b) + + uploadFile := requestData.(UploadFile) + if uploadFile.Reader == nil { + return errors.New("upload file is empty") + } + fw, err := w.CreateFormFile(uploadFile.FieldName, uploadFile.FileName) + if err != nil { + return err + } + if _, err = io.Copy(fw, uploadFile.Reader); err != nil { + return err + } + if err = w.Close(); err != nil { + return err + } + request, _ = http.NewRequest("POST", url2, &b) + request.Header.Set("Content-Type", w.FormDataContentType()) + default: + d, _ := json.Marshal(requestData) + if DEBUG { + log.Printf("url: %s request: %s", url2, string(d)) + } + request, _ = http.NewRequest("POST", url2, bytes.NewReader(d)) + request.Header.Set("Content-Type", typeJSON) + } + } else { + if DEBUG { + log.Printf("url: %s", url2) + } + request, _ = http.NewRequest("GET", url2, nil) + } + resp, err := client.Do(request) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New("Server error: " + resp.Status) + } + + defer resp.Body.Close() + contentType := resp.Header.Get("Content-Type") + if DEBUG { + log.Printf("url: %s response content type: %s", url2, contentType) + } + pos := len(typeJSON) + if len(contentType) >= pos && contentType[0:pos] == typeJSON { + content, err := ioutil.ReadAll(resp.Body) + if DEBUG { + log.Println(string(content)) + } + if err == nil { + json.Unmarshal(content, responseData) + return responseData.checkError() + } + } else { + io.Copy(responseData.getWriter(), resp.Body) + return responseData.checkError() + } + return err +} diff --git a/vendor/github.com/hugozhu/godingtalk/util.go b/vendor/github.com/hugozhu/godingtalk/util.go new file mode 100644 index 0000000000..62523ca369 --- /dev/null +++ b/vendor/github.com/hugozhu/godingtalk/util.go @@ -0,0 +1,102 @@ +package godingtalk + +import ( + "crypto/sha1" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "time" +) + +type Expirable interface { + CreatedAt() int64 + ExpiresIn() int +} + +type Cache interface { + Set(data Expirable) error + Get(data Expirable) error +} + +type FileCache struct { + Path string +} + +func NewFileCache(path string) *FileCache { + return &FileCache{ + Path: path, + } +} + +func (c *FileCache) Set(data Expirable) error { + bytes, err := json.Marshal(data) + if err == nil { + ioutil.WriteFile(c.Path, bytes, 0644) + } + return err +} + +func (c *FileCache) Get(data Expirable) error { + bytes, err := ioutil.ReadFile(c.Path) + if err == nil { + err = json.Unmarshal(bytes, data) + if err == nil { + created := data.CreatedAt() + expires := data.ExpiresIn() + if err == nil && time.Now().Unix() > created+int64(expires-60) { + err = errors.New("Data is already expired") + } + } + } + return err +} + +type InMemoryCache struct { + data []byte +} + +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{} +} + +func (c *InMemoryCache) Set(data Expirable) error { + bytes, err := json.Marshal(data) + if err == nil { + c.data = bytes + } + return err +} + +func (c *InMemoryCache) Get(data Expirable) error { + err := json.Unmarshal(c.data, data) + if err == nil { + created := data.CreatedAt() + expires := data.ExpiresIn() + if err == nil && time.Now().Unix() > created+int64(expires-60) { + err = errors.New("Data is already expired") + } + } + return err +} + +func sha1Sign(s string) string { + // The pattern for generating a hash is `sha1.New()`, + // `sha1.Write(bytes)`, then `sha1.Sum([]byte{})`. + // Here we start with a new hash. + h := sha1.New() + + // `Write` expects bytes. If you have a string `s`, + // use `[]byte(s)` to coerce it to bytes. + h.Write([]byte(s)) + + // This gets the finalized hash result as a byte + // slice. The argument to `Sum` can be used to append + // to an existing byte slice: it usually isn't needed. + bs := h.Sum(nil) + + // SHA1 values are often printed in hex, for example + // in git commits. Use the `%x` format verb to convert + // a hash results to a hex string. + return fmt.Sprintf("%x", bs) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 429d55003d..f1df8bf449 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -542,6 +542,9 @@ github.com/huaweicloud/huaweicloud-sdk-go/auth/aksk # github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible ## explicit github.com/huaweicloud/huaweicloud-sdk-go-obs/obs +# github.com/hugozhu/godingtalk v1.0.6 +## explicit; go 1.13 +github.com/hugozhu/godingtalk # github.com/imdario/mergo v0.3.6 ## explicit github.com/imdario/mergo