Feature/vue webhooks (#4655)
This commit is contained in:
parent
c81cbdde44
commit
798bfd1eb2
|
@ -1,152 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
|
||||
'api_info' => [
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'webhook_url' => [
|
||||
'url',
|
||||
[
|
||||
'label' => __('Discord Web Hook URL'),
|
||||
'description' => __('This URL is provided within the Discord application.'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'triggers' => [
|
||||
'multiCheckbox',
|
||||
[
|
||||
'label' => __('Web Hook Triggers'),
|
||||
'options' => array_diff_key($triggers, ['listener_lost' => 1, 'listener_gained' => 1]),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'message' => [
|
||||
'use_grid' => true,
|
||||
'legend' => __('Customize Message'),
|
||||
'legend_class' => 'd-none',
|
||||
'description' => __(
|
||||
'Variables are in the form of <code>{{ var.name }}</code>. All values in the <a href="%s" target="_blank">Now Playing API response</a> are avaliable for use. Any empty fields are ignored.',
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
|
||||
'elements' => [
|
||||
|
||||
'content' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Main Message Content'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => sprintf(__('Now playing on %s:'), '{{ station.name }}'),
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'title' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Title'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => '{{ now_playing.song.title }}',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'description' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Description'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => '{{ now_playing.song.artist }}',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'url' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('URL'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => '{{ station.listen_url }}',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'author' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Author Name'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => '{{ live.streamer_name }}',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'thumbnail' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Thumbnail Image URL'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => '{{ now_playing.song.art }}',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'footer' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Footer Text'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => sprintf(__('Powered by %s'), $environment->getAppName()),
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'submit_grp' => [
|
||||
'elements' => [
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
|
||||
'message_grp' => [
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'triggers' => [
|
||||
'multiCheckbox',
|
||||
[
|
||||
'label' => __('Web Hook Triggers'),
|
||||
'options' => $triggers,
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
'to' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Message Recipient(s)'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'description' => __('E-mail addresses can be separated by commas.'),
|
||||
'form_group_class' => 'col-sm-6',
|
||||
],
|
||||
],
|
||||
|
||||
'subject' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Message Subject'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'description' => __(
|
||||
'Variables are in the form of <code>{{ var.name }}</code>. All values in the <a href="%s" target="_blank">Now Playing API response</a> are avaliable for use. Any empty fields are ignored.',
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
'form_group_class' => 'col-sm-6',
|
||||
],
|
||||
],
|
||||
|
||||
'message' => [
|
||||
'textarea',
|
||||
[
|
||||
'label' => __('Message Body'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'description' => __(
|
||||
'Variables are in the form of <code>{{ var.name }}</code>. All values in the <a href="%s" target="_blank">Now Playing API response</a> are avaliable for use. Any empty fields are ignored.',
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'submit_grp' => [
|
||||
'elements' => [
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
|
||||
'api_info' => [
|
||||
'use_grid' => true,
|
||||
'legend' => __('Web Hook Details'),
|
||||
'legend_class' => 'd-none',
|
||||
'description' => sprintf(
|
||||
__(
|
||||
'Web hooks automatically send a HTTP POST request to the URL you specify to
|
||||
notify it any time one of the triggers you specify occurs on your station. The body of the POST message
|
||||
is the exact same as the <a href="%s" target="_blank">Now Playing API response</a> for your station.
|
||||
In order to process quickly, web hooks have a short timeout, so the responding service should be
|
||||
optimized to handle the request in under 2 seconds.'
|
||||
),
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'webhook_url' => [
|
||||
'url',
|
||||
[
|
||||
'label' => __('Web Hook URL'),
|
||||
'description' => __(
|
||||
'The URL that will receive the POST messages any time an event is triggered.'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'label_class' => 'mb-2',
|
||||
'form_group_class' => 'col-md-6 mt-1',
|
||||
],
|
||||
],
|
||||
|
||||
'basic_auth_username' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Optional: HTTP Basic Authentication Username'),
|
||||
'description' => __(
|
||||
'If your web hook requires HTTP basic authentication, provide the username here.'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'basic_auth_password' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Optional: HTTP Basic Authentication Password'),
|
||||
'description' => __(
|
||||
'If your web hook requires HTTP basic authentication, provide the password here.'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'triggers' => [
|
||||
'multiCheckbox',
|
||||
[
|
||||
'label' => __('Web Hook Triggers'),
|
||||
'options' => $triggers,
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'submit_grp' => [
|
||||
'elements' => [
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
[
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'tracking_id' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('GA Property Tracking ID'),
|
||||
'description' => __('The property ID used to track live listeners.'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
[
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'matomo_url' => [
|
||||
'url',
|
||||
[
|
||||
'label' => __('Matomo Installation Base URL'),
|
||||
'description' => __('The full base URL of your Matomo installation.'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-12',
|
||||
],
|
||||
],
|
||||
|
||||
'site_id' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Matomo Site ID'),
|
||||
'description' => __('The numeric site ID for this site.'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'token' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Matomo API Token'),
|
||||
'description' => __('Optionally supply an API token to allow IP address overriding.'),
|
||||
'belongsTo' => 'config',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,146 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
|
||||
'api_info' => [
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'bot_token' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Bot Token'),
|
||||
'description' => __(
|
||||
'See the <a href="%s" target="_blank">Telegram Documentation</a> for more details.',
|
||||
'https://core.telegram.org/bots#botfather'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'chat_id' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Chat ID'),
|
||||
'description' => __(
|
||||
'Unique identifier for the target chat or username of the target channel (in the format @channelusername).'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Custom API Base URL'),
|
||||
'label_class' => 'advanced',
|
||||
'description' => __(
|
||||
'Leave blank to use the default Telegram API URL (recommended). Specify the full URL, like <code>https://api.pwrtelegram.xyz/</code>.'
|
||||
),
|
||||
'belongsTo' => 'config',
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'triggers' => [
|
||||
'multiCheckbox',
|
||||
[
|
||||
'label' => __('Web Hook Triggers'),
|
||||
'options' => array_diff_key($triggers, ['listener_lost' => 1, 'listener_gained' => 1]),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'message' => [
|
||||
'use_grid' => true,
|
||||
'legend' => __('Customize Message'),
|
||||
'legend_class' => 'd-none',
|
||||
'description' => sprintf(
|
||||
__(
|
||||
'Variables are in the form of <code>{{ var.name }}</code>. All values in the <a href="%s" target="_blank">Now Playing API response</a> are avaliable for use. Any empty fields are ignored.'
|
||||
),
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
|
||||
'elements' => [
|
||||
|
||||
'text' => [
|
||||
'textarea',
|
||||
[
|
||||
'label' => __('Main Message Content'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => sprintf(
|
||||
__('Now playing on %s: %s by %s! Tune in now.'),
|
||||
'{{ station.name }}',
|
||||
'{{ now_playing.song.title }}',
|
||||
'{{ now_playing.song.artist }}'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
'parse_mode' => [
|
||||
'radio',
|
||||
[
|
||||
'label' => __('Message parsing mode'),
|
||||
'description' => __(
|
||||
'See the <a href="%s" target="_blank">Telegram Documentation</a> for more details.',
|
||||
'https://core.telegram.org/bots/api#sendmessage'
|
||||
),
|
||||
'default' => 'Markdown',
|
||||
'options' => [
|
||||
'Markdown' => 'Markdown',
|
||||
'HTML' => 'HTML',
|
||||
],
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'submit_grp' => [
|
||||
'elements' => [
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
[
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'station_id' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('TuneIn Station ID'),
|
||||
'description' => __('The station ID will be a numeric string that starts with the letter S.'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'partner_id' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('TuneIn Partner ID'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'partner_key' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('TuneIn Partner Key'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,161 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var array $triggers
|
||||
* @var App\Environment $environment
|
||||
* @var App\Http\Router $router
|
||||
*/
|
||||
|
||||
return [
|
||||
'method' => 'post',
|
||||
|
||||
'groups' => [
|
||||
|
||||
'api_info' => [
|
||||
'use_grid' => true,
|
||||
'legend' => __('Twitter Account Details'),
|
||||
'legend_class' => 'd-none',
|
||||
'description' => __(
|
||||
'Steps for configuring a Twitter application:<br>
|
||||
<ol type="1">
|
||||
<li>Create a new app on the <a href="%s" target="_blank">Twitter Applications site</a>.
|
||||
Use this installation\'s base URL as the application URL.</li>
|
||||
<li>In the newly created application, click the "Keys and Access Tokens" tab.</li>
|
||||
<li>At the bottom of the page, click "Create my access token".</li>
|
||||
</ol>
|
||||
<p>Once these steps are completed, enter the information from the "Keys and Access Tokens" page into the fields below.</p>',
|
||||
'https://developer.twitter.com/en/apps'
|
||||
),
|
||||
|
||||
'elements' => [
|
||||
|
||||
'consumer_key' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Consumer Key (API Key)'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'consumer_secret' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Consumer Secret (API Secret)'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'token' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Access Token'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'token_secret' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Access Token Secret'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'rate_limit' => [
|
||||
'select',
|
||||
[
|
||||
'label' => __('Only Send One Tweet Every...'),
|
||||
'belongsTo' => 'config',
|
||||
'default' => 0,
|
||||
'choices' => [
|
||||
0 => __('No Limit'),
|
||||
15 => __('%d seconds', 15),
|
||||
30 => __('%d seconds', 30),
|
||||
60 => __('%d seconds', 60),
|
||||
120 => __('%d minutes', 2),
|
||||
300 => __('%d minutes', 5),
|
||||
600 => __('%d minutes', 10),
|
||||
900 => __('%d minutes', 15),
|
||||
1800 => __('%d minutes', 30),
|
||||
3600 => __('%d minutes', 60),
|
||||
],
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'message_grp' => [
|
||||
'use_grid' => true,
|
||||
'elements' => [
|
||||
|
||||
'name' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Web Hook Name'),
|
||||
'description' => __(
|
||||
'Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.'
|
||||
),
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-md-6',
|
||||
],
|
||||
],
|
||||
|
||||
'triggers' => [
|
||||
'multiCheckbox',
|
||||
[
|
||||
'label' => __('Web Hook Triggers'),
|
||||
'options' => $triggers,
|
||||
'required' => true,
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
'message' => [
|
||||
'textarea',
|
||||
[
|
||||
'label' => __('Message Body'),
|
||||
'belongsTo' => 'config',
|
||||
'required' => true,
|
||||
'default' => __(
|
||||
'Now playing on %s: %s by %s! Tune in now: %s',
|
||||
'{{ station.name }}',
|
||||
'{{ now_playing.song.title }}',
|
||||
'{{ now_playing.song.artist }}',
|
||||
'{{ station.public_player_url }}'
|
||||
),
|
||||
'description' => __(
|
||||
'Variables are in the form of <code>{{ var.name }}</code>. All values in the <a href="%s" target="_blank">Now Playing API response</a> are avaliable for use. Any empty fields are ignored.',
|
||||
$router->named('api:nowplaying:index')
|
||||
),
|
||||
'form_group_class' => 'col-sm-12',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
'submit_grp' => [
|
||||
'elements' => [
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'ui-button btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -9,9 +9,6 @@ return [
|
|||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||
Message\ReprocessMediaMessage::class => Task\CheckMediaTask::class,
|
||||
|
||||
Message\AddNewPodcastMediaMessage::class => Task\CheckPodcastMediaTask::class,
|
||||
Message\ReprocessPodcastMediaMessage::class => Task\CheckPodcastMediaTask::class,
|
||||
|
||||
Message\WritePlaylistFileMessage::class => Liquidsoap\ConfigWriter::class,
|
||||
|
||||
Message\UpdateNowPlayingMessage::class => Task\NowPlayingTask::class,
|
||||
|
@ -21,6 +18,7 @@ return [
|
|||
Message\RunSyncTaskMessage::class => App\Sync\Runner::class,
|
||||
|
||||
Message\DispatchWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||
Message\TestWebhookMessage::class => App\Webhook\Dispatcher::class,
|
||||
|
||||
Mailer\Messenger\SendEmailMessage::class => Mailer\Messenger\MessageHandler::class,
|
||||
];
|
||||
|
|
|
@ -667,6 +667,26 @@ return static function (RouteCollectorProxy $app) {
|
|||
$group->post('/restart', Controller\Api\Stations\ServicesController::class . ':restartAction')
|
||||
->setName('api:stations:restart')
|
||||
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
|
||||
|
||||
$group->group(
|
||||
'/webhook/{id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->put(
|
||||
'/toggle',
|
||||
Controller\Api\Stations\Webhooks\ToggleAction::class
|
||||
)->setName('api:stations:webhook:toggle');
|
||||
|
||||
$group->put(
|
||||
'/test',
|
||||
Controller\Api\Stations\Webhooks\TestAction::class
|
||||
)->setName('api:stations:webhook:test');
|
||||
|
||||
$group->get(
|
||||
'/test-log/{path}',
|
||||
Controller\Api\Stations\Webhooks\TestLogAction::class
|
||||
)->setName('api:stations:webhook:test-log');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_WEB_HOOKS, true));
|
||||
}
|
||||
)->add(Middleware\RequireStation::class)
|
||||
->add(Middleware\GetStation::class);
|
||||
|
|
|
@ -138,36 +138,9 @@ return static function (RouteCollectorProxy $app) {
|
|||
->setName('stations:streamers:index')
|
||||
->add(new Middleware\Permissions(Acl::STATION_STREAMERS, true));
|
||||
|
||||
$group->group(
|
||||
'/webhooks',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Stations\WebhooksController::class . ':indexAction')
|
||||
->setName('stations:webhooks:index');
|
||||
|
||||
$group->map(
|
||||
['GET', 'POST'],
|
||||
'/edit/{id}',
|
||||
Controller\Stations\WebhooksController::class . ':editAction'
|
||||
)
|
||||
->setName('stations:webhooks:edit');
|
||||
|
||||
$group->map(
|
||||
['GET', 'POST'],
|
||||
'/add[/{type}]',
|
||||
Controller\Stations\WebhooksController::class . ':addAction'
|
||||
)
|
||||
->setName('stations:webhooks:add');
|
||||
|
||||
$group->get('/toggle/{id}/{csrf}', Controller\Stations\WebhooksController::class . ':toggleAction')
|
||||
->setName('stations:webhooks:toggle');
|
||||
|
||||
$group->get('/test/{id}/{csrf}', Controller\Stations\WebhooksController::class . ':testAction')
|
||||
->setName('stations:webhooks:test');
|
||||
|
||||
$group->get('/delete/{id}/{csrf}', Controller\Stations\WebhooksController::class . ':deleteAction')
|
||||
->setName('stations:webhooks:delete');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_WEB_HOOKS, true));
|
||||
$group->get('/webhooks', Controller\Stations\WebhooksAction::class)
|
||||
->setName('stations:webhooks:index')
|
||||
->add(new Middleware\Permissions(Acl::STATION_WEB_HOOKS, true));
|
||||
}
|
||||
)
|
||||
->add(Middleware\Module\Stations::class)
|
||||
|
|
|
@ -6,58 +6,74 @@
|
|||
use App\Entity\StationWebhook;
|
||||
use App\Webhook\Connector;
|
||||
|
||||
$triggers = [
|
||||
StationWebhook::TRIGGER_SONG_CHANGED => __('Any time the currently playing song changes'),
|
||||
StationWebhook::TRIGGER_LISTENER_GAINED => __('Any time the listener count increases'),
|
||||
StationWebhook::TRIGGER_LISTENER_LOST => __('Any time the listener count decreases'),
|
||||
StationWebhook::TRIGGER_LIVE_CONNECT => __('Any time a live streamer/DJ connects to the stream'),
|
||||
StationWebhook::TRIGGER_LIVE_DISCONNECT => __('Any time a live streamer/DJ disconnects from the stream'),
|
||||
StationWebhook::TRIGGER_STATION_OFFLINE => __('When the station broadcast goes offline.'),
|
||||
StationWebhook::TRIGGER_STATION_ONLINE => __('When the station broadcast comes online.'),
|
||||
];
|
||||
|
||||
$allTriggers = array_keys($triggers);
|
||||
$allTriggersExceptListeners = array_diff($allTriggers, [
|
||||
StationWebhook::TRIGGER_LISTENER_GAINED,
|
||||
StationWebhook::TRIGGER_LISTENER_LOST,
|
||||
]);
|
||||
|
||||
return [
|
||||
'webhooks' => [
|
||||
Connector\Generic::NAME => [
|
||||
'class' => Connector\Generic::class,
|
||||
'name' => __('Generic Web Hook'),
|
||||
'description' => __('Automatically send a message to any URL when your station data changes.'),
|
||||
'triggers' => $allTriggers,
|
||||
],
|
||||
Connector\Email::NAME => [
|
||||
'class' => Connector\Email::class,
|
||||
'name' => __('Send E-mail'),
|
||||
'description' => __('Send an e-mail to specified address(es).'),
|
||||
'triggers' => $allTriggers,
|
||||
],
|
||||
Connector\TuneIn::NAME => [
|
||||
'class' => Connector\TuneIn::class,
|
||||
'name' => __('TuneIn AIR'),
|
||||
'description' => __('Send song metadata changes to TuneIn.'),
|
||||
'triggers' => [],
|
||||
],
|
||||
Connector\Discord::NAME => [
|
||||
'class' => Connector\Discord::class,
|
||||
'name' => __('Discord Webhook'),
|
||||
'description' => __('Automatically send a customized message to your Discord server.'),
|
||||
'triggers' => $allTriggersExceptListeners,
|
||||
],
|
||||
Connector\Telegram::NAME => [
|
||||
'class' => Connector\Telegram::class,
|
||||
'name' => __('Telegram Chat Message'),
|
||||
'description' => __('Use the Telegram Bot API to send a message to a channel.'),
|
||||
'triggers' => $allTriggersExceptListeners,
|
||||
],
|
||||
Connector\Twitter::NAME => [
|
||||
'class' => Connector\Twitter::class,
|
||||
'name' => __('Twitter Post'),
|
||||
'description' => __('Automatically send a tweet.'),
|
||||
'triggers' => $allTriggers,
|
||||
],
|
||||
Connector\GoogleAnalytics::NAME => [
|
||||
'class' => Connector\GoogleAnalytics::class,
|
||||
'name' => __('Google Analytics Integration'),
|
||||
'description' => __('Send stream listener details to Google Analytics.'),
|
||||
'triggers' => [],
|
||||
],
|
||||
Connector\MatomoAnalytics::NAME => [
|
||||
'class' => Connector\MatomoAnalytics::class,
|
||||
'name' => __('Matomo Analytics Integration'),
|
||||
'description' => __('Send stream listener details to Matomo Analytics.'),
|
||||
'triggers' => [],
|
||||
],
|
||||
],
|
||||
|
||||
// The triggers that can be selected for a web hook to trigger.
|
||||
'triggers' => [
|
||||
StationWebhook::TRIGGER_SONG_CHANGED => __('Any time the currently playing song changes'),
|
||||
StationWebhook::TRIGGER_LISTENER_GAINED => __('Any time the listener count increases'),
|
||||
StationWebhook::TRIGGER_LISTENER_LOST => __('Any time the listener count decreases'),
|
||||
StationWebhook::TRIGGER_LIVE_CONNECT => __('Any time a live streamer/DJ connects to the stream'),
|
||||
StationWebhook::TRIGGER_LIVE_DISCONNECT => __('Any time a live streamer/DJ disconnects from the stream'),
|
||||
StationWebhook::TRIGGER_STATION_OFFLINE => __('When the station broadcast goes offline.'),
|
||||
StationWebhook::TRIGGER_STATION_ONLINE => __('When the station broadcast comes online.'),
|
||||
],
|
||||
'triggers' => $triggers,
|
||||
];
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
"sweetalert2": "^10.16.6",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.3.6",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-gettext": "^2.1.12",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
|
@ -9617,6 +9618,14 @@
|
|||
"vue": "^ 3.0.0 || ^ 2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-clipboard2": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-clipboard2/-/vue-clipboard2-0.3.3.tgz",
|
||||
"integrity": "sha512-aNWXIL2DKgJyY/1OOeITwAQz1fHaCIGvUFHf9h8UcoQBG5a74MkdhS/xqoYe7DNZdQmZRL+TAdIbtUs9OyVjbw==",
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-functional-data-merge": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
|
||||
|
@ -17453,6 +17462,14 @@
|
|||
"merge-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"vue-clipboard2": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-clipboard2/-/vue-clipboard2-0.3.3.tgz",
|
||||
"integrity": "sha512-aNWXIL2DKgJyY/1OOeITwAQz1fHaCIGvUFHf9h8UcoQBG5a74MkdhS/xqoYe7DNZdQmZRL+TAdIbtUs9OyVjbw==",
|
||||
"requires": {
|
||||
"clipboard": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"vue-functional-data-merge": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"sweetalert2": "^10.16.6",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.3.6",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-gettext": "^2.1.12",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<admin-custom-fields-form :form="$v.form" :auto-assign-types="autoAssignTypes">
|
||||
</admin-custom-fields-form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<admin-permissions-global-form :form="$v.form" :global-permissions="globalPermissions">
|
||||
|
@ -74,7 +74,7 @@ export default {
|
|||
};
|
||||
});
|
||||
},
|
||||
buildSubmitRequest () {
|
||||
getSubmittableFormData() {
|
||||
let form = {
|
||||
name: this.form.name,
|
||||
permissions: {
|
||||
|
@ -87,15 +87,7 @@ export default {
|
|||
form.permissions.station[row.station_id] = row.permissions;
|
||||
});
|
||||
|
||||
return {
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
url: (this.isEditMode)
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
data: form
|
||||
};
|
||||
return form;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<storage-location-form :form="$v.form"></storage-location-form>
|
||||
|
||||
|
|
|
@ -65,10 +65,13 @@ export default {
|
|||
this.close();
|
||||
});
|
||||
},
|
||||
populateForm (data) {
|
||||
populateForm(data) {
|
||||
this.form = data;
|
||||
},
|
||||
buildSubmitRequest () {
|
||||
getSubmittableFormData() {
|
||||
return this.form;
|
||||
},
|
||||
buildSubmitRequest() {
|
||||
return {
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
|
@ -76,7 +79,7 @@ export default {
|
|||
url: (this.isEditMode)
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
data: this.form
|
||||
data: this.getSubmittableFormData()
|
||||
};
|
||||
},
|
||||
doSubmit () {
|
||||
|
@ -97,15 +100,17 @@ export default {
|
|||
this.error = error.response.data.message;
|
||||
});
|
||||
},
|
||||
close () {
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
clearContents() {
|
||||
this.$v.form.$reset();
|
||||
|
||||
this.loading = false;
|
||||
this.error = null;
|
||||
this.editUrl = null;
|
||||
this.resetForm();
|
||||
|
||||
this.$v.form.$reset();
|
||||
this.$refs.modal.hide();
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,36 +1,33 @@
|
|||
<template>
|
||||
<button ref="btn" class="btn btn-copy btn-link btn-xs" :data-clipboard-target="target" v-bind="$attrs">
|
||||
<button ref="btn" class="btn btn-copy btn-link btn-xs" @click.prevent="doCopy">
|
||||
<icon class="sm" icon="file_copy"></icon>
|
||||
<span :class="{ 'sr-only': hideText }" key="lang_copy_to_clipboard" v-translate>Copy to Clipboard</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Clipboard from 'clipboard/dist/clipboard.min.js';
|
||||
import '~/vendor/clipboard.js';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default {
|
||||
components: { Icon },
|
||||
props: {
|
||||
target: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
hideText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
clipboard: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.clipboard = new Clipboard(this.$refs.btn);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clipboard.destroy();
|
||||
methods: {
|
||||
doCopy() {
|
||||
this.$copyText(this.text).then(function (e) {
|
||||
}, function (e) {
|
||||
console.error(e);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<b-modal :size="size" :id="id" ref="modal" :title="title" :busy="loading">
|
||||
<b-modal :size="size" :id="id" ref="modal" :title="title" :busy="loading" @hidden="onHidden">
|
||||
<template #default="slotProps">
|
||||
<b-overlay variant="card" :show="loading">
|
||||
<b-alert variant="danger" :show="error != null">{{ error }}</b-alert>
|
||||
|
@ -36,7 +36,7 @@ import InvisibleSubmitButton from "~/components/Common/InvisibleSubmitButton";
|
|||
|
||||
export default {
|
||||
components: {InvisibleSubmitButton},
|
||||
emits: ['submit'],
|
||||
emits: ['submit', 'hidden'],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -75,6 +75,9 @@ export default {
|
|||
doSubmit() {
|
||||
this.$emit('submit');
|
||||
},
|
||||
onHidden() {
|
||||
this.$emit('hidden');
|
||||
},
|
||||
close() {
|
||||
this.hide();
|
||||
},
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<b-modal id="logs_modal" ref="modal" :title="langLogView" @hidden="clearContents">
|
||||
<streaming-log-view ref="logView" :log-url="logUrl"></streaming-log-view>
|
||||
|
||||
<template #modal-footer>
|
||||
<b-button variant="default" type="button" @click="close">
|
||||
<translate key="lang_btn_close">Close</translate>
|
||||
</b-button>
|
||||
<b-button variant="primary" class="btn_copy" @click.prevent="doCopy" type="button">
|
||||
<translate key="lang_btn_copy">Copy to Clipboard</translate>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '~/vendor/clipboard.js';
|
||||
import StreamingLogView from "~/components/Common/StreamingLogView";
|
||||
|
||||
export default {
|
||||
name: 'StreamingLogModal',
|
||||
components: {StreamingLogView},
|
||||
data() {
|
||||
return {
|
||||
logUrl: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
langLogView() {
|
||||
return this.$gettext('Log View');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show(logUrl) {
|
||||
this.logUrl = logUrl;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
doCopy() {
|
||||
this.$copyText(this.$refs.logView.getContents());
|
||||
},
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
clearContents() {
|
||||
this.logUrl = null;
|
||||
this.log = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<b-overlay variant="card" :show="loading">
|
||||
<textarea class="form-control log-viewer" id="log-view-contents" spellcheck="false"
|
||||
readonly>{{ logs }}</textarea>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'StreamingLogView',
|
||||
props: {
|
||||
logUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
logs: '',
|
||||
currentLogPosition: null,
|
||||
timeoutUpdateLog: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
|
||||
this.axios({
|
||||
method: 'GET',
|
||||
url: this.logUrl
|
||||
}).then((resp) => {
|
||||
if (resp.data.contents !== '') {
|
||||
this.logs = resp.data.contents + "\n";
|
||||
} else {
|
||||
this.logs = '';
|
||||
}
|
||||
|
||||
this.currentLogPosition = resp.data.position;
|
||||
|
||||
if (!resp.data.eof) {
|
||||
this.timeoutUpdateLog = setTimeout(this.updateLogs, 2500);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timeoutUpdateLog);
|
||||
},
|
||||
methods: {
|
||||
updateLogs() {
|
||||
this.axios({
|
||||
method: 'GET',
|
||||
url: this.logUrl,
|
||||
params: {
|
||||
position: this.currentLogPosition
|
||||
}
|
||||
}).then((resp) => {
|
||||
if (resp.data.contents !== '') {
|
||||
this.logs = this.logs + resp.data.contents + "\n";
|
||||
}
|
||||
this.currentLogPosition = resp.data.position;
|
||||
|
||||
if (!resp.data.eof) {
|
||||
this.timeoutUpdateLog = setTimeout(this.updateLogs, 2500);
|
||||
}
|
||||
});
|
||||
},
|
||||
getContents() {
|
||||
return this.logs;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -2,7 +2,9 @@
|
|||
<b-form-group v-bind="$attrs" :label-class="labelClassWithRequired" :label-for="id" :state="fieldState">
|
||||
<template #default>
|
||||
<slot name="default" v-bind="{ id, field, state: fieldState }">
|
||||
<b-form-input type="text" :id="id" v-model="field.$model"
|
||||
<b-form-textarea v-if="inputType === 'textarea'" :id="id" v-model="field.$model"
|
||||
:state="fieldState"></b-form-textarea>
|
||||
<b-form-input v-else :type="inputType" :id="id" v-model="field.$model"
|
||||
:state="fieldState"></b-form-input>
|
||||
</slot>
|
||||
|
||||
|
@ -37,6 +39,10 @@ export default {
|
|||
type: Object,
|
||||
required: true
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<mount-form-basic-info :form="$v.form"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<form-basic-info :form="$v.form"></form-basic-info>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<episode-form-basic-info :form="$v.form"></episode-form-basic-info>
|
||||
|
@ -122,7 +122,7 @@ export default {
|
|||
'explicit': d.explicit
|
||||
};
|
||||
},
|
||||
buildSubmitRequest () {
|
||||
getSubmittableFormData() {
|
||||
let modifiedForm = this.form;
|
||||
if (modifiedForm.publish_date.length > 0 && modifiedForm.publish_time.length > 0) {
|
||||
let publishDateTimeString = modifiedForm.publish_date + ' ' + modifiedForm.publish_time;
|
||||
|
@ -131,16 +131,8 @@ export default {
|
|||
modifiedForm.publish_at = publishDateTime.toSeconds();
|
||||
}
|
||||
|
||||
return {
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
url: (this.isEditMode)
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
data: this.form
|
||||
};
|
||||
}
|
||||
return modifiedForm;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -19,17 +19,14 @@
|
|||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_description" :field="form.description">
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_description" :field="form.description"
|
||||
input-type="textarea">
|
||||
<template #label>
|
||||
<translate key="lang_form_edit_description">Description</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_edit_description_desc">The description of the episode. The typical maximum amount of text allowed for this is 4000 characters.</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-textarea :id="props.id" v-model="props.field.$model"
|
||||
:state="props.state"></b-form-textarea>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_edit_publish_date" :field="form.publish_date">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<podcast-form-basic-info :form="$v.form"
|
||||
|
|
|
@ -18,17 +18,14 @@
|
|||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_description" :field="form.description">
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_description" :field="form.description"
|
||||
input-type="textarea">
|
||||
<template #label>
|
||||
<translate key="lang_form_edit_description">Description</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_edit_description_desc">The description of your podcast. The typical maximum amount of text allowed for this is 4000 characters.</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-textarea :id="props.id" v-model="props.field.$model"
|
||||
:state="props.state"></b-form-textarea>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_language" :field="form.language">
|
||||
|
|
|
@ -40,8 +40,9 @@
|
|||
<h2 class="card-title" v-translate key="lang_embed_code">Embed Code</h2>
|
||||
</div>
|
||||
<b-card-body>
|
||||
<textarea id="request_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 100px;">{{ embedCode }}</textarea>
|
||||
<copy-to-clipboard-button target="#request_embed_url"></copy-to-clipboard-button>
|
||||
<textarea class="full-width form-control text-preformatted" spellcheck="false"
|
||||
style="height: 100px;">{{ embedCode }}</textarea>
|
||||
<copy-to-clipboard-button :text="embedCode"></copy-to-clipboard-button>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</b-col>
|
||||
|
|
|
@ -26,22 +26,22 @@
|
|||
<tr>
|
||||
<td key="lang_frontend_admin_pw" v-translate>Administrator Password</td>
|
||||
<td>
|
||||
<span id="frontend_admin_pw">{{ frontendAdminPassword }}</span>
|
||||
<copy-to-clipboard-button target="#frontend_admin_pw" hide-text></copy-to-clipboard-button>
|
||||
{{ frontendAdminPassword }}
|
||||
<copy-to-clipboard-button :text="frontendAdminPassword" hide-text></copy-to-clipboard-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td key="lang_frontend_source_pw" v-translate>Source Password</td>
|
||||
<td>
|
||||
<span id="frontend_source_pw">{{ frontendSourcePassword }}</span>
|
||||
<copy-to-clipboard-button target="#frontend_source_pw" hide-text></copy-to-clipboard-button>
|
||||
{{ frontendSourcePassword }}
|
||||
<copy-to-clipboard-button :text="frontendSourcePassword" hide-text></copy-to-clipboard-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isIcecast">
|
||||
<td key="lang_frontend_relay_pw" v-translate>Relay Password</td>
|
||||
<td>
|
||||
<span id="frontend_relay_pw">{{ frontendRelayPassword }}</span>
|
||||
<copy-to-clipboard-button target="#frontend_relay_pw" hide-text></copy-to-clipboard-button>
|
||||
{{ frontendRelayPassword }}
|
||||
<copy-to-clipboard-button :text="frontendRelayPassword" hide-text></copy-to-clipboard-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<b-modal id="logs_modal" ref="modal" :title="langLogView">
|
||||
<textarea class="form-control log-viewer" id="log-view-contents" spellcheck="false" readonly>{{ logs }}</textarea>
|
||||
<textarea class="form-control log-viewer" spellcheck="false" readonly>{{ logs }}</textarea>
|
||||
|
||||
<template #modal-footer>
|
||||
<b-button variant="default" type="button" @click="close">
|
||||
<translate key="lang_btn_close">Close</translate>
|
||||
</b-button>
|
||||
<b-button variant="primary" class="btn_copy" data-clipboard-target="#log-view-contents" type="button">
|
||||
<b-button variant="primary" class="btn_copy" @click.prevent="doCopy" type="button">
|
||||
<translate key="lang_btn_copy">Copy to Clipboard</translate>
|
||||
</b-button>
|
||||
</template>
|
||||
|
@ -14,14 +14,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Clipboard from 'clipboard/dist/clipboard.min.js';
|
||||
import '~/vendor/clipboard.js';
|
||||
|
||||
export default {
|
||||
name: 'QueueLogsModal',
|
||||
data () {
|
||||
return {
|
||||
logs: 'Loading...',
|
||||
clipboard: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -29,14 +28,8 @@ export default {
|
|||
return this.$gettext('Log View');
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clipboard = new Clipboard('.btn_copy');
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clipboard.destroy();
|
||||
},
|
||||
methods: {
|
||||
show (logs) {
|
||||
show(logs) {
|
||||
let logDisplay = [];
|
||||
logs.forEach(function (log) {
|
||||
logDisplay.push(log.formatted);
|
||||
|
@ -45,7 +38,10 @@ export default {
|
|||
this.logs = logDisplay.join('');
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
close () {
|
||||
doCopy() {
|
||||
this.$copyText(this.logs);
|
||||
},
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<remote-form-basic-info :form="$v.form"></remote-form-basic-info>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit">
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<form-basic-info :form="$v.form"></form-basic-info>
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-card no-body>
|
||||
<b-card-header header-bg-variant="primary-dark">
|
||||
<h2 class="card-title" key="lang_title" v-translate>Web Hooks</h2>
|
||||
</b-card-header>
|
||||
|
||||
<info-card>
|
||||
<translate key="lang_info_card">Web hooks let you connect to external web services and broadcast changes to your station to them.</translate>
|
||||
</info-card>
|
||||
|
||||
<b-card-body body-class="card-padding-sm">
|
||||
<b-button variant="outline-primary" @click.prevent="doCreate">
|
||||
<icon icon="add"></icon>
|
||||
<translate key="lang_add_webhook">Add Web Hook</translate>
|
||||
</b-button>
|
||||
</b-card-body>
|
||||
|
||||
<data-table ref="datatable" id="station_webhooks" :show-toolbar="false" :fields="fields"
|
||||
:api-url="listUrl">
|
||||
<template #cell(name)="row">
|
||||
<big>{{ row.item.name }}</big><br>
|
||||
{{ getWebhookName(row.item.type) }}
|
||||
<b-badge v-if="!row.item.is_enabled" variant="danger">
|
||||
<translate key="lang_webhook_disabled">Disabled</translate>
|
||||
</b-badge>
|
||||
</template>
|
||||
<template #cell(triggers)="row">
|
||||
<div v-for="(name, index) in getTriggerNames(row.item.triggers)" :key="row.item.id+'_'+index"
|
||||
class="small">
|
||||
{{ name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(actions)="row">
|
||||
<b-button-group size="sm">
|
||||
<b-button size="sm" variant="primary" @click.prevent="doEdit(row.item.links.self)">
|
||||
<translate key="lang_btn_edit">Edit</translate>
|
||||
</b-button>
|
||||
<b-button size="sm" :variant="getToggleVariant(row.item)"
|
||||
@click.prevent="doToggle(row.item.links.toggle)">
|
||||
{{ langToggleButton(row.item) }}
|
||||
</b-button>
|
||||
<b-button size="sm" variant="default" @click.prevent="doTest(row.item.links.test)">
|
||||
<translate key="lang_btn_test">Test</translate>
|
||||
</b-button>
|
||||
<b-button size="sm" variant="danger" @click.prevent="doDelete(row.item.links.self)">
|
||||
<translate key="lang_btn_delete">Delete</translate>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</template>
|
||||
</data-table>
|
||||
</b-card>
|
||||
|
||||
<streaming-log-modal ref="logModal"></streaming-log-modal>
|
||||
<edit-modal ref="editModal" :create-url="listUrl" :webhook-types="webhookTypes"
|
||||
:webhook-triggers="webhookTriggers" @relist="relist"></edit-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from '~/components/Common/DataTable';
|
||||
import EditModal from './Webhooks/EditModal';
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import confirmDelete from "~/functions/confirmDelete";
|
||||
import InfoCard from "~/components/Common/InfoCard";
|
||||
import _ from 'lodash';
|
||||
import StreamingLogModal from "~/components/Common/StreamingLogModal";
|
||||
|
||||
export default {
|
||||
name: 'StationWebhooks',
|
||||
components: {StreamingLogModal, InfoCard, Icon, EditModal, DataTable},
|
||||
props: {
|
||||
listUrl: String,
|
||||
webhookTypes: Object,
|
||||
webhookTriggers: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
{key: 'name', isRowHeader: true, label: this.$gettext('Name/Type'), sortable: false},
|
||||
{key: 'triggers', label: this.$gettext('Triggers'), sortable: false},
|
||||
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
langToggleButton(record) {
|
||||
return (record.is_enabled)
|
||||
? this.$gettext('Disable')
|
||||
: this.$gettext('Enable');
|
||||
},
|
||||
getToggleVariant(record) {
|
||||
return (record.is_enabled)
|
||||
? 'warning'
|
||||
: 'success';
|
||||
},
|
||||
getWebhookName(key) {
|
||||
return _.get(this.webhookTypes, [key, 'name'], '');
|
||||
},
|
||||
getTriggerNames(triggers) {
|
||||
return _.map(triggers, (trigger) => {
|
||||
return _.get(this.webhookTriggers, trigger, '');
|
||||
});
|
||||
},
|
||||
relist() {
|
||||
this.$refs.datatable.refresh();
|
||||
},
|
||||
doCreate() {
|
||||
this.$refs.editModal.create();
|
||||
},
|
||||
doEdit(url) {
|
||||
this.$refs.editModal.edit(url);
|
||||
},
|
||||
doToggle(url) {
|
||||
this.$wrapWithLoading(
|
||||
this.axios.put(url)
|
||||
).then((resp) => {
|
||||
this.$notifySuccess(resp.data.message);
|
||||
this.relist();
|
||||
});
|
||||
},
|
||||
doTest(url) {
|
||||
this.$wrapWithLoading(
|
||||
this.axios.put(url)
|
||||
).then((resp) => {
|
||||
this.$refs.logModal.show(resp.data.links.log);
|
||||
});
|
||||
},
|
||||
doDelete(url) {
|
||||
confirmDelete({
|
||||
title: this.$gettext('Delete Web Hook?'),
|
||||
confirmButtonText: this.$gettext('Delete'),
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
this.$wrapWithLoading(
|
||||
this.axios.delete(url)
|
||||
).then((resp) => {
|
||||
this.$notifySuccess(resp.data.message);
|
||||
this.relist();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<type-select v-if="!type" :webhook-types="webhookTypes" @select="setType"></type-select>
|
||||
<b-tabs v-else lazy content-class="mt-3">
|
||||
<basic-info :trigger-options="triggerOptions" :form="$v.form"></basic-info>
|
||||
<component :is="formComponent" :title="typeTitle" :form="$v.form"></component>
|
||||
</b-tabs>
|
||||
|
||||
</modal-form>
|
||||
</template>
|
||||
<script>
|
||||
import {required} from 'vuelidate/dist/validators.min.js';
|
||||
import BaseEditModal from '~/components/Common/BaseEditModal';
|
||||
import TypeSelect from "./Form/TypeSelect";
|
||||
import BasicInfo from "./Form/BasicInfo";
|
||||
import _ from "lodash";
|
||||
import Generic from "~/components/Stations/Webhooks/Form/Generic";
|
||||
import Email from "~/components/Stations/Webhooks/Form/Email";
|
||||
import Tunein from "~/components/Stations/Webhooks/Form/Tunein";
|
||||
import Discord from "~/components/Stations/Webhooks/Form/Discord";
|
||||
import Telegram from "~/components/Stations/Webhooks/Form/Telegram";
|
||||
import Twitter from "~/components/Stations/Webhooks/Form/Twitter";
|
||||
import GoogleAnalytics from "~/components/Stations/Webhooks/Form/GoogleAnalytics";
|
||||
import MatomoAnalytics from "~/components/Stations/Webhooks/Form/MatomoAnalytics";
|
||||
|
||||
export default {
|
||||
name: 'EditModal',
|
||||
components: {BasicInfo, TypeSelect},
|
||||
mixins: [BaseEditModal],
|
||||
props: {
|
||||
webhookTypes: Object,
|
||||
webhookTriggers: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: null,
|
||||
webhookConfig: {
|
||||
'generic': {
|
||||
component: Generic,
|
||||
validations: {
|
||||
webhook_url: {required},
|
||||
basic_auth_username: {},
|
||||
basic_auth_password: {}
|
||||
},
|
||||
defaultConfig: {
|
||||
webhook_url: '',
|
||||
basic_auth_username: '',
|
||||
basic_auth_password: ''
|
||||
}
|
||||
},
|
||||
'email': {
|
||||
component: Email,
|
||||
validations: {
|
||||
to: {required},
|
||||
subject: {required},
|
||||
message: {required}
|
||||
},
|
||||
defaultConfig: {
|
||||
to: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
'tunein': {
|
||||
component: Tunein,
|
||||
validations: {
|
||||
station_id: {required},
|
||||
partner_id: {required},
|
||||
partner_key: {required},
|
||||
},
|
||||
defaultConfig: {
|
||||
station_id: '',
|
||||
partner_id: '',
|
||||
partner_key: ''
|
||||
}
|
||||
},
|
||||
'discord': {
|
||||
component: Discord,
|
||||
validations: {
|
||||
webhook_url: {required},
|
||||
content: {},
|
||||
title: {},
|
||||
description: {},
|
||||
url: {},
|
||||
author: {},
|
||||
thumbnail: {},
|
||||
footer: {},
|
||||
},
|
||||
defaultConfig: {
|
||||
webhook_url: '',
|
||||
content: this.langDiscordDefaultContent,
|
||||
title: '{{ now_playing.song.title }}',
|
||||
description: '{{ now_playing.song.artist }}',
|
||||
url: '{{ station.listen_url }}',
|
||||
author: '{{ live.streamer_name }}',
|
||||
thumbnail: '{{ now_playing.song.art }}',
|
||||
footer: this.langPoweredByAzuraCast,
|
||||
}
|
||||
},
|
||||
'telegram': {
|
||||
component: Telegram,
|
||||
validations: {
|
||||
bot_token: {required},
|
||||
chat_id: {required},
|
||||
api: {},
|
||||
text: {required},
|
||||
parse_mode: {required}
|
||||
},
|
||||
defaultConfig: {
|
||||
bot_token: '',
|
||||
chat_id: '',
|
||||
api: '',
|
||||
text: '',
|
||||
parse_mode: 'Markdown'
|
||||
}
|
||||
},
|
||||
'twitter': {
|
||||
component: Twitter,
|
||||
validations: {
|
||||
consumer_key: {required},
|
||||
consumer_secret: {required},
|
||||
token: {required},
|
||||
token_secret: {required},
|
||||
rate_limit: {},
|
||||
message: {required}
|
||||
},
|
||||
defaultConfig: {
|
||||
consumer_key: '',
|
||||
consumer_secret: '',
|
||||
token: '',
|
||||
token_secret: '',
|
||||
rate_limit: 0,
|
||||
message: this.langTwitterDefaultMessage
|
||||
}
|
||||
},
|
||||
'google_analytics': {
|
||||
component: GoogleAnalytics,
|
||||
validations: {
|
||||
tracking_id: {required}
|
||||
},
|
||||
defaultConfig: {
|
||||
tracking_id: ''
|
||||
}
|
||||
},
|
||||
'matomo_analytics': {
|
||||
component: MatomoAnalytics,
|
||||
validations: {
|
||||
matomo_url: {required},
|
||||
site_id: {required},
|
||||
token: {},
|
||||
},
|
||||
defaultConfig: {
|
||||
matomo_url: '',
|
||||
site_id: '',
|
||||
token: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
validations() {
|
||||
let validations = {
|
||||
type: {required},
|
||||
form: {
|
||||
name: {required},
|
||||
triggers: {},
|
||||
config: {}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.triggerOptions.length > 0) {
|
||||
validations.form.triggers = {required};
|
||||
}
|
||||
|
||||
if (this.type !== null) {
|
||||
validations.form.config = _.get(this.webhookConfig, [this.type, 'validations'], {});
|
||||
}
|
||||
|
||||
return validations;
|
||||
},
|
||||
computed: {
|
||||
langTitle() {
|
||||
return this.isEditMode
|
||||
? this.$gettext('Edit Web Hook')
|
||||
: this.$gettext('Add Web Hook');
|
||||
},
|
||||
triggerOptions () {
|
||||
if (!this.type) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let webhookKeys = _.get(this.webhookTypes, [this.type, 'triggers'], []);
|
||||
return _.map(webhookKeys, (key) => {
|
||||
return {
|
||||
text: this.webhookTriggers[key],
|
||||
value: key
|
||||
};
|
||||
});
|
||||
},
|
||||
typeTitle() {
|
||||
return _.get(this.webhookTypes, [this.type, 'name'], '');
|
||||
},
|
||||
formComponent() {
|
||||
return _.get(this.webhookConfig, [this.type, 'component'], Generic);
|
||||
},
|
||||
langPoweredByAzuraCast() {
|
||||
return this.$gettext('Powered by AzuraCast');
|
||||
},
|
||||
langDiscordDefaultContent() {
|
||||
let msg = this.$gettext('Now playing on %{ station }:');
|
||||
return this.$gettextInterpolate(msg, {'station': '{{ station.name }}'});
|
||||
},
|
||||
langTelegramDefaultContent() {
|
||||
let msg = this.$gettext('Now playing on %{ station }: %{ title } by %{ artist }! Tune in now.');
|
||||
return this.$gettextInterpolate(msg, {
|
||||
station: '{{ station.name }}',
|
||||
title: '{{ now_playing.song.title }}',
|
||||
artist: '{{ now_playing.song.artist }}'
|
||||
});
|
||||
},
|
||||
langTwitterDefaultMessage() {
|
||||
let msg = this.$gettext('Now playing on %{ station }: %{ title } by %{ artist }! Tune in now: %{ url }');
|
||||
return this.$gettextInterpolate(msg, {
|
||||
station: '{{ station.name }}',
|
||||
title: '{{ now_playing.song.title }}',
|
||||
artist: '{{ now_playing.song.artist }}',
|
||||
url: '{{ station.public_player_url }}'
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.type = null;
|
||||
this.form = {
|
||||
name: null,
|
||||
triggers: [],
|
||||
config: {}
|
||||
};
|
||||
},
|
||||
setType(type) {
|
||||
this.type = type;
|
||||
this.form.config = _.get(this.webhookConfig, [type, 'defaultConfig'], {});
|
||||
},
|
||||
getSubmittableFormData() {
|
||||
let formData = this.form;
|
||||
if (!this.isEditMode) {
|
||||
formData.type = this.type;
|
||||
}
|
||||
return formData;
|
||||
},
|
||||
populateForm(d) {
|
||||
this.type = d.type;
|
||||
this.form = {
|
||||
name: d.name,
|
||||
triggers: d.triggers,
|
||||
config: d.config
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<b-tab :title="langTabTitle" active>
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_edit_name" :field="form.name">
|
||||
<template #label>
|
||||
<translate key="lang_form_edit_name">Web Hook Name</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_edit_name_desc">Choose a name for this webhook that will help you distinguish it from others. This will only be shown on the administration page.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group v-if="triggerOptions.length > 0" class="col-md-12"
|
||||
id="edit_form_triggers"
|
||||
:field="form.triggers">
|
||||
<template #label>
|
||||
<translate key="lang_form_triggers">Web Hook Triggers</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_triggers_desc">This web hook will only run when the selected event(s) occur on this specific station.</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-checkbox-group :id="props.id" :options="triggerOptions"
|
||||
v-model="props.field.$model" stacked>
|
||||
</b-form-checkbox-group>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'BasicInfo',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
form: Object,
|
||||
triggerOptions: Array
|
||||
},
|
||||
computed: {
|
||||
langTabTitle() {
|
||||
return this.$gettext('Basic Info');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<translate key="lang_customize_message_hdr">Message Customization Tips</translate>
|
||||
</template>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_customize_message_desc_1">Variables are in the form of: </translate>
|
||||
<code v-pre>{{ var.name }}</code>
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_customize_message_desc_2">All values in the NowPlaying API response are available for use. Any empty fields are ignored.</translate>
|
||||
<br>
|
||||
<a href="https://azuracast.com/api" target="_blank">
|
||||
<translate key="lang_customize_response_link">NowPlaying API Response</translate>
|
||||
</a>
|
||||
</p>
|
||||
</b-form-group>
|
||||
</template>
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_webhook_url" :field="form.config.webhook_url"
|
||||
input-type="url">
|
||||
<template #label>
|
||||
<translate key="lang_form_webhook_url">Discord Web Hook URL</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate
|
||||
key="lang_form_webhook_url">This URL is provided within the Discord application.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
|
||||
<common-formatting-info></common-formatting-info>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_content" :field="form.config.content">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_content">Main Message Content</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_title" :field="form.config.title">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_title">Title</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_description" :field="form.config.description">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_description">Description</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_url" :field="form.config.url" input-type="url">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_url">URL</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_author" :field="form.config.author">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_author">Author Name</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_thumbnail" :field="form.config.thumbnail"
|
||||
input-type="url">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_thumbnail">Thumbnail Image URL</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_footer" :field="form.config.footer">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_footer">Footer Text</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import CommonFormattingInfo from "./CommonFormattingInfo";
|
||||
|
||||
export default {
|
||||
name: 'Discord',
|
||||
components: {CommonFormattingInfo, BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_to" :field="form.config.to">
|
||||
<template #label>
|
||||
<translate key="lang_form_to">Message Recipient(s)</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_to_desc">E-mail addresses can be separated by commas.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
|
||||
<common-formatting-info></common-formatting-info>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_subject" :field="form.config.subject">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_subject">Message Subject</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_message" :field="form.config.message">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_message">Message Body</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import CommonFormattingInfo from "./CommonFormattingInfo";
|
||||
|
||||
export default {
|
||||
name: 'Email',
|
||||
components: {CommonFormattingInfo, BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<translate key="lang_customize_message_hdr">Web Hook Details</translate>
|
||||
</template>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_customize_message_desc_1">Web hooks automatically send a HTTP POST request to the URL you specify to notify it any time one of the triggers you specify occurs on your station.</translate>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<translate key="lang_customize_message_desc_2">The body of the POST message is the exact same as the NowPlaying API response for your station.</translate>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://azuracast.com/api" target="_blank">
|
||||
<translate key="lang_customize_response_link">NowPlaying API Response</translate>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="card-text">
|
||||
<translate key="lang_customize_message_desc_3">In order to process quickly, web hooks have a short timeout, so the responding service should be optimized to handle the request in under 2 seconds.</translate>
|
||||
</p>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_webhook_url" :field="form.config.webhook_url"
|
||||
input-type="url">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_webhook_url">Web Hook URL</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_config_webhook_url_desc">The URL that will receive the POST messages any time an event is triggered.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_basic_auth_username"
|
||||
:field="form.config.basic_auth_username">
|
||||
<template #label>
|
||||
<translate
|
||||
key="lang_form_config_basic_auth_username">Optional: HTTP Basic Authentication Username</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_config_basic_auth_username_desc">If your web hook requires HTTP basic authentication, provide the username here.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_basic_auth_password"
|
||||
:field="form.config.basic_auth_password">
|
||||
<template #label>
|
||||
<translate
|
||||
key="lang_form_config_basic_auth_password">Optional: HTTP Basic Authentication Password</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_config_basic_auth_password_desc">If your web hook requires HTTP basic authentication, provide the password here.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'Generic',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_tracking_id" :field="form.config.tracking_id">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_tracking_id">GA Property Tracking ID</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate
|
||||
key="lang_form_config_tracking_id_desc">The property ID used to track live listeners.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'GoogleAnalytics',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_matomo_url" :field="form.config.matomo_url"
|
||||
input-type="url">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_matomo_url">Matomo Installation Base URL</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate
|
||||
key="lang_form_config_matomo_url_desc">The full base URL of your Matomo installation.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_site_id" :field="form.config.site_id">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_site_id">Matomo Site ID</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate
|
||||
key="lang_form_config_site_id_desc">The numeric site ID for this site.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_token" :field="form.config.token">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_token">Matomo API Token</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate
|
||||
key="lang_form_config_site_id_desc">Optionally supply an API token to allow IP address overriding.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'MatomoAnalytics',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_bot_token" :field="form.config.bot_token">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_bot_token">Bot Token</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<a href="https://core.telegram.org/bots#botfather" target="_blank">
|
||||
<translate key="lang_form_config_bot_token_desc">See the Telegram Documentation for more details.</translate>
|
||||
</a>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_chat_id" :field="form.config.chat_id">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_chat_id">Chat ID</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_config_chat_id_desc">Unique identifier for the target chat or username of the target channel (in the format @channelusername).</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_api" :field="form.config.api">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_api">Custom API Base URL</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_config_api_desc">Leave blank to use the default Telegram API URL (recommended).</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
|
||||
<common-formatting-info></common-formatting-info>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_text" :field="form.config.text"
|
||||
input-type="textarea">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_text">Main Message Content</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_parse_mode" :field="form.config.parse_mode">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_parse_mode">Message parsing mode</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<a href="https://core.telegram.org/bots/api#sendmessage" target="_blank">
|
||||
<translate key="lang_form_config_parse_mode_desc">See the Telegram documentation for more details.</translate>
|
||||
</a>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="parseModeOptions"
|
||||
v-model="props.field.$model">
|
||||
</b-form-radio-group>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import CommonFormattingInfo from "./CommonFormattingInfo";
|
||||
|
||||
export default {
|
||||
name: 'Telegram',
|
||||
components: {CommonFormattingInfo, BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
},
|
||||
computed: {
|
||||
parseModeOptions() {
|
||||
return [
|
||||
{
|
||||
text: this.$gettext('Markdown'),
|
||||
value: 'Markdown',
|
||||
},
|
||||
{
|
||||
text: this.$gettext('HTML'),
|
||||
value: 'HTML',
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_station_id" :field="form.config.station_id">
|
||||
<template #label>
|
||||
<translate key="lang_form_station_id">TuneIn Station ID</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_form_station_id_desc">The station ID will be a numeric string that starts with the letter S.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_partner_id" :field="form.config.partner_id">
|
||||
<template #label>
|
||||
<translate key="lang_form_partner_id">TuneIn Partner ID</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_partner_key" :field="form.config.partner_key">
|
||||
<template #label>
|
||||
<translate key="lang_form_partner_key">TuneIn Partner Key</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'Tunein',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<b-tab :title="title">
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<translate key="lang_twitter_instructions_hdr">Twitter Account Details</translate>
|
||||
</template>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_twitter_instructions_1">Steps for configuring a Twitter application:</translate>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<translate key="lang_twitter_instructions_1">Create a new app on the Twitter Applications site. Use this installation's base URL as the application URL.</translate>
|
||||
<br>
|
||||
<a href="https://developer.twitter.com/en/apps" target="_blank">
|
||||
<translate key="lang_twitter_instructions_url">Twitter Applications</translate>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_twitter_instructions_2">In the newly created application, click the "Keys and Access Tokens" tab.</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_twitter_instructions_3">At the bottom of the page, click "Create my access token".</translate>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="card-text">
|
||||
<translate key="lang_twitter_instructions_4">Once these steps are completed, enter the information from the "Keys and Access Tokens" page into the fields below.</translate>
|
||||
</p>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_consumer_key" :field="form.config.consumer_key">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_consumer_key">Consumer Key (API Key)</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_consumer_secret"
|
||||
:field="form.config.consumer_secret">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_consumer_secret">Consumer Secret (API Secret)</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_token" :field="form.config.token">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_token">Access Token</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_token_secret" :field="form.config.token_secret">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_token_secret">Access Token Secret</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_rate_limit" :field="form.config.rate_limit">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_rate_limit">Only Send One Tweet Every...</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="rateLimitOptions"
|
||||
v-model="props.field.$model">
|
||||
</b-form-radio-group>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
|
||||
<common-formatting-info></common-formatting-info>
|
||||
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_message" :field="form.config.message"
|
||||
input-type="textarea">
|
||||
<template #label>
|
||||
<translate key="lang_form_config_message">Message Body</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import CommonFormattingInfo from "./CommonFormattingInfo";
|
||||
|
||||
export default {
|
||||
name: 'Twitter',
|
||||
components: {CommonFormattingInfo, BWrappedFormGroup},
|
||||
props: {
|
||||
title: String,
|
||||
form: Object
|
||||
},
|
||||
computed: {
|
||||
langSeconds() {
|
||||
return this.$gettext('%{ seconds } seconds');
|
||||
},
|
||||
langMinutes() {
|
||||
return this.$gettext('%{ minutes } minutes');
|
||||
},
|
||||
rateLimitOptions() {
|
||||
return [
|
||||
{
|
||||
text: this.$gettext('No Limit'),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langSeconds, {seconds: 15}),
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langSeconds, {seconds: 30}),
|
||||
value: 30,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langSeconds, {seconds: 60}),
|
||||
value: 60,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 2}),
|
||||
value: 120,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 5}),
|
||||
value: 300,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 10}),
|
||||
value: 600,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 15}),
|
||||
value: 900,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 30}),
|
||||
value: 1800,
|
||||
},
|
||||
{
|
||||
text: this.$gettextInterpolate(this.langMinutes, {minutes: 60}),
|
||||
value: 3600,
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<translate key="lang_select_type_header">Select Web Hook Type</translate>
|
||||
</template>
|
||||
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="(info, key) in webhookTypes" :key="key" href="#" @click.prevent="selectType(key)"
|
||||
class="px-3">
|
||||
<h6 class="font-weight-bold mb-0">{{ info.name }}</h6>
|
||||
<p class="card-text small">{{ info.description }}</p>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TypeSelect',
|
||||
emits: ['select'],
|
||||
props: {
|
||||
webhookTypes: Object,
|
||||
},
|
||||
methods: {
|
||||
selectType(type) {
|
||||
this.$emit('select', type);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
import initBase
|
||||
from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import Webhooks
|
||||
from '~/components/Stations/Webhooks';
|
||||
|
||||
export default initBase(Webhooks);
|
|
@ -0,0 +1,8 @@
|
|||
import Vue
|
||||
from 'vue';
|
||||
import VueClipboard
|
||||
from 'vue-clipboard2';
|
||||
|
||||
VueClipboard.config.autoSetContainer = true;
|
||||
|
||||
Vue.use(VueClipboard);
|
|
@ -31,7 +31,8 @@ module.exports = {
|
|||
StationsReportsRequests: '~/pages/Stations/Reports/Requests.js',
|
||||
StationsReportsOverview: '~/pages/Stations/Reports/Overview.js',
|
||||
StationsReportsPerformance: '~/pages/Stations/Reports/Performance.js',
|
||||
StationsReportsTimeline: '~/pages/Stations/Reports/Timeline.js'
|
||||
StationsReportsTimeline: '~/pages/Stations/Reports/Timeline.js',
|
||||
StationsWebhooks: '~/pages/Stations/Webhooks.js'
|
||||
},
|
||||
resolve: {
|
||||
enforceExtension: false,
|
||||
|
|
|
@ -4,97 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\Api\Traits\HasLogViewer;
|
||||
use App\Entity;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Adapters;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
abstract class AbstractLogViewerController
|
||||
{
|
||||
public static int $maximum_log_size = 1048576;
|
||||
|
||||
protected function view(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $log_path,
|
||||
bool $tail_file = true
|
||||
): ResponseInterface {
|
||||
clearstatcache();
|
||||
|
||||
if (!is_file($log_path)) {
|
||||
throw new NotFoundException('Log file not found!');
|
||||
}
|
||||
|
||||
if (!$tail_file) {
|
||||
$log = file_get_contents($log_path) ?: '';
|
||||
$log_contents = $this->processLog($request, $log);
|
||||
|
||||
return $response->withJson([
|
||||
'contents' => $log_contents,
|
||||
'eof' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$params = $request->getQueryParams();
|
||||
$last_viewed_size = (int)($params['position'] ?? 0);
|
||||
|
||||
$log_size = filesize($log_path);
|
||||
if ($last_viewed_size > $log_size) {
|
||||
$last_viewed_size = $log_size;
|
||||
}
|
||||
|
||||
$log_visible_size = ($log_size - $last_viewed_size);
|
||||
$cut_first_line = false;
|
||||
|
||||
if ($log_visible_size > self::$maximum_log_size) {
|
||||
$log_visible_size = self::$maximum_log_size;
|
||||
$cut_first_line = true;
|
||||
}
|
||||
|
||||
$log_contents = '';
|
||||
|
||||
if ($log_visible_size > 0) {
|
||||
$fp = fopen($log_path, 'rb');
|
||||
if (false === $fp) {
|
||||
throw new \RuntimeException(sprintf('Could not open file at path "%s".', $log_path));
|
||||
}
|
||||
|
||||
fseek($fp, -$log_visible_size, SEEK_END);
|
||||
$log_contents_raw = fread($fp, $log_visible_size) ?: '';
|
||||
fclose($fp);
|
||||
|
||||
$log_contents = $this->processLog($request, $log_contents_raw, $cut_first_line, true);
|
||||
}
|
||||
|
||||
return $response->withJson([
|
||||
'contents' => $log_contents,
|
||||
'position' => $log_size,
|
||||
'eof' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function processLog(
|
||||
ServerRequest $request,
|
||||
string $rawLog,
|
||||
bool $cutFirstLine = false,
|
||||
bool $cutEmptyLastLine = false
|
||||
): string {
|
||||
$logParts = explode("\n", $rawLog);
|
||||
|
||||
if ($cutFirstLine) {
|
||||
array_shift($logParts);
|
||||
}
|
||||
if ($cutEmptyLastLine && end($logParts) === '') {
|
||||
array_pop($logParts);
|
||||
}
|
||||
|
||||
$logParts = str_replace(['>', '<'], ['>', '<'], $logParts);
|
||||
|
||||
$log = implode("\n", $logParts);
|
||||
return mb_convert_encoding($log, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
use HasLogViewer;
|
||||
|
||||
/**
|
||||
* @return array<string, array>
|
||||
|
|
|
@ -171,7 +171,7 @@ class BackupsController extends AbstractLogViewerController
|
|||
): ResponseInterface {
|
||||
$logPath = File::validateTempPath($path);
|
||||
|
||||
return $this->view($request, $response, $logPath, true);
|
||||
return $this->streamLogToResponse($request, $response, $logPath, true);
|
||||
}
|
||||
|
||||
public function downloadAction(
|
||||
|
|
|
@ -90,7 +90,7 @@ class DebugController extends AbstractLogViewerController
|
|||
): ResponseInterface {
|
||||
$logPath = File::validateTempPath($path);
|
||||
|
||||
return $this->view($request, $response, $logPath, true);
|
||||
return $this->streamLogToResponse($request, $response, $logPath, true);
|
||||
}
|
||||
|
||||
public function nextsongAction(
|
||||
|
|
|
@ -101,6 +101,6 @@ class LogsController extends AbstractLogViewerController
|
|||
}
|
||||
|
||||
$logArea = $log_areas[$log];
|
||||
return $this->view($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
return $this->streamLogToResponse($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Webhooks;
|
||||
|
||||
use App\Entity;
|
||||
use App\Exception\NotFoundException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
abstract class AbstractWebhooksAction
|
||||
{
|
||||
public function __construct(
|
||||
protected EntityManagerInterface $em
|
||||
) {
|
||||
}
|
||||
|
||||
protected function requireRecord(Entity\Station $station, int $id): Entity\StationWebhook
|
||||
{
|
||||
$record = $this->em->getRepository(Entity\StationWebhook::class)->findOneBy(
|
||||
[
|
||||
'station' => $station,
|
||||
'id' => $id,
|
||||
]
|
||||
);
|
||||
|
||||
if (!$record instanceof Entity\StationWebhook) {
|
||||
throw new NotFoundException(__('Web hook not found.'));
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Webhooks;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Message\TestWebhookMessage;
|
||||
use App\Utilities\File;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\Messenger\MessageBus;
|
||||
|
||||
class TestAction extends AbstractWebhooksAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
MessageBus $messageBus,
|
||||
int $id
|
||||
): ResponseInterface {
|
||||
$this->requireRecord($request->getStation(), $id);
|
||||
|
||||
$tempFile = File::generateTempPath('webhook_test_' . $id . '.log');
|
||||
|
||||
$message = new TestWebhookMessage();
|
||||
$message->webhookId = $id;
|
||||
$message->outputPath = $tempFile;
|
||||
|
||||
$messageBus->dispatch($message);
|
||||
|
||||
$router = $request->getRouter();
|
||||
return $response->withJson(
|
||||
[
|
||||
'success' => true,
|
||||
'links' => [
|
||||
'log' => (string)$router->fromHere('api:stations:webhook:test-log', [
|
||||
'path' => basename($tempFile),
|
||||
]),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Webhooks;
|
||||
|
||||
use App\Controller\Api\Traits\HasLogViewer;
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\File;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\Messenger\MessageBus;
|
||||
|
||||
class TestLogAction extends AbstractWebhooksAction
|
||||
{
|
||||
use HasLogViewer;
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
MessageBus $messageBus,
|
||||
int $id,
|
||||
string $path
|
||||
): ResponseInterface {
|
||||
$this->requireRecord($request->getStation(), $id);
|
||||
|
||||
$logPathPortion = 'webhook_test_' . $id;
|
||||
if (!str_contains($path, $logPathPortion)) {
|
||||
return $response
|
||||
->withStatus(403)
|
||||
->withJson(new Entity\Api\Error(403, 'Invalid log path.'));
|
||||
}
|
||||
|
||||
$tempPath = File::validateTempPath($path);
|
||||
|
||||
return $this->streamLogToResponse(
|
||||
$request,
|
||||
$response,
|
||||
$tempPath,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Webhooks;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ToggleAction extends AbstractWebhooksAction
|
||||
{
|
||||
public function __invoke(ServerRequest $request, Response $response, int $id): ResponseInterface
|
||||
{
|
||||
$record = $this->requireRecord($request->getStation(), $id);
|
||||
|
||||
$newValue = $record->toggleEnabled();
|
||||
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
|
||||
$flash_message = ($newValue)
|
||||
? __('Web hook enabled.')
|
||||
: __('Web hook disabled.');
|
||||
|
||||
return $response->withJson(new Entity\Api\Status(true, $flash_message));
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\ServerRequest;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
|
@ -98,4 +99,36 @@ class WebhooksController extends AbstractStationApiCrudController
|
|||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*/
|
||||
|
||||
protected function viewRecord(object $record, ServerRequest $request): mixed
|
||||
{
|
||||
if (!($record instanceof Entity\StationWebhook)) {
|
||||
throw new \InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass));
|
||||
}
|
||||
|
||||
$return = $this->toArray($record);
|
||||
|
||||
$isInternal = ('true' === $request->getParam('internal', 'false'));
|
||||
$router = $request->getRouter();
|
||||
|
||||
$return['links'] = [
|
||||
'self' => (string)$router->fromHere(
|
||||
route_name: $this->resourceRouteName,
|
||||
route_params: ['id' => $record->getIdRequired()],
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'toggle' => (string)$router->fromHere(
|
||||
route_name: 'api:stations:webhook:toggle',
|
||||
route_params: ['id' => $record->getIdRequired()],
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'test' => (string)$router->fromHere(
|
||||
route_name: 'api:stations:webhook:test',
|
||||
route_params: ['id' => $record->getIdRequired()],
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Traits;
|
||||
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
trait HasLogViewer
|
||||
{
|
||||
public static int $maximum_log_size = 1048576;
|
||||
|
||||
protected function streamLogToResponse(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $log_path,
|
||||
bool $tail_file = true
|
||||
): ResponseInterface {
|
||||
clearstatcache();
|
||||
|
||||
if (!is_file($log_path)) {
|
||||
throw new NotFoundException('Log file not found!');
|
||||
}
|
||||
|
||||
if (!$tail_file) {
|
||||
$log = file_get_contents($log_path) ?: '';
|
||||
$log_contents = $this->processLog($request, $log);
|
||||
|
||||
return $response->withJson(
|
||||
[
|
||||
'contents' => $log_contents,
|
||||
'eof' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$params = $request->getQueryParams();
|
||||
$last_viewed_size = (int)($params['position'] ?? 0);
|
||||
|
||||
$log_size = filesize($log_path);
|
||||
if ($last_viewed_size > $log_size) {
|
||||
$last_viewed_size = $log_size;
|
||||
}
|
||||
|
||||
$log_visible_size = ($log_size - $last_viewed_size);
|
||||
$cut_first_line = false;
|
||||
|
||||
if ($log_visible_size > self::$maximum_log_size) {
|
||||
$log_visible_size = self::$maximum_log_size;
|
||||
$cut_first_line = true;
|
||||
}
|
||||
|
||||
$log_contents = '';
|
||||
|
||||
if ($log_visible_size > 0) {
|
||||
$fp = fopen($log_path, 'rb');
|
||||
if (false === $fp) {
|
||||
throw new \RuntimeException(sprintf('Could not open file at path "%s".', $log_path));
|
||||
}
|
||||
|
||||
fseek($fp, -$log_visible_size, SEEK_END);
|
||||
$log_contents_raw = fread($fp, $log_visible_size) ?: '';
|
||||
fclose($fp);
|
||||
|
||||
$log_contents = $this->processLog($request, $log_contents_raw, $cut_first_line, true);
|
||||
}
|
||||
|
||||
return $response->withJson(
|
||||
[
|
||||
'contents' => $log_contents,
|
||||
'position' => $log_size,
|
||||
'eof' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function processLog(
|
||||
ServerRequest $request,
|
||||
string $rawLog,
|
||||
bool $cutFirstLine = false,
|
||||
bool $cutEmptyLastLine = false
|
||||
): string {
|
||||
$logParts = explode("\n", $rawLog);
|
||||
|
||||
if ($cutFirstLine) {
|
||||
array_shift($logParts);
|
||||
}
|
||||
if ($cutEmptyLastLine && end($logParts) === '') {
|
||||
array_pop($logParts);
|
||||
}
|
||||
|
||||
$logParts = str_replace(['>', '<'], ['>', '<'], $logParts);
|
||||
|
||||
$log = implode("\n", $logParts);
|
||||
return mb_convert_encoding($log, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ class LogsController extends AbstractLogViewerController
|
|||
}
|
||||
|
||||
$logArea = $log_areas[$log];
|
||||
return $this->view($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
return $this->streamLogToResponse($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
}
|
||||
|
||||
protected function processLog(
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Config;
|
||||
use App\Entity\Repository\SettingsRepository;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class WebhooksAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
SettingsRepository $settingsRepo,
|
||||
Config $config
|
||||
): ResponseInterface {
|
||||
$router = $request->getRouter();
|
||||
|
||||
$settings = $settingsRepo->readSettings();
|
||||
|
||||
$webhookConfig = $config->get('webhooks');
|
||||
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'system/vue',
|
||||
[
|
||||
'title' => __('Web Hooks'),
|
||||
'id' => 'station-webhooks',
|
||||
'component' => 'Vue_StationsWebhooks',
|
||||
'props' => [
|
||||
'listUrl' => (string)$router->fromHere('api:stations:webhooks'),
|
||||
'webhookTypes' => $webhookConfig['webhooks'],
|
||||
'webhookTriggers' => $webhookConfig['triggers'],
|
||||
'enableAdvancedFeatures' => $settings->getEnableAdvancedFeatures(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Entity;
|
||||
use App\Form\StationWebhookForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Session\Flash;
|
||||
use App\Webhook\Dispatcher;
|
||||
use DI\FactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class WebhooksController extends AbstractStationCrudController
|
||||
{
|
||||
protected array $webhook_config;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $dispatcher,
|
||||
FactoryInterface $factory
|
||||
) {
|
||||
$form = $factory->make(StationWebhookForm::class);
|
||||
|
||||
parent::__construct($form);
|
||||
$this->webhook_config = $form->getConfig();
|
||||
|
||||
$this->csrf_namespace = 'stations_webhooks';
|
||||
}
|
||||
|
||||
public function indexAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/webhooks/index', [
|
||||
'webhooks' => $station->getWebhooks(),
|
||||
'webhook_config' => $this->webhook_config,
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addAction(ServerRequest $request, Response $response, string $type = null): ResponseInterface
|
||||
{
|
||||
$view = $request->getView();
|
||||
if ($type === null) {
|
||||
return $view->renderToResponse(
|
||||
$response,
|
||||
'stations/webhooks/add',
|
||||
[
|
||||
'connectors' => array_filter(
|
||||
$this->webhook_config['webhooks'],
|
||||
static function ($webhook) {
|
||||
return !empty($webhook['name']);
|
||||
}
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$record = new Entity\StationWebhook($request->getStation(), $type);
|
||||
|
||||
if (false !== $this->form->process($request, $record)) {
|
||||
$request->getFlash()->addMessage('<b>' . __('Web Hook added.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index'));
|
||||
}
|
||||
|
||||
return $view->renderToResponse($response, 'system/form_page', [
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => __('Add Web Hook'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function editAction(ServerRequest $request, Response $response, int $id): ResponseInterface
|
||||
{
|
||||
if (false !== $this->doEdit($request, $id)) {
|
||||
$request->getFlash()->addMessage('<b>' . __('Web Hook updated.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'system/form_page',
|
||||
[
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => __('Edit Web Hook'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function toggleAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $id,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
|
||||
|
||||
/** @var Entity\StationWebhook $record */
|
||||
$record = $this->getRecord($request->getStation(), $id);
|
||||
|
||||
$new_status = $record->toggleEnabled();
|
||||
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
|
||||
$request->getFlash()->addMessage(
|
||||
'<b>' . ($new_status ? __('Web hook enabled.') : __('Web Hook disabled.')) . '</b>',
|
||||
Flash::SUCCESS
|
||||
);
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index'));
|
||||
}
|
||||
|
||||
public function testAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $id,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$request->getCsrf()->verify($csrf, $this->csrf_namespace);
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
/** @var Entity\StationWebhook $record */
|
||||
$record = $this->getRecord($station, $id);
|
||||
|
||||
$log_records = $this->dispatcher->testDispatch($station, $record)->getRecords();
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'system/log_view', [
|
||||
'title' => __('Web Hook Test Output'),
|
||||
'log_records' => $log_records,
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $id,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$this->doDelete($request, $id, $csrf);
|
||||
|
||||
$request->getFlash()->addMessage('<b>' . __('Web Hook deleted.') . '</b>', Flash::SUCCESS);
|
||||
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index'));
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Config;
|
||||
use App\Entity;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class StationRemoteForm extends EntityForm
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config
|
||||
) {
|
||||
$form_config = $config->get('forms/remote');
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->entityClass = Entity\StationRemote::class;
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Config;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Http\Router;
|
||||
use App\Http\ServerRequest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class StationWebhookForm extends EntityForm
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
protected array $forms;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Environment $environment,
|
||||
Config $config,
|
||||
Router $router
|
||||
) {
|
||||
$webhook_config = $config->get('webhooks');
|
||||
|
||||
$webhook_forms = [];
|
||||
$config_injections = [
|
||||
'router' => $router,
|
||||
'triggers' => $webhook_config['triggers'],
|
||||
'environment' => $environment,
|
||||
];
|
||||
|
||||
foreach ($webhook_config['webhooks'] as $webhook_key => $webhook_info) {
|
||||
$webhook_forms[$webhook_key] = $config->get('forms/webhook/' . $webhook_key, $config_injections);
|
||||
}
|
||||
|
||||
parent::__construct($em, $serializer, $validator);
|
||||
|
||||
$this->config = $webhook_config;
|
||||
$this->forms = $webhook_forms;
|
||||
$this->entityClass = Entity\StationWebhook::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getForms(): array
|
||||
{
|
||||
return $this->forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(ServerRequest $request, $record = null): object|bool
|
||||
{
|
||||
if (!$record instanceof Entity\StationWebhook) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Record is not an instance of %s',
|
||||
Entity\StationWebhook::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->configure($this->forms[$record->getType()]);
|
||||
|
||||
return parent::process($request, $record);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
use App\MessageQueue\QueueManagerInterface;
|
||||
|
||||
class AddNewPodcastMediaMessage extends AbstractUniqueMessage
|
||||
{
|
||||
/** @var int The numeric identifier for the StorageLocation entity. */
|
||||
public int $storageLocationId;
|
||||
|
||||
/** @var string The relative path for the podcast media file to be processed. */
|
||||
public string $path;
|
||||
|
||||
public function getQueue(): string
|
||||
{
|
||||
return QueueManagerInterface::QUEUE_PODCAST_MEDIA;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
use App\MessageQueue\QueueManagerInterface;
|
||||
|
||||
class ReprocessPodcastMediaMessage extends AbstractUniqueMessage
|
||||
{
|
||||
/** @var int The numeric identifier for the PodcastMedia record being processed. */
|
||||
public int $podcastMediaId;
|
||||
|
||||
/** @var bool Whether to force reprocessing even if checks indicate it is not necessary. */
|
||||
public bool $force = false;
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'ReprocessPodcastMediaMessage_' . $this->podcastMediaId;
|
||||
}
|
||||
|
||||
public function getQueue(): string
|
||||
{
|
||||
return QueueManagerInterface::QUEUE_PODCAST_MEDIA;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
use App\Environment;
|
||||
use App\MessageQueue\QueueManagerInterface;
|
||||
|
||||
class TestWebhookMessage extends AbstractUniqueMessage
|
||||
{
|
||||
public int $webhookId;
|
||||
|
||||
/** @var string|null The path to log output of the Backup command to. */
|
||||
public ?string $outputPath = null;
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return 'TestWebHook_' . $this->webhookId;
|
||||
}
|
||||
|
||||
public function getTtl(): ?float
|
||||
{
|
||||
return Environment::getInstance()->getSyncLongExecutionTime();
|
||||
}
|
||||
|
||||
public function getQueue(): string
|
||||
{
|
||||
return QueueManagerInterface::QUEUE_NORMAL_PRIORITY;
|
||||
}
|
||||
}
|
|
@ -10,8 +10,10 @@ use App\Exception;
|
|||
use App\Http\RouterInterface;
|
||||
use App\Message;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LogLevel;
|
||||
use Symfony\Component\Messenger\MessageBus;
|
||||
|
||||
class Dispatcher
|
||||
|
@ -35,46 +37,60 @@ class Dispatcher
|
|||
*/
|
||||
public function __invoke(Message\AbstractMessage $message): void
|
||||
{
|
||||
if (!($message instanceof Message\DispatchWebhookMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$station = $this->em->find(Entity\Station::class, $message->station_id);
|
||||
if (!$station instanceof Entity\Station) {
|
||||
return;
|
||||
}
|
||||
|
||||
$np = $message->np;
|
||||
$triggers = (array)$message->triggers;
|
||||
|
||||
// Always dispatch the special "local" updater task.
|
||||
$this->localHandler->dispatch($station, $np);
|
||||
|
||||
if ($this->environment->isTesting()) {
|
||||
$this->logger->notice('In testing mode; no webhooks dispatched.');
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Entity\StationWebhook[] $enabledWebhooks */
|
||||
$enabledWebhooks = $station->getWebhooks()->filter(
|
||||
function (Entity\StationWebhook $webhook) {
|
||||
return $webhook->isEnabled();
|
||||
if ($message instanceof Message\DispatchWebhookMessage) {
|
||||
$station = $this->em->find(Entity\Station::class, $message->station_id);
|
||||
if (!$station instanceof Entity\Station) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
$this->logger->debug('Webhook dispatch: triggering events: ' . implode(', ', $triggers));
|
||||
$np = $message->np;
|
||||
$triggers = (array)$message->triggers;
|
||||
|
||||
foreach ($enabledWebhooks as $webhook) {
|
||||
$connectorObj = $this->connectors->getConnector($webhook->getType());
|
||||
// Always dispatch the special "local" updater task.
|
||||
$this->localHandler->dispatch($station, $np);
|
||||
|
||||
if ($connectorObj->shouldDispatch($webhook, $triggers)) {
|
||||
$this->logger->debug(sprintf('Dispatching connector "%s".', $webhook->getType()));
|
||||
if ($this->environment->isTesting()) {
|
||||
$this->logger->notice('In testing mode; no webhooks dispatched.');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($connectorObj->dispatch($station, $webhook, $np, $triggers)) {
|
||||
$webhook->updateLastSentTimestamp();
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
/** @var Entity\StationWebhook[] $enabledWebhooks */
|
||||
$enabledWebhooks = $station->getWebhooks()->filter(
|
||||
function (Entity\StationWebhook $webhook) {
|
||||
return $webhook->isEnabled();
|
||||
}
|
||||
);
|
||||
|
||||
$this->logger->debug('Webhook dispatch: triggering events: ' . implode(', ', $triggers));
|
||||
|
||||
foreach ($enabledWebhooks as $webhook) {
|
||||
$connectorObj = $this->connectors->getConnector($webhook->getType());
|
||||
|
||||
if ($connectorObj->shouldDispatch($webhook, $triggers)) {
|
||||
$this->logger->debug(sprintf('Dispatching connector "%s".', $webhook->getType()));
|
||||
|
||||
if ($connectorObj->dispatch($station, $webhook, $np, $triggers)) {
|
||||
$webhook->updateLastSentTimestamp();
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($message instanceof Message\TestWebhookMessage) {
|
||||
$outputPath = $message->outputPath;
|
||||
|
||||
if (null !== $outputPath) {
|
||||
$logHandler = new StreamHandler($outputPath, LogLevel::DEBUG, true);
|
||||
$this->logger->pushHandler($logHandler);
|
||||
}
|
||||
|
||||
$webhook = $this->em->find(Entity\StationWebhook::class, $message->webhookId);
|
||||
if ($webhook instanceof Entity\StationWebhook) {
|
||||
$this->testDispatch($webhook);
|
||||
}
|
||||
|
||||
if (null !== $outputPath) {
|
||||
$this->logger->popHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,16 +99,16 @@ class Dispatcher
|
|||
* Send a "test" dispatch of the web hook, regardless of whether it is currently enabled, and
|
||||
* return any logging information this yields.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
* @param Entity\StationWebhook $webhook
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testDispatch(
|
||||
Entity\Station $station,
|
||||
Entity\StationWebhook $webhook
|
||||
): TestHandler {
|
||||
$handler = new TestHandler(Logger::DEBUG, false);
|
||||
$station = $webhook->getStation();
|
||||
|
||||
$handler = new TestHandler(LogLevel::DEBUG, true);
|
||||
$this->logger->pushHandler($handler);
|
||||
|
||||
$np = $this->nowPlayingApiGen->currentOrEmpty($station);
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
$this->layout('main', [
|
||||
'title' => __('Add Web Hook')
|
||||
]);
|
||||
?>
|
||||
|
||||
<h3 class="mb-4"><?=__('Select the type of web hook to create.') ?></h3>
|
||||
|
||||
<dl>
|
||||
<?php foreach($connectors as $type => $info): ?>
|
||||
<dt class="mb-2"><a href="<?=$router->fromHere(null, ['type' => $type]) ?>" class="btn btn-primary"><?=$info['name'] ?></a></dt>
|
||||
<dd class="pb-3"><?=$info['description'] ?></dd>
|
||||
<?php endforeach; ?>
|
||||
</dl>
|
|
@ -1,74 +0,0 @@
|
|||
<?php $this->layout('main', ['title' => __('Web Hooks'), 'manual' => true]) ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Web Hooks')?></h2>
|
||||
</div>
|
||||
<div class="card-body alert-info d-flex align-items-center" role="alert">
|
||||
<div class="flex-shrink-0 mr-2">
|
||||
<i class="material-icons" aria-hidden="true">info</i>
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<p class="card-text">
|
||||
<?=__('Web hooks let you connect to external web services and broadcast changes to your station to them.')?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-primary" role="button" href="<?=$router->fromHere('stations:webhooks:add')?>">
|
||||
<i class="material-icons" aria-hidden="true">add</i>
|
||||
<?=__('Add Web Hook')?>
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-responsive-md table-striped mb-0">
|
||||
<colgroup>
|
||||
<col width="30%">
|
||||
<col width="35%">
|
||||
<col width="35%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?=__('Actions')?></th>
|
||||
<th><?=__('Name')?> / <?=__('Type')?></th>
|
||||
<th><?=__('Triggers')?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($webhooks as $row): ?>
|
||||
<?php /** @var \App\Entity\StationWebhook $row */ ?>
|
||||
<tr class="align-middle">
|
||||
<td>
|
||||
<a class="btn btn-sm btn-primary" href="<?=$router->fromHere('stations:webhooks:edit',
|
||||
['id' => $row->getId()])?>"><?=__('Edit')?></a>
|
||||
<a class="btn btn-sm <?=($row->isEnabled() ? 'btn-warning' : 'btn-success')?>" href="<?=$router->fromHere('stations:webhooks:toggle',
|
||||
[
|
||||
'id' => $row->getId(),
|
||||
'csrf' => $csrf,
|
||||
])?>"><?=($row->isEnabled() ? __('Disable') : __('Enable'))?></a>
|
||||
<a class="btn btn-sm btn-default" href="<?=$router->fromHere('stations:webhooks:test', [
|
||||
'id' => $row->getId(),
|
||||
'csrf' => $csrf,
|
||||
])?>" title="<?=__('Trigger the web hook manually and view the raw response.')?>"><?=__('Test')?></a>
|
||||
<a class="btn btn-sm btn-danger" data-confirm-title="<?=$this->e(__('Delete web hook "%s"?',
|
||||
$row->getName()))?>" href="<?=$router->fromHere('stations:webhooks:delete',
|
||||
['id' => $row->getId(), 'csrf' => $csrf])?>"><?=__('Delete')?></a>
|
||||
</td>
|
||||
<td>
|
||||
<big><?=$this->e($row->getName())?></big><br>
|
||||
<?=$webhook_config['webhooks'][$row->getType()]['name']?><?php if (!$row->isEnabled()): ?>
|
||||
<span class="label label-danger"><?=__('Disabled')?></span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$trigger_names = [];
|
||||
foreach ((array)$row->getTriggers() as $trigger) {
|
||||
$trigger_names[] = $webhook_config['triggers'][$trigger];
|
||||
}
|
||||
echo implode(', ', $trigger_names);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
Loading…
Reference in New Issue