feat: added a proper function to the token callback controller (#738)

This commit is contained in:
Kyrch
2024-08-29 19:40:07 -03:00
committed by GitHub
parent 8d4f27f8ce
commit a27a46305a
23 changed files with 606 additions and 126 deletions
+7 -1
View File
@@ -253,8 +253,13 @@ DISCORD_BOT_API_URL=
DISCORD_BOT_API_KEY=
DB_UPDATES_DISCORD_CHANNEL=
ADMIN_DISCORD_CHANNEL=
MAL_CLIENT_ID=null
OPENAI_BEARER_TOKEN=null
MAL_CLIENT_ID=null
MAL_CLIENT_SECRET=null
MAL_REDIRECT_URI=null
ANILIST_CLIENT_ID=null
ANILIST_CLIENT_SECRET=null
ANILIST_REDIRECT_URI=null
# session
SESSION_DRIVER=database
@@ -294,3 +299,4 @@ WEB_PATH=
# wiki
WIKI_LOGIN=http://localhost/login
WIKI_RESET_PASSWORD=http://localhost/reset-password
WIKI_EXTERNAL_PROFILE=http://localhost/external
+7 -1
View File
@@ -251,8 +251,13 @@ DISCORD_BOT_API_URL=
DISCORD_BOT_API_KEY=
DB_UPDATES_DISCORD_CHANNEL=
ADMIN_DISCORD_CHANNEL=
MAL_CLIENT_ID=null
OPENAI_BEARER_TOKEN=null
MAL_CLIENT_ID=null
MAL_CLIENT_SECRET=null
MAL_REDIRECT_URI=null
ANILIST_CLIENT_ID=null
ANILIST_CLIENT_SECRET=null
ANILIST_REDIRECT_URI=null
# session
SESSION_DRIVER=database
@@ -292,3 +297,4 @@ WEB_PATH=
# wiki
WIKI_LOGIN=http://localhost/login
WIKI_RESET_PASSWORD=http://localhost/reset-password
WIKI_EXTERNAL_PROFILE=http://localhost/external
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\List;
use App\Enums\Models\List\ExternalProfileSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
/**
* Class BaseStoreExternalProfileAction.
*/
abstract class BaseStoreExternalProfileAction
{
protected Collection $resources;
/**
* Preload the resources for performance proposals.
*
* @param ExternalProfileSite $profileSite
* @param array $entries
* @return void
*/
protected function preloadResources(ExternalProfileSite $profileSite, array $entries): void
{
$externalResources = ExternalResource::query()
->where(ExternalResource::ATTRIBUTE_SITE, $profileSite->getResourceSite()->value)
->whereIn(ExternalResource::ATTRIBUTE_EXTERNAL_ID, Arr::pluck($entries, 'external_id'))
->with(ExternalResource::RELATION_ANIME)
->get()
->mapWithKeys(fn (ExternalResource $resource) => [$resource->external_id => $resource->anime]);
$this->resources = $externalResources;
}
/**
* Get the animes by the external id.
*
* @param int $externalId
* @return Collection<int, Anime>
*/
protected function getAnimesByExternalId(int $externalId): Collection
{
return $this->resources[$externalId] ?? new Collection();
}
}
@@ -15,6 +15,8 @@ use Illuminate\Support\Arr;
*/
abstract class BaseExternalEntryAction
{
protected ?array $response = null;
/**
* Create a new action instance.
*
@@ -54,6 +56,13 @@ abstract class BaseExternalEntryAction
return Arr::get($this->profileParameters, 'name');
}
/**
* Get the id of the external user.
*
* @return int|null
*/
abstract public function getId(): ?int;
/**
* Get the entries of the response.
*
@@ -64,7 +73,7 @@ abstract class BaseExternalEntryAction
/**
* Make the request to the external api.
*
* @return array|null
* @return static
*/
abstract public function makeRequest(): ?array;
abstract protected function makeRequest(): static;
}
@@ -4,6 +4,11 @@ declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile\ExternalEntry;
use App\Models\List\External\ExternalToken;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Support\Facades\Config;
/**
* Class BaseExternalEntryTokenAction
*
@@ -11,23 +16,24 @@ namespace App\Actions\Models\List\ExternalProfile\ExternalEntry;
*/
abstract class BaseExternalEntryTokenAction
{
protected ?array $response = null;
protected ?int $id = null;
/**
* Create a new action instance.
*
* @param array $parameters
*/
public function __construct(protected array $parameters)
public function __construct(protected ExternalToken $token)
{
}
/**
* Get the username of the profile.
* Get the id of the external user.
*
* @return string
* @return int|null
*/
public function getUsername(): string
public function getId(): ?int
{
return ''; // TODO
return $this->id;
}
/**
@@ -37,7 +43,17 @@ abstract class BaseExternalEntryTokenAction
*/
public function getToken(): string
{
return ''; // TODO
return $this->token->access_token;
}
/**
* Get the username.
*
* @return string|null
*/
public function getUsername(): ?string
{
return null;
}
/**
@@ -50,7 +66,7 @@ abstract class BaseExternalEntryTokenAction
/**
* Make the request to the external api.
*
* @return array|null
* @return static
*/
abstract public function makeRequest(): ?array;
abstract protected function makeRequest(): static;
}
@@ -26,39 +26,55 @@ class AnilistExternalEntryAction extends BaseExternalEntryAction
public function getEntries(): array
{
$entries = [];
$response = $this->makeRequest();
if ($response !== null) {
$favorites = Arr::map(Arr::get($response, 'data.User.favourites.anime.nodes'), fn ($value) => $value['id']);
$lists = Arr::where(Arr::get($response, 'data.MediaListCollection.lists'), fn ($value) => $value['isCustomList'] === false);
if ($this->response === null) {
$this->makeRequest();
}
foreach ($lists as $list) {
foreach (Arr::get($list, 'entries') as $entry) {
$entryId = intval(Arr::get($entry, 'media.id'));
$entries[] = [
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $entryId,
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, 'score'),
ExternalEntry::ATTRIBUTE_WATCH_STATUS => ExternalEntryWatchStatus::getAnilistMapping(Arr::get($entry, 'status'))->value,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => in_array($entryId, $favorites),
];
}
$favorites = Arr::map(Arr::get($this->response, 'data.User.favourites.anime.nodes'), fn ($value) => $value['id']);
$lists = Arr::where(Arr::get($this->response, 'data.MediaListCollection.lists'), fn ($value) => $value['isCustomList'] === false);
foreach ($lists as $list) {
foreach (Arr::get($list, 'entries') as $entry) {
$entryId = intval(Arr::get($entry, 'media.id'));
$entries[] = [
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $entryId,
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, 'score'),
ExternalEntry::ATTRIBUTE_WATCH_STATUS => ExternalEntryWatchStatus::getAnilistMapping(Arr::get($entry, 'status'))->value,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => in_array($entryId, $favorites),
];
}
}
return $entries;
}
/**
* Get the id of the external user.
*
* @return int|null
*/
public function getId(): ?int
{
if ($this->response === null) {
$this->makeRequest();
}
return Arr::get($this->response, 'data.User.id');
}
/**
* Make the request to the external api.
*
* @return array|null
* @return static
*/
public function makeRequest(): ?array
protected function makeRequest(): static
{
try {
$query = '
query($userName: String) {
User(name: $userName) {
id
favourites {
anime {
nodes {
@@ -88,14 +104,14 @@ class AnilistExternalEntryAction extends BaseExternalEntryAction
'userName' => $this->getUsername(),
];
$response = Http::post('https://graphql.anilist.co', [
$this->response = Http::post('https://graphql.anilist.co', [
'query' => $query,
'variables' => $variables,
])
->throw()
->json();
return $response;
return $this;
} catch (RequestException $e) {
Log::error($e->getMessage());
@@ -8,8 +8,12 @@ use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryToken
use App\Enums\Models\List\ExternalEntryWatchStatus;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\ExternalResource;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -26,9 +30,12 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
public function getEntries(): array
{
$entries = [];
$response = $this->makeRequest();
if ($response !== null) {
if ($this->response === null) {
$this->makeRequest();
}
if ($response = $this->response) {
$lists = Arr::where(Arr::get($response, 'data.MediaListCollection.lists'), fn ($value) => $value['isCustomList'] === false);
foreach ($lists as $list) {
@@ -47,17 +54,61 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
return $entries;
}
/**
* Get the username.
*
* @return string|null
*/
public function getUsername(): ?string
{
if ($this->response === null) {
$this->makeRequest();
}
return Arr::get($this->response, 'data.Viewer.name');
}
/**
* Get the id of the external user.
*
* @return int|null
*/
public function getId(): ?int
{
if ($this->id !== null) {
return $this->id;
}
// TODO: This should be tested.
try {
$decoded = JWT::decode($this->getToken(), new Key(Config::get('services.anilist.client_secret'), 'HS256'));
$decodedArray = json_decode(json_encode($decoded), true);
$this->id = Arr::get($decodedArray, 'id');
return $this->id;
} catch (Exception $e) {
Log::error($e->getMessage());
return null;
}
}
/**
* Make the request to the external api.
*
* @return array|null
* @return static
*/
public function makeRequest(): ?array
protected function makeRequest(): static
{
try {
$query = '
query($userName: String) {
MediaListCollection(userName: $userName, type: ANIME) {
query($userId: Int) {
Viewer {
name
}
MediaListCollection(userId: $userId, type: ANIME) {
lists {
name
status
@@ -77,10 +128,10 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
';
$variables = [
'userName' => $this->getUsername(),
'userId' => $this->getId(),
];
$response = Http::withToken($this->getToken())
$this->response = Http::withToken($this->getToken())
->post('https://graphql.anilist.co', [
'query' => $query,
'variables' => $variables,
@@ -88,7 +139,7 @@ class AnilistExternalEntryTokenAction extends BaseExternalEntryTokenAction
->throw()
->json();
return $response;
return $this;
} catch (RequestException $e) {
Log::error($e->getMessage());
@@ -11,5 +11,18 @@ use App\Models\List\External\ExternalToken;
*/
abstract class BaseExternalTokenAction
{
abstract public function store(): ExternalToken;
/**
* Create a new action instance.
*/
public function __construct()
{
}
/**
* Use the authorization code to get the tokens and store them.
*
* @param string $code
* @return ExternalToken|null
*/
abstract public function store(string $code): ?ExternalToken;
}
@@ -6,6 +6,11 @@ namespace App\Actions\Models\List\ExternalProfile\ExternalToken\Site;
use App\Actions\Models\List\ExternalProfile\ExternalToken\BaseExternalTokenAction;
use App\Models\List\External\ExternalToken;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Class AnilistExternalTokenAction.
@@ -13,18 +18,40 @@ use App\Models\List\External\ExternalToken;
class AnilistExternalTokenAction extends BaseExternalTokenAction
{
/**
* Create a new action instance.
* Use the authorization code to get the tokens and store them.
*
* @param string $code
* @return ExternalToken|null
*/
public function __construct(protected string $code)
public function store(string $code): ?ExternalToken
{
}
try {
$response = Http::acceptJson()
->asForm()
->post('https://anilist.co/api/v2/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => Config::get('services.anilist.client_id'),
'client_secret' => Config::get('services.anilist.client_secret'),
'redirect_uri' => Config::get('services.anilist.redirect_uri'),
'code' => $code,
])
->throw()
->json();
public function store(): ExternalToken
{
// TODO: Make a request to the AniList API to get the access and the refresh tokens
// and return the external token created.
return new ExternalToken();
$token = Arr::get($response, 'access_token');
if ($token !== null) {
return ExternalToken::query()->create([
ExternalToken::ATTRIBUTE_ACCESS_TOKEN => $token,
]);
}
return null;
} catch (RequestException $e) {
Log::error($e->getMessage());
throw $e;
}
}
}
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Models\List\BaseStoreExternalProfileAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Site\AnilistExternalEntryTokenAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\List\External\ExternalEntry;
use App\Models\List\External\ExternalToken;
use App\Models\List\ExternalProfile;
use Error;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class StoreExternalProfileTokenAction.
*/
class StoreExternalProfileTokenAction extends BaseStoreExternalProfileAction
{
protected Collection $resources;
/**
* Find or store external profile and its entries given determined external token.
*
* @param ExternalToken $token
* @param array $parameters
* @return ExternalProfile
*
* @throws Exception
*/
public function findOrCreate(ExternalToken $token, array $parameters): ExternalProfile
{
try {
DB::beginTransaction();
$site = ExternalProfileSite::fromLocalizedName(Arr::get($parameters, 'site'));
$action = $this->getActionClass($site, $token);
if ($action === null) {
throw new Error("Undefined action for site {$site->localize()}"); // TODO: check if it is working
}
$userId = $action->getId();
// TODO: if the profile already exists, the list should be synced.
$profile = $this->searchForUserId($userId, $site, $action, $parameters);
$entries = $action->getEntries();
$this->preloadResources($site, $entries);
$token->externalprofile()->associate($profile);
$externalEntries = [];
foreach ($entries as $entry) {
$externalId = Arr::get($entry, 'external_id');
foreach ($this->getAnimesByExternalId($externalId) as $anime) {
$externalEntries[] = [
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, ExternalEntry::ATTRIBUTE_SCORE),
ExternalEntry::ATTRIBUTE_IS_FAVORITE => Arr::get($entry, ExternalEntry::ATTRIBUTE_IS_FAVORITE),
ExternalEntry::ATTRIBUTE_WATCH_STATUS => Arr::get($entry, ExternalEntry::ATTRIBUTE_WATCH_STATUS),
ExternalEntry::ATTRIBUTE_ANIME => $anime->getKey(),
ExternalEntry::ATTRIBUTE_PROFILE => $profile->getKey(),
];
}
}
ExternalEntry::insert($externalEntries);
DB::commit();
return $profile;
} catch (Exception $e) {
Log::error($e->getMessage());
DB::rollBack();
throw $e;
}
}
/**
* Find or create the profile for a userId and site.
*
* @param int $userId
* @param ExternalProfileSite $site
* @param BaseExternalEntryTokenAction $action
* @param array $parameters
* @return ExternalProfile|null
*/
protected function searchForUserId(int $userId, ExternalProfileSite $site, BaseExternalEntryTokenAction $action, array $parameters): ?ExternalProfile
{
$claimedProfile = ExternalProfile::query()
->where(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID, $userId)
->where(ExternalProfile::ATTRIBUTE_SITE, $site->value)
->whereHas(ExternalProfile::RELATION_USER)
->first();
if ($claimedProfile instanceof ExternalProfile) {
return $claimedProfile;
}
$unclaimedProfile = ExternalProfile::query()
->where(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID, $userId)
->where(ExternalProfile::ATTRIBUTE_SITE, $site->value)
->whereDoesntHave(ExternalProfile::RELATION_USER)
->first();
if ($unclaimedProfile instanceof ExternalProfile) {
$unclaimedProfile->update([
ExternalProfile::ATTRIBUTE_USER => Arr::get($parameters, ExternalProfile::ATTRIBUTE_USER),
ExternalProfile::ATTRIBUTE_NAME => $action->getUsername(),
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::PRIVATE->value,
]);
return $unclaimedProfile;
}
$storeAction = new StoreAction();
$profile = $storeAction->store(ExternalProfile::query(), [
ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID => $userId,
ExternalProfile::ATTRIBUTE_USER => Arr::get($parameters, ExternalProfile::ATTRIBUTE_USER),
ExternalProfile::ATTRIBUTE_NAME => $action->getUsername(),
ExternalProfile::ATTRIBUTE_SITE => $site->value,
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::PRIVATE->value,
]);
if ($profile instanceof ExternalProfile) {
return $profile;
}
return null;
}
/**
* Get the mapping for the entries token class.
*
* @param ExternalProfileSite $site
* @param ExternalToken $token
* @return BaseExternalEntryTokenAction|null
*/
protected function getActionClass(ExternalProfileSite $site, ExternalToken $token): ?BaseExternalEntryTokenAction
{
return match ($site) {
ExternalProfileSite::ANILIST => new AnilistExternalEntryTokenAction($token),
default => null,
};
}
}
@@ -5,48 +5,50 @@ declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Models\List\BaseStoreExternalProfileAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\BaseExternalEntryAction;
use App\Actions\Models\List\ExternalProfile\ExternalEntry\Site\AnilistExternalEntryAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use App\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use Error;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class StoreExternalProfileAction.
* Class StoreExternalProfileUsernameAction.
*/
class StoreExternalProfileAction
class StoreExternalProfileUsernameAction extends BaseStoreExternalProfileAction
{
protected Collection $resources;
/**
* Store external profile and its entries.
* Find or store an external profile and its entries given determined username.
*
* @param Builder $builder
* @param array $profileParameters
* @return Model
* @return ExternalProfile
*
* @throws Exception
*/
public function store(Builder $builder, array $profileParameters): Model
public function findOrCreate(Builder $builder, array $profileParameters): ExternalProfile
{
try {
DB::beginTransaction();
$storeAction = new StoreAction();
$profileSite = ExternalProfileSite::fromLocalizedName(Arr::get($profileParameters, 'site'));
$findProfile = ExternalProfile::query()
->where(ExternalProfile::ATTRIBUTE_NAME, Arr::get($profileParameters, 'name'))
->where(ExternalProfile::ATTRIBUTE_SITE, $profileSite->value)
->first();
if ($findProfile instanceof ExternalProfile) {
return $findProfile;
}
DB::beginTransaction();
$action = $this->getActionClass($profileSite, $profileParameters);
if ($action === null) {
@@ -57,7 +59,11 @@ class StoreExternalProfileAction
$this->preloadResources($profileSite, $entries);
$storeAction = new StoreAction();
/** @var ExternalProfile $profile */
$profile = $storeAction->store($builder, [
ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID => $action->getId(),
ExternalProfile::ATTRIBUTE_NAME => Arr::get($profileParameters, ExternalProfile::ATTRIBUTE_NAME),
ExternalProfile::ATTRIBUTE_SITE => $profileSite->value,
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::fromLocalizedName(Arr::get($profileParameters, ExternalProfile::ATTRIBUTE_VISIBILITY))->value,
@@ -106,34 +112,4 @@ class StoreExternalProfileAction
default => null,
};
}
/**
* Preload the resources for performance proposals.
*
* @param ExternalProfileSite $profileSite
* @param array $entries
* @return void
*/
protected function preloadResources(ExternalProfileSite $profileSite, array $entries): void
{
$externalResources = ExternalResource::query()
->where(ExternalResource::ATTRIBUTE_SITE, $profileSite->getResourceSite()->value)
->whereIn(ExternalResource::ATTRIBUTE_EXTERNAL_ID, Arr::pluck($entries, 'external_id'))
->with(ExternalResource::RELATION_ANIME)
->get()
->mapWithKeys(fn (ExternalResource $resource) => [$resource->external_id => $resource->anime]);
$this->resources = $externalResources;
}
/**
* Get the animes by the external id.
*
* @param int $externalId
* @return Collection<int, Anime>
*/
protected function getAnimesByExternalId(int $externalId): Collection
{
return $this->resources[$externalId] ?? new Collection();
}
}
}
@@ -7,6 +7,9 @@ namespace App\Actions\Models\List\ExternalProfile;
use App\Actions\Models\List\ExternalProfile\ExternalToken\BaseExternalTokenAction;
use App\Actions\Models\List\ExternalProfile\ExternalToken\Site\AnilistExternalTokenAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Models\List\External\ExternalToken;
use Error;
use Exception;
use Illuminate\Support\Arr;
/**
@@ -14,29 +17,41 @@ use Illuminate\Support\Arr;
*/
class StoreExternalTokenAction
{
public function store($query)
/**
* Store the token given the query of the callback URL.
*
* @param array $query
* @return ExternalToken|null
*
* @throws Exception
*/
public function store(array $query): ?ExternalToken
{
$profileSite = ExternalProfileSite::fromLocalizedName(Arr::get($query, 'site'));
$code = Arr::get($query, 'code');
$site = Arr::get($query, 'site');
$profileSite = ExternalProfileSite::fromLocalizedName($site);
$action = $this->getActionClass($profileSite, $code);
$action = $this->getActionClass($profileSite);
$externalToken = $action->store();
if ($action === null) {
throw new Error("Undefined callback URL for site {$site}");
}
$externalToken = $action->store(Arr::get($query, 'code'));
return $externalToken;
}
/**
* Get the mapping for the token class.
*
* @param ExternalProfileSite $site
* @param string $code
* @return BaseExternalTokenAction|null
*/
protected function getActionClass(ExternalProfileSite $site, string $code): ?BaseExternalTokenAction
protected function getActionClass(ExternalProfileSite $site): ?BaseExternalTokenAction
{
return match ($site) {
ExternalProfileSite::ANILIST => new AnilistExternalTokenAction($code),
ExternalProfileSite::ANILIST => new AnilistExternalTokenAction(),
default => null,
};
}
}
@@ -38,7 +38,7 @@ class MalAnimeApiAction extends ApiAction
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value);
if ($resource instanceof ExternalResource) {
$response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client')])
$response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client_id')])
->get("https://api.myanimelist.net/v2/anime/$resource->external_id", [
'fields' => 'studios',
])
+1
View File
@@ -8,6 +8,7 @@ use App\Console\Commands\Storage\Admin\DocumentDumpCommand;
use App\Console\Commands\Storage\Admin\DumpPruneCommand;
use App\Console\Commands\Storage\Admin\WikiDumpCommand;
use App\Models\BaseModel;
use App\Models\List\ExternalProfile;
use BezhanSalleh\FilamentExceptions\Models\Exception;
use Illuminate\Auth\Console\ClearResetsCommand;
use Illuminate\Cache\Console\PruneStaleTagsCommand;
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Filament\TableActions\Models\List\ExternalProfile;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileUsernameAction;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Filament\Components\Fields\Select;
@@ -47,9 +47,9 @@ class CreateExternalProfileTableAction extends BaseTableAction
$site = Arr::get($fields, ExternalProfile::ATTRIBUTE_SITE);
$visibility = Arr::get($fields, ExternalProfile::ATTRIBUTE_VISIBILITY);
$action = new StoreExternalProfileAction();
$action = new StoreExternalProfileUsernameAction();
$action->store(ExternalProfile::query(), [
$action->findOrCreate(ExternalProfile::query(), [
ExternalProfile::ATTRIBUTE_USER => Filament::auth()->id(),
ExternalProfile::ATTRIBUTE_NAME => $name,
ExternalProfile::ATTRIBUTE_SITE => ExternalProfileSite::from(intval($site))->localize(),
@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\List\External;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileTokenAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalTokenAction;
use App\Features\AllowExternalProfileManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Requests\Api\IndexRequest;
use App\Models\List\External\ExternalToken;
use App\Models\List\ExternalProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
@@ -30,24 +36,44 @@ class ExternalTokenCallbackController extends BaseController
->append(AllowExternalProfileManagement::class)
->__toString();
$this->middleware($isExternalProfileManagementAllowed)->except(['index', 'show']);
$this->middleware($isExternalProfileManagementAllowed);
}
/**
* This is the redirect URL which is set in the external provider.
*
* @param IndexRequest $request
* @return RedirectResponse|JsonResponse
*/
public function index(IndexRequest $request)
public function index(IndexRequest $request): RedirectResponse|JsonResponse
{
//$query = new Query($request->validated());
$validated = array_merge(
$request->validated(),
[ExternalProfile::ATTRIBUTE_USER => Auth::id()]
);
//$action = new StoreExternalTokenAction();
$action = new StoreExternalTokenAction();
//$action->store($query); // This stores the external token.
$externalToken = $action->store($validated);
// TODO: We should find or create a profile with the entries.
if ($externalToken === null) {
return new JsonResponse([
'error' => 'invalid code',
], 400);
}
// TODO: Then, the user should be redirect to the client page, e.g. /external/{site}/{profile}.
$profileAction = new StoreExternalProfileTokenAction();
$profile = $profileAction->findOrCreate($externalToken, $validated);
// https://animethemes.moe/external/{mal|anilist}/{profile_name}
$clientUrl = Str::of(Config::get('wiki.external_profile'))
->append('/')
->append(Str::lower($profile->site->name))
->append('/')
->append($profile->getName())
->__toString();
return Redirect::to($clientUrl);
}
}
@@ -10,7 +10,7 @@ use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\UpdateAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileUsernameAction;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Features\AllowExternalProfileManagement;
use App\Http\Api\Query\Query;
@@ -91,12 +91,12 @@ class ExternalProfileController extends BaseController
* Store a newly created resource.
*
* @param StoreRequest $request
* @param StoreExternalProfileAction $action
* @param StoreExternalProfileUsernameAction $action
* @return ExternalProfileResource
*/
public function store(StoreRequest $request, StoreExternalProfileAction $action): ExternalProfileResource
public function store(StoreRequest $request, StoreExternalProfileUsernameAction $action): ExternalProfileResource
{
$externalprofile = $action->store(ExternalProfile::query(), $request->validated());
$externalprofile = $action->findOrCreate(ExternalProfile::query(), $request->validated());
return new ExternalProfileResource($externalprofile, new Query());
}
+22
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models\List;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Events\List\ExternalProfile\ExternalProfileCreated;
@@ -16,11 +17,13 @@ use App\Models\List\External\ExternalEntry;
use App\Models\List\External\ExternalToken;
use Database\Factories\List\ExternalProfileFactory;
use Elastic\ScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Date;
/**
* Class ExternalProfile.
@@ -28,6 +31,7 @@ use Illuminate\Support\Collection;
* @property int $profile_id
* @property Collection<int, ExternalEntry> $externalentries
* @property ExternalToken|null $externaltoken
* @property int|null $external_user_id
* @property string $name
* @property ExternalProfileSite $site
* @property Carbon|null $synced_at
@@ -44,6 +48,7 @@ class ExternalProfile extends BaseModel
final public const TABLE = 'external_profiles';
final public const ATTRIBUTE_ID = 'profile_id';
final public const ATTRIBUTE_EXTERNAL_USER_ID = 'external_user_id';
final public const ATTRIBUTE_NAME = 'name';
final public const ATTRIBUTE_SITE = 'site';
final public const ATTRIBUTE_VISIBILITY = 'visibility';
@@ -61,6 +66,7 @@ class ExternalProfile extends BaseModel
* @var array<int, string>
*/
protected $fillable = [
ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID,
ExternalProfile::ATTRIBUTE_NAME,
ExternalProfile::ATTRIBUTE_SITE,
ExternalProfile::ATTRIBUTE_SYNCED_AT,
@@ -200,4 +206,20 @@ class ExternalProfile extends BaseModel
{
return $this->hasOne(ExternalToken::class, ExternalToken::ATTRIBUTE_PROFILE);
}
/**
* Get the prunable model query.
*
* @return Builder
*/
public function prunable(): Builder
{
return static::query()
->whereDoesntHave(ExternalProfile::RELATION_USER)
->where(
BaseModel::ATTRIBUTE_CREATED_AT,
ComparisonOperator::LTE->value,
Date::now()->subWeek()
);
}
}
+1
View File
@@ -32,6 +32,7 @@
"fakerphp/faker": "^1.21",
"filament/filament": "3.2.106",
"filament/forms": "3.2.106",
"firebase/php-jwt": "^6.10",
"flowframe/laravel-trend": "*",
"guzzlehttp/guzzle": "^7.5",
"laravel-notification-channels/discord": "^1.4",
Generated
+64 -1
View File
@@ -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": "a7f56b90543d589c1141e6bca68e58c5",
"content-hash": "4a53b034ee5565ea66e4046e539c2fd2",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -2444,6 +2444,69 @@
},
"time": "2024-07-31T11:53:30+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v6.10.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "500501c2ce893c824c801da135d02661199f60c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
"reference": "500501c2ce893c824c801da135d02661199f60c5",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
},
"time": "2024-05-18T18:05:11+00:00"
},
{
"name": "flowframe/laravel-trend",
"version": "v0.2.0",
+12 -4
View File
@@ -41,11 +41,19 @@ return [
'admin_discord_channel' => env('ADMIN_DISCORD_CHANNEL'),
],
'mal' => [
'client' => env('MAL_CLIENT_ID'),
],
'openai' => [
'token' => env('OPENAI_BEARER_TOKEN'),
],
'anilist' => [
'client_id' => env('ANILIST_CLIENT_ID'),
'client_secret' => env('ANILIST_CLIENT_SECRET'),
'redirect_uri' => env('ANILIST_REDIRECT_URI'),
],
'mal' => [
'client_id' => env('MAL_CLIENT_ID'),
'client_secret' => env('MAL_CLIENT_SECRET'),
'redirect_uri' => env('MAL_REDIRECT_URI'),
],
];
+2
View File
@@ -16,4 +16,6 @@ return [
'login' => env('WIKI_LOGIN'),
'reset_password' => env('WIKI_RESET_PASSWORD'),
'external_profile' => env('WIKI_EXTERNAL_PROFILE'),
];
@@ -19,6 +19,12 @@ return new class extends Migration
$table->timestamp(ExternalProfile::ATTRIBUTE_SYNCED_AT, 6)->nullable();
});
}
if (! Schema::hasColumn(ExternalProfile::TABLE, ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID)) {
Schema::table(ExternalProfile::TABLE, function (Blueprint $table) {
$table->integer(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID)->nullable();
});
}
}
/**
@@ -31,5 +37,11 @@ return new class extends Migration
$table->dropColumn(ExternalProfile::ATTRIBUTE_SYNCED_AT);
});
}
if (Schema::hasColumn(ExternalProfile::TABLE, ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID)) {
Schema::table(ExternalProfile::TABLE, function (Blueprint $table) {
$table->dropColumn(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID);
});
}
}
};