From 2222c7779cf5f989b0c2b415a7d3e277cefb433d Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 16 Oct 2019 21:28:13 +0800 Subject: [PATCH] feat(notify): Available uname in api & Unified template management 1. Uname is availbale now in api via adding "uname=true" in query. So we can use uname in climc which is much more visual and use uid in web client which avoid time-consuming remote calls that may occur. 2. Previously, the template for the message was managed by the individual sending components. Now, these templats was managed uniformly by Notify Service so that we can easily add and remove. 3. Now, failure to send a validation message will trigger a notification to the user via websocket. --- build/docker/Dockerfile.notify | 1 + .../yunion/share/notify/email_verify_template | 66 ++++++ cmd/climc/shell/contacts.go | 15 +- cmd/climc/shell/notification.go | 4 +- cmd/climc/shell/notify_template.go | 90 ++++++++ pkg/cloudcommon/db/usercache.go | 4 +- pkg/mcclient/modules/mod_contacts.go | 16 ++ pkg/mcclient/modules/mod_notify_template.go | 43 ++++ .../modules/notify/mod_notification.go | 3 +- pkg/notify/cache/usercache.go | 130 ++---------- pkg/notify/dispatcher.go | 103 ++++++++- pkg/notify/handlers.go | 159 +++++++++++--- pkg/notify/interface/interface.go | 5 + pkg/notify/models/initdb.go | 1 + pkg/notify/models/mod_contact.go | 56 ++++- pkg/notify/models/mod_notification.go | 5 +- pkg/notify/models/mod_template.go | 198 ++++++++++++++++++ pkg/notify/models/worker.go | 12 +- pkg/notify/options/options.go | 13 +- pkg/notify/rpc/apis/send_server.pb.go | 70 ++++--- pkg/notify/rpc/apis/send_server.proto | 24 ++- pkg/notify/rpc/send.go | 27 ++- pkg/notify/service.go | 8 +- pkg/notify/template/doc.go | 1 + pkg/notify/template/template.go | 20 ++ pkg/notify/utils/keystone.go | 21 +- 26 files changed, 867 insertions(+), 228 deletions(-) create mode 100644 build/notify/root/opt/yunion/share/notify/email_verify_template create mode 100644 cmd/climc/shell/notify_template.go create mode 100644 pkg/mcclient/modules/mod_notify_template.go create mode 100644 pkg/notify/models/mod_template.go create mode 100644 pkg/notify/template/doc.go create mode 100644 pkg/notify/template/template.go diff --git a/build/docker/Dockerfile.notify b/build/docker/Dockerfile.notify index b48e39e984..095e2ce31f 100644 --- a/build/docker/Dockerfile.notify +++ b/build/docker/Dockerfile.notify @@ -12,4 +12,5 @@ RUN apk update && \ RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +COPY ./build/notify/root/opt/ /opt/ ADD ./_output/bin/notify /opt/yunion/bin/notify diff --git a/build/notify/root/opt/yunion/share/notify/email_verify_template b/build/notify/root/opt/yunion/share/notify/email_verify_template new file mode 100644 index 0000000000..0f4909da9e --- /dev/null +++ b/build/notify/root/opt/yunion/share/notify/email_verify_template @@ -0,0 +1,66 @@ + + + + + Document + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + + + + + +
+ 产品 + + 解决方案 + + 服务 + + 帮助与文档 + + 关于我们 +
+
+ + + + + + + + + + +
+ 亲爱的{{.name}},您好: +
请点击下面的链接,完成用户邮箱的激活:
{{.link}}
+
*为了确保您的帐号安全,该链接仅48小时内访问有效,请勿直接回复此邮件。
版权所有 © 北京云联万维科技有限公司 2018 保留一切权利 京ICP备17065753号
+ + diff --git a/cmd/climc/shell/contacts.go b/cmd/climc/shell/contacts.go index 8f37c44645..57c8817b78 100644 --- a/cmd/climc/shell/contacts.go +++ b/cmd/climc/shell/contacts.go @@ -167,17 +167,10 @@ func init() { } R(&ContactsVerifyOptions{}, "contact-verify-trigger", "Trigger contact verify", func(s *mcclient.ClientSession, args *ContactsVerifyOptions) error { params := jsonutils.NewDict() - params.Add(jsonutils.NewString(args.CONTACT_TYPE), "contact_type") - params.Add(jsonutils.NewString(args.CONTACT), "contact") - /* - if len(args.Email) > 0 { - params.Add(jsonutils.NewString(args.Email), "email") - } - if len(args.Mobile) > 0 { - params.Add(jsonutils.NewString(args.Mobile), "mobile") - } - */ - + tmpDict := jsonutils.NewDict() + tmpDict.Add(jsonutils.NewString(args.CONTACT_TYPE), "contact_type") + tmpDict.Add(jsonutils.NewString(args.CONTACT), "contact") + params.Add(tmpDict, "contact") _, err := modules.Contacts.PerformAction(s, args.UID, "verify", params) if err != nil { return err diff --git a/cmd/climc/shell/notification.go b/cmd/climc/shell/notification.go index a3d0320809..adffdb4d94 100644 --- a/cmd/climc/shell/notification.go +++ b/cmd/climc/shell/notification.go @@ -37,7 +37,9 @@ func init() { Remark string `help:"Remark or description of the notification"` Group bool `help:"Send to group"` } - R(&NotificationCreateOptions{}, "notify", "Send a notification to sb", func(s *mcclient.ClientSession, args *NotificationCreateOptions) error { + R(&NotificationCreateOptions{}, "notify", "Send a notification to someones", func(s *mcclient.ClientSession, + args *NotificationCreateOptions) error { + msg := notify.SNotifyMessage{} if args.Group { msg.Gid = args.Uid diff --git a/cmd/climc/shell/notify_template.go b/cmd/climc/shell/notify_template.go new file mode 100644 index 0000000000..b635e8eac6 --- /dev/null +++ b/cmd/climc/shell/notify_template.go @@ -0,0 +1,90 @@ +// 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 shell + +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 + }) +} diff --git a/pkg/cloudcommon/db/usercache.go b/pkg/cloudcommon/db/usercache.go index 6a89f46b91..2a76b4587a 100644 --- a/pkg/cloudcommon/db/usercache.go +++ b/pkg/cloudcommon/db/usercache.go @@ -103,10 +103,10 @@ func (manager *SUserCacheManager) fetchUser( return user, nil } } - return manager.fetchUserFromKeystone(ctx, idStr) + return manager.FetchUserFromKeystone(ctx, idStr) } -func (manager *SUserCacheManager) fetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { +func (manager *SUserCacheManager) FetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { if len(idStr) == 0 { log.Debugf("fetch empty user!!!!\n%s", debug.Stack()) return nil, fmt.Errorf("Empty idStr") diff --git a/pkg/mcclient/modules/mod_contacts.go b/pkg/mcclient/modules/mod_contacts.go index 6f8480d440..609f8dd94a 100644 --- a/pkg/mcclient/modules/mod_contacts.go +++ b/pkg/mcclient/modules/mod_contacts.go @@ -16,6 +16,7 @@ package modules import ( "fmt" + "net/url" "yunion.io/x/jsonutils" @@ -44,6 +45,21 @@ func (this *ContactsManager) DoBatchDeleteContacts(s *mcclient.ClientSession, pa return modulebase.Post(this.ResourceManager, s, path, params, this.Keyword) } +func (this *ContactsManager) PerformAction(session *mcclient.ClientSession, id string, action string, + params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + + path := fmt.Sprintf("/%s/%s/%s?uname=true", this.ContextPath(nil), url.PathEscape(id), url.PathEscape(action)) + return modulebase.Post(this.ResourceManager, session, path, params, this.KeywordPlural) +} + +func (this *ContactsManager) Get(session *mcclient.ClientSession, id string, params jsonutils.JSONObject) (jsonutils.JSONObject, + error) { + + q := params.(*jsonutils.JSONDict) + q.Add(jsonutils.JSONTrue, "uname") + return this.ResourceManager.Get(session, id, params) +} + var ( Contacts ContactsManager ) diff --git a/pkg/mcclient/modules/mod_notify_template.go b/pkg/mcclient/modules/mod_notify_template.go new file mode 100644 index 0000000000..7017473305 --- /dev/null +++ b/pkg/mcclient/modules/mod_notify_template.go @@ -0,0 +1,43 @@ +// 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 := "/templates/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) +} diff --git a/pkg/mcclient/modules/notify/mod_notification.go b/pkg/mcclient/modules/notify/mod_notification.go index 330ba34b3c..21ffb8f5f5 100644 --- a/pkg/mcclient/modules/notify/mod_notification.go +++ b/pkg/mcclient/modules/notify/mod_notification.go @@ -42,7 +42,8 @@ type NotificationManager struct { } func (manager *NotificationManager) Send(s *mcclient.ClientSession, msg SNotifyMessage) error { - _, err := manager.Create(s, jsonutils.Marshal(&msg)) + path := "/" + manager.KeywordPlural + _, err := modulebase.Post(manager.ResourceManager, s, path, jsonutils.Marshal(&msg), manager.KeywordPlural) return err } diff --git a/pkg/notify/cache/usercache.go b/pkg/notify/cache/usercache.go index 90303ca9aa..2ee25e96e4 100644 --- a/pkg/notify/cache/usercache.go +++ b/pkg/notify/cache/usercache.go @@ -18,26 +18,21 @@ import ( "context" "database/sql" "fmt" - "time" - "yunion.io/x/log" "yunion.io/x/pkg/errors" - "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" "yunion.io/x/onecloud/pkg/mcclient/auth" - "yunion.io/x/onecloud/pkg/mcclient/modules" "yunion.io/x/onecloud/pkg/util/httputils" ) type SUserCacheManager struct { - db.SKeystoneCacheObjectManager + db.SUserCacheManager } type SUser struct { - db.SKeystoneCacheObject + db.SUser } func (user *SUser) GetModelManager() db.IModelManager { @@ -46,13 +41,6 @@ func (user *SUser) GetModelManager() db.IModelManager { var UserCacheManager *SUserCacheManager -func init() { - UserCacheManager = &SUserCacheManager{ - db.NewKeystoneCacheObjectManager(SUser{}, "users_cache_tbl", "user", "users")} - // log.Debugf("initialize user cache manager %s", UserCacheManager.KeywordPlural()) - UserCacheManager.SetVirtualObject(UserCacheManager) -} - func RegistUserCredCacheUpdater() { auth.RegisterAuthHook(onAuthCompleteUpdateCache) } @@ -63,97 +51,18 @@ func onAuthCompleteUpdateCache(userCred mcclient.TokenCredential) { func (ucm *SUserCacheManager) updateUserCache(userCred mcclient.TokenCredential) { ucm.Save(context.Background(), userCred.GetUserId(), userCred.GetUserName(), - userCred.GetDomainId()) + userCred.GetDomainId(), userCred.GetDomainName()) } -func (ucm *SUserCacheManager) FetchUserByIdOrName(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchByIdOrName(nil, idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) FetchUserById(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchById(idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) FetchUserByName(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchByName(nil, idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string) (*SUser, error) { - lockman.LockRawObject(ctx, ucm.KeywordPlural(), idStr) - defer lockman.ReleaseRawObject(ctx, ucm.KeywordPlural(), idStr) - - objo, err := ucm.FetchById(idStr) - if err != nil && err != sql.ErrNoRows { - log.Errorf("FetchTenantbyId fail %s", err) - return nil, err - } - now := time.Now().UTC() - if err == nil { - obj := objo.(*SUser) - if obj.Id == idStr && obj.Name == name && obj.DomainId == domainId { - db.Update(obj, func() error { - obj.LastCheck = now - return nil - }) - return obj, nil - } - _, err = db.Update(obj, func() error { - obj.Id = idStr - obj.Name = name - obj.DomainId = domainId - obj.LastCheck = now - return nil - }) - if err != nil { - return nil, err - } else { - return obj, nil - } - } else { - objm, err := db.NewModelObject(ucm) - obj := objm.(*SUser) - obj.Id = idStr - obj.Name = name - obj.DomainId = domainId - obj.LastCheck = now - err = ucm.TableSpec().Insert(obj) - if err != nil { - return nil, err - } else { - return obj, nil - } - } -} - -func (ucm *SUserCacheManager) fetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { - if len(idStr) == 0 { - return nil, fmt.Errorf("Empty idStr") - } - s := auth.GetAdminSession(ctx, consts.GetRegion(), "v3") - user, err := modules.UsersV3.GetById(s, idStr, nil) +func (ucm *SUserCacheManager) dealErrFromKeystone(err error) error { if err != nil { if je, ok := err.(*httputils.JSONClientError); ok && je.Code == 404 { - return nil, sql.ErrNoRows + return sql.ErrNoRows + } else { + return errors.Wrap(err, "fetch User info from keystone") } - log.Errorf("fetch project %s fail %s", idStr, err) - return nil, errors.Wrap(err, "modules.Projects.Get") } - userId, _ := user.GetString("id") - userName, _ := user.GetString("name") - domainId, _ := user.GetString("domain_id") - return ucm.Save(ctx, userId, userName, domainId) + return nil } func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) (map[string]SUser, error) { @@ -174,32 +83,21 @@ func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) if _, ok := ret[id]; ok { continue } - user, err := ucm.fetchUserFromKeystone(ctx, id) + user, err := ucm.FetchUserFromKeystone(ctx, id) if err != nil { continue } - ret[id] = *user + ret[id] = SUser{*user} } return ret, nil } -func (ucm *SUserCacheManager) FetchUserByID(ctx context.Context, idStr string, noExpireCheck bool) (*SUser, error) { - - q := ucm.Query().Equals("id", idStr) - uobj, err := db.NewModelObject(ucm) +func (ucm *SUserCacheManager) FetchUserByIDOrName(ctx context.Context, idStr string) (*SUser, error) { + user, err := ucm.SUserCacheManager.FetchUserByIdOrName(ctx, idStr) if err != nil { - return nil, errors.Wrap(err, "NewModelObject") + return nil, err } - err = q.First(uobj) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrap(err, "query") - } else if uobj != nil { - user := uobj.(*SUser) - if noExpireCheck || !user.IsExpired() { - return user, nil - } - } - return ucm.fetchUserFromKeystone(ctx, idStr) + return &SUser{*user}, nil } func (ucm *SUserCacheManager) FetchUserLikeName(ctx context.Context, name string, noExpireCheck bool) ([]SUser, diff --git a/pkg/notify/dispatcher.go b/pkg/notify/dispatcher.go index e3e6139528..c45d9a8a8b 100644 --- a/pkg/notify/dispatcher.go +++ b/pkg/notify/dispatcher.go @@ -24,6 +24,7 @@ import ( "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" @@ -228,8 +229,8 @@ func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map contact, _ := data.GetString("contact") contactType, _ := data.GetString("contact_type") contacts, err := models.ContactManager.FetchByMore(uid, contact, contactType) - if err != nil { - return nil, errors.Error(fmt.Sprintf(`uid %q don't have contact %q of contact_type %q'`, 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] @@ -262,12 +263,16 @@ func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map } if scontact.Status == models.CONTACT_VERIFYING { verifications, err := models.VerifyManager.FetchByCID(scontact.ID, func(q *sqlchemy.SQuery) *sqlchemy.SQuery { - q = q.Equals("status", models.VERIFICATION_SENT).Desc("created_at") + 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) { @@ -292,8 +297,11 @@ func (self *NotifyModelDispatcher) DeleteContacts(ctx context.Context, uids2 []j for i := range uids2 { uids[i] = strings.Trim(uids2[i].String(), `"`) } - - contacts, err := models.ContactManager.FetchByUIDs(uids) + uname := false + if v := ctx.Value("uname"); v != nil { + uname = true + } + contacts, err := models.ContactManager.FetchByUIDs(uids, uname) if err != nil { return httperrors.NewGeneralError(err) } @@ -425,6 +433,91 @@ func (self *NotifyModelDispatcher) UpdateContacts(ctx context.Context, idstr str return 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.ContactManager, 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) diff --git a/pkg/notify/handlers.go b/pkg/notify/handlers.go index a7b58c0dc0..b0d0990029 100644 --- a/pkg/notify/handlers.go +++ b/pkg/notify/handlers.go @@ -18,12 +18,15 @@ import ( "context" "fmt" "net/http" + "net/url" "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" @@ -44,9 +47,35 @@ func InitHandlers(app *appsrv.Application) { db.RegisterModelManager(models.ConfigManager) db.RegisterModelManager(cache.UserCacheManager) db.RegisterModelManager(cache.UserGroupCacheManager) + db.RegisterModelManager(models.TemplateManager) AddNotifyDispatcher("/api/v1/", 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 + params := appctx.AppContextParams(ctx) + if uid, ok := params[""]; ok { + userDetail, err := utils.GetUserByIDOrName(ctx, uid) + if err != nil { + httperrors.NotFoundError(w, "Uid or Uname Not Found") + return + } + params[""] = 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 @@ -56,89 +85,167 @@ func AddNotifyDispatcher(prefix string, app *appsrv.Application) { metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} app.AddHandler2("POST", fmt.Sprintf("%s/%s//update-contact", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(contactUpdateHandler), metadata, "contact_update", tags) + middleware(contactUpdateHandler), metadata, "contact_update", tags) // List app.AddHandler2("GET", fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "list_contacts", tags) + middleware(listHandler), metadata, "list_contacts", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/users", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(keyStoneUserListHandler), metadata, "list_users", tags) + middleware(keyStoneUserListHandler), metadata, "list_users", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(getHandler), metadata, "list_by_uid", tags) + middleware(getHandler), metadata, "list_by_uid", tags) app.AddHandler2("POST", fmt.Sprintf("%s/%s/delete-contact", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(deleteContactHandler), metadata, "delete", tags) + middleware(deleteContactHandler), metadata, "delete", tags) // verify-trigger app.AddHandler2("POST", fmt.Sprintf("%s/%s//verify", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(verifyTriggerHandler), metadata, "verify_trigger", tags) + 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/", prefix, models.VerifyManager.KeywordPlural()), - modelDispatcher.Filter(verifyHandler), metadata, "verify", tags) + 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()), - modelDispatcher.Filter(notificationHandler), metadata, "send_notifications", tags) + middleware(notificationHandler), metadata, "send_notifications", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "send_notifications", tags) + middleware(listHandler), metadata, "send_notifications", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "list_notification_by_id", tags) + 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()), - modelDispatcher.Filter(configUpdateHandler), metadata, "update_configs", tags) + middleware(configUpdateHandler), metadata, "update_configs", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(configGetHandler), metadata, "get_configs", tags) + middleware(configGetHandler), metadata, "get_configs", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(configDeleteHandler), metadata, "delete_configs", tags) + middleware(configDeleteHandler), metadata, "delete_configs", tags) // email handler for being compatible app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + middleware(emailConfigUpdateHandler), metadata, "", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigGetHandler), metadata, "", tags) + middleware(emailConfigGetHandler), metadata, "", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigDeleteHandler), metadata, "", tags) + middleware(emailConfigDeleteHandler), metadata, "", tags) app.AddHandler2("PUT", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + middleware(emailConfigUpdateHandler), metadata, "", tags) app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + middleware(smsConfigUpdateHandler), metadata, "", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigGetHandler), metadata, "", tags) + middleware(smsConfigGetHandler), metadata, "", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigDeleteHandler), metadata, "", tags) + middleware(smsConfigDeleteHandler), metadata, "", tags) app.AddHandler2("PUT", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + middleware(smsConfigUpdateHandler), metadata, "", tags) + // Contact 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//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) + + app.AddHandler2("POST", + fmt.Sprintf("%s/%s/email-url", prefix, modelDispatcher.KeywordPlural()), + middleware(updateEmailUrlHandler), metadata, "update_email_url", tags) + + app.AddHandler2("GET", + fmt.Sprintf("%s/%s/email-url", prefix, modelDispatcher.KeywordPlural()), + middleware(getEmailUrlHandler), metadata, "get_email_url", tags) +} + +func updateEmailUrlHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + userCred := policy.FetchUserCredential(ctx) + if !userCred.HasSystemAdminPrivilege() { + httperrors.ForbiddenError(w, "only system admin can update email url") + return + } + _, _, _, body := fetchEnv(ctx, w, r) + if !body.Contains("email_url") { + httperrors.InputParameterError(w, "miss email_url") + } + emailUrl, _ := body.GetString("email_url") + eUrl, err := url.Parse(emailUrl) + if err != nil { + httperrors.InputParameterError(w, "invalid url") + } + models.TemplateManager.SetEmailUrl(eUrl.String()) +} + +func getEmailUrlHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewString(models.TemplateManager.GetEmailUrl()), "email_url") + appsrv.Send(w, ret.PrettyString()) +} + +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(w, httperrors.NewInputParameterError("need %s or %s", manager.Keyword(), + manager.KeywordPlural())) + return + } + ctype := params[""] + if len(ctype) == 0 { + httperrors.InputParameterError(w, "ctype of template should not be empty") + } + err = manager.UpdateTemplate(ctx, ctype, mergeQueryParams(params, query), data) + if err != nil { + httperrors.GeneralServerError(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(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(w, err) + } } func configDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -211,14 +318,8 @@ func contactUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Re manager.KeywordPlural())) return } - // check that if the uid is exist + uid := params[""] - _, err = utils.GetUserByID(ctx, uid) - if err != nil { - log.Errorf(`uid %q not found`, uid) - httperrors.NotFoundError(w, "Uid Not Found") - return - } queryDict := mergeQueryParams(params, query) update, _ := body.Bool(manager.Keyword(), "update_dingtalk") if update { @@ -258,7 +359,7 @@ func deleteContactHandler(ctx context.Context, w http.ResponseWriter, r *http.Re // 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()) + data, err := body.Get(models.ContactManager.Keyword(), models.ContactManager.KeywordPlural()) if err != nil { httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural()) return diff --git a/pkg/notify/interface/interface.go b/pkg/notify/interface/interface.go index 97027b669a..44eaaccac5 100644 --- a/pkg/notify/interface/interface.go +++ b/pkg/notify/interface/interface.go @@ -18,6 +18,7 @@ import ( "context" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/rpc/apis" ) type INotifyService interface { @@ -34,4 +35,8 @@ type IServiceConfigStore interface { SetConfig(serviceName string, config SConfig) error } +type ITemplateStore interface { + NotifyFilter(contactType, topic, msg string) (params apis.SendParams, err error) +} + type SConfig map[string]string diff --git a/pkg/notify/models/initdb.go b/pkg/notify/models/initdb.go index e662fca861..f82e4a4760 100644 --- a/pkg/notify/models/initdb.go +++ b/pkg/notify/models/initdb.go @@ -31,6 +31,7 @@ func InitDB() error { VerifyManager, NotificationManager, ConfigManager, + TemplateManager, } { err := manager.InitializeData() if err != nil { diff --git a/pkg/notify/models/mod_contact.go b/pkg/notify/models/mod_contact.go index 2667a4b053..c102390219 100644 --- a/pkg/notify/models/mod_contact.go +++ b/pkg/notify/models/mod_contact.go @@ -22,6 +22,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/sets" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/cloudcommon/db" @@ -99,20 +100,57 @@ func (self *SContactManager) InitializeData() error { return nil } -func (self *SContactManager) FetchByUIDs(uids []string) ([]SContact, error) { +func (self *SContactManager) FetchByUIDs(uids []string, uname bool) ([]SContact, error) { + var err error + if uname { + uids, err = self._UIDsFromUIDOrName(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) + err = db.FetchModelObjects(self, q, &records) if err != nil { return nil, err } return records, nil } +func (self *SContactManager) _UIDsFromUIDOrName(uidStrs []string) ([]string, error) { + users, err := utils.GetUsersWithoutRemote(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) + } + return uids, nil +} + func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []string) ([]SContact, error) { - q := self.Query("id", "uid", "contact_type", "contact", "enabled") - q = q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.In(q.Field("contact_type"), contactTypes))) + 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 { @@ -122,8 +160,7 @@ func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []strin } func (self *SContactManager) FetchByMore(uid, contact, contactType string) ([]SContact, error) { - q := self.Query() - q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.Equals(q.Field("contact"), contact), sqlchemy.Equals(q.Field("contact_type"), contactType))) + 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 { @@ -215,6 +252,12 @@ func (self *SContactManager) GetAllNotify(ctx context.Context, ids []string, con q := self.Query() if !group { + if v := ctx.Value("uname"); v != nil { + ids, err = self._UIDsFromUIDOrName(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) @@ -225,6 +268,7 @@ func (self *SContactManager) GetAllNotify(ctx context.Context, ids []string, con } uid = append(uid, tmpUids...) } + uids = uid } q.Filter(sqlchemy.AND(sqlchemy.In(q.Field("uid"), uids), sqlchemy.Equals(q.Field("contact_type"), contactType), sqlchemy.Equals(q.Field("status"), CONTACT_VERIFIED))) diff --git a/pkg/notify/models/mod_notification.go b/pkg/notify/models/mod_notification.go index 25b526c8be..bd48f9b745 100644 --- a/pkg/notify/models/mod_notification.go +++ b/pkg/notify/models/mod_notification.go @@ -468,11 +468,12 @@ func sendWithoutUserCred(notifications []SNotification) { wg.Wait() } -func ReSend(minutes int) { - scope := time.Duration(minutes) * time.Minute +func ReSend(seconds int) { + scope := time.Duration(seconds) * time.Second notifications, err := NotificationManager.FetchNotOK(time.Now().Add(-scope)) if err != nil { return } + log.Debugf("Start to resend message with a total of %d", len(notifications)) sendWithoutUserCred(notifications) } diff --git a/pkg/notify/models/mod_template.go b/pkg/notify/models/mod_template.go new file mode 100644 index 0000000000..3d39979938 --- /dev/null +++ b/pkg/notify/models/mod_template.go @@ -0,0 +1,198 @@ +// 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" + "database/sql" + "encoding/json" + "io/ioutil" + "os" + "strings" + "sync" + ptem "text/template" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/rpc/apis" + "yunion.io/x/onecloud/pkg/notify/template" +) + +type STemplateManager struct { + SStandaloneResourceBaseManager +} + +var TemplateManager *STemplateManager + +func init() { + TemplateManager = &STemplateManager{ + SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager( + STemplate{}, + "notify_t_template", + "notifytemplate", + "notifytemplates", + ), + } +} + +const ( + TEMPLATE_TYPE_TITLE = "title" + TEMPLATE_TYPE_CONTENT = "content" + TEMPLATE_TYPE_REMOTE = "remote" +) + +var ( + DefaultEmailUrl = "" + EmailUrl = "" + EmailUrlLock sync.RWMutex +) + +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"` +} + +func (tm *STemplateManager) GetEmailUrl() string { + EmailUrlLock.RLock() + defer EmailUrlLock.RUnlock() + if len(EmailUrl) == 0 { + return DefaultEmailUrl + } + return EmailUrl +} + +func (tm *STemplateManager) SetEmailUrl(url string) { + EmailUrlLock.Lock() + defer EmailUrlLock.Unlock() + EmailUrl = url +} + +func (tm *STemplateManager) InitializeData() error { + q := tm.Query().Equals("contact_type", "email").Equals("topic", "VERIFY").Equals("template_type", "content") + count, _ := q.CountWithError() + if count > 0 { + return nil + } + content, err := ioutil.ReadFile(template.EMAIL_VERIFY_CONTENT_PATH) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return errors.Wrapf(err, "os.Open for '%s'", template.EMAIL_VERIFY_CONTENT_PATH) + } + contentTem := STemplate{ + ContactType: "email", + Topic: "VERIFY", + TemplateType: "content", + Content: string(content), + } + titleTem := STemplate{ + ContactType: "email", + Topic: "VERIFY", + TemplateType: "title", + Content: template.EMAIL_VERIFY_TITLE, + } + err = tm.TableSpec().InsertOrUpdate(&contentTem) + if err != nil { + return errors.Wrap(err, "sqlchemy.TableSpec.InsertOrUpdate") + } + tm.TableSpec().InsertOrUpdate(&titleTem) + if err != nil { + return errors.Wrap(err, "sqlchemy.TableSpec.InsertOrUpdate") + } + return nil +} + +// 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) { + params.Topic = topic + templates := make([]STemplate, 0, 3) + q := tm.Query().Equals("contact_type", contactType).Equals("topic", strings.ToUpper(topic)) + err = db.FetchModelObjects(tm, q, &templates) + if errors.Cause(err) == sql.ErrNoRows { + // no such template, return as is + params.Title = topic + params.Message = msg + return + } + if err != nil { + err = errors.Wrap(err, "db.FetchModelObjects") + return + } + for _, template := range templates { + var title, content string + switch template.TemplateType { + case TEMPLATE_TYPE_TITLE: + title, err = template.Execute(msg) + if err != nil { + return + } + params.Title = title + case TEMPLATE_TYPE_CONTENT: + content, err = template.Execute(msg) + if err != nil { + return + } + params.Message = content + case TEMPLATE_TYPE_REMOTE: + params.RemoteTemplate = template.Content + params.Message = msg + default: + err = errors.Error("no support template type") + return + } + } + return +} + +func (tm *STemplate) Execute(str string) (string, error) { + tem, err := ptem.New("tmp").Parse(tm.Content) + if err != nil { + return "", errors.Wrapf(err, "Template.Parse for template %s", tm.GetId()) + } + var buffer bytes.Buffer + tmpMap := make(map[string]interface{}) + err = json.Unmarshal([]byte(str), &tmpMap) + if err != nil { + return "", errors.Wrap(err, "json.Unmarshal") + } + err = tem.Execute(&buffer, tmpMap) + if err != nil { + return "", errors.Wrap(err, "template,Execute") + } + 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) + } + return data, nil +} diff --git a/pkg/notify/models/worker.go b/pkg/notify/models/worker.go index 6b40673d0b..42403cd1fb 100644 --- a/pkg/notify/models/worker.go +++ b/pkg/notify/models/worker.go @@ -25,8 +25,9 @@ import ( "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/mcclient/modules/notify" "yunion.io/x/onecloud/pkg/notify/utils" ) @@ -54,7 +55,7 @@ func sendone(ctx context.Context, userCred mcclient.TokenCredential, notificatio err = NotifyService.Send(ctx, notification.ContactType, contact, notification.Topic, notification.Msg, notification.Priority) if err != nil { - log.Errorf("Send notification failed because that %s.", err.Error()) + log.Errorf("Send notification failed: %s.", err.Error()) notification.SetStatus(userCred, NOTIFY_FAIL, err.Error()) } else { log.Debugf("send notification successfully") @@ -82,7 +83,7 @@ func sendVerifyMessage(ctx context.Context, userCred mcclient.TokenCredential, v ) processId, token := verify.ID, verify.Token if contactType == "email" { - emailUrl := strings.Replace(options.Options.VerifyEmailUrl, "{0}", processId, 1) + emailUrl := strings.Replace(TemplateManager.GetEmailUrl(), "{0}", processId, 1) emailUrl = strings.Replace(emailUrl, "{1}", token, 1) // get uName @@ -105,7 +106,10 @@ func sendVerifyMessage(ctx context.Context, userCred mcclient.TokenCredential, v err = NotifyService.Send(ctx, contactType, contact, "verify", msg, "") if err != nil { verify.SetStatus(userCred, VERIFICATION_SENT_FAIL, "") - log.Errorf("Send verify message failed because that %s.", err.Error()) + // notify the uid through the webconsole + notifyclient.RawNotify([]string{uid}, false, notify.NotifyByWebConsole, notify.NotifyPriorityCritical, + "Send Verify Message Failed", jsonutils.NewString(err.Error())) + log.Errorf("Send verify message failed: %s.", err.Error()) return } verify.SetStatus(userCred, VERIFICATION_SENT, "") diff --git a/pkg/notify/options/options.go b/pkg/notify/options/options.go index f92e069ece..500e645121 100644 --- a/pkg/notify/options/options.go +++ b/pkg/notify/options/options.go @@ -22,12 +22,13 @@ type NotifyOption struct { options.CommonOptions 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:s)" default:"30"` - VerifyEmailUrl string `help:"url of verify email"` - ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope minutes"` - InitNotificationScope int `help:"initialize data of notification with in InitNotificationScope hours" default:"100"` + DingtalkEnabled bool `help:"Enable dingtalk"` + SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/notify"` + UpdateInterval int `help:"Update send services interval(unit:s)" default:"30"` + VerifyEmailUrl string `help:"url of verify email"` + ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope +seconds"` + InitNotificationScope int `help:"initialize data of notification with in InitNotificationScope hours" default:"100"` } var Options NotifyOption diff --git a/pkg/notify/rpc/apis/send_server.pb.go b/pkg/notify/rpc/apis/send_server.pb.go index 2b30129f9a..f1db91f6b1 100644 --- a/pkg/notify/rpc/apis/send_server.pb.go +++ b/pkg/notify/rpc/apis/send_server.pb.go @@ -40,10 +40,12 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type SendParams struct { - Contact string `protobuf:"bytes,1,opt,name=contact,proto3" json:"contact,omitempty"` - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,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"` + Contact string `protobuf:"bytes,1,opt,name=Contact,proto3" json:"Contact,omitempty"` + Topic string `protobuf:"bytes,2,opt,name=Topic,proto3" json:"Topic,omitempty"` + Title string `protobuf:"bytes,3,opt,name=Title,proto3" json:"Title,omitempty"` + Message string `protobuf:"bytes,4,opt,name=Message,proto3" json:"Message,omitempty"` + Priority string `protobuf:"bytes,5,opt,name=Priority,proto3" json:"Priority,omitempty"` + RemoteTemplate string `protobuf:"bytes,6,opt,name=RemoteTemplate,proto3" json:"RemoteTemplate,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -88,6 +90,13 @@ func (m *SendParams) GetTopic() string { return "" } +func (m *SendParams) GetTitle() string { + if m != nil { + return m.Title + } + return "" +} + func (m *SendParams) GetMessage() string { if m != nil { return m.Message @@ -102,6 +111,13 @@ func (m *SendParams) GetPriority() string { return "" } +func (m *SendParams) GetRemoteTemplate() string { + if m != nil { + return m.RemoteTemplate + } + return "" +} + type UpdateConfigParams struct { Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -262,28 +278,30 @@ func init() { func init() { proto.RegisterFile("send_server.proto", fileDescriptor_63fdd68f7eb311f9) } var fileDescriptor_63fdd68f7eb311f9 = []byte{ - // 325 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcd, 0x4a, 0x03, 0x31, - 0x10, 0xc7, 0xd9, 0x7e, 0xda, 0x69, 0x91, 0x1a, 0x8b, 0xc4, 0x3d, 0x95, 0x85, 0x42, 0x2f, 0xee, - 0xa1, 0x22, 0x48, 0x2f, 0xa2, 0x52, 0x3c, 0x09, 0xa5, 0xd2, 0xb3, 0xa4, 0xdd, 0xe9, 0x12, 0xec, - 0x6e, 0x96, 0x24, 0x2d, 0xec, 0x63, 0xf8, 0x2a, 0x3e, 0xa1, 0xe4, 0x63, 0xb5, 0xc5, 0x7a, 0xcb, - 0x6f, 0x66, 0xfe, 0x99, 0xf9, 0x4f, 0x02, 0x17, 0x0a, 0xf3, 0xe4, 0x5d, 0xa1, 0xdc, 0xa3, 0x8c, - 0x0b, 0x29, 0xb4, 0x20, 0x0d, 0x56, 0x70, 0x15, 0x49, 0x80, 0x37, 0xcc, 0x93, 0x39, 0x93, 0x2c, - 0x53, 0x84, 0x42, 0x7b, 0x2d, 0x72, 0xcd, 0xd6, 0x9a, 0x06, 0xc3, 0x60, 0xdc, 0x59, 0x54, 0x48, - 0x06, 0xd0, 0xd4, 0xa2, 0xe0, 0x6b, 0x5a, 0xb3, 0x71, 0x07, 0xa6, 0x3e, 0x43, 0xa5, 0x58, 0x8a, - 0xb4, 0xee, 0xea, 0x3d, 0x92, 0x10, 0xce, 0xe6, 0x92, 0x0b, 0xc9, 0x75, 0x49, 0x1b, 0x36, 0xf5, - 0xc3, 0xd1, 0x67, 0x00, 0x64, 0x59, 0x24, 0x4c, 0xe3, 0xb3, 0xc8, 0x37, 0x3c, 0xf5, 0xcd, 0x1f, - 0x6c, 0xf3, 0x0d, 0x4f, 0x15, 0x0d, 0x86, 0xf5, 0x71, 0x77, 0x32, 0x8a, 0xcd, 0x88, 0xf1, 0xdf, - 0xd2, 0xd8, 0x81, 0x9a, 0xe5, 0x5a, 0x96, 0x8b, 0x4a, 0x15, 0x4e, 0xa1, 0x77, 0x98, 0x20, 0x7d, - 0xa8, 0x7f, 0x60, 0xe9, 0x9d, 0x98, 0xa3, 0x71, 0xb1, 0x67, 0xdb, 0x1d, 0x56, 0x2e, 0x2c, 0x4c, - 0x6b, 0xf7, 0x41, 0x14, 0xc3, 0x60, 0xa9, 0x50, 0xf2, 0xe4, 0xa9, 0x7c, 0x15, 0x2b, 0xbe, 0x45, - 0x3f, 0xd4, 0x15, 0xb4, 0x32, 0xcb, 0xfe, 0x1a, 0x4f, 0x51, 0x1b, 0x9a, 0xb3, 0xac, 0xd0, 0x65, - 0x74, 0x03, 0x97, 0xc7, 0xc2, 0x05, 0x16, 0xdb, 0xd2, 0xe8, 0x76, 0x36, 0x5c, 0xe9, 0x1c, 0x4d, - 0xbe, 0x02, 0xe8, 0x98, 0x85, 0x3f, 0xa6, 0x98, 0x6b, 0x32, 0x82, 0x86, 0x01, 0xd2, 0x77, 0x4e, - 0x7f, 0x5f, 0x22, 0xec, 0xba, 0x88, 0xed, 0x41, 0xee, 0xa0, 0x77, 0xb8, 0x04, 0x42, 0xff, 0x5b, - 0xcc, 0xb1, 0xec, 0x05, 0xce, 0x8f, 0x47, 0x23, 0xa1, 0x17, 0x9e, 0x70, 0x1a, 0x5e, 0x9f, 0xca, - 0x59, 0x33, 0xab, 0x96, 0xfd, 0x31, 0xb7, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0x12, 0x30, - 0x1e, 0x46, 0x02, 0x00, 0x00, + // 357 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcf, 0x6a, 0xe3, 0x30, + 0x10, 0xc6, 0x71, 0xfe, 0x6e, 0x26, 0x21, 0x64, 0xb5, 0x61, 0xd1, 0xfa, 0x14, 0x0c, 0x59, 0x72, + 0x59, 0x1f, 0xb2, 0x2c, 0x2c, 0xb9, 0x94, 0x36, 0x84, 0x9e, 0x02, 0xc1, 0x4d, 0xce, 0x45, 0x89, + 0xa7, 0x41, 0xd4, 0xb6, 0x8c, 0xa4, 0x04, 0xfc, 0x18, 0x7d, 0x93, 0xd2, 0x27, 0x2c, 0x92, 0xe5, + 0x36, 0x69, 0xd3, 0x9b, 0x7f, 0xdf, 0xe8, 0x1b, 0xcf, 0x37, 0x12, 0x7c, 0x57, 0x98, 0xc5, 0xf7, + 0x0a, 0xe5, 0x11, 0x65, 0x98, 0x4b, 0xa1, 0x05, 0x69, 0xb0, 0x9c, 0xab, 0xe0, 0xd9, 0x03, 0xb8, + 0xc3, 0x2c, 0x5e, 0x31, 0xc9, 0x52, 0x45, 0x28, 0xb4, 0xe7, 0x22, 0xd3, 0x6c, 0xa7, 0xa9, 0x37, + 0xf2, 0x26, 0x9d, 0xa8, 0x42, 0x32, 0x84, 0xe6, 0x5a, 0xe4, 0x7c, 0x47, 0x6b, 0x56, 0x2f, 0xc1, + 0xaa, 0x5c, 0x27, 0x48, 0xeb, 0x4e, 0x35, 0x60, 0xba, 0x2c, 0x51, 0x29, 0xb6, 0x47, 0xda, 0x28, + 0xbb, 0x38, 0x24, 0x3e, 0x7c, 0x5b, 0x49, 0x2e, 0x24, 0xd7, 0x05, 0x6d, 0xda, 0xd2, 0x1b, 0x93, + 0xdf, 0xd0, 0x8f, 0x30, 0x15, 0x1a, 0xd7, 0x98, 0xe6, 0x09, 0xd3, 0x48, 0x5b, 0xf6, 0xc4, 0x07, + 0x35, 0x78, 0xf2, 0x80, 0x6c, 0xf2, 0x98, 0x69, 0x9c, 0x8b, 0xec, 0x81, 0xef, 0xdd, 0xe8, 0x57, + 0xd0, 0xde, 0x59, 0x56, 0xd4, 0x1b, 0xd5, 0x27, 0xdd, 0xe9, 0x38, 0x34, 0x09, 0xc3, 0xcf, 0x47, + 0xc3, 0x12, 0xd4, 0x22, 0xd3, 0xb2, 0x88, 0x2a, 0x97, 0x3f, 0x83, 0xde, 0x69, 0x81, 0x0c, 0xa0, + 0xfe, 0x88, 0x85, 0xdb, 0x83, 0xf9, 0x34, 0x69, 0x8f, 0x2c, 0x39, 0x60, 0xb5, 0x03, 0x0b, 0xb3, + 0xda, 0x7f, 0x2f, 0x08, 0x61, 0xb8, 0x51, 0x28, 0x79, 0x7c, 0x53, 0x2c, 0xc5, 0x96, 0x27, 0xe8, + 0x86, 0xfa, 0x09, 0xad, 0xd4, 0xb2, 0x6b, 0xe3, 0x28, 0x68, 0x43, 0x73, 0x91, 0xe6, 0xba, 0x08, + 0xfe, 0xc0, 0x8f, 0x73, 0x63, 0x84, 0x79, 0x52, 0x18, 0xdf, 0xc1, 0xca, 0x95, 0xaf, 0xa4, 0xe9, + 0x8b, 0x07, 0x1d, 0x73, 0x5d, 0xd7, 0x7b, 0xcc, 0x34, 0x19, 0x43, 0xc3, 0x00, 0x19, 0x94, 0x49, + 0xdf, 0xef, 0xd1, 0xef, 0x96, 0x8a, 0xfd, 0x07, 0xf9, 0x07, 0xbd, 0xd3, 0x25, 0x10, 0xfa, 0xd5, + 0x62, 0xce, 0x6d, 0xb7, 0xd0, 0x3f, 0x1f, 0x8d, 0xf8, 0xce, 0x78, 0x21, 0xa9, 0xff, 0xeb, 0x52, + 0xcd, 0x86, 0xd9, 0xb6, 0xec, 0x83, 0xfb, 0xfb, 0x1a, 0x00, 0x00, 0xff, 0xff, 0xd7, 0x6c, 0xcb, + 0x1b, 0x85, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/pkg/notify/rpc/apis/send_server.proto b/pkg/notify/rpc/apis/send_server.proto index e2060ecfc8..c065347486 100644 --- a/pkg/notify/rpc/apis/send_server.proto +++ b/pkg/notify/rpc/apis/send_server.proto @@ -1,12 +1,28 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + syntax = "proto3"; package apis; message SendParams { - string contact = 1; - string topic = 2; - string message = 3; - string Priority = 4; + string Contact = 1; + string Topic = 2; + string Title = 3; + string Message = 4; + string Priority = 5; + string RemoteTemplate = 6; } message UpdateConfigParams { diff --git a/pkg/notify/rpc/send.go b/pkg/notify/rpc/send.go index e1d9405a48..b6db8fe127 100644 --- a/pkg/notify/rpc/send.go +++ b/pkg/notify/rpc/send.go @@ -50,14 +50,17 @@ type SRpcService struct { SendServices *ServiceMap socketFileDir string configStore _interface.IServiceConfigStore + templateStore _interface.ITemplateStore } // NewSRpcService create a SRpcService -func NewSRpcService(socketFileDir string, configStore _interface.IServiceConfigStore) *SRpcService { +func NewSRpcService(socketFileDir string, configStore _interface.IServiceConfigStore, + tempalteStore _interface.ITemplateStore) *SRpcService { return &SRpcService{ SendServices: NewServiceMap(), socketFileDir: socketFileDir, configStore: configStore, + templateStore: tempalteStore, } } @@ -106,20 +109,24 @@ 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 { - args := apis.SendParams{ - Contact: contact, - Topic: topic, - Message: msg, - Priority: priority, + args, err := self.templateStore.NotifyFilter(contactType, topic, msg) + if err != nil { + return errors.Wrap(err, "templateStore.NotifyFilter") } + args.Contact = contact + args.Priority = priority + f := func(service *apis.SendNotificationClient) (interface{}, error) { log.Debugf("send one") return service.Send(ctx, &args) } - _, err := self.execute(ctx, f, contactType) - return err + _, err = self.execute(ctx, f, contactType) + if err != nil { + return errors.Wrapf(err, "contactType: %s", contactType) + } + return nil } // RestartService can restart remote rpc server and pass config info. @@ -179,7 +186,7 @@ func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNo } if st.Message() != ErrSendServiceNotInit.Error() { - return nil, errors.Error(fmt.Sprintf("Send message failed because that %s.", st.Message())) + return nil, errors.Error(st.Message()) } // if NOINIT, try to restart server and send again @@ -197,7 +204,7 @@ func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNo return nil, errors.Wrap(ErrSendServiceNotFound, serviceName) } - return nil, errors.Error(fmt.Sprintf("Send message failed because that %s.", st.Message())) + return nil, errors.Error(st.Message()) } } return ret, nil diff --git a/pkg/notify/service.go b/pkg/notify/service.go index 95c2ea8e72..7b40a57e01 100644 --- a/pkg/notify/service.go +++ b/pkg/notify/service.go @@ -43,6 +43,9 @@ func StartService() { baseOpts := &options.Options.BaseOptions common_options.ParseOptions(opts, os.Args, "notify.conf", "notify") + // init email url + models.TemplateManager.SetEmailUrl(options.Options.VerifyEmailUrl) + // init auth app.InitAuth(commonOpts, func() { log.Infof("Auth complete!") @@ -60,7 +63,7 @@ func StartService() { cache.RegistUserCredCacheUpdater() // init notify service - models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager) + models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager, models.TemplateManager) models.NotifyService.InitAll() defer models.NotifyService.StopAll() @@ -72,7 +75,8 @@ func StartService() { resend := func(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { models.ReSend(opts.ReSendScope) } - cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Minute, resend) + cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Second, resend) + cron.Start() app.ServeForever(applicaion, baseOpts) } diff --git a/pkg/notify/template/doc.go b/pkg/notify/template/doc.go new file mode 100644 index 0000000000..aebfaab473 --- /dev/null +++ b/pkg/notify/template/doc.go @@ -0,0 +1 @@ +package template // import "yunion.io/x/onecloud/pkg/notify/template" diff --git a/pkg/notify/template/template.go b/pkg/notify/template/template.go new file mode 100644 index 0000000000..2ecc8ab580 --- /dev/null +++ b/pkg/notify/template/template.go @@ -0,0 +1,20 @@ +// 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 template + +const ( + EMAIL_VERIFY_CONTENT_PATH = "/opt/yunion/share/notify/email_verify_template" + EMAIL_VERIFY_TITLE = "Yunion Verify" +) diff --git a/pkg/notify/utils/keystone.go b/pkg/notify/utils/keystone.go index d72c0703d1..5529906139 100644 --- a/pkg/notify/utils/keystone.go +++ b/pkg/notify/utils/keystone.go @@ -17,11 +17,26 @@ package utils import ( "context" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/notify/cache" ) -func GetUserByID(ctx context.Context, id string) (*cache.SUser, error) { - return cache.UserCacheManager.FetchUserByID(ctx, id, false) +func GetUserByIDOrName(ctx context.Context, idStr string) (*cache.SUser, error) { + return cache.UserCacheManager.FetchUserByIDOrName(ctx, idStr) +} + +func GetUsersWithoutRemote(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))) + users := make([]cache.SUser, 0, 1) + err := db.FetchModelObjects(cache.UserCacheManager, q, &users) + if err != nil { + return nil, errors.Wrapf(err, "fetch user cache failed") + } + return users, nil } func GetUserIdsLikeName(ctx context.Context, name string) ([]string, error) { @@ -49,7 +64,7 @@ func GetUsersByGroupID(ctx context.Context, gid string) ([]string, error) { } func GetUsernameByID(ctx context.Context, id string) (string, error) { - user, err := GetUserByID(ctx, id) + user, err := GetUserByIDOrName(ctx, id) if err != nil { return "", err }