Refactor for Notify (#7611)

* refactor(notify): ALL

* feat(climc): Notify v2

* feat: support notify for codegen.py

* delete deprecated code and Maintain SDK compatibility

* Adjust the template for mail validation to support captcha

* fix: The issue of notify refactoring

* add copyright for notifyv2 cli

* Adjust according to the advice of make check

* Remove utils package in notify

* fix import format

* remove redundant Template module

* fix validateCreate and validateUpdate of notifytemplate

* fix update issues of receivers

* fix: Add SetStageComplete in tasks
This commit is contained in:
Rain Zheng
2020-08-25 17:19:02 +08:00
committed by GitHub
parent c2bca8ab39
commit 853153c739
74 changed files with 3952 additions and 4405 deletions

View File

@@ -47,16 +47,13 @@
</td>
</tr>
<tr>
<td style="padding-bottom: 10px;">请点击下面的链接,完成用户邮箱的激活:</td>
</tr>
<tr>
<td style="padding-bottom: 30px;"><a href="{{.link}}">{{.link}}</a></td>
<td style="padding-bottom: 10px;">您正在验证邮箱,请在验证码输入框中输入:{{.code}},已完成验证。</td>
</tr>
</table>
</td>
</tr>
<tr style="width: 96%;">
<td colspan="2" style="border-top: 1px dashed #ccc; color: #333; font-size: 12px; padding-bottom: 10px; font-weight: 100;">*为了确保您的帐号安全该链接仅48小时内访问有效请勿直接回复此邮件。</td>
<td colspan="2" style="border-top: 1px dashed #ccc; color: #333; font-size: 12px; padding-bottom: 10px; font-weight: 100;">如非本人操作,请及时登录平台并修改密码以保证账户的安全</td>
</tr>
<tr>
<td colspan="2" style="background: #333; text-align: right; padding-right: 20px; font-size: 12px; color: #fff; height: 50px;">版权所有 © {{.copyright}} 保留一切权利</td>

View File

@@ -32,7 +32,7 @@ import (
_ "yunion.io/x/onecloud/cmd/climc/shell/meter"
_ "yunion.io/x/onecloud/cmd/climc/shell/misc"
_ "yunion.io/x/onecloud/cmd/climc/shell/monitor"
_ "yunion.io/x/onecloud/cmd/climc/shell/notify"
_ "yunion.io/x/onecloud/cmd/climc/shell/notifyv2"
_ "yunion.io/x/onecloud/cmd/climc/shell/servicetree"
_ "yunion.io/x/onecloud/cmd/climc/shell/yunionconf"
)

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 notify
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
/**
* 获得一个全部通信地址组
*/
type ContactGroupsListOptions struct {
options.BaseListOptions
}
R(&ContactGroupsListOptions{}, "contact-group-list", "List all contact groups for all the domainsconta", func(s *mcclient.ClientSession, args *ContactGroupsListOptions) error {
var params *jsonutils.JSONDict
{
var err error
params, err = args.BaseListOptions.Params()
if err != nil {
return err
}
}
result, err := modules.ContactGroups.List(s, params)
if err != nil {
return err
}
printList(result, modules.ContactGroups.GetColumns(s))
return nil
})
}

View File

@@ -1,218 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
/**
* 操作用户的通信地址(如果用户的通信地址不存在则进行添加;如果已存在则进行修改;如果设置空则进行删除。)
*/
type ContactsUpdateOptions struct {
UID string `help:"The user you wanna add contact to (Keystone User ID)"`
CONTACTTYPE string `help:"The contact type email|mobile" choices:"email|mobile|dingtalk"`
CONTACT string `help:"The contacts details mobile number or email address or dingtalk's userid, if set it the empty str means delete"`
Status string `help:"Enabled or disabled contact status" choices:"enable|disable"`
Pull []string `help:"pull some subcontacts(e.g., dingtalk, feishu, etc) related to mobile"`
}
R(&ContactsUpdateOptions{}, "contact-update", "Create, delete or update contact for user", func(s *mcclient.ClientSession, args *ContactsUpdateOptions) error {
arr := jsonutils.NewArray()
tmpObj := jsonutils.NewDict()
tmpObj.Add(jsonutils.NewString(args.CONTACTTYPE), "contact_type")
tmpObj.Add(jsonutils.NewString(args.CONTACT), "contact")
if len(args.Status) > 0 {
if args.Status == "disable" {
tmpObj.Add(jsonutils.NewInt(0), "enabled")
} else {
tmpObj.Add(jsonutils.NewInt(1), "enabled")
}
}
arr.Add(tmpObj)
params := jsonutils.NewDict()
params.Add(arr, "contacts")
pulls := jsonutils.NewArray()
for _, pull := range args.Pull {
pulls.Add(jsonutils.NewString(pull))
}
params.Add(pulls, "pull")
contact, err := modules.Contacts.CustomizedPerformAction(s, args.UID, "update-contact", params)
if err != nil {
return err
}
printObject(contact)
return nil
})
type ContactsPullOptions struct {
UID string `help:"The user you wanna pull contact"`
CONTACTTYPE string `help:"The contact type"`
}
R(&ContactsPullOptions{}, "contact-pull", "Pull contact", func(s *mcclient.ClientSession, args *ContactsPullOptions) error {
params := jsonutils.NewDict()
params.Set("contacts", jsonutils.NewArray())
params.Set("pull", jsonutils.NewArray(jsonutils.NewString(args.CONTACTTYPE)))
contact, err := modules.Contacts.CustomizedPerformAction(s, args.UID, "update-contact", params)
if err != nil {
return err
}
printObject(contact)
return nil
})
type ContactsDeleteOptions struct {
UID string `help:"The user you wanna add contact to (Keystone User ID)"`
CONTACTTYPE string `help:"The contact type email|mobile|dingtalk" choices:"email|mobile|dingtalk"`
}
R(&ContactsDeleteOptions{}, "contact-delete", "Delete contact for user", func(s *mcclient.ClientSession, args *ContactsDeleteOptions) error {
arr := jsonutils.NewArray()
tmpObj := jsonutils.NewDict()
tmpObj.Add(jsonutils.NewString(args.CONTACTTYPE), "contact_type")
tmpObj.Add(jsonutils.NewString(""), "contact")
arr.Add(tmpObj)
params := jsonutils.NewDict()
params.Add(arr, "contacts")
contact, err := modules.Contacts.CustomizedPerformAction(s, args.UID, "update-contact", params)
if err != nil {
return err
}
printObject(contact)
return nil
})
/**
* 获得所有用户的所有通信地址列表
*/
type ContactsListOptions struct {
options.BaseListOptions
}
R(&ContactsListOptions{}, "contact-list", "List all contacts for all users", func(s *mcclient.ClientSession, args *ContactsListOptions) error {
var params *jsonutils.JSONDict
{
var err error
params, err = args.BaseListOptions.Params()
if err != nil {
return err
}
}
params.Add(jsonutils.JSONTrue, "details")
result, err := modules.Contacts.List(s, params)
if err != nil {
return err
}
printList(result, modules.Contacts.GetColumns(s))
return nil
})
/**
* 获得一个用户全部通信地址
*/
type ContactsListForUserOptions struct {
options.BaseListOptions
UID string `help:"The user you wanna find contact from (Keystone User ID)"`
}
R(&ContactsListForUserOptions{}, "contact-show", "List all contacts for the users", func(s *mcclient.ClientSession, args *ContactsListForUserOptions) error {
var params *jsonutils.JSONDict
{
var err error
params, err = args.BaseListOptions.Params()
if err != nil {
return err
}
}
params.Add(jsonutils.JSONTrue, "details")
result, err := modules.Contacts.CustomizedGet(s, args.UID, params)
if err != nil {
return err
}
contactsStr, err := result.GetString("details")
if err != nil {
return nil
}
contactsJson, err := jsonutils.ParseString(contactsStr)
if err != nil {
return err
}
contacts, err := contactsJson.GetArray()
if err != nil {
return err
}
printList(&modulebase.ListResult{Data: contacts}, nil)
return nil
})
/**
* 触发验证通信地址操作
*/
type ContactsVerifyOptions struct {
UID string `help:"The user you wanna verify contact for (Keystone User ID)"`
CONTACT_TYPE string `help:"The contact type email|mobile"`
CONTACT string `help:"The contacts details mobile number or email address"`
}
R(&ContactsVerifyOptions{}, "contact-verify-trigger", "Trigger contact verify", func(s *mcclient.ClientSession, args *ContactsVerifyOptions) error {
tmpDict := jsonutils.NewDict()
tmpDict.Add(jsonutils.NewString(args.CONTACT_TYPE), "contact_type")
tmpDict.Add(jsonutils.NewString(args.CONTACT), "contact")
_, err := modules.Contacts.CustomizedPerformAction(s, args.UID, "verify", tmpDict)
if err != nil {
return err
}
return nil
})
type ContactsBatchDeleteOptions struct {
UIDS []string `help:"All user'id you wanna to delete contacts (Keystone User ID)"`
}
R(&ContactsBatchDeleteOptions{}, "contact-delete", "Delete all contacts for the user", func(s *mcclient.ClientSession, args *ContactsBatchDeleteOptions) error {
arr := jsonutils.NewArray()
for _, f := range args.UIDS {
arr.Add(jsonutils.NewString(f))
}
params := jsonutils.NewDict()
params.Add(arr, "contacts")
contact, err := modules.Contacts.DoBatchDeleteContacts(s, params)
if err != nil {
return err
}
printObject(contact)
return nil
})
}

View File

@@ -1,112 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
func init() {
/**
* 查询邮件配置信息
*/
type EmailConfigShowOptions struct {
TYPE string `help:"type "`
}
R(&EmailConfigShowOptions{}, "email-config-show", "Show email-config details",
func(s *mcclient.ClientSession, args *EmailConfigShowOptions) error {
result, err := modules.EmailConfigs.Get(s, args.TYPE, nil)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 增加邮件配置信息
*/
type EmailConfigCreateOptions struct {
USERNAME string `help:"Username for email sender"`
PASSWORD string `help:"Password for email sender"`
HOSTNAME string `help:"Email server name"`
SSLGLOBAL string `help:"use ssl_global"`
HOSTPORT int64 `help:"Email server port"`
}
R(&EmailConfigCreateOptions{}, "email-config-create", "Create a Email Config",
func(s *mcclient.ClientSession, args *EmailConfigCreateOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.USERNAME), "username")
params.Add(jsonutils.NewString(args.PASSWORD), "password")
params.Add(jsonutils.NewString(args.HOSTNAME), "hostname")
params.Add(jsonutils.NewString(args.SSLGLOBAL), "ssl_global")
params.Add(jsonutils.NewInt(args.HOSTPORT), "hostport")
result, err := modules.EmailConfigs.Create(s, params)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 修改
*/
type EmailConfigUpdateOptions struct {
TYPE string `help:"type of email "`
USERNAME string `help:"Username for email sender"`
PASSWORD string `help:"Password for email sender"`
HOSTNAME string `help:"Email server name"`
SSLGLOBAL string `help:"use ssl_global"`
HOSTPORT int64 `help:"Email server port"`
}
R(&EmailConfigUpdateOptions{}, "email-config-update", "Update a email-config", func(s *mcclient.ClientSession, args *EmailConfigUpdateOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.USERNAME), "username")
params.Add(jsonutils.NewString(args.PASSWORD), "password")
params.Add(jsonutils.NewString(args.HOSTNAME), "hostname")
params.Add(jsonutils.NewString(args.SSLGLOBAL), "ssl_global")
params.Add(jsonutils.NewInt(args.HOSTPORT), "hostport")
result, err := modules.EmailConfigs.Put(s, args.TYPE, params)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 删除
*/
type EmailConfigDeleteOptions struct {
TYPE string `help:"type of email "`
}
R(&EmailConfigDeleteOptions{}, "email-config-delete", "Delete a email config", func(s *mcclient.ClientSession, args *EmailConfigDeleteOptions) error {
result, e := modules.EmailConfigs.Delete(s, args.TYPE, nil)
if e != nil {
return e
}
printObject(result)
return nil
})
}

View File

@@ -1,161 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules/notify"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
/**
* 新建一个通知发送任务
*/
type NotificationCreateOptions struct {
CONTACTTYPE string `help:"User's contacts type"`
TOPIC string `help:"Title or topic of the notification"`
PRIORITY string `help:"Priority of the notification" choices:"normal|important|fatal"`
MSG string `help:"The content of the notification"`
Remark string `help:"Remark or description of the notification"`
Group bool `help:"Send to group"`
}
type NotificationCreateSingleOptions struct {
UID string `help:"The user you wanna sent to (Keystone User ID)"`
NotificationCreateOptions
}
R(&NotificationCreateSingleOptions{}, "notify", "Send a notification to someone", func(s *mcclient.ClientSession,
args *NotificationCreateSingleOptions) error {
msg := notify.SNotifyMessage{}
if args.Group {
msg.Gid = []string{args.UID}
} else {
msg.Uid = []string{args.UID}
}
msg.ContactType = notify.TNotifyChannel(args.CONTACTTYPE)
msg.Topic = args.TOPIC
msg.Priority = notify.TNotifyPriority(args.PRIORITY)
msg.Msg = args.MSG
msg.Remark = args.Remark
err := notify.Notifications.Send(s, msg)
if err != nil {
return err
}
return nil
})
type NotificationCreateMultiOptions struct {
Uid []string `help:"The user you wanna sent to (Keystone User ID)"`
NotificationCreateOptions
}
R(&NotificationCreateMultiOptions{}, "notify-batch", "Send a notification to someones",
func(s *mcclient.ClientSession, args *NotificationCreateMultiOptions) error {
msg := notify.SNotifyMessage{}
if args.Group {
msg.Gid = args.Uid
} else {
msg.Uid = args.Uid
}
msg.ContactType = notify.TNotifyChannel(args.CONTACTTYPE)
msg.Topic = args.TOPIC
msg.Priority = notify.TNotifyPriority(args.PRIORITY)
msg.Msg = args.MSG
msg.Remark = args.Remark
err := notify.Notifications.Send(s, msg)
if err != nil {
return err
}
return nil
})
/**
* 发送全局通知
*/
type NotificationBroadcastOptions struct {
// CONTACTTYPE string `help:"User's contacts type, cloud be email|mobile|dingtalk|/webconsole" choices:"email|mobile|dingtalk|webconsole"`
Topic string `required:"true" help:"Title or topic of the notification"`
Priority string `help:"Priority of the notification" choices:"normal|important|fatal" default:"normal"`
Msg string `help:"The content of the notification"`
Remark string `help:"Remark or description of the notification"`
// Group bool `help:"Send to group"`
}
R(&NotificationBroadcastOptions{}, "notify-broadcast", "Send a notification to all online users", func(s *mcclient.ClientSession, args *NotificationBroadcastOptions) error {
msg := notify.SNotifyMessage{}
msg.Broadcast = true
msg.ContactType = notify.NotifyByWebConsole
msg.Topic = args.Topic
msg.Priority = notify.TNotifyPriority(args.Priority)
msg.Msg = args.Msg
msg.Remark = args.Remark
err := notify.Notifications.Send(s, msg)
if err != nil {
return err
}
return nil
})
/**
* 修改通知发送任务的状态
*/
type NotificationUpdateCallbackOptions struct {
ID string `help:"ID of the notification send task"`
STATUS string `help:"Notification send status" choices:"sent_ok|send_fail"`
Remark string `help:"Remark or description of the operation or fail reason"`
}
R(&NotificationUpdateCallbackOptions{}, "notification-update-callback", "UpdateItem send status of the notification task", func(s *mcclient.ClientSession, args *NotificationUpdateCallbackOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.STATUS), "status")
if len(args.Remark) > 0 {
params.Add(jsonutils.NewString(args.Remark), "remark")
}
notification, err := notify.Notifications.Put(s, args.ID, params)
if err != nil {
return err
}
printObject(notification)
return nil
})
/**
* 查询已发送的通知任务
*/
type NotificationListOptions struct {
options.BaseListOptions
}
R(&NotificationListOptions{}, "notify-list", "List notification history", func(s *mcclient.ClientSession, args *NotificationListOptions) error {
result, err := notify.Notifications.List(s, nil)
if err != nil {
return err
}
printList(result, notify.Notifications.GetColumns(s))
return nil
})
}

View File

@@ -1,77 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
func init() {
type ConfigCreate2Options struct {
CONTACTTYPE string `help:"contact type (email|sms_aliyun or others)"`
CONFIGS []string `help:"config (k, v)"`
}
R(&ConfigCreate2Options{}, "notify-config-update", "config update, example: notify_config-update email mail.smtp.hostname hostname mail.smtp.hostport 123.", func(s *mcclient.ClientSession, args *ConfigCreate2Options) error {
tmp := jsonutils.NewDict()
for i := 0; i+1 < len(args.CONFIGS); i += 2 {
tmp.Add(jsonutils.NewString(args.CONFIGS[i+1]), args.CONFIGS[i])
}
body := jsonutils.NewDict()
body.Add(tmp, args.CONTACTTYPE)
ret, err := modules.Configs.Create(s, body)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ConfigCreate2Options{}, "notify-config-validate", "config validate", func(s *mcclient.ClientSession,
args *ConfigCreate2Options) error {
tmp := jsonutils.NewDict()
for i := 0; i+1 < len(args.CONFIGS); i += 2 {
tmp.Add(jsonutils.NewString(args.CONFIGS[i+1]), args.CONFIGS[i])
}
ret, err := modules.Configs.PerformAction(s, args.CONTACTTYPE, "validate", tmp)
if err != nil {
return err
}
printObject(ret)
return nil
})
type ConfigGet2Options struct {
TYPE string `help:"contact type (email|sms_aliyun or others)"`
}
R(&ConfigGet2Options{}, "notify-config-show", "config show", func(s *mcclient.ClientSession, args *ConfigGet2Options) error {
result, err := modules.Configs.Get(s, args.TYPE, jsonutils.JSONNull)
if err != nil {
return err
}
printObject(result)
return nil
})
R(&ConfigGet2Options{}, "notify-config-delete", "config delete", func(s *mcclient.ClientSession, args *ConfigGet2Options) error {
result, err := modules.Configs.Delete(s, args.TYPE, jsonutils.JSONNull)
if err != nil {
return err
}
printObject(result)
return nil
})
}

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 notify
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type NotifyTemplateUpdateOptions struct {
CONTACTTYPE string `help:"the contanct type, such as 'email', 'mobile'"`
TOPIC string `help:"the topic of temlate, such as 'VERIFY', 'ALARM'"`
TEMPLATETYPE string `help:"the type of template" choices:"content|title|remote"`
CONTENT string `help:"the content of template"`
}
R(&NotifyTemplateUpdateOptions{}, "notify-template-update", "Create, update contact for user", func(s *mcclient.ClientSession,
args *NotifyTemplateUpdateOptions) error {
arr := jsonutils.NewArray()
tmpObj := jsonutils.NewDict()
tmpObj.Add(jsonutils.NewString(args.TOPIC), "topic")
tmpObj.Add(jsonutils.NewString(args.TEMPLATETYPE), "template_type")
tmpObj.Add(jsonutils.NewString(args.CONTENT), "content")
arr.Add(tmpObj)
params := jsonutils.NewDict()
params.Add(arr, "notifytemplates")
contact, err := modules.NotifyTemplates.PerformAction(s, args.CONTACTTYPE, "update-template", params)
if err != nil {
return err
}
printObject(contact)
return nil
})
type NotifyTemplateDeleteOptions struct {
CONTACTTYPE string `help:"the contanct type, such as 'email', 'mobile'"`
Topic string `help:"the topic of temlate, such as 'VERIFY', 'ALARM'"`
}
R(&NotifyTemplateDeleteOptions{}, "notify-template-delete", "delete notify template",
func(s *mcclient.ClientSession, args *NotifyTemplateDeleteOptions) error {
tmpObj := jsonutils.NewDict()
tmpObj.Add(jsonutils.NewString(args.CONTACTTYPE), "contact_type")
tmpObj.Add(jsonutils.NewString(args.Topic), "topic")
_, err := modules.NotifyTemplates.DeleteContents(s, tmpObj)
if err != nil {
return err
}
return nil
})
type NotifyTemplateListOptions struct {
options.BaseListOptions
}
R(&NotifyTemplateListOptions{}, "notify-template-list", "List all notify template",
func(s *mcclient.ClientSession, args *NotifyTemplateListOptions) error {
params, err := args.BaseListOptions.Params()
if err != nil {
return err
}
result, err := modules.NotifyTemplates.List(s, params)
if err != nil {
return err
}
printList(result, modules.NotifyTemplates.GetColumns(s))
return nil
})
}

View File

@@ -1,124 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
func init() {
/**
* 查询短信配置信息
*/
type SmsConfigShowOptions struct {
TYPE string `help:"type "`
}
R(&SmsConfigShowOptions{}, "sms-config-show", "Show sms config details",
func(s *mcclient.ClientSession, args *SmsConfigShowOptions) error {
result, err := modules.SmsConfigs.Get(s, args.TYPE, nil)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 增加短信配置信息
*/
type SmsConfigCreateOptions struct {
TYPE string `help:"sms vendor"`
ACCESSKEYID string `help:"ACCESSKEYID for sms vendor"`
ACCESSKEYSECRET string `help:"ACCESSKEYSECRET for sms vendor"`
SIGNATURE string `help:"SIGNATURE for sms vendor"`
SmsTemplateOne string `help:"Sms TemplateOne"`
SmsTemplateTwo string `help:"Sms TemplateTwo"`
SmsTemplateThree string `help:"Sms TemplateThree"`
SmsCheckCode string `help:"Sms Check Code "`
}
R(&SmsConfigCreateOptions{}, "sms-config-create", "Create a sms Config",
func(s *mcclient.ClientSession, args *SmsConfigCreateOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.TYPE), "type")
params.Add(jsonutils.NewString(args.ACCESSKEYID), "access_key_id")
params.Add(jsonutils.NewString(args.ACCESSKEYSECRET), "access_key_secret")
params.Add(jsonutils.NewString(args.SIGNATURE), "signature")
params.Add(jsonutils.NewString(args.SmsTemplateOne), "sms_template_one")
params.Add(jsonutils.NewString(args.SmsTemplateTwo), "sms_template_two")
params.Add(jsonutils.NewString(args.SmsTemplateThree), "sms_template_three")
params.Add(jsonutils.NewString(args.SmsCheckCode), "sms_check_code")
result, err := modules.SmsConfigs.Create(s, params)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 修改
*/
type SmsConfigUpdateOptions struct {
TYPE string `help:"sms vendor"`
ACCESSKEYID string `help:"ACCESSKEYID for sms vendor"`
ACCESSKEYSECRET string `help:"ACCESSKEYSECRET for sms vendor"`
SIGNATURE string `help:"SIGNATURE for sms vendor"`
SmsTemplateOne string `help:"Sms TemplateOne"`
SmsTemplateTwo string `help:"Sms TemplateTwo"`
SmsTemplateThree string `help:"Sms TemplateThree"`
SmsCheckCode string `help:"Sms Check Code "`
}
R(&SmsConfigUpdateOptions{}, "sms-config-update", "Update a sms-config", func(s *mcclient.ClientSession, args *SmsConfigUpdateOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.TYPE), "type")
params.Add(jsonutils.NewString(args.ACCESSKEYID), "access_key_id")
params.Add(jsonutils.NewString(args.ACCESSKEYSECRET), "access_key_secret")
params.Add(jsonutils.NewString(args.SIGNATURE), "signature")
params.Add(jsonutils.NewString(args.SmsTemplateOne), "sms_template_one")
params.Add(jsonutils.NewString(args.SmsTemplateTwo), "sms_template_two")
params.Add(jsonutils.NewString(args.SmsTemplateThree), "sms_template_three")
params.Add(jsonutils.NewString(args.SmsCheckCode), "sms_check_code")
result, err := modules.SmsConfigs.Put(s, args.TYPE, params)
if err != nil {
return err
}
printObject(result)
return nil
})
/**
* 删除
*/
type SmsConfigDeleteOptions struct {
TYPE string `help:"sms vendor"`
}
R(&SmsConfigDeleteOptions{}, "sms-config-delete", "Delete a sms config", func(s *mcclient.ClientSession, args *SmsConfigDeleteOptions) error {
result, e := modules.SmsConfigs.Delete(s, args.TYPE, nil)
if e != nil {
return e
}
printObject(result)
return nil
})
}

View File

@@ -1,47 +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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
func init() {
/**
* 通信地址验证
*/
type ContactsVerifyOptions struct {
ID string `help:"Verification process ID"`
TOKEN string `help:"Temporary token issued to the user when the validation is triggered"`
}
R(&ContactsVerifyOptions{}, "contact-verify", "Trigger contact verify", func(s *mcclient.ClientSession, args *ContactsVerifyOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.TOKEN), "token")
result, err := modules.Verifications.Get(s, args.ID, params)
if err != nil {
return err
}
printObject(result)
return nil
})
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
package notifyv2
import (
"yunion.io/x/onecloud/cmd/climc/shell"

View File

@@ -0,0 +1,123 @@
// 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 notifyv2
import (
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type ConfigListOptions struct {
options.BaseListOptions
}
R(&ConfigListOptions{}, "notify-config-list", "List notify config", func(s *mcclient.ClientSession, args *ConfigListOptions) error {
params, err := options.ListStructToParams(args)
if err != nil {
return err
}
result, err := modules.NotifyConfig.List(s, params)
if err != nil {
return err
}
printList(result, modules.NotifyConfig.GetColumns(s))
return nil
})
type ConfigCreateOptions struct {
TYPE string `help:"Type contact config"`
Configs []string `help:"Config content, format: 'key:value'"`
}
R(&ConfigCreateOptions{}, "notify-config-create", "Create notify config", func(s *mcclient.ClientSession, args *ConfigCreateOptions) error {
configs := jsonutils.NewDict()
for _, kv := range args.Configs {
index := strings.IndexByte(kv, ':')
configs.Set(kv[:index], jsonutils.NewString(kv[index+1:]))
}
params := jsonutils.NewDict()
params.Set("type", jsonutils.NewString(args.TYPE))
params.Set("content", configs)
ret, err := modules.NotifyConfig.Create(s, params)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ConfigCreateOptions{}, "notify-config-update", "Update notify config", func(s *mcclient.ClientSession, args *ConfigCreateOptions) error {
configs := jsonutils.NewDict()
for _, kv := range args.Configs {
index := strings.IndexByte(kv, ':')
configs.Set(kv[:index], jsonutils.NewString(kv[index+1:]))
}
params := jsonutils.NewDict()
params.Set("content", configs)
id, err := configIdFromType(s, args.TYPE)
if err != nil {
return err
}
ret, err := modules.NotifyConfig.Update(s, id, params)
if err != nil {
return err
}
printObject(ret)
return nil
})
type ConfigOptions struct {
TYPE string `help:"Type contact config"`
}
R(&ConfigOptions{}, "notify-config-delete", "Delete notify config", func(s *mcclient.ClientSession, args *ConfigOptions) error {
id, err := configIdFromType(s, args.TYPE)
if err != nil {
return err
}
ret, err := modules.NotifyConfig.Delete(s, id, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ConfigOptions{}, "notify-config-show", "Show notify config", func(s *mcclient.ClientSession, args *ConfigOptions) error {
listParams := jsonutils.NewDict()
listParams.Set("type", jsonutils.NewString(args.TYPE))
list, err := modules.NotifyConfig.List(s, listParams)
if err != nil {
return err
}
data := list.Data[0]
printObject(data)
return nil
})
}
func configIdFromType(s *mcclient.ClientSession, t string) (string, error) {
listParams := jsonutils.NewDict()
listParams.Set("type", jsonutils.NewString(t))
list, err := modules.NotifyConfig.List(s, listParams)
if err != nil {
return "", err
}
id, err := list.Data[0].GetString("id")
if err != nil {
return "", err
}
return id, nil
}

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 notifyv2
import (
"yunion.io/x/jsonutils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type NotificationCreateInput struct {
Receivers []string `help:"ID or Name of Receiver"`
ContactType string `help:"Contact type of receiver"`
TOPIC string `help:"Topic"`
Priority string `help:"Priority"`
MESSAGE string `help:"Message"`
}
R(&NotificationCreateInput{}, "notify-send", "Send a notify message", func(s *mcclient.ClientSession, args *NotificationCreateInput) error {
input := api.NotificationCreateInput{
Receivers: args.Receivers,
ContactType: args.ContactType,
Topic: args.TOPIC,
Priority: args.Priority,
Message: args.MESSAGE,
}
ret, err := modules.Notification.Create(s, jsonutils.Marshal(input))
if err != nil {
return err
}
printObject(ret)
return nil
})
type NotificationInput struct {
ID string `help:"Id of notification"`
}
R(&NotificationInput{}, "notify-show", "Show a notify message", func(s *mcclient.ClientSession, args *NotificationInput) error {
ret, err := modules.Notification.Get(s, args.ID, nil)
if err != nil {
return nil
}
printObject(ret)
return nil
})
type NotificationListInput struct {
options.BaseListOptions
ContactType string `help:"contact_type"`
ReceiverId string `help:"receiver_id"`
}
R(&NotificationListInput{}, "notify-list", "List notify message", func(s *mcclient.ClientSession, args *NotificationListInput) error {
params, err := options.ListStructToParams(args)
if err != nil {
return err
}
ret, err := modules.Notification.List(s, params)
if err != nil {
return err
}
printList(ret, modules.Notification.GetColumns(s))
return nil
})
}

View File

@@ -0,0 +1,165 @@
// 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 notifyv2
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type ReceiverListOptions struct {
options.BaseListOptions
UId string `help:"user id in keystone"`
UName string `help:"user name in keystone"`
EnabledContactType string `help:"enabled contact type"`
VerifiedContactType string `help:"verified contact type"`
}
R(&ReceiverListOptions{}, "notify-receiver-list", "List notify receiver", func(s *mcclient.ClientSession, args *ReceiverListOptions) error {
params, err := options.ListStructToParams(args)
if err != nil {
return err
}
result, err := modules.NotifyReceiver.List(s, params)
if err != nil {
return err
}
printList(result, modules.NotifyReceiver.GetColumns(s))
return nil
})
type ReceiverCreateOptions struct {
UID string `help:"user id in keystone"`
Email string `help:"email of receiver"`
Mobile string `help:"mobile of receiver"`
EnabledContactTypes []string `help:"enabled contact type"`
}
R(&ReceiverCreateOptions{}, "notify-receiver-create", "Create notify receiver", func(s *mcclient.ClientSession, args *ReceiverCreateOptions) error {
params := jsonutils.Marshal(args).(*jsonutils.JSONDict)
receiver, err := modules.NotifyReceiver.Create(s, params)
if err != nil {
return err
}
printObject(receiver)
return nil
})
type ReceiverUpdateInput struct {
ID string `help:"Id or Name of receiver"`
Email string `help:"email of receiver"`
Mobile string `help:"mobile of receiver"`
EnabledContactType []string `help:"enabled contact type"`
}
R(&ReceiverUpdateInput{}, "notify-receiver-update", "Update notify receiver", func(s *mcclient.ClientSession, args *ReceiverUpdateInput) error {
params := jsonutils.NewDict()
if len(args.Email) > 0 {
params.Set("email", jsonutils.NewString(args.Email))
}
if len(args.Mobile) > 0 {
params.Set("mobile", jsonutils.NewString(args.Mobile))
}
if len(args.EnabledContactType) > 0 {
params.Set("enabled_contact_types", jsonutils.NewStringArray(args.EnabledContactType))
}
ret, err := modules.NotifyReceiver.Update(s, args.ID, params)
if err != nil {
return err
}
printObject(ret)
return nil
})
type ReceiverOptions struct {
ID string `help:"Id or Name of receiver"`
}
R(&ReceiverOptions{}, "notify-receiver-show", "Show notify receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
ret, err := modules.NotifyReceiver.Get(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ReceiverOptions{}, "notify-receiver-delete", "Delete notify receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
receiver, err := modules.NotifyReceiver.Delete(s, args.ID, jsonutils.NewDict())
if err != nil {
return err
}
printObject(receiver)
return nil
})
R(&ReceiverOptions{}, "notify-receiver-enable", "Enable notify receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "enable", jsonutils.NewDict())
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ReceiverOptions{}, "notify-receiver-disable", "Disable notify receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "disable", jsonutils.NewDict())
if err != nil {
return err
}
printObject(ret)
return nil
})
type ReceiverTriggerVerifyInput struct {
ID string `help:"Id or Name of receiver"`
ContactType string `help:"Contact type to trigger verify" choices:"email|mobile"`
}
R(&ReceiverTriggerVerifyInput{}, "notify-receiver-trigger-verify", "Trigger verification for receiver about some contact", func(s *mcclient.ClientSession, args *ReceiverTriggerVerifyInput) error {
params := jsonutils.NewDict()
params.Set("contact_type", jsonutils.NewString(args.ContactType))
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "trigger-verify", params)
if err != nil {
return err
}
printObject(ret)
return nil
})
type ReceiverVerifyInput struct {
ID string `help:"Id or Name of receiver"`
ContactType string `help:"Contact type to trigger verify" choices:"email|mobile"`
Token string `help:"Token from verify message sent to you"`
}
R(&ReceiverVerifyInput{}, "notify-receiver-verify", "Verify receiver about some contact type", func(s *mcclient.ClientSession, args *ReceiverVerifyInput) error {
params := jsonutils.NewDict()
params.Set("contact_type", jsonutils.NewString(args.ContactType))
params.Set("token", jsonutils.NewString(args.Token))
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "verify", params)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ReceiverOptions{}, "notify-receiver-enable", "Enable receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "enable", nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&ReceiverOptions{}, "notify-receiver-disable", "Disable receiver", func(s *mcclient.ClientSession, args *ReceiverOptions) error {
ret, err := modules.NotifyReceiver.PerformAction(s, args.ID, "disable", nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
}

View File

@@ -0,0 +1,106 @@
// 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 notifyv2
import (
"yunion.io/x/jsonutils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/mcclient/options"
)
func init() {
type TemplateCreateInput struct {
NAME string `help:"Name"`
ContactType string `help:"Contact type, specifically, setting it to all means all contact type"`
TemplateType string `help:"Template type"`
Topic string `help:"Template topic"`
Content string `help:"Template content"`
Example string `help:"Example for using this template"`
}
R(&TemplateCreateInput{}, "notify-template-create", "Create notify template", func(s *mcclient.ClientSession, args *TemplateCreateInput) error {
input := api.TemplateCreateInput{
ContactType: args.ContactType,
TemplateType: args.TemplateType,
Topic: args.Topic,
Content: args.Content,
Example: args.Example,
}
input.Name = args.NAME
ret, err := modules.NotifyTemplate.Create(s, jsonutils.Marshal(input))
if err != nil {
return err
}
printObject(ret)
return nil
})
type TemplateListInput struct {
options.BaseListOptions
ContactType string `help:"Contact type"`
TemplateType string `help:"Template type"`
Topic string `help:"Topic"`
}
R(&TemplateListInput{}, "notify-template-list", "List notify template", func(s *mcclient.ClientSession, args *TemplateListInput) error {
params, err := options.ListStructToParams(args)
if err != nil {
return err
}
list, err := modules.NotifyTemplate.List(s, params)
if err != nil {
return err
}
printList(list, modules.NotifyTemplate.GetColumns(s))
return nil
})
type TemplateInput struct {
ID string `help:"id or name of template"`
}
R(&TemplateInput{}, "notify-template-get", "Get notify template", func(s *mcclient.ClientSession, args *TemplateInput) error {
ret, err := modules.NotifyTemplate.Get(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
type TemplateUpdateInput struct {
ID string `help:"id or name of template"`
Content string `help:"Template content"`
Example string `help:"Example for using this template"`
}
R(&TemplateUpdateInput{}, "notify-template-update", "Update notify template", func(s *mcclient.ClientSession, args *TemplateUpdateInput) error {
input := api.TemplateUpdateInput{
Content: args.Content,
Example: args.Example,
}
ret, err := modules.NotifyTemplate.Update(s, args.ID, jsonutils.Marshal(input))
if err != nil {
return err
}
printObject(ret)
return nil
})
R(&TemplateInput{}, "notify-template-delete", "Delete notify template", func(s *mcclient.ClientSession, args *TemplateInput) error {
ret, err := modules.NotifyTemplate.Delete(s, args.ID, nil)
if err != nil {
return err
}
printObject(ret)
return nil
})
}

View File

@@ -15,9 +15,9 @@
package main
import (
"yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/service"
)
func main() {
notify.StartService()
service.StartService()
}

63
pkg/apis/notify/config.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/apis"
)
type ConfigCreateInput struct {
apis.StandaloneResourceCreateInput
// description: config type
// required: true
// example: feishu
Type string `json:"type"`
// description: config content
// required: true
// example: {"app_id": "123456", "app_secret": "feishu_nihao"}
Content jsonutils.JSONObject `json:"content"`
}
type ConfigDetails struct {
apis.StandaloneResourceDetails
SConfig
}
type ConfigListInput struct {
apis.StandaloneResourceListInput
Type string `json:"type"`
}
type ConfigValidateInput struct {
// description: config type
// required: true
// example: feishu
Type string `json:"type"`
// description: config content
// required: true
// example: {"app_id": "123456", "app_secret": "feishu_nihao"}
Content jsonutils.JSONObject `json:"content"`
}
type ConfigValidateOutput struct {
IsValid bool `json:"is_valid"`
Message string `json:"message"`
}

63
pkg/apis/notify/const.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 "yunion.io/x/onecloud/pkg/apis"
const (
SERVICE_TYPE = apis.SERVICE_TYPE_NOTIFY
SERVICE_VERSION = ""
EMAIL = "email"
MOBILE = "mobile"
DINGTALK = "dingtalk"
FEISHU = "feishu"
WEBCONSOLE = "webconsole"
WORKWX = "workwx"
FEISHU_ROBOT = "feishu-robot"
DINGTALK_ROBOT = "dingtalk-robot"
WORKWX_ROBOT = "workwx-robot"
ROBOT = "robot"
RECEIVER_NOTIFICATION_RECEIVED = "received" // Received a task about sending a notification
RECEIVER_NOTIFICATION_SENT = "sending" // Nofity module has sent notification, but result unkown
RECEIVER_NOTIFICATION_OK = "sent_ok" // Notification was sent successfully
RECEIVER_NOTIFICATION_FAIL = "sent_fail" // That sent a notification is failed
VERIFICATION_SENT = "sent" // Verification was sent
VERIFICATION_SENT_FAIL = "sent_fail" // Verification was sent failed
VERIFICATION_VERIFIED = "verified" // Verification was verified
VERIFICATION_TOKEN_EXPIRED = "Verification code expired"
VERIFICATION_TOKEN_INVALID = "Incorrect verification code"
RECEIVER_STATUS_READY = "ready"
RECEIVER_STATUS_PULLING = "pulling"
RECEIVER_STATUS_PULL_FAILED = "pull_failed"
NOTIFICATION_PRIORITY_IMPORTANT = "important"
NOTIFICATION_PRIORITY_CRITICAL = "fatal"
NOTIFICATION_PRIORITY_NORMAL = "normal"
NOTIFICATION_STATUS_RECEIVED = "received"
NOTIFICATION_STATUS_SENDING = "sending"
NOTIFICATION_STATUS_FAILED = "failed"
NOTIFICATION_STATUS_OK = "ok"
NOTIFICATION_STATUS_PART_OK = "part_ok"
TEMPLATE_TYPE_TITLE = "title"
TEMPLATE_TYPE_CONTENT = "content"
TEMPLATE_TYPE_REMOTE = "remote"
)

View File

@@ -1,25 +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 "yunion.io/x/onecloud/pkg/apis"
type ContactDetails struct {
apis.ResourceBaseDetails
UID string `json:"uid"`
Name string `json:"name"`
Details string `json:"details"`
}

View File

@@ -15,18 +15,62 @@
package notify
import (
"yunion.io/x/jsonutils"
"time"
"yunion.io/x/onecloud/pkg/apis"
)
type NotificationDetails struct {
apis.ResourceBaseDetails
type NotificationCreateInput struct {
apis.StatusStandaloneResourceCreateInput
UserList jsonutils.JSONObject `json:"user_list"`
// description: ids or names of receiver
// required: false
// example: {"adfb720ccdd34c638346ea4fa7a713a8", "zhangsan"}
Receivers []string `json:"receivers"`
// description: direct contact, admin privileges required
// required: false
Contacts []string `json:"contacts"`
// description: contact type
// required: ture
// example: email
ContactType string `json:"contact_type"`
// description: notification topic
// required: true
// example: IMAGE_ACTIVE
Topic string `json:"topic"`
// description: notification priority
// required: false
// enum: fatal,important,nomal
// example: normal
Priority string `json:"priority"`
// description: message content or jsonobject
// required: ture
Message string `json:"message"`
}
type ReceiveDetail struct {
ReceiverId string `json:"receiver_id"`
ReceiverName string `json:"receiver_name"`
Contact string `json:"contact"`
SendAt time.Time `json:"sendAt"`
SendBy string `json:"send_by"`
Status string `json:"status"`
FailedReason string `json:"failed_reason"`
}
type NotificationDetails struct {
apis.StatusStandaloneResourceDetails
SNotification
Title string `json:"title"`
Content string `json:"content"`
ReceiveDetails []ReceiveDetail `json:"receive_details"`
}
type NotificationListInput struct {
Scope string `json:"scope"`
ContactType string `json:"contact_type"`
apis.StatusStandaloneResourceListInput
ContactType string
ReceiverId string
}

View File

@@ -1,22 +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 "yunion.io/x/onecloud/pkg/apis"
const (
SERVICE_TYPE = apis.SERVICE_TYPE_NOTIFY
SERVICE_VERSION = ""
)

110
pkg/apis/notify/receiver.go Normal file
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 notify
import (
"yunion.io/x/onecloud/pkg/apis"
)
type ReceiverCreateInput struct {
apis.StatusStandaloneResourceCreateInput
apis.DomainizedResourceCreateInput
apis.EnabledBaseResourceCreateInput
// description: user id in keystone
// example: adfb720ccdd34c638346ea4fa7a713a8
UID string `json:"uid"`
// description: user name in keystone
// example: hello
UName string `json:"uname"`
// description: user email
// example: example@gmail.com
Email string `json:"email"`
// description: user mobile
// example: 17812345678
Mobile string `json:"mobile"`
// description: enabled contact types for user
// example: {"email", "mobile", "feishu", "dingtalk", "workwx"}
EnabledContactTypes []string `json:"enabled_contact_types"`
}
type ReceiverDetails struct {
apis.StatusStandaloneResourceDetails
apis.DomainizedResourceInfo
SReceiver
// description: enabled contact types for user
// example: eamil, mobile, feishu, dingtalk, workwx
EnabledContactTypes []string `json:"enabled_contact_types"`
// description: verified contact types for user
// example: email, mobile, feishu, dingtalk, workwx
VerifiedContactTypes []string `json:"verified_contact_types"`
}
type ReceiverListInput struct {
apis.StatusStandaloneResourceListInput
apis.DomainizedResourceListInput
apis.EnabledResourceBaseListInput
UID string `json:"uid"`
UName string `json:"uname"`
EnabledContactType string `json:"enabled_contact_type"`
VerifiedContactType string `json:"verified_contact_type"`
}
type ReceiverUpdateInput struct {
apis.StatusStandaloneResourceBaseUpdateInput
// description: user email
// example: example@gmail.com
Email string `json:"email"`
// description: user mobile
// example: 17812345678
Mobile string `json:"mobile"`
// description: enabled contacts for user
// example: {"email", "mobile", "feishu", "dingtalk", "workwx"}
EnabledContactTypes []string `json:"enabled_contact_types"`
}
type ReceiverTriggerVerifyInput struct {
// description: contact type
// required: true
// example: email
// enum: email,mobile
ContactType string `json:"contact_type"`
}
type ReceiverVerifyInput struct {
// description: Contact type
// required: true
// example: email
// enum: email,mobile
ContactType string `json:"contact_type"`
// description: token user input
// required: true
// example: 123456
Token string `json:"token"`
}

View File

@@ -0,0 +1,82 @@
// 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 "yunion.io/x/onecloud/pkg/apis"
type TemplateCreateInput struct {
apis.StandaloneResourceCreateInput
// description: Contact type, specifically, setting it to all means all contact type
// require: true
// example: email
ContactType string `json:"contact_type"`
// description: Template type
// enum: title,content,remote
// example: title
TemplateType string `json:"template_type"`
// description: Template topic
// required: true
// example: IMAGE_ACTIVE
Topic string `json:"topic"`
// description: Template content
// required: true
// example: 镜像 {{.name}} 上传完成
Content string `json:"content"`
// description: Example for using this template
// required: true
// example: {"name": "centos7.6"}
Example string `json:"example"`
}
type TemplateListInput struct {
apis.StandaloneResourceListInput
// description: Contact type, specifically, setting it to all means all contact type
// require: true
// example: email
ContactType string `json:"contact_type"`
// description: Template type
// enum: title,content,remote
// example: title
TemplateType string `json:"template_type"`
// description: template topic
// required: true
// example: IMAGE_ACTIVE
Topic string `json:"topic"`
}
type TemplateUpdateInput struct {
apis.StandaloneResourceCreateInput
// description: template content
// required: true
// example: 镜像 {{.name}} 上传完成
Content string `json:"content"`
// description: all example for using this template
// required: true
// example: {"name": "centos7.6"}
Example string `json:"example"`
}
type TemplateDetails struct {
apis.StandaloneResourceDetails
STemplate
}

View File

@@ -0,0 +1,101 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by model-api-gen. DO NOT EDIT.
package notify
import (
time "time"
"yunion.io/x/onecloud/pkg/apis"
)
// SConfig is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SConfig.
type SConfig struct {
apis.SStandaloneResourceBase
Type string `json:"type"`
Content interface{} `json:"content"`
}
// SNotification is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SNotification.
type SNotification struct {
apis.SStatusStandaloneResourceBase
ContactType string `json:"contact_type"`
// swagger:ignore
Topic string `json:"topic"`
Priority string `json:"priority"`
// swagger:ignore
Message string `json:"message"`
ReceivedAt time.Time `json:"received_at"`
SendTimes int `json:"send_times"`
}
// SReceiver is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SReceiver.
type SReceiver struct {
apis.SStatusStandaloneResourceBase
apis.SDomainizedResourceBase
apis.SEnabledResourceBase
Email string `json:"email"`
Mobile string `json:"mobile"`
// swagger:ignore
EnabledEmail *bool `json:"enabled_email,omitempty"`
// swagger:ignore
VerifiedEmail *bool `json:"verified_email,omitempty"`
// swagger:ignore
EnabledMobile *bool `json:"enabled_mobile,omitempty"`
// swagger:ignore
VerifiedMobile *bool `json:"verified_mobile,omitempty"`
}
// SReceiverNotification is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SReceiverNotification.
type SReceiverNotification struct {
apis.SJointResourceBase
RecevierID string `json:"recevier_id"`
NotificationID string `json:"notification_id"`
// ignore if RecevierID is not empty
Contact string `json:"contact"`
SendBy string `json:"send_by"`
Status string `json:"status"`
FailedReason string `json:"failed_reason"`
}
// SSubContact is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubContact.
type SSubContact struct {
apis.SResourceBase
// id of receiver user
RecevierID string `json:"recevier_id"`
Type string `json:"type"`
Contact string `json:"contact"`
ParentContactType string `json:"parent_contact_type"`
Enabled *bool `json:"enabled,omitempty"`
Verified *bool `json:"verified,omitempty"`
}
// STemplate is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.STemplate.
type STemplate struct {
apis.SStandaloneResourceBase
ContactType string `json:"contact_type"`
Topic string `json:"topic"`
// title | content | remote
TemplateType string `json:"template_type"`
Content string `json:"content"`
Example string `json:"example"`
}
// SVerification is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SVerification.
type SVerification struct {
apis.SResourceBase
ReceiverId string `json:"receiver_id"`
ContactType string `json:"contact_type"`
Token string `json:"token"`
}

View File

@@ -74,6 +74,14 @@ func NewNotifyManager(keyword, keywordPlural string, columns, adminColumns []str
Keyword: keyword, KeywordPlural: keywordPlural}
}
func NewNotifyv2Manager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager(apis.SERVICE_TYPE_NOTIFY, "", "v2", columns, adminColumns),
Keyword: keyword,
KeywordPlural: keywordPlural,
}
}
func NewJointComputeManager(keyword, keywordPlural string, columns, adminColumns []string, master, slave modulebase.Manager) modulebase.JointResourceManager {
return modulebase.JointResourceManager{
ResourceManager: NewComputeManager(keyword, keywordPlural, columns, adminColumns),

View File

@@ -21,13 +21,43 @@ type ConfigsManager struct {
}
var (
Configs ConfigsManager
NotifyReceiver modulebase.ResourceManager
NotifyConfig modulebase.ResourceManager
Notification modulebase.ResourceManager
NotifyTemplate modulebase.ResourceManager
Configs ConfigsManager
)
func init() {
Configs = ConfigsManager{NewNotifyManager("config", "configs",
NotifyReceiver = NewNotifyv2Manager(
"receiver",
"receivers",
[]string{"ID", "Name", "Email", "Mobile", "Enabled_Contact_Types", "Verified_Contact_Types"},
[]string{},
[]string{})}
)
register(&NotifyReceiver)
register(&Configs)
NotifyConfig = NewNotifyv2Manager(
"notifyconfig",
"notifyconfigs",
[]string{"Type", "Content"},
[]string{},
)
register(&NotifyConfig)
Notification = NewNotifyv2Manager(
"notification",
"notifications",
[]string{"Title", "Content", "ContactType", "Priority", "Receiver_Details"},
[]string{},
)
register(&Notification)
NotifyTemplate = NewNotifyv2Manager(
"notifytemplate",
"notifytemplates",
[]string{"ID", "Name", "Contact_Type", "Topic", "Template_Type", "Content", "Example"},
[]string{},
)
register(&NotifyTemplate)
}

View File

@@ -1,43 +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 modules
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
)
type NotifyTemplateManager struct {
modulebase.ResourceManager
}
func (nm *NotifyTemplateManager) DeleteContents(s *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
path := "/notifytemplates/delete-template"
return modulebase.Post(nm.ResourceManager, s, path, params, "")
}
var (
NotifyTemplates NotifyTemplateManager
)
func init() {
NotifyTemplates = NotifyTemplateManager{NewNotifyManager("notifytemplate", "notifytemplates",
[]string{"Contact_Type", "Topic", "Template_Type"},
[]string{})}
register(&NotifyTemplates)
}

View File

@@ -15,9 +15,9 @@
package notify
import (
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
@@ -39,26 +39,54 @@ type SNotifyMessage struct {
Broadcast bool `json:"broadcast,omitempty"`
}
type SNotifyV2Message struct {
ReceiverIds []string `json:"receiver_ids"`
ContactType string `json:"contact_type"`
Topic string `json:"topic"`
Priority string `json:"priority"`
Message string `json:"message"`
}
type NotificationManager struct {
modulebase.ResourceManager
}
func (manager *NotificationManager) Send(s *mcclient.ClientSession, msg SNotifyMessage) error {
params := jsonutils.Marshal(&msg)
body := jsonutils.NewDict()
body.Add(params, manager.Keyword)
receiverIds := make([]string, 0, len(msg.Uid))
if len(msg.Gid) > 0 {
// fetch uid
uidSet := sets.NewString()
for _, gid := range msg.Gid {
users, err := modules.Groups.GetUsers(s, gid, nil)
if err != nil {
return errors.Wrapf(err, "Groups.GetUsers for group %q", gid)
}
for i := range users.Data {
id, _ := users.Data[i].GetString("id")
uidSet.Insert(id)
}
}
for _, uid := range uidSet.UnsortedList() {
receiverIds = append(receiverIds, uid)
}
}
receiverIds = append(receiverIds, msg.Uid...)
path := fmt.Sprintf("/%s?uname=true", manager.ContextPath(nil))
_, err := modulebase.Post(manager.ResourceManager, s, path, body, manager.KeywordPlural)
v2msg := SNotifyV2Message{
ReceiverIds: receiverIds,
ContactType: string(msg.ContactType),
Topic: msg.Topic,
Priority: string(msg.Priority),
Message: msg.Msg,
}
params := jsonutils.Marshal(&v2msg)
_, err := manager.Create(s, params)
return err
}
func init() {
Notifications = NotificationManager{
modules.NewNotifyManager("notification", "notifications",
[]string{"id", "uid", "contact_type", "topic", "priority", "msg", "received_at", "send_by", "status", "create_at", "update_at", "delete_at", "create_by", "update_by", "delete_by", "is_deleted", "broadcast", "remark"},
[]string{}),
modules.Notification,
}
modules.Register(&Notifications)
}

View File

@@ -1,38 +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
// TODO: fix this test
/*
import (
"testing"
"yunion.io/x/jsonutils"
)
func TestNotificationManager(t *testing.T) {
msg := SNotifyMessage{
Uid: "testuser",
ContactType: []TNotifyChannel{
NotifyByEmail, NotifyByWebConsole,
},
Topic: "test message",
Priority: NotifyPriorityNormal,
Msg: "This is a test message. Yey!!",
Remark: "Yunion",
}
msgJson := jsonutils.Marshal(msg)
t.Logf("msg: %s", msgJson)
}*/

View File

@@ -1,188 +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 cache
import (
"context"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/compare"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
type SUserGroupCacheManager struct {
db.SResourceBaseManager
}
type SUserGroup struct {
db.SResourceBase
UserId string
GroupId string
LastCheck time.Time `nullable:"false"`
}
func (ug *SUserGroup) GetModelManager() db.IModelManager {
return UserGroupCacheManager
}
var UserGroupCacheManager *SUserGroupCacheManager
func init() {
UserGroupCacheManager = &SUserGroupCacheManager{db.NewResourceBaseManager(
SUserGroup{},
"user_group_cache_tbl",
"usergroup",
"usergroups",
)}
}
func (ug *SUserGroup) IsExpired() bool {
if ug.LastCheck.IsZero() {
return true
}
now := time.Now().UTC()
if ug.LastCheck.Add(consts.GetTenantCacheExpireSeconds()).Before(now) {
return true
}
return false
}
func (manager *SUserGroupCacheManager) FetchByGroupId(ctx context.Context, groupId string) ([]SUserGroup, error) {
q := manager.Query().Equals("gourp_id", groupId)
ugs := make([]SUserGroup, 0)
err := db.FetchModelObjects(manager, q, &ugs)
if err != nil {
return nil, err
}
var needSync bool
if len(ugs) == 0 {
needSync = true
}
now := time.Now().UTC()
expireTime := now.Add(-consts.GetTenantCacheExpireSeconds())
for i := range ugs {
if ugs[i].LastCheck.Before(expireTime) {
needSync = true
break
}
}
if !needSync {
return ugs, nil
}
ugs, syncResult, err := manager.Sync(ctx, ugs, groupId)
if err != nil {
return nil, err
}
if syncResult.IsError() {
log.Errorf(syncResult.Result())
}
return ugs, nil
}
func (manager *SUserGroupCacheManager) Sync(ctx context.Context, ugCache []SUserGroup, groupId string) ([]SUserGroup,
compare.SyncResult, error) {
lockman.LockRawObject(ctx, manager.KeywordPlural(), groupId)
defer lockman.ReleaseRawObject(ctx, manager.KeywordPlural(), groupId)
syncResult := compare.SyncResult{}
// It's to query all groups and their users.
query := jsonutils.NewDict()
query.Set("scope", jsonutils.NewString("system"))
query.Set("system", jsonutils.JSONTrue)
s := auth.GetAdminSession(ctx, consts.GetRegion(), "v3")
users, err := modules.Groups.GetUsers(s, groupId, query)
if err != nil {
return nil, syncResult, errors.Wrap(err, "fetch users by group id from keystone failed")
}
newUgCache := make([]SUserGroup, len(users.Data))
for i := range users.Data {
userId, _ := users.Data[i].GetString("id")
newUgCache[i] = SUserGroup{
UserId: userId,
GroupId: groupId,
}
}
added := make([]SUserGroup, 0)
removed := make([]SUserGroup, 0)
commondb := make([]SUserGroup, 0)
compareSets(ugCache, newUgCache, &added, &removed, &commondb)
now := time.Now().UTC()
for i := range added {
added[i].LastCheck = now
err := manager.TableSpec().Insert(ctx, &added[i])
if err != nil {
syncResult.AddError(err)
} else {
syncResult.Add()
}
}
for i := range commondb {
ug := &commondb[i]
_, err := db.Update(ug, func() error {
ug.LastCheck = now
return nil
})
if err != nil {
syncResult.UpdateError(err)
} else {
syncResult.Update()
}
}
for i := range removed {
ug := &removed[i]
_, err := db.Update(ug, func() error {
return ug.MarkDelete()
})
if err != nil {
syncResult.DeleteError(err)
} else {
syncResult.Delete()
}
}
return newUgCache, syncResult, nil
}
func compareSets(dbs, remotes []SUserGroup, added, removed, commondb *[]SUserGroup) {
dbmap := make(map[string]SUserGroup)
for i := range dbs {
dbmap[dbs[i].UserId] = dbs[i]
}
for i := range remotes {
userId := remotes[i].UserId
if _, ok := dbmap[userId]; ok {
*commondb = append(*commondb, remotes[i])
} else {
*added = append(*added, remotes[i])
}
delete(dbmap, userId)
}
for _, v := range dbmap {
*removed = append(*removed, v)
}
}

View File

@@ -1,128 +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"
"net/http"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/httperrors"
)
const (
EMAIL_KEYWORDPLURAL = "email_configs"
EMAIL_KEYWORD = "email_config"
EMAIL = "email"
SMS_KEYWORDPLURAL = "sms_configs"
SMS_KEYWORD = "sms_config"
SMS = "mobile"
)
func emailConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// do not need modify
manager, _, _, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": EMAIL,
}
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func emailConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": EMAIL,
}
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
data, _ := ret.Get("config")
dataDict := data.(*jsonutils.JSONDict)
output := jsonutils.NewDict()
output.Add(dataDict, EMAIL_KEYWORD)
appsrv.SendJSON(w, output)
}
func emailConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
body, _ = body.Get(EMAIL_KEYWORD)
bodyRet := jsonutils.DeepCopy(body)
newBody := jsonutils.NewDict()
newBody.Add(body, EMAIL)
err := manager.UpdateConfig(ctx, newBody)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
data := jsonutils.NewDict()
data.Add(jsonutils.NewInt(200), "code")
data.Add(jsonutils.NewString("OK"), "title")
data.Add(bodyRet, "message")
ret := jsonutils.NewDict()
ret.Add(data, EMAIL_KEYWORD)
appsrv.SendJSON(w, ret)
}
func smsConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// do not need modify
manager, _, _, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": SMS,
}
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func smsConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": SMS,
}
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
// modify
data, _ := ret.Get("config")
appsrv.SendJSON(w, jsonutils.Marshal(map[string]jsonutils.JSONObject{
SMS_KEYWORD: data,
}))
}
func smsConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
body, _ = body.Get(SMS_KEYWORD)
newBody := jsonutils.NewDict()
newBody.Add(body, SMS)
err := manager.UpdateConfig(ctx, newBody)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
data := jsonutils.NewDict()
data.Add(jsonutils.NewInt(200), "code")
data.Add(jsonutils.NewString("OK"), "title")
data.Add(body, "message")
ret := jsonutils.NewDict()
ret.Add(data, SMS_KEYWORD)
appsrv.SendJSON(w, ret)
}

View File

@@ -1,774 +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"
"fmt"
"net/http"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/appctx"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/models"
noutils "yunion.io/x/onecloud/pkg/notify/utils"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
// NotifyModelDispatcher is designed to complete some function that db.DBModelDispatcher can't.
// The apis of notify module has a certain degree of particularity so that we can't use common function.
type NotifyModelDispatcher struct {
db.DBModelDispatcher
}
func NewNotifyModelDispatcher(manager db.IModelManager) *NotifyModelDispatcher {
return &NotifyModelDispatcher{*db.NewModelHandler(manager)}
}
func (self *NotifyModelDispatcher) GetConfig(ctx context.Context, params map[string]string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
listResult, err := self.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
return nil, err
}
configs := jsonutils.NewDict()
for _, data := range listResult.Data {
key, _ := data.GetString("key_text")
value, _ := data.Get("value_text")
configs.Add(value, key)
}
cType, ok := params["<type>"]
if ok {
configs = models.ConfigManager.Database2Display(cType, configs)
}
output := jsonutils.NewDict()
output.Add(configs, models.ConfigManager.Keyword())
return output, nil
}
func (self *NotifyModelDispatcher) DeleteConfig(ctx context.Context, params map[string]string) error {
contactType := params["<type>"]
configs, err := models.ConfigManager.GetConfigByType(contactType)
if err != nil {
return errors.Wrap(err, "Get Config by contactType failed")
}
userCred := policy.FetchUserCredential(ctx)
for i := range configs {
err = DeleteItem(&configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return errors.Wrapf(err, "Delete part of old one, so please input new data again.")
}
}
return nil
}
// UpdateConfig update config and restart corresponding send service.
func (self *NotifyModelDispatcher) UpdateConfig(ctx context.Context, body jsonutils.JSONObject) error {
data := body.(*jsonutils.JSONDict)
contactType := data.SortedKeys()[0]
originData, err := models.ConfigManager.GetConfig(contactType)
if err != nil {
return err
}
tmp, _ := data.Get(contactType)
data = tmp.(*jsonutils.JSONDict)
data = models.ConfigManager.Display2Database(contactType, data)
userCred := policy.FetchUserCredential(ctx)
// If no config of type 'contactType' in database, create news.
// Else delete original ones and create news.
if len(originData) != 0 {
// delete original
configs, err := models.ConfigManager.GetConfigByType(contactType)
if err != nil {
return errors.Wrap(err, "Get Config by contactType failed")
}
for i := range configs {
err = DeleteItem(&configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return errors.Wrapf(err, "Delete part of old one, so please input new data again.")
}
}
}
log.Debugf("update body: %s", data)
keys := data.SortedKeys()
config := make(map[string]string)
createDataList := make([]jsonutils.JSONObject, 0, len(keys))
// Extract data
for _, key := range keys {
createData := jsonutils.NewDict()
tmp, _ = data.Get(key)
createData.Add(tmp, "value_text")
createData.Add(jsonutils.NewString(key), "key_text")
createData.Add(jsonutils.NewString(contactType), "type")
createDataList = append(createDataList, createData)
value, _ := tmp.GetString()
config[key] = value
}
// validate configs
log.Debugf("config: %#v", config)
isValid, message, err := models.NotifyService.ValidateConfig(ctx, contactType, config)
if err != nil {
if errors.Cause(err) != errors.ErrNotImplemented {
return httperrors.NewInternalServerError("Validate Config error: %s", err.Error())
}
isValid = true
}
if !isValid {
return httperrors.NewInputParameterError("validate failed: %s", message)
}
// create
for _, createData := range createDataList {
_, err := self.Create(ctx, jsonutils.NewDict(), createData, nil)
if err != nil {
return errors.Wrapf(err, "Create config %s for contact type %s failed", createData.String(), contactType)
}
}
// update config
models.RestartService(config, contactType)
return nil
}
func (self *NotifyModelDispatcher) ValidateConfig(ctx context.Context, contactType string, body jsonutils.JSONObject) error {
dict, ok := body.(*jsonutils.JSONDict)
if !ok {
return httperrors.NewInputParameterError("")
}
dict = models.ConfigManager.Display2Database(contactType, dict)
configs := make(map[string]string)
for _, key := range dict.SortedKeys() {
value, err := dict.GetString(key)
if err != nil {
return errors.Wrap(err, "jsonutils.JsonDict.GetString")
}
configs[key] = value
}
isValid, message, err := models.NotifyService.ValidateConfig(ctx, contactType, configs)
if err != nil {
if errors.Cause(err) == errors.ErrNotImplemented {
return httperrors.NewNotImplementedError("validating config of %s", contactType)
}
return err
}
if isValid == false {
return httperrors.NewInputParameterError(message)
}
return nil
}
// CreateNotification create new notifications and send them through rpc.RpcService.
// If data contains 'gid' field, that means that send message to all users in group.
// Else send messager to user whose uid equals 'uid' in data.
func (self *NotifyModelDispatcher) CreateNotification(ctx context.Context, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
// Get all contacts info of group if data contains "gid".
// If no contact, return ErrContactNotFound.
contactType, _ := data.GetString("contact_type")
group := false
var ids []string
if data.Contains("gid") {
group = true
ids = self.getIds(data, "gid")
} else {
ids = self.getIds(data, "uid")
}
contacts, err := models.ContactManager.GetAllNotify(ctx, ids, contactType, group)
if err != nil {
return nil, httperrors.NewGeneralError(errors.Wrap(err, "get all contacts error"))
}
notificationIDs, err := models.NotificationManager.BatchCreate(ctx, data, contacts)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
ret := jsonutils.NewDict()
ret.Add(jsonutils.NewStringArray(notificationIDs), "notifications")
return ret, nil
}
func (self *NotifyModelDispatcher) getIds(data jsonutils.JSONObject, key string) []string {
var ids []string
tmpIds, err := data.GetArray(key)
if err != nil {
id, _ := data.GetString(key)
ids = make([]string, 1)
ids[0] = id
} else {
ids = noutils.JsonArrayToStringArray(tmpIds)
}
// remove entry in which empty content
ret := make([]string, 0, len(ids))
for _, id := range ids {
if len(id) == 0 {
continue
}
ret = append(ret, id)
}
return ret
}
// Verify process:
// 1.fetch verify by ID; 2.check that if verify is expired;
// 3.if not check that if token is correct and update status of contact whose id is verify's CID
// 4.otherwise generate a new verify and delete old one
func (self *NotifyModelDispatcher) Verify(ctx context.Context, params map[string]string, query jsonutils.JSONObject) error {
processID := params["<id>"]
token, _ := query.GetString("token")
manager := models.VerifyManager
verifys, err := manager.FetchByID(processID)
if err != nil {
return httperrors.NewGeneralError(err)
}
var verifition models.SVerify
if len(verifys) == 0 {
return httperrors.NewNotFoundError("%s verify record not found", processID)
}
current, have := time.Now(), false
for i := range verifys {
if current.Before(verifys[i].ExpireAt) {
verifition = verifys[i]
have = true
break
}
}
if !have {
return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_EXPIRED)
}
if verifition.Token != token {
return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_INVALID)
}
// modify contact's status and verified time.
data := jsonutils.NewDict()
data.Set("status", jsonutils.NewString(models.VERIFICATION_VERIFIED))
data.Set("verified_at", jsonutils.NewTimeString(current))
_, err = self.Update(ctx, verifition.CID, jsonutils.NewDict(), data, nil)
if err != nil {
return httperrors.NewGeneralError(err)
}
return nil
}
// VerifyTrigger process:
// 1.fetch contact by the information in data
// 2.if contact'status is 'init', make a new verify and send a verify message to the contact adress
// 3.if contact'status is 'verifying', fetch verify by CID, generate a new verify if it has expired
// or return a error mention that "please don't try again".
func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map[string]string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
uid := params["<uid>"]
contact, _ := data.GetString("contact")
contactType, _ := data.GetString("contact_type")
contacts, err := models.ContactManager.FetchByMore(uid, contact, contactType)
if err != nil || len(contacts) == 0 {
return nil, errors.Error(fmt.Sprintf("uid '%s' don't have contact '%s' of contact_type '%s'", uid, contact, contactType))
}
userCred := policy.FetchUserCredential(ctx)
scontact := contacts[0]
makeNewVerify := func() (jsonutils.JSONObject, error) {
verification := models.NewSVerify(contactType, scontact.ID)
err = models.VerifyManager.Create(ctx, userCred, verification)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
// update contact state
if scontact.Status != models.CONTACT_VERIFYING {
scontact.SetStatus(userCred, models.CONTACT_VERIFYING, "")
}
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
processID := verification.ID
err := models.SendVerifyMessage(ctx, userCred, verification, &scontact)
if err != nil {
return nil, err
}
ret := map[string]map[string]string{
"contact": {
"process_id": processID,
},
}
return jsonutils.Marshal(ret), nil
}
if scontact.Status == models.CONTACT_INIT {
return makeNewVerify()
}
if scontact.Status == models.CONTACT_VERIFYING {
verifications, err := models.VerifyManager.FetchByCID(scontact.ID, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
q = q.In("status", []string{models.VERIFICATION_SENT, "init"}).Desc("created_at")
return q
})
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
if len(verifications) == 0 {
// no verifications in status "sent"
return makeNewVerify()
}
current := time.Now()
for _, verification := range verifications {
if current.After(verification.ExpireAt) {
//delete old one
err = DeleteItem(&verification, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
return makeNewVerify()
}
}
return nil, httperrors.NewGeneralError(models.ErrVeritying)
}
return jsonutils.JSONNull, nil
}
// DeleteContacts delete a group of contacts
func (self *NotifyModelDispatcher) DeleteContacts(ctx context.Context, uidArray []jsonutils.JSONObject) error {
// Get all id of uid
uids := make([]string, len(uidArray))
log.Debugf("uidArray: %s", uidArray)
for i := range uidArray {
uids[i], _ = uidArray[i].GetString()
}
log.Debugf("uids: %#v", uids)
uname := false
if v := ctx.Value("uname"); v != nil {
uname = true
}
contacts, err := models.ContactManager.FetchByUIDs(ctx, uids, uname)
if err != nil {
return httperrors.NewGeneralError(err)
}
userCred := policy.FetchUserCredential(ctx)
deleteFailed := make([]string, 0, 1)
for _, contact := range contacts {
err = DeleteItem(&contact, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
deleteFailed = append(deleteFailed, contact.ID)
}
}
// clean cache
noutils.DeleteUsers(ctx, userCred, uids)
if len(deleteFailed) != 0 {
errInfo := strings.Join(deleteFailed, ", ") + " ; these contact delete failed."
return errors.Error(errInfo)
}
return nil
}
// UpdateContacts analysis the data and update corresponding contacts if they exist in the database create new ones.
func (self *NotifyModelDispatcher) UpdateContacts(ctx context.Context, idstr string, query jsonutils.JSONObject,
datas []jsonutils.JSONObject, pullCtypes []jsonutils.JSONObject, ctxIds []dispatcher.SResourceContext) (jsonutils.JSONObject,
error) {
type pair struct {
contact string
enabled string
}
// contactInfos will be used to find all contact info need to update.
// And others will be created.
contactInfos := make(map[string]pair)
contactTypes := make([]string, len(datas))
for i := range datas {
contactType, _ := datas[i].GetString("contact_type")
if _, ok := models.UpdateNotAllow[contactType]; ok {
continue
}
contact, _ := datas[i].GetString("contact")
enabled := "-1"
if datas[i].Contains("enabled") {
enabled, _ = datas[i].GetString("enabled")
}
contactInfos[contactType] = pair{contact, enabled}
contactTypes[i] = contactType
}
records, err := models.ContactManager.FetchByUIDAndCType(idstr, contactTypes)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
// updateFailed record the information of failed update
updateFailed := make([]string, 0, 1)
deleteFailed := make([]string, 0, 1)
// UpdateItem contact info
userCred := policy.FetchUserCredential(ctx)
for i := range records {
contactType := records[i].ContactType
pairUpdate := contactInfos[contactType]
if len(pairUpdate.contact) == 0 {
// delete
err = DeleteItem(&records[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
deleteFailed = append(deleteFailed, fmt.Sprintf(`uid:%q, contact_type:%q`, idstr, contactType))
}
continue
}
updateData := jsonutils.NewDict()
updateData.Set("contact", jsonutils.NewString(pairUpdate.contact))
if pairUpdate.enabled != "-1" {
updateData.Set("enabled", jsonutils.NewString(pairUpdate.enabled))
}
if records[i].Contact != pairUpdate.contact {
updateData.Set("status", jsonutils.NewString(models.CONTACT_INIT))
}
// update is not relational
//updateData.Set("status", jsonutils.NewString("init"))
err = UpdateItem(models.ContactManager, &records[i], ctx, userCred, jsonutils.JSONNull, updateData)
if err != nil {
updateFailed = append(updateFailed, fmt.Sprintf(`uid:%q, contact_type:%q, contact:%q`, idstr, contactType, pairUpdate.contact))
}
delete(contactInfos, contactType)
}
// createFailed record the information of failed creation
createFailed := make([]string, 0, 1)
// Create contact info
type CreateData struct {
UID string
ContactType string
Contact string
Enabled string
}
newDatas := make([]CreateData, 0, len(contactInfos))
for conType, conPair := range contactInfos {
tmp := CreateData{
UID: idstr,
ContactType: conType,
Contact: conPair.contact,
}
if conPair.enabled != "-1" {
tmp.Enabled = conPair.enabled
}
newDatas = append(newDatas, tmp)
}
// Enable closing feishu and dingtalk notify.
// Temporary solution for 3.2.
allPullType := sets.NewString(models.FEISHU, models.DINGTALK)
pulls := make([]string, len(pullCtypes))
for i := range pullCtypes {
pulls[i], _ = pullCtypes[i].GetString()
allPullType.Delete(pulls[i])
}
// delete first
records, err = models.ContactManager.FetchByUIDAndCType(idstr, allPullType.UnsortedList())
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
for i := range records {
err := DeleteItem(&records[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
deleteFailed = append(deleteFailed, fmt.Sprintf(`uid:%q, contact_type:%q`, idstr, records[i].ContactType))
}
}
if len(pulls) > 0 {
contacts, err := models.ContactManager.FetchByUIDAndCType(idstr, pulls)
if err != nil {
return nil, err
}
set := sets.NewString(pulls...)
for i := range contacts {
set.Delete(contacts[i].ContactType)
}
for _, ct := range set.UnsortedList() {
tmp := CreateData{
UID: idstr,
ContactType: ct,
Enabled: "1",
Contact: "user_id",
}
newDatas = append(newDatas, tmp)
}
}
log.Debugf("newDatas: %s", newDatas)
for _, newData := range newDatas {
_, err := self.Create(ctx, jsonutils.NewDict(), jsonutils.Marshal(newData), ctxIds)
if err != nil {
createFailed = append(createFailed, fmt.Sprintf(`uid:%q, contact_type:%q, contact:%q`, idstr,
newData.ContactType, newData.Contact))
}
}
// generate error through updateFailed and createFailed
if len(updateFailed) != 0 || len(createFailed) != 0 || len(deleteFailed) != 0 {
var errInfoBuffer strings.Builder
if len(updateFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(updateFailed, "; "))
errInfoBuffer.WriteString(" update failed. ")
}
if len(deleteFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(updateFailed, "; "))
errInfoBuffer.WriteString(" delete failed. ")
}
if len(createFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(createFailed, "; "))
errInfoBuffer.WriteString(" create failed. ")
}
errInfo := errInfoBuffer.String()
return nil, httperrors.NewGeneralError(errors.Error(errInfo))
}
models.PullContact(idstr, pulls)
// keep the return value same as this of the GET interface
ret := jsonutils.NewDict()
contacts, err := models.ContactManager.FetchByUIDs(ctx, []string{idstr}, false)
if err != nil {
log.Errorf("fetch contact %s: %v", idstr, err)
return ret, nil
}
if len(contacts) == 0 {
return nil, nil
}
contact := contacts[0]
outDetails, err := contact.GetExtraDetails(ctx, userCred, ret, false)
if err != nil {
log.Errorf("fetch contact details %s(%s): %v",
contact.GetName(), contact.GetId(), err)
return ret, nil
}
out := jsonutils.Marshal(outDetails)
out.(*jsonutils.JSONDict).Set("created_at", jsonutils.NewString(contact.CreatedAt.String()))
return out, nil
}
func (self *NotifyModelDispatcher) UpdateTemplate(ctx context.Context, ctype string, query jsonutils.JSONObject,
datas []jsonutils.JSONObject) error {
type sTemplate struct {
ContactType string
Topic string
TemplateType string
Content string
}
templates := make([]sTemplate, 0, len(datas))
topics := sets.NewString()
for _, data := range datas {
var tem sTemplate
err := data.Unmarshal(&tem)
if err != nil {
return errors.Wrap(err, "data.Unmarshal")
}
if tem.TemplateType != models.TEMPLATE_TYPE_REMOTE && tem.TemplateType != models.
TEMPLATE_TYPE_CONTENT && tem.TemplateType != models.TEMPLATE_TYPE_TITLE {
return httperrors.NewInputParameterError("no support for such template type '%s'", tem.TemplateType)
}
tem.ContactType = ctype
tem.Topic = strings.ToUpper(tem.Topic)
templates = append(templates, tem)
topics.Insert(tem.Topic)
}
q := models.TemplateManager.Query().Equals("contact_type", ctype).In("topic", topics.List())
templateModels := make([]models.STemplate, 0, 1)
err := db.FetchModelObjects(models.TemplateManager, q, &templateModels)
if err != nil {
log.Errorf("db.FetchModelObjects sql: %s", q.String())
return errors.Wrap(err, "db.FetchModelObjects")
}
templateMaps := make(map[string]*models.STemplate)
for i := range templateModels {
k := fmt.Sprintf("%s/%s/%s", templateModels[i].ContactType, templateModels[i].Topic, templateModels[i].TemplateType)
templateMaps[k] = &templateModels[i]
}
userCred := policy.FetchUserCredential(ctx)
for _, tem := range templates {
k := fmt.Sprintf("%s/%s/%s", tem.ContactType, tem.Topic, tem.TemplateType)
if tmod, ok := templateMaps[k]; ok {
updateData := jsonutils.NewDict()
updateData.Add(jsonutils.NewString(tem.Content), "content")
err = UpdateItem(models.TemplateManager, tmod, ctx, userCred, query, updateData)
if err != nil {
return errors.Wrapf(err, "fail to update template '%s'", tmod.ID)
}
continue
}
_, err = self.Create(ctx, query, jsonutils.Marshal(tem), nil)
if err != nil {
return errors.Wrapf(err, "fail to create template(contact_type: %s, topic: %s, template_type: %s)",
tem.ContactType, tem.Topic, tem.TemplateType)
}
}
return nil
}
func (self *NotifyModelDispatcher) DeleteTemplate(ctx context.Context, query jsonutils.JSONObject, ctype, topic string) error {
q := models.TemplateManager.Query().Equals("contact_type", ctype)
if len(topic) != 0 {
q = q.Equals("topic", topic)
}
templates := make([]models.STemplate, 0, 1)
err := db.FetchModelObjects(models.TemplateManager, q, &templates)
if err != nil {
log.Errorf("db.FetchModelObjects sql: %s", q.String())
return errors.Wrap(err, "db.FetchModelObjects")
}
userCred := policy.FetchUserCredential(ctx)
for i := range templates {
err = DeleteItem(&templates[i], ctx, userCred, query, jsonutils.JSONNull)
if err != nil {
return errors.Wrapf(err, "fail to delete template '%s'", templates[i].ID)
}
}
return nil
}
// fetchEnv fetch handler, params, query and body from ctx(context.Context)
func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*NotifyModelDispatcher, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) {
params, query, body := appsrv.FetchEnv(ctx, w, r)
metadata := appctx.AppContextMetadata(ctx)
manager, ok := metadata["manager"].(*NotifyModelDispatcher)
if !ok {
log.Fatalf("No manager found for URL: %s", r.URL)
}
return manager, params, query, body
}
func mergeQueryParams(params map[string]string, query jsonutils.JSONObject, excludes ...string) jsonutils.JSONObject {
if query == nil {
query = jsonutils.NewDict()
}
queryDict := query.(*jsonutils.JSONDict)
for k, v := range params {
if !utils.IsInStringArray(k, excludes) {
queryDict.Add(jsonutils.NewString(v), k[1:len(k)-1])
}
}
return queryDict
}
// DeleteItem delete a database record corresponding to model
func DeleteItem(model db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
lockman.LockObject(ctx, model)
defer lockman.ReleaseObject(ctx, model)
err := model.ValidateDeleteCondition(ctx)
if err != nil {
log.Errorf("validate delete condition error: %s", err)
return err
}
err = db.CustomizeDelete(model, ctx, userCred, query, data)
if err != nil {
log.Errorf("customize delete error: %s", err)
return httperrors.NewNotAcceptableError("%v", err)
}
model.PreDelete(ctx, userCred)
err = model.Delete(ctx, userCred)
if err != nil {
log.Errorf("Delete error %s", err)
return err
}
model.PostDelete(ctx, userCred)
return nil
}
// UpdateItem update a database record corresponding to model whose update fields are in data
func UpdateItem(manager db.IModelManager, item db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
lockman.LockObject(ctx, item)
defer lockman.ReleaseObject(ctx, item)
var err error
err = item.ValidateUpdateCondition(ctx)
if err != nil {
log.Errorf("validate update condition error: %s", err)
return httperrors.NewGeneralError(err)
}
dataDict, ok := data.(*jsonutils.JSONDict)
if !ok {
return httperrors.NewInternalServerError("Invalid data JSONObject")
}
dataDict, err = db.ValidateUpdateData(item, ctx, userCred, query, dataDict)
if err != nil {
errMsg := fmt.Sprintf("validate update data error: %s", err)
log.Errorf(errMsg)
return httperrors.NewGeneralError(err)
}
item.PreUpdate(ctx, userCred, query, dataDict)
diff, err := db.Update(item, func() error {
filterData := dataDict.CopyIncludes(updateFields(manager, userCred)...)
err = filterData.Unmarshal(item)
if err != nil {
errMsg := fmt.Sprintf("unmarshal fail: %s", err)
log.Errorf(errMsg)
return httperrors.NewGeneralError(err)
}
return nil
})
if err != nil {
log.Errorf("save update error: %s", err)
return httperrors.NewGeneralError(err)
}
db.OpsLog.LogEvent(item, db.ACT_UPDATE, diff, userCred)
item.PostUpdate(ctx, userCred, query, data)
return nil
}
// get the field of model which is d
func updateFields(manager db.IModelManager, userCred mcclient.TokenCredential) []string {
ret := make([]string, 0)
for _, col := range manager.TableSpec().Columns() {
tags := col.Tags()
update := tags["update"]
if allowAction(manager, userCred, update, db.IsAllowUpdate) {
ret = append(ret, col.Name())
}
}
return ret
}
func allowAction(manager db.IResource, userCred mcclient.TokenCredential, action string, testfunc func(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, manager db.IResource) bool) bool {
if action == "user" {
return true
}
if action == "domain" && (testfunc(rbacutils.ScopeDomain, userCred, manager) || testfunc(rbacutils.ScopeSystem, userCred, manager)) {
return true
}
if action == "admin" && testfunc(rbacutils.ScopeSystem, userCred, manager) {
return true
}
return false
}

View File

@@ -1,450 +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"
"fmt"
"net/http"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/appctx"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/notify/cache"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/notify/utils"
)
var API_VERSION = "api/v1"
func InitHandlers(app *appsrv.Application) {
// add version handler with API_VERSION prefix
app.AddDefaultHandler("GET", API_VERSION+"/version", appsrv.VersionHandler, "version")
db.AddScopeResourceCountHandler(API_VERSION, app)
db.RegisterModelManager(models.ContactManager)
db.RegisterModelManager(models.VerifyManager)
db.RegisterModelManager(models.NotificationManager)
db.RegisterModelManager(models.ConfigManager)
db.RegisterModelManager(cache.UserCacheManager)
db.RegisterModelManager(cache.UserGroupCacheManager)
db.RegisterModelManager(models.TemplateManager)
AddNotifyDispatcher(API_VERSION, app)
}
// Middleware
func middleware(f appsrv.FilterHandler) appsrv.FilterHandler {
hander := func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
if _, ok := r.URL.Query()["uname"]; ok {
// Uname
log.Debugf("url.Query has a uname")
params := appctx.AppContextParams(ctx)
if uid, ok := params["<uid>"]; ok {
userDetail, err := utils.GetUserByIDOrName(ctx, uid)
if err != nil {
httperrors.NotFoundError(ctx, w, "Uid or Uname Not Found")
return
}
log.Debugf("find userDetail, id: %s, name: %s", userDetail.Id, userDetail.Name)
params["<uid>"] = userDetail.Id
}
ctx = context.WithValue(ctx, "uname", true)
}
f(ctx, w, r)
}
if consts.IsRbacEnabled() {
return auth.AuthenticateWithDelayDecision(hander, true)
} else {
return auth.Authenticate(hander)
}
}
func AddNotifyDispatcher(prefix string, app *appsrv.Application) {
var metadata map[string]interface{}
var tags map[string]string
// Contact Handler
modelDispatcher := NewNotifyModelDispatcher(models.ContactManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<uid>/update-contact", prefix, modelDispatcher.KeywordPlural()),
middleware(contactUpdateHandler), metadata, "contact_update", tags)
// List
app.AddHandler2("GET",
fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()),
middleware(listHandler), metadata, "list_contacts", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/users", prefix, modelDispatcher.KeywordPlural()),
middleware(keyStoneUserListHandler), metadata, "list_users", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<uid>", prefix, modelDispatcher.KeywordPlural()),
middleware(getHandler), metadata, "list_by_uid", tags)
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/delete-contact", prefix, modelDispatcher.KeywordPlural()),
middleware(deleteContactHandler), metadata, "delete", tags)
// verify-trigger
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<uid>/verify", prefix, modelDispatcher.KeywordPlural()),
middleware(verifyTriggerHandler), metadata, "verify_trigger", tags)
// Verify Handler, this modelDispatcher need db.DBModelDispatcher'Create function to create Contact so this modelDispatcher is
// NotifyModelDispatcher whose DBModelDispatcher has modelManager models.ContactManager
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": models.VerifyManager.KeywordPlural()}
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<id>", prefix, models.VerifyManager.KeywordPlural()),
middleware(verifyHandler), metadata, "verify", tags)
// notification Handler
modelDispatcher = NewNotifyModelDispatcher(models.NotificationManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
middleware(notificationHandler), metadata, "send_notifications", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
middleware(listHandler), metadata, "send_notifications", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<id>", prefix, modelDispatcher.KeywordPlural()),
middleware(listHandler), metadata, "list_notification_by_id", tags)
// config Handler
modelDispatcher = NewNotifyModelDispatcher(models.ConfigManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
middleware(configUpdateHandler), metadata, "update_configs", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, modelDispatcher.KeywordPlural()),
middleware(configGetHandler), metadata, "get_configs", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, modelDispatcher.KeywordPlural()),
middleware(configDeleteHandler), metadata, "delete_configs", tags)
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<type>/validate", prefix, modelDispatcher.KeywordPlural()),
middleware(configValidateHandler), metadata, "validate_configs", tags)
// email handler for being compatible
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL),
middleware(emailConfigUpdateHandler), metadata, "", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
middleware(emailConfigGetHandler), metadata, "", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
middleware(emailConfigDeleteHandler), metadata, "", tags)
app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
middleware(emailConfigUpdateHandler), metadata, "", tags)
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL),
middleware(smsConfigUpdateHandler), metadata, "", tags)
app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
middleware(smsConfigGetHandler), metadata, "", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
middleware(smsConfigDeleteHandler), metadata, "", tags)
app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
middleware(smsConfigUpdateHandler), metadata, "", tags)
// Template Handler
modelDispatcher = NewNotifyModelDispatcher(models.TemplateManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<ctype>/update-template", prefix, modelDispatcher.KeywordPlural()),
middleware(templateUpdateHandler), metadata, "update_template", tags)
// List
app.AddHandler2("GET",
fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()),
middleware(listHandler), metadata, "list_template", tags)
app.AddHandler2("POST",
fmt.Sprintf("%s/%s/delete-template", prefix, modelDispatcher.KeywordPlural()),
middleware(deleteTemplateHandler), metadata, "delete", tags)
}
func templateUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, body := fetchEnv(ctx, w, r)
data, err := body.GetArray(manager.Keyword(), manager.KeywordPlural())
if err != nil {
httperrors.GeneralServerError(ctx, w, httperrors.NewInputParameterError("need %s and %s",
manager.Keyword(), manager.KeywordPlural()))
return
}
ctype := params["<ctype>"]
if len(ctype) == 0 {
httperrors.InputParameterError(ctx, w, "ctype of template should not be empty")
}
err = manager.UpdateTemplate(ctx, ctype, mergeQueryParams(params, query), data)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func deleteTemplateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, body := fetchEnv(ctx, w, r)
if !body.Contains("contact_type") {
httperrors.InputParameterError(ctx, w, "miss contact_type")
return
}
ctype, _ := body.GetString("contact_type")
topic, _ := body.GetString("topic")
err := manager.DeleteTemplate(ctx, query, ctype, topic)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func configDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, _ := fetchEnv(ctx, w, r)
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func configGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
appsrv.SendJSON(w, ret)
}
func configUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
body, err := body.Get(models.ConfigManager.Keyword())
if err != nil {
httperrors.GeneralServerError(ctx, w, httperrors.NewInputParameterError("need config or configs"))
return
}
err = manager.UpdateConfig(ctx, body)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
func configValidateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, body := fetchEnv(ctx, w, r)
body, err := body.Get(models.ConfigManager.Keyword())
if err != nil {
httperrors.GeneralServerError(ctx, w, httperrors.NewInputParameterError("need config"))
}
ctype := params["<type>"]
err = manager.ValidateConfig(ctx, ctype, body)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
}
func notificationHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
data, err := body.Get(manager.Keyword())
if err != nil {
httperrors.BadRequestError(ctx, w, "request body should contain %s", manager.Keyword())
return
}
if !data.Contains("gid") && !data.Contains("uid") {
httperrors.MissingParameterError(ctx, w, "gid | uid")
return
}
_, err = manager.CreateNotification(ctx, data)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
}
// verify handler
func verifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
err := manager.Verify(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
}
}
// contact update handler
func contactUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, body := fetchEnv(ctx, w, r)
var data []jsonutils.JSONObject
data, err := body.GetArray(manager.Keyword(), manager.KeywordPlural())
if err != nil {
log.Errorf("body: %s, err: %s\n", body.String(), err)
httperrors.GeneralServerError(ctx, w, httperrors.NewInputParameterError("need %s and %s",
manager.Keyword(), manager.KeywordPlural()))
return
}
log.Debugf("data: %s", data)
pullCtypes, _ := body.GetArray(manager.Keyword(), "pull")
log.Debugf("pullCtypes: %s", pullCtypes)
uid := params["<uid>"]
out, err := manager.UpdateContacts(ctx, uid, mergeQueryParams(params, query), data, pullCtypes, nil)
if err != nil {
log.Errorf(err.Error())
httperrors.BadRequestError(ctx, w, "")
return
}
if out == nil {
return
}
appsrv.SendJSON(w, wrap(out, manager.Keyword()))
}
// delete contact handler
func deleteContactHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
var data []jsonutils.JSONObject
var err error
if body != nil {
data, err = body.GetArray(manager.KeywordPlural())
if err != nil {
httperrors.BadRequestError(ctx, w, "request body should have %s", manager.KeywordPlural())
return
}
}
err = manager.DeleteContacts(ctx, data)
if err != nil {
log.Errorf("delete contact of %s failed, error: %s", data, err)
httperrors.GeneralServerError(ctx, w, errors.Error("delete failed"))
}
}
// verify trigger handler
func verifyTriggerHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, body := fetchEnv(ctx, w, r)
data, err := body.Get(models.ContactManager.Keyword())
if err != nil {
httperrors.BadRequestError(ctx, w, "request body should have %s", manager.Keyword())
return
}
ret, err := manager.VerifyTrigger(ctx, params, data)
if err != nil {
log.Errorf("verifyTrigger failed beacause %s", err)
httperrors.GeneralServerError(ctx, w, err)
return
}
appsrv.SendJSON(w, ret)
}
// list handler for all resource in notify module
func listHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
appsrv.SendJSON(w, modulebase.ListResult2JSONWithKey(listResult, manager.KeywordPlural()))
}
func getHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
var data jsonutils.JSONObject
if len(listResult.Data) == 0 {
data = jsonutils.NewDict()
} else {
data = listResult.Data[0]
}
appsrv.SendJSON(w, wrap(data, manager.Keyword()))
}
func wrap(data jsonutils.JSONObject, key string) jsonutils.JSONObject {
ret := jsonutils.NewDict()
ret.Add(data, key)
return ret
}
// offset ang limit is not useable for here
func keyStoneUserListHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, _ := fetchEnv(ctx, w, r)
haveContacts := jsonutils.QueryBoolean(query, "have_contacts", false)
userCred := policy.FetchUserCredential(ctx)
s := auth.GetSession(ctx, userCred, options.Options.Region, "")
users, err := modules.UsersV3.List(s, query)
if err != nil {
log.Errorf("keystone list error: %s", err)
httperrors.InternalServerError(ctx, w, err.Error())
return
}
q := models.ContactManager.Query("uid").GroupBy("uid")
row, err := q.Rows()
if err != nil {
log.Errorf("get contact's uid error: %s", err)
httperrors.InternalServerError(ctx, w, err.Error())
return
}
defer row.Close()
uidSet, uid := make(map[string]struct{}), ""
for row.Next() {
row.Scan(&uid)
uidSet[uid] = struct{}{}
}
type sPair struct {
ID string
Name string
}
newDatas := make([]sPair, 0, len(users.Data))
for _, data := range users.Data {
id, _ := data.GetString("id")
name, _ := data.GetString("name")
if _, ok := uidSet[id]; ok {
if haveContacts {
newDatas = append(newDatas, sPair{id, name})
}
continue
}
if haveContacts {
continue
}
newDatas = append(newDatas, sPair{id, name})
}
ret := jsonutils.NewDict()
ret.Add(jsonutils.Marshal(newDatas), manager.Keyword())
appsrv.SendJSON(w, ret)
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package _interface
package notify
import (
"context"
@@ -28,6 +28,7 @@ type INotifyService interface {
RestartService(ctx context.Context, config SConfig, serviceName string)
Send(ctx context.Context, contactType, contact, topic, msg, priority string) error
ContactByMobile(ctx context.Context, mobile, serviceName string) (string, error)
BatchSend(ctx context.Context, contacts []string, contactType, topic, message, priority string) ([]*apis.FailedRecord, error)
ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error)
}

296
pkg/notify/models/config.go Normal file
View File

@@ -0,0 +1,296 @@
// 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 (
"context"
"database/sql"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
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"
"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/stringutils2"
)
type SConfigManager struct {
db.SStandaloneResourceBaseManager
}
var ConfigManager *SConfigManager
func init() {
ConfigManager = &SConfigManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SConfig{},
"configs_tbl",
"notifyconfig",
"notifyconfigs",
),
}
ConfigManager.SetVirtualObject(ConfigManager)
}
type SConfig struct {
db.SStandaloneResourceBase
Type string `width:"15" nullable:"false" create:"required" get:"admin" list:"admin"`
Content jsonutils.JSONObject `nullable:"false" create:"required" update:"admin" get:"admin" list:"admin"`
}
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)
if err != nil {
return input, err
}
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}) {
return input, httperrors.NewInputParameterError("unkown type %q", input.Type)
}
if input.Content == nil {
return input, httperrors.NewMissingParameterError("content")
}
config, err := cm.GetConfigByType(input.Type)
if err == nil && config != nil {
return input, httperrors.NewDuplicateResourceError("duplicate type %q", input.Type)
}
if err != nil && errors.Cause(err) != sql.ErrNoRows {
return input, err
}
// validate
configs := make(map[string]string)
err = input.Content.Unmarshal(&configs)
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)
}
if len(input.Name) == 0 {
input.Name = input.Type
}
return input, nil
}
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 = q.NotEquals("type", api.WEBCONSOLE)
if len(input.Type) > 0 {
q.Filter(sqlchemy.Equals(q.Field("type"), input.Type))
}
return q, nil
}
func (cm *SConfigManager) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (api.ConfigDetails, error) {
return api.ConfigDetails{}, nil
}
func (cm *SConfigManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.ConfigDetails {
sRows := cm.SStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
rows := make([]api.ConfigDetails, len(objs))
for i := range rows {
rows[i].StandaloneResourceDetails = 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
}
return q, nil
}
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
}
return q, nil
}
func (cm *SConfigManager) AllowPerformValidate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return db.IsAdminAllowPerform(userCred, cm, "validate")
}
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}) {
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)
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, 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)
if err != nil {
return errors.Wrap(err, "db.FetchModelObjects")
}
// 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)
}
tcMap[t] = append(tcMap[t], &configs[i])
}
for t, configs := range tcMap {
cMap := make(map[string]string)
for _, config := range configs {
cMap[config.KeyText] = config.ValueText
}
newConfig := SConfig{
Type: t,
Content: jsonutils.Marshal(cMap),
}
err := self.TableSpec().Insert(ctx, &newConfig)
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)
return nil
}
// Fetch all SConfig struct which type is contactType.
func (self *SConfigManager) GetConfigByType(contactType string) (*SConfig, error) {
var config SConfig
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("type"), contactType))
err := q.First(&config)
if err != nil {
return nil, errors.Wrap(err, "fail to fetch SConfigs by type")
}
return &config, nil
}
func (self *SConfigManager) GetConfig(contactType string) (notifyv2.SConfig, error) {
config, err := self.GetConfigByType(contactType)
if err != nil {
return nil, err
}
ret := make(map[string]string)
err = config.Content.Unmarshal(&ret)
if err != nil {
return nil, errors.Wrap(err, "fail unmarshal config content")
}
return ret, nil
}
func (self *SConfigManager) SetConfig(contactType string, config notifyv2.SConfig) error {
content := jsonutils.Marshal(config)
sConfig := &SConfig{
Type: contactType,
Content: content,
}
return self.TableSpec().InsertOrUpdate(context.Background(), sConfig)
}

View File

@@ -1,47 +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 models
const (
EMAIL = "email"
MOBILE = "mobile"
DINGTALK = "dingtalk"
FEISHU = "feishu"
WEBCONSOLE = "webconsole"
ROBOT = "robot"
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
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
VERIFICATION_SENT = "sent" // Verification was sent
VERIFICATION_SENT_FAIL = "sent_fail" // Verification was sent failed
VERIFICATION_VERIFIED = "verified" // Verification was verified
VERIFICATION_TOKEN_EXPIRED = "Verification code expired"
VERIFICATION_TOKEN_INVALID = "Incorrect verification code"
)
// Dingtalk account will be update automatically as mobile number change so that update dingtalk is not allowed
// In webconsole, uid is the same as contact.
var UpdateNotAllow = map[string]struct{}{
DINGTALK: {},
FEISHU: {},
WEBCONSOLE: {},
}

View File

@@ -27,8 +27,7 @@ func InitDB() error {
* initialization order matters, do not change the order
*/
ContactManager,
VerifyManager,
ReceiverManager,
NotificationManager,
ConfigManager,
TemplateManager,

View File

@@ -1,252 +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 models
import (
"context"
"fmt"
"strconv"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
_interface "yunion.io/x/onecloud/pkg/notify/interface"
"yunion.io/x/onecloud/pkg/notify/options"
)
type SConfigManager struct {
SStatusStandaloneResourceBaseManager
}
var ConfigManager *SConfigManager
func init() {
ConfigManager = &SConfigManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SConfig{},
"notify_t_config",
"config",
"configs",
),
}
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"`
}
// ListItemFilter is a hook function belong to IModelManager interface when Listing.
// This will Called in yunion.io/x/onecloud/pkg/cloudcommon/db.List function.
func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
if !query.Contains("type") {
return q, nil
}
contactType, _ := query.GetString("type")
q.Filter(sqlchemy.Equals(q.Field("type"), contactType))
return q, nil
}
// GetValue fetch the SConfig struct corresponding to key and type.
func (self *SConfigManager) GetValue(key, contactType string) (*SConfig, error) {
q := self.Query()
q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("type"), contactType), sqlchemy.Equals(q.Field("key_text"), key)))
configs := make([]SConfig, 0, 1)
err := db.FetchModelObjects(self, q, &configs)
if err != nil {
return nil, errors.Wrap(err, "Fetch SConfig by key and type failed")
}
if len(configs) == 0 {
return nil, errors.Error("There is no SConfig whose key and type meet the requirement")
}
return &configs[0], nil
}
func (self *SConfigManager) InitializeData() error {
q := self.Query()
q = q.Filter(sqlchemy.OR(sqlchemy.IsNotNull(q.Field("updated_at")), sqlchemy.IsNotNull(q.Field("created_at")),
sqlchemy.IsNotNull(q.Field("deleted_at")), sqlchemy.IsTrue(q.Field("deleted")),
sqlchemy.IsNotNull(q.Field("update_by")), sqlchemy.IsNotNull(q.Field("delete_by"))))
n, err := q.CountWithError()
if err != nil {
return err
}
if n <= 0 {
log.Debugf("need to init data for %s", self.TableSpec().Name())
// need to update
sql := fmt.Sprintf("update %s set updated_at=gmt_modified, deleted=is_deleted, created_at=gmt_create, deleted_at=gmt_deleted, update_by=modified_by, delete_by=deleted_by", self.TableSpec().Name())
q := sqlchemy.NewRawQuery(sql, "")
q.Row()
sql = fmt.Sprintf("update %s set type='mobile' where type='sms_aliyun'", self.TableSpec().Name())
q = sqlchemy.NewRawQuery(sql, "")
q.Row()
}
// init webconsole's config
sql := fmt.Sprintf("update %s set deleted='1' where type='webconsole'", self.TableSpec().Name())
q = sqlchemy.NewRawQuery(sql)
q.Row()
configs := []SConfig{
{
Type: "webconsole",
KeyText: "auth_uri",
ValueText: options.Options.AuthURL,
},
{
Type: "webconsole",
KeyText: "admin_user",
ValueText: options.Options.AdminUser,
},
{
Type: "webconsole",
KeyText: "admin_password",
ValueText: options.Options.AdminPassword,
},
{
Type: "webconsole",
KeyText: "admin_tenant_name",
ValueText: options.Options.AdminProject,
},
}
for _, config := range configs {
err := self.TableSpec().Insert(context.TODO(), &config)
if err != nil {
return err
}
}
return nil
}
// Fetch all SConfig struct which type is contactType.
func (self *SConfigManager) GetConfigByType(contactType string) ([]SConfig, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("type"), contactType))
configs := make([]SConfig, 0, 5)
err := db.FetchModelObjects(self, q, &configs)
if err != nil {
return nil, errors.Wrap(err, "Fetch SConfigs by type failed")
}
//if len(configs) == 0 {
// return nil, errors.Error("There is no SConfig whose type meet the requirement")
//}
return configs, nil
}
func (self *SConfigManager) GetConfig(contactType string) (_interface.SConfig, error) {
configs, err := self.GetConfigByType(contactType)
if err != nil {
return nil, err
}
ret := make(map[string]string)
for i := range configs {
ret[configs[i].KeyText] = configs[i].ValueText
}
return ret, nil
}
func (self *SConfigManager) SetConfig(contactType string, config _interface.SConfig) error {
return fmt.Errorf("SetConfig Not Implemented")
}
type SConvertFunc func(*jsonutils.JSONDict) *jsonutils.JSONDict
var (
// toDisplay store SConvertFunc which convert the data from client to the form required by database of contactType
toDisplay map[string]SConvertFunc
// fromDisplay store SConvertFunc which convert the data from database to the form required by client of contactType
fromDisplay map[string]SConvertFunc
)
func init() {
toDisplay = map[string]SConvertFunc{
EMAIL: emailToDisplay,
}
fromDisplay = map[string]SConvertFunc{
EMAIL: emailFromDisplay,
}
}
func emailFromDisplay(dict *jsonutils.JSONDict) *jsonutils.JSONDict {
keys := dict.SortedKeys()
newKey := ""
for _, key := range keys {
switch key {
case "username", "password":
newKey = "mail." + key
case "hostname", "hostport":
newKey = "mail.smtp." + key
case "ssl_global":
newKey = "mail.global.ssl"
}
v, _ := dict.Get(key)
dict.Add(v, newKey)
dict.Remove(key)
}
return dict
}
func emailToDisplay(dict *jsonutils.JSONDict) *jsonutils.JSONDict {
keys := dict.SortedKeys()
for _, key := range keys {
newKey := ""
value, _ := dict.Get(key)
switch key {
case "mail.username", "mail.password":
newKey = key[5:]
case "mail.smtp.hostname":
newKey = key[10:]
case "mail.smtp.hostport":
newKey = key[10:]
portStr, _ := value.GetString()
port, _ := strconv.Atoi(portStr)
value = jsonutils.NewInt(int64(port))
case "mail.global.ssl":
newKey = "ssl_global"
sslStr, _ := value.GetString()
ssl, _ := strconv.ParseBool(sslStr)
value = jsonutils.NewBool(ssl)
}
dict.Add(value, newKey)
dict.Remove(key)
}
return dict
}
func (self *SConfigManager) Display2Database(cType string, dict *jsonutils.JSONDict) *jsonutils.JSONDict {
cf, ok := fromDisplay[cType]
if ok {
return cf(dict)
}
return dict
}
func (self *SConfigManager) Database2Display(cType string, dict *jsonutils.JSONDict) *jsonutils.JSONDict {
cf, ok := toDisplay[cType]
if ok {
return cf(dict)
}
return dict
}

View File

@@ -1,404 +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 models
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/sqlchemy"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/notify/utils"
"yunion.io/x/onecloud/pkg/util/rbacutils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SContactManager struct {
SStatusStandaloneResourceBaseManager
}
var ContactManager *SContactManager
func init() {
ContactManager = &SContactManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SContact{},
"notify_t_contacts",
"contact",
"contacts",
),
}
ContactManager.SetVirtualObject(ContactManager)
}
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"`
}
func (self *SContactManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return true
}
func (self *SContactManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return true
}
func (self *SContactManager) ResourceScope() rbacutils.TRbacScope {
return rbacutils.ScopeUser
}
func (self *SContactManager) NamespaceScope() rbacutils.TRbacScope {
return rbacutils.ScopeUser
}
func (self *SContactManager) FetchOwnerId(ctx context.Context,
data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
return db.FetchUserInfo(ctx, data)
}
func (self *SContactManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider,
scope rbacutils.TRbacScope) *sqlchemy.SQuery {
if owner != nil {
if scope == rbacutils.ScopeUser {
if len(owner.GetUserId()) > 0 {
q = q.Equals("uid", owner.GetUserId())
}
}
}
return q
}
func (self *SContactManager) InitializeData() error {
q := self.Query()
q = q.Filter(sqlchemy.OR(sqlchemy.IsNotNull(q.Field("updated_at")), sqlchemy.IsTrue(q.Field("deleted"))))
n, err := q.CountWithError()
if err != nil {
return err
}
if n > 0 {
log.Debugf("no need to init data for %s", self.TableSpec().Name())
// no need to init data
return nil
}
log.Debugf("need to init data for %s", self.TableSpec().Name())
sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name())
q = sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
// FetchByUIDs fetch all SContancts whose uid included in uids.
// If some elements of uids are uname of users, setting param 'uname' as true will fetch correct results.
func (self *SContactManager) FetchByUIDs(ctx context.Context, uids []string, uname bool) ([]SContact, error) {
var err error
if uname {
uids, err = self._UIDsFromUIDOrName(ctx, uids)
if err != nil {
return nil, err
}
}
q := self.Query()
q = q.Filter(sqlchemy.In(q.Field("uid"), uids))
records := make([]SContact, 0, len(uids))
err = db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContactManager) _UIDsFromUIDOrName(ctx context.Context, uidStrs []string) ([]string, error) {
users, err := utils.GetUsersWithoutRemote(ctx, uidStrs)
if err != nil {
return nil, err
}
uids := make([]string, 0, len(uidStrs))
uidSet := sets.NewString(uidStrs...)
var (
uid string
uname string
)
for i := range users {
uid = users[i].Id
uname = users[i].Name
if uidSet.Has(uid) {
uids = append(uids, uid)
uidSet.Delete(uid)
continue
}
if uidSet.Has(uname) {
uids = append(uids, uid)
uidSet.Delete(uname)
continue
}
}
for _, uid = range uidSet.UnsortedList() {
uids = append(uids, uid)
}
log.Debugf("uids %s => %s", uidStrs, uids)
return uids, nil
}
func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []string) ([]SContact, error) {
q := self.Query("id", "uid", "contact_type", "contact", "enabled").Equals("uid", uid).In("contact_type", contactTypes)
records := make([]SContact, 0, len(contactTypes))
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContactManager) FetchByMore(uid, contact, contactType string) ([]SContact, error) {
q := self.Query().Equals("uid", uid).Equals("contact", contact).Equals("contact_type", contactType)
records := make([]SContact, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContact) getMoreDetail(ctx context.Context, userCred mcclient.TokenCredential,
out api.ContactDetails) (api.ContactDetails, error) {
uname, err := utils.GetUsernameByID(ctx, self.UID)
if errors.Cause(err) == sql.ErrNoRows {
uname = self.UID
err = nil
}
if err != nil {
return out, err
}
q := ContactManager.Query().Equals("uid", self.UID)
contacts := make([]SContact, 0)
err = db.FetchModelObjects(ContactManager, q, &contacts)
if err != nil {
return out, errors.Wrapf(err, "fetch Contacts of uid %s error", self.UID)
}
out.UID = self.UID
out.Name = uname
out.Details = jsonutils.Marshal(contacts).String()
return out, nil
}
func (manager *SContactManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.ContactDetails {
rows := make([]api.ContactDetails, len(objs))
stdRows := manager.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
var err error
for i := range rows {
rows[i] = api.ContactDetails{
ResourceBaseDetails: stdRows[i],
}
rows[i], err = objs[i].(*SContact).getMoreDetail(ctx, userCred, rows[i])
if err != nil {
log.Errorf(err.Error())
}
}
return rows
}
func (self *SContact) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.ContactDetails, error) {
return api.ContactDetails{}, nil
}
// 联系方式列表
func (self *SContactManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
queryDict := query.(*jsonutils.JSONDict)
if queryDict.Contains("uid") {
uid, _ := queryDict.GetString("uid")
q = q.Equals("uid", uid)
}
// for now
if queryDict.Contains("filter") {
filterCon, _ := queryDict.GetString("filter")
queryDict.Remove("filter")
contain := "name.contains("
index := strings.Index(filterCon, contain)
if index < 0 {
return q, nil
}
filterCon = filterCon[index+len(contain):]
index = strings.Index(filterCon, ")")
if index < 0 {
return q, nil
}
name := filterCon[:index]
ids, err := utils.GetUserIdsLikeName(ctx, name)
if err != nil {
return q, nil
}
q = q.In("uid", ids)
}
scopeStr, err := query.GetString("scope")
if err != nil {
scopeStr = "project"
}
scope := rbacutils.TRbacScope(scopeStr)
switch {
case scope.HigherEqual(rbacutils.ScopeSystem):
case scope.HigherEqual(rbacutils.ScopeDomain):
uids, err := self.uidsInDomain(ctx, userCred)
if err != nil {
return q, err
}
q = q.In("uid", uids)
default:
q = q.Equals("uid", userCred.GetUserId())
}
q = q.GroupBy("uid").Desc("created_at")
return q, nil
}
func (self *SContactManager) uidsInDomain(ctx context.Context, userCred mcclient.TokenCredential) ([]string, error) {
session := auth.GetSession(ctx, userCred, "", "")
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("domain"))
ret, err := modules.UsersV3.List(session, params)
if err != nil {
return nil, errors.Wrap(err, "modules.Userv3.List")
}
uids := make([]string, 0, len(ret.Data))
for i := range ret.Data {
id, _ := ret.Data[i].GetString("id")
uids = append(uids, id)
}
return uids, nil
}
// Contacts query all contacts by uids and contactType
func (self *SContactManager) Contacts(uids []string, contactType string) ([]SContact, error) {
contacts := make([]SContact, 0, len(uids))
if contactType == WEBCONSOLE {
for _, uid := range uids {
contacts = append(contacts, SContact{
UID: uid,
ContactType: WEBCONSOLE,
Contact: uid,
})
}
return contacts, nil
}
queryCon := contactType
if strings.Contains(contactType, ROBOT) {
queryCon = MOBILE
}
q := self.Query().Equals("contact_type", queryCon).Equals("enabled", "1").In("uid", uids)
err := db.FetchModelObjects(self, q, &contacts)
if err != nil {
return nil, err
}
// For Robot Sender, only one message of the same content is sent for multiple users,
// so the user's contact information is a collection of all contact information
if strings.Contains(contactType, ROBOT) {
// hack
contactVals := make([]string, len(contacts))
uidVals := make([]string, len(contacts))
for i := range contacts {
contactVals[i] = contacts[i].Contact
uidVals[i] = contacts[i].UID
}
contacts = []SContact{
{
UID: strings.Join(uidVals, ","),
ContactType: contactType,
Contact: strings.Join(contactVals, ","),
},
}
}
return contacts, nil
}
func (self *SContactManager) GetAllNotify(ctx context.Context, ids []string, contactType string, group bool) ([]SContact, error) {
var uids []string
var err error
if !group {
if v := ctx.Value("uname"); v != nil {
ids, err = self._UIDsFromUIDOrName(ctx, ids)
if err != nil {
return nil, errors.Wrap(err, "fail to transfer array of UID or Uname to UIDs")
}
}
uids = ids
} else {
uid := make([]string, 0)
for _, id := range uids {
tmpUids, err := utils.GetUsersByGroupID(ctx, id)
if err != nil {
return nil, err
}
uid = append(uid, tmpUids...)
}
uids = uid
}
return self.Contacts(uids, contactType)
}
type SContactResponse struct {
Id string
Name string
Details string
}
func NewSContactResponse(ctx context.Context, uid string, details string) SContactResponse {
name, _ := utils.GetUsernameByID(ctx, uid)
return SContactResponse{
Id: uid,
Name: name,
Details: details,
}
}

View File

@@ -1,506 +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 models
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/cache"
_interface "yunion.io/x/onecloud/pkg/notify/interface"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/notify/utils"
"yunion.io/x/onecloud/pkg/util/rbacutils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SNotificationManager struct {
SStatusStandaloneResourceBaseManager
}
var NotificationManager *SNotificationManager
var NotifyService _interface.INotifyService
func init() {
NotificationManager = &SNotificationManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SNotification{},
"notify_t_notification",
"notification",
"notifications",
),
}
NotificationManager.SetVirtualObject(NotificationManager)
}
func (self *SNotificationManager) ResourceScope() rbacutils.TRbacScope {
return rbacutils.ScopeUser
}
func (self *SNotificationManager) NamespaceScope() rbacutils.TRbacScope {
return rbacutils.ScopeUser
}
func (self *SNotificationManager) FetchOwnerId(ctx context.Context,
data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
return db.FetchUserInfo(ctx, data)
}
func (self *SNotificationManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider,
scope rbacutils.TRbacScope) *sqlchemy.SQuery {
if owner != nil {
if scope == rbacutils.ScopeUser {
if len(owner.GetUserId()) > 0 {
q = q.Equals("uid", owner.GetUserId())
}
}
}
return q
}
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"`
}
type UserDetail struct {
Status string
Name string
ReceivedAt time.Time
}
func (manager *SNotificationManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.NotificationDetails {
rows := make([]api.NotificationDetails, len(objs))
resRows := manager.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
for i := range rows {
rows[i] = api.NotificationDetails{
ResourceBaseDetails: resRows[i],
}
rows[i], _ = objs[i].(*SNotification).getMoreDetails(ctx, query, rows[i])
}
return rows
}
func (self *SNotification) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.NotificationDetails, error) {
return api.NotificationDetails{}, nil
}
func (self *SNotification) getMoreDetails(ctx context.Context, query jsonutils.JSONObject, out api.NotificationDetails) (api.NotificationDetails, error) {
var err error
var scopeStr string
scopeStr, err = query.GetString("scope")
if err != nil {
scopeStr = "system"
}
scope := rbacutils.TRbacScope(scopeStr)
var userDetails []UserDetail
if scope.HigherEqual(rbacutils.ScopeSystem) {
userDetails, err = NotificationManager.fetchUserDetailByClusterID(ctx, self.ClusterID)
if err != nil {
return out, errors.Wrap(err, "NotificationManager.fetchUserDetailByClusterID")
}
} else {
userDetail := UserDetail{
Status: self.Status,
Name: self.UID,
ReceivedAt: self.ReceivedAt,
}
name, err := utils.GetUsernameByID(ctx, self.UID)
if err == nil && len(name) != 0 {
userDetail.Name = name
}
userDetails = []UserDetail{userDetail}
}
out.UserList = jsonutils.Marshal(userDetails)
return out, nil
}
func (self *SNotificationManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return db.IsAdminAllowList(userCred, self)
}
func (self *SNotificationManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAdminAllowCreate(userCred, self)
}
type sUpdate struct {
ID string
UID string
Topic string
Priority string
ContactType string
}
func (self *SNotificationManager) InitializeData() error {
scope := time.Duration(options.Options.InitNotificationScope) * time.Hour
time := time.Now().Add(-scope)
q := self.Query("id", "uid", "topic", "priority", "contact_type").GE("created_at",
time).Desc("received_at").Equals("contact_type", "webconsole")
q = q.Filter(sqlchemy.OR(sqlchemy.IsNull(q.Field("cluster_id")), sqlchemy.IsEmpty(q.Field("cluster_id"))))
rows, err := q.Rows()
if err != nil {
return err
}
updates, update := make([]sUpdate, 0, 10), sUpdate{}
for rows.Next() {
err := rows.Scan(&update.ID, &update.UID, &update.Topic, &update.Priority, &update.ContactType)
if err == nil {
updates = append(updates, update)
}
}
log.Debugf("this is total %d updates", len(updates))
// updates is too little
//if len(updates) < 100 {
// updates = updates[:0]
// q := self.Query("id", "uid", "topic", "priority", "contact_type").Desc("received_at").Equals("contact_type",
// "webconsole").Limit(500)
// q = q.Filter(sqlchemy.OR(sqlchemy.IsNull(q.Field("cluster_id")), sqlchemy.IsEmpty(q.Field("cluster_id"))))
// rows, err := q.Rows()
// if err != nil {
// return err
// }
// for rows.Next() {
// err := rows.Scan(&update.ID, &update.UID, &update.Topic, &update.Priority, &update.ContactType)
// if err == nil {
// updates = append(updates, update)
// }
// }
// log.Debugf("this is total %d updates", len(updates))
//}
cache := make([]string, 0, 10)
if len(updates) > 0 {
cache = append(cache, updates[0].ID)
}
for i := 1; i < len(updates); i++ {
if updates[i].Topic == updates[i-1].Topic && updates[i].Priority == updates[i-1].Priority {
cache = append(cache, updates[i].ID)
continue
}
err = self.syncDatabase(cache)
if err != nil {
return errors.Wrap(err, "exec sql error")
}
cache = cache[:0]
if i < len(updates)-1 {
cache = append(cache, updates[i].ID)
}
}
if len(cache) == 0 {
return nil
}
err = self.syncDatabase(cache)
if err != nil {
return errors.Wrap(err, "exec sql error")
}
return nil
}
func (self *SNotificationManager) syncDatabase(ids []string) error {
sql := "update %s set updated_at=update_at, deleted=is_deleted, cluster_id='%s' where id in %s"
newUid := DefaultUUIDGenerator()
buffer := new(strings.Builder)
buffer.WriteString("(")
for _, id := range ids {
buffer.WriteString("'")
buffer.WriteString(id)
buffer.WriteString("', ")
}
newSql := fmt.Sprintf(sql, self.TableSpec().Name(), newUid, buffer.String()[:buffer.Len()-2]+")")
q := sqlchemy.NewRawQuery(newSql)
rows, err := q.Rows()
defer rows.Close()
return err
}
// 通知消息列表
func (self *SNotificationManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential,
input api.NotificationListInput) (*sqlchemy.SQuery, error) {
// no domainID for now
scopeStr := "system"
if len(input.Scope) > 0 {
scopeStr = input.Scope
}
scope := rbacutils.TRbacScope(scopeStr)
if !scope.HigherEqual(rbacutils.ScopeSystem) {
q = q.Equals("uid", userCred.GetUserId())
}
if len(input.ContactType) > 0 {
q = q.Equals("contact_type", input.ContactType)
}
q = q.GroupBy("cluster_id").Desc("received_at")
return q, nil
}
func (self *SNotificationManager) BatchCreate(ctx context.Context, data jsonutils.JSONObject, contacts []SContact) ([]string, error) {
userCred := policy.FetchUserCredential(ctx)
ownerID, err := utils.FetchOwnerId(ctx, NotificationManager, userCred, jsonutils.JSONNull)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
msg, _ := data.GetString("msg")
priority, _ := data.GetString("priority")
topic, _ := data.GetString("topic")
createFailed, createSuccess, contactSuccess := make([]string, 0), make([]*SNotification, 0, len(contacts)/2), make([]string, 0, len(contacts)/2)
now, clusterId := time.Now(), DefaultUUIDGenerator()
for i := range contacts {
createData := map[string]interface{}{
"uid": contacts[i].UID,
"contact_type": contacts[i].ContactType,
"topic": topic,
"priority": priority,
"msg": msg,
"received_at": now,
"send_by": userCred.GetUserId(),
"status": NOTIFY_RECEIVED,
"cluster_id": clusterId,
}
model, err := db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, jsonutils.Marshal(createData), ownerID)
if err != nil {
createFailed = append(createFailed, contacts[i].ID)
} else {
createSuccess = append(createSuccess, model.(*SNotification))
contactSuccess = append(contactSuccess, contacts[i].Contact)
}
}
Send(createSuccess, userCred, contactSuccess)
if len(createFailed) != 0 {
errInfo := new(bytes.Buffer)
errInfo.WriteString("notifications whose uid are ")
for i := range createFailed {
errInfo.WriteString(createFailed[i])
errInfo.WriteString(", ")
}
errInfo.Truncate(errInfo.Len() - 2)
errInfo.WriteString("created failed.")
log.Errorf(errInfo.String())
return nil, errors.Error("Not all notifications were sent successfully")
}
notificationIDs := make([]string, len(createSuccess))
for i := range createSuccess {
notificationIDs[i] = createSuccess[i].ID
}
return notificationIDs, nil
}
func (self *SNotificationManager) fetchUserDetailByClusterID(ctx context.Context, clusterID string) ([]UserDetail,
error) {
q := self.Query("uid", "status", "received_at").Equals("cluster_id", clusterID)
row, err := q.Rows()
if err != nil {
return nil, err
}
ret := make([]UserDetail, 0)
userIds := make([]string, 0)
var userId, status string
var receviedAt time.Time
for row.Next() {
err := row.Scan(&userId, &status, &receviedAt)
if err != nil {
return nil, errors.Wrap(err, "sql.row parse error")
}
userIds = append(userIds, userId)
ret = append(ret, UserDetail{
Status: status,
Name: userId,
ReceivedAt: receviedAt,
})
}
userMap, err := cache.UserCacheManager.FetchUsersByIDs(ctx, userIds)
if err != nil {
return nil, errors.Wrap(err, "fetch users by ids failed")
}
for i := range ret {
if user, ok := userMap[ret[i].Name]; ok {
ret[i].Name = user.Name
}
}
return ret, nil
}
func (self *SNotificationManager) FetchFailed(lastTime time.Time) ([]SNotification, error) {
q := self.Query()
q.Filter(sqlchemy.AND(sqlchemy.GE(q.Field("created_at"), lastTime), sqlchemy.Equals(q.Field("status"), NOTIFY_FAIL)))
records := make([]SNotification, 0, 10)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SNotification) SetStatus(userCred mcclient.TokenCredential, status string, reason string) error {
if self.Status == status {
return nil
}
oldStatus := self.Status
_, err := db.Update(self, func() error {
self.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(self, db.ACT_UPDATE_STATUS, notes, userCred)
}
return nil
}
func (self *SNotification) SetSentAndTime(userCred mcclient.TokenCredential) error {
status := NOTIFY_SENT
if self.Status == status {
return nil
}
oldStatus := self.Status
_, err := db.Update(self, func() error {
self.Status = status
self.SendAt = time.Now()
return nil
})
if err != nil {
return err
}
reason := "sent notification"
if userCred != nil {
notes := fmt.Sprintf("%s=>%s", oldStatus, status)
if len(reason) > 0 {
notes = fmt.Sprintf("%s: %s", notes, reason)
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE_STATUS, notes, userCred)
}
return nil
}
func (self *SNotification) SetStatusWithoutUserCred(status string) error {
_, err := db.Update(self, func() error {
self.Status = status
return nil
})
if err != nil {
return err
}
return nil
}
func sendWithoutUserCred(notifications []SNotification) {
// limit the number of Concurrency
Max := 10
limit := make(chan struct{}, Max)
sendone := func(notification SNotification) {
defer func() {
<-limit
}()
// Get contact
contact, err := ContactManager.FetchByUIDAndCType(notification.UID, []string{notification.ContactType})
if err != nil {
log.Debugf("fail to fetch contacts with uid '%s' in ReSend Cron Job", notification.UID)
return
}
if len(contact) == 0 {
return
}
// sent_at update todo
notification.SetStatusWithoutUserCred(NOTIFY_SENT)
err = NotifyService.Send(context.Background(), notification.ContactType, contact[0].Contact, notification.Topic,
notification.Msg,
notification.Priority)
if err == nil {
return
}
if err != nil {
log.Errorf("Send notification failed in ReSend Cron Job: %s.", err.Error())
notification.SetStatusWithoutUserCred(NOTIFY_FAIL)
} else {
notification.SetStatusWithoutUserCred(NOTIFY_OK)
}
}
for i := range notifications {
limit <- struct{}{}
go sendone(notifications[i])
}
// wait all finish
for i := 0; i < Max; i++ {
limit <- struct{}{}
}
}
func ReSend(seconds int) {
scope := time.Duration(seconds+30) * time.Second
notifications, err := NotificationManager.FetchFailed(time.Now().Add(-scope))
if err != nil {
return
}
log.Debugf("Start to resend message with a total of %d", len(notifications))
sendWithoutUserCred(notifications)
}

View File

@@ -1,134 +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 models
import (
"context"
"fmt"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/utils"
)
type SVerifyManager struct {
SStatusStandaloneResourceBaseManager
}
var VerifyManager *SVerifyManager
func init() {
VerifyManager = &SVerifyManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SVerify{},
"notify_t_verify",
"verification",
"verifications",
),
}
VerifyManager.SetVirtualObject(VerifyManager)
}
type SVerify struct {
SStatusStandaloneResourceBase
CID string `width:"128" nullable:"false" create:"required" list:"user"`
Token string `width:"200" nullable:"false" create:"required" list:"user"`
SendAt time.Time `nullable:"true" create:"optional"`
ExpireAt time.Time `create:"required" list:"user"`
}
// NewSVerify Generate a SVerify instance which implement a Verification Token.
func NewSVerify(contactType string, cid string) *SVerify {
var token string
var expireAt time.Time
now := time.Now()
if contactType == EMAIL {
token = utils.GenerateEmailToken(32)
expireAt = now.Add(12 * time.Hour)
} else {
token = utils.GenerateMobileToken()
expireAt = now.Add(2 * time.Minute)
}
ret := &SVerify{
CID: cid,
Token: token,
ExpireAt: expireAt,
SendAt: now,
}
ret.ID = DefaultUUIDGenerator()
ret.SetModelManager(VerifyManager, ret)
return ret
}
func (self *SVerifyManager) InitializeData() error {
q := self.Query()
q = q.Filter(sqlchemy.OR(sqlchemy.IsNotNull(q.Field("updated_at")), sqlchemy.IsTrue(q.Field("deleted"))))
n, err := q.CountWithError()
if err != nil {
return err
}
if n > 0 {
log.Debugf("no need to init data for %s", self.TableSpec().Name())
// no need to init data
return nil
}
log.Debugf("need to init data for %s", self.TableSpec().Name())
sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name())
q = sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
func (self *SVerifyManager) FetchByCID(cid string, filter func(q *sqlchemy.SQuery) *sqlchemy.SQuery) ([]SVerify, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("cid"), cid))
q = filter(q)
records := make([]SVerify, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SVerifyManager) FetchByID(id string) ([]SVerify, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("id"), id))
records := make([]SVerify, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SVerifyManager) Create(ctx context.Context, userCred mcclient.TokenCredential, verify *SVerify) error {
data := jsonutils.Marshal(verify)
ownerID, err := utils.FetchOwnerId(ctx, self, userCred, data)
if err != nil {
return err
}
_, err = db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, data, ownerID)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,419 @@
// 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 (
"context"
"database/sql"
"fmt"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/sqlchemy"
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"
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/rbacutils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SNotificationManager struct {
db.SStatusStandaloneResourceBaseManager
}
var NotificationManager *SNotificationManager
var NotifyService notifyv2.INotifyService
func init() {
NotificationManager = &SNotificationManager{
SStatusStandaloneResourceBaseManager: db.NewStatusStandaloneResourceBaseManager(
SNotification{},
"notifications_tbl",
"notification",
"notifications",
),
}
NotificationManager.SetVirtualObject(NotificationManager)
}
type SNotification struct {
db.SStatusStandaloneResourceBase
ContactType string `width:"16" nullable:"false" create:"required" list:"user" get:"user" index:"true"`
// swagger:ignore
Topic string `width:"128" nullable:"true" create:"required"`
Priority string `width:"16" nullable:"true" create:"optional" list:"user" get:"user"`
// swagger:ignore
Message string `create:"required"`
ReceivedAt time.Time `nullable:"true" list:"user" get:"user"`
SendTimes int
}
func (nm *SNotificationManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.NotificationCreateInput) (api.NotificationCreateInput, error) {
// check uids, rids and contacts
if len(input.Receivers) == 0 && len(input.Contacts) == 0 {
return input, httperrors.NewMissingParameterError("receivers | contacts")
}
// check receivers
if len(input.Receivers) > 0 {
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
}
return input, httperrors.NewInputParameterError("no such receiver whose uid is %q", re)
}
input.Receivers = idSet.UnsortedList()
}
nowStr := time.Now().Format("2006-01-02 15:04:05")
// hack
input.Name = fmt.Sprintf("%s(%s)", input.Topic, nowStr)
return input, nil
}
func (n *SNotification) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
n.ReceivedAt = time.Now()
n.Id = db.DefaultUUIDGenerator()
var input api.NotificationCreateInput
err := data.Unmarshal(&input)
if err != nil {
return err
}
for i := range input.Receivers {
_, err := ReceiverNotificationManager.Create(ctx, userCred, input.Receivers[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.Create")
}
}
return nil
}
func (n *SNotification) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
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)
}
}
func (nm *SNotificationManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.NotificationDetails {
rows := make([]api.NotificationDetails, len(objs))
resRows := nm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
var err error
for i := range rows {
rows[i], err = objs[i].(*SNotification).getMoreDetails(ctx, query, rows[i])
if err != nil {
log.Errorf("Notification.getMoreDetails: %v", err)
}
rows[i].StatusStandaloneResourceDetails = resRows[i]
}
return rows
}
func (n *SNotification) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.NotificationDetails, error) {
return api.NotificationDetails{}, nil
}
func (n *SNotification) ReceiverNotificationsNotOK() ([]SReceiverNotification, error) {
rnq := ReceiverNotificationManager.Query().Equals("notification_id", n.Id).NotEquals("status", api.RECEIVER_NOTIFICATION_OK)
rns := make([]SReceiverNotification, 0, 1)
err := db.FetchModelObjects(ReceiverNotificationManager, rnq, &rns)
if err != nil {
return nil, err
}
return rns, nil
}
func (n *SNotification) ReceiveDetails() ([]api.ReceiveDetail, error) {
subRQ := ReceiverManager.Query("id", "name").SubQuery()
q := ReceiverNotificationManager.Query("receiver_id", "notification_id", "contact", "send_at", "send_by", "status", "failed_reason").Equals("notification_id", n.Id)
q.AppendField(subRQ.Field("name", "receiver_name"))
q = q.Join(subRQ, sqlchemy.Equals(q.Field("receiver_id"), subRQ.Field("id")))
ret := make([]api.ReceiveDetail, 0, 2)
err := q.All(&ret)
if err != nil && errors.Cause(err) != sql.ErrNoRows {
log.Errorf("SQuery.All: %v", err)
return nil, err
}
return ret, nil
}
func (n *SNotification) getMoreDetails(ctx context.Context, query jsonutils.JSONObject, out api.NotificationDetails) (api.NotificationDetails, error) {
// get title adn content
p, err := TemplateManager.NotifyFilter(n.ContactType, n.Topic, n.Message)
if err != nil {
return out, err
}
out.Title = p.Title
out.Content = p.Message
// get receive details
out.ReceiveDetails, err = n.ReceiveDetails()
if err != nil {
return out, err
}
return out, nil
}
func (nm *SNotificationManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
if !data.Contains("contacts") {
return db.IsAdminAllowCreate(userCred, nm)
}
return true
}
func (nm *SNotificationManager) AllowUpdateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return false
}
func (nm *SNotificationManager) ResourceScope() rbacutils.TRbacScope {
return rbacutils.ScopeUser
}
func (nm *SNotificationManager) NamespaceScope() rbacutils.TRbacScope {
return rbacutils.ScopeSystem
}
func (nm *SNotificationManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
return db.FetchUserInfo(ctx, data)
}
func (nm *SNotificationManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider, scope rbacutils.TRbacScope) *sqlchemy.SQuery {
if owner == nil {
return q
}
switch scope {
case rbacutils.ScopeDomain:
subRq := ReceiverManager.Query("id").Equals("domain_id", owner.GetDomainId()).SubQuery()
subRNq := ReceiverNotificationManager.Query("notification_id").Join(subRq, sqlchemy.Equals(q.Field("receiver_id"), subRq.Field("id"))).SubQuery()
q = q.Join(subRNq, sqlchemy.Equals(q.Field("id"), subRNq.Field("notification_id")))
case rbacutils.ScopeProject, rbacutils.ScopeUser:
subq := ReceiverNotificationManager.Query("notification_id").Equals("receiver_id", owner.GetUserId()).SubQuery()
q = q.Join(subq, sqlchemy.Equals(q.Field("id"), subq.Field("notification_id")))
}
return q
}
func (n *SNotification) AddOne() error {
_, err := db.Update(n, func() error {
n.SendTimes += 1
return nil
})
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 {
// 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)
if err != nil {
return nil, err
}
if len(input.ContactType) > 0 {
q = q.Equals("contact_type", input.ContactType)
}
if len(input.ReceiverId) > 0 {
subq := ReceiverNotificationManager.Query("notification_id").Equals("receiver_id", input.ReceiverId).SubQuery()
q = q.Join(subq, sqlchemy.Equals(q.Field("id"), subq.Field("notification_id")))
}
return q, nil
}
func (nm *SNotificationManager) ReSend(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
q := nm.Query().NotEquals("status", api.NOTIFICATION_STATUS_OK).LT("send_times", options.Options.MaxSendTimes)
ns := make([]SNotification, 0, 2)
err := db.FetchModelObjects(nm, q, &ns)
if err != nil {
log.Errorf("fail to FetchModelObjects: %v", err)
return
}
for i := range ns {
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", &ns[i], userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
}
}

View File

@@ -0,0 +1,884 @@
// 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 (
"context"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/util/regutils"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
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/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/informer"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/notify/oldmodels"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
var (
AllContactTypes = []string{
api.EMAIL,
api.MOBILE,
api.DINGTALK,
api.FEISHU,
api.WORKWX,
api.WEBCONSOLE,
}
AllSubContactTypes = []string{
api.DINGTALK,
api.FEISHU,
api.WORKWX,
api.WEBCONSOLE,
}
AllRobotContactTypes = []string{
api.FEISHU_ROBOT,
api.DINGTALK_ROBOT,
api.WORKWX_ROBOT,
}
)
type SReceiverManager struct {
db.SStatusStandaloneResourceBaseManager
db.SDomainizedResourceBaseManager
db.SEnabledResourceBaseManager
}
var ReceiverManager *SReceiverManager
func init() {
ReceiverManager = &SReceiverManager{
SStatusStandaloneResourceBaseManager: db.NewStatusStandaloneResourceBaseManager(
SReceiver{},
"receivers_tbl",
"receiver",
"receivers",
),
}
ReceiverManager.SetVirtualObject(ReceiverManager)
}
type SReceiver struct {
db.SStatusStandaloneResourceBase
db.SDomainizedResourceBase
db.SEnabledResourceBase
Email string `width:"64" nullable:"false" create:"optional" update:"user" get:"user" list:"admin"`
Mobile string `width:"16" nullable:"false" create:"optional" update:"user" get:"user" list:"admin"`
// swagger:ignore
EnabledEmail tristate.TriState `nullable:"false" default:"false" update:"user"`
// swagger:ignore
VerifiedEmail tristate.TriState `nullable:"false" default:"false" update:"user"`
// swagger:ignore
EnabledMobile tristate.TriState `nullable:"false" default:"false" update:"user"`
// swagger:ignore
VerifiedMobile tristate.TriState `nullable:"false" default:"false" update:"user"`
// swagger:ignore
subContactCache map[string]*SSubContact `json:"-"`
}
func (rm *SReceiverManager) InitializeData() error {
ctx := context.Background()
userCred := auth.AdminCredential()
log.Infof("Init Receiver...")
// Fetch all old SContact
q := oldmodels.ContactManager.Query()
contacts := make([]oldmodels.SContact, 0, 50)
err := db.FetchModelObjects(oldmodels.ContactManager, q, &contacts)
if err != nil {
return errors.Wrap(err, "db.FetchModelObjects")
}
// build uid map
uids := make([]string, 0, 10)
contactMap := make(map[string][]*oldmodels.SContact, 10)
for i := range contacts {
uid := contacts[i].UID
if _, ok := contactMap[uid]; !ok {
contactMap[uid] = make([]*oldmodels.SContact, 0, 4)
uids = append(uids, uid)
}
contactMap[uid] = append(contactMap[uid], &contacts[i])
}
// build uid->uname map
userMap, err := oldmodels.UserCacheManager.FetchUsersByIDs(context.Background(), uids)
if err != nil {
return errors.Wrap(err, "oldmodels.UserCacheManager.FetchUsersByIDs")
}
// build Receivers
for uid, contacts := range contactMap {
var receiver SReceiver
receiver.subContactCache = make(map[string]*SSubContact)
receiver.Enabled = tristate.True
receiver.Status = api.RECEIVER_STATUS_READY
receiver.Id = uid
user, ok := userMap[uid]
if !ok {
log.Errorf("no user %q in usercache", uid)
} else {
receiver.Name = user.Name
receiver.DomainId = user.DomainId
}
webconsole := false
for _, contact := range contacts {
switch contact.ContactType {
case api.EMAIL:
receiver.Email = contact.Contact
if contact.Enabled == "1" {
receiver.EnabledEmail = tristate.True
} else {
receiver.EnabledEmail = tristate.False
}
if contact.Status == oldmodels.CONTACT_VERIFIED {
receiver.VerifiedEmail = tristate.True
} else {
receiver.VerifiedEmail = tristate.False
}
case api.MOBILE:
receiver.Mobile = contact.Contact
if contact.Enabled == "1" {
receiver.EnabledMobile = tristate.True
} else {
receiver.EnabledMobile = tristate.False
}
if contact.Status == oldmodels.CONTACT_VERIFIED {
receiver.VerifiedMobile = tristate.True
} else {
receiver.VerifiedMobile = tristate.False
}
default:
var subContact SSubContact
subContact.Type = contact.ContactType
if subContact.Type == api.WEBCONSOLE {
webconsole = true
subContact.Contact = uid
} else {
subContact.Contact = contact.Contact
}
subContact.ReceiverID = uid
subContact.ParentContactType = api.MOBILE
if contact.Enabled == "1" {
subContact.Enabled = tristate.True
} else {
subContact.Enabled = tristate.False
}
if contact.Status == oldmodels.CONTACT_VERIFIED && len(contact.Contact) > 0 {
subContact.Verified = tristate.True
} else {
subContact.Verified = tristate.False
}
receiver.subContactCache[contact.ContactType] = &subContact
}
}
if !webconsole {
receiver.subContactCache[api.WEBCONSOLE] = &SSubContact{
ReceiverID: receiver.Id,
Type: api.WEBCONSOLE,
Contact: receiver.Id,
ParentContactType: "",
Enabled: tristate.True,
Verified: tristate.True,
}
}
err := rm.TableSpec().InsertOrUpdate(ctx, &receiver)
if err != nil {
return errors.Wrap(err, "InsertOrUpdate")
}
err = receiver.PushCache(ctx)
if err != nil {
return errors.Wrap(err, "PushCache")
}
//delete old one
for _, contact := range contacts {
err := contact.Delete(ctx, userCred)
if err != nil {
return errors.Wrap(err, "Delete")
}
}
}
return nil
}
func (rm *SReceiverManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ReceiverCreateInput) (api.ReceiverCreateInput, error) {
var err error
input.StatusStandaloneResourceCreateInput, err = rm.SStatusStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StatusStandaloneResourceCreateInput)
if err != nil {
return input, err
}
// check uid
session := auth.GetSession(ctx, userCred, "", "")
if len(input.UID) > 0 {
userObj, err := modules.UsersV3.GetById(session, input.UID, nil)
if err != nil {
if jErr, ok := err.(*httputils.JSONClientError); ok {
if jErr.Code == 404 {
return input, httperrors.NewInputParameterError("no such user")
}
}
return input, err
}
uname, _ := userObj.GetString("name")
input.UName = uname
domainId, _ := userObj.GetString("domain_id")
input.ProjectDomainId = domainId
} else {
if len(input.UName) == 0 {
return input, httperrors.NewMissingParameterError("uid or uname")
} else {
userObj, err := modules.UsersV3.GetByName(session, input.UName, nil)
if err != nil {
if jErr, ok := err.(*httputils.JSONClientError); ok {
if jErr.Code == 404 {
return input, httperrors.NewInputParameterError("no such user")
}
}
return input, err
}
uid, _ := userObj.GetString("id")
input.UID = uid
domainId, _ := userObj.GetString("domain_id")
input.ProjectDomainId = domainId
}
}
// hack
input.Name = input.UName
// validate email
if ok := regutils.MatchEmail(input.Email); !ok {
return input, httperrors.NewInputParameterError("invalid email")
}
// validate mobile
if ok := regutils.MatchMobile(input.Mobile); !ok {
return input, httperrors.NewInputParameterError("invalid mobile")
}
return input, nil
}
func (r *SReceiver) IsEnabledContactType(ct string) (bool, error) {
if utils.IsInStringArray(ct, AllRobotContactTypes) {
return true, nil
}
cts, err := r.GetEnabledContactTypes()
if err != nil {
return false, errors.Wrap(err, "GetEnabledContactTypes")
}
return utils.IsInStringArray(ct, cts), nil
}
func (r *SReceiver) IsVerifiedContactType(ct string) (bool, error) {
if utils.IsInStringArray(ct, AllRobotContactTypes) {
return true, nil
}
cts, err := r.GetVerifiedContactTypes()
if err != nil {
return false, errors.Wrap(err, "GetVerifiedContactTypes")
}
return utils.IsInStringArray(ct, cts), nil
}
func (r *SReceiver) GetEnabledContactTypes() ([]string, error) {
if err := r.PullCache(false); err != nil {
return nil, err
}
ret := make([]string, 0, 1)
// for email and mobile
if r.EnabledEmail.IsTrue() {
ret = append(ret, api.EMAIL)
}
if r.EnabledMobile.IsTrue() {
ret = append(ret, api.MOBILE)
}
for subct, subc := range r.subContactCache {
if subc.Enabled.IsTrue() {
ret = append(ret, subct)
}
}
return ret, nil
}
func (r *SReceiver) setEnabledContactType(contactType string, enabled bool) {
switch contactType {
case api.EMAIL:
r.EnabledEmail = tristate.NewFromBool(enabled)
case api.MOBILE:
r.EnabledMobile = tristate.NewFromBool(enabled)
default:
if sc, ok := r.subContactCache[contactType]; ok {
sc.Enabled = tristate.NewFromBool(enabled)
} else {
r.subContactCache[contactType] = &SSubContact{
Type: contactType,
ReceiverID: r.Id,
Enabled: tristate.NewFromBool(enabled),
}
}
}
}
func (r *SReceiver) SetEnabledContactTypes(contactTypes []string) error {
if err := r.PullCache(false); err != nil {
return err
}
ctSet := sets.NewString(contactTypes...)
for _, ct := range AllContactTypes {
if ctSet.Has(ct) {
r.setEnabledContactType(ct, true)
} else {
r.setEnabledContactType(ct, false)
}
}
return nil
}
func (r *SReceiver) MarkContactTypeVerified(contactType string) error {
if err := r.PullCache(false); err != nil {
return err
}
if sc, ok := r.subContactCache[contactType]; ok {
sc.Verified = tristate.True
} else {
r.subContactCache[contactType] = &SSubContact{
ReceiverID: r.Id,
Verified: tristate.True,
}
}
return nil
}
func (r *SReceiver) setVerifiedContactType(contactType string, enabled bool) {
switch contactType {
case api.EMAIL:
r.VerifiedEmail = tristate.NewFromBool(enabled)
case api.MOBILE:
r.VerifiedMobile = tristate.NewFromBool(enabled)
default:
if sc, ok := r.subContactCache[contactType]; ok {
sc.Verified = tristate.NewFromBool(enabled)
} else {
r.subContactCache[contactType] = &SSubContact{
ReceiverID: r.Id,
Verified: tristate.NewFromBool(enabled),
}
}
}
}
func (r *SReceiver) GetVerifiedContactTypes() ([]string, error) {
if err := r.PullCache(false); err != nil {
return nil, err
}
ret := make([]string, 0, 1)
// for email and mobile
if r.VerifiedEmail.IsTrue() {
ret = append(ret, api.EMAIL)
}
if r.VerifiedMobile.IsTrue() {
ret = append(ret, api.MOBILE)
}
for subct, subc := range r.subContactCache {
if subc.Verified.IsTrue() {
ret = append(ret, subct)
}
}
return ret, nil
}
func (r *SReceiver) SetVerifiedContactTypes(contactTypes []string) error {
if err := r.PullCache(false); err != nil {
return err
}
ctSet := sets.NewString(contactTypes...)
for _, ct := range AllContactTypes {
if ctSet.Has(ct) {
r.setVerifiedContactType(ct, true)
} else {
r.setVerifiedContactType(ct, false)
}
}
return nil
}
func (r *SReceiver) PullCache(force bool) error {
if !force && r.subContactCache != nil {
return nil
}
cache, err := SubContactManager.fetchMapByReceiverID(r.Id)
if err != nil {
return err
}
r.subContactCache = cache
return nil
}
func (r *SReceiver) PushCache(ctx context.Context) error {
for subct, subc := range r.subContactCache {
err := SubContactManager.TableSpec().InsertOrUpdate(ctx, subc)
if err != nil {
return errors.Wrapf(err, "fail to save subcontact %q to db", subct)
}
}
return nil
}
func (rm *SReceiverManager) EnabledContactFilter(contactType string, q *sqlchemy.SQuery) *sqlchemy.SQuery {
subQuery := SubContactManager.Query("receiver_id").Equals("type", contactType).IsTrue("enabled").SubQuery()
q = q.Join(subQuery, sqlchemy.Equals(subQuery.Field("receiver_id"), q.Field("id")))
return q
}
func (rm *SReceiverManager) VerifiedContactFilter(contactType string, q *sqlchemy.SQuery) *sqlchemy.SQuery {
subQuery := SubContactManager.Query("receiver_id").Equals("type", contactType).IsTrue("verified").SubQuery()
q = q.Join(subQuery, sqlchemy.Equals(subQuery.Field("receiver_id"), q.Field("id")))
return q
}
func (rm *SReceiverManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ReceiverListInput) (*sqlchemy.SQuery, error) {
q, err := rm.SStatusStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StatusStandaloneResourceListInput)
if err != nil {
return nil, err
}
q, err = rm.SDomainizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainizedResourceListInput)
if err != nil {
return nil, err
}
q, err = rm.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledResourceBaseListInput)
if err != nil {
return nil, err
}
if len(input.UID) > 0 {
q = q.Equals("id", input.UID)
}
if len(input.UName) > 0 {
q = q.Equals("name", input.UName)
}
if len(input.EnabledContactType) > 0 {
q = rm.EnabledContactFilter(input.EnabledContactType, q)
}
if len(input.VerifiedContactType) > 0 {
q = rm.VerifiedContactFilter(input.VerifiedContactType, q)
}
return q, nil
}
func (rm *SReceiverManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.ReceiverDetails {
sRows := rm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
dRows := rm.SDomainizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
rows := make([]api.ReceiverDetails, len(objs))
var err error
for i := range rows {
rows[i].StatusStandaloneResourceDetails = sRows[i]
rows[i].DomainizedResourceInfo = dRows[i]
user := objs[i].(*SReceiver)
if rows[i].EnabledContactTypes, err = user.GetEnabledContactTypes(); err != nil {
log.Errorf("GetEnabledContactTypes: %v", err)
}
if rows[i].VerifiedContactTypes, err = user.GetVerifiedContactTypes(); err != nil {
log.Errorf("GetVerifiedContactTypes: %v", err)
}
}
return rows
}
func (rm *SReceiverManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
q, err := rm.SStatusStandaloneResourceBaseManager.QueryDistinctExtraField(q, field)
if err != nil {
return nil, err
}
q, err = rm.SDomainizedResourceBaseManager.QueryDistinctExtraField(q, field)
if err != nil {
return nil, err
}
return q, nil
}
func (rm *SReceiverManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.ReceiverListInput) (*sqlchemy.SQuery, error) {
q, err := rm.SStatusStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StatusStandaloneResourceListInput)
if err != nil {
return nil, err
}
q, err = rm.SDomainizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainizedResourceListInput)
return q, nil
}
func (r *SReceiver) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
r.SStatusStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
// set status
r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "")
logclient.AddActionLogWithContext(ctx, r, logclient.ACT_CREATE, nil, userCred, true)
task, err := taskman.TaskManager.NewTask(ctx, "SubcontactPullTask", r, userCred, nil, "", "")
if err != nil {
log.Errorf("ContactPullTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
}
func (r *SReceiver) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := r.SStatusStandaloneResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
if err != nil {
return nil
}
var input api.ReceiverCreateInput
err = data.Unmarshal(&input)
if err != nil {
return err
}
// set id and name
r.Id = input.UID
r.Name = input.UName
r.DomainId = input.ProjectDomainId
if input.Enabled == nil {
r.Enabled = tristate.True
}
err = r.SetEnabledContactTypes(input.EnabledContactTypes)
if err != nil {
return errors.Wrap(err, "SetEnabledContactTypes")
}
err = r.PushCache(ctx)
if err != nil {
return errors.Wrap(err, "PushCache")
}
return nil
}
func (r *SReceiver) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverUpdateInput) (api.ReceiverUpdateInput, error) {
var err error
input.StatusStandaloneResourceBaseUpdateInput, err = r.SStatusStandaloneResourceBase.ValidateUpdateData(ctx, userCred, query, input.StatusStandaloneResourceBaseUpdateInput)
if err != nil {
return input, err
}
// validate email
if ok := regutils.MatchEmail(input.Email); !ok {
return input, httperrors.NewInputParameterError("invalid email")
}
// validate mobile
if ok := regutils.MatchMobile(input.Mobile); !ok {
return input, httperrors.NewInputParameterError("invalid mobile")
}
return input, nil
}
func (r *SReceiver) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
r.SStatusStandaloneResourceBase.PreUpdate(ctx, userCred, query, data)
var input api.ReceiverUpdateInput
err := data.Unmarshal(&input)
if err != nil {
log.Errorf("fail to unmarshal to ContactUpdateInput: %v", err)
}
err = r.PullCache(false)
if err != nil {
log.Errorf("PullCache: %v", err)
}
err = r.SetEnabledContactTypes(input.EnabledContactTypes)
if len(input.Email) != 0 {
r.VerifiedEmail = tristate.False
for _, c := range r.subContactCache {
if c.ParentContactType == input.Email {
c.Verified = tristate.False
}
}
}
if len(input.Mobile) != 0 {
r.VerifiedMobile = tristate.False
for _, c := range r.subContactCache {
if c.ParentContactType == input.Mobile {
c.Verified = tristate.False
}
}
}
err = r.PushCache(ctx)
if err != nil {
log.Errorf("PushCache: %v", err)
}
err = ReceiverManager.TableSpec().InsertOrUpdate(ctx, r)
if err != nil {
log.Errorf("InsertOrUpdate: %v", err)
}
}
func (r *SReceiver) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
r.SStatusStandaloneResourceBase.PostUpdate(ctx, userCred, query, data)
// set status
r.SetStatus(userCred, api.RECEIVER_STATUS_PULLING, "")
logclient.AddActionLogWithContext(ctx, r, logclient.ACT_UPDATE, nil, userCred, true)
task, err := taskman.TaskManager.NewTask(ctx, "SubcontactPullTask", r, userCred, nil, "", "")
if err != nil {
log.Errorf("ContactPullTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
}
func (r *SReceiver) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
err := r.PullCache(false)
if err != nil {
return err
}
for _, sc := range r.subContactCache {
err := sc.Delete(ctx, userCred)
if err != nil {
return err
}
}
return r.SStatusStandaloneResourceBase.Delete(ctx, userCred)
}
func (r *SReceiver) IsOwner(userCred mcclient.TokenCredential) bool {
return r.Id == userCred.GetUserId()
}
func (r *SReceiver) AllowPerformTriggerVerify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return r.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, r, "trigger_verify")
}
func (r *SReceiver) PerformTriggerVerify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverTriggerVerifyInput) (jsonutils.JSONObject, error) {
if len(input.ContactType) == 0 {
return nil, httperrors.NewMissingParameterError("contact_type")
}
if !utils.IsInStringArray(input.ContactType, []string{api.EMAIL, api.MOBILE}) {
return nil, httperrors.NewInputParameterError("not support such contact type %q", input.ContactType)
}
_, err := VerificationManager.Create(ctx, r.Id, input.ContactType)
if err == ErrVerifyFrequently {
return nil, httperrors.NewForbiddenError("Send verify message too frequently, please try again later")
}
if err != nil {
return nil, err
}
params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
task, err := taskman.TaskManager.NewTask(ctx, "VerificationSendTask", r, userCred, params, "", "")
if err != nil {
log.Errorf("ContactPullTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
return nil, nil
}
func (r *SReceiver) AllowPerformVerify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return r.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, r, "verify")
}
func (r *SReceiver) PerformVerify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ReceiverVerifyInput) (jsonutils.JSONObject, error) {
if len(input.ContactType) == 0 {
return nil, httperrors.NewMissingParameterError("contact_type")
}
if !utils.IsInStringArray(input.ContactType, []string{api.EMAIL, api.MOBILE}) {
return nil, httperrors.NewInputParameterError("not support such contact type %q", input.ContactType)
}
verification, err := VerificationManager.Get(r.Id, input.ContactType)
if err != nil {
return nil, err
}
if verification.Token != input.Token {
return nil, httperrors.NewInputParameterError("wrong token")
}
_, err = db.Update(r, func() error {
switch input.ContactType {
case api.EMAIL:
r.VerifiedEmail = tristate.True
case api.MOBILE:
r.VerifiedMobile = tristate.True
default:
// no way
}
return nil
})
return nil, err
}
func (r *SReceiver) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformEnableInput) bool {
return r.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, r, "enable")
}
func (r *SReceiver) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformEnableInput) (jsonutils.JSONObject, error) {
err := db.EnabledPerformEnable(r, ctx, userCred, true)
if err != nil {
return nil, errors.Wrap(err, "EnabledPerformEnable")
}
return nil, nil
}
func (r *SReceiver) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformDisableInput) bool {
return r.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, r, "disable")
}
func (r *SReceiver) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformDisableInput) (jsonutils.JSONObject, error) {
err := db.EnabledPerformEnable(r, ctx, userCred, false)
if err != nil {
return nil, errors.Wrap(err, "EnabledPerformEnable")
}
return nil, nil
}
// Implemente interface EventHandler
func (rm *SReceiverManager) OnAdd(obj *jsonutils.JSONDict) {
// do nothing
return
}
func (rm *SReceiverManager) OnUpdate(oldObj, newObj *jsonutils.JSONDict) {
userId, _ := newObj.GetString("id")
receivers, err := rm.FetchByIDs(context.Background(), userId)
if err != nil {
log.Errorf("fail to FetchByIDs: %v", err)
return
}
receiver := &receivers[0]
uname, _ := newObj.GetString("name")
domainId, _ := newObj.GetString("domain_id")
if receiver.Name == uname && receiver.DomainId == domainId {
return
}
_, err = db.Update(receiver, func() error {
receiver.Name = uname
receiver.DomainId = domainId
return nil
})
if err != nil {
log.Errorf("fail to update uname of contact %q: %v", receiver.Id, err)
}
}
func (rm *SReceiverManager) OnDelete(obj *jsonutils.JSONDict) {
userId, _ := obj.GetString("id")
receivers, err := rm.FetchByIDs(context.Background(), userId)
if err != nil {
log.Errorf("fail to FetchByIDs: %v", err)
return
}
receiver := &receivers[0]
err = receiver.Delete(context.Background(), auth.GetAdminSession(context.Background(), "", "").GetToken())
if err != nil {
log.Errorf("fail to delete contact %q: %v", receiver.Id, err)
}
}
func (rm *SReceiverManager) StartWatchUserInKeystone() error {
adminSession := auth.GetAdminSession(context.Background(), "", "")
watchMan, err := informer.NewWatchManagerBySession(adminSession)
if err != nil {
return err
}
resMan := &modules.UsersV3
return watchMan.For(resMan).AddEventHandler(context.Background(), rm)
}
func (rm *SReceiverManager) FetchByIDs(ctx context.Context, ids ...string) ([]SReceiver, error) {
if len(ids) == 0 {
return nil, nil
}
var err error
q := rm.Query()
if len(ids) == 1 {
q = q.Equals("id", ids[0])
} else {
q = q.In("id", ids)
}
contacts := make([]SReceiver, 0, len(ids))
err = db.FetchModelObjects(rm, q, &contacts)
if err != nil {
return nil, err
}
return contacts, nil
}
func (rm *SReceiverManager) FetchByIdOrNames(ctx context.Context, idOrNames ...string) ([]SReceiver, error) {
if len(idOrNames) == 0 {
return nil, nil
}
var err error
q := rm.Query()
if len(idOrNames) == 1 {
q = q.Filter(sqlchemy.OR(
sqlchemy.Equals(q.Field("id"), idOrNames[0]),
sqlchemy.Equals(q.Field("name"), idOrNames[0]),
))
} else {
q = q.Filter(sqlchemy.OR(
sqlchemy.In(q.Field("id"), idOrNames),
sqlchemy.In(q.Field("name"), idOrNames),
))
}
receivers := make([]SReceiver, 0, len(idOrNames))
err = db.FetchModelObjects(rm, q, &receivers)
if err != nil {
return nil, err
}
return receivers, nil
}
func (r *SReceiver) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.ReceiverDetails, error) {
return api.ReceiverDetails{}, nil
}
func (r *SReceiver) SetContact(cType string, contact string) error {
if err := r.PullCache(false); err != nil {
return err
}
switch cType {
case api.EMAIL:
r.Email = contact
case api.MOBILE:
r.Mobile = contact
default:
if sc, ok := r.subContactCache[cType]; ok {
sc.Contact = contact
}
}
return nil
}
func (r *SReceiver) GetContact(cType string) (string, error) {
if err := r.PullCache(false); err != nil {
return "", err
}
switch {
case cType == api.EMAIL:
return r.Email, nil
case cType == api.MOBILE:
return r.Mobile, nil
case utils.IsInStringArray(cType, AllRobotContactTypes):
return r.Mobile, nil
default:
if sc, ok := r.subContactCache[cType]; ok {
return sc.Contact, nil
}
}
return "", nil
}

View File

@@ -0,0 +1,109 @@
package models
import (
"context"
"time"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
)
var ReceiverNotificationManager *SReceiverNotificationManager
func init() {
db.InitManager(func() {
ReceiverNotificationManager = &SReceiverNotificationManager{
SJointResourceBaseManager: db.NewJointResourceBaseManager(
SReceiverNotification{},
"receivernotification_tbl",
"receivernotification",
"receivernotifications",
NotificationManager,
ReceiverManager,
),
}
ReceiverNotificationManager.SetVirtualObject(ReceiverNotificationManager)
})
}
type SReceiverNotificationManager struct {
db.SJointResourceBaseManager
}
// +onecloud:swagger-gen-ignore
type SReceiverNotification struct {
db.SJointResourceBase
ReceiverID string `width:"128" charset:"ascii" nullable:"false"`
NotificationID string `width:"128" charset:"ascii" nullable:"false"`
// ignore if ReceiverID is not empty
Contact string `width:"128" nullable:"false"`
SendAt time.Time `nullable:"false"`
SendBy string `width:"128" nullable:"false"`
Status string `width:"36" charset:"ascii"`
FailedReason string `width:"1024"`
}
func (rnm *SReceiverNotificationManager) Create(ctx context.Context, userCred mcclient.TokenCredential, receiverID, notificationID string) (*SReceiverNotification, error) {
rn := &SReceiverNotification{
ReceiverID: receiverID,
NotificationID: notificationID,
Status: api.RECEIVER_NOTIFICATION_RECEIVED,
SendBy: userCred.GetUserId(),
}
return rn, rnm.TableSpec().Insert(ctx, rn)
}
func (rnm *SReceiverNotificationManager) GetMasterFieldName() string {
return "notification_id"
}
func (rnm *SReceiverNotificationManager) GetSlaveFieldName() string {
return "receiver_id"
}
func (rnm *SReceiverNotificationManager) CreateWithoutReceiver(ctx context.Context, userCred mcclient.TokenCredential, contact, notificationID string) (*SReceiverNotification, error) {
rn := &SReceiverNotification{
NotificationID: notificationID,
Contact: contact,
Status: api.RECEIVER_NOTIFICATION_RECEIVED,
SendBy: userCred.GetUserId(),
}
return rn, rnm.TableSpec().Insert(ctx, rn)
}
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
}
return &receiver, nil
}
func (rn *SReceiverNotification) BeforeSend(ctx context.Context, sendTime time.Time) error {
if sendTime.IsZero() {
sendTime = time.Now()
}
_, err := db.Update(rn, func() error {
rn.SendAt = sendTime
rn.Status = api.RECEIVER_NOTIFICATION_SENT
return nil
})
return err
}
func (rn *SReceiverNotification) AfterSend(ctx context.Context, success bool, reason string) error {
_, err := db.Update(rn, func() error {
if success {
rn.Status = api.RECEIVER_NOTIFICATION_OK
} else {
rn.Status = api.RECEIVER_NOTIFICATION_FAIL
rn.FailedReason = reason
}
return nil
})
return err
}

View File

@@ -0,0 +1,106 @@
// 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 (
"yunion.io/x/pkg/tristate"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
)
type SSubContactManager struct {
db.SStandaloneResourceBaseManager
}
// +onecloud:swagger-gen-ignore
type SSubContact struct {
db.SStandaloneResourceBase
// id of receiver user
ReceiverID string `width:"128" nullable:"false" index:"true"`
Type string `width:"16" nullable:"false" index:"true"`
Contact string `width:"128" nullable:"false"`
ParentContactType string `width:"16" nullable:"false"`
Enabled tristate.TriState `nullable:"false" default:"false"`
Verified tristate.TriState `nullable:"false" default:"false"`
}
var SubContactManager *SSubContactManager
var (
vTrue = true
vFalse = false
pTrue = &vTrue
pFalse = &vFalse
)
func init() {
SubContactManager = &SSubContactManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SSubContact{},
"subcontacts_tbl",
"subcontact",
"subcontacts",
),
}
SubContactManager.SetVirtualObject(SubContactManager)
}
func (scm *SSubContactManager) fetchMapByReceiverID(receiverID string) (map[string]*SSubContact, error) {
q := scm.Query().Equals("receiver_id", receiverID)
scontacts := make([]SSubContact, 0, 3)
err := db.FetchModelObjects(scm, q, &scontacts)
if err != nil {
return nil, err
}
ret := make(map[string]*SSubContact, len(scontacts))
for i := range scontacts {
ret[scontacts[i].Type] = &scontacts[i]
}
return ret, nil
}
func (sc *SSubContact) Enable() error {
return sc.Update(nil, pTrue, nil)
}
func (sc *SSubContact) Disable() error {
return sc.Update(nil, pFalse, nil)
}
func (sc *SSubContact) Verify() error {
return sc.Update(nil, nil, pTrue)
}
func (sc *SSubContact) Disverify() error {
return sc.Update(nil, nil, pFalse)
}
func (sc *SSubContact) Update(contact *string, enabled *bool, verified *bool) error {
_, err := db.Update(sc, func() error {
if contact != nil {
sc.Contact = *contact
}
if enabled != nil {
sc.Enabled = tristate.NewFromBool(*enabled)
}
if verified != nil {
sc.Verified = tristate.NewFromBool(*verified)
}
return nil
})
return err
}

View File

@@ -27,8 +27,10 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
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"
@@ -40,31 +42,29 @@ import (
)
type STemplateManager struct {
SStandaloneResourceBaseManager
db.SStandaloneResourceBaseManager
}
var TemplateManager *STemplateManager
func init() {
TemplateManager = &STemplateManager{
SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
STemplate{},
"notify_t_template",
"template_tbl",
"notifytemplate",
"notifytemplates",
),
}
TemplateManager.SetVirtualObject(TemplateManager)
}
const (
TEMPLATE_TYPE_TITLE = "title"
TEMPLATE_TYPE_CONTENT = "content"
TEMPLATE_TYPE_REMOTE = "remote"
CONTACTTYPE_ALL = "all"
CONTACTTYPE_ALL = "all"
)
type STemplate struct {
SStandaloneResourceBase
db.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"`
@@ -72,21 +72,18 @@ type STemplate struct {
// 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"`
Example string `nullable:"false" created:"required" get:"user" list:"user" update:"user"`
}
const (
verifyUrlPath = "/email-verification/id/{0}/token/{1}?region=%s"
templatePath = "/opt/yunion/share/template"
)
func (tm *STemplateManager) GetEmailUrl() string {
if len(options.Options.ApiServer) > 0 {
return httputils.JoinPath(options.Options.ApiServer, fmt.Sprintf(verifyUrlPath, options.Options.Region))
}
return options.Options.VerifyEmailUrl
return httputils.JoinPath(options.Options.ApiServer, fmt.Sprintf(verifyUrlPath, options.Options.Region))
}
var templatePath = "/opt/yunion/share/template"
func (tm *STemplateManager) defaultTemplate() ([]STemplate, error) {
templates := make([]STemplate, 0, 4)
@@ -143,6 +140,12 @@ func (tm *STemplateManager) GetCompanyInfo(ctx context.Context) (SCompanyInfo, e
return info, nil
}
var (
ForceInitType = []string{
api.EMAIL,
}
)
func (tm *STemplateManager) InitializeData() error {
templates, err := tm.defaultTemplate()
if err != nil {
@@ -151,12 +154,40 @@ func (tm *STemplateManager) InitializeData() error {
for _, template := range templates {
q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType)
count, _ := q.CountWithError()
if count > 0 {
if count > 0 && !utils.IsInStringArray(template.ContactType, ForceInitType) {
continue
}
err := tm.TableSpec().InsertOrUpdate(context.TODO(), &template)
if count == 0 {
err := tm.TableSpec().Insert(context.TODO(), &template)
if err != nil {
return errors.Wrap(err, "sqlchemy.TableSpec.Insert")
}
continue
}
oldTemplates := make([]STemplate, 0, 1)
err := db.FetchModelObjects(tm, q, &oldTemplates)
if err != nil {
return errors.Wrap(err, "sqlchemy.TableSpec.InsertOrUpdate")
return errors.Wrap(err, "db.FetchModelObjects")
}
// delete addtion
var (
ctx = context.Background()
userCred = auth.AdminCredential()
)
for i := 1; i < len(oldTemplates); i++ {
err := oldTemplates[i].Delete(ctx, userCred)
if err != nil {
return errors.Wrap(err, "STemplate.Delete")
}
}
// update
oldTemplate := &oldTemplates[0]
_, err = db.Update(oldTemplate, func() error {
oldTemplate.Content = template.Content
return nil
})
if err != nil {
return errors.Wrap(err, "db.Update")
}
}
return nil
@@ -182,19 +213,19 @@ func (tm *STemplateManager) NotifyFilter(contactType, topic, msg string) (params
for _, template := range templates {
var title, content string
switch template.TemplateType {
case TEMPLATE_TYPE_TITLE:
case api.TEMPLATE_TYPE_TITLE:
title, err = template.Execute(msg)
if err != nil {
return
}
params.Title = title
case TEMPLATE_TYPE_CONTENT:
case api.TEMPLATE_TYPE_CONTENT:
content, err = template.Execute(msg)
if err != nil {
return
}
params.Message = content
case TEMPLATE_TYPE_REMOTE:
case api.TEMPLATE_TYPE_REMOTE:
params.RemoteTemplate = template.Content
params.Message = msg
default:
@@ -223,29 +254,69 @@ func (tm *STemplate) Execute(str string) (string, error) {
return buffer.String(), nil
}
func (manager *STemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
ty, _ := data.GetString("template_type")
if ty != TEMPLATE_TYPE_TITLE && ty != TEMPLATE_TYPE_CONTENT && ty != TEMPLATE_TYPE_REMOTE {
return nil, httperrors.NewInputParameterError("no such support for tempalte type %s", ty)
func (tm *STemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.TemplateCreateInput) (api.TemplateCreateInput, error) {
if !utils.IsInStringArray(input.TemplateType, []string{
api.TEMPLATE_TYPE_CONTENT, api.TEMPLATE_TYPE_REMOTE, api.TEMPLATE_TYPE_TITLE,
}) {
return input, httperrors.NewInputParameterError("no such support for tempalte type %s", input.TemplateType)
}
return data, nil
if input.TemplateType != api.TEMPLATE_TYPE_REMOTE {
if err := tm.validate(input.Content, input.Example); err != nil {
return input, httperrors.NewInputParameterError(err.Error())
}
}
if len(input.Name) == 0 {
input.Name = fmt.Sprintf("%s-%s-%s", input.ContactType, input.Topic, input.TemplateType)
}
return input, nil
}
func (self *STemplateManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
queryDict := query.(*jsonutils.JSONDict)
if queryDict.Contains("topic") {
val, _ := queryDict.GetString("topic")
q = q.Equals("topic", val)
func (tm *STemplateManager) validate(template string, example string) error {
// check example availability
tem, err := ptem.New("tmp").Parse(template)
if err != nil {
return errors.Wrap(err, "invalid template")
}
if queryDict.Contains("template_type") {
val, _ := queryDict.GetString("template_type")
q = q.Equals("template_type", val)
var buffer bytes.Buffer
tmpMap := make(map[string]interface{})
err = json.Unmarshal([]byte(example), &tmpMap)
if err != nil {
return errors.Wrap(err, "invalid example")
}
if queryDict.Contains("contact_type") {
val, _ := queryDict.GetString("contact_type")
q = q.Equals("contact_type", val)
err = tem.Execute(&buffer, tmpMap)
if err != nil {
return errors.Wrap(err, "invalid example")
}
return nil
}
func (tm *STemplateManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.TemplateListInput) (*sqlchemy.SQuery, error) {
q, err := tm.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput)
if err != nil {
return nil, err
}
if len(input.Topic) > 0 {
q = q.Equals("topic", input.Topic)
}
if len(input.TemplateType) > 0 {
q = q.Equals("template_type", input.TemplateType)
}
if len(input.ContactType) > 0 {
q = q.Equals("contact_type", input.ContactType)
}
return q, nil
}
func (t *STemplate) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.TemplateUpdateInput) (api.TemplateUpdateInput, error) {
if t.TemplateType == api.TEMPLATE_TYPE_REMOTE {
return input, nil
}
if err := TemplateManager.validate(input.Content, input.Example); err != nil {
return input, httperrors.NewInputParameterError(err.Error())
}
return input, nil
}
func (t *STemplate) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (api.TemplateDetails, error) {
return api.TemplateDetails{}, nil
}

108
pkg/notify/models/verify.go Normal file
View File

@@ -0,0 +1,108 @@
// 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 (
"context"
"database/sql"
"fmt"
"math/rand"
"time"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/notify/options"
)
type SVerificationManager struct {
db.SStandaloneResourceBaseManager
}
var VerificationManager *SVerificationManager
func init() {
VerificationManager = &SVerificationManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SVerification{},
"verification_tbl",
"verification",
"verifications",
),
}
VerificationManager.SetVirtualObject(VerificationManager)
}
// +onecloud:swagger-gen-ignore
type SVerification struct {
db.SStandaloneResourceBase
ReceiverId string `width:"128" nullable:"false"`
ContactType string `width:"16" nullable:"false"`
Token string `width:"200" nullable:"false"`
}
var ErrVerifyFrequently = errors.Error("Send validation messages too frequently")
func (vm *SVerificationManager) generateVerifyToken() string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
token := fmt.Sprintf("%06v", rnd.Int31n(1000000))
return token
}
func (vm *SVerificationManager) Create(ctx context.Context, receiverId, contactType string) (*SVerification, error) {
// try to reuse
ret, err := vm.Get(receiverId, contactType)
if err != nil && errors.Cause(err) != sql.ErrNoRows {
return nil, err
}
if ret == nil {
ret = &SVerification{
ReceiverId: receiverId,
ContactType: contactType,
Token: vm.generateVerifyToken(),
}
err := vm.TableSpec().Insert(ctx, ret)
if err != nil {
return nil, err
}
} else {
now := time.Now()
if now.Before(ret.CreatedAt.Add(time.Duration(options.Options.VerifyExpireInterval) * time.Minute)) {
return nil, ErrVerifyFrequently
}
_, err := db.Update(ret, func() error {
ret.Token = vm.generateVerifyToken()
ret.CreatedAt = now
ret.UpdatedAt = now
return nil
})
if err != nil {
return nil, err
}
}
return ret, nil
}
func (vm *SVerificationManager) Get(receiverId, contactType string) (*SVerification, error) {
q := vm.Query().Equals("receiver_id", receiverId).Equals("contact_type", contactType)
var verification SVerification
err := q.First(&verification)
if err != nil {
return nil, err
}
verification.SetModelManager(vm, &verification)
return &verification, nil
}

View File

@@ -1,184 +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 models
import (
"context"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/utils"
)
var workMan *appsrv.SWorkerManager
func init() {
workMan = appsrv.NewWorkerManager("NotifyWokerManager", 16, 512, false)
}
func Send(notifications []*SNotification, userCred mcclient.TokenCredential, contacts []string) {
for i := range notifications {
notification, contact := notifications[i], contacts[i]
workMan.Run(func() {
sendone(context.Background(), userCred, notification, contact)
}, nil, nil)
}
}
func sendone(ctx context.Context, userCred mcclient.TokenCredential, notification *SNotification, contact string) {
err := notification.SetSentAndTime(userCred)
if err != nil {
log.Errorf("Change notification's status failed.")
return
}
err = NotifyService.Send(ctx, notification.ContactType, contact, notification.Topic, notification.Msg,
notification.Priority)
if err != nil {
log.Errorf("Send notification failed: %s.", err.Error())
notification.SetStatus(userCred, NOTIFY_FAIL, err.Error())
} else {
log.Debugf("send notification successfully")
notification.SetStatus(userCred, NOTIFY_OK, "")
}
}
func RestartService(config map[string]string, serviceName string) {
workMan.Run(func() {
NotifyService.RestartService(context.Background(), config, serviceName)
}, nil, nil)
}
func SendVerifyMessage(ctx context.Context, userCred mcclient.TokenCredential, verify *SVerify,
contact *SContact) error {
var (
err error
msg string
)
info, err := TemplateManager.GetCompanyInfo(ctx)
if err != nil {
log.Errorf("unable to try to get company info: %s", err.Error())
}
processId, token := verify.ID, verify.Token
if contact.ContactType == "email" {
emailUrl := strings.Replace(TemplateManager.GetEmailUrl(), "{0}", processId, 1)
emailUrl = strings.Replace(emailUrl, "{1}", token, 1)
// get uName
uName, err := utils.GetUsernameByID(ctx, contact.UID)
if err != nil || len(uName) == 0 {
uName = "用户"
}
data := struct {
Name string
Link string
SCompanyInfo
}{
Name: uName,
Link: emailUrl,
SCompanyInfo: info,
}
msg = jsonutils.Marshal(data).String()
} else if contact.ContactType == "mobile" {
msg = fmt.Sprintf(`{"code": "%s"}`, token)
} else {
// todo
return nil
}
err = NotifyService.Send(ctx, contact.ContactType, contact.Contact, "verify", msg, "")
if err != nil {
verify.SetStatus(userCred, VERIFICATION_SENT_FAIL, "")
// set contact's status as "init"
contact.SetStatus(userCred, CONTACT_INIT, "send verify message failed")
log.Errorf("Send verify message failed: %s.", err.Error())
return errors.Wrap(err, "Send Verify Message Failed")
}
verify.SetStatus(userCred, VERIFICATION_SENT, "")
return nil
}
func PullContact(uid string, contactTypes []string) {
for i := range contactTypes {
ct := contactTypes[i]
workMan.Run(func() {
pullContact(context.Background(), uid, ct)
}, nil, nil)
}
}
func pullContact(ctx context.Context, uid string, contactType string) {
contacts, err := ContactManager.FetchByUIDAndCType(uid, []string{MOBILE, contactType})
if err != nil {
log.Errorf("fetch contacts error")
}
if len(contacts) == 0 {
return
}
var mobileContact, subContact *SContact
for i := range contacts {
if contacts[i].ContactType == MOBILE {
mobileContact = &contacts[i]
} else {
subContact = &contacts[i]
}
}
if mobileContact == nil {
return
}
userid, err := NotifyService.ContactByMobile(ctx, mobileContact.Contact, contactType)
if err != nil {
log.Errorf("fetch %s contact by mobile failed: %s", contactType, err.Error())
}
if subContact != nil {
subContact.SetModelManager(ContactManager, subContact)
origin := subContact.Contact
_, err := db.Update(subContact, func() error {
subContact.Contact = userid
subContact.VerifiedAt = time.Now()
if subContact.Status != CONTACT_VERIFIED {
subContact.Status = CONTACT_VERIFIED
}
return nil
})
if err != nil {
log.Errorf("update %s contact userid %s => %s failed", contactType, origin, userid)
}
return
}
contact := SContact{
UID: uid,
ContactType: contactType,
Contact: userid,
Enabled: "1",
VerifiedAt: time.Now(),
}
contact.Status = CONTACT_VERIFIED
err = ContactManager.TableSpec().Insert(ctx, &contact)
if err != nil {
log.Errorf("create new %s contact failed", contactType)
}
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package models
package oldmodels
import (
"context"

View File

@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package _interface // import "yunion.io/x/onecloud/pkg/notify/interface"
package oldmodels // import "yunion.io/x/onecloud/pkg/notify/oldmodels"

View File

@@ -0,0 +1,44 @@
// 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"`
}

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 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"`
}

View File

@@ -0,0 +1,52 @@
// 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"`
}

View File

@@ -0,0 +1,51 @@
// 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"`
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package models
package oldmodels
import (
"database/sql"

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package models
package oldmodels
import (
"context"

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cache
package oldmodels
import (
"context"
@@ -42,7 +42,7 @@ var UserCacheManager *SUserCacheManager
func init() {
dbUserCacheManager := db.SUserCacheManager{
SKeystoneCacheObjectManager: db.NewKeystoneCacheObjectManager(
db.SUser{}, "users_cache_tbl", "user", "users"),
db.SUser{}, "users_cache_tbl", "olduser", "oldusers"),
}
UserCacheManager = &SUserCacheManager{
dbUserCacheManager,

View File

@@ -22,17 +22,16 @@ type NotifyOption struct {
common_options.CommonOptions
common_options.DBOptions
DingtalkEnabled bool `help:"Enable dingtalk"`
SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/notify"`
UpdateInterval int `help:"Update send services interval(unit:min)" default:"30"`
// VerifyEmailUrlPath string `help:"url of verify email" json:"verify_email_url_path"`
SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/socket"`
UpdateInterval int `help:"Update send services interval(unit:min)" default:"30"`
// Deprecated
VerifyEmailUrl string `help:"url of verify email" json:"verify_email_url"`
ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope seconds" default:"30"`
ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope seconds" default:"60"`
MaxSendTimes int `help:"Resend all messages whose sendTimes less than MaxSendTimes" default:"2"`
InitNotificationScope int `help:"initialize data of notification with in InitNotificationScope hours" default:"100"`
MaxSyncNotification int `help:"The max number of notification sync from old data source" default:"1000"`
VerifyExpireInterval int `help:"expire interval of verify message; minutes" default:"5"`
}
var Options NotifyOption

View File

@@ -58,3 +58,9 @@ func (c *SendNotificationClient) UseridByMobile(ctx context.Context, in *UseridB
defer cancel()
return c.sendAgentClient.UseridByMobile(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...)
}

View File

@@ -1,17 +1,3 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: send_server.proto
@@ -313,6 +299,163 @@ func (m *ValidateConfigReply) GetMsg() string {
return ""
}
type BatchSendParams struct {
Contacts []string `protobuf:"bytes,1,rep,name=Contacts,proto3" json:"Contacts,omitempty"`
Title string `protobuf:"bytes,2,opt,name=Title,proto3" json:"Title,omitempty"`
Message string `protobuf:"bytes,3,opt,name=Message,proto3" json:"Message,omitempty"`
Priority string `protobuf:"bytes,4,opt,name=Priority,proto3" json:"Priority,omitempty"`
RemoteTemplate string `protobuf:"bytes,5,opt,name=RemoteTemplate,proto3" json:"RemoteTemplate,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *BatchSendParams) Reset() { *m = BatchSendParams{} }
func (m *BatchSendParams) String() string { return proto.CompactTextString(m) }
func (*BatchSendParams) ProtoMessage() {}
func (*BatchSendParams) Descriptor() ([]byte, []int) {
return fileDescriptor_63fdd68f7eb311f9, []int{6}
}
func (m *BatchSendParams) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_BatchSendParams.Unmarshal(m, b)
}
func (m *BatchSendParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_BatchSendParams.Marshal(b, m, deterministic)
}
func (m *BatchSendParams) XXX_Merge(src proto.Message) {
xxx_messageInfo_BatchSendParams.Merge(m, src)
}
func (m *BatchSendParams) XXX_Size() int {
return xxx_messageInfo_BatchSendParams.Size(m)
}
func (m *BatchSendParams) XXX_DiscardUnknown() {
xxx_messageInfo_BatchSendParams.DiscardUnknown(m)
}
var xxx_messageInfo_BatchSendParams proto.InternalMessageInfo
func (m *BatchSendParams) GetContacts() []string {
if m != nil {
return m.Contacts
}
return nil
}
func (m *BatchSendParams) GetTitle() string {
if m != nil {
return m.Title
}
return ""
}
func (m *BatchSendParams) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func (m *BatchSendParams) GetPriority() string {
if m != nil {
return m.Priority
}
return ""
}
func (m *BatchSendParams) GetRemoteTemplate() string {
if m != nil {
return m.RemoteTemplate
}
return ""
}
type FailedRecord struct {
Contact string `protobuf:"bytes,1,opt,name=Contact,proto3" json:"Contact,omitempty"`
Reason string `protobuf:"bytes,2,opt,name=Reason,proto3" json:"Reason,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *FailedRecord) Reset() { *m = FailedRecord{} }
func (m *FailedRecord) String() string { return proto.CompactTextString(m) }
func (*FailedRecord) ProtoMessage() {}
func (*FailedRecord) Descriptor() ([]byte, []int) {
return fileDescriptor_63fdd68f7eb311f9, []int{7}
}
func (m *FailedRecord) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_FailedRecord.Unmarshal(m, b)
}
func (m *FailedRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_FailedRecord.Marshal(b, m, deterministic)
}
func (m *FailedRecord) XXX_Merge(src proto.Message) {
xxx_messageInfo_FailedRecord.Merge(m, src)
}
func (m *FailedRecord) XXX_Size() int {
return xxx_messageInfo_FailedRecord.Size(m)
}
func (m *FailedRecord) XXX_DiscardUnknown() {
xxx_messageInfo_FailedRecord.DiscardUnknown(m)
}
var xxx_messageInfo_FailedRecord proto.InternalMessageInfo
func (m *FailedRecord) GetContact() string {
if m != nil {
return m.Contact
}
return ""
}
func (m *FailedRecord) GetReason() string {
if m != nil {
return m.Reason
}
return ""
}
type BatchSendReply struct {
FailedRecords []*FailedRecord `protobuf:"bytes,1,rep,name=FailedRecords,proto3" json:"FailedRecords,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *BatchSendReply) Reset() { *m = BatchSendReply{} }
func (m *BatchSendReply) String() string { return proto.CompactTextString(m) }
func (*BatchSendReply) ProtoMessage() {}
func (*BatchSendReply) Descriptor() ([]byte, []int) {
return fileDescriptor_63fdd68f7eb311f9, []int{8}
}
func (m *BatchSendReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_BatchSendReply.Unmarshal(m, b)
}
func (m *BatchSendReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_BatchSendReply.Marshal(b, m, deterministic)
}
func (m *BatchSendReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_BatchSendReply.Merge(m, src)
}
func (m *BatchSendReply) XXX_Size() int {
return xxx_messageInfo_BatchSendReply.Size(m)
}
func (m *BatchSendReply) XXX_DiscardUnknown() {
xxx_messageInfo_BatchSendReply.DiscardUnknown(m)
}
var xxx_messageInfo_BatchSendReply proto.InternalMessageInfo
func (m *BatchSendReply) GetFailedRecords() []*FailedRecord {
if m != nil {
return m.FailedRecords
}
return nil
}
func init() {
proto.RegisterType((*SendParams)(nil), "apis.SendParams")
proto.RegisterType((*UpdateConfigParams)(nil), "apis.UpdateConfigParams")
@@ -321,37 +464,47 @@ func init() {
proto.RegisterType((*Empty)(nil), "apis.Empty")
proto.RegisterType((*UseridByMobileReply)(nil), "apis.UseridByMobileReply")
proto.RegisterType((*ValidateConfigReply)(nil), "apis.ValidateConfigReply")
proto.RegisterType((*BatchSendParams)(nil), "apis.BatchSendParams")
proto.RegisterType((*FailedRecord)(nil), "apis.FailedRecord")
proto.RegisterType((*BatchSendReply)(nil), "apis.BatchSendReply")
}
func init() { proto.RegisterFile("send_server.proto", fileDescriptor_63fdd68f7eb311f9) }
var fileDescriptor_63fdd68f7eb311f9 = []byte{
// 398 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0xcf, 0x6a, 0xdb, 0x40,
0x10, 0xc6, 0x51, 0xfc, 0x47, 0xc9, 0x24, 0x98, 0x74, 0x13, 0xca, 0x56, 0xa7, 0x20, 0x70, 0xf1,
0xa5, 0x3a, 0xb8, 0x14, 0x8a, 0x2f, 0xc5, 0x35, 0xa6, 0x27, 0x83, 0x51, 0xed, 0x5e, 0xcb, 0xda,
0x9a, 0x8a, 0xa5, 0x92, 0x56, 0xec, 0xae, 0x0d, 0x7a, 0x8c, 0xbe, 0x49, 0x5f, 0xaf, 0xb7, 0xb2,
0x7f, 0x54, 0x5b, 0xad, 0x9b, 0x9b, 0x7e, 0xdf, 0xcc, 0x37, 0xcc, 0x7c, 0x5a, 0x78, 0xa1, 0xb0,
0xca, 0xbe, 0x2a, 0x94, 0x47, 0x94, 0x49, 0x2d, 0x85, 0x16, 0xa4, 0xcf, 0x6a, 0xae, 0xe2, 0x9f,
0x01, 0xc0, 0x67, 0xac, 0xb2, 0x35, 0x93, 0xac, 0x54, 0x84, 0x42, 0xb8, 0x10, 0x95, 0x66, 0x7b,
0x4d, 0x83, 0xa7, 0x60, 0x72, 0x93, 0xb6, 0x48, 0x1e, 0x61, 0xb0, 0x11, 0x35, 0xdf, 0xd3, 0x2b,
0xab, 0x3b, 0xb0, 0x2a, 0xd7, 0x05, 0xd2, 0x9e, 0x57, 0x0d, 0x98, 0x29, 0x2b, 0x54, 0x8a, 0xe5,
0x48, 0xfb, 0x6e, 0x8a, 0x47, 0x12, 0xc1, 0xf5, 0x5a, 0x72, 0x21, 0xb9, 0x6e, 0xe8, 0xc0, 0x96,
0xfe, 0x30, 0x79, 0x0d, 0xa3, 0x14, 0x4b, 0xa1, 0x71, 0x83, 0x65, 0x5d, 0x30, 0x8d, 0x74, 0x68,
0x3b, 0xfe, 0x52, 0xe3, 0x1f, 0x01, 0x90, 0x6d, 0x9d, 0x31, 0x8d, 0x0b, 0x51, 0x7d, 0xe3, 0xb9,
0x5f, 0xfd, 0x03, 0x84, 0x7b, 0xcb, 0x8a, 0x06, 0x4f, 0xbd, 0xc9, 0xed, 0x74, 0x9c, 0x98, 0x0b,
0x93, 0x7f, 0x5b, 0x13, 0x07, 0x6a, 0x59, 0x69, 0xd9, 0xa4, 0xad, 0x2b, 0x9a, 0xc1, 0xdd, 0x79,
0x81, 0xdc, 0x43, 0xef, 0x3b, 0x36, 0x3e, 0x07, 0xf3, 0x69, 0xae, 0x3d, 0xb2, 0xe2, 0x80, 0x6d,
0x06, 0x16, 0x66, 0x57, 0xef, 0x83, 0x38, 0x81, 0xc7, 0xad, 0x42, 0xc9, 0xb3, 0x8f, 0xcd, 0x4a,
0xec, 0x78, 0x81, 0x7e, 0xa9, 0x97, 0x30, 0x2c, 0x2d, 0xfb, 0x31, 0x9e, 0xe2, 0x10, 0x06, 0xcb,
0xb2, 0xd6, 0x4d, 0xfc, 0x06, 0x1e, 0xba, 0xc6, 0x14, 0xeb, 0xa2, 0x31, 0xbe, 0x83, 0x95, 0x5b,
0x9f, 0xa3, 0x78, 0x0e, 0x0f, 0x5f, 0x58, 0xc1, 0x4f, 0x17, 0xb9, 0x76, 0x0a, 0x21, 0x57, 0xb6,
0x60, 0xfb, 0xaf, 0xd3, 0x16, 0xcd, 0x11, 0xa5, 0xca, 0xfd, 0xc2, 0xe6, 0x73, 0xfa, 0x2b, 0x80,
0x1b, 0xf3, 0xc7, 0xe7, 0x39, 0x56, 0x9a, 0x8c, 0xa1, 0x6f, 0x80, 0xdc, 0xbb, 0xb0, 0x4e, 0x4f,
0x21, 0xba, 0x75, 0x8a, 0x5d, 0x93, 0xbc, 0x83, 0xbb, 0xf3, 0x1c, 0x09, 0xfd, 0x5f, 0xb6, 0x5d,
0xdb, 0x12, 0x46, 0xdd, 0x75, 0x9f, 0x31, 0xbe, 0x72, 0x95, 0x4b, 0xe7, 0x7d, 0x82, 0x51, 0x37,
0x24, 0x12, 0xf9, 0x31, 0x17, 0x32, 0x6f, 0x07, 0x5d, 0x88, 0x75, 0x37, 0xb4, 0x4f, 0xff, 0xed,
0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x8d, 0x12, 0x77, 0x0f, 0x03, 0x00, 0x00,
// 497 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x94, 0xdd, 0x6a, 0xd4, 0x40,
0x14, 0xc7, 0xc9, 0x7e, 0xef, 0xe9, 0xba, 0xd6, 0xe9, 0x5a, 0xc6, 0x5c, 0x95, 0x40, 0xa5, 0x37,
0xe6, 0xa2, 0x22, 0x2c, 0xbd, 0xd1, 0xb6, 0xac, 0x82, 0x50, 0x28, 0xb1, 0xf5, 0x56, 0xa6, 0xc9,
0x71, 0x1d, 0x4c, 0x32, 0x21, 0x33, 0x2d, 0xe4, 0x31, 0x7c, 0x04, 0xdf, 0x40, 0xf0, 0x05, 0x65,
0x3e, 0xb2, 0x9b, 0xac, 0xbb, 0xed, 0x5d, 0x7e, 0xe7, 0x8b, 0x39, 0xe7, 0xff, 0x27, 0xf0, 0x42,
0x62, 0x9e, 0x7c, 0x93, 0x58, 0x3e, 0x60, 0x19, 0x16, 0xa5, 0x50, 0x82, 0xf4, 0x58, 0xc1, 0x65,
0xf0, 0xc7, 0x03, 0xf8, 0x82, 0x79, 0x72, 0xcd, 0x4a, 0x96, 0x49, 0x42, 0x61, 0x78, 0x29, 0x72,
0xc5, 0x62, 0x45, 0xbd, 0x23, 0xef, 0x64, 0x1c, 0xd5, 0x48, 0x66, 0xd0, 0xbf, 0x11, 0x05, 0x8f,
0x69, 0xc7, 0xc4, 0x2d, 0x98, 0x28, 0x57, 0x29, 0xd2, 0xae, 0x8b, 0x6a, 0xd0, 0x53, 0xae, 0x50,
0x4a, 0xb6, 0x44, 0xda, 0xb3, 0x53, 0x1c, 0x12, 0x1f, 0x46, 0xd7, 0x25, 0x17, 0x25, 0x57, 0x15,
0xed, 0x9b, 0xd4, 0x8a, 0xc9, 0x6b, 0x98, 0x46, 0x98, 0x09, 0x85, 0x37, 0x98, 0x15, 0x29, 0x53,
0x48, 0x07, 0xa6, 0x62, 0x23, 0x1a, 0xfc, 0xf2, 0x80, 0xdc, 0x16, 0x09, 0x53, 0x78, 0x29, 0xf2,
0xef, 0x7c, 0xe9, 0x9e, 0xfe, 0x1e, 0x86, 0xb1, 0x61, 0x49, 0xbd, 0xa3, 0xee, 0xc9, 0xde, 0xe9,
0x71, 0xa8, 0x37, 0x0c, 0xff, 0x2f, 0x0d, 0x2d, 0xc8, 0x45, 0xae, 0xca, 0x2a, 0xaa, 0xbb, 0xfc,
0x33, 0x98, 0x34, 0x13, 0x64, 0x1f, 0xba, 0x3f, 0xb1, 0x72, 0x77, 0xd0, 0x9f, 0x7a, 0xdb, 0x07,
0x96, 0xde, 0x63, 0x7d, 0x03, 0x03, 0x67, 0x9d, 0xb9, 0x17, 0x84, 0x30, 0xbb, 0x95, 0x58, 0xf2,
0xe4, 0xa2, 0xba, 0x12, 0x77, 0x3c, 0x45, 0xf7, 0xa8, 0x43, 0x18, 0x64, 0x86, 0xdd, 0x18, 0x47,
0xc1, 0x10, 0xfa, 0x8b, 0xac, 0x50, 0x55, 0xf0, 0x06, 0x0e, 0xda, 0x8d, 0x11, 0x16, 0x69, 0xa5,
0xfb, 0xee, 0x4d, 0xb8, 0xee, 0xb3, 0x14, 0x9c, 0xc3, 0xc1, 0x57, 0x96, 0xf2, 0xf5, 0x46, 0xb6,
0x9c, 0xc2, 0x90, 0x4b, 0x93, 0x30, 0xf5, 0xa3, 0xa8, 0x46, 0xbd, 0x44, 0x26, 0x97, 0xee, 0xc1,
0xfa, 0x33, 0xf8, 0xed, 0xc1, 0xf3, 0x0b, 0xa6, 0xe2, 0x1f, 0x0d, 0xd9, 0x7d, 0x18, 0x39, 0x9d,
0xed, 0xf1, 0xc6, 0xd1, 0x8a, 0xd7, 0x12, 0x77, 0x76, 0x48, 0xdc, 0xdd, 0x2d, 0x71, 0xef, 0x49,
0x89, 0xfb, 0x5b, 0x25, 0xfe, 0x00, 0x93, 0x8f, 0x8c, 0xa7, 0x98, 0x44, 0x18, 0x8b, 0x32, 0x79,
0xc4, 0x96, 0x87, 0x30, 0x88, 0x90, 0x49, 0x91, 0xbb, 0xe7, 0x39, 0x0a, 0x3e, 0xc3, 0x74, 0xb5,
0xa4, 0xbd, 0xd1, 0x1c, 0x9e, 0x35, 0x67, 0xd6, 0x2e, 0x21, 0xd6, 0x25, 0xcd, 0x54, 0xd4, 0x2e,
0x3c, 0xfd, 0xdb, 0x81, 0xb1, 0x9e, 0x73, 0xbe, 0xc4, 0x5c, 0x91, 0x63, 0xe8, 0x69, 0x20, 0xfb,
0xb6, 0x71, 0x7d, 0x45, 0x7f, 0xcf, 0x46, 0x8c, 0xb0, 0xe4, 0x1d, 0x4c, 0x9a, 0xce, 0x23, 0x74,
0x97, 0x1b, 0xdb, 0x6d, 0x0b, 0x98, 0xb6, 0x05, 0x7e, 0xa4, 0xf1, 0x95, 0xcd, 0x6c, 0x33, 0xc4,
0x27, 0x98, 0xb6, 0x6d, 0x45, 0x7c, 0x37, 0x66, 0x8b, 0x4b, 0xeb, 0x41, 0xdb, 0x8c, 0x38, 0x87,
0xf1, 0xea, 0x8e, 0xe4, 0xa5, 0xad, 0xdb, 0x70, 0x8f, 0x3f, 0xdb, 0x08, 0x9b, 0xce, 0xbb, 0x81,
0xf9, 0xcd, 0xbc, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0x71, 0x6f, 0x12, 0x61, 0x7b, 0x04, 0x00,
0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@@ -370,6 +523,7 @@ type SendAgentClient interface {
UpdateConfig(ctx context.Context, in *UpdateConfigParams, opts ...grpc.CallOption) (*Empty, error)
ValidateConfig(ctx context.Context, in *UpdateConfigParams, opts ...grpc.CallOption) (*ValidateConfigReply, error)
UseridByMobile(ctx context.Context, in *UseridByMobileParams, opts ...grpc.CallOption) (*UseridByMobileReply, error)
BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error)
}
type sendAgentClient struct {
@@ -416,12 +570,22 @@ func (c *sendAgentClient) UseridByMobile(ctx context.Context, in *UseridByMobile
return out, nil
}
func (c *sendAgentClient) BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error) {
out := new(BatchSendReply)
err := c.cc.Invoke(ctx, "/apis.SendAgent/BatchSend", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SendAgentServer is the server API for SendAgent service.
type SendAgentServer interface {
Send(context.Context, *SendParams) (*Empty, error)
UpdateConfig(context.Context, *UpdateConfigParams) (*Empty, error)
ValidateConfig(context.Context, *UpdateConfigParams) (*ValidateConfigReply, error)
UseridByMobile(context.Context, *UseridByMobileParams) (*UseridByMobileReply, error)
BatchSend(context.Context, *BatchSendParams) (*BatchSendReply, error)
}
// UnimplementedSendAgentServer can be embedded to have forward compatible implementations.
@@ -440,6 +604,9 @@ func (*UnimplementedSendAgentServer) ValidateConfig(ctx context.Context, req *Up
func (*UnimplementedSendAgentServer) UseridByMobile(ctx context.Context, req *UseridByMobileParams) (*UseridByMobileReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method UseridByMobile not implemented")
}
func (*UnimplementedSendAgentServer) BatchSend(ctx context.Context, req *BatchSendParams) (*BatchSendReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchSend not implemented")
}
func RegisterSendAgentServer(s *grpc.Server, srv SendAgentServer) {
s.RegisterService(&_SendAgent_serviceDesc, srv)
@@ -517,6 +684,24 @@ func _SendAgent_UseridByMobile_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _SendAgent_BatchSend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchSendParams)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SendAgentServer).BatchSend(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/apis.SendAgent/BatchSend",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SendAgentServer).BatchSend(ctx, req.(*BatchSendParams))
}
return interceptor(ctx, in, info, handler)
}
var _SendAgent_serviceDesc = grpc.ServiceDesc{
ServiceName: "apis.SendAgent",
HandlerType: (*SendAgentServer)(nil),
@@ -537,6 +722,10 @@ var _SendAgent_serviceDesc = grpc.ServiceDesc{
MethodName: "UseridByMobile",
Handler: _SendAgent_UseridByMobile_Handler,
},
{
MethodName: "BatchSend",
Handler: _SendAgent_BatchSend_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "send_server.proto",

View File

@@ -45,9 +45,27 @@ message ValidateConfigReply {
string msg = 2;
}
message BatchSendParams {
repeated string Contacts = 1;
string Title = 2;
string Message = 3;
string Priority = 4;
string RemoteTemplate = 5;
}
message FailedRecord {
string Contact = 1;
string Reason = 2;
}
message BatchSendReply {
repeated FailedRecord FailedRecords = 1;
}
service SendAgent {
rpc Send(SendParams) returns (Empty);
rpc UpdateConfig(UpdateConfigParams) returns (Empty);
rpc ValidateConfig(UpdateConfigParams) returns (ValidateConfigReply);
rpc UseridByMobile(UseridByMobileParams) returns (UseridByMobileReply);
rpc BatchSend (BatchSendParams) returns (BatchSendReply);
}

View File

@@ -32,7 +32,7 @@ import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/mcclient"
_interface "yunion.io/x/onecloud/pkg/notify/interface"
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/fileutils2"
@@ -49,13 +49,13 @@ const (
type SRpcService struct {
SendServices *ServiceMap
socketFileDir string
configStore _interface.IServiceConfigStore
templateStore _interface.ITemplateStore
configStore notifyv2.IServiceConfigStore
templateStore notifyv2.ITemplateStore
}
// NewSRpcService create a SRpcService
func NewSRpcService(socketFileDir string, configStore _interface.IServiceConfigStore,
tempalteStore _interface.ITemplateStore) *SRpcService {
func NewSRpcService(socketFileDir string, configStore notifyv2.IServiceConfigStore,
tempalteStore notifyv2.ITemplateStore) *SRpcService {
return &SRpcService{
SendServices: NewServiceMap(),
socketFileDir: socketFileDir,
@@ -129,10 +129,36 @@ func (self *SRpcService) Send(ctx context.Context, contactType, contact, topic,
return nil
}
func (self *SRpcService) BatchSend(ctx context.Context, contacts []string, contactType, topic, message, priority string) ([]*apis.FailedRecord, error) {
args, err := self.templateStore.NotifyFilter(contactType, topic, message)
if err != nil {
return nil, errors.Wrap(err, "templateStore.NotifyFilter")
}
batchSendParams := apis.BatchSendParams{
Contacts: contacts,
Title: args.Title,
Message: args.Message,
Priority: args.Priority,
RemoteTemplate: args.RemoteTemplate,
}
f := func(service *apis.SendNotificationClient) (interface{}, error) {
return service.BatchSend(ctx, &batchSendParams)
}
ret, err := self.execute(ctx, f, contactType)
if err != nil {
return nil, errors.Wrapf(err, "contactType '%s'", contactType)
}
reply := ret.(*apis.BatchSendReply)
return reply.FailedRecords, nil
}
// RestartService can restart remote rpc server and pass config info.
// This function should be call immediately after init notify server firstly
// This function should be call immediately after accept the request about changing config.
func (self *SRpcService) RestartService(ctx context.Context, config _interface.SConfig, serviceName string) {
func (self *SRpcService) RestartService(ctx context.Context, config notifyv2.SConfig, serviceName string) {
_, err := self.restartWithConfig(ctx, serviceName, config)
if err != nil {
log.Debugf("restart service failed: %s", err)

View File

@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cache // import "yunion.io/x/onecloud/pkg/notify/cache"
package service // import "yunion.io/x/onecloud/pkg/notify/service"

View File

@@ -0,0 +1,63 @@
package service
import (
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
"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"
)
const (
API_VERSION = "v2"
)
func InitHandlers(app *appsrv.Application) {
db.InitAllManagers()
db.RegistUserCredCacheUpdater()
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,
taskman.SubTaskManager,
taskman.TaskObjectManager,
db.UserCacheManager,
db.TenantCacheManager,
models.SubContactManager,
models.VerificationManager,
} {
db.RegisterModelManager(manager)
}
for _, manager := range []db.IModelManager{
db.OpsLog,
db.Metadata,
models.ReceiverManager,
models.NotificationManager,
models.ConfigManager,
models.TemplateManager,
} {
db.RegisterModelManager(manager)
handler := db.NewModelHandler(manager)
dispatcher.AddModelDispatcher(API_VERSION, app, handler)
}
for _, manager := range []db.IJointModelManager{
models.ReceiverNotificationManager,
} {
db.RegisterModelManager(manager)
handler := db.NewJointModelHandler(manager)
dispatcher.AddJointModelDispatcher(API_VERSION, app, handler)
}
}

View File

@@ -12,10 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
package service
import (
"context"
"os"
"time"
@@ -29,12 +28,11 @@ import (
"yunion.io/x/onecloud/pkg/cloudcommon/cronman"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/cache"
"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/tasks"
)
func StartService() {
@@ -60,9 +58,6 @@ func StartService() {
db.EnsureAppInitSyncDB(applicaion, dbOpts, models.InitDB)
defer cloudcommon.CloseDB()
// init cache
cache.RegistUserCredCacheUpdater()
// init notify service
models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager, models.TemplateManager)
models.NotifyService.InitAll()
@@ -73,10 +68,7 @@ func StartService() {
cron.AddJobAtIntervals("UpdateServices", time.Duration(opts.UpdateInterval)*time.Minute, models.NotifyService.UpdateServices)
// wrapped func to resend notifications
resend := func(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
models.ReSend(opts.ReSendScope)
}
cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Second, resend)
cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Second, models.NotificationManager.ReSend)
cron.Start()
app.ServeForever(applicaion, baseOpts)

View File

@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package utils // import "yunion.io/x/onecloud/pkg/notify/utils"
package tasks // import "yunion.io/x/onecloud/pkg/notify/tasks"

View File

@@ -0,0 +1,148 @@
package tasks
import (
"context"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
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/util/logclient"
)
type NotificationSendTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(NotificationSendTask{})
}
func (self *NotificationSendTask) taskFailed(ctx context.Context, notification *models.SNotification, reason string, all bool) {
log.Errorf("fail to send notification %q", notification.GetId())
if all {
notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_FAILED, reason)
} else {
notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_PART_OK, reason)
}
notification.AddOne()
logclient.AddActionLogWithContext(ctx, notification, logclient.ACT_SEND_NOTIFICATION, reason, self.UserCred, false)
self.SetStageFailed(ctx, jsonutils.NewString(reason))
}
func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
notification := obj.(*models.SNotification)
if notification.Status == apis.NOTIFICATION_STATUS_OK {
self.SetStageComplete(ctx, nil)
return
}
rns, err := notification.ReceiverNotificationsNotOK()
if err != nil {
self.taskFailed(ctx, notification, "fail to fetch ReceiverNotifications", true)
return
}
notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_SENDING, "")
// sort out what needs to be sent
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
contactMap := make(map[string]*models.SReceiverNotification)
for i := range rns {
if len(rns[i].ReceiverID) == 0 {
contactMap[rns[i].Contact] = &rns[i]
continue
}
receiver, err := rns[i].Receiver()
if err != nil {
sendFail(&rns[i], fmt.Sprintf("fail to fetch Receiver: %s", err.Error()))
continue
}
// check receiver enabled
if receiver.Enabled.IsFalse() {
sendFail(&rns[i], fmt.Sprintf("disabled receiver"))
continue
}
// check contact enabled
enabled, err := receiver.IsEnabledContactType(notification.ContactType)
if err != nil {
sendFail(&rns[i], fmt.Sprintf("IsEnabledContactType error for receiver: %s", err.Error()))
continue
}
if !enabled {
sendFail(&rns[i], fmt.Sprintf("disabled contactType %q", notification.ContactType))
continue
}
// check contact verified
verified, err := receiver.IsVerifiedContactType(notification.ContactType)
if err != nil {
sendFail(&rns[i], fmt.Sprintf("IsVerifiedContactType error for receiver: %s", err.Error()))
continue
}
if !verified {
sendFail(&rns[i], fmt.Sprintf("unverified contactType %q", notification.ContactType))
continue
}
contact, err := receiver.GetContact(notification.ContactType)
if err != nil {
reason := fmt.Sprintf("fail to fetch contact: %s", err.Error())
sendFail(&rns[i], reason)
continue
}
contactMap[contact] = &rns[i]
}
if len(contactMap) == 0 {
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), true)
}
// set status before send
now := time.Now()
contacts := make([]string, 0, len(contactMap))
for c, rn := range contactMap {
rn.BeforeSend(ctx, now)
contacts = append(contacts, c)
}
// send
ret, err := models.NotifyService.BatchSend(ctx, contacts, notification.ContactType, notification.Topic, notification.Message, notification.Priority)
if err != nil {
for _, rn := range contactMap {
rn.AfterSend(ctx, false, err.Error())
}
failedRecord = append(failedRecord, fmt.Sprintf("others: %s", err.Error()))
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), true)
return
}
// check result
for _, fd := range ret {
rn := contactMap[fd.Contact]
rn.AfterSend(ctx, false, fd.Reason)
failedRecord = append(failedRecord, fmt.Sprintf("%s: %s", rn.ReceiverID, fd.Reason))
}
if len(failedRecord) == len(contacts) {
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), true)
return
}
if len(failedRecord) > 0 {
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), false)
return
}
log.Infof("successfully send notification %q", notification.GetId())
notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_OK, "")
logclient.AddActionLogWithContext(ctx, notification, logclient.ACT_SEND_NOTIFICATION, "", self.UserCred, true)
self.SetStageComplete(ctx, nil)
}

View File

@@ -0,0 +1,87 @@
// 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"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/utils"
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/util/logclient"
)
var PullContactType = []string{
apis.DINGTALK,
apis.FEISHU,
apis.WORKWX,
}
type SubcontactPullTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(SubcontactPullTask{})
}
func (self *SubcontactPullTask) taskFailed(ctx context.Context, receiver *models.SReceiver, reason string) {
log.Errorf("fail to pull subcontact of receiver %q: %s", receiver.Id, reason)
receiver.SetStatus(self.UserCred, apis.RECEIVER_STATUS_PULL_FAILED, reason)
logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_PULL_SUBCONTACT, reason, self.UserCred, false)
self.SetStageFailed(ctx, jsonutils.NewString(reason))
}
func (self *SubcontactPullTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
// pull contacts
receiver := obj.(*models.SReceiver)
if len(receiver.Mobile) == 0 {
self.SetStageComplete(ctx, nil)
return
}
enabledContactTypes, _ := receiver.GetEnabledContactTypes()
for _, cType := range enabledContactTypes {
if !utils.IsInStringArray(cType, PullContactType) {
continue
}
userid, err := models.NotifyService.ContactByMobile(ctx, receiver.Mobile, cType)
if err != nil {
reason := fmt.Sprintf("fail to get %s contact by mobile %q: %v", cType, receiver.Mobile, err)
self.taskFailed(ctx, receiver, reason)
return
}
receiver.SetContact(cType, userid)
receiver.MarkContactTypeVerified(cType)
}
receiver.SetContact(apis.WEBCONSOLE, receiver.Id)
receiver.MarkContactTypeVerified(apis.WEBCONSOLE)
// push cache
err := receiver.PushCache(ctx)
if err != nil {
reason := fmt.Sprintf("PushCache: %v", err)
self.taskFailed(ctx, receiver, reason)
return
}
// success
receiver.SetStatus(self.UserCred, apis.RECEIVER_STATUS_READY, "")
logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_PULL_SUBCONTACT, "", self.UserCred, true)
self.SetStageComplete(ctx, nil)
}

View File

@@ -0,0 +1,76 @@
package tasks
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
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/notify/models"
"yunion.io/x/onecloud/pkg/util/logclient"
)
type VerificationSendTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(VerificationSendTask{})
}
func (self *VerificationSendTask) taskFailed(ctx context.Context, receiver *models.SReceiver, reason string) {
log.Errorf("fail to send verification: %s", reason)
logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_SEND_VERIFICATION, reason, self.UserCred, false)
self.SetStageFailed(ctx, jsonutils.NewString(reason))
}
func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
receiver := obj.(*models.SReceiver)
contactType, _ := self.Params.GetString("contact_type")
verification, err := models.VerificationManager.Get(receiver.GetId(), contactType)
if err != nil {
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
}
// build message
var message string
switch contactType {
case api.EMAIL:
info, err := models.TemplateManager.GetCompanyInfo(ctx)
if err != nil {
self.taskFailed(ctx, receiver, fmt.Sprintf("fail to get company info: %s", err.Error()))
return
}
data := struct {
models.SCompanyInfo
Name string
Code string
}{
Name: receiver.Name,
Code: verification.Token,
SCompanyInfo: info,
}
message = jsonutils.Marshal(data).String()
case api.MOBILE:
message = fmt.Sprintf(`{"code": "%s"}`, verification.Token)
default:
// no way
}
err = models.NotifyService.Send(ctx, contactType, contact, "verify", message, "")
if err != nil {
self.taskFailed(ctx, receiver, err.Error())
return
}
logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_SEND_VERIFICATION, "", self.UserCred, true)
self.SetStageComplete(ctx, nil)
}

View File

@@ -1,81 +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 utils
import (
"context"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/cache"
)
func GetUserByIDOrName(ctx context.Context, idStr string) (*cache.SUser, error) {
return cache.UserCacheManager.FetchUserByIDOrName(ctx, idStr)
}
func GetUsersWithoutRemote(ctx context.Context, idStr []string) ([]cache.SUser, error) {
q := cache.UserCacheManager.Query()
q = q.Filter(sqlchemy.OR(sqlchemy.In(q.Field("id"), idStr), sqlchemy.In(q.Field("name"), idStr))).Desc("updated_at")
return cache.UserCacheManager.FetchUserFromLoaclCache(ctx, q)
}
func DeleteUsers(ctx context.Context, userCred mcclient.TokenCredential, ids []string) error {
users, err := GetUsersWithoutRemote(ctx, ids)
if err != nil {
return err
}
for i := range users {
err := users[i].Delete(ctx, userCred)
if err != nil {
return errors.Wrapf(err, "delete cache.SUser %s error", users[i].Id)
}
}
return nil
}
func GetUserIdsLikeName(ctx context.Context, name string) ([]string, error) {
users, err := cache.UserCacheManager.FetchUserLikeName(ctx, name, true)
if err != nil {
return nil, err
}
ret := make([]string, len(users))
for i := range users {
ret[i] = users[i].Id
}
return ret, nil
}
func GetUsersByGroupID(ctx context.Context, gid string) ([]string, error) {
ret, err := cache.UserGroupCacheManager.FetchByGroupId(ctx, gid)
if err != nil {
return nil, err
}
ids := make([]string, len(ret))
for i := range ret {
ids[i] = ret[i].UserId
}
return ids, nil
}
func GetUsernameByID(ctx context.Context, id string) (string, error) {
user, err := GetUserByIDOrName(ctx, id)
if err != nil {
return "", err
}
return user.Name, nil
}

View File

@@ -1,68 +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 utils
import (
"bytes"
"context"
"fmt"
"math/rand"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
func FetchOwnerId(ctx context.Context, manager db.IModelManager, userCred mcclient.TokenCredential, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
var ownerId mcclient.IIdentityProvider
var err error
if manager.ResourceScope() != rbacutils.ScopeSystem {
ownerId, err = manager.FetchOwnerId(ctx, data)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
}
if ownerId == nil {
ownerId = userCred
}
return ownerId, nil
}
func GenerateMobileToken() string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
token := fmt.Sprintf("%06v", rnd.Int31n(1000000))
return token
}
func GenerateEmailToken(tokenLen int) string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
token := new(bytes.Buffer)
for token.Len() < tokenLen {
token.WriteString(fmt.Sprintf("%x", rnd.Int31()))
}
return token.String()[:tokenLen]
}
func JsonArrayToStringArray(src []jsonutils.JSONObject) []string {
des := make([]string, len(src))
for i := range src {
des[i], _ = src[i].GetString()
}
return des
}

View File

@@ -1,33 +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 utils
import (
"testing"
)
func TestGenEmailToken(t *testing.T) {
s := GenerateEmailToken(32)
if len(s) != 32 {
t.Error("email token length should be 32")
}
}
func TestGenMobileToke(t *testing.T) {
s := GenerateMobileToken()
if len(s) != 6 {
t.Errorf("mobile token length should be 6")
}
}

View File

@@ -205,4 +205,8 @@ const (
ACT_UPDATE_BILLING_OPTIONS = "更新账单文件"
ACT_UPDATE_CREDENTIAL = "更新账号密码"
ACT_PULL_SUBCONTACT = "拉取联系方式"
ACT_SEND_NOTIFICATION = "发送通知消息"
ACT_SEND_VERIFICATION = "发送验证消息"
)

View File

@@ -56,6 +56,8 @@ def run_swagger_yaml(svc, swagger_pkg_dir, output_dir):
cmd.extend(["-o", pjoin(output_dir, "swagger_%s.yaml" % svc)])
run_cmd(cmd)
def remove_prefix(text, prefix):
return text[text.startswith(prefix) and len(prefix):]
class FuncDispatcher(object):
@@ -71,7 +73,7 @@ class FuncDispatcher(object):
func = getattr(self, attr)
if not callable(func):
continue
svc = attr.lstrip('gen_')
svc = remove_prefix(attr, 'gen_')
gen_dict[svc] = func
self.gen_dict = gen_dict
@@ -131,6 +133,7 @@ class ModelAPI(FuncDispatcher):
self.run_model("compute")
self.run_model("image")
self.run_model("cloudid")
self.run_model("notify")
def gen_monitor(self):
self.run(pkg=["monitor", "models"], out=["monitor"])
@@ -174,6 +177,8 @@ class SwaggerCode(FuncDispatcher):
def gen_monitor(self):
self.run("monitor", pkg=["models"], out="monitor")
def gen_notify(self):
self.run("notify", pkg=["models"], out="notify")
class SwaggerYAML(FuncDispatcher):
@@ -204,6 +209,8 @@ class SwaggerYAML(FuncDispatcher):
def gen_monitor(self):
self.run("monitor")
def gen_notify(self):
self.run("notify")
class SwaggerServe(object):