mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
feat: added MAL option to external profiles (#760)
This commit is contained in:
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-6
@@ -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())
|
||||
|
||||
+131
@@ -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;
|
||||
}
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user