fix notify

This commit is contained in:
mhf
2022-12-07 16:30:49 +08:00
parent 52a9b94536
commit 6e42f11024
96 changed files with 5606 additions and 4303 deletions
+10 -3
View File
@@ -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 {
+3 -2
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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{}
})
}
+12 -1
View File
@@ -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")
)
+4 -4
View File
@@ -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"
+10 -4
View File
@@ -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 {
+4 -9
View File
@@ -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
+1
View File
@@ -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
+5
View File
@@ -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"
+1
View File
@@ -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 {
+8 -3
View File
@@ -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
+4 -3
View File
@@ -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 {
+4
View File
@@ -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,
-1
View File
@@ -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")
+1
View File
@@ -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)
}
+7
View File
@@ -0,0 +1,7 @@
package websocket
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
func register(mod modulebase.IBaseManager) {
modulebase.Register(mod)
}
-85
View File
@@ -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
View File
@@ -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()
}
+5 -11
View File
@@ -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) {
+13 -9
View File
@@ -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 {
+9 -7
View File
@@ -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
View File
@@ -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)
}
+73
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+38 -10
View File
@@ -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
View File
@@ -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
}
+41
View File
@@ -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
}
+17
View File
@@ -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
+5 -5
View File
@@ -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
}
+89 -9
View File
@@ -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"
-98
View File
@@ -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
}
-44
View File
@@ -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"`
}
-53
View File
@@ -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"`
}
-52
View File
@@ -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"`
}
-51
View File
@@ -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"`
}
-103
View File
@@ -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)
}
-82
View File
@@ -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
}
-127
View File
@@ -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)
}
+3 -1
View File
@@ -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
-90
View File
@@ -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
-108
View File
@@ -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);
}
-480
View File
@@ -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, &param)
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)
}),
)
}
-97
View File
@@ -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)
}
}
+53
View File
@@ -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"
+162
View File
@@ -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{},
})
}
+110
View File
@@ -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
View File
@@ -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)
+163
View File
@@ -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{},
})
}
+112
View File
@@ -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{},
})
}
+113
View File
@@ -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{},
})
}
+130
View File
@@ -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{})
}
+40
View File
@@ -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"
+102
View File
@@ -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{})
}
+78
View File
@@ -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
}
+55
View File
@@ -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
}
+118
View File
@@ -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{},
})
}
+103
View File
@@ -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{},
})
}
+120
View File
@@ -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{},
})
}
+140
View File
@@ -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{},
})
}
+95
View File
@@ -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{},
})
}
+1 -8
View File
@@ -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,
+7 -10
View File
@@ -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
+91 -90
View File
@@ -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
}
+15 -15
View File
@@ -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 {
+45 -16
View File
@@ -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, "; ")
+302
View File
@@ -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) {
}
+7 -9
View File
@@ -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
+2
View File
@@ -240,4 +240,6 @@ const (
ACT_PANIC = "panic"
ACT_IP_MAC_BIND = "ip_mac_bind"
// 程序内初始化notifyconfigmap错误
ACT_INIT_NOTIFY_CONFIGMAP = "init_notify_configmap"
)
+24
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,108 @@
# DingTalk Open API golang SDK
![image](http://static.dingtalk.com/media/lALOAQ6nfSvM5Q_229_43.png)
Check out DingTalk Open API document at: https://ding-doc.dingtalk.com/
## Usage
Fetch the SDK
```
export GOPATH=`pwd`
go get github.com/hugozhu/godingtalk
```
### Example code to send a micro app message
```
package main
import (
"github.com/hugozhu/godingtalk"
"log"
"os"
)
func main() {
c := godingtalk.NewDingTalkClient(os.Getenv("corpid"), os.Getenv("corpsecret"))
c.RefreshAccessToken()
err := c.SendAppMessage(os.Args[1], os.Args[2], os.Args[3])
if err != nil {
log.Println(err)
}
}
```
## Guide
Step-by-step Guide to use this SDK
http://hugozhu.myalert.info/2016/05/02/66-use-dingtalk-golang-sdk-to-send-message-on-pi.html
## Tools
**ding_alert** : Command line tool to send app/text/oa ... messages
```
export GOPATH=`pwd`
go get github.com/hugozhu/godingtalk/demo/ding_alert
export corpid=<组织的corpid 通过 https://oa.dingtalk.com 获取>
export corpsecret=<组织的corpsecret 通过 https://oa.dingtalk.com 获取>
./bin/ding_alert
Usage of ./bin/ding_alert:
-agent string
agent Id (default "22194403")
-chat string
chat id (default "chat6a93bc1ee3b7d660d372b1b877a9de62")
-file string
file path for media message
-link string
link url (default "http://hugozhu.myalert.info/dingtalk")
-sender string
sender id (default "011217462940")
-text string
text for link message (default "This is link text")
-title string
title for link message (default "This is link title")
-touser string
touser id (default "0420506555")
-type string
message type (app, text, image, voice, link, oa) (default "app")
```
**github**: Deliver Github webhook events to DingTalk, which can be deployed on Google AppEngine.
more info at: http://hugozhu.myalert.info/2016/05/15/67-use-free-google-cloud-service-to-deliver-github-webhook-events-to-dingtalk.html
```
export GOPATH=`pwd`
go get github.com/hugozhu/godingtalk/demo/github/appengine
```
Modify `app.yaml`
```
cd src/github.com/hugozhu/godingtalk/demo/github/appengine
cat app.yaml
application: github-alert-<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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 数据签名需要用到的tokenISV(服务提供商)推荐使用注册套件时填写的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
View File
@@ -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&timestamp=%s&url=%s", ticket, nonceStr, timeStamp, url)
return sha1Sign(s)
}
+187
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+3
View File
@@ -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