feat(filters): external webhook retry on status codes (#1206)

* feat: external filter retry status codes

* chore: go mod tidy

* fix(database): migrations

---------

Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
Steven Kreitzer 2023-10-27 10:37:57 -05:00 committed by GitHub
parent 40a1a4c014
commit 2080136669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 330 additions and 165 deletions

1
go.mod
View file

@ -13,6 +13,7 @@ require (
github.com/autobrr/go-qbittorrent v1.6.0 github.com/autobrr/go-qbittorrent v1.6.0
github.com/autobrr/go-rtorrent v1.10.0 github.com/autobrr/go-rtorrent v1.10.0
github.com/avast/retry-go v3.0.0+incompatible github.com/avast/retry-go v3.0.0+incompatible
github.com/avast/retry-go/v4 v4.5.0
github.com/dcarbone/zadapters/zstdlog v1.0.0 github.com/dcarbone/zadapters/zstdlog v1.0.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/ergochat/irc-go v0.4.0 github.com/ergochat/irc-go v0.4.0

2
go.sum
View file

@ -102,6 +102,8 @@ github.com/autobrr/sse/v2 v2.0.0-20230520125637-530e06346d7d h1:9EGCYgeugAVWLBAt
github.com/autobrr/sse/v2 v2.0.0-20230520125637-530e06346d7d/go.mod h1:zCozZ9lp4DE340T2+wfMPL/eoQwLVIGDOCKCDEFwTQU= github.com/autobrr/sse/v2 v2.0.0-20230520125637-530e06346d7d/go.mod h1:zCozZ9lp4DE340T2+wfMPL/eoQwLVIGDOCKCDEFwTQU=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=

View file

@ -247,6 +247,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
"fe.webhook_data", "fe.webhook_data",
"fe.webhook_headers", "fe.webhook_headers",
"fe.webhook_expect_status", "fe.webhook_expect_status",
"fe.webhook_retry_status",
"fe.webhook_retry_attempts",
"fe.webhook_retry_delay_seconds",
"fe.webhook_retry_max_jitter_seconds",
). ).
From("filter f"). From("filter f").
LeftJoin("filter_external fe ON f.id = fe.filter_id"). LeftJoin("filter_external fe ON f.id = fe.filter_id").
@ -276,8 +280,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
var delay, maxDownloads, logScore sql.NullInt32 var delay, maxDownloads, logScore sql.NullInt32
// filter external // filter external
var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData sql.NullString var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData, extWebhookRetryStatus sql.NullString
var extId, extIndex, extWebhookStatus, extExecStatus sql.NullInt32 var extId, extIndex, extWebhookStatus, extWebhookRetryAttempts, extWebhookDelaySeconds, extWebhookRetryJitterSeconds, extExecStatus sql.NullInt32
var extEnabled sql.NullBool var extEnabled sql.NullBool
if err := rows.Scan( if err := rows.Scan(
@ -354,6 +358,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
&extWebhookData, &extWebhookData,
&extWebhookHeaders, &extWebhookHeaders,
&extWebhookStatus, &extWebhookStatus,
&extWebhookRetryStatus,
&extWebhookRetryAttempts,
&extWebhookDelaySeconds,
&extWebhookRetryJitterSeconds,
); err != nil { ); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -396,19 +404,23 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
if extId.Valid { if extId.Valid {
external := domain.FilterExternal{ external := domain.FilterExternal{
ID: int(extId.Int32), ID: int(extId.Int32),
Name: extName.String, Name: extName.String,
Index: int(extIndex.Int32), Index: int(extIndex.Int32),
Type: domain.FilterExternalType(extType.String), Type: domain.FilterExternalType(extType.String),
Enabled: extEnabled.Bool, Enabled: extEnabled.Bool,
ExecCmd: extExecCmd.String, ExecCmd: extExecCmd.String,
ExecArgs: extExecArgs.String, ExecArgs: extExecArgs.String,
ExecExpectStatus: int(extExecStatus.Int32), ExecExpectStatus: int(extExecStatus.Int32),
WebhookHost: extWebhookHost.String, WebhookHost: extWebhookHost.String,
WebhookMethod: extWebhookMethod.String, WebhookMethod: extWebhookMethod.String,
WebhookData: extWebhookData.String, WebhookData: extWebhookData.String,
WebhookHeaders: extWebhookHeaders.String, WebhookHeaders: extWebhookHeaders.String,
WebhookExpectStatus: int(extWebhookStatus.Int32), WebhookExpectStatus: int(extWebhookStatus.Int32),
WebhookRetryStatus: extWebhookRetryStatus.String,
WebhookRetryAttempts: int(extWebhookRetryAttempts.Int32),
WebhookRetryDelaySeconds: int(extWebhookDelaySeconds.Int32),
WebhookRetryMaxJitterSeconds: int(extWebhookRetryJitterSeconds.Int32),
} }
externalMap[external.ID] = external externalMap[external.ID] = external
} }
@ -502,6 +514,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
"fe.webhook_data", "fe.webhook_data",
"fe.webhook_headers", "fe.webhook_headers",
"fe.webhook_expect_status", "fe.webhook_expect_status",
"fe.webhook_retry_status",
"fe.webhook_retry_attempts",
"fe.webhook_retry_delay_seconds",
"fe.webhook_retry_max_jitter_seconds",
"fe.filter_id", "fe.filter_id",
). ).
From("filter f"). From("filter f").
@ -537,8 +553,8 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
var delay, maxDownloads, logScore sql.NullInt32 var delay, maxDownloads, logScore sql.NullInt32
// filter external // filter external
var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData sql.NullString var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData, extWebhookRetryStatus sql.NullString
var extId, extIndex, extWebhookStatus, extExecStatus, extFilterId sql.NullInt32 var extId, extIndex, extWebhookStatus, extWebhookRetryAttempts, extWebhookDelaySeconds, extWebhookRetryJitterSeconds, extExecStatus, extFilterId sql.NullInt32
var extEnabled sql.NullBool var extEnabled sql.NullBool
if err := rows.Scan( if err := rows.Scan(
@ -615,6 +631,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
&extWebhookData, &extWebhookData,
&extWebhookHeaders, &extWebhookHeaders,
&extWebhookStatus, &extWebhookStatus,
&extWebhookRetryStatus,
&extWebhookRetryAttempts,
&extWebhookDelaySeconds,
&extWebhookRetryJitterSeconds,
&extFilterId, &extFilterId,
); err != nil { ); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
@ -658,20 +678,24 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
if extId.Valid { if extId.Valid {
external := domain.FilterExternal{ external := domain.FilterExternal{
ID: int(extId.Int32), ID: int(extId.Int32),
Name: extName.String, Name: extName.String,
Index: int(extIndex.Int32), Index: int(extIndex.Int32),
Type: domain.FilterExternalType(extType.String), Type: domain.FilterExternalType(extType.String),
Enabled: extEnabled.Bool, Enabled: extEnabled.Bool,
ExecCmd: extExecCmd.String, ExecCmd: extExecCmd.String,
ExecArgs: extExecArgs.String, ExecArgs: extExecArgs.String,
ExecExpectStatus: int(extExecStatus.Int32), ExecExpectStatus: int(extExecStatus.Int32),
WebhookHost: extWebhookHost.String, WebhookHost: extWebhookHost.String,
WebhookMethod: extWebhookMethod.String, WebhookMethod: extWebhookMethod.String,
WebhookData: extWebhookData.String, WebhookData: extWebhookData.String,
WebhookHeaders: extWebhookHeaders.String, WebhookHeaders: extWebhookHeaders.String,
WebhookExpectStatus: int(extWebhookStatus.Int32), WebhookExpectStatus: int(extWebhookStatus.Int32),
FilterId: int(extFilterId.Int32), WebhookRetryStatus: extWebhookRetryStatus.String,
WebhookRetryAttempts: int(extWebhookRetryAttempts.Int32),
WebhookRetryDelaySeconds: int(extWebhookDelaySeconds.Int32),
WebhookRetryMaxJitterSeconds: int(extWebhookRetryJitterSeconds.Int32),
FilterId: int(extFilterId.Int32),
} }
externalMap[external.FilterId] = append(externalMap[external.FilterId], external) externalMap[external.FilterId] = append(externalMap[external.FilterId], external)
} }
@ -709,6 +733,10 @@ func (r *FilterRepo) FindExternalFiltersByID(ctx context.Context, filterId int)
"fe.webhook_data", "fe.webhook_data",
"fe.webhook_headers", "fe.webhook_headers",
"fe.webhook_expect_status", "fe.webhook_expect_status",
"fe.webhook_retry_status",
"fe.webhook_retry_attempts",
"fe.webhook_retry_delay_seconds",
"fe.webhook_retry_max_jitter_seconds",
). ).
From("filter_external fe"). From("filter_external fe").
Where(sq.Eq{"fe.filter_id": filterId}) Where(sq.Eq{"fe.filter_id": filterId})
@ -732,8 +760,8 @@ func (r *FilterRepo) FindExternalFiltersByID(ctx context.Context, filterId int)
var external domain.FilterExternal var external domain.FilterExternal
// filter external // filter external
var extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData sql.NullString var extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData, extWebhookRetryStatus sql.NullString
var extWebhookStatus, extExecStatus sql.NullInt32 var extWebhookStatus, extWebhookRetryAttempts, extWebhookDelaySeconds, extWebhookRetryJitterSeconds, extExecStatus sql.NullInt32
if err := rows.Scan( if err := rows.Scan(
&external.ID, &external.ID,
@ -749,6 +777,10 @@ func (r *FilterRepo) FindExternalFiltersByID(ctx context.Context, filterId int)
&extWebhookData, &extWebhookData,
&extWebhookHeaders, &extWebhookHeaders,
&extWebhookStatus, &extWebhookStatus,
&extWebhookRetryStatus,
&extWebhookRetryAttempts,
&extWebhookDelaySeconds,
&extWebhookRetryJitterSeconds,
); err != nil { ); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -762,6 +794,10 @@ func (r *FilterRepo) FindExternalFiltersByID(ctx context.Context, filterId int)
external.WebhookData = extWebhookData.String external.WebhookData = extWebhookData.String
external.WebhookHeaders = extWebhookHeaders.String external.WebhookHeaders = extWebhookHeaders.String
external.WebhookExpectStatus = int(extWebhookStatus.Int32) external.WebhookExpectStatus = int(extWebhookStatus.Int32)
external.WebhookRetryStatus = extWebhookRetryStatus.String
external.WebhookRetryAttempts = int(extWebhookRetryAttempts.Int32)
external.WebhookRetryDelaySeconds = int(extWebhookDelaySeconds.Int32)
external.WebhookRetryMaxJitterSeconds = int(extWebhookRetryJitterSeconds.Int32)
externalFilters = append(externalFilters, external) externalFilters = append(externalFilters, external)
} }
@ -1182,6 +1218,18 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda
if filter.ExternalWebhookExpectStatus != nil { if filter.ExternalWebhookExpectStatus != nil {
q = q.Set("external_webhook_expect_status", filter.ExternalWebhookExpectStatus) q = q.Set("external_webhook_expect_status", filter.ExternalWebhookExpectStatus)
} }
if filter.ExternalWebhookRetryStatus != nil {
q = q.Set("external_webhook_retry_status", filter.ExternalWebhookRetryStatus)
}
if filter.ExternalWebhookRetryAttempts != nil {
q = q.Set("external_webhook_retry_attempts", filter.ExternalWebhookRetryAttempts)
}
if filter.ExternalWebhookRetryDelaySeconds != nil {
q = q.Set("external_webhook_retry_delay_seconds", filter.ExternalWebhookRetryDelaySeconds)
}
if filter.ExternalWebhookRetryMaxJitterSeconds != nil {
q = q.Set("external_webhook_retry_max_jitter_seconds", filter.ExternalWebhookRetryMaxJitterSeconds)
}
q = q.Where(sq.Eq{"id": filter.ID}) q = q.Where(sq.Eq{"id": filter.ID})
@ -1462,6 +1510,10 @@ func (r *FilterRepo) StoreFilterExternal(ctx context.Context, filterID int, exte
"webhook_data", "webhook_data",
"webhook_headers", "webhook_headers",
"webhook_expect_status", "webhook_expect_status",
"webhook_retry_status",
"webhook_retry_attempts",
"webhook_retry_delay_seconds",
"webhook_retry_max_jitter_seconds",
"filter_id", "filter_id",
) )
@ -1479,6 +1531,10 @@ func (r *FilterRepo) StoreFilterExternal(ctx context.Context, filterID int, exte
toNullString(external.WebhookData), toNullString(external.WebhookData),
toNullString(external.WebhookHeaders), toNullString(external.WebhookHeaders),
toNullInt32(int32(external.WebhookExpectStatus)), toNullInt32(int32(external.WebhookExpectStatus)),
toNullString(external.WebhookRetryStatus),
toNullInt32(int32(external.WebhookRetryAttempts)),
toNullInt32(int32(external.WebhookRetryDelaySeconds)),
toNullInt32(int32(external.WebhookRetryMaxJitterSeconds)),
filterID, filterID,
) )
} }

View file

@ -133,21 +133,25 @@ CREATE TABLE filter
CREATE TABLE filter_external CREATE TABLE filter_external
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
idx INTEGER, idx INTEGER,
type TEXT, type TEXT,
enabled BOOLEAN, enabled BOOLEAN,
exec_cmd TEXT, exec_cmd TEXT,
exec_args TEXT, exec_args TEXT,
exec_expect_status INTEGER, exec_expect_status INTEGER,
webhook_host TEXT, webhook_host TEXT,
webhook_method TEXT, webhook_method TEXT,
webhook_data TEXT, webhook_data TEXT,
webhook_headers TEXT, webhook_headers TEXT,
webhook_expect_status INTEGER, webhook_expect_status INTEGER,
filter_id INTEGER NOT NULL, webhook_retry_status TEXT,
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE webhook_retry_attempts INTEGER,
webhook_retry_delay_seconds INTEGER,
webhook_retry_max_jitter_seconds INTEGER,
filter_id INTEGER NOT NULL,
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE
); );
CREATE TABLE filter_indexer CREATE TABLE filter_indexer
@ -797,5 +801,17 @@ CREATE INDEX feed_cache_feed_id_key_index
`, `,
`ALTER TABLE action `ALTER TABLE action
ADD COLUMN external_client_id INTEGER; ADD COLUMN external_client_id INTEGER;
`,
`ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_status TEXT;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_attempts INTEGER;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_delay_seconds INTEGER;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_max_jitter_seconds INTEGER;
`, `,
} }

View file

@ -133,21 +133,25 @@ CREATE TABLE filter
CREATE TABLE filter_external CREATE TABLE filter_external
( (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
idx INTEGER, idx INTEGER,
type TEXT, type TEXT,
enabled BOOLEAN, enabled BOOLEAN,
exec_cmd TEXT, exec_cmd TEXT,
exec_args TEXT, exec_args TEXT,
exec_expect_status INTEGER, exec_expect_status INTEGER,
webhook_host TEXT, webhook_host TEXT,
webhook_method TEXT, webhook_method TEXT,
webhook_data TEXT, webhook_data TEXT,
webhook_headers TEXT, webhook_headers TEXT,
webhook_expect_status INTEGER, webhook_expect_status INTEGER,
filter_id INTEGER NOT NULL, webhook_retry_status TEXT,
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE webhook_retry_attempts INTEGER,
webhook_retry_delay_seconds INTEGER,
webhook_retry_max_jitter_seconds INTEGER,
filter_id INTEGER NOT NULL,
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE
); );
CREATE TABLE filter_indexer CREATE TABLE filter_indexer
@ -1348,5 +1352,17 @@ CREATE INDEX feed_cache_feed_id_key_index
`, `,
`ALTER TABLE action `ALTER TABLE action
ADD COLUMN external_client_id INTEGER; ADD COLUMN external_client_id INTEGER;
`,
`ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_status TEXT;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_attempts INTEGER;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_delay_seconds INTEGER;
ALTER TABLE filter_external
ADD COLUMN external_webhook_retry_max_jitter_seconds INTEGER;
`, `,
} }

View file

@ -138,20 +138,24 @@ type Filter struct {
} }
type FilterExternal struct { type FilterExternal struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Index int `json:"index"` Index int `json:"index"`
Type FilterExternalType `json:"type"` Type FilterExternalType `json:"type"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ExecCmd string `json:"exec_cmd,omitempty"` ExecCmd string `json:"exec_cmd,omitempty"`
ExecArgs string `json:"exec_args,omitempty"` ExecArgs string `json:"exec_args,omitempty"`
ExecExpectStatus int `json:"exec_expect_status,omitempty"` ExecExpectStatus int `json:"exec_expect_status,omitempty"`
WebhookHost string `json:"webhook_host,omitempty"` WebhookHost string `json:"webhook_host,omitempty"`
WebhookMethod string `json:"webhook_method,omitempty"` WebhookMethod string `json:"webhook_method,omitempty"`
WebhookData string `json:"webhook_data,omitempty"` WebhookData string `json:"webhook_data,omitempty"`
WebhookHeaders string `json:"webhook_headers,omitempty"` WebhookHeaders string `json:"webhook_headers,omitempty"`
WebhookExpectStatus int `json:"webhook_expect_status,omitempty"` WebhookExpectStatus int `json:"webhook_expect_status,omitempty"`
FilterId int `json:"-"` WebhookRetryStatus string `json:"webhook_retry_status,omitempty"`
WebhookRetryAttempts int `json:"webhook_retry_attempts,omitempty"`
WebhookRetryDelaySeconds int `json:"webhook_retry_delay_seconds,omitempty"`
WebhookRetryMaxJitterSeconds int `json:"webhook_retry_max_jitter_seconds,omitempty"`
FilterId int `json:"-"`
} }
type FilterExternalType string type FilterExternalType string
@ -162,79 +166,83 @@ const (
) )
type FilterUpdate struct { type FilterUpdate struct {
ID int `json:"id"` ID int `json:"id"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
MinSize *string `json:"min_size,omitempty"` MinSize *string `json:"min_size,omitempty"`
MaxSize *string `json:"max_size,omitempty"` MaxSize *string `json:"max_size,omitempty"`
Delay *int `json:"delay,omitempty"` Delay *int `json:"delay,omitempty"`
Priority *int32 `json:"priority,omitempty"` Priority *int32 `json:"priority,omitempty"`
MaxDownloads *int `json:"max_downloads,omitempty"` MaxDownloads *int `json:"max_downloads,omitempty"`
MaxDownloadsUnit *FilterMaxDownloadsUnit `json:"max_downloads_unit,omitempty"` MaxDownloadsUnit *FilterMaxDownloadsUnit `json:"max_downloads_unit,omitempty"`
MatchReleases *string `json:"match_releases,omitempty"` MatchReleases *string `json:"match_releases,omitempty"`
ExceptReleases *string `json:"except_releases,omitempty"` ExceptReleases *string `json:"except_releases,omitempty"`
UseRegex *bool `json:"use_regex,omitempty"` UseRegex *bool `json:"use_regex,omitempty"`
MatchReleaseGroups *string `json:"match_release_groups,omitempty"` MatchReleaseGroups *string `json:"match_release_groups,omitempty"`
ExceptReleaseGroups *string `json:"except_release_groups,omitempty"` ExceptReleaseGroups *string `json:"except_release_groups,omitempty"`
MatchReleaseTags *string `json:"match_release_tags,omitempty"` MatchReleaseTags *string `json:"match_release_tags,omitempty"`
ExceptReleaseTags *string `json:"except_release_tags,omitempty"` ExceptReleaseTags *string `json:"except_release_tags,omitempty"`
UseRegexReleaseTags *bool `json:"use_regex_release_tags,omitempty"` UseRegexReleaseTags *bool `json:"use_regex_release_tags,omitempty"`
MatchDescription *string `json:"match_description,omitempty"` MatchDescription *string `json:"match_description,omitempty"`
ExceptDescription *string `json:"except_description,omitempty"` ExceptDescription *string `json:"except_description,omitempty"`
UseRegexDescription *bool `json:"use_regex_description,omitempty"` UseRegexDescription *bool `json:"use_regex_description,omitempty"`
Scene *bool `json:"scene,omitempty"` Scene *bool `json:"scene,omitempty"`
Origins *[]string `json:"origins,omitempty"` Origins *[]string `json:"origins,omitempty"`
ExceptOrigins *[]string `json:"except_origins,omitempty"` ExceptOrigins *[]string `json:"except_origins,omitempty"`
Bonus *[]string `json:"bonus,omitempty"` Bonus *[]string `json:"bonus,omitempty"`
Freeleech *bool `json:"freeleech,omitempty"` Freeleech *bool `json:"freeleech,omitempty"`
FreeleechPercent *string `json:"freeleech_percent,omitempty"` FreeleechPercent *string `json:"freeleech_percent,omitempty"`
SmartEpisode *bool `json:"smart_episode,omitempty"` SmartEpisode *bool `json:"smart_episode,omitempty"`
Shows *string `json:"shows,omitempty"` Shows *string `json:"shows,omitempty"`
Seasons *string `json:"seasons,omitempty"` Seasons *string `json:"seasons,omitempty"`
Episodes *string `json:"episodes,omitempty"` Episodes *string `json:"episodes,omitempty"`
Resolutions *[]string `json:"resolutions,omitempty"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p. Resolutions *[]string `json:"resolutions,omitempty"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p.
Codecs *[]string `json:"codecs,omitempty"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux). Codecs *[]string `json:"codecs,omitempty"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux).
Sources *[]string `json:"sources,omitempty"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC Sources *[]string `json:"sources,omitempty"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC
Containers *[]string `json:"containers,omitempty"` Containers *[]string `json:"containers,omitempty"`
MatchHDR *[]string `json:"match_hdr,omitempty"` MatchHDR *[]string `json:"match_hdr,omitempty"`
ExceptHDR *[]string `json:"except_hdr,omitempty"` ExceptHDR *[]string `json:"except_hdr,omitempty"`
MatchOther *[]string `json:"match_other,omitempty"` MatchOther *[]string `json:"match_other,omitempty"`
ExceptOther *[]string `json:"except_other,omitempty"` ExceptOther *[]string `json:"except_other,omitempty"`
Years *string `json:"years,omitempty"` Years *string `json:"years,omitempty"`
Artists *string `json:"artists,omitempty"` Artists *string `json:"artists,omitempty"`
Albums *string `json:"albums,omitempty"` Albums *string `json:"albums,omitempty"`
MatchReleaseTypes *[]string `json:"match_release_types,omitempty"` // Album,Single,EP MatchReleaseTypes *[]string `json:"match_release_types,omitempty"` // Album,Single,EP
ExceptReleaseTypes *string `json:"except_release_types,omitempty"` ExceptReleaseTypes *string `json:"except_release_types,omitempty"`
Formats *[]string `json:"formats,omitempty"` // MP3, FLAC, Ogg, AAC, AC3, DTS Formats *[]string `json:"formats,omitempty"` // MP3, FLAC, Ogg, AAC, AC3, DTS
Quality *[]string `json:"quality,omitempty"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other Quality *[]string `json:"quality,omitempty"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
Media *[]string `json:"media,omitempty"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other Media *[]string `json:"media,omitempty"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
PerfectFlac *bool `json:"perfect_flac,omitempty"` PerfectFlac *bool `json:"perfect_flac,omitempty"`
Cue *bool `json:"cue,omitempty"` Cue *bool `json:"cue,omitempty"`
Log *bool `json:"log,omitempty"` Log *bool `json:"log,omitempty"`
LogScore *int `json:"log_score,omitempty"` LogScore *int `json:"log_score,omitempty"`
MatchCategories *string `json:"match_categories,omitempty"` MatchCategories *string `json:"match_categories,omitempty"`
ExceptCategories *string `json:"except_categories,omitempty"` ExceptCategories *string `json:"except_categories,omitempty"`
MatchUploaders *string `json:"match_uploaders,omitempty"` MatchUploaders *string `json:"match_uploaders,omitempty"`
ExceptUploaders *string `json:"except_uploaders,omitempty"` ExceptUploaders *string `json:"except_uploaders,omitempty"`
MatchLanguage *[]string `json:"match_language,omitempty"` MatchLanguage *[]string `json:"match_language,omitempty"`
ExceptLanguage *[]string `json:"except_language,omitempty"` ExceptLanguage *[]string `json:"except_language,omitempty"`
Tags *string `json:"tags,omitempty"` Tags *string `json:"tags,omitempty"`
ExceptTags *string `json:"except_tags,omitempty"` ExceptTags *string `json:"except_tags,omitempty"`
TagsAny *string `json:"tags_any,omitempty"` TagsAny *string `json:"tags_any,omitempty"`
ExceptTagsAny *string `json:"except_tags_any,omitempty"` ExceptTagsAny *string `json:"except_tags_any,omitempty"`
TagsMatchLogic *string `json:"tags_match_logic,omitempty"` TagsMatchLogic *string `json:"tags_match_logic,omitempty"`
ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"` ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"`
ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"` ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"`
ExternalScriptCmd *string `json:"external_script_cmd,omitempty"` ExternalScriptCmd *string `json:"external_script_cmd,omitempty"`
ExternalScriptArgs *string `json:"external_script_args,omitempty"` ExternalScriptArgs *string `json:"external_script_args,omitempty"`
ExternalScriptExpectStatus *int `json:"external_script_expect_status,omitempty"` ExternalScriptExpectStatus *int `json:"external_script_expect_status,omitempty"`
ExternalWebhookEnabled *bool `json:"external_webhook_enabled,omitempty"` ExternalWebhookEnabled *bool `json:"external_webhook_enabled,omitempty"`
ExternalWebhookHost *string `json:"external_webhook_host,omitempty"` ExternalWebhookHost *string `json:"external_webhook_host,omitempty"`
ExternalWebhookData *string `json:"external_webhook_data,omitempty"` ExternalWebhookData *string `json:"external_webhook_data,omitempty"`
ExternalWebhookExpectStatus *int `json:"external_webhook_expect_status,omitempty"` ExternalWebhookExpectStatus *int `json:"external_webhook_expect_status,omitempty"`
Actions []*Action `json:"actions,omitempty"` ExternalWebhookRetryStatus *string `json:"external_webhook_retry_status,omitempty"`
External []FilterExternal `json:"external,omitempty"` ExternalWebhookRetryAttempts *int `json:"external_webhook_retry_attempts,omitempty"`
Indexers []Indexer `json:"indexers,omitempty"` ExternalWebhookRetryDelaySeconds *int `json:"external_webhook_retry_delay_seconds,omitempty"`
ExternalWebhookRetryMaxJitterSeconds *int `json:"external_webhook_retry_max_jitter_seconds,omitempty"`
Actions []*Action `json:"actions,omitempty"`
External []FilterExternal `json:"external,omitempty"`
Indexers []Indexer `json:"indexers,omitempty"`
} }
func (f Filter) CheckFilter(r *Release) ([]string, bool) { func (f Filter) CheckFilter(r *Release) ([]string, bool) {

View file

@ -13,14 +13,17 @@ import (
"os" "os"
"os/exec" "os/exec"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/indexer" "github.com/autobrr/autobrr/internal/indexer"
"github.com/autobrr/autobrr/internal/logger" "github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/internal/utils"
"github.com/autobrr/autobrr/pkg/errors" "github.com/autobrr/autobrr/pkg/errors"
"github.com/avast/retry-go/v4"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -730,25 +733,53 @@ func (s *service) webhook(ctx context.Context, external domain.FilterExternal, r
} }
} }
var opts []retry.Option
if external.WebhookRetryAttempts > 0 {
option := retry.Attempts(uint(external.WebhookRetryAttempts))
opts = append(opts, option)
}
if external.WebhookRetryDelaySeconds > 0 {
option := retry.Delay(time.Duration(external.WebhookRetryDelaySeconds) * time.Second)
opts = append(opts, option)
}
if external.WebhookRetryMaxJitterSeconds > 0 {
option := retry.MaxJitter(time.Duration(external.WebhookRetryMaxJitterSeconds) * time.Second)
opts = append(opts, option)
}
start := time.Now() start := time.Now()
res, err := client.Do(req) statusCode, err := retry.DoWithData(
if err != nil { func() (int, error) {
return 0, errors.Wrap(err, "could not make request for webhook") res, err := client.Do(req)
} if err != nil {
return 0, errors.Wrap(err, "could not make request for webhook")
}
defer res.Body.Close() defer res.Body.Close()
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return 0, errors.Wrap(err, "could not read request body") return 0, errors.Wrap(err, "could not read request body")
} }
if len(body) > 0 { if len(body) > 0 {
s.log.Debug().Msgf("filter external webhook response status: %d body: %s", res.StatusCode, body) s.log.Debug().Msgf("filter external webhook response status: %d body: %s", res.StatusCode, body)
} }
if external.WebhookRetryStatus != "" {
retryStatusCodes := strings.Split(strings.ReplaceAll(external.WebhookRetryStatus, " ", ""), ",")
if utils.StrSliceContains(retryStatusCodes, strconv.Itoa(res.StatusCode)) {
return 0, errors.New("retrying webhook request, got status code: %d", res.StatusCode)
}
}
return res.StatusCode, nil
},
opts...)
s.log.Debug().Msgf("successfully ran external webhook filter to: (%s) payload: (%s) finished in %s", external.WebhookHost, dataArgs, time.Since(start)) s.log.Debug().Msgf("successfully ran external webhook filter to: (%s) payload: (%s) finished in %s", external.WebhookHost, dataArgs, time.Since(start))
return res.StatusCode, nil return statusCode, err
} }

View file

@ -218,6 +218,10 @@ const externalFilterSchema = z.object({
webhook_method: z.string().optional(), webhook_method: z.string().optional(),
webhook_data: z.string().optional(), webhook_data: z.string().optional(),
webhook_expect_status: z.number().optional(), webhook_expect_status: z.number().optional(),
webhook_retry_status: z.string().optional(),
webhook_retry_attempts: z.number().optional(),
webhook_retry_delay_seconds: z.number().optional(),
webhook_retry_max_jitter_seconds: z.number().optional(),
}); });
const indexerSchema = z.object({ const indexerSchema = z.object({

View file

@ -312,12 +312,31 @@ const TypeForm = ({ external, idx }: TypeFormProps) => {
rows={5} rows={5}
placeholder={"Request data: { \"key\": \"value\" }"} placeholder={"Request data: { \"key\": \"value\" }"}
/> />
<NumberField <NumberField
name={`external.${idx}.webhook_expect_status`} name={`external.${idx}.webhook_expect_status`}
label="Expected http status" label="Expected http status code"
placeholder="200" placeholder="200"
/> />
<TextField
name={`external.${idx}.webhook_retry_status`}
label="Retry http status code(s)"
placeholder="Retry on status eg. 202, 204"
/>
<NumberField
name={`external.${idx}.webhook_retry_attempts`}
label="Maximum retry attempts"
placeholder="10"
/>
<NumberField
name={`external.${idx}.webhook_retry_delay_seconds`}
label="Retry delay in seconds"
placeholder="1"
/>
<NumberField
name={`external.${idx}.webhook_retry_max_jitter_seconds`}
label="Max jitter in seconds"
placeholder="1"
/>
</div> </div>
); );

View file

@ -292,6 +292,10 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
external_webhook_host: any; external_webhook_host: any;
external_webhook_data: any; external_webhook_data: any;
external_webhook_expect_status: any; external_webhook_expect_status: any;
external_webhook_retry_status: any;
external_webhook_retry_attempts: any;
external_webhook_retry_delay_seconds: any;
external_webhook_retry_max_jitter_seconds: any;
}; };
const completeFilter = await APIClient.filters.getByID(filter.id) as Partial<CompleteFilterType>; const completeFilter = await APIClient.filters.getByID(filter.id) as Partial<CompleteFilterType>;
@ -313,6 +317,10 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
delete completeFilter.external_webhook_host; delete completeFilter.external_webhook_host;
delete completeFilter.external_webhook_data; delete completeFilter.external_webhook_data;
delete completeFilter.external_webhook_expect_status; delete completeFilter.external_webhook_expect_status;
delete completeFilter.external_webhook_retry_status;
delete completeFilter.external_webhook_retry_attempts;
delete completeFilter.external_webhook_retry_delay_seconds;
delete completeFilter.external_webhook_retry_max_jitter_seconds;
// Remove properties with default values from the exported filter to minimize the size of the JSON string // Remove properties with default values from the exported filter to minimize the size of the JSON string
["enabled", "priority", "smart_episode", "resolutions", "sources", "codecs", "containers", "tags_match_logic", "except_tags_match_logic"].forEach((key) => { ["enabled", "priority", "smart_episode", "resolutions", "sources", "codecs", "containers", "tags_match_logic", "except_tags_match_logic"].forEach((key) => {

View file

@ -130,5 +130,9 @@ interface ExternalFilter {
webhook_data?: string, webhook_data?: string,
webhook_headers?: string; webhook_headers?: string;
webhook_expect_status?: number; webhook_expect_status?: number;
webhook_retry_status?: string,
webhook_retry_attempts?: number;
webhook_retry_delay_seconds?: number;
webhook_retry_max_jitter_seconds?: number;
filter_id?: number; filter_id?: number;
} }