mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
303 lines
9.9 KiB
Go
303 lines
9.9 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/smtp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"vocat/internal/store"
|
|
)
|
|
|
|
type automaticTaskNotification struct {
|
|
Title string
|
|
Text string
|
|
Time time.Time
|
|
Task store.AutomaticTask
|
|
Run store.AutomaticTaskRun
|
|
}
|
|
|
|
func (s *Server) notifyAutomaticTask(ctx context.Context, task store.AutomaticTask, run store.AutomaticTaskRun) {
|
|
deviceLabel := task.DeviceID
|
|
if configured, err := s.store.Device(ctx, task.DeviceID); err == nil {
|
|
deviceLabel = firstNonEmpty(configured.Name, configured.ID)
|
|
}
|
|
status := "成功"
|
|
detail := firstNonEmpty(run.Output, "任务已完成")
|
|
if run.Status != "success" {
|
|
status = "失败"
|
|
detail = firstNonEmpty(run.Error, "未知错误")
|
|
}
|
|
taskType := map[string]string{"sms": "发送短信", "call": "拨打电话", "public_ip": "获取漫游公网 IP"}[task.TaskType]
|
|
environment := map[string]string{"vowifi": "VoWiFi", "cellular": "基站直连"}[task.Environment]
|
|
notification := automaticTaskNotification{
|
|
Title: "自动任务执行" + status,
|
|
Text: strings.Join([]string{
|
|
"自动任务执行" + status,
|
|
"任务 " + task.Name,
|
|
"设备 " + deviceLabel,
|
|
"类型 " + firstNonEmpty(taskType, task.TaskType),
|
|
"环境 " + firstNonEmpty(environment, task.Environment),
|
|
"时间 " + run.FinishedAt.Local().Format("2006-01-02 15:04:05"),
|
|
"结果 " + detail,
|
|
}, "\n"),
|
|
Time: run.FinishedAt, Task: task, Run: run,
|
|
}
|
|
for _, channel := range []string{"telegram", "bark", "email", "pushplus", "webhook", "wecom"} {
|
|
setting, err := s.store.NotificationSetting(ctx, channel)
|
|
if errors.Is(err, store.ErrNotFound) || (err == nil && !setting.Enabled) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
s.logger.Warn("read automatic task notification setting", "channel", channel, "error", err)
|
|
continue
|
|
}
|
|
var config map[string]any
|
|
if err := json.Unmarshal(setting.Config, &config); err != nil {
|
|
s.logger.Warn("decode automatic task notification setting", "channel", channel, "error", err)
|
|
continue
|
|
}
|
|
if err := sendAutomaticTaskNotification(ctx, channel, config, notification); err != nil {
|
|
s.logger.Warn("send automatic task notification", "channel", channel, "task_id", task.ID, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendAutomaticTaskNotification(ctx context.Context, channel string, config map[string]any, message automaticTaskNotification) error {
|
|
switch channel {
|
|
case "telegram":
|
|
return sendTelegramTextNotification(ctx, config, message.Text)
|
|
case "bark":
|
|
return sendBarkTextNotification(ctx, config, message.Title, message.Text)
|
|
case "email":
|
|
return sendEmailTextNotification(ctx, config, message.Title, message.Text)
|
|
case "pushplus":
|
|
return sendPushplusTextNotification(ctx, config, message.Title, message.Text)
|
|
case "webhook":
|
|
return sendAutomaticTaskWebhook(ctx, config, message)
|
|
case "wecom":
|
|
return sendWecomNotification(ctx, config, wecomAutomaticTaskValues(message))
|
|
default:
|
|
return fmt.Errorf("unsupported notification channel %q", channel)
|
|
}
|
|
}
|
|
|
|
func sendTelegramTextNotification(ctx context.Context, config map[string]any, text string) error {
|
|
token := configString(config, "bot_token")
|
|
parsed, err := validateTelegramAPIURL(ctx, configString(config, "base_url"), token, "sendMessage")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client, err := restrictedHTTPClient(ctx, 8*time.Second, configString(config, "proxy"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload, _ := json.Marshal(map[string]any{"chat_id": configString(config, "chat_id"), "text": text})
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, parsed.String(), bytes.NewReader(payload))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("User-Agent", "vocat-automatic-task/1")
|
|
return performNotificationRequest(client, request, true)
|
|
}
|
|
|
|
func sendBarkTextNotification(ctx context.Context, config map[string]any, title, text string) error {
|
|
client, err := restrictedHTTPClient(ctx, 8*time.Second, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := map[string]any{"title": title, "body": text}
|
|
for _, field := range []string{"group", "icon", "level"} {
|
|
if value := configString(config, field); value != "" {
|
|
payload[field] = value
|
|
}
|
|
}
|
|
encoded, _ := json.Marshal(payload)
|
|
for _, destination := range configStrings(config, "urls") {
|
|
parsed, err := validateOutboundURL(ctx, destination, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, parsed.String(), bytes.NewReader(encoded))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
request.Header.Set("User-Agent", "vocat-automatic-task/1")
|
|
if err := performNotificationRequest(client, request, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sendPushplusTextNotification(ctx context.Context, config map[string]any, title, text string) error {
|
|
destination, err := validateOutboundURL(ctx, "https://www.pushplus.plus/send", true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := map[string]any{"token": configString(config, "token"), "title": title, "content": text, "template": "txt", "timestamp": time.Now().UnixMilli()}
|
|
if topic := configString(config, "topic"); topic != "" {
|
|
payload["topic"] = topic
|
|
}
|
|
if channel := configString(config, "channel"); channel != "" {
|
|
payload["channel"] = channel
|
|
}
|
|
encoded, _ := json.Marshal(payload)
|
|
client, err := restrictedHTTPClient(ctx, 8*time.Second, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, destination.String(), bytes.NewReader(encoded))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
request.Header.Set("User-Agent", "vocat-automatic-task/1")
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
body, _ := io.ReadAll(io.LimitReader(response.Body, 64<<10))
|
|
var result struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 || json.Unmarshal(body, &result) != nil || result.Code != 200 {
|
|
return fmt.Errorf("%w: Pushplus HTTP %d code %d %s", errProviderRejected, response.StatusCode, result.Code, result.Msg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sendAutomaticTaskWebhook(ctx context.Context, config map[string]any, message automaticTaskNotification) error {
|
|
payload, _ := json.Marshal(map[string]any{
|
|
"event": "automatic_task.completed", "message": message.Text,
|
|
"timestamp": message.Time.UTC().Format(time.RFC3339), "task_id": message.Task.ID,
|
|
"task_name": message.Task.Name, "device_id": message.Task.DeviceID,
|
|
"task_type": message.Task.TaskType, "environment": message.Task.Environment,
|
|
"status": message.Run.Status, "attempts": message.Run.Attempts,
|
|
"output": message.Run.Output, "error": message.Run.Error,
|
|
})
|
|
client, err := restrictedHTTPClient(ctx, durationMilliseconds(configInt(config, "timeout_ms"), 5*time.Second), "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, destination := range configStrings(config, "urls") {
|
|
parsed, err := validateOutboundURL(ctx, destination, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, parsed.String(), bytes.NewReader(payload))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for name, value := range configStringMap(config, "headers") {
|
|
request.Header.Set(name, value)
|
|
}
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("User-Agent", "vocat-automatic-task/1")
|
|
if secret := configString(config, "secret"); secret != "" {
|
|
signature := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = signature.Write(payload)
|
|
request.Header.Set("X-vocat-Signature", "sha256="+hex.EncodeToString(signature.Sum(nil)))
|
|
}
|
|
if err := performNotificationRequest(client, request, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sendEmailTextNotification(ctx context.Context, config map[string]any, subject, text string) error {
|
|
host := strings.TrimSpace(configString(config, "smtp_host"))
|
|
port := configInt(config, "smtp_port")
|
|
if port == 0 {
|
|
port = 587
|
|
}
|
|
timeout := 8 * time.Second
|
|
connection, err := dialRestricted(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)), timeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer connection.Close()
|
|
if err := connection.SetDeadline(time.Now().Add(timeout)); err != nil {
|
|
return err
|
|
}
|
|
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, ServerName: host}
|
|
useSSL, _ := config["use_ssl"].(bool)
|
|
implicitTLS := port == 465 || useSSL
|
|
if implicitTLS {
|
|
secure := tls.Client(connection, tlsConfig)
|
|
if err := secure.HandshakeContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
connection = secure
|
|
}
|
|
client, err := smtp.NewClient(connection, host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
if !implicitTLS {
|
|
if available, _ := client.Extension("STARTTLS"); !available {
|
|
return errors.New("SMTP server does not offer STARTTLS")
|
|
}
|
|
if err := client.StartTLS(tlsConfig); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
username, password := configString(config, "username"), configString(config, "password")
|
|
if username != "" {
|
|
if err := client.Auth(smtp.PlainAuth("", username, password, host)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
from, err := parseMailAddress(configString(config, "from_address"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var recipients []*mail.Address
|
|
for _, item := range configStrings(config, "to_addresses") {
|
|
address, err := parseMailAddress(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recipients = append(recipients, address)
|
|
}
|
|
if err := client.Mail(from.Address); err != nil {
|
|
return err
|
|
}
|
|
for _, recipient := range recipients {
|
|
if err := client.Rcpt(recipient.Address); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
writer, err := client.Data()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writePlainTextMail(writer, from, recipients, subject, text); err != nil {
|
|
_ = writer.Close()
|
|
return err
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return err
|
|
}
|
|
return client.Quit()
|
|
}
|