feat(notify): send notifications of different templates according to language

This commit is contained in:
rainzm
2021-01-11 15:51:39 +08:00
parent 07114b53d9
commit 76e7aa4018
16 changed files with 291 additions and 60 deletions
@@ -47,7 +47,7 @@
</td>
</tr>
<tr>
<td style="padding-bottom: 10px;">您正在验证邮箱,请在验证码输入框中输入:{{.code}},完成验证。</td>
<td style="padding-bottom: 10px;">您正在验证邮箱,请在验证码输入框中输入:{{.code}},完成验证。</td>
</tr>
</table>
</td>
@@ -0,0 +1 @@
{{.os_type}} image {{.name}} upload completed
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<table style="width: 650px; margin-bottom: 20px;" border="0" cellpadding="0" cellspacing="0" align="center">
<tr style="height: 50px; background: #333; overflow: hidden;">
<td>
<table style="margin-left: 20px;">
<tr>
<td>
<img src="data:{{.login_logo_format}};base64,{{.login_logo}}" alt="" style="color: #fff; height: 32px; vertical-align: middle;">
</td>
</tr>
</table>
</td>
<td>
<table style="float: right;">
<tr>
<td style="padding-left: 20px; padding-right: 20px;">
<a href="" style="text-decoration: none; color: #20A0FF; font-size: 12px; display: none;">Product</a>
</td>
<td style="padding-left: 20px; padding-right: 20px;">
<a href="" style="text-decoration: none; color: #20A0FF; font-size: 12px; display: none;">Solution</a>
</td>
<td style="padding-left: 20px; padding-right: 20px;">
<a href="" style="text-decoration: none; color: #20A0FF; font-size: 12px; display: none;">Service</a>
</td>
<td style="padding-left: 20px; padding-right: 20px;">
<a href="" style="text-decoration: none; color: #20A0FF; font-size: 12px; display: none;">Help and Documentation</a>
</td>
<td style="padding-left: 20px; padding-right: 20px;">
<a href="" style="text-decoration: none; color: #20A0FF; font-size: 12px; display: none;">About</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="width: 100%;" colspan="2">
<table style="padding: 20px 10px; width: 100%;">
<tr>
<td style="padding-bottom: 13px;">
Dear {{.name}}
</td>
</tr>
<tr>
<td style="padding-bottom: 13px;">You are verifying your email, please enter the following code on the email verification page:</td>
</tr>
<tr>
<td style="padding-bottom: 10px; font-size: large">{{.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;">If you are not operating by yourself, please log in to the platform in time and change your password to ensure the security of your account.</td>
</tr>
<tr>
<td colspan="2" style="background: #333; text-align: right; padding-right: 20px; font-size: 12px; color: #fff; height: 50px;">Copyrights © {{.copyright}}. All rights reserved.</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1 @@
Image {{.name}} upload completed
@@ -0,0 +1 @@
{{.copyright}} Verify
+3
View File
@@ -62,6 +62,9 @@ const (
TEMPLATE_TYPE_CONTENT = "content"
TEMPLATE_TYPE_REMOTE = "remote"
TEMPLATE_LANG_EN = "en"
TEMPLATE_LANG_CN = "cn"
CTYPE_ROBOT_YES = "yes"
CTYPE_ROBOT_ONLY = "only"
)
+21 -3
View File
@@ -28,19 +28,37 @@ type INotifyService interface {
StopAll()
UpdateServices(ctx context.Context, userCred mcclient.TokenCredential, isStart bool)
RestartService(ctx context.Context, config SConfig, serviceName string)
Send(ctx context.Context, contactType, contact, topic, msg, priority string) error
Send(ctx context.Context, p SSendParams) error
ContactByMobile(ctx context.Context, mobile, serviceName string) (string, error)
BatchSend(ctx context.Context, contacts []string, contactType, topic, message, priority string) ([]*apis.FailedRecord, error)
BatchSend(ctx context.Context, p SBatchSendParams) ([]*apis.FailedRecord, error)
ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error)
}
type SSendParams struct {
ContactType string
Contact string
Topic string
Message string
Priority string
Lang string
}
type SBatchSendParams struct {
ContactType string
Contacts []string
Topic string
Message string
Priority string
Lang string
}
type IServiceConfigStore interface {
GetConfig(serviceName string) (SConfig, error)
SetConfig(serviceName string, config SConfig) error
}
type ITemplateStore interface {
NotifyFilter(contactType, topic, msg string) (params apis.SendParams, err error)
NotifyFilter(contactType, topic, msg, lang string) (params apis.SendParams, err error)
}
type SConfig map[string]string
+1 -1
View File
@@ -231,7 +231,7 @@ func (n *SNotification) ReceiveDetails() ([]api.ReceiveDetail, error) {
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)
p, err := TemplateManager.NotifyFilter(n.ContactType, n.Topic, n.Message, getTemplateLangFromCtx(ctx))
if err != nil {
return out, err
}
+48
View File
@@ -20,6 +20,8 @@ import (
"regexp"
"time"
"golang.org/x/text/language"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
@@ -97,6 +99,7 @@ type SReceiver struct {
Email string `width:"64" nullable:"false" create:"optional" update:"user" get:"user" list:"user"`
Mobile string `width:"16" nullable:"false" create:"optional" update:"user" get:"user" list:"user"`
Lang string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user"`
// swagger:ignore
EnabledEmail tristate.TriState `nullable:"false" default:"false" update:"user"`
@@ -910,6 +913,51 @@ func (r *SReceiver) PerformDisable(ctx context.Context, userCred mcclient.TokenC
return nil, nil
}
func (r *SReceiver) Sync(ctx context.Context) error {
session := auth.GetAdminSessionWithInternal(ctx, "", "")
params := jsonutils.NewDict()
params.Set("scope", jsonutils.NewString("system"))
params.Set("system", jsonutils.JSONTrue)
data, err := modules.UsersV3.GetById(session, r.Id, params)
if err != nil {
jerr := err.(*httputils.JSONClientError)
if jerr.Code == 404 {
err := r.Delete(ctx, session.GetToken())
if err != nil {
return errors.Wrapf(err, "unable to delete receiver %s", r.Id)
}
return errors.Wrapf(errors.ErrNotFound, "no such receiver %s", r.Id)
}
return err
}
uname, _ := data.GetString("name")
domainId, _ := data.GetString("domain_id")
lang, _ := data.GetString("lang")
_, err = db.Update(r, func() error {
r.Name = uname
r.DomainId = domainId
r.Lang = lang
return nil
})
return errors.Wrap(err, "unable to update")
}
func (r *SReceiver) GetTemplateLang(ctx context.Context) (string, error) {
if len(r.Lang) == 0 {
err := r.Sync(ctx)
if err != nil {
return "", err
}
}
log.Infof("lang: %s", r.Lang)
lang, err := language.Parse(r.Lang)
if err != nil {
return "", errors.Wrapf(err, "unable to prase language %q", r.Lang)
}
tLang := notifyclientI18nTable.LookupByLang(lang, tempalteLang)
return tLang, nil
}
// Implemente interface EventHandler
func (rm *SReceiverManager) OnAdd(obj *jsonutils.JSONDict) {
// do nothing
+65 -27
View File
@@ -33,6 +33,7 @@ import (
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/i18n"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
@@ -72,6 +73,7 @@ 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"`
Lang string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user"`
Example string `nullable:"false" created:"required" get:"user" list:"user" update:"user"`
}
@@ -88,32 +90,35 @@ func (tm *STemplateManager) defaultTemplate() ([]STemplate, error) {
templates := make([]STemplate, 0, 4)
for _, templateType := range []string{"title", "content"} {
contactType, topic := CONTACTTYPE_ALL, ""
titleTemplatePath := fmt.Sprintf("%s/%s", templatePath, templateType)
files, err := ioutil.ReadDir(titleTemplatePath)
if err != nil {
return templates, errors.Wrapf(err, "Read Dir '%s'", titleTemplatePath)
}
for _, file := range files {
if file.IsDir() {
continue
}
spliteName := strings.Split(file.Name(), ".")
topic = spliteName[0]
if len(spliteName) > 1 {
contactType = spliteName[1]
}
fullPath := filepath.Join(titleTemplatePath, file.Name())
content, err := ioutil.ReadFile(fullPath)
for _, lang := range []string{api.TEMPLATE_LANG_CN, api.TEMPLATE_LANG_EN} {
contactType, topic := CONTACTTYPE_ALL, ""
titleTemplatePath := fmt.Sprintf("%s/%s@%s", templatePath, templateType, lang)
files, err := ioutil.ReadDir(titleTemplatePath)
if err != nil {
return templates, err
return templates, errors.Wrapf(err, "Read Dir '%s'", titleTemplatePath)
}
for _, file := range files {
if file.IsDir() {
continue
}
spliteName := strings.Split(file.Name(), ".")
topic = spliteName[0]
if len(spliteName) > 1 {
contactType = spliteName[1]
}
fullPath := filepath.Join(titleTemplatePath, file.Name())
content, err := ioutil.ReadFile(fullPath)
if err != nil {
return templates, err
}
templates = append(templates, STemplate{
ContactType: contactType,
Topic: topic,
Lang: lang,
TemplateType: templateType,
Content: string(content),
})
}
templates = append(templates, STemplate{
ContactType: contactType,
Topic: topic,
TemplateType: templateType,
Content: string(content),
})
}
}
return templates, nil
@@ -144,15 +149,43 @@ var (
ForceInitType = []string{
api.EMAIL,
}
notifyclientI18nTable = i18n.Table{}
defaultLang = api.TEMPLATE_LANG_CN
tempalteLang = "lang"
)
func init() {
notifyclientI18nTable.Set(tempalteLang, i18n.NewTableEntry().EN(api.TEMPLATE_LANG_EN).CN(api.TEMPLATE_LANG_CN))
}
func getTemplateLangFromCtx(ctx context.Context) string {
return notifyclientI18nTable.Lookup(ctx, tempalteLang)
}
func (tm *STemplateManager) InitializeData() error {
// init lang
q := tm.Query().IsEmpty("lang")
var noLangTemplates []STemplate
err := db.FetchModelObjects(tm, q, &noLangTemplates)
if err != nil {
return errors.Wrap(err, "unable to fetch templates")
}
for i := range noLangTemplates {
t := &noLangTemplates[i]
_, err := db.Update(t, func() error {
t.Lang = defaultLang
return nil
})
if err != nil {
return err
}
}
templates, err := tm.defaultTemplate()
if err != nil {
return err
}
for _, template := range templates {
q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType)
q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType).Equals("lang", template.Lang)
count, _ := q.CountWithError()
if count > 0 && !utils.IsInStringArray(template.ContactType, ForceInitType) {
continue
@@ -195,10 +228,15 @@ func (tm *STemplateManager) InitializeData() error {
// NotifyFilter will return the title and content generated by corresponding template.
// Local cache about common template will be considered in case of performance issues.
func (tm *STemplateManager) NotifyFilter(contactType, topic, msg string) (params apis.SendParams, err error) {
func (tm *STemplateManager) NotifyFilter(contactType, topic, msg, lang string) (params apis.SendParams, err error) {
params.Topic = topic
if len(lang) == 0 {
params.Title = topic
params.Message = msg
return
}
templates := make([]STemplate, 0, 3)
q := tm.Query().Equals("topic", strings.ToUpper(topic)).In("contact_type", []string{CONTACTTYPE_ALL, contactType})
q := tm.Query().Equals("topic", strings.ToUpper(topic)).Equals("lang", lang).In("contact_type", []string{CONTACTTYPE_ALL, contactType})
err = db.FetchModelObjects(tm, q, &templates)
if errors.Cause(err) == sql.ErrNoRows || len(templates) == 0 {
// no such template, return as is
+10 -9
View File
@@ -32,6 +32,7 @@ import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
@@ -107,22 +108,22 @@ func (self *SRpcService) StopAll() {
}
// Send call the corresponding rpc server to send messager.
func (self *SRpcService) Send(ctx context.Context, contactType, contact, topic, msg, priority string) error {
func (self *SRpcService) Send(ctx context.Context, p notify.SSendParams) error {
args, err := self.templateStore.NotifyFilter(contactType, topic, msg)
args, err := self.templateStore.NotifyFilter(p.ContactType, p.Topic, p.Message, p.Lang)
if err != nil {
return errors.Wrap(err, "templateStore.NotifyFilter")
}
args.Contact = contact
args.Priority = priority
args.Contact = p.Contact
args.Priority = p.Priority
f := func(service *apis.SendNotificationClient) (interface{}, error) {
log.Debugf("send one")
return service.Send(ctx, &args)
}
_, err = self.execute(ctx, f, contactType)
_, err = self.execute(ctx, f, p.ContactType)
if err != nil {
s, ok := status.FromError(err)
if !ok {
@@ -133,14 +134,14 @@ 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)
func (self *SRpcService) BatchSend(ctx context.Context, p notify.SBatchSendParams) ([]*apis.FailedRecord, error) {
args, err := self.templateStore.NotifyFilter(p.ContactType, p.Topic, p.Message, p.Lang)
if err != nil {
return nil, errors.Wrap(err, "templateStore.NotifyFilter")
}
batchSendParams := apis.BatchSendParams{
Contacts: contacts,
Contacts: p.Contacts,
Title: args.Title,
Message: args.Message,
Priority: args.Priority,
@@ -151,7 +152,7 @@ func (self *SRpcService) BatchSend(ctx context.Context, contacts []string, conta
return service.BatchSend(ctx, &batchSendParams)
}
ret, err := self.execute(ctx, f, contactType)
ret, err := self.execute(ctx, f, p.ContactType)
if err != nil {
s, ok := status.FromError(err)
if !ok {
+59 -18
View File
@@ -12,7 +12,9 @@ import (
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"
"yunion.io/x/onecloud/pkg/notify/models"
rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis"
"yunion.io/x/onecloud/pkg/util/logclient"
)
@@ -71,6 +73,8 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
// build contactMap
contactMap := make(map[string]*models.SReceiverNotification)
contactMapEn := make(map[string]*models.SReceiverNotification)
contactmapCn := make(map[string]*models.SReceiverNotification)
for i := range rnsWithReceiver {
if len(rnsWithReceiver[i].ReceiverID) == 0 {
contactMap[rnsWithReceiver[i].Contact] = rnsWithReceiver[i]
@@ -114,32 +118,69 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
sendFail(rnsWithReceiver[i], reason)
continue
}
contactMap[contact] = rnsWithReceiver[i]
lang, err := receiver.GetTemplateLang(ctx)
if err != nil {
reason := fmt.Sprintf("fail to GetTemplateLang: %s", err.Error())
sendFail(rnsWithReceiver[i], reason)
continue
}
switch lang {
case "":
contactMap[contact] = rnsWithReceiver[i]
case apis.TEMPLATE_LANG_EN:
contactMapEn[contact] = rnsWithReceiver[i]
case apis.TEMPLATE_LANG_CN:
contactmapCn[contact] = rnsWithReceiver[i]
}
}
for i := range rnsWithoutReceiver {
contactMap[rnsWithoutReceiver[i].Contact] = rnsWithoutReceiver[i]
}
// 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)
}
var (
ret []*rpcapi.FailedRecord
contactLen int
)
// 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())
for lang, contactMap := range map[string]map[string]*models.SReceiverNotification{
"": contactMap,
apis.TEMPLATE_LANG_CN: contactmapCn,
apis.TEMPLATE_LANG_EN: contactMapEn,
} {
if len(contactMap) == 0 {
return
}
// 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)
}
failedRecord = append(failedRecord, fmt.Sprintf("others: %s", err.Error()))
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), true)
return
}
contactLen += len(contacts)
p := notify.SBatchSendParams{
Contacts: contacts,
ContactType: notification.ContactType,
Topic: notification.Topic,
Message: notification.Message,
Priority: notification.Priority,
Lang: lang,
}
// send
fds, err := models.NotifyService.BatchSend(ctx, p)
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
}
ret = append(ret, fds...)
}
// check result
for _, fd := range ret {
rn := contactMap[fd.Contact]
@@ -151,7 +192,7 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
for _, rn := range contactMap {
rn.AfterSend(ctx, true, "")
}
if len(failedRecord) > 0 && len(failedRecord) == len(contacts) {
if len(failedRecord) > 0 && len(failedRecord) == contactLen {
self.taskFailed(ctx, notification, strings.Join(failedRecord, "; "), true)
return
}
+14 -1
View File
@@ -10,6 +10,7 @@ import (
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/util/logclient"
)
@@ -66,7 +67,19 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
default:
// no way
}
err = models.NotifyService.Send(ctx, contactType, contact, "verify", message, "")
tLang, err := receiver.GetTemplateLang(ctx)
if err != nil {
self.taskFailed(ctx, receiver, fmt.Sprintf("unable to GetTemplateLang for receiver %q: %v", receiver.Id, err))
}
sendP := notify.SSendParams{
ContactType: contactType,
Contact: contact,
Message: message,
Topic: "verify",
Priority: "",
Lang: tLang,
}
err = models.NotifyService.Send(ctx, sendP)
if err != nil {
self.taskFailed(ctx, receiver, err.Error())
return