Set up Event-based handling for a few things
All checks were successful
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2020-05-06 13:16:40 -04:00
parent dc20d8de7c
commit 05c50387f6
8 changed files with 183 additions and 46 deletions

View File

@ -27,6 +27,7 @@ use Aviat\AnimeClient\API\{
}; };
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Event;
use Throwable; use Throwable;
use const PHP_SAPI; use const PHP_SAPI;
@ -66,6 +67,8 @@ final class Auth {
$this->segment = $container->get('session') $this->segment = $container->get('session')
->getSegment(SESSION_SEGMENT); ->getSegment(SESSION_SEGMENT);
$this->model = $container->get('kitsu-model'); $this->model = $container->get('kitsu-model');
Event::on('::unauthorized::', [$this, 'reAuthenticate']);
} }
/** /**
@ -87,6 +90,28 @@ final class Auth {
return $this->storeAuth($auth); return $this->storeAuth($auth);
} }
/**
* Make the call to re-authenticate with the existing refresh token
*
* @param string $refreshToken
* @return boolean
* @throws InvalidArgumentException
* @throws Throwable
*/
public function reAuthenticate(?string $refreshToken): bool
{
$refreshToken ??= $this->getAuthToken();
if (empty($refreshToken))
{
return FALSE;
}
$auth = $this->model->reAuthenticate($refreshToken);
return $this->storeAuth($auth);
}
/** /**
* Check whether the current user is authenticated * Check whether the current user is authenticated
* *
@ -110,7 +135,7 @@ final class Auth {
/** /**
* Retrieve the authentication token from the session * Retrieve the authentication token from the session
* *
* @return string|false * @return string
*/ */
private function getAuthToken(): ?string private function getAuthToken(): ?string
{ {
@ -146,22 +171,14 @@ final class Auth {
return $token; return $token;
} }
/** private function getRefreshToken(): ?string
* Make the call to re-authenticate with the existing refresh token
*
* @param string $token
* @return boolean
* @throws InvalidArgumentException
* @throws Throwable
*/
private function reAuthenticate(string $token): bool
{ {
$auth = $this->model->reAuthenticate($token); return (PHP_SAPI === 'cli')
? $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL)
return $this->storeAuth($auth); : $this->segment->get('refresh_token');
} }
private function storeAuth($auth): bool private function storeAuth(bool $auth): bool
{ {
if (FALSE !== $auth) if (FALSE !== $auth)
{ {

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Enum\EventType;
use function in_array;
use const PHP_SAPI; use const PHP_SAPI;
use const Aviat\AnimeClient\SESSION_SEGMENT; use const Aviat\AnimeClient\SESSION_SEGMENT;
@ -24,10 +26,8 @@ use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Amp\Http\Client\Response; use Amp\Http\Client\Response;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K};
FailedResponseException, use Aviat\Ion\Event;
Kitsu as K
};
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aviat\Ion\JsonException; use Aviat\Ion\JsonException;
@ -80,7 +80,7 @@ trait KitsuTrait {
else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{ {
$token = $sessionSegment->get('auth_token'); $token = $sessionSegment->get('auth_token');
if ( ! $cacheItem->isHit()) if ( ! (empty($token) || $cacheItem->isHit()))
{ {
$cacheItem->set($token); $cacheItem->set($token);
$cacheItem->save(); $cacheItem->save();
@ -168,12 +168,20 @@ trait KitsuTrait {
} }
$response = $this->getResponse($type, $url, $options); $response = $this->getResponse($type, $url, $options);
$statusCode = $response->getStatus();
if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200) // Check for requests that are unauthorized
if ($statusCode === 401 || $statusCode === 403)
{
Event::emit(EventType::UNAUTHORIZED);
}
// Any other type of failed request
if ($statusCode > 299 || $statusCode < 200)
{ {
if ($logger) if ($logger)
{ {
$logger->warning('Non 200 response for api call', (array)$response); $logger->warning('Non 2xx response for api call', (array)$response);
} }
throw new FailedResponseException('Failed to get the proper response from the API'); throw new FailedResponseException('Failed to get the proper response from the API');
@ -188,7 +196,6 @@ trait KitsuTrait {
print_r($e); print_r($e);
die(); die();
} }
} }
/** /**
@ -233,13 +240,10 @@ trait KitsuTrait {
$response = $this->getResponse('POST', ...$args); $response = $this->getResponse('POST', ...$args);
$validResponseCodes = [200, 201]; $validResponseCodes = [200, 201];
if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE)) if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger)
{
if ($logger)
{ {
$logger->warning('Non 201 response for POST api call', $response->getBody()); $logger->warning('Non 201 response for POST api call', $response->getBody());
} }
}
return JSON::decode(wait($response->getBody()->buffer()), TRUE); return JSON::decode(wait($response->getBody()->buffer()), TRUE);
} }
@ -254,6 +258,6 @@ trait KitsuTrait {
protected function deleteRequest(...$args): bool protected function deleteRequest(...$args): bool
{ {
$response = $this->getResponse('DELETE', ...$args); $response = $this->getResponse('DELETE', ...$args);
return ((int) $response->getStatus() === 204); return ($response->getStatus() === 204);
} }
} }

View File

@ -582,7 +582,7 @@ final class Model {
{ {
$defaultOptions = [ $defaultOptions = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => 'anime' 'kind' => 'anime'
], ],
'page' => [ 'page' => [
@ -608,7 +608,7 @@ final class Model {
{ {
$options = [ $options = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => 'anime', 'kind' => 'anime',
'status' => $status, 'status' => $status,
], ],
@ -683,7 +683,7 @@ final class Model {
$options = [ $options = [
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => 'manga', 'kind' => 'manga',
'status' => $status, 'status' => $status,
], ],
@ -811,7 +811,7 @@ final class Model {
{ {
$defaultOptions = [ $defaultOptions = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => 'manga' 'kind' => 'manga'
], ],
'page' => [ 'page' => [
@ -866,7 +866,7 @@ final class Model {
*/ */
public function createListItem(array $data): ?Request public function createListItem(array $data): ?Request
{ {
$data['user_id'] = $this->getUserIdByUsername($this->getUsername()); $data['user_id'] = $this->getUserId();
if ($data['id'] === NULL) if ($data['id'] === NULL)
{ {
return NULL; return NULL;
@ -941,7 +941,7 @@ final class Model {
{ {
$options = [ $options = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => $type, 'kind' => $type,
], ],
'include' => "{$type},{$type}.mappings", 'include' => "{$type},{$type}.mappings",
@ -1001,7 +1001,7 @@ final class Model {
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'kind' => 'progressed,updated', 'kind' => 'progressed,updated',
'userId' => $this->getUserIdByUsername($this->getUsername()), 'userId' => $this->getUserId(),
], ],
'page' => [ 'page' => [
'offset' => $offset, 'offset' => $offset,
@ -1018,6 +1018,18 @@ final class Model {
]); ]);
} }
private function getUserId(): string
{
static $userId = NULL;
if ($userId === NULL)
{
$userId = $this->getUserIdByUsername($this->getUsername());
}
return $userId;
}
/** /**
* Get the kitsu username from config * Get the kitsu username from config
* *
@ -1105,7 +1117,7 @@ final class Model {
$options = [ $options = [
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername(), 'user_id' => $this->getUserId(),
'kind' => $type, 'kind' => $type,
], ],
'page' => [ 'page' => [
@ -1175,7 +1187,7 @@ final class Model {
{ {
$defaultOptions = [ $defaultOptions = [
'filter' => [ 'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()), 'user_id' => $this->getUserId(),
'kind' => $type, 'kind' => $type,
], ],
'page' => [ 'page' => [

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Enum\EventType;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
use Aura\Router\Generator; use Aura\Router\Generator;
@ -32,6 +33,7 @@ use Aviat\Ion\Di\{
Exception\ContainerException, Exception\ContainerException,
Exception\NotFoundException Exception\NotFoundException
}; };
use Aviat\Ion\Event;
use Aviat\Ion\Exception\DoubleRenderException; use Aviat\Ion\Exception\DoubleRenderException;
use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
use InvalidArgumentException; use InvalidArgumentException;
@ -131,6 +133,9 @@ class Controller {
'url_type' => 'anime', 'url_type' => 'anime',
'urlGenerator' => $urlGenerator, 'urlGenerator' => $urlGenerator,
]; ];
Event::on(EventType::CLEAR_CACHE, fn () => $this->emptyCache());
Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->removeCacheItem($key));
} }
/** /**
@ -430,5 +435,15 @@ class Controller {
(new HttpView($this->container))->redirect($url, $code); (new HttpView($this->container))->redirect($url, $code);
exit(); exit();
} }
private function emptyCache(): void
{
$this->cache->emptyCache();
}
private function removeCacheItem(string $key): void
{
$this->cache->deleteItem($key);
}
} }
// End of BaseController.php // End of BaseController.php

View File

@ -17,6 +17,8 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Event;
use Aviat\Ion\View\HtmlView; use Aviat\Ion\View\HtmlView;
/** /**
@ -30,7 +32,10 @@ final class Misc extends BaseController {
*/ */
public function clearCache(): void public function clearCache(): void
{ {
$this->cache->clear(); $this->checkAuth();
Event::emit(EventType::CLEAR_CACHE);
$this->outputHTML('blank', [ $this->outputHTML('blank', [
'title' => 'Cache cleared' 'title' => 'Cache cleared'
]); ]);
@ -89,8 +94,6 @@ final class Misc extends BaseController {
*/ */
public function logout(): void public function logout(): void
{ {
$this->checkAuth();
$auth = $this->container->get('auth'); $auth = $this->container->get('auth');
$auth->logout(); $auth->logout();

View File

@ -16,12 +16,14 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Enum\EventType;
use function Aviat\Ion\_dir; use function Aviat\Ion\_dir;
use Aura\Router\{Map, Matcher, Route, Rule}; use Aura\Router\{Map, Matcher, Route, Rule};
use Aviat\AnimeClient\API\FailedResponseException; use Aviat\AnimeClient\API\FailedResponseException;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Event;
use Aviat\Ion\Friend; use Aviat\Ion\Friend;
use Aviat\Ion\Type\StringType; use Aviat\Ion\Type\StringType;
use LogicException; use LogicException;
@ -161,10 +163,7 @@ final class Dispatcher extends RoutingBase {
throw new LogicException('Missing controller'); throw new LogicException('Missing controller');
} }
if (array_key_exists('controller', $route->attributes))
{
$controllerName = $route->attributes['controller']; $controllerName = $route->attributes['controller'];
}
// Get the full namespace for a controller if a short name is given // Get the full namespace for a controller if a short name is given
if (strpos($controllerName, '\\') === FALSE) if (strpos($controllerName, '\\') === FALSE)
@ -283,7 +282,7 @@ final class Dispatcher extends RoutingBase {
$logger->debug('Dispatcher - controller arguments', $params); $logger->debug('Dispatcher - controller arguments', $params);
} }
\call_user_func_array([$controller, $method], $params); call_user_func_array([$controller, $method], $params);
} }
catch (FailedResponseException $e) catch (FailedResponseException $e)
{ {
@ -293,7 +292,14 @@ final class Dispatcher extends RoutingBase {
'API request timed out', 'API request timed out',
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻'); 'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻');
} }
finally
{
// Log out on session/api token expiration
Event::on(EventType::UNAUTHORIZED, static function () {
$controllerName = DEFAULT_CONTROLLER;
(new $controllerName($this->container))->logout();
});
}
} }
/** /**

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Enum;
use Aviat\Ion\Enum as BaseEnum;
final class EventType extends BaseEnum {
public const CLEAR_CACHE = '::clear-cache::';
public const RESET_CACHE_KEY = '::reset-cache-key::';
public const UNAUTHORIZED = '::unauthorized::';
}

55
src/Ion/Event.php Normal file
View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\Ion;
/**
* A basic event handler
*/
class Event {
private static array $eventMap = [];
/**
* Subscribe to an event
*
* @param string $eventName
* @param callable $handler
*/
public static function on(string $eventName, callable $handler): void
{
if ( ! array_key_exists($eventName, static::$eventMap))
{
static::$eventMap[$eventName] = [];
}
static::$eventMap[$eventName][] = $handler;
}
/**
* Fire off an event
*
* @param string $eventName
* @param array $args
*/
public static function emit(string $eventName, array $args = []): void
{
// Call each subscriber with the provided arguments
if (array_key_exists($eventName, static::$eventMap))
{
array_walk(static::$eventMap[$eventName], fn ($fn) => $fn(...$args));
}
}
}