Add Mastodon webhook; refactor Webhook dispatching.
This commit is contained in:
parent
493423c6bf
commit
33a1c84b5a
|
@ -5,6 +5,8 @@ release channel, you can take advantage of these new features and fixes.
|
|||
|
||||
## New Features/Changes
|
||||
|
||||
- **Mastodon Posting Support**: Publish to Mastodon via a Web Hook, the same way you do with Twitter!
|
||||
|
||||
- **Cover Art Files Support**: Many users keep the cover art for their media alongside the media in a separate image
|
||||
file. AzuraCast now detects image files in the same folder as your media and uses it as the default album art for that
|
||||
media. Because cover art files are often named a variety of things, we currently will use _any_ image file that exists
|
||||
|
@ -20,6 +22,8 @@ release channel, you can take advantage of these new features and fixes.
|
|||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed an issue where listener connection times over a day didn't properly show up.
|
||||
|
||||
- Fixed several issues contributing to slow load times on media manager pages.
|
||||
|
||||
- Fixed a bug where if a station only had "Allowed IPs", it wouldn't be enforced.
|
||||
|
|
|
@ -61,6 +61,12 @@ return [
|
|||
'description' => __('Automatically send a tweet.'),
|
||||
'triggers' => $allTriggers,
|
||||
],
|
||||
Connector\Mastodon::NAME => [
|
||||
'class' => Connector\Mastodon::class,
|
||||
'name' => __('Mastodon Post'),
|
||||
'description' => __('Automatically publish to a Mastodon instance.'),
|
||||
'triggers' => [],
|
||||
],
|
||||
Connector\GoogleAnalytics::NAME => [
|
||||
'class' => Connector\GoogleAnalytics::class,
|
||||
'name' => __('Google Analytics Integration'),
|
||||
|
|
|
@ -24,14 +24,15 @@ 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";
|
||||
import Generic from "./Form/Generic";
|
||||
import Email from "./Form/Email";
|
||||
import Tunein from "./Form/Tunein";
|
||||
import Discord from "./Form/Discord";
|
||||
import Telegram from "./Form/Telegram";
|
||||
import Twitter from "./Form/Twitter";
|
||||
import GoogleAnalytics from "./Form/GoogleAnalytics";
|
||||
import MatomoAnalytics from "./Form/MatomoAnalytics";
|
||||
import Mastodon from "./Form/Mastodon";
|
||||
|
||||
export default {
|
||||
name: 'EditModal',
|
||||
|
@ -193,6 +194,23 @@ export default {
|
|||
message: this.langTwitterDefaultMessage
|
||||
}
|
||||
},
|
||||
'mastodon': {
|
||||
component: Mastodon,
|
||||
validations: {
|
||||
instance_url: {required},
|
||||
access_token: {required},
|
||||
rate_limit: {},
|
||||
message: {required},
|
||||
visibility: {required}
|
||||
},
|
||||
defaultConfig: {
|
||||
instance_url: '',
|
||||
access_token: '',
|
||||
rate_limit: 0,
|
||||
message: this.langTwitterDefaultMessage,
|
||||
visibility: 'public'
|
||||
}
|
||||
},
|
||||
'google_analytics': {
|
||||
component: GoogleAnalytics,
|
||||
validations: {
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<template #label>
|
||||
<translate key="lang_mastodon_hdr">Mastodon Account Details</translate>
|
||||
</template>
|
||||
|
||||
<p class="card-text">
|
||||
<translate key="lang_mastodon_instructions_1">Steps for configuring a Mastodon application:</translate>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<translate key="lang_mastodon_instructions_1">Visit your Mastodon instance.</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_mastodon_instructions_2">Click the "Preferences" link, then "Development" on the left side menu.</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_mastodon_instructions_3">Click "New Application"</translate>
|
||||
</li>
|
||||
<li>
|
||||
<translate key="lang_mastodon_instructions_4">Enter "AzuraCast" as the application name. You can leave the URL fields unchanged. For "Scopes", only "write:media" and "write:statuses" are required.</translate>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="card-text">
|
||||
<translate key="lang_twitter_instructions_5">Once these steps are completed, enter the "Access Token" from the application's page into the field below.</translate>
|
||||
</p>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_instance_url" :field="form.config.instance_url">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Mastodon Instance URL</translate>
|
||||
</template>
|
||||
<template #description="{lang}">
|
||||
<translate
|
||||
:key="lang">If your Mastodon username is "@test@example.com", enter "example.com".</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="form_config_access_token"
|
||||
:field="form.config.access_token">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Access Token</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="{lang}">
|
||||
<translate :key="lang">Only Post Once 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-form-row>
|
||||
</b-form-group>
|
||||
|
||||
<common-formatting-info></common-formatting-info>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-row>
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_message" :field="form.config.message"
|
||||
input-type="textarea">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Message Body</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="form_config_visibility" :field="form.config.visibility">
|
||||
<template #label="{lang}">
|
||||
<translate :key="lang">Message Visibility</translate>
|
||||
</template>
|
||||
<template #default="props">
|
||||
<b-form-radio-group stacked :id="props.id" :options="visibilityOptions"
|
||||
v-model="props.field.$model">
|
||||
</b-form-radio-group>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</b-form-row>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import CommonFormattingInfo from "./CommonFormattingInfo";
|
||||
|
||||
export default {
|
||||
name: 'Twitter',
|
||||
components: {CommonFormattingInfo, BWrappedFormGroup},
|
||||
props: {
|
||||
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,
|
||||
}
|
||||
];
|
||||
},
|
||||
visibilityOptions() {
|
||||
return [
|
||||
{
|
||||
text: this.$gettext('Public'),
|
||||
value: 'public',
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Unlisted'),
|
||||
value: 'unlisted',
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Private'),
|
||||
value: 'private',
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -114,4 +114,14 @@ abstract class AbstractConnector implements ConnectorInterface
|
|||
$pattern = sprintf(UrlValidator::PATTERN, 'http|https');
|
||||
return (preg_match($pattern, $url)) ? $url : null;
|
||||
}
|
||||
|
||||
protected function incompleteConfigException(): \InvalidArgumentException
|
||||
{
|
||||
return new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'Webhook %s is missing necessary configuration. Skipping...',
|
||||
static::NAME
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,11 @@ interface ConnectorInterface
|
|||
* @param Entity\StationWebhook $webhook
|
||||
* @param Entity\Api\NowPlaying\NowPlaying $np
|
||||
* @param array<string> $triggers
|
||||
*
|
||||
* @return bool Whether the webhook actually dispatched.
|
||||
*/
|
||||
public function dispatch(
|
||||
Entity\Station $station,
|
||||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool;
|
||||
): void;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Monolog\Level;
|
||||
|
||||
/*
|
||||
|
@ -72,14 +71,13 @@ final class Discord extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
$webhook_url = $this->getValidUrl($config['webhook_url'] ?? '');
|
||||
|
||||
if (empty($webhook_url)) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
$raw_vars = [
|
||||
|
@ -131,29 +129,22 @@ final class Discord extends AbstractConnector
|
|||
// Dispatch webhook
|
||||
$this->logger->debug('Dispatching Discord webhook...');
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
$webhook_url,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $webhook_body,
|
||||
]
|
||||
);
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
$webhook_url,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $webhook_body,
|
||||
]
|
||||
);
|
||||
|
||||
$this->logger->addRecord(
|
||||
($response->getStatusCode() !== 204 ? Level::Error : Level::Debug),
|
||||
sprintf('Webhook %s returned code %d', self::NAME, $response->getStatusCode()),
|
||||
['message_sent' => $webhook_body, 'response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(sprintf('Error from Discord (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$this->logger->addRecord(
|
||||
($response->getStatusCode() !== 204 ? Level::Error : Level::Debug),
|
||||
sprintf('Webhook %s returned code %d', self::NAME, $response->getStatusCode()),
|
||||
['message_sent' => $webhook_body, 'response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
}
|
||||
|
||||
/** @noinspection HttpUrlsUsage */
|
||||
|
|
|
@ -8,7 +8,6 @@ use App\Entity;
|
|||
use App\Service\Mail;
|
||||
use GuzzleHttp\Client;
|
||||
use Monolog\Logger;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
|
||||
final class Email extends AbstractConnector
|
||||
{
|
||||
|
@ -30,10 +29,9 @@ final class Email extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
if (!$this->mail->isEnabled()) {
|
||||
$this->logger->error('E-mail delivery is not currently enabled. Skipping webhook delivery...');
|
||||
return false;
|
||||
throw new \RuntimeException('E-mail delivery is not currently enabled. Skipping webhook delivery...');
|
||||
}
|
||||
|
||||
$config = $webhook->getConfig();
|
||||
|
@ -42,32 +40,24 @@ final class Email extends AbstractConnector
|
|||
$emailBody = $config['message'];
|
||||
|
||||
if (empty($emailTo) || empty($emailSubject) || empty($emailBody)) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
try {
|
||||
$email = $this->mail->createMessage();
|
||||
$email = $this->mail->createMessage();
|
||||
|
||||
foreach (explode(',', $emailTo) as $emailToPart) {
|
||||
$email->addTo(trim($emailToPart));
|
||||
}
|
||||
|
||||
$vars = [
|
||||
'subject' => $emailSubject,
|
||||
'body' => $emailBody,
|
||||
];
|
||||
$vars = $this->replaceVariables($vars, $np);
|
||||
|
||||
$email->subject($vars['subject']);
|
||||
$email->text($vars['body']);
|
||||
|
||||
$this->mail->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->error(sprintf('Error from e-mail (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
foreach (explode(',', $emailTo) as $emailToPart) {
|
||||
$email->addTo(trim($emailToPart));
|
||||
}
|
||||
|
||||
return true;
|
||||
$vars = [
|
||||
'subject' => $emailSubject,
|
||||
'body' => $emailBody,
|
||||
];
|
||||
$vars = $this->replaceVariables($vars, $np);
|
||||
|
||||
$email->subject($vars['subject']);
|
||||
$email->text($vars['body']);
|
||||
|
||||
$this->mail->send($email);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
final class Generic extends AbstractConnector
|
||||
{
|
||||
|
@ -19,43 +18,35 @@ final class Generic extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
$webhook_url = $this->getValidUrl($config['webhook_url'] ?? '');
|
||||
|
||||
if (empty($webhook_url)) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
try {
|
||||
$request_options = [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $np,
|
||||
'timeout' => (float)($config['timeout'] ?? 5.0),
|
||||
$request_options = [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $np,
|
||||
'timeout' => (float)($config['timeout'] ?? 5.0),
|
||||
];
|
||||
|
||||
if (!empty($config['basic_auth_username']) && !empty($config['basic_auth_password'])) {
|
||||
$request_options['auth'] = [
|
||||
$config['basic_auth_username'],
|
||||
$config['basic_auth_password'],
|
||||
];
|
||||
|
||||
if (!empty($config['basic_auth_username']) && !empty($config['basic_auth_password'])) {
|
||||
$request_options['auth'] = [
|
||||
$config['basic_auth_username'],
|
||||
$config['basic_auth_password'],
|
||||
];
|
||||
}
|
||||
|
||||
$response = $this->httpClient->request('POST', $webhook_url, $request_options);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Generic webhook returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(sprintf('Error from generic webhook (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$response = $this->httpClient->request('POST', $webhook_url, $request_options);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Generic webhook returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,11 +31,10 @@ final class GoogleAnalytics extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
if (empty($config['tracking_id'])) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
// Get listen URLs for each mount point.
|
||||
|
@ -95,7 +94,5 @@ final class GoogleAnalytics extends AbstractConnector
|
|||
}
|
||||
|
||||
$analytics->sendEnqueuedHits();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use App\Utilities\Urls;
|
||||
|
||||
/**
|
||||
* Mastodon web hook connector.
|
||||
*/
|
||||
final class Mastodon extends AbstractConnector
|
||||
{
|
||||
public const NAME = 'mastodon';
|
||||
|
||||
protected function getRateLimitTime(Entity\StationWebhook $webhook): ?int
|
||||
{
|
||||
$config = $webhook->getConfig();
|
||||
$rateLimitSeconds = (int)($config['rate_limit'] ?? 0);
|
||||
|
||||
return max(10, $rateLimitSeconds);
|
||||
}
|
||||
|
||||
public function dispatch(
|
||||
Entity\Station $station,
|
||||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
$instanceUrl = trim($config['instance_url'] ?? '');
|
||||
$accessToken = trim($config['access_token'] ?? '');
|
||||
|
||||
if (empty($instanceUrl) || empty($accessToken)) {
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
$messages = $this->replaceVariables(
|
||||
[
|
||||
'message' => $config['message'] ?? '',
|
||||
],
|
||||
$np
|
||||
);
|
||||
|
||||
$instanceUri = Urls::parseUserUrl($instanceUrl, 'Mastodon Instance URL');
|
||||
$visibility = $config['visibility'] ?? 'public';
|
||||
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
$instanceUri->withPath('/api/v1/statuses'),
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $accessToken,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'status' => $messages['message'],
|
||||
'visibility' => $visibility,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Webhook %s returned code %d', self::NAME, $response->getStatusCode()),
|
||||
[
|
||||
'instanceUri' => (string)$instanceUri,
|
||||
'response' => $response->getBody()->getContents(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ use App\Entity;
|
|||
use App\Http\RouterInterface;
|
||||
use App\Utilities\Urls;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Monolog\Logger;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
|
@ -33,12 +32,11 @@ final class MatomoAnalytics extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
if (empty($config['matomo_url']) || empty($config['site_id'])) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
// Get listen URLs for each mount point.
|
||||
|
@ -119,21 +117,19 @@ final class MatomoAnalytics extends AbstractConnector
|
|||
$i++;
|
||||
|
||||
if (100 === $i) {
|
||||
if (!$this->sendBatch($apiUrl, $apiToken, $entries)) {
|
||||
return false;
|
||||
}
|
||||
$this->sendBatch($apiUrl, $apiToken, $entries);
|
||||
$entries = [];
|
||||
$i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendBatch($apiUrl, $apiToken, $entries);
|
||||
$this->sendBatch($apiUrl, $apiToken, $entries);
|
||||
}
|
||||
|
||||
private function sendBatch(UriInterface $apiUrl, ?string $apiToken, array $entries): bool
|
||||
private function sendBatch(UriInterface $apiUrl, ?string $apiToken, array $entries): void
|
||||
{
|
||||
if (empty($entries)) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
$jsonBody = [
|
||||
|
@ -148,20 +144,13 @@ final class MatomoAnalytics extends AbstractConnector
|
|||
|
||||
$this->logger->debug('Message body for Matomo API Query', ['body' => $jsonBody]);
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->post($apiUrl, [
|
||||
'json' => $jsonBody,
|
||||
]);
|
||||
$response = $this->httpClient->post($apiUrl, [
|
||||
'json' => $jsonBody,
|
||||
]);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Matomo returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(sprintf('Error from Matomo (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$this->logger->debug(
|
||||
sprintf('Matomo returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
/**
|
||||
* Telegram web hook connector.
|
||||
|
@ -24,15 +23,14 @@ final class Telegram extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
$bot_token = trim($config['bot_token'] ?? '');
|
||||
$chat_id = trim($config['chat_id'] ?? '');
|
||||
|
||||
if (empty($bot_token) || empty($chat_id)) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
$messages = $this->replaceVariables(
|
||||
|
@ -42,48 +40,33 @@ final class Telegram extends AbstractConnector
|
|||
$np
|
||||
);
|
||||
|
||||
try {
|
||||
$api_url = (!empty($config['api'])) ? rtrim($config['api'], '/') : 'https://api.telegram.org';
|
||||
$webhook_url = $api_url . '/bot' . $bot_token . '/sendMessage';
|
||||
$api_url = (!empty($config['api'])) ? rtrim($config['api'], '/') : 'https://api.telegram.org';
|
||||
$webhook_url = $api_url . '/bot' . $bot_token . '/sendMessage';
|
||||
|
||||
$request_params = [
|
||||
'chat_id' => $chat_id,
|
||||
'text' => $messages['text'],
|
||||
'parse_mode' => $config['parse_mode'] ?? 'Markdown', // Markdown or HTML
|
||||
];
|
||||
$request_params = [
|
||||
'chat_id' => $chat_id,
|
||||
'text' => $messages['text'],
|
||||
'parse_mode' => $config['parse_mode'] ?? 'Markdown', // Markdown or HTML
|
||||
];
|
||||
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
$webhook_url,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $request_params,
|
||||
]
|
||||
);
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
$webhook_url,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $request_params,
|
||||
]
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Webhook %s returned code %d', self::NAME, $response->getStatusCode()),
|
||||
[
|
||||
'request_url' => $webhook_url,
|
||||
'request_params' => $request_params,
|
||||
'response_body' => $response->getBody()->getContents(),
|
||||
]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'Error from webhook %s (%d): %s',
|
||||
self::NAME,
|
||||
$e->getCode(),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$this->logger->debug(
|
||||
sprintf('Webhook %s returned code %d', self::NAME, $response->getStatusCode()),
|
||||
[
|
||||
'request_url' => $webhook_url,
|
||||
'request_params' => $request_params,
|
||||
'response_body' => $response->getBody()->getContents(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
final class TuneIn extends AbstractConnector
|
||||
{
|
||||
|
@ -24,40 +23,32 @@ final class TuneIn extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
if (empty($config['partner_id']) || empty($config['partner_key']) || empty($config['station_id'])) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
$this->logger->debug('Dispatching TuneIn AIR API call...');
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->get(
|
||||
'https://air.radiotime.com/Playing.ashx',
|
||||
[
|
||||
'query' => [
|
||||
'partnerId' => $config['partner_id'],
|
||||
'partnerKey' => $config['partner_key'],
|
||||
'id' => $config['station_id'],
|
||||
'title' => $np->now_playing?->song?->title,
|
||||
'artist' => $np->now_playing?->song?->artist,
|
||||
'album' => $np->now_playing?->song?->album,
|
||||
],
|
||||
]
|
||||
);
|
||||
$response = $this->httpClient->get(
|
||||
'https://air.radiotime.com/Playing.ashx',
|
||||
[
|
||||
'query' => [
|
||||
'partnerId' => $config['partner_id'],
|
||||
'partnerKey' => $config['partner_key'],
|
||||
'id' => $config['station_id'],
|
||||
'title' => $np->now_playing?->song?->title,
|
||||
'artist' => $np->now_playing?->song?->artist,
|
||||
'album' => $np->now_playing?->song?->album,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('TuneIn returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(sprintf('Error from TuneIn (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$this->logger->debug(
|
||||
sprintf('TuneIn returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace App\Webhook\Connector;
|
|||
use App\Entity;
|
||||
use App\Service\GuzzleFactory;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Subscriber\Oauth\Oauth1;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
@ -39,7 +38,7 @@ final class Twitter extends AbstractConnector
|
|||
Entity\StationWebhook $webhook,
|
||||
Entity\Api\NowPlaying\NowPlaying $np,
|
||||
array $triggers
|
||||
): bool {
|
||||
): void {
|
||||
$config = $webhook->getConfig();
|
||||
|
||||
if (
|
||||
|
@ -48,8 +47,7 @@ final class Twitter extends AbstractConnector
|
|||
|| empty($config['token'])
|
||||
|| empty($config['token_secret'])
|
||||
) {
|
||||
$this->logger->error('Webhook ' . self::NAME . ' is missing necessary configuration. Skipping...');
|
||||
return false;
|
||||
throw $this->incompleteConfigException();
|
||||
}
|
||||
|
||||
// Set up Twitter OAuth
|
||||
|
@ -74,28 +72,21 @@ final class Twitter extends AbstractConnector
|
|||
// Dispatch webhook
|
||||
$this->logger->debug('Posting to Twitter...');
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
'https://api.twitter.com/1.1/statuses/update.json',
|
||||
[
|
||||
'auth' => 'oauth',
|
||||
'handler' => $stack,
|
||||
'form_params' => [
|
||||
'status' => $vars['message'],
|
||||
],
|
||||
]
|
||||
);
|
||||
$response = $this->httpClient->request(
|
||||
'POST',
|
||||
'https://api.twitter.com/1.1/statuses/update.json',
|
||||
[
|
||||
'auth' => 'oauth',
|
||||
'handler' => $stack,
|
||||
'form_params' => [
|
||||
'status' => $vars['message'],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf('Twitter returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
} catch (TransferException $e) {
|
||||
$this->logger->error(sprintf('Error from Twitter (%d): %s', $e->getCode(), $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
$this->logger->debug(
|
||||
sprintf('Twitter returned code %d', $response->getStatusCode()),
|
||||
['response_body' => $response->getBody()->getContents()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,12 @@ namespace App\Webhook;
|
|||
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Exception;
|
||||
use App\Http\RouterInterface;
|
||||
use App\Message;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\TestHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
final class Dispatcher
|
||||
{
|
||||
|
@ -36,88 +34,108 @@ final class Dispatcher
|
|||
public function __invoke(Message\AbstractMessage $message): void
|
||||
{
|
||||
if ($message instanceof Message\DispatchWebhookMessage) {
|
||||
$station = $this->em->find(Entity\Station::class, $message->station_id);
|
||||
if (!$station instanceof Entity\Station) {
|
||||
return;
|
||||
$this->handleDispatch($message);
|
||||
} elseif ($message instanceof Message\TestWebhookMessage) {
|
||||
$this->testDispatch($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleDispatch(Message\DispatchWebhookMessage $message): void
|
||||
{
|
||||
$station = $this->em->find(Entity\Station::class, $message->station_id);
|
||||
if (!$station instanceof Entity\Station) {
|
||||
return;
|
||||
}
|
||||
|
||||
$np = $message->np;
|
||||
$triggers = $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->getIsEnabled();
|
||||
}
|
||||
);
|
||||
|
||||
$np = $message->np;
|
||||
$triggers = (array)$message->triggers;
|
||||
$this->logger->debug('Webhook dispatch: triggering events: ' . implode(', ', $triggers));
|
||||
|
||||
// Always dispatch the special "local" updater task.
|
||||
$this->localHandler->dispatch($station, $np);
|
||||
foreach ($enabledWebhooks as $webhook) {
|
||||
$connectorObj = $this->connectors->getConnector($webhook->getType());
|
||||
|
||||
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->getIsEnabled();
|
||||
}
|
||||
);
|
||||
|
||||
$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->shouldDispatch($webhook, $triggers)) {
|
||||
$this->logger->debug(sprintf('Dispatching connector "%s".', $webhook->getType()));
|
||||
|
||||
try {
|
||||
if ($connectorObj->dispatch($station, $webhook, $np, $triggers)) {
|
||||
$webhook->updateLastSentTimestamp();
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'%s L%d: %s',
|
||||
$e->getFile(),
|
||||
$e->getLine(),
|
||||
$e->getMessage()
|
||||
),
|
||||
[
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif ($message instanceof Message\TestWebhookMessage) {
|
||||
$outputPath = $message->outputPath;
|
||||
}
|
||||
|
||||
if (null !== $outputPath) {
|
||||
$logHandler = new StreamHandler($outputPath, LogLevel::DEBUG, true);
|
||||
$this->logger->pushHandler($logHandler);
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function testDispatch(
|
||||
Message\TestWebhookMessage $message
|
||||
): void {
|
||||
$outputPath = $message->outputPath;
|
||||
|
||||
if (null !== $outputPath) {
|
||||
$logHandler = new StreamHandler($outputPath, Level::Debug, true);
|
||||
$this->logger->pushHandler($logHandler);
|
||||
}
|
||||
|
||||
try {
|
||||
$webhook = $this->em->find(Entity\StationWebhook::class, $message->webhookId);
|
||||
if ($webhook instanceof Entity\StationWebhook) {
|
||||
$this->testDispatch($webhook);
|
||||
if (!($webhook instanceof Entity\StationWebhook)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$station = $webhook->getStation();
|
||||
$np = $this->nowPlayingApiGen->currentOrEmpty($station);
|
||||
$np->resolveUrls($this->router->getBaseUrl());
|
||||
$np->cache = 'event';
|
||||
|
||||
$connectorObj = $this->connectors->getConnector($webhook->getType());
|
||||
$connectorObj->dispatch($station, $webhook, $np, [Entity\StationWebhook::TRIGGER_ALL]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'%s L%d: %s',
|
||||
$e->getFile(),
|
||||
$e->getLine(),
|
||||
$e->getMessage()
|
||||
),
|
||||
[
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
} finally {
|
||||
if (null !== $outputPath) {
|
||||
$this->logger->popHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "test" dispatch of the web hook, regardless of whether it is currently enabled, and
|
||||
* return any logging information this yields.
|
||||
*
|
||||
* @param Entity\StationWebhook $webhook
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testDispatch(
|
||||
Entity\StationWebhook $webhook
|
||||
): TestHandler {
|
||||
$station = $webhook->getStation();
|
||||
|
||||
$handler = new TestHandler(LogLevel::DEBUG, true);
|
||||
$this->logger->pushHandler($handler);
|
||||
|
||||
$np = $this->nowPlayingApiGen->currentOrEmpty($station);
|
||||
$np->resolveUrls($this->router->getBaseUrl());
|
||||
$np->cache = 'event';
|
||||
|
||||
$connectorObj = $this->connectors->getConnector($webhook->getType());
|
||||
$connectorObj->dispatch($station, $webhook, $np, [Entity\StationWebhook::TRIGGER_ALL]);
|
||||
|
||||
$this->logger->popHandler();
|
||||
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,16 +63,5 @@ final class LocalWebhookHandler
|
|||
$staticNpPath,
|
||||
json_encode($np, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: ''
|
||||
);
|
||||
|
||||
// Send Nchan notification.
|
||||
$this->logger->debug('Dispatching Nchan notification...');
|
||||
|
||||
$this->httpClient->post(
|
||||
$this->environment->getInternalUri()
|
||||
->withPath('/pub/' . urlencode($station->getShortName())),
|
||||
[
|
||||
'json' => $np,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue