Skip to content
This repository has been archived by the owner on Feb 28, 2023. It is now read-only.

Commit

Permalink
feat(notify): Support template & fix some bugs
Browse files Browse the repository at this point in the history
Signed-off-by: qwqcode <[email protected]>
  • Loading branch information
qwqcode committed May 23, 2022
1 parent 2af6e3e commit d5b67a2
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 134 deletions.
13 changes: 10 additions & 3 deletions artalk-go.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ captcha:
email:
enabled: false # 总开关
send_type: "smtp" # 发送方式 [smtp, ali_dm, sendmail]
send_name: '{{reply_nick}}' # 发信人昵称
send_name: "{{reply_nick}}" # 发信人昵称
send_addr: "[email protected]" # 发信人地址
mail_subject: "[{{site_name}}] 您收到了来自 @{{reply_nick}} 的回复"
mail_tpl: "default" # 邮件模板文件
Expand Down Expand Up @@ -133,10 +133,13 @@ img_upload:

# 管理员多元推送
admin_notify:
# 通知模版
notify_tpl: "default"
noise_mode: false
# 邮件通知管理员
email:
enabled: true # 当使用其他推送方式时,可以关闭管理员邮件通知
mail_subject: '[{{site_name}}] 您的文章 "{{page_title}}" 有新回复'
mail_subject: "[{{site_name}}] 您的文章{{page_title}}有新回复"
# Telegram
telegram:
enabled: false
Expand All @@ -147,5 +150,9 @@ admin_notify:
bark:
enabled: false
server: "http://day.app/xxxxxxx/"
# 另支持 飞书机器人、钉钉、Slack、LINE 等多种推送方式
# 飞书
lark:
enabled: false
webhook_url: ""
# 另支持 钉钉、Slack、LINE 等多种推送方式
# 详情参考:https://artalk.js.org/guide/backend/notify.html
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func init() {
Use: "config",
Short: "输出配置信息",
Run: func(cmd *cobra.Command, args []string) {
loadCore()
initConfig()
buf, _ := json.MarshalIndent(config.Instance, "", " ")
fmt.Println(string(buf))
},
Expand Down
22 changes: 12 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,19 +218,21 @@ type UpgitConf struct {

// 其他通知方式
type AdminNotifyConf struct {
Email *AdminEmailConf `mapstructure:"email" json:"email"` // 邮件通知
Telegram NotifyTelegramConf `mapstructure:"telegram" json:"telegram"` // TG
Lark NotifyLarkConf `mapstructure:"lark" json:"lark"` // 飞书
DingTalk NotifyDingTalkConf `mapstructure:"ding_talk" json:"ding_talk"` // 钉钉
Bark NotifyBarkConf `mapstructure:"bark" json:"bark"` // bark
Slack NotifySlackConf `mapstructure:"slack" json:"slack"` // slack
LINE NotifyLINEConf `mapstructure:"line" json:"line"` // LINE
NoiseMode bool `mapstructure:"noise_mode" json:"noise_mode"` // 嘈杂模式 (非回复管理员的评论也发送通知)
NotifyTpl string `mapstructure:"notify_tpl" json:"notify_tpl"` // 通知模板
NotifySubject string `mapstructure:"notify_subject" json:"notify_subject"` // 通知标题
Email *AdminEmailConf `mapstructure:"email" json:"email"` // 邮件通知
Telegram NotifyTelegramConf `mapstructure:"telegram" json:"telegram"` // TG
Lark NotifyLarkConf `mapstructure:"lark" json:"lark"` // 飞书
DingTalk NotifyDingTalkConf `mapstructure:"ding_talk" json:"ding_talk"` // 钉钉
Bark NotifyBarkConf `mapstructure:"bark" json:"bark"` // bark
Slack NotifySlackConf `mapstructure:"slack" json:"slack"` // slack
LINE NotifyLINEConf `mapstructure:"line" json:"line"` // LINE
NoiseMode bool `mapstructure:"noise_mode" json:"noise_mode"` // 嘈杂模式 (非回复管理员的评论也发送通知)
}

type AdminEmailConf struct {
Enabled bool `mapstructure:"enabled" json:"enabled"` // 总开关
MailSubject string `mapstructure:"mail_subject" json:"mail_subject"` // 邮件标题
Enabled bool `mapstructure:"enabled" json:"enabled"` // 管理员总开关
MailSubject string `mapstructure:"mail_subject" json:"mail_subject"` // 管理员邮件标题
}

type NotifyTelegramConf struct {
Expand Down
14 changes: 14 additions & 0 deletions config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,18 @@ func postInit() {
logrus.Warn("email.mail_subject_to_admin 配置项已废弃,请使用 admin_notify.email.mail_subject 代替")
Instance.AdminNotify.Email.MailSubject = Instance.Email.MailSubjectToAdmin
}

// 管理员邮件通知配置继承
if Instance.AdminNotify.Email.MailSubject == "" {
if Instance.AdminNotify.NotifySubject != "" {
Instance.AdminNotify.Email.MailSubject = Instance.AdminNotify.NotifySubject
} else if Instance.Email.MailSubject != "" {
Instance.AdminNotify.Email.MailSubject = Instance.Email.MailSubject
}
}

// 默认待审模式下开启管理员通知嘈杂模式,保证管理员能看到待审核文章
if Instance.Moderator.PendingDefault {
Instance.AdminNotify.NoiseMode = true
}
}
4 changes: 4 additions & 0 deletions http/admin_comment_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func RenotifyWhenPendingModified(comment *model.Comment) {
return // 回复对象是管理员,则不再发送通知,因为已经看到了
}

if comment.UserID == pComment.UserID {
return // 自己回复自己,不通知
}

notify := model.FindCreateNotify(pComment.UserID, comment.ID)
if notify.IsEmailed {
return // 邮件已经发送过,则不再重复发送
Expand Down
51 changes: 15 additions & 36 deletions lib/email/email.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package email

import (
"time"

"github.com/ArtalkJS/ArtalkGo/config"
"github.com/ArtalkJS/ArtalkGo/model"
"github.com/sirupsen/logrus"
)

func AsyncSendTo(subject string, body string, toAddr string) {
Expand All @@ -24,48 +27,24 @@ func AsyncSend(notify *model.Notify) {
return
}

comment := notify.FetchComment()
parentComment := notify.GetParentComment()

from := comment.ToCookedForEmail()
to := parentComment.ToCookedForEmail()

fromName := RenderCommon(config.Instance.Email.SendName, notify, from, to)
subject := RenderCommon(config.Instance.Email.MailSubject, notify, from, to)
body := RenderEmailTpl(notify, from, to)

AddToQueue(Email{
FromAddr: config.Instance.Email.SendAddr,
FromName: fromName,
ToAddr: to.Email,
Subject: subject,
Body: body,
LinkedNotify: notify,
})
}

func AsyncSendToAdmin(notify *model.Notify, admin *model.User) {
if !config.Instance.Email.Enabled {
return
}
receiveUser := notify.FetchUser()

comment := notify.FetchComment()
from := comment.ToCookedForEmail()
to := model.CookedCommentForEmail{
Nick: admin.Name,
Email: admin.Email,
mailBody := RenderEmailBody(notify)
mailSubject := ""
if !receiveUser.IsAdmin {
mailSubject = RenderCommon(config.Instance.Email.MailSubject, notify)
} else {
mailSubject = RenderCommon(config.Instance.AdminNotify.Email.MailSubject, notify)
}

fromName := RenderCommon(config.Instance.Email.SendName, notify, from, to)
subject := RenderCommon(config.Instance.AdminNotify.Email.MailSubject, notify, from, to)
body := RenderEmailTpl(notify, from, to)
logrus.Debug(time.Now(), " "+receiveUser.Email)

AddToQueue(Email{
FromAddr: config.Instance.Email.SendAddr,
FromName: fromName,
ToAddr: admin.Email,
Subject: subject,
Body: body,
FromName: RenderCommon(config.Instance.Email.SendName, notify),
ToAddr: receiveUser.Email,
Subject: mailSubject,
Body: mailBody,
LinkedNotify: notify,
})
}
159 changes: 104 additions & 55 deletions lib/email/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,36 @@ import (
"github.com/ArtalkJS/ArtalkGo/pkged"
)

func RenderEmailTpl(notify *model.Notify, from model.CookedCommentForEmail, to model.CookedCommentForEmail) string {
tplName := config.Instance.Email.MailTpl
tpl := ""
if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) {
tpl = GetInternalEmailTpl(tplName)
} else {
tpl = GetExternalEmailTpl(tplName)
func RenderCommon(str string, notify *model.Notify) string {
fromComment := notify.FetchComment()
from := fromComment.ToCookedForEmail()

toComment := notify.GetParentComment()
to := toComment.ToCookedForEmail()

toUser := notify.FetchUser()

cf := CommonFields{
From: from,
To: to,
Comment: from,
ParentComment: to,

Nick: toUser.Name,
Content: to.Content,
ReplyNick: from.Nick,
ReplyContent: from.Content,
PageTitle: from.Page.Title,
PageURL: from.Page.URL,
SiteName: from.SiteName,
SiteURL: from.Site.FirstUrl,

LinkToReply: notify.GetReadLink(),
}

tpl = RenderCommon(tpl, notify, from, to)
flat := lib.StructToFlatDotMap(&cf)

return tpl
return ReplaceAllMustache(str, flat)
}

type CommonFields struct {
Expand All @@ -49,33 +67,90 @@ type CommonFields struct {
LinkToReply string `json:"link_to_reply"`
}

func RenderCommon(str string, notify *model.Notify, from model.CookedCommentForEmail, to model.CookedCommentForEmail) string {
cf := CommonFields{
From: from,
To: to,
Comment: from,
ParentComment: to,
// 替换 {{ key }} 为 val
func ReplaceAllMustache(data string, dict map[string]interface{}) string {
r := regexp.MustCompile(`{{\s*(.*?)\s*}}`)
return r.ReplaceAllStringFunc(data, func(m string) string {
key := r.FindStringSubmatch(m)[1]
if val, isExist := dict[key]; isExist {
return GetPurifiedValue(key, val)
}

Nick: to.Nick,
Content: to.Content,
ReplyNick: from.Nick,
ReplyContent: from.Content,
PageTitle: from.Page.Title,
PageURL: from.Page.URL,
SiteName: from.SiteName,
SiteURL: from.Site.FirstUrl,
return m
})
}

LinkToReply: notify.GetReadLink(),
// 净化文本,防止 XSS
func GetPurifiedValue(k string, v interface{}) string {
val := fmt.Sprintf("%v", v)

// 白名单
ignoreEscapeKeys := []string{"reply_content", "content", "link_to_reply"}
if lib.ContainsStr(ignoreEscapeKeys, k) ||
strings.HasSuffix(k, ".content") || // 排除 model.CookedComment.content
strings.HasSuffix(k, ".content_raw") {
return val
}

flat := lib.StructToFlatDotMap(&cf)
val = html.EscapeString(val)
return val
}

return ReplaceAllMustache(str, flat)
// 渲染邮件 Body 内容
func RenderEmailBody(notify *model.Notify) string {
tplName := config.Instance.Email.MailTpl
if tplName == "" {
tplName = "default"
}

tpl := ""
if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) {
tpl = GetInternalEmailTpl(tplName)
} else {
tpl = GetExternalTpl(tplName)
}

tpl = RenderCommon(tpl, notify)

return tpl
}

// 获取内建模版
// 渲染管理员推送 Body 内容
func RenderNotifyBody(notify *model.Notify) string {
tplName := config.Instance.AdminNotify.NotifyTpl
if tplName == "" {
tplName = "default"
}

tpl := ""
if _, err := os.Stat(tplName); errors.Is(err, os.ErrNotExist) {
tpl = GetInternalNotifyTpl(tplName)
} else {
tpl = GetExternalTpl(tplName)
}

tpl = RenderCommon(tpl, notify)

return tpl
}

// 获取内建邮件模版
func GetInternalEmailTpl(tplName string) string {
filename := fmt.Sprintf("/email-tpl/%s.html", tplName)
return GetInternalTpl("email-tpl", tplName)
}

// 获取内建通知模版
func GetInternalNotifyTpl(tplName string) string {
if tplName == "default" {
return "@{{reply_nick}}:\n\n{{comment.content_raw}}\n\n{{link_to_reply}}"
}

return GetInternalTpl("notify-tpl", tplName)
}

// 获取内建模版
func GetInternalTpl(basePath string, tplName string) string {
filename := fmt.Sprintf("/%s/%s.html", basePath, tplName)
f, err := pkged.Open(filename)
if err != nil {
return ""
Expand All @@ -91,37 +166,11 @@ func GetInternalEmailTpl(tplName string) string {
}

// 获取外置模版
func GetExternalEmailTpl(filename string) string {
func GetExternalTpl(filename string) string {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}

return string(buf)
}

// 替换 {{ key }} 为 val
func ReplaceAllMustache(data string, dict map[string]interface{}) string {
r := regexp.MustCompile(`{{\s*(.*?)\s*}}`)
return r.ReplaceAllStringFunc(data, func(m string) string {
key := r.FindStringSubmatch(m)[1]
if val, isExist := dict[key]; isExist {
return GetPurifiedValue(key, val)
}

return m
})
}

// 净化文本,防止 XSS
func GetPurifiedValue(k string, v interface{}) string {
val := fmt.Sprintf("%v", v)

// 去除标签数据,防止数据泄漏
if k == "reply_content" || strings.HasSuffix(k, ".content") { // 排除 model.CookedComment.content
return val
}

val = html.EscapeString(val)
return val
}
Loading

0 comments on commit d5b67a2

Please sign in to comment.