diff --git a/.gitignore b/.gitignore
index e992ad1b2..cb384473a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,8 @@ web/static/yarn-error\.log
# Docker files
/docker-compose.yml
/*.tar.gz
+
+# Plugins
+/plugins/*
+/plugins/**/*
+!/plugins/.gitkeep
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 3f63edefa..a2e44e314 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -58,6 +58,10 @@ ini_set('session.use_strict_mode', 1);
$autoloader = require(APP_INCLUDE_VENDOR . '/autoload.php');
$autoloader->setPsr4('Proxy\\', APP_INCLUDE_TEMP . '/proxies');
+// Initialize plugins
+$plugins = new \App\Plugins(APP_INCLUDE_ROOT.'/plugins');
+$plugins->registerAutoloaders($autoloader);
+
// Set up DI container.
$di = new \Slim\Container([
'settings' => [
@@ -69,8 +73,12 @@ $di = new \Slim\Container([
]
]);
+$di[\App\Plugins::class] = $plugins;
+
// Define services.
$settings = require(dirname(__DIR__).'/config/settings.php');
call_user_func(include(dirname(__DIR__).'/config/services.php'), $di, $settings);
+$plugins->registerServices($di, $settings);
+
return $di;
diff --git a/composer.json b/composer.json
index 89356da46..a77b993ce 100644
--- a/composer.json
+++ b/composer.json
@@ -41,6 +41,7 @@
"php-http/guzzle6-adapter": "^1.1",
"slim/slim": "^3.0",
"supervisorphp/supervisor": "^3.0",
+ "symfony/event-dispatcher": "^4.1",
"symfony/finder": "^4.1",
"zendframework/zend-config": "^3.1.0",
"zendframework/zend-paginator": "^2.7"
diff --git a/composer.lock b/composer.lock
index 51887cb13..b21acc0cc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "594ac3f2423a143d5621fb2cffbd11fa",
+ "content-hash": "1d77d7c553cb7dd8c3f52c09642fcf36",
"packages": [
{
"name": "azuracast/azuraforms",
@@ -3419,6 +3419,69 @@
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
},
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v4.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bfb30c2ad377615a463ebbc875eba64a99f6aa3e",
+ "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<3.4"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/stopwatch": "~3.4|~4.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony EventDispatcher Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-07-26T09:10:45+00:00"
+ },
{
"name": "symfony/finder",
"version": "v4.1.4",
@@ -5956,69 +6019,6 @@
"homepage": "https://symfony.com",
"time": "2018-07-26T11:00:49+00:00"
},
- {
- "name": "symfony/event-dispatcher",
- "version": "v4.1.4",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bfb30c2ad377615a463ebbc875eba64a99f6aa3e",
- "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e",
- "shasum": ""
- },
- "require": {
- "php": "^7.1.3"
- },
- "conflict": {
- "symfony/dependency-injection": "<3.4"
- },
- "require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "~3.4|~4.0",
- "symfony/dependency-injection": "~3.4|~4.0",
- "symfony/expression-language": "~3.4|~4.0",
- "symfony/stopwatch": "~3.4|~4.0"
- },
- "suggest": {
- "symfony/dependency-injection": "",
- "symfony/http-kernel": ""
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.1-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\EventDispatcher\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony EventDispatcher Component",
- "homepage": "https://symfony.com",
- "time": "2018-07-26T09:10:45+00:00"
- },
{
"name": "symfony/polyfill-ctype",
"version": "v1.9.0",
diff --git a/config/events.php b/config/events.php
new file mode 100644
index 000000000..4bee5cf7c
--- /dev/null
+++ b/config/events.php
@@ -0,0 +1,14 @@
+addSubscriber(new \App\EventHandler\DefaultRoutes(__DIR__.'/routes.php'));
+ $dispatcher->addSubscriber(new \App\EventHandler\DefaultView($di));
+ $dispatcher->addSubscriber(new \App\EventHandler\DefaultNowPlaying());
+
+ $dispatcher->addListener(\App\Event\SendWebhooks::NAME, function(\App\Event\SendWebhooks $event) use ($di) {
+ /** @var \App\Webhook\Dispatcher $webhook_dispatcher */
+ $webhook_dispatcher = $di[\App\Webhook\Dispatcher::class];
+
+ $webhook_dispatcher->dispatch($event);
+ });
+};
diff --git a/config/services.php b/config/services.php
index 28fd2dce0..0beb8b28b 100644
--- a/config/services.php
+++ b/config/services.php
@@ -233,46 +233,9 @@ return function (\Slim\Container $di, $settings) {
$view = new App\View(dirname(__DIR__) . '/resources/templates');
$view->setFileExtension('phtml');
- $view->registerFunction('service', function($service) use ($di) {
- return $di->get($service);
- });
-
- $view->registerFunction('escapeJs', function($string) {
- return json_encode($string);
- });
-
- $view->registerFunction('mailto', function ($address, $link_text = null) {
- $address = substr(chunk_split(bin2hex(" $address"), 2, ";"), 3, -3);
- $link_text = $link_text ?? $address;
-
- return '' . $link_text . '';
- });
-
- $view->registerFunction('pluralize', function ($word, $num = 0) {
- if ((int)$num === 1) {
- return $word;
- } else {
- return \Doctrine\Common\Inflector\Inflector::pluralize($word);
- }
- });
-
- $view->registerFunction('truncate', function ($text, $length = 80) {
- return \App\Utilities::truncate_text($text, $length);
- });
-
- /** @var \App\Session $session */
- $session = $di[\App\Session::class];
-
- $view->addData([
- 'app_settings' => $di['app_settings'],
- 'router' => $di['router'],
- 'request' => $di['request'],
- 'assets' => $di[\App\Assets::class],
- 'auth' => $di[\App\Auth::class],
- 'acl' => $di[\App\Acl::class],
- 'flash' => $session->getFlash(),
- 'customization' => $di[\App\Customization::class],
- ]);
+ /** @var \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */
+ $dispatcher = $di[\Symfony\Component\EventDispatcher\EventDispatcher::class];
+ $dispatcher->dispatch(\App\Event\BuildView::NAME, new \App\Event\BuildView($view));
return $view;
});
@@ -337,6 +300,21 @@ return function (\Slim\Container $di, $settings) {
]);
};
+ $di[\Symfony\Component\EventDispatcher\EventDispatcher::class] = function($di) use ($settings) {
+ $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
+
+ // Register application default events.
+ call_user_func(include(__DIR__.'/events.php'), $dispatcher, $di, $settings);
+
+ /** @var \App\Plugins $plugins */
+ $plugins = $di[\App\Plugins::class];
+
+ // Register plugin-provided events.
+ $plugins->registerEvents($dispatcher, $di, $settings);
+
+ return $dispatcher;
+ };
+
//
// AzuraCast-specific dependencies
//
@@ -401,23 +379,9 @@ return function (\Slim\Container $di, $settings) {
$app = new \Slim\App($di);
- // Get the current user entity object and assign it into the request if it exists.
- $app->add(\App\Middleware\GetCurrentUser::class);
-
- // Inject the application router into the request object.
- $app->add(\App\Middleware\EnableRouter::class);
-
- // Inject the session manager into the request object.
- $app->add(\App\Middleware\EnableSession::class);
-
- // Check HTTPS setting and enforce Content Security Policy accordingly.
- $app->add(\App\Middleware\EnforceSecurity::class);
-
- // Remove trailing slash from all URLs when routing.
- $app->add(\App\Middleware\RemoveSlashes::class);
-
- // Load routes
- call_user_func(include(__DIR__.'/routes.php'), $app);
+ /** @var \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */
+ $dispatcher = $di[\Symfony\Component\EventDispatcher\EventDispatcher::class];
+ $dispatcher->dispatch(\App\Event\BuildRoutes::NAME, new \App\Event\BuildRoutes($app));
return $app;
};
diff --git a/plugins/.gitkeep b/plugins/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/Event/BuildRoutes.php b/src/Event/BuildRoutes.php
new file mode 100644
index 000000000..e84404b24
--- /dev/null
+++ b/src/Event/BuildRoutes.php
@@ -0,0 +1,22 @@
+app = $app;
+ }
+
+ public function getApp(): App
+ {
+ return $this->app;
+ }
+}
diff --git a/src/Event/BuildView.php b/src/Event/BuildView.php
new file mode 100644
index 000000000..d63727caa
--- /dev/null
+++ b/src/Event/BuildView.php
@@ -0,0 +1,22 @@
+view = $view;
+ }
+
+ public function getView(): View
+ {
+ return $this->view;
+ }
+}
diff --git a/src/Event/GenerateRawNowPlaying.php b/src/Event/GenerateRawNowPlaying.php
new file mode 100644
index 000000000..93c06ee80
--- /dev/null
+++ b/src/Event/GenerateRawNowPlaying.php
@@ -0,0 +1,82 @@
+station = $station;
+ $this->frontend = $frontend;
+ $this->remotes = $remotes;
+ $this->payload = $payload;
+ $this->include_clients = $include_clients;
+ }
+
+ public function getStation(): Station
+ {
+ return $this->station;
+ }
+
+ public function getFrontend(): FrontendAbstract
+ {
+ return $this->frontend;
+ }
+
+ /**
+ * @return RemoteAbstract[]
+ */
+ public function getRemotes(): array
+ {
+ return $this->remotes;
+ }
+
+ public function includeClients(): bool
+ {
+ return $this->include_clients;
+ }
+
+ public function getPayload(): ?string
+ {
+ return $this->payload;
+ }
+
+ public function getRawResponse(): array
+ {
+ return $this->np_raw;
+ }
+
+ public function setRawResponse(array $np): void
+ {
+ $this->np_raw = $np;
+ }
+}
diff --git a/src/Event/SendWebhooks.php b/src/Event/SendWebhooks.php
new file mode 100644
index 000000000..69bfcbaa0
--- /dev/null
+++ b/src/Event/SendWebhooks.php
@@ -0,0 +1,93 @@
+station = $station;
+ $this->np = $np;
+ $this->is_standalone = $is_standalone;
+
+ $to_trigger = ['all'];
+
+ if ($np_old instanceof NowPlaying) {
+ if ($np_old->now_playing->song->id !== $np->now_playing->song->id) {
+ $to_trigger[] = 'song_changed';
+ }
+
+ if ($np_old->listeners->current > $np->listeners->current) {
+ $to_trigger[] = 'listener_lost';
+ } elseif ($np_old->listeners->current < $np->listeners->current) {
+ $to_trigger[] = 'listener_gained';
+ }
+
+ if ($np_old->live->is_live === false && $np->live->is_live === true) {
+ $to_trigger[] = 'live_connect';
+ } elseif ($np_old->live->is_live === true && $np->live->is_live === false) {
+ $to_trigger[] = 'live_disconnect';
+ }
+ }
+
+ $this->triggers = $to_trigger;
+ }
+
+ /**
+ * @return Station
+ */
+ public function getStation(): Station
+ {
+ return $this->station;
+ }
+
+ /**
+ * @return NowPlaying
+ */
+ public function getNowPlaying(): NowPlaying
+ {
+ return $this->np;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTriggers(): array
+ {
+ return $this->triggers;
+ }
+
+ /**
+ * @param $trigger_name
+ * @return bool
+ */
+ public function hasTrigger($trigger_name): bool
+ {
+ return in_array($trigger_name, $this->triggers);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isStandalone(): bool
+ {
+ return $this->is_standalone;
+ }
+}
diff --git a/src/EventHandler/DefaultNowPlaying.php b/src/EventHandler/DefaultNowPlaying.php
new file mode 100644
index 000000000..ffeb74065
--- /dev/null
+++ b/src/EventHandler/DefaultNowPlaying.php
@@ -0,0 +1,55 @@
+ [
+ ['loadRawFromFrontend', 10],
+ ['addToRawFromRemotes', 0],
+ ['cleanUpRawOutput', -10],
+ ],
+ ];
+ }
+
+ public function loadRawFromFrontend(GenerateRawNowPlaying $event)
+ {
+ $np_raw = $event->getFrontend()->getNowPlaying($event->getPayload(), $event->includeClients());
+
+ $event->setRawResponse($np_raw);
+ }
+
+ public function addToRawFromRemotes(GenerateRawNowPlaying $event)
+ {
+ $np_raw = $event->getRawResponse();
+
+ // Loop through all remotes and update NP data accordingly.
+ foreach($event->getRemotes() as $remote_adapter) {
+ $remote_adapter->updateNowPlaying($np_raw, $event->includeClients());
+ }
+
+ $event->setRawResponse($np_raw);
+ }
+
+ public function cleanUpRawOutput(GenerateRawNowPlaying $event)
+ {
+ $np_raw = $event->getRawResponse();
+
+ array_walk($np_raw['current_song'], function(&$value) {
+ $value = htmlspecialchars_decode($value);
+ $value = trim($value);
+ });
+
+ $event->setRawResponse($np_raw);
+ }
+
+}
diff --git a/src/EventHandler/DefaultRoutes.php b/src/EventHandler/DefaultRoutes.php
new file mode 100644
index 000000000..ad6c014d3
--- /dev/null
+++ b/src/EventHandler/DefaultRoutes.php
@@ -0,0 +1,50 @@
+ [
+ ['addDefaultMiddleware', 1],
+ ['addDefaultRoutes', 0],
+ ],
+ ];
+ }
+
+ protected $routes_path;
+
+ public function __construct($routes_path)
+ {
+ $this->routes_path = $routes_path;
+ }
+
+ public function addDefaultMiddleware(BuildRoutes $event)
+ {
+ $app = $event->getApp();
+
+ // Get the current user entity object and assign it into the request if it exists.
+ $app->add(\App\Middleware\GetCurrentUser::class);
+
+ // Inject the application router into the request object.
+ $app->add(\App\Middleware\EnableRouter::class);
+
+ // Inject the session manager into the request object.
+ $app->add(\App\Middleware\EnableSession::class);
+
+ // Check HTTPS setting and enforce Content Security Policy accordingly.
+ $app->add(\App\Middleware\EnforceSecurity::class);
+
+ // Remove trailing slash from all URLs when routing.
+ $app->add(\App\Middleware\RemoveSlashes::class);
+ }
+
+ public function addDefaultRoutes(BuildRoutes $event)
+ {
+ call_user_func(include($this->routes_path), $event->getApp());
+ }
+}
diff --git a/src/EventHandler/DefaultView.php b/src/EventHandler/DefaultView.php
new file mode 100644
index 000000000..18d4c975b
--- /dev/null
+++ b/src/EventHandler/DefaultView.php
@@ -0,0 +1,76 @@
+ [
+ ['addViewFunctions', 1],
+ ['addViewData', 0],
+ ],
+ ];
+ }
+
+ /** @var Container */
+ protected $di;
+
+ public function __construct(Container $di)
+ {
+ $this->di = $di;
+ }
+
+ public function addViewFunctions(BuildView $event)
+ {
+ $view = $event->getView();
+
+ $view->registerFunction('service', function($service) {
+ return $this->di->get($service);
+ });
+
+ $view->registerFunction('escapeJs', function($string) {
+ return json_encode($string);
+ });
+
+ $view->registerFunction('mailto', function ($address, $link_text = null) {
+ $address = substr(chunk_split(bin2hex(" $address"), 2, ";"), 3, -3);
+ $link_text = $link_text ?? $address;
+
+ return '' . $link_text . '';
+ });
+
+ $view->registerFunction('pluralize', function ($word, $num = 0) {
+ if ((int)$num === 1) {
+ return $word;
+ } else {
+ return \Doctrine\Common\Inflector\Inflector::pluralize($word);
+ }
+ });
+
+ $view->registerFunction('truncate', function ($text, $length = 80) {
+ return \App\Utilities::truncate_text($text, $length);
+ });
+ }
+
+ public function addViewData(BuildView $event)
+ {
+ /** @var \App\Session $session */
+ $session = $this->di[\App\Session::class];
+
+ $event->getView()->addData([
+ 'app_settings' => $this->di['app_settings'],
+ 'router' => $this->di['router'],
+ 'request' => $this->di['request'],
+ 'assets' => $this->di[\App\Assets::class],
+ 'auth' => $this->di[\App\Auth::class],
+ 'acl' => $this->di[\App\Acl::class],
+ 'flash' => $session->getFlash(),
+ 'customization' => $this->di[\App\Customization::class],
+ ]);
+ }
+}
diff --git a/src/EventHandler/DefaultWebhooks.php b/src/EventHandler/DefaultWebhooks.php
new file mode 100644
index 000000000..0dbc4c5de
--- /dev/null
+++ b/src/EventHandler/DefaultWebhooks.php
@@ -0,0 +1,137 @@
+logger = $logger;
+ $this->connectors = $connectors;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ if (APP_TESTING_MODE) {
+ return [];
+ }
+
+ return [
+ SendWebhooks::NAME => [
+ ['dispatchWebhooks', 0],
+ ],
+ ];
+ }
+
+ public function dispatchWebhooks(SendWebhooks $event)
+ {
+ // Compile list of connectors for the station. Always dispatch to the local websocket receiver.
+ $connectors = [];
+
+ if ($event->isStandalone()) {
+ $connectors[] = [
+ 'type' => 'local',
+ 'triggers' => [],
+ 'config' => [],
+ ];
+ }
+
+ // Assemble list of webhooks for the station
+ $station_webhooks = $event->getStation()->getWebhooks();
+
+ if ($station_webhooks->count() > 0) {
+ foreach($station_webhooks as $webhook) {
+ /** @var Entity\StationWebhook $webhook */
+ if ($webhook->isEnabled()) {
+ $connectors[] = [
+ 'type' => $webhook->getType(),
+ 'triggers' => $webhook->getTriggers() ?: [],
+ 'config' => $webhook->getConfig() ?: [],
+ ];
+ }
+ }
+ }
+
+ $this->logger->debug('Triggering events: '.implode(', ', $event->getTriggers()));
+
+ // Trigger all appropriate webhooks.
+ foreach($connectors as $connector) {
+ if (!$this->connectors->has($connector['type'])) {
+ $this->logger->error(sprintf('Webhook connector "%s" does not exist; skipping.', $connector['type']));
+ continue;
+ }
+
+ /** @var Connector\ConnectorInterface $connector_obj */
+ $connector_obj = $this->connectors->get($connector['type']);
+
+ if ($connector_obj->shouldDispatch($event, (array)$connector['triggers'])) {
+ $this->logger->debug(sprintf('Dispatching connector "%s".', $connector['type']));
+
+ $connector_obj->dispatch($event, (array)$connector['config']);
+ }
+ }
+ }
+
+ /**
+ * 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
+ * @return TestHandler
+ * @throws Exception
+ */
+ public function testDispatch(Entity\Station $station, Entity\StationWebhook $webhook)
+ {
+ $webhook_type = $webhook->getType();
+ $webhook_config = $webhook->getConfig();
+
+ if (!$this->connectors->has($webhook_type)) {
+ throw new Exception(sprintf('Webhook connector "%s" does not exist; skipping.', $webhook_type));
+ }
+
+ $handler = new TestHandler(Logger::DEBUG, false);
+ $this->logger->pushHandler($handler);
+
+ /** @var Connector\ConnectorInterface $connector_obj */
+ $connector_obj = $this->connectors->get($webhook_type);
+
+ $np = $station->getNowplaying();
+
+ $event = new SendWebhooks($station, $np);
+ $connector_obj->dispatch($event, $webhook_config);
+
+ $this->logger->popHandler();
+
+ return $handler;
+ }
+
+ /**
+ * Directly access a webhook connector of the specified type.
+ *
+ * @param $type
+ * @return Connector\ConnectorInterface
+ */
+ public function getConnector($type): Connector\ConnectorInterface
+ {
+ if ($this->connectors->has($type)) {
+ return $this->connectors->get($type);
+ }
+
+ throw new \InvalidArgumentException('Invalid web hook connector type specified.');
+ }
+}
diff --git a/src/Middleware/EnableView.php b/src/Middleware/EnableView.php
index ece0874ea..6400a3972 100644
--- a/src/Middleware/EnableView.php
+++ b/src/Middleware/EnableView.php
@@ -28,7 +28,6 @@ class EnableView
{
$this->view->addData([
'request' => $request,
- 'user' => $request->getAttribute(Request::ATTRIBUTE_USER),
]);
$request = $request->withAttribute(Request::ATTRIBUTE_VIEW, $this->view);
diff --git a/src/Middleware/GetCurrentUser.php b/src/Middleware/GetCurrentUser.php
index 618869ce0..762b586d4 100644
--- a/src/Middleware/GetCurrentUser.php
+++ b/src/Middleware/GetCurrentUser.php
@@ -3,9 +3,11 @@ namespace App\Middleware;
use App\Auth;
use App\Customization;
+use App\Event\BuildView;
use App\Http\Request;
use App\Http\Response;
use App\Entity;
+use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Get the current user entity object and assign it into the request if it exists.
@@ -18,10 +20,14 @@ class GetCurrentUser
/** @var Customization */
protected $customization;
- public function __construct(Auth $auth, Customization $customization)
+ /** @var EventDispatcher */
+ protected $dispatcher;
+
+ public function __construct(Auth $auth, Customization $customization, EventDispatcher $dispatcher)
{
$this->auth = $auth;
$this->customization = $customization;
+ $this->dispatcher = $dispatcher;
}
/**
@@ -39,6 +45,12 @@ class GetCurrentUser
$this->customization->setUser($user);
$request = $this->customization->init($request);
+ $this->dispatcher->addListener(BuildView::NAME, function(BuildView $event) use ($user) {
+ $event->getView()->addData([
+ 'user' => $user,
+ ]);
+ });
+
$request = $request
->withAttribute(Request::ATTRIBUTE_USER, $user)
->withAttribute('is_logged_in', ($user instanceof Entity\User));
diff --git a/src/Plugins.php b/src/Plugins.php
new file mode 100644
index 000000000..33039f942
--- /dev/null
+++ b/src/Plugins.php
@@ -0,0 +1,91 @@
+loadDirectory($base_dir);
+ }
+
+ public function loadDirectory($dir): void
+ {
+ $plugins = (new Finder())
+ ->ignoreUnreadableDirs()
+ ->directories()
+ ->in($dir);
+
+ foreach($plugins as $plugin_dir) {
+ /** @var SplFileInfo $plugin_dir */
+ $plugin_prefix = $plugin_dir->getRelativePathname();
+ $plugin_namespace = 'Plugin\\'.\Doctrine\Common\Inflector\Inflector::classify($plugin_prefix).'\\';
+
+ $this->plugins[$plugin_prefix] = [
+ 'namespace' => $plugin_namespace,
+ 'path' => $plugin_dir->getPathname(),
+ ];
+ }
+ }
+
+ /**
+ * Add plugin namespace classes (and any Composer dependencies) to the global include list.
+ *
+ * @param ClassLoader $autoload
+ */
+ public function registerAutoloaders(ClassLoader $autoload): void
+ {
+ foreach($this->plugins as $plugin) {
+ $plugin_path = $plugin['path'];
+
+ if (file_exists($plugin_path.'/vendor/autoload.php')) {
+ require($plugin_path.'/vendor/autoload.php');
+ }
+
+ $autoload->addPsr4($plugin['namespace'], $plugin_path.'/src');
+ }
+ }
+
+ /**
+ * Register or override any services contained in the global Dependency Injection container.
+ *
+ * @param Container $di
+ * @param array $settings
+ */
+ public function registerServices(Container $di, array $settings): void
+ {
+ foreach($this->plugins as $plugin) {
+ $plugin_path = $plugin['path'];
+
+ if (file_exists($plugin_path . '/services.php')) {
+ call_user_func(include($plugin_path . '/services.php'), $di, $settings);
+ }
+ }
+ }
+
+ /**
+ * Register custom events that the plugin overrides with the Event Dispatcher.
+ *
+ * @param EventDispatcher $dispatcher
+ * @param Container $di
+ * @param array $settings
+ */
+ public function registerEvents(EventDispatcher $dispatcher, Container $di, array $settings): void
+ {
+ foreach($this->plugins as $plugin) {
+ $plugin_path = $plugin['path'];
+
+ if (file_exists($plugin_path . '/events.php')) {
+ call_user_func(include($plugin_path . '/events.php'), $dispatcher, $di, $settings);
+ }
+ }
+ }
+}
diff --git a/src/Provider/MiddlewareProvider.php b/src/Provider/MiddlewareProvider.php
index c19a386e8..e1c8cebbe 100644
--- a/src/Provider/MiddlewareProvider.php
+++ b/src/Provider/MiddlewareProvider.php
@@ -33,7 +33,8 @@ class MiddlewareProvider implements ServiceProviderInterface
$di[Middleware\GetCurrentUser::class] = function($di) {
return new Middleware\GetCurrentUser(
$di[App\Auth::class],
- $di[App\Customization::class]
+ $di[App\Customization::class],
+ $di[\Symfony\Component\EventDispatcher\EventDispatcher::class]
);
};
diff --git a/src/Provider/SyncProvider.php b/src/Provider/SyncProvider.php
index 61e94f884..420cb1f30 100644
--- a/src/Provider/SyncProvider.php
+++ b/src/Provider/SyncProvider.php
@@ -57,8 +57,8 @@ class SyncProvider implements ServiceProviderInterface
$di[\App\Radio\AutoDJ::class],
$di[\App\Cache::class],
$di[\InfluxDB\Database::class],
- $di[\App\Webhook\Dispatcher::class],
$di[\Doctrine\ORM\EntityManager::class],
+ $di[\Symfony\Component\EventDispatcher\EventDispatcher::class],
$di[\Monolog\Logger::class]
);
};
diff --git a/src/Sync/Task/NowPlaying.php b/src/Sync/Task/NowPlaying.php
index b36cec4fb..cca4f5761 100644
--- a/src/Sync/Task/NowPlaying.php
+++ b/src/Sync/Task/NowPlaying.php
@@ -2,15 +2,17 @@
namespace App\Sync\Task;
use App\Cache;
+use App\Event\GenerateRawNowPlaying;
+use App\Event\SendWebhooks;
use App\Radio\AutoDJ;
use App\ApiUtilities;
use App\Radio\Adapters;
-use App\Radio\Frontend\FrontendAbstract;
use App\Webhook\Dispatcher;
use Doctrine\ORM\EntityManager;
use InfluxDB\Database;
use App\Entity;
use Monolog\Logger;
+use Symfony\Component\EventDispatcher\EventDispatcher;
class NowPlaying extends TaskAbstract
{
@@ -32,8 +34,8 @@ class NowPlaying extends TaskAbstract
/** @var Logger */
protected $logger;
- /** @var Dispatcher */
- protected $webhook_dispatcher;
+ /** @var EventDispatcher */
+ protected $event_dispatcher;
/** @var ApiUtilities */
protected $api_utils;
@@ -54,14 +56,15 @@ class NowPlaying extends TaskAbstract
protected $analytics_level;
/**
- * @param EntityManager $em
- * @param Database $influx
- * @param Cache $cache
* @param Adapters $adapters
- * @param Dispatcher $webhook_dispatcher
* @param ApiUtilities $api_utils
* @param AutoDJ $autodj
+ * @param Cache $cache
+ * @param Database $influx
+ * @param EntityManager $em
+ * @param EventDispatcher $event_dispatcher
* @param Logger $logger
+ *
* @see \App\Provider\SyncProvider
*/
public function __construct(
@@ -70,8 +73,8 @@ class NowPlaying extends TaskAbstract
AutoDJ $autodj,
Cache $cache,
Database $influx,
- Dispatcher $webhook_dispatcher,
EntityManager $em,
+ EventDispatcher $event_dispatcher,
Logger $logger)
{
$this->adapters = $adapters;
@@ -79,9 +82,9 @@ class NowPlaying extends TaskAbstract
$this->autodj = $autodj;
$this->cache = $cache;
$this->em = $em;
+ $this->event_dispatcher = $event_dispatcher;
$this->influx = $influx;
$this->logger = $logger;
- $this->webhook_dispatcher = $webhook_dispatcher;
$this->history_repo = $this->em->getRepository(Entity\SongHistory::class);
$this->song_repo = $this->em->getRepository(Entity\Song::class);
@@ -198,27 +201,13 @@ class NowPlaying extends TaskAbstract
/** @var Entity\Api\NowPlaying|null $np_old */
$np_old = $station->getNowplaying();
+ // Build the new "raw" NowPlaying data.
+ $event = new GenerateRawNowPlaying($station, $frontend_adapter, $remote_adapters, $payload, $include_clients);
+ $this->event_dispatcher->dispatch(GenerateRawNowPlaying::NAME, $event);
+ $np_raw = $event->getRawResponse();
+
$np = new Entity\Api\NowPlaying;
$np->station = $station->api($frontend_adapter, $remote_adapters);
-
- // Build the new "raw" NowPlaying data from the adapters.
- if (APP_TESTING_MODE) {
- $np_raw = \NowPlaying\Adapter\AdapterAbstract::NOWPLAYING_EMPTY;
- } else {
- $np_raw = $frontend_adapter->getNowPlaying($payload, $include_clients);
-
- // Loop through all remotes and update NP data accordingly.
- foreach($remote_adapters as $remote_adapter) {
- $remote_adapter->updateNowPlaying($np_raw, $include_clients);
- }
-
- array_walk($np_raw['current_song'], function(&$value) {
- $value = htmlspecialchars_decode($value);
- $value = trim($value);
- });
- }
-
- // Start to convert the "raw" NowPlaying data into the proper API entities.
$np->listeners = new Entity\Api\NowPlayingListeners($np_raw['listeners']);
if (empty($np_raw['current_song']['text'])) {
@@ -295,8 +284,9 @@ class NowPlaying extends TaskAbstract
$this->em->persist($station);
$this->em->flush();
- $np_old = ($np_old instanceof Entity\Api\NowPlaying) ? $np_old : $np;
- $this->webhook_dispatcher->dispatch($station, $np_old, $np, ($payload !== null));
+ // Trigger the dispatching of webhooks.
+ $webhook_event = new SendWebhooks($station, $np, $np_old, ($payload !== null));
+ $this->event_dispatcher->dispatch(SendWebhooks::NAME, $webhook_event);
$this->logger->popProcessor();
diff --git a/src/Webhook/Connector/AbstractConnector.php b/src/Webhook/Connector/AbstractConnector.php
index 419881d24..33f850dd8 100644
--- a/src/Webhook/Connector/AbstractConnector.php
+++ b/src/Webhook/Connector/AbstractConnector.php
@@ -2,6 +2,7 @@
namespace App\Webhook\Connector;
use App\Entity;
+use App\Event\SendWebhooks;
use App\Utilities;
use GuzzleHttp\Client;
use Monolog\Logger;
@@ -20,14 +21,14 @@ abstract class AbstractConnector implements ConnectorInterface
$this->http_client = $http_client;
}
- public function shouldDispatch(array $current_events, array $triggers): bool
+ public function shouldDispatch(SendWebhooks $event, array $triggers): bool
{
if (empty($triggers)) {
return true;
}
foreach($triggers as $trigger) {
- if (in_array($trigger, $current_events)) {
+ if ($event->hasTrigger($trigger)) {
return true;
}
}
diff --git a/src/Webhook/Connector/ConnectorInterface.php b/src/Webhook/Connector/ConnectorInterface.php
index 18d1aad8f..b6adab5df 100644
--- a/src/Webhook/Connector/ConnectorInterface.php
+++ b/src/Webhook/Connector/ConnectorInterface.php
@@ -1,7 +1,7 @@
_getValidUrl($config['webhook_url'] ?? '');
@@ -84,7 +80,7 @@ class Discord extends AbstractConnector
'footer' => $config['footer'] ?? '',
];
- $vars = $this->_replaceVariables($raw_vars, $np);
+ $vars = $this->_replaceVariables($raw_vars, $event->getNowPlaying());
// Compose webhook
$embed = [
diff --git a/src/Webhook/Connector/Generic.php b/src/Webhook/Connector/Generic.php
index 4ccccf841..fe2ad78ef 100644
--- a/src/Webhook/Connector/Generic.php
+++ b/src/Webhook/Connector/Generic.php
@@ -2,17 +2,13 @@
namespace App\Webhook\Connector;
use App\Entity;
+use App\Event\SendWebhooks;
use GuzzleHttp\Exception\TransferException;
use Monolog\Logger;
class Generic extends AbstractConnector
{
- /**
- * @param Entity\Station $station
- * @param Entity\Api\NowPlaying $np
- * @param array $config
- */
- public function dispatch(Entity\Station $station, Entity\Api\NowPlaying $np, array $config): void
+ public function dispatch(SendWebhooks $event, array $config): void
{
$webhook_url = $this->_getValidUrl($config['webhook_url'] ?? '');
@@ -26,7 +22,7 @@ class Generic extends AbstractConnector
'headers' => [
'Content-Type' => 'application/json',
],
- 'json' => $np,
+ 'json' => $event->getNowPlaying(),
];
if (!empty($config['basic_auth_username']) && !empty($config['basic_auth_password'])) {
diff --git a/src/Webhook/Connector/Local.php b/src/Webhook/Connector/Local.php
index 59a0e96d1..54cf079de 100644
--- a/src/Webhook/Connector/Local.php
+++ b/src/Webhook/Connector/Local.php
@@ -3,6 +3,7 @@ namespace App\Webhook\Connector;
use App\Cache;
use App\Entity;
+use App\Event\SendWebhooks;
use GuzzleHttp\Client;
use InfluxDB\Database;
use Monolog\Logger;
@@ -27,33 +28,21 @@ class Local extends AbstractConnector
$this->settings_repo = $settings_repo;
}
- /**
- * @param array $current_events
- * @param array|null $triggers
- * @return bool
- */
- public function shouldDispatch(array $current_events, array $triggers): bool
+ public function shouldDispatch(SendWebhooks $event, array $triggers): bool
{
return true;
}
- /**
- * @param Entity\Station $station
- * @param Entity\Api\NowPlaying $np
- * @param array $config
- * @throws Database\Exception
- * @throws \InfluxDB\Exception
- */
- public function dispatch(Entity\Station $station, Entity\Api\NowPlaying $np, array $config): void
+ public function dispatch(SendWebhooks $event, array $config): void
{
$this->logger->debug('Writing entry to InfluxDB...');
// Post statistics to InfluxDB.
$influx_point = new \InfluxDB\Point(
- 'station.' . $station->getId() . '.listeners',
+ 'station.' . $event->getStation()->getId() . '.listeners',
(int)$np->listeners->current,
[],
- ['station' => $station->getId()],
+ ['station' => $event->getStation()->getId()],
time()
);
@@ -67,7 +56,7 @@ class Local extends AbstractConnector
if ($np_full) {
foreach($np_full as &$np_row) {
/** @var Entity\Api\NowPlaying $np_row */
- if ($np_row->station->id === $station->getId()) {
+ if ($np_row->station->id === $event->getStation()->getId()) {
$np_row = $np;
}
diff --git a/src/Webhook/Connector/Telegram.php b/src/Webhook/Connector/Telegram.php
index 5a534bf9f..eece895f8 100644
--- a/src/Webhook/Connector/Telegram.php
+++ b/src/Webhook/Connector/Telegram.php
@@ -1,7 +1,7 @@
_replaceVariables([
'text' => $config['text'],
- ], $np);
+ ], $event->getNowPlaying());
try {
$api_url = (!empty($config['api'])) ? rtrim($config['api'], '/') : 'https://api.telegram.org';
diff --git a/src/Webhook/Connector/TuneIn.php b/src/Webhook/Connector/TuneIn.php
index 088d8d166..a229c2b3b 100644
--- a/src/Webhook/Connector/TuneIn.php
+++ b/src/Webhook/Connector/TuneIn.php
@@ -2,27 +2,18 @@
namespace App\Webhook\Connector;
use App\Entity;
+use App\Event\SendWebhooks;
use GuzzleHttp\Exception\TransferException;
use Monolog\Logger;
class TuneIn extends AbstractConnector
{
- /**
- * @param array $current_events
- * @param array|null $triggers
- * @return bool
- */
- public function shouldDispatch(array $current_events, array $triggers): bool
+ public function shouldDispatch(SendWebhooks $event, array $triggers): bool
{
- return in_array('song_changed', $current_events);
+ return $event->hasTrigger('song_changed');
}
- /**
- * @param Entity\Station $station
- * @param Entity\Api\NowPlaying $np
- * @param array $config
- */
- public function dispatch(Entity\Station $station, Entity\Api\NowPlaying $np, array $config): void
+ public function dispatch(SendWebhooks $event, array $config): void
{
if (empty($config['partner_id']) || empty($config['partner_key']) || empty($config['station_id'])) {
$this->logger->error('Webhook '.$this->_getName().' is missing necessary configuration. Skipping...');
@@ -32,6 +23,8 @@ class TuneIn extends AbstractConnector
$this->logger->debug('Dispatching TuneIn AIR API call...');
try {
+ $np = $event->getNowPlaying();
+
$response = $this->http_client->get('http://air.radiotime.com/Playing.ashx', [
'query' => [
'partnerId' => $config['partner_id'],
diff --git a/src/Webhook/Connector/Twitter.php b/src/Webhook/Connector/Twitter.php
index af3ac3006..b1adae94d 100644
--- a/src/Webhook/Connector/Twitter.php
+++ b/src/Webhook/Connector/Twitter.php
@@ -2,18 +2,14 @@
namespace App\Webhook\Connector;
use App\Entity;
+use App\Event\SendWebhooks;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\HandlerStack;
use Monolog\Logger;
class Twitter extends AbstractConnector
{
- /**
- * @param Entity\Station $station
- * @param Entity\Api\NowPlaying $np
- * @param array $config
- */
- public function dispatch(Entity\Station $station, Entity\Api\NowPlaying $np, array $config): void
+ public function dispatch(SendWebhooks $event, array $config): void
{
if (empty($config['consumer_key'])
|| empty($config['consumer_secret'])
@@ -40,7 +36,7 @@ class Twitter extends AbstractConnector
'message' => $config['message'] ?? '',
];
- $vars = $this->_replaceVariables($raw_vars, $np);
+ $vars = $this->_replaceVariables($raw_vars, $event->getNowPlaying());
// Dispatch webhook
$this->logger->debug('Posting to Twitter...');
diff --git a/src/Webhook/Dispatcher.php b/src/Webhook/Dispatcher.php
index df51d6831..7f8a55c3b 100644
--- a/src/Webhook/Dispatcher.php
+++ b/src/Webhook/Dispatcher.php
@@ -3,6 +3,7 @@ namespace App\Webhook;
use App\Config;
use App\Entity;
+use App\Event\SendWebhooks;
use App\Exception;
use App\Provider\WebhookProvider;
use Monolog\Handler\TestHandler;
@@ -31,12 +32,9 @@ class Dispatcher
/**
* Determine which webhooks to dispatch for a given change in Now Playing data, and dispatch them.
*
- * @param Entity\Station $station
- * @param Entity\Api\NowPlaying $np_old
- * @param Entity\Api\NowPlaying $np_new
- * @param boolean $is_standalone
+ * @param SendWebhooks $event
*/
- public function dispatch(Entity\Station $station, Entity\Api\NowPlaying $np_old, Entity\Api\NowPlaying $np_new, $is_standalone = true): void
+ public function dispatch(SendWebhooks $event): void
{
if (APP_TESTING_MODE) {
$this->logger->info('In testing mode; no webhooks dispatched.');
@@ -46,7 +44,7 @@ class Dispatcher
// Compile list of connectors for the station. Always dispatch to the local websocket receiver.
$connectors = [];
- if ($is_standalone) {
+ if ($event->isStandalone()) {
$connectors[] = [
'type' => 'local',
'triggers' => [],
@@ -55,7 +53,7 @@ class Dispatcher
}
// Assemble list of webhooks for the station
- $station_webhooks = $station->getWebhooks();
+ $station_webhooks = $event->getStation()->getWebhooks();
if ($station_webhooks->count() > 0) {
foreach($station_webhooks as $webhook) {
@@ -70,26 +68,7 @@ class Dispatcher
}
}
- // Determine which events should be triggered as a result of this change.
- $to_trigger = ['all'];
-
- if ($np_old->now_playing->song->id !== $np_new->now_playing->song->id) {
- $to_trigger[] = 'song_changed';
- }
-
- if ($np_old->listeners->current > $np_new->listeners->current) {
- $to_trigger[] = 'listener_lost';
- } elseif ($np_old->listeners->current < $np_new->listeners->current) {
- $to_trigger[] = 'listener_gained';
- }
-
- if ($np_old->live->is_live === false && $np_new->live->is_live === true) {
- $to_trigger[] = 'live_connect';
- } elseif ($np_old->live->is_live === true && $np_new->live->is_live === false) {
- $to_trigger[] = 'live_disconnect';
- }
-
- $this->logger->debug('Triggering events: '.implode(', ', $to_trigger));
+ $this->logger->debug('Triggering events: '.implode(', ', $event->getTriggers()));
// Trigger all appropriate webhooks.
foreach($connectors as $connector) {
@@ -101,10 +80,10 @@ class Dispatcher
/** @var Connector\ConnectorInterface $connector_obj */
$connector_obj = $this->connectors->get($connector['type']);
- if ($connector_obj->shouldDispatch($to_trigger, (array)$connector['triggers'])) {
+ if ($connector_obj->shouldDispatch($event, (array)$connector['triggers'])) {
$this->logger->debug(sprintf('Dispatching connector "%s".', $connector['type']));
- $connector_obj->dispatch($station, $np_new, (array)$connector['config']);
+ $connector_obj->dispatch($event, (array)$connector['config']);
}
}
}
@@ -135,7 +114,8 @@ class Dispatcher
$np = $station->getNowplaying();
- $connector_obj->dispatch($station, $np, $webhook_config);
+ $event = new SendWebhooks($station, $np);
+ $connector_obj->dispatch($event, $webhook_config);
$this->logger->popHandler();