mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
feat(actions): add webhook support (#184)
* feat(actions): add webhook support * feat: add type and method
This commit is contained in:
parent
3c323004c0
commit
159133ef35
9 changed files with 178 additions and 35 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) => (
|
||||
<div
|
||||
className={classNames(
|
||||
columns ? `col-span-${columns}` : "col-span-12"
|
||||
hidden ? "hidden" : "",
|
||||
columns ? `col-span-${columns}` : "col-span-12",
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
|
@ -40,6 +45,7 @@ export const TextField = ({
|
|||
{...field}
|
||||
id={name}
|
||||
type="text"
|
||||
defaultValue={defaultValue}
|
||||
autoComplete={autoComplete}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100"
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -191,6 +191,7 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
|
|||
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||
|
@ -203,6 +204,7 @@ export const ActionTypeOptions: RadioFieldsetOption[] = [
|
|||
export const ActionTypeNameMap = {
|
||||
"TEST": "Test",
|
||||
"WATCH_FOLDER": "Watch folder",
|
||||
"WEBHOOK": "Webhook",
|
||||
"EXEC": "Exec",
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
|
|
|
@ -178,7 +178,16 @@ export default function FilterDetails() {
|
|||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
const handleSubmit = (data: Filter) => {
|
||||
// force set method and type on webhook actions
|
||||
// TODO add options for these
|
||||
data.actions.forEach((a: Action) => {
|
||||
if (a.type === "WEBHOOK") {
|
||||
a.webhook_method = "POST"
|
||||
a.webhook_type = "JSON"
|
||||
}
|
||||
})
|
||||
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
|
||||
|
@ -268,8 +277,6 @@ export default function FilterDetails() {
|
|||
except_uploaders: filter.except_uploaders,
|
||||
freeleech: filter.freeleech,
|
||||
freeleech_percent: filter.freeleech_percent,
|
||||
indexers: filter.indexers || [],
|
||||
actions: filter.actions || [],
|
||||
formats: filter.formats || [],
|
||||
quality: filter.quality || [],
|
||||
media: filter.media || [],
|
||||
|
@ -280,6 +287,8 @@ export default function FilterDetails() {
|
|||
perfect_flac: filter.perfect_flac,
|
||||
artists: filter.artists,
|
||||
albums: filter.albums,
|
||||
indexers: filter.indexers || [],
|
||||
actions: filter.actions || [],
|
||||
} as Filter}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
@ -624,6 +633,11 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
|||
limit_upload_speed: 0,
|
||||
limit_download_speed: 0,
|
||||
filter_id: filter.id,
|
||||
webhook_host: "",
|
||||
webhook_type: "",
|
||||
webhook_method: "",
|
||||
webhook_data: "",
|
||||
webhook_headers: [],
|
||||
// client_id: 0,
|
||||
}
|
||||
|
||||
|
@ -719,6 +733,23 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
case "WEBHOOK":
|
||||
return (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField
|
||||
name={`actions.${idx}.webhook_host`}
|
||||
label="Host"
|
||||
columns={6}
|
||||
placeholder="Host eg. http://localhost/webhook"
|
||||
/>
|
||||
<TextField
|
||||
name={`actions.${idx}.webhook_data`}
|
||||
label="Data (json)"
|
||||
columns={6}
|
||||
placeholder={`Request data: { "key": "value" }`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "QBITTORRENT":
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
|
7
web/src/types/Filter.d.ts
vendored
7
web/src/types/Filter.d.ts
vendored
|
@ -66,8 +66,13 @@ interface Action {
|
|||
ignore_rules?: boolean;
|
||||
limit_upload_speed?: number;
|
||||
limit_download_speed?: number;
|
||||
webhook_host: string,
|
||||
webhook_type: string;
|
||||
webhook_method: string;
|
||||
webhook_data: string,
|
||||
webhook_headers: string[];
|
||||
filter_id?: number;
|
||||
client_id?: number;
|
||||
}
|
||||
|
||||
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | DownloadClientType;
|
||||
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'WEBHOOK' | DownloadClientType;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue