feat: added MAL option to external profiles (#760)

This commit is contained in:
Kyrch
2024-11-18 15:23:51 -03:00
committed by GitHub
parent ed6662c11f
commit 3ff694fba2
12 changed files with 277 additions and 47 deletions
@@ -15,7 +15,7 @@ use Illuminate\Support\Facades\Crypt;
abstract class BaseExternalEntryTokenAction
{
protected ?array $response = null;
protected ?int $id = null;
protected ?int $userId = null;
/**
* Create a new action instance.
@@ -29,9 +29,9 @@ abstract class BaseExternalEntryTokenAction
*
* @return int|null
*/
public function getId(): ?int
public function getUserId(): ?int
{
return $this->id;
return $this->userId;
}
/**
@@ -69,19 +69,19 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
*
* @return int|null
*/
public function getId(): ?int
public function getUserId(): ?int
{
if ($this->id !== null) {
return $this->id;
if ($this->userId !== null) {
return $this->userId;
}
[, $payload] = explode('.', $this->getToken());
$decodedArray = json_decode(base64_decode($payload), true);
$this->id = intval(Arr::get($decodedArray, 'sub'));
$this->userId = intval(Arr::get($decodedArray, 'sub'));
return $this->id;
return $this->userId;
}
/**
@@ -117,7 +117,7 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
';
$variables = [
'userId' => $this->getId(),
'userId' => $this->getUserId(),
];
$this->response = Http::withToken($this->getToken())
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile\ExternalEntry\Token;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryTokenAction;
use App\Enums\Models\List\ExternalEntryWatchStatus;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\ExternalResource;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Class MalExternalEntryTokenAction.
*/
class MalExternalEntryTokenAction extends BaseExternalEntryTokenAction
{
protected ?array $userResponse = null;
/**
* Get the entries of the response.
*
* @return array
*/
public function getEntries(): array
{
$entries = [];
if ($this->response === null) {
$this->makeRequest();
}
if ($response = $this->response) {
foreach (Arr::get($response, 'data') as $data) {
$animeInfo = Arr::get($data, 'node');
$listStatus = Arr::get($data, 'list_status');
$watchStatus = Arr::get($listStatus, 'is_rewatching')
? 'rewatching'
: Arr::get($listStatus, 'status');
$entries[] = [
ExternalResource::ATTRIBUTE_EXTERNAL_ID => Arr::get($animeInfo, 'id'),
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($listStatus, 'score'),
ExternalEntry::ATTRIBUTE_WATCH_STATUS => ExternalEntryWatchStatus::getMalMapping($watchStatus)->value,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => false,
];
}
}
return $entries;
}
/**
* Get the username.
*
* @return string|null
*/
public function getUsername(): ?string
{
if ($this->userResponse === null) {
$this->makeUserRequest();
}
return Arr::get($this->userResponse, 'name');
}
/**
* Get the id of the external user.
*
* @return int|null
*/
public function getUserId(): ?int
{
if ($this->response === null) {
$this->makeUserRequest();
}
return Arr::get($this->userResponse, 'id');
}
/**
* Make the request to the user endpoint of the external api.
*
* @return static
*/
protected function makeUserRequest(): static
{
try {
$this->userResponse = Http::withToken($this->getToken())
->get('https://api.myanimelist.net/v2/users/@me')
->throw()
->json();
return $this;
} catch (RequestException $e) {
Log::error($e->getMessage());
throw $e;
}
}
/**
* Make the request to the external api.
*
* @return static
*/
protected function makeRequest(): static
{
try {
$this->response = Http::withToken($this->getToken())
->get('https://api.myanimelist.net/v2/users/@me/animelist', [
'fields' => 'list_status',
'limit' => '1000',
])
->throw()
->json();
var_dump($this->response);
return $this;
} catch (RequestException $e) {
Log::error($e->getMessage());
throw $e;
}
}
}
@@ -21,8 +21,8 @@ abstract class BaseExternalTokenAction
/**
* Use the authorization code to get the tokens and store them.
*
* @param string $code
* @param array $parameters
* @return ExternalToken|null
*/
abstract public function store(string $code): ?ExternalToken;
abstract public function store(array $parameters): ?ExternalToken;
}
@@ -22,13 +22,15 @@ class AnilistExternalTokenAction extends BaseExternalTokenAction
/**
* Use the authorization code to get the tokens and store them.
*
* @param string $code
* @param array $parameters
* @return ExternalToken
*
* @throws Exception
*/
public function store(string $code): ExternalToken
public function store(array $parameters): ExternalToken
{
$code = Arr::get($parameters, 'code');
try {
$response = Http::acceptJson()
->asForm()
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile\ExternalToken\Site;
use App\Actions\Models\List\ExternalProfile\ExternalToken\BaseExternalTokenAction;
use App\Models\List\External\ExternalToken;
use Error;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Class MalExternalTokenAction.
*/
class MalExternalTokenAction extends BaseExternalTokenAction
{
/**
* Use the authorization code to get the tokens and store them.
*
* @param array $parameters
* @return ExternalToken
*
* @throws Exception
*/
public function store(array $parameters): ExternalToken
{
$code = Arr::get($parameters, 'code');
$state = Arr::get($parameters, 'state');
$codeVerifier = Cache::get("mal-external-token-request-{$state}");
Cache::forget("mal-external-token-request-{$state}");
try {
$response = Http::asForm()
->post('https://myanimelist.net/v1/oauth2/token', [
'grant_type' => 'authorization_code',
'client_id' => Config::get('services.mal.client_id'),
'client_secret' => Config::get('services.mal.client_secret'),
'redirect_uri' => Config::get('services.mal.redirect_uri'),
'code' => $code,
'code_verifier' => $codeVerifier,
])
->throw()
->json();
$token = Arr::get($response, 'access_token');
$refreshToken = Arr::get($response, 'refresh_token');
if ($token === null) {
throw new Error('Failed to get token');
}
return ExternalToken::query()->create([
ExternalToken::ATTRIBUTE_ACCESS_TOKEN => Crypt::encrypt($token),
ExternalToken::ATTRIBUTE_REFRESH_TOKEN => Crypt::encrypt($refreshToken),
]);
} catch (Exception $e) {
Log::error($e->getMessage());
throw $e;
}
}
}
@@ -7,6 +7,7 @@ namespace App\Actions\Models\List\ExternalProfile;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Token\AnilistExternalEntryTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Token\MalExternalEntryTokenAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\List\External\ExternalToken;
@@ -43,7 +44,7 @@ class StoreExternalProfileTokenAction
return null;
}
$userId = $action->getId();
$userId = $action->getUserId();
$profile = $this->findForUserIdOrCreate($userId, $site, $action, $parameters);
@@ -52,7 +53,7 @@ class StoreExternalProfileTokenAction
} catch (Exception $e) {
Log::error($e->getMessage());
return null;
throw $e;
}
}
@@ -118,6 +119,7 @@ class StoreExternalProfileTokenAction
{
return match ($site) {
ExternalProfileSite::ANILIST => new AnilistExternalEntryTokenAction($token),
ExternalProfileSite::MAL => new MalExternalEntryTokenAction($token),
default => null,
};
}
@@ -8,6 +8,7 @@ use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryActio
use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Username\AnilistExternalEntryAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Token\AnilistExternalEntryTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Token\MalExternalEntryTokenAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
@@ -63,7 +64,11 @@ class SyncExternalProfileAction
}
}
$profile->externalentries()->upsert($externalEntries, [ExternalEntry::ATTRIBUTE_ANIME, ExternalEntry::ATTRIBUTE_PROFILE]);
ExternalEntry::upsert($externalEntries, [ExternalEntry::ATTRIBUTE_ANIME, ExternalEntry::ATTRIBUTE_PROFILE]);
$profile->update([ExternalProfile::ATTRIBUTE_SYNCED_AT => now()]);
DB::commit();
return $profile;
} catch (Exception $e) {
@@ -85,6 +90,7 @@ class SyncExternalProfileAction
{
return match ($profile->site) {
ExternalProfileSite::ANILIST => new AnilistExternalEntryTokenAction($profile->externaltoken),
ExternalProfileSite::MAL => new MalExternalEntryTokenAction($profile->externaltoken),
default => null,
};
}
@@ -6,6 +6,7 @@ namespace App\Actions\Models\List;
use App\Actions\Models\List\ExternalProfile\ExternalToken\BaseExternalTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalToken\Site\AnilistExternalTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalToken\Site\MalExternalTokenAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileTokenAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Models\List\ExternalProfile;
@@ -42,7 +43,7 @@ class ExternalTokenCallbackAction
throw new Error("Action not found for site {$profileSite->localize()}", 404);
}
$externalToken = $action->store(Arr::get($parameters, 'code'));
$externalToken = $action->store($parameters);
if ($externalToken === null) {
throw new Error('Invalid Code', 400);
@@ -52,6 +53,8 @@ class ExternalTokenCallbackAction
$profile = $profileAction->findOrCreate($externalToken, $parameters);
$externalToken->externalprofile()->associate($profile);
DB::commit();
return $profile;
@@ -74,6 +77,7 @@ class ExternalTokenCallbackAction
{
return match ($site) {
ExternalProfileSite::ANILIST => new AnilistExternalTokenAction(),
ExternalProfileSite::MAL => new MalExternalTokenAction(),
default => null,
};
}
@@ -29,12 +29,12 @@ enum ExternalEntryWatchStatus: int
public static function getMalMapping(string $status): static
{
return match ($status) {
'rewatching' => static::REWATCHING,
'watching' => static::WATCHING,
'completed' => static::COMPLETED,
'on_hold' => static::PAUSED,
'dropped' => static::DROPPED,
default => static::PLAN_TO_WATCH,
'plan_to_watch' => static::PLAN_TO_WATCH,
default => static::REWATCHING,
};
}
@@ -6,6 +6,9 @@ namespace App\Enums\Models\List;
use App\Concerns\Enums\LocalizesName;
use App\Enums\Models\Wiki\ResourceSite;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
/**
* Enum ExternalProfileSite.
@@ -31,4 +34,43 @@ enum ExternalProfileSite: int
static::KITSU => ResourceSite::KITSU,
};
}
/**
* Get the link of the external site to authenticate the user.
*
* @return string|null
*/
public function getAuthorizeUrl(): ?string
{
if ($this === static::MAL) {
$codeVerifier = bin2hex(random_bytes(64));
$id = Str::uuid()->toString();
Cache::set("mal-external-token-request-{$id}", $codeVerifier);
$query = [
'client_id' => Config::get('services.mal.client_id'),
'redirect_uri' => Config::get('services.mal.redirect_uri'),
'code_challenge' => $codeVerifier,
'state' => $id,
'response_type' => 'code',
'code_challenge_method' => 'plain',
];
return 'https://myanimelist.net/v1/oauth2/authorize?' . http_build_query($query);
}
if ($this === static::ANILIST) {
$query = [
'client_id' => Config::get('services.anilist.client_id'),
'redirect_uri' => Config::get('services.anilist.redirect_uri'),
'response_type' => 'code',
];
return 'https://anilist.co/api/v2/oauth/authorize?' . http_build_query($query);
}
return null;
}
}
@@ -14,7 +14,6 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
@@ -55,7 +54,7 @@ class ExternalTokenAuthController extends Controller
$profileSite = ExternalProfileSite::fromLocalizedName($site);
if ($profileSite instanceof ExternalProfileSite) {
$link = $this->getRedirectLink($profileSite);
$link = $profileSite->getAuthorizeUrl();
if ($link !== null) {
return Redirect::to($link);
@@ -66,30 +65,4 @@ class ExternalTokenAuthController extends Controller
'error' => 'invalid site',
], 400);
}
/**
* Get the link of the external site to authenticate the user.
*
* @param ExternalProfileSite $site
* @return string
*/
private function getRedirectLink(ExternalProfileSite $site): ?string
{
switch ($site) {
case ExternalProfileSite::KITSU:
return null;
case ExternalProfileSite::MAL:
return null;
case ExternalProfileSite::ANILIST:
$query = [
'client_id' => Config::get('services.anilist.client_id'),
'redirect_uri' => Config::get('services.anilist.redirect_uri'),
'response_type' => 'code',
];
return 'https://anilist.co/api/v2/oauth/authorize?' . http_build_query($query);
default:
return null;
}
}
}