diff --git a/internal/action/run.go b/internal/action/run.go index 80cbfc0..4310bb4 100644 --- a/internal/action/run.go +++ b/internal/action/run.go @@ -1,7 +1,11 @@ package action import ( + "bytes" + "crypto/tls" + "encoding/json" "io" + "net/http" "os" "path" "time" @@ -70,6 +74,16 @@ func (s *service) runAction(action domain.Action, release domain.Release) error s.watchFolder(action, release) + case domain.ActionTypeWebhook: + if release.TorrentTmpFile == "" { + if err := release.DownloadTorrentFile(nil); err != nil { + log.Error().Stack().Err(err) + return err + } + } + + s.webhook(action, release) + case domain.ActionTypeDelugeV1, domain.ActionTypeDelugeV2: canDownload, err := s.delugeCheckRulesCanDownload(action) if err != nil { @@ -251,3 +265,52 @@ func (s *service) watchFolder(action domain.Action, release domain.Release) { log.Info().Msgf("saved file to watch folder: %v", fullFileName) } + +func (s *service) webhook(action domain.Action, release domain.Release) { + m := NewMacro(release) + + // parse and replace values in argument string before continuing + dataArgs, err := m.Parse(action.WebhookData) + if err != nil { + log.Error().Stack().Err(err).Msgf("could not parse macro: %v", action.WebhookData) + return + } + + log.Trace().Msgf("action WEBHOOK: '%v' file: %v", action.Name, release.TorrentName) + log.Trace().Msgf("webhook action '%v' - host: %v data: %v", action.Name, action.WebhookHost, action.WebhookData) + + jsonData, err := json.Marshal(dataArgs) + if err != nil { + log.Error().Err(err).Msgf("webhook client could not marshal data: %v", action.WebhookHost) + return + } + + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + client := http.Client{Transport: t, Timeout: 15 * time.Second} + + req, err := http.NewRequest(http.MethodPost, action.WebhookHost, bytes.NewBuffer(jsonData)) + if err != nil { + log.Error().Err(err).Msgf("webhook client request error: %v", action.WebhookHost) + return + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "autobrr") + + res, err := client.Do(req) + if err != nil { + log.Error().Err(err).Msgf("webhook client request error: %v", action.WebhookHost) + return + } + + defer res.Body.Close() + + log.Info().Msgf("successfully ran webhook action: '%v' to: %v payload: %v", action.Name, action.WebhookHost, dataArgs) + + return +} diff --git a/internal/database/action.go b/internal/database/action.go index eb7deab..596844b 100644 --- a/internal/database/action.go +++ b/internal/database/action.go @@ -3,6 +3,7 @@ package database import ( "context" "database/sql" + "github.com/autobrr/autobrr/internal/domain" "github.com/rs/zerolog/log" @@ -20,7 +21,7 @@ func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]domain //r.db.lock.RLock() //defer r.db.lock.RUnlock() - rows, err := r.db.handler.Query("SELECT id, name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_download_speed, limit_upload_speed, client_id FROM action WHERE action.filter_id = ?", filterID) + rows, err := r.db.handler.QueryContext(ctx, "SELECT id, name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_download_speed, limit_upload_speed, webhook_host, webhook_data, webhook_type, webhook_method, client_id FROM action WHERE action.filter_id = ?", filterID) if err != nil { log.Fatal().Err(err) } @@ -31,13 +32,13 @@ func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]domain for rows.Next() { var a domain.Action - var execCmd, execArgs, watchFolder, category, tags, label, savePath sql.NullString + var execCmd, execArgs, watchFolder, category, tags, label, savePath, host, data, webhookType, webhookMethod sql.NullString var limitUl, limitDl sql.NullInt64 var clientID sql.NullInt32 // filterID var paused, ignoreRules sql.NullBool - if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &clientID); err != nil { + if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &host, &data, &webhookType, &webhookMethod, &clientID); err != nil { log.Fatal().Err(err) } if err != nil { @@ -55,6 +56,10 @@ func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]domain a.IgnoreRules = ignoreRules.Bool a.LimitUploadSpeed = limitUl.Int64 a.LimitDownloadSpeed = limitDl.Int64 + a.WebhookHost = host.String + a.WebhookData = data.String + a.WebhookType = webhookType.String + a.WebhookMethod = webhookMethod.String a.ClientID = clientID.Int32 actions = append(actions, a) @@ -154,6 +159,10 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A tags := toNullString(action.Tags) label := toNullString(action.Label) savePath := toNullString(action.SavePath) + host := toNullString(action.WebhookHost) + data := toNullString(action.WebhookData) + webhookType := toNullString(action.WebhookType) + webhookMethod := toNullString(action.WebhookMethod) limitDL := toNullInt64(action.LimitDownloadSpeed) limitUL := toNullInt64(action.LimitUploadSpeed) @@ -163,13 +172,13 @@ func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.A var err error if action.ID != 0 { log.Debug().Msg("actions: update existing record") - _, err = r.db.handler.ExecContext(ctx, `UPDATE action SET name = ?, type = ?, enabled = ?, exec_cmd = ?, exec_args = ?, watch_folder = ? , category =? , tags = ?, label = ?, save_path = ?, paused = ?, ignore_rules = ?, limit_upload_speed = ?, limit_download_speed = ?, client_id = ? - WHERE id = ?`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, action.ID) + _, err = r.db.handler.ExecContext(ctx, `UPDATE action SET name = ?, type = ?, enabled = ?, exec_cmd = ?, exec_args = ?, watch_folder = ? , category =? , tags = ?, label = ?, save_path = ?, paused = ?, ignore_rules = ?, limit_upload_speed = ?, limit_download_speed = ?, webhook_host = ?, webhook_data = ?, webhook_type = ?, webhook_method = ?, client_id = ? + WHERE id = ?`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, host, data, webhookType, webhookMethod, clientID, action.ID) } else { var res sql.Result - res, err = r.db.handler.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id) - VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, filterID) + res, err = r.db.handler.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, webhook_host, webhook_data, webhook_type, webhook_method, client_id, filter_id) + VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, host, data, webhookType, webhookMethod, clientID, filterID) if err != nil { log.Error().Err(err) return nil, err @@ -208,6 +217,10 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []domain.Ac tags := toNullString(action.Tags) label := toNullString(action.Label) savePath := toNullString(action.SavePath) + host := toNullString(action.WebhookHost) + data := toNullString(action.WebhookData) + webhookType := toNullString(action.WebhookType) + webhookMethod := toNullString(action.WebhookMethod) limitDL := toNullInt64(action.LimitDownloadSpeed) limitUL := toNullInt64(action.LimitUploadSpeed) @@ -216,8 +229,8 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []domain.Ac var err error var res sql.Result - res, err = tx.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id) - VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, filterID) + res, err = tx.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, webhook_host, webhook_data, webhook_type, webhook_method, client_id, filter_id) + VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, host, data, webhookType, webhookMethod, clientID, filterID) if err != nil { log.Error().Stack().Err(err).Msg("actions: error executing query") return nil, err @@ -257,23 +270,3 @@ func (r *ActionRepo) ToggleEnabled(actionID int) error { return nil } - -func toNullString(s string) sql.NullString { - return sql.NullString{ - String: s, - Valid: s != "", - } -} - -func toNullInt32(s int32) sql.NullInt32 { - return sql.NullInt32{ - Int32: s, - Valid: s != 0, - } -} -func toNullInt64(s int64) sql.NullInt64 { - return sql.NullInt64{ - Int64: s, - Valid: s != 0, - } -} diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 17071aa..e5273a8 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -354,6 +354,22 @@ var migrations = []string{ ALTER TABLE "client" RENAME COLUMN ssl TO tls; `, + ` + ALTER TABLE "action" + ADD COLUMN webhook_host TEXT; + + ALTER TABLE "action" + ADD COLUMN webhook_data TEXT; + + ALTER TABLE "action" + ADD COLUMN webhook_method TEXT; + + ALTER TABLE "action" + ADD COLUMN webhook_type TEXT; + + ALTER TABLE "action" + ADD COLUMN webhook_headers TEXT [] DEFAULT '{}'; + `, } func (db *SqliteDB) migrate() error { diff --git a/internal/database/utils.go b/internal/database/utils.go index c8e6df8..a6a4ba5 100644 --- a/internal/database/utils.go +++ b/internal/database/utils.go @@ -1,6 +1,7 @@ package database import ( + "database/sql" "path" ) @@ -11,3 +12,23 @@ func dataSourceName(configPath string, name string) string { return name } + +func toNullString(s string) sql.NullString { + return sql.NullString{ + String: s, + Valid: s != "", + } +} + +func toNullInt32(s int32) sql.NullInt32 { + return sql.NullInt32{ + Int32: s, + Valid: s != 0, + } +} +func toNullInt64(s int64) sql.NullInt64 { + return sql.NullInt64{ + Int64: s, + Valid: s != 0, + } +} diff --git a/internal/domain/action.go b/internal/domain/action.go index 68aebe4..eafd18f 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -28,6 +28,11 @@ type Action struct { IgnoreRules bool `json:"ignore_rules,omitempty"` LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"` LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"` + WebhookHost string `json:"webhook_host,omitempty"` + WebhookType string `json:"webhook_type,omitempty"` + WebhookMethod string `json:"webhook_method,omitempty"` + WebhookData string `json:"webhook_data,omitempty"` + WebhookHeaders []string `json:"webhook_headers,omitempty"` FilterID int `json:"filter_id,omitempty"` ClientID int32 `json:"client_id,omitempty"` } @@ -41,6 +46,7 @@ const ( ActionTypeDelugeV1 ActionType = "DELUGE_V1" ActionTypeDelugeV2 ActionType = "DELUGE_V2" ActionTypeWatchFolder ActionType = "WATCH_FOLDER" + ActionTypeWebhook ActionType = "WEBHOOK" ActionTypeRadarr ActionType = "RADARR" ActionTypeSonarr ActionType = "SONARR" ActionTypeLidarr ActionType = "LIDARR" diff --git a/web/src/components/inputs/input.tsx b/web/src/components/inputs/input.tsx index 62c28cc..b43eb0e 100644 --- a/web/src/components/inputs/input.tsx +++ b/web/src/components/inputs/input.tsx @@ -7,22 +7,27 @@ type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; interface TextFieldProps { name: string; + defaultValue?: string; label?: string; placeholder?: string; columns?: COL_WIDTHS; autoComplete?: string; + hidden?: boolean; } export const TextField = ({ name, + defaultValue, label, placeholder, columns, - autoComplete + autoComplete, + hidden, }: TextFieldProps) => (