template.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package models
  15. import (
  16. "bytes"
  17. "context"
  18. "database/sql"
  19. "encoding/json"
  20. "fmt"
  21. "html"
  22. "io/ioutil"
  23. "path/filepath"
  24. "strings"
  25. ptem "text/template"
  26. "yunion.io/x/jsonutils"
  27. "yunion.io/x/pkg/appctx"
  28. "yunion.io/x/pkg/errors"
  29. "yunion.io/x/pkg/util/httputils"
  30. "yunion.io/x/pkg/utils"
  31. "yunion.io/x/sqlchemy"
  32. api "yunion.io/x/onecloud/pkg/apis/notify"
  33. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  34. "yunion.io/x/onecloud/pkg/httperrors"
  35. "yunion.io/x/onecloud/pkg/mcclient"
  36. "yunion.io/x/onecloud/pkg/mcclient/auth"
  37. "yunion.io/x/onecloud/pkg/notify/options"
  38. )
  39. type STemplateManager struct {
  40. db.SStandaloneAnonResourceBaseManager
  41. }
  42. var TemplateManager *STemplateManager
  43. func init() {
  44. TemplateManager = &STemplateManager{
  45. SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager(
  46. STemplate{},
  47. "template_tbl",
  48. "notifytemplate",
  49. "notifytemplates",
  50. ),
  51. }
  52. TemplateManager.SetVirtualObject(TemplateManager)
  53. }
  54. const (
  55. CONTACTTYPE_ALL = "all"
  56. )
  57. type STemplate struct {
  58. db.SStandaloneAnonResourceBase
  59. ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"`
  60. Topic string `width:"128" nullable:"false" create:"required" update:"user" list:"user"`
  61. // title | content | remote
  62. TemplateType string `width:"10" nullable:"false" create:"required" update:"user" list:"user"`
  63. Content string `length:"text" nullable:"true" create:"required" get:"user" list:"user" update:"user"`
  64. Lang string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user" create:"optional"`
  65. Example string `nullable:"true" create:"optional" get:"user" list:"user" update:"user"`
  66. }
  67. const (
  68. verifyUrlPath = "/email-verification/id/{0}/token/{1}?region=%s"
  69. templatePath = "/opt/yunion/share/template"
  70. )
  71. func (tm *STemplateManager) GetEmailUrl() string {
  72. return httputils.JoinPath(options.Options.ApiServer, fmt.Sprintf(verifyUrlPath, options.Options.Region))
  73. }
  74. func (tm *STemplateManager) defaultTemplate() ([]STemplate, error) {
  75. templates := make([]STemplate, 0, 4)
  76. for _, templateType := range []string{"title", "content"} {
  77. for _, lang := range []string{api.TEMPLATE_LANG_CN, api.TEMPLATE_LANG_EN} {
  78. contactType, topic := CONTACTTYPE_ALL, ""
  79. titleTemplatePath := fmt.Sprintf("%s/%s@%s", templatePath, templateType, lang)
  80. files, err := ioutil.ReadDir(titleTemplatePath)
  81. if err != nil {
  82. return templates, errors.Wrapf(err, "Read Dir '%s'", titleTemplatePath)
  83. }
  84. for _, file := range files {
  85. if file.IsDir() {
  86. continue
  87. }
  88. spliteName := strings.Split(file.Name(), ".")
  89. topic = spliteName[0]
  90. if len(spliteName) > 1 {
  91. contactType = spliteName[1]
  92. }
  93. fullPath := filepath.Join(titleTemplatePath, file.Name())
  94. content, err := ioutil.ReadFile(fullPath)
  95. if err != nil {
  96. return templates, err
  97. }
  98. templates = append(templates, STemplate{
  99. ContactType: contactType,
  100. Topic: topic,
  101. Lang: lang,
  102. TemplateType: templateType,
  103. Content: string(content),
  104. })
  105. }
  106. }
  107. }
  108. return templates, nil
  109. }
  110. type SCompanyInfo struct {
  111. LoginLogo string `json:"login_logo"`
  112. LoginLogoFormat string `json:"login_logo_format"`
  113. Copyright string `json:"copyright"`
  114. Name string `json:"name"`
  115. }
  116. func (tm *STemplateManager) GetCompanyInfo(ctx context.Context) (SCompanyInfo, error) {
  117. return SCompanyInfo{
  118. Name: options.Options.GetPlatformName(appctx.Lang(ctx)),
  119. }, nil
  120. }
  121. var (
  122. forceInitTopic = []string{
  123. "VERIFY",
  124. "USER_LOGIN_EXCEPTION",
  125. }
  126. defaultLang = api.TEMPLATE_LANG_CN
  127. )
  128. func getTemplateLangFromCtx(ctx context.Context) string {
  129. return notifyclientI18nTable.Lookup(ctx, tempalteLang)
  130. }
  131. func (tm *STemplateManager) InitializeData() error {
  132. // init lang
  133. q := tm.Query().IsEmpty("lang")
  134. var noLangTemplates []STemplate
  135. err := db.FetchModelObjects(tm, q, &noLangTemplates)
  136. if err != nil {
  137. return errors.Wrap(err, "unable to fetch templates")
  138. }
  139. for i := range noLangTemplates {
  140. t := &noLangTemplates[i]
  141. _, err := db.Update(t, func() error {
  142. t.Lang = defaultLang
  143. return nil
  144. })
  145. if err != nil {
  146. return err
  147. }
  148. }
  149. templates, err := tm.defaultTemplate()
  150. if err != nil {
  151. return err
  152. }
  153. for _, template := range templates {
  154. q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType).Equals("lang", template.Lang)
  155. count, _ := q.CountWithError()
  156. if count > 0 && !utils.IsInStringArray(template.Topic, forceInitTopic) {
  157. continue
  158. }
  159. if count == 0 {
  160. err := tm.TableSpec().Insert(context.TODO(), &template)
  161. if err != nil {
  162. return errors.Wrap(err, "sqlchemy.TableSpec.Insert")
  163. }
  164. continue
  165. }
  166. oldTemplates := make([]STemplate, 0, 1)
  167. err := db.FetchModelObjects(tm, q, &oldTemplates)
  168. if err != nil {
  169. return errors.Wrap(err, "db.FetchModelObjects")
  170. }
  171. // delete addtion
  172. var (
  173. ctx = context.Background()
  174. userCred = auth.AdminCredential()
  175. )
  176. for i := 1; i < len(oldTemplates); i++ {
  177. err := oldTemplates[i].Delete(ctx, userCred)
  178. if err != nil {
  179. return errors.Wrap(err, "STemplate.Delete")
  180. }
  181. }
  182. // update
  183. oldTemplate := &oldTemplates[0]
  184. _, err = db.Update(oldTemplate, func() error {
  185. oldTemplate.Content = template.Content
  186. return nil
  187. })
  188. if err != nil {
  189. return errors.Wrap(err, "db.Update")
  190. }
  191. }
  192. return nil
  193. }
  194. // FillWithTemplate will return the title and content generated by corresponding template.
  195. // Local cache about common template will be considered in case of performance issues.
  196. func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) {
  197. if len(lang) == 0 {
  198. params.Title = no.Topic
  199. params.Message = no.Message
  200. return
  201. }
  202. params.Topic = no.Topic
  203. templates := make([]STemplate, 0, 3)
  204. // if strings.Contains(no.Topic, "-cn") || strings.Contains(no.Topic, "-en") {
  205. // no.Topic = no.Topic[:len(no.Topic)-3]
  206. // }
  207. q := tm.Query().Equals("topic", strings.ToUpper(no.Topic)).Equals("lang", lang).In("contact_type", []string{CONTACTTYPE_ALL, no.ContactType})
  208. err = db.FetchModelObjects(tm, q, &templates)
  209. if errors.Cause(err) == sql.ErrNoRows || len(templates) == 0 {
  210. // no such template, return as is
  211. params.Title = no.Topic
  212. params.Message = no.Message
  213. return
  214. }
  215. if err != nil {
  216. err = errors.Wrap(err, "db.FetchModelObjects")
  217. return
  218. }
  219. for _, template := range tm.chooseTemplate(no.ContactType, templates) {
  220. var title, content string
  221. switch template.TemplateType {
  222. case api.TEMPLATE_TYPE_TITLE:
  223. title, err = template.Execute(no.Message)
  224. if err != nil {
  225. return
  226. }
  227. params.Title = title
  228. case api.TEMPLATE_TYPE_CONTENT:
  229. content, err = template.Execute(no.Message)
  230. if err != nil {
  231. return
  232. }
  233. params.Message = content
  234. case api.TEMPLATE_TYPE_REMOTE:
  235. params.RemoteTemplate = template.Content
  236. params.Message = no.Message
  237. default:
  238. err = errors.Error("no support template type")
  239. return
  240. }
  241. }
  242. params.Message = html.UnescapeString(params.Message)
  243. return
  244. }
  245. func (tm *STemplateManager) chooseTemplate(contactType string, tempaltes []STemplate) []*STemplate {
  246. var titleTemplate, contentTemplate *STemplate
  247. // contactType first
  248. for i := range tempaltes {
  249. switch tempaltes[i].TemplateType {
  250. case api.TEMPLATE_TYPE_REMOTE:
  251. if tempaltes[i].ContactType == contactType {
  252. return []*STemplate{&tempaltes[i]}
  253. }
  254. case api.TEMPLATE_TYPE_TITLE:
  255. if tempaltes[i].ContactType == contactType {
  256. titleTemplate = &tempaltes[i]
  257. } else if titleTemplate == nil {
  258. titleTemplate = &tempaltes[i]
  259. }
  260. case api.TEMPLATE_TYPE_CONTENT:
  261. if tempaltes[i].ContactType == contactType {
  262. contentTemplate = &tempaltes[i]
  263. } else if contentTemplate == nil {
  264. contentTemplate = &tempaltes[i]
  265. }
  266. }
  267. }
  268. ret := make([]*STemplate, 0, 2)
  269. if titleTemplate != nil {
  270. ret = append(ret, titleTemplate)
  271. }
  272. if contentTemplate != nil {
  273. ret = append(ret, contentTemplate)
  274. }
  275. return ret
  276. }
  277. func (tm *STemplate) Execute(str string) (string, error) {
  278. tem, err := ptem.New("tmp").Parse(tm.Content)
  279. if err != nil {
  280. return "", errors.Wrapf(err, "Template.Parse for template %s", tm.GetId())
  281. }
  282. var buffer bytes.Buffer
  283. tmpMap := make(map[string]interface{})
  284. err = json.Unmarshal([]byte(str), &tmpMap)
  285. if err != nil {
  286. return "", errors.Wrap(err, "json.Unmarshal")
  287. }
  288. err = tem.Execute(&buffer, tmpMap)
  289. if err != nil {
  290. return "", errors.Wrap(err, "template,Execute")
  291. }
  292. return buffer.String(), nil
  293. }
  294. func (tm *STemplateManager) PerformSave(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.TemplateManagerSaveInput) (jsonutils.JSONObject, error) {
  295. q := tm.Query().Equals("contact_type", input.ContactType)
  296. templates := []STemplate{}
  297. err := db.FetchModelObjects(tm, q, &templates)
  298. if err != nil {
  299. return nil, err
  300. }
  301. tempaltesMap := make(map[string]*api.TemplateCreateInput, len(input.Templates))
  302. for i := range input.Templates {
  303. template := &input.Templates[i]
  304. if template.ContactType != input.ContactType {
  305. continue
  306. }
  307. input.Templates[i], err = tm.ValidateCreateData(ctx, userCred, userCred, nil, input.Templates[i])
  308. if err != nil {
  309. return nil, err
  310. }
  311. key := fmt.Sprintf("%s-%s-%s", template.Topic, template.TemplateType, template.Lang)
  312. tempaltesMap[key] = template
  313. }
  314. for i := range templates {
  315. key := fmt.Sprintf("%s-%s-%s", templates[i].Topic, templates[i].TemplateType, templates[i].Lang)
  316. if _, ok := tempaltesMap[key]; !ok {
  317. continue
  318. }
  319. if input.Force {
  320. err := templates[i].Delete(ctx, userCred)
  321. if err != nil {
  322. return nil, errors.Wrapf(err, "unable to delete template %s", templates[i].Id)
  323. }
  324. } else {
  325. delete(tempaltesMap, key)
  326. }
  327. }
  328. for _, template := range tempaltesMap {
  329. t := STemplate{
  330. ContactType: input.ContactType,
  331. Topic: template.Topic,
  332. TemplateType: template.TemplateType,
  333. Lang: template.Lang,
  334. Example: template.Example,
  335. Content: template.Content,
  336. }
  337. err = tm.TableSpec().Insert(ctx, &t)
  338. if err != nil {
  339. return nil, errors.Wrap(err, "unable to insert template")
  340. }
  341. }
  342. return nil, nil
  343. }
  344. func (tm *STemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.TemplateCreateInput) (api.TemplateCreateInput, error) {
  345. var err error
  346. input.StandaloneAnonResourceCreateInput, err = tm.SStandaloneAnonResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StandaloneAnonResourceCreateInput)
  347. if err != nil {
  348. return input, err
  349. }
  350. if !utils.IsInStringArray(input.TemplateType, []string{
  351. api.TEMPLATE_TYPE_CONTENT, api.TEMPLATE_TYPE_REMOTE, api.TEMPLATE_TYPE_TITLE,
  352. }) {
  353. return input, httperrors.NewInputParameterError("no such support for tempalte type %s", input.TemplateType)
  354. }
  355. if input.TemplateType != api.TEMPLATE_TYPE_REMOTE {
  356. if err := tm.validate(input.Content, input.Example); err != nil {
  357. return input, httperrors.NewInputParameterError("%s", err.Error())
  358. }
  359. }
  360. if input.Lang == "" {
  361. input.Lang = api.TEMPLATE_LANG_CN
  362. }
  363. if !utils.IsInStringArray(input.Lang, []string{api.TEMPLATE_LANG_EN, api.TEMPLATE_LANG_CN}) {
  364. return input, httperrors.NewInputParameterError("no such lang %s", input.Lang)
  365. }
  366. return input, nil
  367. }
  368. func (tm *STemplateManager) validate(template string, example string) error {
  369. // check example availability
  370. tem, err := ptem.New("tmp").Parse(template)
  371. if err != nil {
  372. return errors.Wrap(err, "invalid template")
  373. }
  374. var buffer bytes.Buffer
  375. tmpMap := make(map[string]interface{})
  376. err = json.Unmarshal([]byte(example), &tmpMap)
  377. if err != nil {
  378. return errors.Wrap(err, "invalid example")
  379. }
  380. err = tem.Execute(&buffer, tmpMap)
  381. if err != nil {
  382. return errors.Wrap(err, "invalid example")
  383. }
  384. return nil
  385. }
  386. func (tm *STemplateManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.TemplateListInput) (*sqlchemy.SQuery, error) {
  387. q, err := tm.SStandaloneAnonResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneAnonResourceListInput)
  388. if err != nil {
  389. return nil, err
  390. }
  391. if len(input.Topic) > 0 {
  392. q = q.Equals("topic", input.Topic)
  393. }
  394. if len(input.TemplateType) > 0 {
  395. q = q.Equals("template_type", input.TemplateType)
  396. }
  397. if len(input.ContactType) > 0 {
  398. q = q.Equals("contact_type", input.ContactType)
  399. }
  400. if len(input.Lang) > 0 {
  401. q = q.Equals("lang", input.Lang)
  402. }
  403. return q, nil
  404. }
  405. func (t *STemplate) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.TemplateUpdateInput) (api.TemplateUpdateInput, error) {
  406. var err error
  407. input.StandaloneAnonResourceBaseUpdateInput, err = t.SStandaloneAnonResourceBase.ValidateUpdateData(ctx, userCred, query, input.StandaloneAnonResourceBaseUpdateInput)
  408. if err != nil {
  409. return input, err
  410. }
  411. if t.TemplateType == api.TEMPLATE_TYPE_REMOTE {
  412. return input, nil
  413. }
  414. if err := TemplateManager.validate(input.Content, input.Example); err != nil {
  415. return input, httperrors.NewInputParameterError("%s", err.Error())
  416. }
  417. return input, nil
  418. }