mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-29 03:51:54 +08:00
fix notify
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+129
-9
@@ -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{}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package websocket
|
||||
|
||||
const (
|
||||
SERVICE_TYPE_WEBSOCKET = "websocket"
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package websocket // import "yunion.io/x/cloudpods/pkg/mcclient/modules/websocket"
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
package websocket
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
|
||||
func register(mod modulebase.IBaseManager) {
|
||||
modulebase.Register(mod)
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
+108
-352
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+176
-302
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+336
-545
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
+39
-165
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<<resourceV), 0))
|
||||
q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("actions"), 1<<actionV), 0))
|
||||
topics := []STopic{}
|
||||
err := db.FetchModelObjects(sm, q, &topics)
|
||||
return topics, err
|
||||
}
|
||||
|
||||
func (sm *STopicManager) TopicsByEvent(eventStr string, advanceDays int) ([]STopic, error) {
|
||||
@@ -561,7 +590,6 @@ func (sm *STopicManager) TopicsByEvent(eventStr string, advanceDays int) ([]STop
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to parse event %q", event)
|
||||
}
|
||||
log.Infof("event: %s", event.String())
|
||||
resourceV := converter.resourceValue(event.ResourceType())
|
||||
if resourceV < 0 {
|
||||
log.Warningf("unknown resource type: %s", event.ResourceType())
|
||||
@@ -768,3 +796,55 @@ func (rc *sConverter) action(actionValue int) notify.SAction {
|
||||
}
|
||||
return a.(notify.SAction)
|
||||
}
|
||||
|
||||
func (self *STopic) StartMessageSendTask(ctx context.Context, userCred mcclient.TokenCredential, input api.NotificationManagerEventNotifyInput) error {
|
||||
params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "TopicMessageSendTask", self, userCred, params, "", "")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "NewTask")
|
||||
}
|
||||
return task.ScheduleRun(nil)
|
||||
}
|
||||
|
||||
func (self *STopic) CreateEvent(ctx context.Context, resType, action, message string) (*SEvent, error) {
|
||||
eve := &SEvent{
|
||||
Message: message,
|
||||
ResourceType: resType,
|
||||
Action: action,
|
||||
AdvanceDays: self.AdvanceDays,
|
||||
TopicId: self.Id,
|
||||
}
|
||||
return eve, EventManager.TableSpec().Insert(ctx, eve)
|
||||
}
|
||||
|
||||
func (self *STopic) GetEnabledSubscribers(domainId, projectId string) ([]SSubscriber, error) {
|
||||
q := SubscriberManager.Query().Equals("topic_id", self.Id).IsTrue("enabled")
|
||||
q = q.Filter(sqlchemy.OR(
|
||||
sqlchemy.AND(
|
||||
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_PROJECT),
|
||||
sqlchemy.Equals(q.Field("resource_attribution_id"), projectId),
|
||||
),
|
||||
sqlchemy.AND(
|
||||
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_DOMAIN),
|
||||
sqlchemy.Equals(q.Field("resource_attribution_id"), domainId),
|
||||
),
|
||||
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_SYSTEM),
|
||||
))
|
||||
ret := []SSubscriber{}
|
||||
err := db.FetchModelObjects(SubscriberManager, q, &ret)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (sm *STopicManager) TopicByEvent(eventStr string, advanceDays int) (*STopic, error) {
|
||||
topics, err := sm.TopicsByEvent(eventStr, advanceDays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(topics) == 1 {
|
||||
return &topics[0], nil
|
||||
}
|
||||
if len(topics) == 0 {
|
||||
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
|
||||
}
|
||||
return nil, errors.Wrapf(cloudprovider.ErrDuplicateId, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sender
|
||||
package models
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/appsrv"
|
||||
|
||||
@@ -1,98 +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/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
type SResourceBase struct {
|
||||
db.SResourceBase
|
||||
|
||||
CreateBy string `width:"128" charset:"ascii" nullable:"true" create:"optional"`
|
||||
UpdateBy string `width:"128" charset:"ascii" nullable:"true" update:"user"`
|
||||
DeleteBy string `width:"128" charset:"ascii" nullable:"true"`
|
||||
|
||||
Remark jsonutils.JSONObject `get:"user"`
|
||||
}
|
||||
|
||||
type SResourceBaseManager struct {
|
||||
db.SResourceBaseManager
|
||||
}
|
||||
|
||||
type IResourceBaseModel interface {
|
||||
db.IModel
|
||||
GetIResourceBaseModel() IResourceBaseModel
|
||||
SetDeleteBy(string)
|
||||
}
|
||||
|
||||
func NewResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SResourceBaseManager {
|
||||
return SResourceBaseManager{db.NewResourceBaseManager(dt, tableName, keyword, keywordPlural)}
|
||||
}
|
||||
|
||||
func (self *SResourceBase) GetIResourceBaseModel() IResourceBaseModel {
|
||||
return self.GetVirtualObject().(IResourceBaseModel)
|
||||
}
|
||||
|
||||
func (self *SResourceBase) SetDeleteBy(uid string) {
|
||||
self.DeleteBy = uid
|
||||
}
|
||||
|
||||
func (self *SResourceBaseManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
|
||||
input := apis.ResourceBaseCreateInput{}
|
||||
err := data.Unmarshal(&input)
|
||||
if err != nil {
|
||||
return nil, httperrors.NewInternalServerError("unmarshal ResourceBaseCreateInput fail %s", err)
|
||||
}
|
||||
input, err = self.SResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.Update(jsonutils.Marshal(input))
|
||||
|
||||
data.Set("create_by", jsonutils.NewString(userCred.GetUserId()))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (self *SResourceBase) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
|
||||
self.SResourceBase.PreUpdate(ctx, userCred, query, data)
|
||||
self.UpdateBy = userCred.GetUserId()
|
||||
}
|
||||
|
||||
func (self *SResourceBase) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
|
||||
item := self.GetIResourceBaseModel()
|
||||
_, err := db.Update(item, func() error {
|
||||
item.SetDeleteBy(userCred.GetUserId())
|
||||
return item.MarkDelete()
|
||||
})
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("save update error %s", err)
|
||||
log.Errorf(msg)
|
||||
return httperrors.NewGeneralError(err)
|
||||
}
|
||||
if userCred != nil {
|
||||
db.OpsLog.LogEvent(self, db.ACT_DELETE, self.GetShortDesc(ctx), userCred)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,44 +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
|
||||
|
||||
type SConfigManager struct {
|
||||
SStatusStandaloneResourceBaseManager
|
||||
}
|
||||
|
||||
var ConfigManager *SConfigManager
|
||||
|
||||
func init() {
|
||||
ConfigManager = &SConfigManager{
|
||||
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
|
||||
SConfig{},
|
||||
"notify_t_config",
|
||||
"oldconfig",
|
||||
"oldconfigs",
|
||||
),
|
||||
}
|
||||
ConfigManager.SetVirtualObject(ConfigManager)
|
||||
}
|
||||
|
||||
// SConfig is a table which storage (k,v) and its type.
|
||||
// The three important concepts are key, value and type.
|
||||
// Key and type uniquely identify a value.
|
||||
type SConfig struct {
|
||||
SStatusStandaloneResourceBase
|
||||
|
||||
Type string `width:"15" nullable:"false" create:"required" list:"user"`
|
||||
KeyText string `width:"30" nullable:"false" create:"required" list:"user"`
|
||||
ValueText string `width:"256" nullable:"false" create:"required" list:"user"`
|
||||
}
|
||||
@@ -1,53 +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 (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SContactManager struct {
|
||||
SStatusStandaloneResourceBaseManager
|
||||
}
|
||||
|
||||
var ContactManager *SContactManager
|
||||
|
||||
func init() {
|
||||
ContactManager = &SContactManager{
|
||||
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
|
||||
SContact{},
|
||||
"notify_t_contacts",
|
||||
"contact",
|
||||
"contacts",
|
||||
),
|
||||
}
|
||||
ContactManager.SetVirtualObject(ContactManager)
|
||||
}
|
||||
|
||||
const (
|
||||
CONTACT_INIT = "init" // Contact's status is init which means no verifying
|
||||
CONTACT_VERIFYING = "verifying" // Contact's status is verifying
|
||||
CONTACT_VERIFIED = "verified" // Contact's status is verified
|
||||
)
|
||||
|
||||
type SContact struct {
|
||||
SStatusStandaloneResourceBase
|
||||
|
||||
UID string `width:"128" nullable:"false" create:"required" update:"user" list:"user" get:"user"`
|
||||
ContactType string `width:"16" nullable:"false" create:"required" update:"user"`
|
||||
Contact string `width:"64" nullable:"false" create:"required" update:"user"`
|
||||
Enabled string `width:"5" nullable:"false" default:"1" create:"optional" update:"user"`
|
||||
VerifiedAt time.Time `update:"user"`
|
||||
}
|
||||
@@ -1,52 +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 (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SNotificationManager struct {
|
||||
SStatusStandaloneResourceBaseManager
|
||||
}
|
||||
|
||||
var NotificationManager *SNotificationManager
|
||||
|
||||
func init() {
|
||||
NotificationManager = &SNotificationManager{
|
||||
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
|
||||
SNotification{},
|
||||
"notify_t_notification",
|
||||
"oldnotification",
|
||||
"oldnotifications",
|
||||
),
|
||||
}
|
||||
NotificationManager.SetVirtualObject(NotificationManager)
|
||||
}
|
||||
|
||||
type SNotification struct {
|
||||
SStatusStandaloneResourceBase
|
||||
|
||||
UID string `width:"128" nullable:"false" create:"required"`
|
||||
ContactType string `width:"16" nullable:"false" create:"required" list:"user" index:"true"`
|
||||
Topic string `width:"128" nullable:"true" create:"optional" list:"user"`
|
||||
Priority string `width:"16" nullable:"true" create:"optional" list:"user"`
|
||||
Msg string `create:"required"`
|
||||
ReceivedAt time.Time `nullable:"true" list:"user" create:"optional"`
|
||||
SendAt time.Time `nullable:"false"`
|
||||
SendBy string `width:"128" nullable:"false"`
|
||||
// ClusterID identify message with same topic, msg, priority
|
||||
ClusterID string `width:"128" charset:"ascii" primary:"true" create:"optional" list:"user" get:"user"`
|
||||
}
|
||||
@@ -1,51 +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
|
||||
|
||||
type STemplateManager struct {
|
||||
SStandaloneResourceBaseManager
|
||||
}
|
||||
|
||||
var TemplateManager *STemplateManager
|
||||
|
||||
func init() {
|
||||
TemplateManager = &STemplateManager{
|
||||
SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(
|
||||
STemplate{},
|
||||
"notify_t_template",
|
||||
"oldtemplate",
|
||||
"oldtemplates",
|
||||
),
|
||||
}
|
||||
TemplateManager.SetVirtualObject(TemplateManager)
|
||||
}
|
||||
|
||||
const (
|
||||
TEMPLATE_TYPE_TITLE = "title"
|
||||
TEMPLATE_TYPE_CONTENT = "content"
|
||||
TEMPLATE_TYPE_REMOTE = "remote"
|
||||
CONTACTTYPE_ALL = "all"
|
||||
)
|
||||
|
||||
type STemplate struct {
|
||||
SStandaloneResourceBase
|
||||
|
||||
ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"`
|
||||
Topic string `width:"20" 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"`
|
||||
}
|
||||
@@ -1,103 +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 (
|
||||
"database/sql"
|
||||
|
||||
"yunion.io/x/pkg/util/stringutils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
)
|
||||
|
||||
type UUIDGenerator func() string
|
||||
|
||||
var (
|
||||
DefaultUUIDGenerator = stringutils.UUID4
|
||||
)
|
||||
|
||||
type SStandaloneResourceBase struct {
|
||||
SResourceBase
|
||||
|
||||
ID string `width:"128" charset:"ascii" primary:"true" create:"optional"`
|
||||
}
|
||||
|
||||
func (model *SStandaloneResourceBase) BeforeInsert() {
|
||||
if len(model.ID) == 0 {
|
||||
model.ID = DefaultUUIDGenerator()
|
||||
}
|
||||
}
|
||||
|
||||
type SStandaloneResourceBaseManager struct {
|
||||
SResourceBaseManager
|
||||
}
|
||||
|
||||
func NewStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStandaloneResourceBaseManager {
|
||||
return SStandaloneResourceBaseManager{NewResourceBaseManager(dt, tableName, keyword, keywordPlural)}
|
||||
}
|
||||
|
||||
func (manager *SStandaloneResourceBaseManager) GetIStandaloneModelManager() db.IStandaloneModelManager {
|
||||
return manager.GetVirtualObject().(db.IStandaloneModelManager)
|
||||
}
|
||||
|
||||
func (manager *SStandaloneResourceBaseManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
|
||||
return q.Equals("id", idStr)
|
||||
}
|
||||
|
||||
func (manager *SStandaloneResourceBaseManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
|
||||
return q.NotEquals("id", idStr)
|
||||
}
|
||||
|
||||
func (manager *SStandaloneResourceBaseManager) FetchById(idStr string) (db.IModel, error) {
|
||||
return FetchById(manager.GetIStandaloneModelManager(), idStr)
|
||||
}
|
||||
|
||||
func FetchById(manager db.IModelManager, idStr string) (db.IModel, error) {
|
||||
q := manager.Query()
|
||||
q = manager.FilterById(q, idStr)
|
||||
count, err := q.CountWithError()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 1 {
|
||||
obj, err := db.NewModelObject(manager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = q.First(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return obj, nil
|
||||
}
|
||||
} else if count > 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<string, string> configs = 1;
|
||||
}
|
||||
|
||||
message AddConfigInput {
|
||||
map<string, string> configs = 1;
|
||||
string domainId = 2;
|
||||
}
|
||||
|
||||
message UpdateConfigInput {
|
||||
map<string, string> 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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
+191
-8
@@ -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)
|
||||
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, "; ")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -240,4 +240,6 @@ const (
|
||||
ACT_PANIC = "panic"
|
||||
|
||||
ACT_IP_MAC_BIND = "ip_mac_bind"
|
||||
// 程序内初始化notifyconfigmap错误
|
||||
ACT_INIT_NOTIFY_CONFIGMAP = "init_notify_configmap"
|
||||
)
|
||||
|
||||
+24
@@ -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
|
||||
+21
@@ -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.
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# DingTalk Open API golang SDK
|
||||
|
||||

|
||||
|
||||
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-<random_number>
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Author Kevin Zhu
|
||||
*
|
||||
* Direct questions, comments to <ipandtcp@gmail.com>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+59
@@ -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
|
||||
}
|
||||
+49
@@ -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
|
||||
}
|
||||
+146
@@ -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
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
+38
@@ -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
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
+259
@@ -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
|
||||
}
|
||||
+75
@@ -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
|
||||
}
|
||||
+109
@@ -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
|
||||
}
|
||||
+164
@@ -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)
|
||||
}
|
||||
+189
@@ -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)
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Author Kevin Zhu
|
||||
*
|
||||
* Direct questions, comments to <ipandtcp@gmail.com>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Author Kevin Zhu
|
||||
*
|
||||
* Direct questions, comments to <ipandtcp@gmail.com>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Author Kevin Zhu
|
||||
*
|
||||
* Direct questions, comments to <ipandtcp@gmail.com>
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
+119
@@ -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
|
||||
}
|
||||
+102
@@ -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)
|
||||
}
|
||||
Vendored
+3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user