mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-09 19:59:39 +08:00
first commit
This commit is contained in:
88
server/internal/notify/email.go
Normal file
88
server/internal/notify/email.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EmailNotifier struct{}
|
||||
|
||||
func NewEmailNotifier() *EmailNotifier { return &EmailNotifier{} }
|
||||
func (n *EmailNotifier) Type() string { return "email" }
|
||||
func (n *EmailNotifier) SensitiveFields() []string { return []string{"password"} }
|
||||
|
||||
func (n *EmailNotifier) Validate(config map[string]any) error {
|
||||
host := strings.TrimSpace(asString(config["host"]))
|
||||
port := asInt(config["port"])
|
||||
from := strings.TrimSpace(asString(config["from"]))
|
||||
to := strings.TrimSpace(asString(config["to"]))
|
||||
if host == "" || port <= 0 || from == "" || to == "" {
|
||||
return fmt.Errorf("email host/port/from/to are required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *EmailNotifier) Send(_ context.Context, config map[string]any, message Message) error {
|
||||
if err := n.Validate(config); err != nil {
|
||||
return err
|
||||
}
|
||||
host := strings.TrimSpace(asString(config["host"]))
|
||||
port := asInt(config["port"])
|
||||
username := strings.TrimSpace(asString(config["username"]))
|
||||
password := strings.TrimSpace(asString(config["password"]))
|
||||
from := strings.TrimSpace(asString(config["from"]))
|
||||
toList := splitCommaValues(asString(config["to"]))
|
||||
address := host + ":" + strconv.Itoa(port)
|
||||
headers := []string{"From: " + from, "To: " + strings.Join(toList, ", "), "Subject: " + message.Title, "MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "", message.Body}
|
||||
var auth smtp.Auth
|
||||
if username != "" {
|
||||
auth = smtp.PlainAuth("", username, password, host)
|
||||
}
|
||||
|
||||
rawMessage := []byte(strings.Join(headers, "\r\n"))
|
||||
|
||||
if port == 465 {
|
||||
tlsConfig := &tls.Config{ServerName: host}
|
||||
conn, err := tls.Dial("tcp", address, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial tls for smtp port 465 failed: %w", err)
|
||||
}
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create smtp client over tls failed: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
if auth != nil {
|
||||
if ok, _ := client.Extension("AUTH"); ok {
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("smtp auth failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("smtp mail from failed: %w", err)
|
||||
}
|
||||
for _, toAddr := range toList {
|
||||
if err = client.Rcpt(toAddr); err != nil {
|
||||
return fmt.Errorf("smtp rcpt failed for %s: %w", toAddr, err)
|
||||
}
|
||||
}
|
||||
writer, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp data failed: %w", err)
|
||||
}
|
||||
if _, err = writer.Write(rawMessage); err != nil {
|
||||
return fmt.Errorf("smtp write message failed: %w", err)
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
return fmt.Errorf("smtp data close failed: %w", err)
|
||||
}
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
return smtp.SendMail(address, auth, from, toList, rawMessage)
|
||||
}
|
||||
49
server/internal/notify/helpers.go
Normal file
49
server/internal/notify/helpers.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func asString(value any) string {
|
||||
text, _ := value.(string)
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func asInt(value any) int {
|
||||
switch actual := value.(type) {
|
||||
case int:
|
||||
return actual
|
||||
case int64:
|
||||
return int(actual)
|
||||
case float64:
|
||||
return int(actual)
|
||||
case string:
|
||||
parsed, _ := strconv.Atoi(strings.TrimSpace(actual))
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func splitCommaValues(value string) []string {
|
||||
items := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
trimmed := strings.TrimSpace(item)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validateRequiredConfig(config map[string]any, fields ...string) error {
|
||||
for _, field := range fields {
|
||||
if strings.TrimSpace(asString(config[field])) == "" {
|
||||
return fmt.Errorf("%s is required", field)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
75
server/internal/notify/registry.go
Normal file
75
server/internal/notify/registry.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
notifiers map[string]Notifier
|
||||
}
|
||||
|
||||
func NewRegistry(notifiers ...Notifier) *Registry {
|
||||
registry := &Registry{notifiers: make(map[string]Notifier)}
|
||||
for _, notifier := range notifiers {
|
||||
registry.Register(notifier)
|
||||
}
|
||||
return registry
|
||||
}
|
||||
|
||||
func (r *Registry) Register(notifier Notifier) {
|
||||
if notifier == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.notifiers == nil {
|
||||
r.notifiers = make(map[string]Notifier)
|
||||
}
|
||||
r.notifiers[notifier.Type()] = notifier
|
||||
}
|
||||
|
||||
func (r *Registry) Types() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
items := make([]string, 0, len(r.notifiers))
|
||||
for key := range r.notifiers {
|
||||
items = append(items, key)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return items
|
||||
}
|
||||
|
||||
func (r *Registry) SensitiveFields(notificationType string) []string {
|
||||
notifier, ok := r.Notifier(notificationType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return notifier.SensitiveFields()
|
||||
}
|
||||
|
||||
func (r *Registry) Validate(notificationType string, config map[string]any) error {
|
||||
notifier, ok := r.Notifier(notificationType)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported notification type: %s", notificationType)
|
||||
}
|
||||
return notifier.Validate(config)
|
||||
}
|
||||
|
||||
func (r *Registry) Send(ctx context.Context, notificationType string, config map[string]any, message Message) error {
|
||||
notifier, ok := r.Notifier(notificationType)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported notification type: %s", notificationType)
|
||||
}
|
||||
return notifier.Send(ctx, config, message)
|
||||
}
|
||||
|
||||
func (r *Registry) Notifier(notificationType string) (Notifier, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
notifier, ok := r.notifiers[notificationType]
|
||||
return notifier, ok
|
||||
}
|
||||
54
server/internal/notify/telegram.go
Normal file
54
server/internal/notify/telegram.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TelegramNotifier struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewTelegramNotifier() *TelegramNotifier {
|
||||
return &TelegramNotifier{client: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
func (n *TelegramNotifier) Type() string { return "telegram" }
|
||||
func (n *TelegramNotifier) SensitiveFields() []string { return []string{"botToken"} }
|
||||
|
||||
func (n *TelegramNotifier) Validate(config map[string]any) error {
|
||||
if strings.TrimSpace(asString(config["botToken"])) == "" || strings.TrimSpace(asString(config["chatId"])) == "" {
|
||||
return fmt.Errorf("telegram botToken/chatId are required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *TelegramNotifier) Send(ctx context.Context, config map[string]any, message Message) error {
|
||||
if err := n.Validate(config); err != nil {
|
||||
return err
|
||||
}
|
||||
botToken := strings.TrimSpace(asString(config["botToken"]))
|
||||
chatID := strings.TrimSpace(asString(config["chatId"]))
|
||||
payload, err := json.Marshal(map[string]any{"chat_id": chatID, "text": message.Title + "\n\n" + message.Body})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal telegram payload: %w", err)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.telegram.org/bot"+botToken+"/sendMessage", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create telegram request: %w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := n.client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send telegram request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode >= http.StatusBadRequest {
|
||||
return fmt.Errorf("telegram response status: %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
server/internal/notify/types.go
Normal file
16
server/internal/notify/types.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package notify
|
||||
|
||||
import "context"
|
||||
|
||||
type Message struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Fields map[string]any `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
Type() string
|
||||
SensitiveFields() []string
|
||||
Validate(config map[string]any) error
|
||||
Send(ctx context.Context, config map[string]any, message Message) error
|
||||
}
|
||||
55
server/internal/notify/webhook.go
Normal file
55
server/internal/notify/webhook.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WebhookNotifier struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewWebhookNotifier() *WebhookNotifier {
|
||||
return &WebhookNotifier{client: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
func (n *WebhookNotifier) Type() string { return "webhook" }
|
||||
func (n *WebhookNotifier) SensitiveFields() []string { return []string{"secret"} }
|
||||
|
||||
func (n *WebhookNotifier) Validate(config map[string]any) error {
|
||||
if strings.TrimSpace(asString(config["url"])) == "" {
|
||||
return fmt.Errorf("webhook url is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *WebhookNotifier) Send(ctx context.Context, config map[string]any, message Message) error {
|
||||
if err := n.Validate(config); err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := json.Marshal(map[string]any{"title": message.Title, "body": message.Body, "fields": message.Fields})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal webhook payload: %w", err)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(asString(config["url"])), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create webhook request: %w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if secret := strings.TrimSpace(asString(config["secret"])); secret != "" {
|
||||
request.Header.Set("X-BackupX-Secret", secret)
|
||||
}
|
||||
response, err := n.client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send webhook request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode >= http.StatusBadRequest {
|
||||
return fmt.Errorf("webhook response status: %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user