Skip to content

Commit

Permalink
feat: add support for orpheus
Browse files Browse the repository at this point in the history
  • Loading branch information
s0up4200 committed Nov 7, 2023
1 parent 855dfde commit 9d65b5a
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 32 deletions.
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# RedactedHook

RedactedHook is a webhook companion service for [autobrr](https://github.com/autobrr/autobrr) designed to check the names of uploaders, your ratio, and record labels associated with torrents on Redacted. It provides a simple and efficient way to validate if uploaders are blacklisted or whitelisted, to stop racing in case your ratio falls below a certain point, and to verify if a torrent's record label matches against a specified list.
RedactedHook is a webhook companion service for [autobrr](https://github.com/autobrr/autobrr) designed to check the names of uploaders, your ratio, and record labels associated with torrents on **Redacted** and **Orpheus**. It provides a simple and efficient way to validate if uploaders are blacklisted or whitelisted, to stop racing in case your ratio falls below a certain point, and to verify if a torrent's record label matches against a specified list.

## Features

- Verify if an uploader's name is on a provided whitelist or blacklist.
- Check for record labels. Useful for grabbing torrents from a specific label.
- Check if a user's ratio meets a specified minimum value (additional API hit if used with the other two).
- Check for record labels. Useful for grabbing torrents from a specific record label.
- Check if a user's ratio meets a specified minimum value.
- Check the torrentSize (Useful for not hitting the API from both autobrr and redactedhook)
- Easy to integrate with other applications via webhook.
- Rate Limiter set to a maximum of 10 requests per 10 seconds to abide by RED rules.
- Rate-limited to comply with tracker API request policies.

It was made with [autobrr](https://github.com/autobrr/autobrr) in mind.

## Getting Started

### Warning

Remember that autobrr also checks the RED API if you have min/max sizes set. This will result in you hitting the API 2x.
Remember that autobrr also checks the RED/OPS API if you have min/max sizes set. This will result in you hitting the API 2x.
So for your own good, don't set size checks in your autobrr filter is you use RedactedHook.

### Prerequisites
Expand Down Expand Up @@ -88,16 +88,17 @@ make build

To use RedactedHook, send POST requests to the following endpoint:

Endpoint: http://127.0.0.1:42135/redacted/hook
Endpoint: http://127.0.0.1:42135/hook
Method: POST
Expected HTTP Status: 200

You can check the ratio, uploader, size and record label in a single request or separately. Keep in mind that checking the ratio will result in an additional API hit on the RED API, as it's not part of the same endpoint as uploaders and record labels.
You can check the ratio, uploader, size and record label in a single request or separately.

**JSON Payload for everything:**

```json
{
"indexer": {{.Indexer}},
"user_id": USER_ID,
"apikey": "API_KEY",
"minratio": MINIMUM_RATIO,
Expand All @@ -112,6 +113,7 @@ You can check the ratio, uploader, size and record label in a single request or

```json
{
"indexer": {{.Indexer}},
"user_id": USER_ID,
"apikey": "API_KEY",
"minratio": MINIMUM_RATIO
Expand All @@ -122,6 +124,7 @@ You can check the ratio, uploader, size and record label in a single request or

```json
{
"indexer": {{.Indexer}},
"torrent_id": {{.TorrentID}},
"apikey": "API_KEY",
"uploaders": "USER1,USER2,USER3",
Expand All @@ -134,15 +137,21 @@ You can check the ratio, uploader, size and record label in a single request or

```json
{
"indexer": {{.Indexer}},
"torrent_id": {{.TorrentID}},
"apikey": "API_KEY",
"record_labels": "LABEL1,LABEL2,LABEL3"
}
```
`indexer` is the indexer that pushed the release within autobrr.

`torrent_id` is the TorrentID of the pushed release within autobrr.

`user_id` is the number in the URL when you visit your profile.

`api_key` is your Redacted API key. Needs user and torrents privileges.
`red_apikey` is your Redacted API key. Needs user and torrents privileges.

`ops_apikey` is your Orpheus API key. Needs user and torrents privileges.

`minsize` is the minimum allowed size **measured in bytes** you want to grab.

Expand All @@ -157,12 +166,12 @@ You can check the ratio, uploader, size and record label in a single request or
#### curl commands for easy testing

```bash
curl -X POST -H "Content-Type: application/json" -d '{"user_id": 3855, "apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "minratio": 1.0}' http://127.0.0.1:42135/redacted/hook
curl -X POST -H "Content-Type: application/json" -d '{"indexer": "redacted", "user_id": 3855, "red_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "ops_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "minratio": 1.0}' http://127.0.0.1:42135/hook
```
```bash
curl -X POST -H "Content-Type: application/json" -d '{"torrent_id": 3931392, "apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "mode": "blacklist", "uploaders": "blacklisted_user1,blacklisted_user2,blacklisted_user3"}' http://127.0.0.1:42135/redacted/hook
curl -X POST -H "Content-Type: application/json" -d '{"indexer": "redacted", "torrent_id": 3931392, "red_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "ops_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "mode": "blacklist", "uploaders": "blacklisted_user1,blacklisted_user2,blacklisted_user3"}' http://127.0.0.1:42135/hook
```

```bash
curl -X POST -H "Content-Type: application/json" -d '{"torrent_id": 3931392, "apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "maxsize": 340155737}' http://127.0.0.1:42135/redacted/hook
curl -X POST -H "Content-Type: application/json" -d '{"indexer": "redacted", "torrent_id": 3931392, "red_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "ops_apikey": "e1be0c8f.6a1d6f89de6e9f6a61e6edcbb6a3a32d", "maxsize": 340155737}' http://127.0.0.1:42135/hook
```
124 changes: 103 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import (
)

const (
APIEndpointBase = "https://redacted.ch/ajax.php"
Pathhook = "/redacted/hook"
APIEndpointBaseRedacted = "https://redacted.ch/ajax.php"
APIEndpointBaseOrpheus = "https://orpheus.network/ajax.php"
Pathhook = "/hook"
)

// Rate limit requests to max 10 requests per 10 seconds
var limiter = rate.NewLimiter(rate.Every(1*time.Second), 10)
var redactedLimiter = rate.NewLimiter(rate.Every(1*time.Second), 10) // 10 requests per 10 seconds for Redacted
var orpheusLimiter = rate.NewLimiter(rate.Every(2*time.Second), 1) // 5 requests per 10 seconds for Orpheus

//var (
// version = "dev"
Expand All @@ -30,13 +31,15 @@ var limiter = rate.NewLimiter(rate.Every(1*time.Second), 10)
type RequestData struct {
UserID int `json:"user_id,omitempty"`
TorrentID int `json:"torrent_id,omitempty"`
APIKey string `json:"apikey"`
REDKey string `json:"red_apikey,omitempty"`
OPSKey string `json:"ops_apikey,omitempty"`
MinRatio float64 `json:"minratio,omitempty"`
MinSize int64 `json:"minsize,omitempty"`
MaxSize int64 `json:"maxsize,omitempty"`
Uploaders string `json:"uploaders,omitempty"`
RecordLabel string `json:"record_labels,omitempty"`
Mode string `json:"mode,omitempty"`
Indexer string `json:"indexer"`
}

type ResponseData struct {
Expand All @@ -60,14 +63,27 @@ type ResponseData struct {
} `json:"response"`
}

func fetchTorrentData(torrentID int, apiKey string) (*ResponseData, error) {
func fetchTorrentData(torrentID int, apiKey string, apiBase string, indexer string) (*ResponseData, error) {

// Determine the correct limiter based on the indexer
var limiter *rate.Limiter
switch indexer {
case "redacted":
limiter = redactedLimiter
case "ops":
limiter = orpheusLimiter
default:
// Return an error instead of using http.Error
return nil, fmt.Errorf("invalid indexer")
}

// Use the limiter
if !limiter.Allow() {
log.Warn().Msg("Too many requests (fetchTorrentData)")
log.Warn().Msgf(("%s: Too many requests (fetchTorrentData)"), indexer)
return nil, fmt.Errorf("too many requests")
}

endpoint := fmt.Sprintf("%s?action=torrent&id=%d", APIEndpointBase, torrentID)
endpoint := fmt.Sprintf("%s?action=torrent&id=%d", apiBase, torrentID)
req, err := http.NewRequest("GET", endpoint, nil)
req.Header.Set("Authorization", apiKey)

Expand Down Expand Up @@ -96,21 +112,40 @@ func fetchTorrentData(torrentID int, apiKey string) (*ResponseData, error) {
}

if responseData.Status != "success" {
log.Warn().Msgf("Received API response from RED with status '%s' and error message: '%s'", responseData.Status, responseData.Error)
return nil, fmt.Errorf("API error: %s", responseData.Error)
var sourceName string
if indexer == "redacted" {
sourceName = "RED"
} else if indexer == "ops" {
sourceName = "OPS"
}
log.Warn().Msgf("Received API response from %s with status '%s' and error message: '%s'", sourceName, responseData.Status, responseData.Error)
return nil, fmt.Errorf("API error from %s: %s", sourceName, responseData.Error)
}

return &responseData, nil
}

func fetchUserData(userID int, apiKey string) (*ResponseData, error) {
func fetchUserData(userID int, apiKey string, indexer string, apiBase string) (*ResponseData, error) {

// Determine the correct limiter based on the indexer
var limiter *rate.Limiter
switch indexer {
case "redacted":
limiter = redactedLimiter
case "ops":
limiter = orpheusLimiter
default:
// Return an error instead of using http.Error
return nil, fmt.Errorf("invalid indexer")
}

// Use the limiter
if !limiter.Allow() {
log.Warn().Msg("Too many requests (fetchUserData)")
log.Warn().Msgf(("%s: Too many requests (fetchUserData)"), indexer)
return nil, fmt.Errorf("too many requests")
}

endpoint := fmt.Sprintf("%s?action=user&id=%d", APIEndpointBase, userID)
endpoint := fmt.Sprintf("%s?action=user&id=%d", apiBase, userID)
req, err := http.NewRequest("GET", endpoint, nil)
req.Header.Set("Authorization", apiKey)

Expand Down Expand Up @@ -139,8 +174,14 @@ func fetchUserData(userID int, apiKey string) (*ResponseData, error) {
}

if responseData.Status != "success" {
log.Warn().Msgf("Received API response from RED with status '%s' and error message: '%s'", responseData.Status, responseData.Error)
return nil, fmt.Errorf("API error: %s", responseData.Error)
var sourceName string
if indexer == "redacted" {
sourceName = "RED"
} else if indexer == "ops" {
sourceName = "OPS"
}
log.Warn().Msgf("Received API response from %s with status '%s' and error message: '%s'", sourceName, responseData.Status, responseData.Error)
return nil, fmt.Errorf("API error from %s: %s", sourceName, responseData.Error)
}

return &responseData, nil
Expand All @@ -151,7 +192,6 @@ func hookData(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Only POST method is supported", http.StatusBadRequest)
return
}

var torrentData *ResponseData
var userData *ResponseData

Expand All @@ -174,14 +214,38 @@ func hookData(w http.ResponseWriter, r *http.Request) {
return
}

// Determine the appropriate API base based on the requested hook path
var apiBase string
switch requestData.Indexer {
case "redacted":
apiBase = APIEndpointBaseRedacted
case "ops":
apiBase = APIEndpointBaseOrpheus
default:
http.Error(w, "Invalid path", http.StatusNotFound)
return
}

reqHeader := make(http.Header)
reqHeader.Set("Authorization", requestData.APIKey)
var apiKey string
if requestData.Indexer == "redacted" {
apiKey = requestData.REDKey
} else if requestData.Indexer == "ops" {
apiKey = requestData.OPSKey
}
reqHeader.Set("Authorization", apiKey)

// hook ratio
if requestData.UserID != 0 && requestData.MinRatio != 0 {

if userData == nil {
userData, err = fetchUserData(requestData.UserID, requestData.APIKey)
var apiKey string
if requestData.Indexer == "redacted" {
apiKey = requestData.REDKey
} else if requestData.Indexer == "ops" {
apiKey = requestData.OPSKey
}
userData, err = fetchUserData(requestData.UserID, apiKey, apiBase, requestData.Indexer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand All @@ -204,7 +268,13 @@ func hookData(w http.ResponseWriter, r *http.Request) {
// hook uploader
if requestData.TorrentID != 0 && requestData.Uploaders != "" {
if torrentData == nil {
torrentData, err = fetchTorrentData(requestData.TorrentID, requestData.APIKey)
var apiKey string
if requestData.Indexer == "redacted" {
apiKey = requestData.REDKey
} else if requestData.Indexer == "ops" {
apiKey = requestData.OPSKey
}
torrentData, err = fetchTorrentData(requestData.TorrentID, apiKey, apiBase, requestData.Indexer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -237,7 +307,13 @@ func hookData(w http.ResponseWriter, r *http.Request) {
// hook record label
if requestData.TorrentID != 0 && requestData.RecordLabel != "" {
if torrentData == nil {
torrentData, err = fetchTorrentData(requestData.TorrentID, requestData.APIKey)
var apiKey string
if requestData.Indexer == "redacted" {
apiKey = requestData.REDKey
} else if requestData.Indexer == "ops" {
apiKey = requestData.OPSKey
}
torrentData, err = fetchTorrentData(requestData.TorrentID, apiKey, apiBase, requestData.Indexer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -291,7 +367,13 @@ func hookData(w http.ResponseWriter, r *http.Request) {
// hook size
if requestData.TorrentID != 0 && (requestData.MinSize != 0 || requestData.MaxSize != 0) {
if torrentData == nil {
torrentData, err = fetchTorrentData(requestData.TorrentID, requestData.APIKey)
var apiKey string
if requestData.Indexer == "redacted" {
apiKey = requestData.REDKey
} else if requestData.Indexer == "ops" {
apiKey = requestData.OPSKey
}
torrentData, err = fetchTorrentData(requestData.TorrentID, apiKey, apiBase, requestData.Indexer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down

0 comments on commit 9d65b5a

Please sign in to comment.