Make authentication more reliable for list syncing
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good Details

This commit is contained in:
Timothy Warren 2020-05-01 17:08:20 -04:00
parent 7373cf93b7
commit ee18d407a2
8 changed files with 216 additions and 120 deletions

View File

@ -21,7 +21,6 @@ use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp;
use Amp\Http\Client\Request;
use Amp\Http\Client\Body\FormBody;
use Aviat\Ion\Json;

View File

@ -54,7 +54,7 @@ class AnimeListTransformer extends AbstractTransformer {
'reconsuming' => $reconsuming,
'status' => $reconsuming
? KitsuStatus::WATCHING
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
: AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt'])
->format(DateTime::W3C)

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Kitsu as KitsuStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaListItem;
use Aviat\AnimeClient\Types\FormItem;
@ -40,6 +41,8 @@ class MangaListTransformer extends AbstractTransformer {
*/
public function untransform(array $item): FormItem
{
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
return FormItem::from([
'id' => $item['id'],
'mal_id' => $item['media']['idMal'],
@ -49,8 +52,10 @@ class MangaListTransformer extends AbstractTransformer {
'progress' => $item['progress'],
'rating' => $item['score'],
'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
'reconsuming' => $reconsuming,
'status' => $reconsuming
? KitsuStatus::READING
: MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt'])
->format(DateTime::W3C),

View File

@ -29,6 +29,7 @@ use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Throwable;
use const PHP_SAPI;
/**
* Kitsu API Authentication
@ -83,35 +84,67 @@ final class Auth {
$auth = $this->model->authenticate($username, $password);
if (FALSE !== $auth)
{
// Set the token in the cache for command line operations
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
$cacheItem->set($auth['access_token']);
$cacheItem->save();
// Set the token expiration in the cache
$expireTime = $auth['created_at'] + $auth['expires_in'];
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
$cacheItem->set($expireTime);
$cacheItem->save();
// Set the refresh token in the cache
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY);
$cacheItem->set($auth['refresh_token']);
$cacheItem->save();
// Set the session values
$this->segment->set('auth_token', $auth['access_token']);
$this->segment->set('auth_token_expires', $expireTime);
$this->segment->set('refresh_token', $auth['refresh_token']);
return TRUE;
}
return FALSE;
return $this->storeAuth($auth);
}
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function isAuthenticated(): bool
{
return ($this->getAuthToken() !== NULL);
}
/**
* Clear authentication values
*
* @return void
*/
public function logout(): void
{
$this->segment->clear();
}
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
private function getAuthToken(): ?string
{
$now = time();
if (PHP_SAPI === 'cli')
{
$token = $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL);
$refreshToken = $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL);
$expireTime = $this->cacheGet(K::AUTH_TOKEN_EXP_CACHE_KEY);
$isExpired = $now > $expireTime;
}
else
{
$token = $this->segment->get('auth_token', NULL);
$refreshToken = $this->segment->get('refresh_token', NULL);
$isExpired = $now > $this->segment->get('auth_token_expires', $now + 5000);
}
// Attempt to re-authenticate with refresh token
/* if ($isExpired === TRUE && $refreshToken !== NULL)
{
if ($this->reAuthenticate($refreshToken) !== NULL)
{
return (PHP_SAPI === 'cli')
? $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL)
: $this->segment->get('auth_token', NULL);
}
return NULL;
}*/
return $token;
}
/**
* Make the call to re-authenticate with the existing refresh token
@ -121,10 +154,15 @@ final class Auth {
* @throws InvalidArgumentException
* @throws Throwable
*/
public function reAuthenticate(string $token): bool
private function reAuthenticate(string $token): bool
{
$auth = $this->model->reAuthenticate($token);
return $this->storeAuth($auth);
}
private function storeAuth($auth): bool
{
if (FALSE !== $auth)
{
// Set the token in the cache for command line operations
@ -153,52 +191,15 @@ final class Auth {
return FALSE;
}
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function isAuthenticated(): bool
private function cacheGet(string $key, $default = NULL)
{
return ($this->get_auth_token() !== FALSE);
}
/**
* Clear authentication values
*
* @return void
*/
public function logout(): void
{
$this->segment->clear();
}
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
public function get_auth_token()
{
$now = time();
$token = $this->segment->get('auth_token', FALSE);
$refreshToken = $this->segment->get('refresh_token', FALSE);
$isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000);
// Attempt to re-authenticate with refresh token
/* if ($isExpired && $refreshToken)
$cacheItem = $this->cache->getItem($key);
if ( ! $cacheItem->isHit())
{
if ($this->reAuthenticate($refreshToken))
{
return $this->segment->get('auth_token', FALSE);
}
return $default;
}
return FALSE;
} */
return $token;
return $cacheItem->get();
}
}
// End of KitsuAuth.php

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu;
use const PHP_SAPI;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait;
@ -69,11 +70,14 @@ trait KitsuTrait {
->getSegment(SESSION_SEGMENT);
$cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token');
$cacheItem = $cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
$token = null;
if ($sessionSegment->get('auth_token') !== NULL && $url !== K::AUTH_URL)
if (PHP_SAPI === 'cli' && $cacheItem->isHit())
{
$token = $cacheItem->get();
}
else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{
$token = $sessionSegment->get('auth_token');
if ( ! $cacheItem->isHit())
@ -82,12 +86,8 @@ trait KitsuTrait {
$cacheItem->save();
}
}
else if ($sessionSegment->get('auth_token') === NULL && $cacheItem->isHit())
{
$token = $cacheItem->get();
}
if (NULL !== $token)
if ($token !== NULL)
{
$request = $request->setAuth('bearer', $token);
}

View File

@ -26,8 +26,8 @@ use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
use Aviat\Banker\Pool;
use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware};
use ConsoleKit\{Command, ConsoleException};
use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware};
use ConsoleKit\{Colors, Command, ConsoleException};
use ConsoleKit\Widgets\Box;
use Laminas\Diactoros\{Response, ServerRequestFactory};
use Monolog\Handler\RotatingFileHandler;
@ -44,15 +44,30 @@ abstract class BaseCommand extends Command {
* Echo text in a box
*
* @param string $message
* @param string|int|null $fgColor
* @param string|int|null $bgColor
* @return void
*/
protected function echoBox($message): void
public function echoBox(string $message, $fgColor = NULL, $bgColor = NULL): void
{
try
{
echo "\n";
$len = strlen($message);
// color message
$message = Colors::colorize($message, $fgColor, $bgColor);
$colorLen = strlen($message);
// create the box
$box = new Box($this->getConsole(), $message);
if ($len !== $colorLen)
{
$box->setPadding((($colorLen - $len) / 2) + 2);
}
$box->write();
echo "\n";
}
catch (ConsoleException $e)
@ -61,12 +76,42 @@ abstract class BaseCommand extends Command {
}
}
public function echo(string $message): void
{
$this->_line($message);
}
public function echoSuccess(string $message): void
{
$this->_line($message, Colors::GREEN | Colors::BOLD, Colors::BLACK);
}
public function echoWarning(string $message): void
{
$this->_line($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
}
public function echoWarningBox(string $message): void
{
$this->echoBox($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
}
public function echoError(string $message): void
{
$this->_line($message, Colors::RED | Colors::BOLD, Colors::BLACK);
}
public function echoErrorBox(string $message): void
{
$this->echoBox($message, Colors::RED | Colors::BOLD, Colors::BLACK);
}
/**
* Setup the Di container
*
* @return Container
* @return Containerinterface
*/
protected function setupContainer(): Container
public function setupContainer(): ContainerInterface
{
$APP_DIR = realpath(__DIR__ . '/../../../app');
$APPCONF_DIR = realpath("{$APP_DIR}/appConf/");
@ -82,7 +127,7 @@ abstract class BaseCommand extends Command {
$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig);
$di = static function ($configArray) use ($APP_DIR): Container {
$di = static function (array $configArray) use ($APP_DIR): Container {
$container = new Container();
// -------------------------------------------------------------------------
@ -103,9 +148,7 @@ abstract class BaseCommand extends Command {
$container->setLogger($kitsu_request_logger, 'kitsu-request');
// Create Config Object
$container->set('config', static function() use ($configArray): Config {
return new Config($configArray);
});
$container->set('config', fn () => new Config($configArray));
// Create Cache Object
$container->set('cache', static function($container) {
@ -115,28 +158,20 @@ abstract class BaseCommand extends Command {
});
// Create Aura Router Object
$container->set('aura-router', static function() {
return new RouterContainer;
});
$container->set('aura-router', fn () => new RouterContainer);
// Create Request/Response Objects
$container->set('request', static function() {
return ServerRequestFactory::fromGlobals(
$_SERVER,
$_GET,
$_POST,
$_COOKIE,
$_FILES
);
});
$container->set('response', static function(): Response {
return new Response;
});
$container->set('request', fn () => ServerRequestFactory::fromGlobals(
$_SERVER,
$_GET,
$_POST,
$_COOKIE,
$_FILES
));
$container->set('response', fn () => new Response);
// Create session Object
$container->set('session', static function() {
return (new SessionFactory())->newInstance($_COOKIE);
});
$container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
// Models
$container->set('kitsu-model', static function($container): Kitsu\Model {
@ -175,21 +210,21 @@ abstract class BaseCommand extends Command {
return $model;
});
$container->set('auth', static function($container): Kitsu\Auth {
return new Kitsu\Auth($container);
});
$container->set('auth', fn ($container) => new Kitsu\Auth($container));
$container->set('url-generator', static function($container): UrlGenerator {
return new UrlGenerator($container);
});
$container->set('url-generator', fn ($container) => new UrlGenerator($container));
$container->set('util', static function($container): Util {
return new Util($container);
});
$container->set('util', fn ($container) => new Util($container));
return $container;
};
return $di($configArray);
}
private function _line(string $message, $fgColor = NULL, $bgColor = NULL): void
{
$message = Colors::colorize($message, $fgColor, $bgColor);
$this->getConsole()->writeln($message);
}
}

View File

@ -0,0 +1,28 @@
<?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;
/**
* Types of lists
*/
final class ListType extends BaseEnum {
public const ANIME = 'anime';
public const DRAMA = 'drama';
public const MANGA = 'manga';
}

View File

@ -0,0 +1,28 @@
<?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;
/**
* Types of actions when syncing lists from different APIs
*/
final class SyncAction extends BaseEnum {
public const CREATE = 'create';
public const UPDATE = 'update';
public const DELETE = 'delete';
}