Renamed :webhook command to :edit to better reflect the intention of the command and to expand the configurable settings via the command.

This commit is contained in:
Abdulmhsen B. A. A
2022-03-03 15:30:36 +03:00
parent 2d865db17a
commit aa00e5bd97
6 changed files with 332 additions and 182 deletions

View File

@@ -76,7 +76,7 @@ To start receiving webhook events from servers you need to do few more steps.
Run the following commands to generate api key for each server
```sh
$ docker exec -ti watchstate console servers:webhook [SERVER_NAME] --status=enable --push==enable --generate
$ docker exec -ti watchstate console servers:edit [SERVER_NAME] --webhook-import-status=enable --webhook-push-status=enable --webhook-key-generate
Server '[SERVER_NAME]' Webhook API key is: random_string
```
@@ -90,8 +90,22 @@ same API key, you still keep them separate but have the same `webhook.token` val
to whitelist IPs for each server.
```bash
docker exec -ti watchstate console servers:webhook [PLEX_SERVER_1] --require-ips 'comma seperated list of ips and CIDR'
docker exec -ti watchstate console servers:webhook [PLEX_SERVER_2] --require-ips '10.0.0.0/8,172.23.0.1,etc...'
docker exec -ti watchstate console servers:edit [PLEX_SERVER_1] --webhook-require-ips 'comma seperated list of ips/CIDR'
docker exec -ti watchstate console servers:edit [PLEX_SERVER_2] --webhook-require-ips '10.0.0.0/8,172.23.0.1,etc...'
```
You can also use server unique ID to identify the server, however you have to get the server ID manually by visiting
server settings > General then look at the ID in the URL for example
`https://app.plex.tv/desktop/#!/settings/server/[RANDOM_STRING]/settings/general`
##### [RANDOM_STRING]
will be a randomly generated string that looks like the 2nd example.
```bash
docker exec -ti watchstate console servers:edit [PLEX_SERVER_1] --webhook-server-uuid '[RANDOM_STRING]'
docker exec -ti watchstate console servers:edit [PLEX_SERVER_2] --webhook-server-uuid '0d4d365add01145852146d0c25b3776c1effc507'
```
The reason for this is, because Plex link webhook to user account instead of server, as such for all servers you have

View File

@@ -15,5 +15,5 @@ return [
'scheduler:closure' => App\Commands\Scheduler\RunClosure::class,
'webhooks:queued' => App\Commands\State\QueueCommand::class,
'servers:list' => App\Commands\Servers\ListCommand::class,
'servers:webhook' => App\Commands\Servers\WebhookCommand::class,
'servers:edit' => App\Commands\Servers\EditCommand::class,
];

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace App\Commands\Servers;
use App\Command;
use App\Libs\Config;
use Exception;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
final class EditCommand extends Command
{
private const ON_OFF_FLAGS = [
'enabled' => true,
'enable' => true,
'yes' => true,
'disabled' => false,
'disable' => false,
'no' => false,
];
protected function configure(): void
{
$values = implode('|', array_keys(self::ON_OFF_FLAGS));
$this->setName('servers:edit')
->setDescription('Edit Server settings.')
->addOption(
'type',
null,
InputOption::VALUE_REQUIRED,
sprintf(
'Change server type. Expected value is one of [%s]',
implode('|', array_keys(Config::get('supported', [])))
)
)
->addOption(
'url',
null,
InputOption::VALUE_REQUIRED,
'Change server url.'
)
->addOption(
'token',
null,
InputOption::VALUE_REQUIRED,
'Change server API key.'
)
->addOption(
'user',
null,
InputOption::VALUE_REQUIRED,
'Change server User Id.'
)
->addOption(
'export-status',
null,
InputOption::VALUE_REQUIRED,
sprintf('Enable/Disable manual exporting to this server. Expected value is one of [%s]', $values)
)
->addOption(
'import-status',
null,
InputOption::VALUE_REQUIRED,
sprintf('Enable/Disable manual importing from this server. Expected value is one of [%s]', $values)
)
->addOption(
'webhook-key-generate',
null,
InputOption::VALUE_NONE,
'Generate API key for this server. *WILL NOT* override existing key.'
)
->addOption(
'webhook-key-regenerate',
null,
InputOption::VALUE_NONE,
'Regenerate API key, it will invalidate old keys please update related server config.'
)
->addOption(
'webhook-key-length',
null,
InputOption::VALUE_OPTIONAL,
'Change default API key random generator length.',
(int)Config::get('webhook.keyLength', 16)
)
->addOption(
'webhook-import-status',
null,
InputOption::VALUE_REQUIRED,
sprintf('Enable/Disable the webhook api for this server. Expected value is one of [%s]', $values)
)
->addOption(
'webhook-require-ips',
null,
InputOption::VALUE_REQUIRED,
'Comma seperated ips/CIDR to link a server to specific ips. Useful for Multi Plex servers setup.',
)
->addOption(
'webhook-server-uuid',
null,
InputOption::VALUE_REQUIRED,
'Limit this server specific UUID. Useful for Multi Plex servers setup.',
)
->addOption(
'webhook-push-status',
null,
InputOption::VALUE_REQUIRED,
sprintf('Enable/Disable pushing to this server on webhook events. Expected value are [%s]', $values)
)
->addOption('use-config', null, InputOption::VALUE_REQUIRED, 'Use different servers.yaml.')
->addArgument('name', InputArgument::REQUIRED, 'Server name');
}
/**
* @throws Exception
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
// -- Use Custom servers.yaml file.
if (($config = $input->getOption('use-config'))) {
if (!is_string($config) || !is_file($config) || !is_readable($config)) {
throw new RuntimeException('Unable to read data given config.');
}
Config::save('servers', Yaml::parseFile($config));
} else {
$config = Config::get('path') . '/config/servers.yaml';
}
$name = $input->getArgument('name');
$ref = "servers.{$name}";
if (null === Config::get("{$ref}.type", null)) {
throw new RuntimeException(sprintf('No server named \'%s\' was found in %s.', $name, $config));
}
// -- $type
if ($input->getOption('type')) {
if (!array_key_exists($input->getOption('type'), Config::get('supported', []))) {
$output->writeln(
sprintf(
'<error>Unexpected value for --type, was expecting one of [%s] but got \'%s\' instead.',
implode('|', array_keys(Config::get('supported', []))),
$input->getOption('type')
)
);
return self::INVALID;
}
Config::save("{$ref}.type", $input->getOption('type'));
}
// -- $ref.url
if ($input->getOption('url')) {
if (!filter_var($input->getOption('url'), FILTER_VALIDATE_URL)) {
$output->writeln(sprintf('<error>Invalid --url value \'%s\' was given.', $input->getOption('url')));
return self::INVALID;
}
Config::save("{$ref}.url", $input->getOption('url'));
}
// -- $ref.user
if ($input->getOption('user')) {
if (!is_string($input->getOption('user')) && !is_int($input->getOption('user'))) {
$output->writeln(
sprintf(
'<error>Expecting --user value to be string or integer. but got \'%s\' instead.',
get_debug_type($input->getOption('user'))
)
);
return self::INVALID;
}
Config::save("{$ref}.user", $input->getOption('user'));
}
// -- $ref.token
if ($input->getOption('token')) {
if (!is_string($input->getOption('token')) && !is_int($input->getOption('token'))) {
$output->writeln(
sprintf(
'<error>Expecting --token value to be string or integer. but got \'%s\' instead.',
get_debug_type($input->getOption('token'))
)
);
return self::INVALID;
}
Config::save("{$ref}.token", $input->getOption('token'));
}
// -- $ref.export.enabled
if ($input->getOption('export-status')) {
$statusName = strtolower($input->getOption('export-status'));
if (!array_key_exists($statusName, self::ON_OFF_FLAGS)) {
$output->writeln(
sprintf(
'<error>Unexpected value for --export-status, was expecting one of [%s] but got \'%s\' instead.',
implode('|', array_keys(self::ON_OFF_FLAGS)),
$statusName
)
);
return self::INVALID;
}
Config::save("{$ref}.export.enabled", (bool)self::ON_OFF_FLAGS[$statusName]);
}
// -- $ref.import.enabled
if ($input->getOption('import-status')) {
$statusName = strtolower($input->getOption('import-status'));
if (!array_key_exists($statusName, self::ON_OFF_FLAGS)) {
$output->writeln(
sprintf(
'<error>Unexpected value for --import-status, was expecting one of [%s] but got \'%s\' instead.',
implode('|', array_keys(self::ON_OFF_FLAGS)),
$statusName
)
);
return self::INVALID;
}
Config::save("{$ref}.export.enabled", (bool)self::ON_OFF_FLAGS[$statusName]);
}
// -- $ref.webhook.token
if ($input->getOption('webhook-key-generate') || $input->getOption('webhook-key-regenerate')) {
if (!Config::get("{$ref}.webhook.token") || $input->getOption('webhook-key-regenerate')) {
$apiToken = bin2hex(random_bytes($input->getOption('api-key-length')));
$output->writeln(sprintf('<info>Server \'%s\' Webhook API key is: %s</info>', $name, $apiToken));
Config::save("{$ref}.webhook.token", $apiToken);
}
}
// -- $ref.webhook.enabled
if ($input->getOption('webhook-import-status')) {
$statusName = strtolower($input->getOption('webhook-import-status'));
if (!array_key_exists($statusName, self::ON_OFF_FLAGS)) {
$output->writeln(
sprintf(
'<error>Unexpected value for --webhook-import-status, was expecting one of [%s] but got \'%s\' instead.',
implode('|', array_keys(self::ON_OFF_FLAGS)),
$statusName
)
);
return self::INVALID;
}
$status = self::ON_OFF_FLAGS[$statusName];
Config::save($ref . '.webhook.import', (bool)$status);
}
// -- $ref.webhook.push
if ($input->getOption('webhook-push-status')) {
$statusName = strtolower($input->getOption('webhook-push-status'));
if (!array_key_exists($statusName, self::ON_OFF_FLAGS)) {
$output->writeln(
sprintf(
'<error>Unexpected value for --webhook-push-status, was expecting one of [%s] but got \'%s\' instead.',
implode('|', array_keys(self::ON_OFF_FLAGS)),
$statusName
)
);
return self::INVALID;
}
Config::save($ref . '.webhook.push', (bool)self::ON_OFF_FLAGS[$statusName]);
}
// -- $ref.webhook.ips
if ($input->getOption('webhook-require-ips')) {
Config::save($ref . '.webhook.ips', explode(',', $input->getOption('webhook-require-ips')));
}
// -- $ref.webhook.uuid
if ($input->getOption('webhook-server-uuid')) {
Config::save($ref . '.webhook.uuid', $input->getOption('webhook-server-uuid'));
}
file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2));
return self::SUCCESS;
}
}

View File

@@ -44,9 +44,10 @@ final class ListCommand extends Command
'Name',
'Type',
'URL',
'Webhook',
'Last Import at',
'Last Export at'
'WH Import',
'WH Push',
'Last Manual Import at',
'Last Manual Export at'
]
);
@@ -55,7 +56,8 @@ final class ListCommand extends Command
$name,
ag($server, 'type'),
ag($server, 'url'),
ag($server, 'webhook.token') && ag($server, 'webhook.enabled') ? 'Enabled' : 'Disabled',
ag($server, 'webhook.token') && ag($server, 'webhook.import') ? 'Enabled' : 'Disabled',
true === ag($server, 'webhook.push') ? 'Enabled' : 'Disabled',
($lastImportSync = ag($server, 'import.lastSync')) ? makeDate($lastImportSync) : 'Never',
($lastExportSync = ag($server, 'export.lastSync')) ? makeDate($lastExportSync) : 'Never',
];

View File

@@ -1,170 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Commands\Servers;
use App\Command;
use App\Libs\Config;
use Exception;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
final class WebhookCommand extends Command
{
private const WEBHOOK_STATUS_VALUES = [
'enabled' => true,
'enable' => true,
'yes' => true,
'disabled' => false,
'disable' => false,
'no' => false,
];
protected function configure(): void
{
$this->setName('servers:webhook')
->setDescription('Change Server Webhook settings.')
->addOption(
'api-key-generate',
'g',
InputOption::VALUE_NONE,
'Generate API key for this server. *WILL NOT* override existing key.'
)
->addOption(
'api-key-regenerate',
'r',
InputOption::VALUE_NONE,
'Regenerate API key, it will invalidate old keys please update related server config.'
)
->addOption(
'api-key-length',
'l',
InputOption::VALUE_OPTIONAL,
'Change default API key random generator length.',
(int)Config::get('webhook.keyLength', 16)
)
->addOption(
'status',
null,
InputOption::VALUE_REQUIRED,
sprintf(
'Enable/Disable the webhook api for this server. Expected value are [%s]',
implode('|', array_keys(self::WEBHOOK_STATUS_VALUES))
)
)
->addOption(
'require-ips',
null,
InputOption::VALUE_REQUIRED,
'Comma seperated ips/cdr to link a server to specific ips. Mainly for Plex.',
)
->addOption(
'push',
null,
InputOption::VALUE_REQUIRED,
sprintf(
'Enable/Disable pushing to this server on webhook events. Expected value are [%s]',
implode('|', array_keys(self::WEBHOOK_STATUS_VALUES))
)
)
->addOption('use-config', null, InputOption::VALUE_REQUIRED, 'Use different servers.yaml.')
->addArgument('name', InputArgument::REQUIRED, 'Server name');
}
/**
* @throws Exception
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
// -- Use Custom servers.yaml file.
if (($config = $input->getOption('use-config'))) {
if (!is_string($config) || !is_file($config) || !is_readable($config)) {
throw new RuntimeException('Unable to read data given config.');
}
Config::save('servers', Yaml::parseFile($config));
} else {
$config = Config::get('path') . '/config/servers.yaml';
}
$name = $input->getArgument('name');
$ref = "servers.{$name}";
if (null === Config::get("{$ref}.type", null)) {
throw new RuntimeException(sprintf('No server named \'%s\' was found in %s.', $name, $config));
}
// -- webhook.token
if ($input->getOption('api-key-generate') || $input->getOption('api-key-regenerate')) {
if (!Config::get("{$ref}.webhook.token") || $input->getOption('api-key-regenerate')) {
$apiToken = bin2hex(random_bytes($input->getOption('api-key-length')));
$output->writeln(sprintf('<info>Server \'%s\' Webhook API key is: %s</info>', $name, $apiToken));
Config::save("{$ref}.webhook.token", $apiToken);
}
}
// -- webhook.enabled
if ($input->getOption('status')) {
$statusName = strtolower($input->getOption('status'));
if (!array_key_exists($statusName, self::WEBHOOK_STATUS_VALUES)) {
throw new RuntimeException(
sprintf(
'Invalid value was given to --status \'%s\', expected values are [%s]',
$statusName,
implode(', ', array_keys(self::WEBHOOK_STATUS_VALUES))
)
);
}
$status = self::WEBHOOK_STATUS_VALUES[$statusName];
if (true === $status && !Config::get($ref . '.webhook.token')) {
$output->writeln(
sprintf(
'<error>You must generate api key for this server \'%s\' using [-g, --generate] flag before you can start using the Webhook API.</error>',
$name
)
);
return self::INVALID;
}
Config::save($ref . '.webhook.enabled', (bool)$status);
}
// -- webhook.push
if ($input->getOption('push')) {
$statusName = strtolower($input->getOption('push'));
if (!array_key_exists($statusName, self::WEBHOOK_STATUS_VALUES)) {
throw new RuntimeException(
sprintf(
'Invalid value was given to --push \'%s\', expected values are [%s]',
$statusName,
implode(', ', array_keys(self::WEBHOOK_STATUS_VALUES))
)
);
}
$status = self::WEBHOOK_STATUS_VALUES[$statusName];
Config::save($ref . '.webhook.push', (bool)$status);
}
// -- webhook.ips
if ($input->getOption('require-ips')) {
Config::save($ref . '.webhook.ips', explode(',', $input->getOption('require-ips')));
}
file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2));
return self::SUCCESS;
}
}

View File

@@ -294,7 +294,6 @@ if (!function_exists('preServeHttpRequest')) {
if (!function_exists('serveHttpRequest')) {
function serveHttpRequest(ServerRequestInterface $request): ResponseInterface
{
$logger = Container::get(LoggerInterface::class);
try {
@@ -330,6 +329,12 @@ if (!function_exists('serveHttpRequest')) {
continue;
}
$uuid = ag($info, 'webhook.uuid', null);
if (null !== $uuid && $uuid !== $request->getAttribute('SERVER_ID', null)) {
continue;
}
$server = array_replace_recursive(['name' => $name], $info);
break;
}
@@ -338,12 +343,13 @@ if (!function_exists('serveHttpRequest')) {
throw new HttpException('Invalid API key was given.', 401);
}
if (true !== ag($server, 'webhook.enabled')) {
if (true !== ag($server, 'webhook.import')) {
throw new HttpException(
sprintf(
'Webhook API is disabled for \'%s\' via config.',
'Import via webhook for this server \'%s\' is disabled.',
ag($server, 'name')
), 500
),
500
);
}