feat: added modules

This commit is contained in:
Marvin Zhang
2024-06-14 15:42:50 +08:00
parent f1833fed21
commit 0b67fd9ece
626 changed files with 60104 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package notification
import (
"github.com/crawlab-team/crawlab/core/interfaces"
"github.com/crawlab-team/crawlab/core/models/delegate"
"github.com/crawlab-team/crawlab/core/models/models"
"github.com/gavv/httpexpect/v2"
"github.com/spf13/viper"
"go.mongodb.org/mongo-driver/bson/primitive"
"net/http/httptest"
"testing"
)
func init() {
viper.Set("mongo.db", "crawlab_test")
var err error
T, err = NewTest()
if err != nil {
panic(err)
}
}
type Test struct {
svc *Service
svr *httptest.Server
// test data
TestNode interfaces.Node
TestSpider interfaces.Spider
TestTask interfaces.Task
TestTaskStat interfaces.TaskStat
}
func (t *Test) Setup(t2 *testing.T) {
_ = t.svc.Start()
t2.Cleanup(t.Cleanup)
}
func (t *Test) Cleanup() {
_ = t.svc.Stop()
}
func (t *Test) NewExpect(t2 *testing.T) (e *httpexpect.Expect) {
e = httpexpect.New(t2, t.svr.URL)
return e
}
var T *Test
func NewTest() (res *Test, err error) {
// test
t := &Test{
svc: NewService(),
}
// test node
t.TestNode = &models.Node{Id: primitive.NewObjectID(), Name: "test-node"}
_ = delegate.NewModelDelegate(t.TestNode).Add()
// test spider
t.TestSpider = &models.Spider{Id: primitive.NewObjectID(), Name: "test-spider"}
_ = delegate.NewModelDelegate(t.TestSpider).Add()
// test task
t.TestTask = &models.Task{Id: primitive.NewObjectID(), SpiderId: t.TestSpider.GetId(), NodeId: t.TestNode.GetId()}
_ = delegate.NewModelDelegate(t.TestTask).Add()
// test task stat
t.TestTaskStat = &models.TaskStat{Id: t.TestTask.GetId()}
_ = delegate.NewModelDelegate(t.TestTaskStat).Add()
return t, nil
}

View File

@@ -0,0 +1,10 @@
package notification
const (
TypeMail = "mail"
TypeMobile = "mobile"
)
const (
SettingsColName = "notification_settings"
)

178
core/notification/mail.go Normal file
View File

@@ -0,0 +1,178 @@
package notification
import (
"errors"
"github.com/apex/log"
"github.com/crawlab-team/crawlab/core/models/models"
"github.com/matcornic/hermes/v2"
"gopkg.in/gomail.v2"
"net/mail"
"runtime/debug"
"strconv"
"strings"
)
func SendMail(m *models.NotificationSettingMail, to, cc, title, content string) error {
// theme
theme := new(MailThemeFlat)
// hermes instance
h := hermes.Hermes{
Theme: theme,
Product: hermes.Product{
Logo: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGZpbGw9Im5vbmUiPgogICAgICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iMTMwIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQwIiBzdHJva2U9IiM0MDllZmYiPgogICAgICAgIDwvY2lyY2xlPgogICAgICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iMTEwIiBmaWxsPSJ3aGl0ZSI+CiAgICAgICAgPC9jaXJjbGU+CiAgICAgICAgPGNpcmNsZSBjeD0iMTUwIiBjeT0iMTUwIiByPSI3MCIgZmlsbD0iIzQwOWVmZiI+CiAgICAgICAgPC9jaXJjbGU+CiAgICAgICAgPHBhdGggZD0iCiAgICAgICAgICAgIE0gMTUwLDE1MAogICAgICAgICAgICBMIDI4MCwyMjUKICAgICAgICAgICAgQSAxNTAsMTUwIDkwIDAgMCAyODAsNzUKICAgICAgICAgICAgIiBmaWxsPSIjNDA5ZWZmIj4KICAgICAgICA8L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPgo=",
Name: "Crawlab",
Copyright: "© 2024 Crawlab-Team",
},
}
// config
port, _ := strconv.Atoi(m.Port)
password := m.Password // test password: ALWVDPRHBEXOENXD
SMTPUser := m.User
smtpConfig := smtpAuthentication{
Server: m.Server,
Port: port,
SenderEmail: m.SenderEmail,
SenderIdentity: m.SenderIdentity,
SMTPPassword: password,
SMTPUser: SMTPUser,
}
options := sendOptions{
To: to,
Cc: cc,
Subject: title,
}
// add style
content += theme.GetStyle()
// markdown
markdown := hermes.Markdown(content + GetFooter())
// email instance
email := hermes.Email{
Body: hermes.Body{
Signature: "Happy Crawling ☺",
FreeMarkdown: markdown,
},
}
// generate html
html, err := h.GenerateHTML(email)
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return err
}
// generate text
text, err := h.GeneratePlainText(email)
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return err
}
// send the email
if err := send(smtpConfig, options, html, text); err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return err
}
return nil
}
type smtpAuthentication struct {
Server string
Port int
SenderEmail string
SenderIdentity string
SMTPUser string
SMTPPassword string
}
// sendOptions are options for sending an email
type sendOptions struct {
To string
Subject string
Cc string
}
// send email
func send(smtpConfig smtpAuthentication, options sendOptions, htmlBody string, txtBody string) error {
if smtpConfig.Server == "" {
return errors.New("SMTP server config is empty")
}
if smtpConfig.Port == 0 {
return errors.New("SMTP port config is empty")
}
if smtpConfig.SMTPUser == "" {
return errors.New("SMTP user is empty")
}
if smtpConfig.SenderIdentity == "" {
return errors.New("SMTP sender identity is empty")
}
if smtpConfig.SenderEmail == "" {
return errors.New("SMTP sender email is empty")
}
if options.To == "" {
return errors.New("no receiver emails configured")
}
from := mail.Address{
Name: smtpConfig.SenderIdentity,
Address: smtpConfig.SenderEmail,
}
var toList []string
if strings.Contains(options.To, ";") {
toList = strings.Split(options.To, ";")
// trim space
for i, to := range toList {
toList[i] = strings.TrimSpace(to)
}
} else {
toList = []string{options.To}
}
m := gomail.NewMessage()
m.SetHeader("From", from.String())
m.SetHeader("To", getRecipientList(options.To)...)
m.SetHeader("Subject", options.Subject)
if options.Cc != "" {
m.SetHeader("Cc", getRecipientList(options.Cc)...)
}
m.SetBody("text/plain", txtBody)
m.AddAlternative("text/html", htmlBody)
d := gomail.NewDialer(smtpConfig.Server, smtpConfig.Port, smtpConfig.SMTPUser, smtpConfig.SMTPPassword)
return d.DialAndSend(m)
}
func getRecipientList(value string) (values []string) {
if strings.Contains(value, ";") {
values = strings.Split(value, ";")
// trim space
for i, v := range values {
values[i] = strings.TrimSpace(v)
}
} else {
values = []string{value}
}
return values
}
func GetFooter() string {
return `
[Github](https://github.com/crawlab-team/crawlab) | [Documentation](http://docs.crawlab.cn) | [Docker](https://hub.docker.com/r/tikazyq/crawlab)
`
}

View File

@@ -0,0 +1,8 @@
package notification
import "github.com/matcornic/hermes/v2"
type MailTheme interface {
hermes.Theme
GetStyle() string
}

View File

@@ -0,0 +1,287 @@
package notification
// MailThemeFlat is a theme
type MailThemeFlat struct{}
// Name returns the name of the flat theme
func (dt *MailThemeFlat) Name() string {
return "flat"
}
// HTMLTemplate returns a Golang template that will generate an HTML email.
func (dt *MailThemeFlat) HTMLTemplate() string {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body dir="{{.Hermes.TextDirection}}">
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="content">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0">
<!-- Logo -->
<tr>
<td class="email-masthead">
<a class="email-masthead_name" href="{{.Hermes.Product.Link}}" target="_blank">
{{ if .Hermes.Product.Logo }}
<img src="{{.Hermes.Product.Logo | url }}" class="email-logo" style="height: 48px"/>
<span style="font-size:36px;font-weight:600;margin-left:12px;color:#409eff">{{ .Hermes.Product.Name}} </span>
{{ else }}
{{ .Hermes.Product.Name }}
{{ end }}
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="100%">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0">
<!-- Body content -->
<tr>
<td class="content-cell">
{{ if (ne .Email.Body.FreeMarkdown "") }}
{{ .Email.Body.FreeMarkdown.ToHTML }}
{{ else }}
{{ with .Email.Body.Dictionary }}
{{ if gt (len .) 0 }}
<dl class="body-dictionary">
{{ range $entry := . }}
<dt>{{ $entry.Key }}:</dt>
<dd>{{ $entry.Value }}</dd>
{{ end }}
</dl>
{{ end }}
{{ end }}
<!-- Table -->
{{ with .Email.Body.Table }}
{{ $data := .Data }}
{{ $columns := .Columns }}
{{ if gt (len $data) 0 }}
<table class="data-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td colspan="2">
<table class="data-table" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ $col := index $data 0 }}
{{ range $entry := $col }}
<th
{{ with $columns }}
{{ $width := index .CustomWidth $entry.Key }}
{{ with $width }}
width="{{ . }}"
{{ end }}
{{ $align := index .CustomAlignment $entry.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
<p>{{ $entry.Key }}</p>
</th>
{{ end }}
</tr>
{{ range $row := $data }}
<tr>
{{ range $cell := $row }}
<td
{{ with $columns }}
{{ $align := index .CustomAlignment $cell.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
{{ $cell.Value }}
</td>
{{ end }}
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
{{ end }}
{{ end }}
<!-- Action -->
{{ with .Email.Body.Actions }}
{{ if gt (len .) 0 }}
{{ range $action := . }}
<p>{{ $action.Instructions }}</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<div>
<a href="{{ $action.Button.Link }}" class="button" style="background-color: {{ $action.Button.Color }}; color: {{ $action.Button.TextColor }}" target="_blank">
{{ $action.Button.Text }}
</a>
</div>
</td>
</tr>
</table>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ with .Email.Body.Outros }}
{{ if gt (len .) 0 }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ end }}
<p>
{{.Email.Body.Signature}}
</p>
{{ if (eq .Email.Body.FreeMarkdown "") }}
{{ with .Email.Body.Actions }}
<table class="body-sub">
<tbody>
{{ range $action := . }}
<tr>
<td>
<p class="sub">{{$.Hermes.Product.TroubleText | replace "{ACTION}" $action.Button.Text}}</p>
<p class="sub"><a href="{{ $action.Button.Link }}">{{ $action.Button.Link }}</a></p>
</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}
{{ end }}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0">
<tr>
<td class="content-cell">
<p class="sub center">
{{.Hermes.Product.Copyright}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`
}
// PlainTextTemplate returns a Golang template that will generate an plain text email.
func (dt *MailThemeFlat) PlainTextTemplate() string {
return `{{ with .Email.Body.Intros }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ if (ne .Email.Body.FreeMarkdown "") }}
{{ .Email.Body.FreeMarkdown.ToHTML }}
{{ else }}
{{ with .Email.Body.Dictionary }}
<ul>
{{ range $entry := . }}
<li>{{ $entry.Key }}: {{ $entry.Value }}</li>
{{ end }}
</ul>
{{ end }}
{{ with .Email.Body.Table }}
{{ $data := .Data }}
{{ $columns := .Columns }}
{{ if gt (len $data) 0 }}
<table class="data-table" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ $col := index $data 0 }}
{{ range $entry := $col }}
<th>{{ $entry.Key }} </th>
{{ end }}
</tr>
{{ range $row := $data }}
<tr>
{{ range $cell := $row }}
<td>
{{ $cell.Value }}
</td>
{{ end }}
</tr>
{{ end }}
</table>
{{ end }}
{{ end }}
{{ with .Email.Body.Actions }}
{{ range $action := . }}
<p>{{ $action.Instructions }} {{ $action.Button.Link }}</p>
{{ end }}
{{ end }}
{{ end }}
{{ with .Email.Body.Outros }}
{{ range $line := . }}
<p>{{ $line }}<p>
{{ end }}
{{ end }}
<p>{{.Email.Body.Signature}},<br>{{.Hermes.Product.Name}} - {{.Hermes.Product.Link}}</p>
<p>{{.Hermes.Product.Copyright}}</p>
`
}
func (dt *MailThemeFlat) GetStyle() string {
return `
<style>
.content-cell table {
width: 100%;
border-collapse: collapse;
}
.content-cell table,
.content-cell th,
.content-cell td {
border: 1px solid #EDEFF2;
}
.content-cell th,
.content-cell td {
padding: 10px;
font-size: 14px;
line-height: 18px;
}
.content-cell th {
background: #409eff;
color: white;
}
.content-cell td {
color: #606266;
}
.content-cell p {
color: #606266;
}
.content-cell a {
color: #409eff;
}
.email-masthead .email-masthead_name {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: #409eff;
margin-bottom: 20px;
}
</style>
`
}

View File

@@ -0,0 +1,62 @@
package notification
import (
"errors"
"github.com/crawlab-team/go-trace"
"github.com/imroc/req"
"strings"
)
type ResBody struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
func SendMobileNotification(webhook string, title string, content string) error {
// request header
header := req.Header{
"Content-Type": "application/json; charset=utf-8",
}
// request data
data := req.Param{
"msgtype": "markdown",
"markdown": req.Param{
"title": title,
"text": content,
"content": content,
},
"at": req.Param{
"atMobiles": []string{},
"isAtAll": false,
},
"text": content,
}
if strings.Contains(strings.ToLower(webhook), "feishu") {
data = req.Param{
"msg_type": "text",
"content": req.Param{
"text": content,
},
}
}
// perform request
res, err := req.Post(webhook, header, req.BodyJSON(&data))
if err != nil {
return trace.TraceError(err)
}
// parse response
var resBody ResBody
if err := res.ToJSON(&resBody); err != nil {
return trace.TraceError(err)
}
// validate response code
if resBody.ErrCode != 0 {
return errors.New(resBody.ErrMsg)
}
return nil
}

View File

@@ -0,0 +1,32 @@
package notification
import "go.mongodb.org/mongo-driver/bson/primitive"
type NotificationSetting struct {
Id primitive.ObjectID `json:"_id" bson:"_id"`
Type string `json:"type" bson:"type"`
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Enabled bool `json:"enabled" bson:"enabled"`
Global bool `json:"global" bson:"global"`
Title string `json:"title,omitempty" bson:"title,omitempty"`
Template string `json:"template,omitempty" bson:"template,omitempty"`
TaskTrigger string `json:"task_trigger" bson:"task_trigger"`
Mail NotificationSettingMail `json:"mail,omitempty" bson:"mail,omitempty"`
Mobile NotificationSettingMobile `json:"mobile,omitempty" bson:"mobile,omitempty"`
}
type NotificationSettingMail struct {
Server string `json:"server" bson:"server"`
Port string `json:"port,omitempty" bson:"port,omitempty"`
User string `json:"user,omitempty" bson:"user,omitempty"`
Password string `json:"password,omitempty" bson:"password,omitempty"`
SenderEmail string `json:"sender_email,omitempty" bson:"sender_email,omitempty"`
SenderIdentity string `json:"sender_identity,omitempty" bson:"sender_identity,omitempty"`
To string `json:"to,omitempty" bson:"to,omitempty"`
Cc string `json:"cc,omitempty" bson:"cc,omitempty"`
}
type NotificationSettingMobile struct {
Webhook string `json:"webhook" bson:"webhook"`
}

View File

@@ -0,0 +1,8 @@
package notification
import "go.mongodb.org/mongo-driver/bson/primitive"
type SendPayload struct {
TaskId primitive.ObjectID `json:"task_id"`
Data string `json:"data"`
}

View File

@@ -0,0 +1,395 @@
package notification
import (
"github.com/apex/log"
mongo2 "github.com/crawlab-team/crawlab-db/mongo"
"github.com/crawlab-team/crawlab/core/constants"
"github.com/crawlab-team/crawlab/core/entity"
"github.com/crawlab-team/crawlab/core/models/models"
"github.com/crawlab-team/crawlab/core/models/service"
"github.com/crawlab-team/crawlab/core/utils"
parser "github.com/crawlab-team/template-parser"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type Service struct {
col *mongo2.Col // notification settings
modelSvc service.ModelService
}
func (svc *Service) Init() (err error) {
if !utils.IsPro() {
return nil
}
return nil
}
func (svc *Service) Start() (err error) {
// initialize data
if err := svc.initData(); err != nil {
return err
}
return nil
}
func (svc *Service) Stop() (err error) {
return nil
}
func (svc *Service) initData() (err error) {
total, err := svc.col.Count(nil)
if err != nil {
return err
}
if total > 0 {
return nil
}
// data to initialize
settings := []NotificationSetting{
{
Id: primitive.NewObjectID(),
Type: TypeMail,
Enabled: true,
Name: "任务通知(邮件)",
Description: "这是默认的邮件通知。您可以使用您自己的设置进行编辑。",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] 爬虫任务更新: {{$.status}}",
Template: `尊敬的 {{$.user.username}},
请查看下面的任务数据。
|键|值|
|:-:|:--|
|任务状态|{{$.status}}|
|任务优先级|{{$.priority}}|
|任务模式|{{$.mode}}|
|执行命令|{{$.cmd}}|
|执行参数|{{$.param}}|
|错误信息|{{$.error}}|
|节点|{{$.node.name}}|
|爬虫|{{$.spider.name}}|
|项目|{{$.spider.project.name}}|
|定时任务|{{$.schedule.name}}|
|结果数|{{$.:task_stat.result_count}}|
|等待时间(秒)|{#{{$.:task_stat.wait_duration}}/1000#}|
|运行时间(秒)|{#{{$.:task_stat.runtime_duration}}/1000#}|
|总时间(秒)|{#{{$.:task_stat.total_duration}}/1000#}|
|平均结果数/秒|{#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}|
`,
Mail: NotificationSettingMail{
Server: "smtp.163.com",
Port: "465",
To: "{{$.user[create].email}}",
},
},
{
Id: primitive.NewObjectID(),
Type: TypeMail,
Enabled: true,
Name: "Task Change (Mail)",
Description: "This is the default mail notification. You can edit it with your own settings",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] Task Update: {{$.status}}",
Template: `Dear {{$.user.username}},
Please find the task data as below.
|Key|Value|
|:-:|:--|
|Task Status|{{$.status}}|
|Task Priority|{{$.priority}}|
|Task Mode|{{$.mode}}|
|Task Command|{{$.cmd}}|
|Task Params|{{$.param}}|
|Error Message|{{$.error}}|
|Node|{{$.node.name}}|
|Spider|{{$.spider.name}}|
|Project|{{$.spider.project.name}}|
|Schedule|{{$.schedule.name}}|
|Result Count|{{$.:task_stat.result_count}}|
|Wait Duration (sec)|{#{{$.:task_stat.wait_duration}}/1000#}|
|Runtime Duration (sec)|{#{{$.:task_stat.runtime_duration}}/1000#}|
|Total Duration (sec)|{#{{$.:task_stat.total_duration}}/1000#}|
|Avg Results / Sec|{#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}|
`,
Mail: NotificationSettingMail{
Server: "smtp.163.com",
Port: "465",
To: "{{$.user[create].email}}",
},
},
{
Id: primitive.NewObjectID(),
Type: TypeMobile,
Enabled: true,
Name: "任务通知(移动端)",
Description: "这是默认的手机通知。您可以使用您自己的设置进行编辑。",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] 任务更新: {{$.status}}",
Template: `尊敬的 {{$.user.username}},
请查看下面的任务数据。
- **任务状态**: {{$.status}}
- **任务优先级**: {{$.priority}}
- **任务模式**: {{$.mode}}
- **执行命令**: {{$.cmd}}
- **执行参数**: {{$.param}}
- **错误信息**: {{$.error}}
- **节点**: {{$.node.name}}
- **爬虫**: {{$.spider.name}}
- **项目**: {{$.spider.project.name}}
- **定时任务**: {{$.schedule.name}}
- **结果数**: {{$.:task_stat.result_count}}
- **等待时间(秒)**: {#{{$.:task_stat.wait_duration}}/1000#}
- **运行时间(秒)**: {#{{$.:task_stat.runtime_duration}}/1000#}
- **总时间(秒)**: {#{{$.:task_stat.total_duration}}/1000#}
- **平均结果数/秒**: {#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}`,
Mobile: NotificationSettingMobile{},
},
{
Id: primitive.NewObjectID(),
Type: TypeMobile,
Enabled: true,
Name: "Task Change (Mobile)",
Description: "This is the default mobile notification. You can edit it with your own settings",
TaskTrigger: constants.NotificationTriggerTaskError,
Title: "[Crawlab] Task Update: {{$.status}}",
Template: `Dear {{$.user.username}},
Please find the task data as below.
- **Task Status**: {{$.status}}
- **Task Priority**: {{$.priority}}
- **Task Mode**: {{$.mode}}
- **Task Command**: {{$.cmd}}
- **Task Params**: {{$.param}}
- **Error Message**: {{$.error}}
- **Node**: {{$.node.name}}
- **Spider**: {{$.spider.name}}
- **Project**: {{$.spider.project.name}}
- **Schedule**: {{$.schedule.name}}
- **Result Count**: {{$.:task_stat.result_count}}
- **Wait Duration (sec)**: {#{{$.:task_stat.wait_duration}}/1000#}
- **Runtime Duration (sec)**: {#{{$.:task_stat.runtime_duration}}/1000#}
- **Total Duration (sec)**: {#{{$.:task_stat.total_duration}}/1000#}
- **Avg Results / Sec**: {#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}`,
Mobile: NotificationSettingMobile{},
},
}
var data []interface{}
for _, s := range settings {
data = append(data, s)
}
_, err = svc.col.InsertMany(data)
if err != nil {
return err
}
return nil
}
func (svc *Service) Send(s NotificationSetting, entity bson.M) (err error) {
switch s.Type {
case TypeMail:
return svc.SendMail(s, entity)
case TypeMobile:
return svc.SendMobile(s, entity)
}
return nil
}
func (svc *Service) SendMail(s NotificationSetting, entity bson.M) (err error) {
// to
to, err := parser.Parse(s.Mail.To, entity)
if err != nil {
log.Warnf("parsing 'to' error: %v", err)
}
if to == "" {
return nil
}
// cc
cc, err := parser.Parse(s.Mail.Cc, entity)
if err != nil {
log.Warnf("parsing 'cc' error: %v", err)
}
// title
title, err := parser.Parse(s.Title, entity)
if err != nil {
log.Warnf("parsing 'title' error: %v", err)
}
// content
content, err := parser.Parse(s.Template, entity)
if err != nil {
log.Warnf("parsing 'content' error: %v", err)
}
// send mail
if err := SendMail(&models.NotificationSettingMail{
Server: s.Mail.Server,
Port: s.Mail.Port,
User: s.Mail.User,
Password: s.Mail.Password,
SenderEmail: s.Mail.SenderEmail,
SenderIdentity: s.Mail.SenderIdentity,
To: s.Mail.To,
Cc: s.Mail.Cc,
}, to, cc, title, content); err != nil {
return err
}
return nil
}
func (svc *Service) SendMobile(s NotificationSetting, entity bson.M) (err error) {
// webhook
webhook, err := parser.Parse(s.Mobile.Webhook, entity)
if err != nil {
log.Warnf("parsing 'webhook' error: %v", err)
}
if webhook == "" {
return nil
}
// title
title, err := parser.Parse(s.Title, entity)
if err != nil {
log.Warnf("parsing 'title' error: %v", err)
}
// content
content, err := parser.Parse(s.Template, entity)
if err != nil {
log.Warnf("parsing 'content' error: %v", err)
}
// send
if err := SendMobileNotification(webhook, title, content); err != nil {
return err
}
return nil
}
func (svc *Service) GetSettingList(query bson.M, pagination *entity.Pagination, sort bson.D) (res []NotificationSetting, total int, err error) {
// options
var options *mongo2.FindOptions
if pagination != nil || sort != nil {
options = new(mongo2.FindOptions)
if pagination != nil {
options.Skip = pagination.Size * (pagination.Page - 1)
options.Limit = pagination.Size
}
if sort != nil {
options.Sort = sort
}
}
// get list
var list []NotificationSetting
if err := svc.col.Find(query, options).All(&list); err != nil {
if err.Error() == mongo.ErrNoDocuments.Error() {
return nil, 0, nil
} else {
return nil, 0, err
}
}
// total count
total, err = svc.col.Count(query)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
func (svc *Service) GetSetting(id primitive.ObjectID) (res *NotificationSetting, err error) {
var s NotificationSetting
if err := svc.col.FindId(id).One(&s); err != nil {
return nil, err
}
return &s, nil
}
func (svc *Service) PosSetting(s *NotificationSetting) (err error) {
s.Id = primitive.NewObjectID()
if _, err := svc.col.Insert(s); err != nil {
return err
}
return nil
}
func (svc *Service) PutSetting(id primitive.ObjectID, s NotificationSetting) (err error) {
if err := svc.col.ReplaceId(id, s); err != nil {
return err
}
return nil
}
func (svc *Service) DeleteSetting(id primitive.ObjectID) (err error) {
if err := svc.col.DeleteId(id); err != nil {
return err
}
return nil
}
func (svc *Service) EnableSetting(id primitive.ObjectID) (err error) {
return svc._toggleSettingFunc(true)(id)
}
func (svc *Service) DisableSetting(id primitive.ObjectID) (err error) {
return svc._toggleSettingFunc(false)(id)
}
func (svc *Service) _toggleSettingFunc(value bool) func(id primitive.ObjectID) error {
return func(id primitive.ObjectID) (err error) {
var s NotificationSetting
if err := svc.col.FindId(id).One(&s); err != nil {
return err
}
s.Enabled = value
if err := svc.col.ReplaceId(id, s); err != nil {
return err
}
return nil
}
}
func NewService() *Service {
// service
svc := &Service{
col: mongo2.GetMongoCol(SettingsColName),
}
// model service
modelSvc, err := service.GetService()
if err != nil {
panic(err)
}
svc.modelSvc = modelSvc
if err := svc.Init(); err != nil {
panic(err)
}
return svc
}
var _service *Service
func GetService() *Service {
if _service == nil {
_service = NewService()
}
return _service
}

View File

@@ -0,0 +1,19 @@
package notification
import (
"net/http"
"testing"
"time"
)
func TestService_sendMobile(t *testing.T) {
T.Setup(t)
e := T.NewExpect(t)
time.Sleep(1 * time.Second)
data := map[string]interface{}{
"task_id": T.TestTask.GetId().Hex(),
}
e.POST("/send/mobile").WithJSON(data).
Expect().Status(http.StatusOK)
}

View File

@@ -0,0 +1,362 @@
package notification
import (
"github.com/apex/log"
mongo2 "github.com/crawlab-team/crawlab-db/mongo"
"github.com/crawlab-team/crawlab/core/constants"
"github.com/crawlab-team/crawlab/core/entity"
"github.com/crawlab-team/crawlab/core/models/models"
"github.com/crawlab-team/crawlab/core/models/service"
parser "github.com/crawlab-team/template-parser"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type ServiceV2 struct {
}
func (svc *ServiceV2) Start() (err error) {
// initialize data
if err := svc.initData(); err != nil {
return err
}
return nil
}
func (svc *ServiceV2) Stop() (err error) {
return nil
}
func (svc *ServiceV2) initData() (err error) {
total, err := service.NewModelServiceV2[models.NotificationSettingV2]().Count(nil)
if err != nil {
return err
}
if total > 0 {
return nil
}
// data to initialize
settings := []models.NotificationSettingV2{
{
Id: primitive.NewObjectID(),
Type: TypeMail,
Enabled: true,
Name: "任务通知(邮件)",
Description: "这是默认的邮件通知。您可以使用您自己的设置进行编辑。",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] 爬虫任务更新: {{$.status}}",
Template: `尊敬的 {{$.user.username}},
请查看下面的任务数据。
|键|值|
|:-:|:--|
|任务状态|{{$.status}}|
|任务优先级|{{$.priority}}|
|任务模式|{{$.mode}}|
|执行命令|{{$.cmd}}|
|执行参数|{{$.param}}|
|错误信息|{{$.error}}|
|节点|{{$.node.name}}|
|爬虫|{{$.spider.name}}|
|项目|{{$.spider.project.name}}|
|定时任务|{{$.schedule.name}}|
|结果数|{{$.:task_stat.result_count}}|
|等待时间(秒)|{#{{$.:task_stat.wait_duration}}/1000#}|
|运行时间(秒)|{#{{$.:task_stat.runtime_duration}}/1000#}|
|总时间(秒)|{#{{$.:task_stat.total_duration}}/1000#}|
|平均结果数/秒|{#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}|
`,
Mail: models.NotificationSettingMail{
Server: "smtp.163.com",
Port: "465",
To: "{{$.user[create].email}}",
},
},
{
Id: primitive.NewObjectID(),
Type: TypeMail,
Enabled: true,
Name: "Task Change (Mail)",
Description: "This is the default mail notification. You can edit it with your own settings",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] Task Update: {{$.status}}",
Template: `Dear {{$.user.username}},
Please find the task data as below.
|Key|Value|
|:-:|:--|
|Task Status|{{$.status}}|
|Task Priority|{{$.priority}}|
|Task Mode|{{$.mode}}|
|Task Command|{{$.cmd}}|
|Task Params|{{$.param}}|
|Error Message|{{$.error}}|
|Node|{{$.node.name}}|
|Spider|{{$.spider.name}}|
|Project|{{$.spider.project.name}}|
|Schedule|{{$.schedule.name}}|
|Result Count|{{$.:task_stat.result_count}}|
|Wait Duration (sec)|{#{{$.:task_stat.wait_duration}}/1000#}|
|Runtime Duration (sec)|{#{{$.:task_stat.runtime_duration}}/1000#}|
|Total Duration (sec)|{#{{$.:task_stat.total_duration}}/1000#}|
|Avg Results / Sec|{#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}|
`,
Mail: models.NotificationSettingMail{
Server: "smtp.163.com",
Port: "465",
To: "{{$.user[create].email}}",
},
},
{
Id: primitive.NewObjectID(),
Type: TypeMobile,
Enabled: true,
Name: "任务通知(移动端)",
Description: "这是默认的手机通知。您可以使用您自己的设置进行编辑。",
TaskTrigger: constants.NotificationTriggerTaskFinish,
Title: "[Crawlab] 任务更新: {{$.status}}",
Template: `尊敬的 {{$.user.username}},
请查看下面的任务数据。
- **任务状态**: {{$.status}}
- **任务优先级**: {{$.priority}}
- **任务模式**: {{$.mode}}
- **执行命令**: {{$.cmd}}
- **执行参数**: {{$.param}}
- **错误信息**: {{$.error}}
- **节点**: {{$.node.name}}
- **爬虫**: {{$.spider.name}}
- **项目**: {{$.spider.project.name}}
- **定时任务**: {{$.schedule.name}}
- **结果数**: {{$.:task_stat.result_count}}
- **等待时间(秒)**: {#{{$.:task_stat.wait_duration}}/1000#}
- **运行时间(秒)**: {#{{$.:task_stat.runtime_duration}}/1000#}
- **总时间(秒)**: {#{{$.:task_stat.total_duration}}/1000#}
- **平均结果数/秒**: {#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}`,
Mobile: models.NotificationSettingMobile{},
},
{
Id: primitive.NewObjectID(),
Type: TypeMobile,
Enabled: true,
Name: "Task Change (Mobile)",
Description: "This is the default mobile notification. You can edit it with your own settings",
TaskTrigger: constants.NotificationTriggerTaskError,
Title: "[Crawlab] Task Update: {{$.status}}",
Template: `Dear {{$.user.username}},
Please find the task data as below.
- **Task Status**: {{$.status}}
- **Task Priority**: {{$.priority}}
- **Task Mode**: {{$.mode}}
- **Task Command**: {{$.cmd}}
- **Task Params**: {{$.param}}
- **Error Message**: {{$.error}}
- **Node**: {{$.node.name}}
- **Spider**: {{$.spider.name}}
- **Project**: {{$.spider.project.name}}
- **Schedule**: {{$.schedule.name}}
- **Result Count**: {{$.:task_stat.result_count}}
- **Wait Duration (sec)**: {#{{$.:task_stat.wait_duration}}/1000#}
- **Runtime Duration (sec)**: {#{{$.:task_stat.runtime_duration}}/1000#}
- **Total Duration (sec)**: {#{{$.:task_stat.total_duration}}/1000#}
- **Avg Results / Sec**: {#{{$.:task_stat.result_count}}/({{$.:task_stat.total_duration}}/1000)#}`,
Mobile: models.NotificationSettingMobile{},
},
}
_, err = service.NewModelServiceV2[models.NotificationSettingV2]().InsertMany(settings)
if err != nil {
return err
}
return nil
}
func (svc *ServiceV2) Send(s *models.NotificationSettingV2, entity bson.M) (err error) {
switch s.Type {
case TypeMail:
return svc.SendMail(s, entity)
case TypeMobile:
return svc.SendMobile(s, entity)
}
return nil
}
func (svc *ServiceV2) SendMail(s *models.NotificationSettingV2, entity bson.M) (err error) {
// to
to, err := parser.Parse(s.Mail.To, entity)
if err != nil {
log.Warnf("parsing 'to' error: %v", err)
}
if to == "" {
return nil
}
// cc
cc, err := parser.Parse(s.Mail.Cc, entity)
if err != nil {
log.Warnf("parsing 'cc' error: %v", err)
}
// title
title, err := parser.Parse(s.Title, entity)
if err != nil {
log.Warnf("parsing 'title' error: %v", err)
}
// content
content, err := parser.Parse(s.Template, entity)
if err != nil {
log.Warnf("parsing 'content' error: %v", err)
}
// send mail
if err := SendMail(&s.Mail, to, cc, title, content); err != nil {
return err
}
return nil
}
func (svc *ServiceV2) SendMobile(s *models.NotificationSettingV2, entity bson.M) (err error) {
// webhook
webhook, err := parser.Parse(s.Mobile.Webhook, entity)
if err != nil {
log.Warnf("parsing 'webhook' error: %v", err)
}
if webhook == "" {
return nil
}
// title
title, err := parser.Parse(s.Title, entity)
if err != nil {
log.Warnf("parsing 'title' error: %v", err)
}
// content
content, err := parser.Parse(s.Template, entity)
if err != nil {
log.Warnf("parsing 'content' error: %v", err)
}
// send
if err := SendMobileNotification(webhook, title, content); err != nil {
return err
}
return nil
}
func (svc *ServiceV2) GetSettingList(query bson.M, pagination *entity.Pagination, sort bson.D) (res []models.NotificationSettingV2, total int, err error) {
// options
var options *mongo2.FindOptions
if pagination != nil || sort != nil {
options = new(mongo2.FindOptions)
if pagination != nil {
options.Skip = pagination.Size * (pagination.Page - 1)
options.Limit = pagination.Size
}
if sort != nil {
options.Sort = sort
}
}
// get list
list, err := service.NewModelServiceV2[models.NotificationSettingV2]().GetMany(query, options)
if err != nil {
if err.Error() == mongo.ErrNoDocuments.Error() {
return nil, 0, nil
} else {
return nil, 0, err
}
}
// total count
total, err = service.NewModelServiceV2[models.NotificationSettingV2]().Count(query)
if err != nil {
return nil, 0, err
}
return list, total, nil
}
func (svc *ServiceV2) GetSetting(id primitive.ObjectID) (res *models.NotificationSettingV2, err error) {
s, err := service.NewModelServiceV2[models.NotificationSettingV2]().GetById(id)
if err != nil {
return nil, err
}
return s, nil
}
func (svc *ServiceV2) PosSetting(s *models.NotificationSettingV2) (err error) {
s.Id = primitive.NewObjectID()
_, err = service.NewModelServiceV2[models.NotificationSettingV2]().InsertOne(*s)
if err != nil {
return err
}
return nil
}
func (svc *ServiceV2) PutSetting(id primitive.ObjectID, s models.NotificationSettingV2) (err error) {
err = service.NewModelServiceV2[models.NotificationSettingV2]().ReplaceById(id, s)
if err != nil {
return err
}
return nil
}
func (svc *ServiceV2) DeleteSetting(id primitive.ObjectID) (err error) {
err = service.NewModelServiceV2[models.NotificationSettingV2]().DeleteById(id)
if err != nil {
return err
}
return nil
}
func (svc *ServiceV2) EnableSetting(id primitive.ObjectID) (err error) {
return svc._toggleSettingFunc(true)(id)
}
func (svc *ServiceV2) DisableSetting(id primitive.ObjectID) (err error) {
return svc._toggleSettingFunc(false)(id)
}
func (svc *ServiceV2) _toggleSettingFunc(value bool) func(id primitive.ObjectID) error {
return func(id primitive.ObjectID) (err error) {
s, err := service.NewModelServiceV2[models.NotificationSettingV2]().GetById(id)
if err != nil {
return err
}
s.Enabled = value
err = service.NewModelServiceV2[models.NotificationSettingV2]().ReplaceById(id, *s)
if err != nil {
return err
}
return nil
}
}
func NewServiceV2() *ServiceV2 {
// service
svc := &ServiceV2{}
return svc
}
var _serviceV2 *ServiceV2
func GetServiceV2() *ServiceV2 {
if _serviceV2 == nil {
_serviceV2 = NewServiceV2()
}
return _serviceV2
}