mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 08:10:55 +00:00
231 lines
5.2 KiB
Go
231 lines
5.2 KiB
Go
package download
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
)
|
|
|
|
const (
|
|
webhookTimeout = 10 * time.Second
|
|
)
|
|
|
|
// WebhookEvent represents the type of webhook event
|
|
type WebhookEvent string
|
|
|
|
const (
|
|
WebhookEventDownloadDone WebhookEvent = "DOWNLOAD_DONE"
|
|
WebhookEventDownloadError WebhookEvent = "DOWNLOAD_ERROR"
|
|
WebhookEventDownloadPause WebhookEvent = "DOWNLOAD_PAUSE"
|
|
)
|
|
|
|
// WebhookData is the data sent to webhook URLs
|
|
type WebhookData struct {
|
|
Event WebhookEvent `json:"event"`
|
|
Time int64 `json:"time"` // Unix timestamp in milliseconds
|
|
Payload *WebhookPayload `json:"payload"`
|
|
}
|
|
|
|
// WebhookPayload contains the task data
|
|
type WebhookPayload struct {
|
|
Task *Task `json:"task"`
|
|
}
|
|
|
|
// getWebhookUrls extracts webhook URLs from config
|
|
// Supports both new webhook config format and legacy extra field for backward compatibility
|
|
func (d *Downloader) getWebhookUrls() []string {
|
|
cfg := d.cfg.DownloaderStoreConfig
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
// Try new webhook config first
|
|
if cfg.Webhook != nil && cfg.Webhook.Enable && len(cfg.Webhook.URLs) > 0 {
|
|
urls := make([]string, 0, len(cfg.Webhook.URLs))
|
|
for _, url := range cfg.Webhook.URLs {
|
|
if url != "" {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
if len(urls) > 0 {
|
|
return urls
|
|
}
|
|
}
|
|
|
|
// Fall back to legacy extra field for backward compatibility
|
|
if cfg.Extra == nil {
|
|
return nil
|
|
}
|
|
|
|
webhookUrls, ok := cfg.Extra["webhookUrls"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Try direct string slice first
|
|
if urlsStr, ok := webhookUrls.([]string); ok {
|
|
if len(urlsStr) == 0 {
|
|
return nil
|
|
}
|
|
return urlsStr
|
|
}
|
|
|
|
// Convert []interface{} to []string
|
|
if urlsInterface, ok := webhookUrls.([]any); ok {
|
|
if len(urlsInterface) == 0 {
|
|
return nil
|
|
}
|
|
urls := make([]string, 0, len(urlsInterface))
|
|
for _, urlInterface := range urlsInterface {
|
|
if url, ok := urlInterface.(string); ok && url != "" {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
if len(urls) == 0 {
|
|
return nil
|
|
}
|
|
return urls
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendWebhookToUrl sends webhook data to a single URL
|
|
// Returns the HTTP status code and any error that occurred
|
|
func (d *Downloader) sendWebhookToUrl(url string, data *WebhookData) (int, error) {
|
|
if url == "" {
|
|
return 0, fmt.Errorf("webhook URL is empty")
|
|
}
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: webhookTimeout,
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", "Gopeed-Webhook/1.0")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
// triggerWebhooks sends webhook notifications to all configured URLs
|
|
func (d *Downloader) triggerWebhooks(event WebhookEvent, task *Task, err error) {
|
|
urls := d.getWebhookUrls()
|
|
if len(urls) == 0 {
|
|
return
|
|
}
|
|
|
|
data := &WebhookData{
|
|
Event: event,
|
|
Time: time.Now().UnixMilli(),
|
|
Payload: &WebhookPayload{
|
|
Task: task.clone(),
|
|
},
|
|
}
|
|
|
|
go d.sendWebhooks(urls, data)
|
|
}
|
|
|
|
func (d *Downloader) sendWebhooks(urls []string, data *WebhookData) {
|
|
for _, url := range urls {
|
|
if url == "" {
|
|
continue
|
|
}
|
|
go func(webhookUrl string) {
|
|
statusCode, err := d.sendWebhookToUrl(webhookUrl, data)
|
|
if err != nil {
|
|
d.Logger.Warn().Err(err).Str("url", webhookUrl).Msg("webhook: failed to send request")
|
|
return
|
|
}
|
|
if statusCode >= 200 && statusCode < 300 {
|
|
d.Logger.Debug().Str("url", webhookUrl).Int("status", statusCode).Msg("webhook: sent successfully")
|
|
} else {
|
|
d.Logger.Warn().Str("url", webhookUrl).Int("status", statusCode).Msg("webhook: received non-success status")
|
|
}
|
|
}(url)
|
|
}
|
|
}
|
|
|
|
// SendTestWebhook sends a test webhook with a simulated payload
|
|
// Returns error if any webhook URL does not respond with HTTP 200
|
|
func (d *Downloader) SendTestWebhook() error {
|
|
urls := d.getWebhookUrls()
|
|
if len(urls) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, url := range urls {
|
|
if url == "" {
|
|
continue
|
|
}
|
|
if err := d.TestWebhookUrl(url); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestWebhookUrl tests a single webhook URL with a simulated payload
|
|
// Returns error if the URL does not respond with HTTP 200
|
|
func (d *Downloader) TestWebhookUrl(url string) error {
|
|
// Create a simulated test task with minimal required fields
|
|
testTask := NewTask()
|
|
testTask.Protocol = "http"
|
|
testTask.Status = base.DownloadStatusDone
|
|
testTask.Meta = &fetcher.FetcherMeta{
|
|
Req: &base.Request{
|
|
URL: "https://example.com/test-file.zip",
|
|
},
|
|
Opts: &base.Options{
|
|
Name: "test-file.zip",
|
|
Path: "/downloads",
|
|
},
|
|
Res: &base.Resource{
|
|
Size: 1024 * 1024 * 100, // 100MB
|
|
Files: []*base.FileInfo{
|
|
{Name: "test-file.zip", Size: 1024 * 1024 * 100},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create test data
|
|
testData := &WebhookData{
|
|
Event: WebhookEventDownloadDone,
|
|
Time: time.Now().UnixMilli(),
|
|
Payload: &WebhookPayload{
|
|
Task: testTask,
|
|
},
|
|
}
|
|
|
|
statusCode, err := d.sendWebhookToUrl(url, testData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if statusCode != http.StatusOK {
|
|
return fmt.Errorf("webhook test failed: %s returned status %d", url, statusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|