Add initial support in webhooks for sub-users.
This commit is contained in:
21
FAQ.md
21
FAQ.md
@@ -479,15 +479,21 @@ $ docker exec -ti watchstate console system:env --list
|
||||
|
||||
### How to add webhooks?
|
||||
|
||||
The Webhook URL is backend specific, the request path is `/v1/api/backend/[BACKEND_NAME]/webhook`,
|
||||
Where `[BACKEND_NAME]` is the name of the backend you want to add webhook for. Typically, the full URL
|
||||
is `http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`. Or simply go to the `WebUI > Backends` and click
|
||||
The Webhook URL is backend specific, the request path is `/v1/api/backend/[USER]@[BACKEND_NAME]/webhook`,
|
||||
Where `[USER]` is the username for sub user or `main` for main user and `[BACKEND_NAME]` is the name of the backend you
|
||||
want to add webhook for. Typically, the full URL
|
||||
is `http://localhost:8080/v1/api/backend/[USER]@[BACKEND_NAME]/webhook`. Or simply go to the `WebUI > Backends` and
|
||||
click
|
||||
on `Copy Webhook URL`.
|
||||
|
||||
> [!NOTE]
|
||||
> You will keep seeing the `webhook.token` key, it's being kept for backward compatibility, and will be removed in the
|
||||
> future. It has no effect except as pointer to the new method.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As support more sub users expands, it's important to turn on `Webhook match user` for all backends to prevent
|
||||
> sub users from changing the main user watch state. in case of plex backend.
|
||||
|
||||
-----
|
||||
|
||||
#### Emby (you need `Emby Premiere` to use webhooks).
|
||||
@@ -496,9 +502,10 @@ Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook)
|
||||
|
||||
##### Webhook/Notifications URL:
|
||||
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`
|
||||
`http://localhost:8080/v1/api/backend/[USER]@[BACKEND_NAME]/webhook`
|
||||
|
||||
* Replace `[BACKEND_NAME]` with the name you have chosen for your backend.
|
||||
* Replace `[USER]` with the `main` for main user or the sub user username.
|
||||
|
||||
##### Request content type (Emby v4.9+):
|
||||
|
||||
@@ -536,9 +543,10 @@ Go to your Plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK
|
||||
|
||||
##### URL:
|
||||
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`
|
||||
`http://localhost:8080/v1/api/backend/[USER]@[BACKEND_NAME]/webhook`
|
||||
|
||||
* Replace `[BACKEND_NAME]` with the name you have chosen for your backend.
|
||||
* Replace `[USER]` with the `main` for main user or the sub user username.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you have enabled `WS_SECURE_API_ENDPOINTS`, you have to add `?apikey=yourapikey` to the end of the URL.
|
||||
@@ -566,9 +574,10 @@ go back again to dashboard > plugins > webhook. Add `Add Generic Destination`,
|
||||
|
||||
##### Webhook Url:
|
||||
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`
|
||||
`http://localhost:8080/v1/api/backend/[USER]@[BACKEND_NAME]/webhook`
|
||||
|
||||
* Replace `[BACKEND_NAME]` with the name you have chosen for your backend.
|
||||
* Replace `[USER]` with the `main` for main user or the sub user username.
|
||||
|
||||
##### Notification Type:
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ return (function () {
|
||||
'secure' => (bool)env('WS_SECURE_API_ENDPOINTS', false),
|
||||
'auto' => (bool)env('WS_API_AUTO', false),
|
||||
'pattern_match' => [
|
||||
'backend' => '[a-zA-Z0-9_-]+',
|
||||
'backend' => '[a-zA-Z0-9_\-]+',
|
||||
'ubackend' => '[a-zA-Z0-9_\-\@]+',
|
||||
],
|
||||
'logInternal' => (bool)env('WS_API_LOG_INTERNAL', false),
|
||||
'response' => [
|
||||
|
||||
@@ -6,11 +6,13 @@ namespace App\API\Backend;
|
||||
|
||||
use App\Libs\Attributes\Route\Route;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface as iState;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
use App\Libs\Extends\LogMessageProcessor;
|
||||
use App\Libs\LogSuppressor;
|
||||
use App\Libs\Mappers\Import\DirectMapper;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use App\Libs\Uri;
|
||||
@@ -55,7 +57,7 @@ final class Webhooks
|
||||
*
|
||||
* @return iResponse The response object.
|
||||
*/
|
||||
#[Route(['POST', 'PUT'], Index::URL . '/{name:backend}/webhook[/]', name: 'backend.webhook')]
|
||||
#[Route(['POST', 'PUT'], Index::URL . '/{name:ubackend}/webhook[/]', name: 'backend.webhook')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
@@ -76,14 +78,35 @@ final class Webhooks
|
||||
private function process(string $name, iRequest $request): iResponse
|
||||
{
|
||||
try {
|
||||
$backend = $this->getBackends(name: $name);
|
||||
// -- sub-user
|
||||
if (true === str_contains($name, '@')) {
|
||||
[$user, $ubackend] = explode('@', $name, 2);
|
||||
} else {
|
||||
$user = 'main';
|
||||
$ubackend = $name;
|
||||
}
|
||||
|
||||
try {
|
||||
$userContext = getUserContext(
|
||||
user: $user,
|
||||
mapper: Container::get(DirectMapper::class),
|
||||
logger: Container::get(iLogger::class)
|
||||
);
|
||||
} catch (RuntimeException $ex) {
|
||||
return api_error($ex->getMessage(), Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
$backend = $this->getBackends(name: $ubackend, userContext: $userContext);
|
||||
if (empty($backend)) {
|
||||
throw new RuntimeException(r("Backend '{backend}' not found.", ['backend ' => $name]));
|
||||
throw new RuntimeException(r("Backend '{user}@{backend}' {backends} not found.", [
|
||||
'user' => $user,
|
||||
'backend' => $ubackend,
|
||||
]));
|
||||
}
|
||||
|
||||
$backend = array_pop($backend);
|
||||
|
||||
$client = $this->getClient(name: $name);
|
||||
$client = $this->getClient(name: $ubackend, userContext: $userContext);
|
||||
} catch (RuntimeException $e) {
|
||||
return api_error($e->getMessage(), Status::NOT_FOUND);
|
||||
}
|
||||
@@ -103,7 +126,7 @@ final class Webhooks
|
||||
}
|
||||
|
||||
if (false === hash_equals((string)$userId, (string)$requestUser)) {
|
||||
$message = r('Request user id [{req_user}] does not match configured value [{config_user}]', [
|
||||
$message = r("Request user id '{req_user}' does not match configured value '{config_user}'.", [
|
||||
'req_user' => $requestUser ?? 'NOT SET',
|
||||
'config_user' => $userId,
|
||||
]);
|
||||
@@ -120,7 +143,7 @@ final class Webhooks
|
||||
}
|
||||
|
||||
if (false === hash_equals((string)$uuid, (string)$requestBackendId)) {
|
||||
$message = r('Request backend unique id [{req_uid}] does not match backend uuid [{config_uid}].', [
|
||||
$message = r("Request backend unique id '{req_uid}' does not match backend uuid '{config_uid}'.", [
|
||||
'req_uid' => $requestBackendId ?? 'NOT SET',
|
||||
'config_uid' => $uuid,
|
||||
]);
|
||||
@@ -139,7 +162,8 @@ final class Webhooks
|
||||
|
||||
if (true !== $metadataOnly && true !== (bool)ag($backend, 'import.enabled')) {
|
||||
$response = api_response(Status::NOT_ACCEPTABLE);
|
||||
$this->write($request, Level::Error, r('Import are disabled for [{backend}].', [
|
||||
$this->write($request, Level::Error, r("Import are disabled for '{user}@{backend}'.", [
|
||||
'user' => $userContext->name,
|
||||
'backend' => $client->getName(),
|
||||
]), forceContext: true);
|
||||
|
||||
@@ -156,8 +180,9 @@ final class Webhooks
|
||||
$this->write(
|
||||
$request,
|
||||
Level::Info,
|
||||
'Ignoring [{backend}] {item.type} [{item.title}]. No valid/supported external ids.',
|
||||
"Ignoring '{user}@{backend}' {item.type} '{item.title}'. No valid/supported external ids.",
|
||||
[
|
||||
'user' => $userContext->name,
|
||||
'backend' => $entity->via,
|
||||
'item' => [
|
||||
'title' => $entity->getName(),
|
||||
@@ -173,8 +198,9 @@ final class Webhooks
|
||||
$this->write(
|
||||
$request,
|
||||
Level::Notice,
|
||||
'Ignoring [{backend}] {item.type} [{item.title}]. No episode/season number present.',
|
||||
"Ignoring '{user}@{backend}' {item.type} '{item.title}'. No episode/season number present.",
|
||||
[
|
||||
'user' => $userContext->name,
|
||||
'backend' => $entity->via,
|
||||
'item' => [
|
||||
'title' => $entity->getName(),
|
||||
@@ -188,7 +214,8 @@ final class Webhooks
|
||||
return api_response(Status::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
$itemId = r('{type}://{id}:{tainted}@{backend}', [
|
||||
$itemId = r('{type}://{id}:{tainted}@{backend}/{user}', [
|
||||
'user' => $userContext->name,
|
||||
'type' => $entity->type,
|
||||
'backend' => $entity->via,
|
||||
'tainted' => $entity->isTainted() ? 'tainted' : 'untainted',
|
||||
@@ -203,13 +230,15 @@ final class Webhooks
|
||||
Options::IMPORT_METADATA_ONLY => $metadataOnly,
|
||||
Options::REQUEST_ID => ag($request->getServerParams(), 'X_REQUEST_ID'),
|
||||
],
|
||||
Options::CONTEXT_USER => $userContext->name,
|
||||
]);
|
||||
|
||||
$this->write(
|
||||
$request,
|
||||
Level::Info,
|
||||
"Queued {tainted} request '{backend}: {event}' {item.type} '{item.title}' - 'state: {state}, progress: {has_progress}'. request_id '{req}'.",
|
||||
"Queued {tainted} request '{user}@{backend}: {event}' {item.type} '{item.title}' - 'state: {state}, progress: {has_progress}'. request_id '{req}'.",
|
||||
[
|
||||
'user' => $userContext->name,
|
||||
'backend' => $entity->via,
|
||||
'event' => ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_EVENT),
|
||||
'has_progress' => $entity->hasPlayProgress() ? 'Yes' : 'No',
|
||||
|
||||
@@ -6,10 +6,12 @@ namespace App\API\Backends;
|
||||
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Psr\Log\LoggerInterface as iLogger;
|
||||
|
||||
final class Index
|
||||
{
|
||||
@@ -25,11 +27,12 @@ final class Index
|
||||
];
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'backends')]
|
||||
public function __invoke(iRequest $request): iResponse
|
||||
public function __invoke(iRequest $request, iEImport $mapper, iLogger $logger): iResponse
|
||||
{
|
||||
$list = [];
|
||||
$user = $this->getUserContext($request, $mapper, $logger);
|
||||
|
||||
foreach ($this->getBackends() as $backend) {
|
||||
foreach ($this->getBackends(userContext: $user) as $backend) {
|
||||
$item = array_filter(
|
||||
$backend,
|
||||
fn($key) => false === in_array($key, ['options', 'webhook'], true),
|
||||
|
||||
@@ -83,7 +83,8 @@ trait APITraits
|
||||
$backend = ag_set($backend, 'export.lastSync', $export ? makeDate($export) : null);
|
||||
}
|
||||
|
||||
$webhookUrl = parseConfigValue(Index::URL) . "/{$backendName}/webhook";
|
||||
$user = $userContext?->name ?? 'main';
|
||||
$webhookUrl = parseConfigValue(Index::URL) . "/{$user}@{$backendName}/webhook";
|
||||
|
||||
if (true === (bool)Config::get('api.secure')) {
|
||||
$webhookUrl .= '?apikey=' . Config::get('api.key');
|
||||
|
||||
Reference in New Issue
Block a user