198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"regexp"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
|
"github.com/containrrr/shoutrrr"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type NotificationService struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
func NewNotificationService(db *gorm.DB) *NotificationService {
|
|
return &NotificationService{DB: db}
|
|
}
|
|
|
|
var discordWebhookRegex = regexp.MustCompile(`^https://discord(?:app)?\.com/api/webhooks/(\d+)/([a-zA-Z0-9_-]+)`)
|
|
|
|
func normalizeURL(serviceType, rawURL string) string {
|
|
if serviceType == "discord" {
|
|
matches := discordWebhookRegex.FindStringSubmatch(rawURL)
|
|
if len(matches) == 3 {
|
|
id := matches[1]
|
|
token := matches[2]
|
|
return fmt.Sprintf("discord://%s@%s", token, id)
|
|
}
|
|
}
|
|
return rawURL
|
|
}
|
|
|
|
// Internal Notifications (DB)
|
|
|
|
func (s *NotificationService) Create(nType models.NotificationType, title, message string) (*models.Notification, error) {
|
|
notification := &models.Notification{
|
|
Type: nType,
|
|
Title: title,
|
|
Message: message,
|
|
Read: false,
|
|
}
|
|
result := s.DB.Create(notification)
|
|
return notification, result.Error
|
|
}
|
|
|
|
func (s *NotificationService) List(unreadOnly bool) ([]models.Notification, error) {
|
|
var notifications []models.Notification
|
|
query := s.DB.Order("created_at desc")
|
|
if unreadOnly {
|
|
query = query.Where("read = ?", false)
|
|
}
|
|
result := query.Find(¬ifications)
|
|
return notifications, result.Error
|
|
}
|
|
|
|
func (s *NotificationService) MarkAsRead(id string) error {
|
|
return s.DB.Model(&models.Notification{}).Where("id = ?", id).Update("read", true).Error
|
|
}
|
|
|
|
func (s *NotificationService) MarkAllAsRead() error {
|
|
return s.DB.Model(&models.Notification{}).Where("read = ?", false).Update("read", true).Error
|
|
}
|
|
|
|
// External Notifications (Shoutrrr & Custom Webhooks)
|
|
|
|
func (s *NotificationService) SendExternal(eventType, title, message string, data map[string]interface{}) {
|
|
var providers []models.NotificationProvider
|
|
if err := s.DB.Where("enabled = ?", true).Find(&providers).Error; err != nil {
|
|
log.Printf("Failed to fetch notification providers: %v", err)
|
|
return
|
|
}
|
|
|
|
// Prepare data for templates
|
|
if data == nil {
|
|
data = make(map[string]interface{})
|
|
}
|
|
data["Title"] = title
|
|
data["Message"] = message
|
|
data["Time"] = time.Now().Format(time.RFC3339)
|
|
data["EventType"] = eventType
|
|
|
|
for _, provider := range providers {
|
|
// Filter based on preferences
|
|
shouldSend := false
|
|
switch eventType {
|
|
case "proxy_host":
|
|
shouldSend = provider.NotifyProxyHosts
|
|
case "remote_server":
|
|
shouldSend = provider.NotifyRemoteServers
|
|
case "domain":
|
|
shouldSend = provider.NotifyDomains
|
|
case "cert":
|
|
shouldSend = provider.NotifyCerts
|
|
case "uptime":
|
|
shouldSend = provider.NotifyUptime
|
|
case "test":
|
|
shouldSend = true
|
|
default:
|
|
// Default to true for unknown types or generic messages?
|
|
// Or false to be safe? Let's say true for now to avoid missing things,
|
|
// or maybe we should enforce types.
|
|
shouldSend = true
|
|
}
|
|
|
|
if !shouldSend {
|
|
continue
|
|
}
|
|
|
|
go func(p models.NotificationProvider) {
|
|
if p.Type == "webhook" {
|
|
if err := s.sendCustomWebhook(p, data); err != nil {
|
|
log.Printf("Failed to send webhook to %s: %v", p.Name, err)
|
|
}
|
|
} else {
|
|
url := normalizeURL(p.Type, p.URL)
|
|
// Use newline for better formatting in chat apps
|
|
msg := fmt.Sprintf("%s\n\n%s", title, message)
|
|
if err := shoutrrr.Send(url, msg); err != nil {
|
|
log.Printf("Failed to send notification to %s: %v", p.Name, err)
|
|
}
|
|
}
|
|
}(provider)
|
|
}
|
|
}
|
|
|
|
func (s *NotificationService) sendCustomWebhook(p models.NotificationProvider, data map[string]interface{}) error {
|
|
// Default template if empty
|
|
tmplStr := p.Config
|
|
if tmplStr == "" {
|
|
tmplStr = `{"content": "{{.Title}}: {{.Message}}"}`
|
|
}
|
|
|
|
// Parse template
|
|
tmpl, err := template.New("webhook").Parse(tmplStr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse webhook template: %w", err)
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
if err := tmpl.Execute(&body, data); err != nil {
|
|
return fmt.Errorf("failed to execute webhook template: %w", err)
|
|
}
|
|
|
|
// Send Request
|
|
resp, err := http.Post(p.URL, "application/json", &body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send webhook: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("webhook returned status: %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *NotificationService) TestProvider(provider models.NotificationProvider) error {
|
|
if provider.Type == "webhook" {
|
|
data := map[string]interface{}{
|
|
"Title": "Test Notification",
|
|
"Message": "This is a test notification from CaddyProxyManager+",
|
|
"Status": "TEST",
|
|
"Name": "Test Monitor",
|
|
"Latency": 123,
|
|
"Time": time.Now().Format(time.RFC3339),
|
|
}
|
|
return s.sendCustomWebhook(provider, data)
|
|
}
|
|
url := normalizeURL(provider.Type, provider.URL)
|
|
return shoutrrr.Send(url, "Test notification from CaddyProxyManager+")
|
|
}
|
|
|
|
// Provider Management
|
|
|
|
func (s *NotificationService) ListProviders() ([]models.NotificationProvider, error) {
|
|
var providers []models.NotificationProvider
|
|
result := s.DB.Find(&providers)
|
|
return providers, result.Error
|
|
}
|
|
|
|
func (s *NotificationService) CreateProvider(provider *models.NotificationProvider) error {
|
|
return s.DB.Create(provider).Error
|
|
}
|
|
|
|
func (s *NotificationService) UpdateProvider(provider *models.NotificationProvider) error {
|
|
return s.DB.Save(provider).Error
|
|
}
|
|
|
|
func (s *NotificationService) DeleteProvider(id string) error {
|
|
return s.DB.Delete(&models.NotificationProvider{}, "id = ?", id).Error
|
|
}
|