diff --git a/build/notify/root/opt/yunion/share/template/content/IMAGE_ACTIVED b/build/notify/root/opt/yunion/share/template/content@cn/IMAGE_ACTIVED similarity index 100% rename from build/notify/root/opt/yunion/share/template/content/IMAGE_ACTIVED rename to build/notify/root/opt/yunion/share/template/content@cn/IMAGE_ACTIVED diff --git a/build/notify/root/opt/yunion/share/template/content/VERIFY.email b/build/notify/root/opt/yunion/share/template/content@cn/VERIFY.email similarity index 96% rename from build/notify/root/opt/yunion/share/template/content/VERIFY.email rename to build/notify/root/opt/yunion/share/template/content@cn/VERIFY.email index 042ae56740..69ca135634 100644 --- a/build/notify/root/opt/yunion/share/template/content/VERIFY.email +++ b/build/notify/root/opt/yunion/share/template/content@cn/VERIFY.email @@ -47,7 +47,7 @@ - 您正在验证邮箱,请在验证码输入框中输入:{{.code}},已完成验证。 + 您正在验证邮箱,请在验证码输入框中输入:{{.code}},以完成验证。 diff --git a/build/notify/root/opt/yunion/share/template/content@en/IMAGE_ACTIVED b/build/notify/root/opt/yunion/share/template/content@en/IMAGE_ACTIVED new file mode 100644 index 0000000000..e6b7f366a5 --- /dev/null +++ b/build/notify/root/opt/yunion/share/template/content@en/IMAGE_ACTIVED @@ -0,0 +1 @@ +{{.os_type}} image {{.name}} upload completed diff --git a/build/notify/root/opt/yunion/share/template/content@en/VERIFY.email b/build/notify/root/opt/yunion/share/template/content@en/VERIFY.email new file mode 100644 index 0000000000..5bf1019e19 --- /dev/null +++ b/build/notify/root/opt/yunion/share/template/content@en/VERIFY.email @@ -0,0 +1,66 @@ + + + + + Document + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + + + + + +
+ Product + + Solution + + Service + + Help and Documentation + + About +
+
+ + + + + + + + + + +
+ Dear {{.name}}: +
You are verifying your email, please enter the following code on the email verification page:
{{.code}}
+
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.
Copyrights © {{.copyright}}. All rights reserved.
+ + diff --git a/build/notify/root/opt/yunion/share/template/title/IMAGE_ACTIVED b/build/notify/root/opt/yunion/share/template/title@cn/IMAGE_ACTIVED similarity index 100% rename from build/notify/root/opt/yunion/share/template/title/IMAGE_ACTIVED rename to build/notify/root/opt/yunion/share/template/title@cn/IMAGE_ACTIVED diff --git a/build/notify/root/opt/yunion/share/template/title/VERIFY.email b/build/notify/root/opt/yunion/share/template/title@cn/VERIFY.email similarity index 100% rename from build/notify/root/opt/yunion/share/template/title/VERIFY.email rename to build/notify/root/opt/yunion/share/template/title@cn/VERIFY.email diff --git a/build/notify/root/opt/yunion/share/template/title@en/IMAGE_ACTIVED b/build/notify/root/opt/yunion/share/template/title@en/IMAGE_ACTIVED new file mode 100644 index 0000000000..66ea80efb0 --- /dev/null +++ b/build/notify/root/opt/yunion/share/template/title@en/IMAGE_ACTIVED @@ -0,0 +1 @@ +Image {{.name}} upload completed diff --git a/build/notify/root/opt/yunion/share/template/title@en/VERIFY.email b/build/notify/root/opt/yunion/share/template/title@en/VERIFY.email new file mode 100644 index 0000000000..ab70a85680 --- /dev/null +++ b/build/notify/root/opt/yunion/share/template/title@en/VERIFY.email @@ -0,0 +1 @@ +{{.copyright}} Verify diff --git a/pkg/apis/notify/const.go b/pkg/apis/notify/const.go index 63238685b9..dd34a96246 100644 --- a/pkg/apis/notify/const.go +++ b/pkg/apis/notify/const.go @@ -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" ) diff --git a/pkg/notify/interface.go b/pkg/notify/interface.go index 49c382e28b..971342edbd 100644 --- a/pkg/notify/interface.go +++ b/pkg/notify/interface.go @@ -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 diff --git a/pkg/notify/models/notification.go b/pkg/notify/models/notification.go index b5ab8b68b4..9dafa0e853 100644 --- a/pkg/notify/models/notification.go +++ b/pkg/notify/models/notification.go @@ -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 } diff --git a/pkg/notify/models/receiver.go b/pkg/notify/models/receiver.go index fd1d829298..53312f6537 100644 --- a/pkg/notify/models/receiver.go +++ b/pkg/notify/models/receiver.go @@ -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 diff --git a/pkg/notify/models/template.go b/pkg/notify/models/template.go index 0d76581cc7..1f70b18cc2 100644 --- a/pkg/notify/models/template.go +++ b/pkg/notify/models/template.go @@ -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 diff --git a/pkg/notify/rpc/send.go b/pkg/notify/rpc/send.go index 761ffd23cb..4b0710f5b8 100644 --- a/pkg/notify/rpc/send.go +++ b/pkg/notify/rpc/send.go @@ -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 { diff --git a/pkg/notify/tasks/notifications_send_task.go b/pkg/notify/tasks/notifications_send_task.go index 80ee236e14..8b037c6b73 100644 --- a/pkg/notify/tasks/notifications_send_task.go +++ b/pkg/notify/tasks/notifications_send_task.go @@ -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 } diff --git a/pkg/notify/tasks/verification_send_task.go b/pkg/notify/tasks/verification_send_task.go index 5b7e52ffce..b66910ff34 100644 --- a/pkg/notify/tasks/verification_send_task.go +++ b/pkg/notify/tasks/verification_send_task.go @@ -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