diff --git a/app/Http/Resources/Api/V0/ModResource.php b/app/Http/Resources/Api/V0/ModResource.php index 23ed9cc..bfd5925 100644 --- a/app/Http/Resources/Api/V0/ModResource.php +++ b/app/Http/Resources/Api/V0/ModResource.php @@ -15,6 +15,8 @@ class ModResource extends JsonResource */ public function toArray(Request $request): array { + $this->load(['users', 'versions', 'license']); + return [ 'type' => 'mod', 'id' => $this->id, @@ -51,11 +53,8 @@ class ModResource extends JsonResource 'type' => 'version', 'id' => $version->id, ], - - // TODO: The download link to the version can be placed here, but I'd like to track the number of - // downloads that are made, so we'll need a new route/feature for that. #35 'links' => [ - 'self' => $version->link, + 'self' => $version->downloadUrl(absolute: true), ], ])->toArray(), diff --git a/app/Http/Resources/Api/V0/ModVersionResource.php b/app/Http/Resources/Api/V0/ModVersionResource.php index 63e5a3e..3a5037f 100644 --- a/app/Http/Resources/Api/V0/ModVersionResource.php +++ b/app/Http/Resources/Api/V0/ModVersionResource.php @@ -28,10 +28,7 @@ class ModVersionResource extends JsonResource // $this->description //), - // TODO: The download link to the version can be placed here, but I'd like to track the number of - // downloads that are made, so we'll need a new route/feature for that. #35 - 'link' => $this->link, - + 'link' => $this->downloadUrl(absolute: true), 'virus_total_link' => $this->virus_total_link, 'downloads' => $this->downloads, 'created_at' => $this->created_at, diff --git a/app/Http/Resources/Api/V0/UserResource.php b/app/Http/Resources/Api/V0/UserResource.php index ce37906..1b26c64 100644 --- a/app/Http/Resources/Api/V0/UserResource.php +++ b/app/Http/Resources/Api/V0/UserResource.php @@ -15,6 +15,8 @@ class UserResource extends JsonResource */ public function toArray(Request $request): array { + $this->load('role'); + return [ 'type' => 'user', 'id' => $this->id, @@ -34,7 +36,7 @@ class UserResource extends JsonResource ], 'includes' => $this->when( ApiController::shouldInclude('user_role'), - new UserRoleResource($this->role) + new UserRoleResource($this->role), ), 'links' => [ 'self' => $this->profileUrl(), diff --git a/app/Jobs/Import/ImportHubDataJob.php b/app/Jobs/Import/ImportHubDataJob.php index 084d1c3..28192c5 100644 --- a/app/Jobs/Import/ImportHubDataJob.php +++ b/app/Jobs/Import/ImportHubDataJob.php @@ -30,6 +30,7 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use League\HTMLToMarkdown\HtmlConverter; use Stevebauman\Purify\Facades\Purify; +use Throwable; class ImportHubDataJob implements ShouldBeUnique, ShouldQueue { @@ -40,6 +41,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue // Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary // tables to store the data to save on memory; we don't want this to be a hog. $this->bringUserAvatarLocal(); + $this->bringUserOptionsLocal(); $this->bringFileAuthorsLocal(); $this->bringFileOptionsLocal(); $this->bringFileContentLocal(); @@ -98,6 +100,35 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue }); } + /** + * Bring the user options table from the Hub database to the local database temporary table. + */ + private function bringUserOptionsLocal(): void + { + DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_user_options_values'); + DB::statement('CREATE TEMPORARY TABLE temp_user_options_values ( + userID INT, + about LONGTEXT + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci'); + + DB::connection('mysql_hub') + ->table('wcf1_user_option_value') + ->orderBy('userID') + ->chunk(200, function ($options) { + $insertData = []; + foreach ($options as $option) { + $insertData[] = [ + 'userID' => (int) $option->userID, + 'about' => $option->userOption1, + ]; + } + + if ($insertData) { + DB::table('temp_user_options_values')->insert($insertData); + } + }); + } + /** * Bring the file authors from the Hub database to the local database temporary table. */ @@ -357,6 +388,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue 'name' => $hubUser->username, 'email' => Str::lower($hubUser->email), 'password' => $this->cleanPasswordHash($hubUser->password), + 'about' => $this->fetchUserAbout($hubUser->userID), 'profile_photo_path' => $this->fetchUserAvatar($curl, $hubUser), 'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $hubUser), 'created_at' => $this->cleanRegistrationDate($hubUser->registrationDate), @@ -377,6 +409,32 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue return str_starts_with($clean, '$2') ? $clean : ''; } + /** + * Fetch the user about text from the temporary table. + */ + private function fetchUserAbout(int $userID): string + { + $about = DB::table('temp_user_options_values') + ->where('userID', $userID) + ->limit(1) + ->value('about'); + + return $this->cleanHubContent($about ?? ''); + } + + /** + * Convert the mod description from WoltHub flavoured HTML to Markdown. + */ + protected function cleanHubContent(string $dirty): string + { + // Alright, hear me out... Shut up. + + $converter = new HtmlConverter; + $clean = Purify::clean($dirty); + + return $converter->convert($clean); + } + /** * Fetch the user avatar from the Hub and store it anew. */ @@ -914,19 +972,6 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue curl_close($curl); } - /** - * Convert the mod description from WoltHub flavoured HTML to Markdown. - */ - protected function cleanHubContent(string $dirty): string - { - // Alright, hear me out... Shut up. - - $converter = new HtmlConverter; - $clean = Purify::clean($dirty); - - return $converter->convert($clean); - } - /** * Fetch the mod thumbnail from the Hub and store it anew. */ @@ -1034,10 +1079,11 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue /** * The job failed to process. */ - public function failed(Exception $exception): void + public function failed(Throwable $exception): void { // Explicitly drop the temporary tables. DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar'); + DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_options_values'); DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author'); DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values'); DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content'); diff --git a/app/Livewire/User/FollowCard.php b/app/Livewire/User/FollowCard.php index 859db82..1227aa9 100644 --- a/app/Livewire/User/FollowCard.php +++ b/app/Livewire/User/FollowCard.php @@ -75,6 +75,11 @@ class FollowCard extends Component #[Locked] public Collection $followUsers; + /** + * The events the component should listen for. + */ + protected $listeners = ['refreshComponent' => '$refresh']; + /** * The number of users being displayed. */ @@ -148,6 +153,8 @@ class FollowCard extends Component { // Update the collection of profile user's followers (or following). $this->followUsers = $this->profileUser->{$this->relationship}()->get(); + + $this->dispatch('refreshComponent'); } /** diff --git a/app/Livewire/User/FollowCards.php b/app/Livewire/User/FollowCards.php index b95affe..c1847e1 100644 --- a/app/Livewire/User/FollowCards.php +++ b/app/Livewire/User/FollowCards.php @@ -23,6 +23,16 @@ class FollowCards extends Component #[Locked] public Collection $authFollowIds; + /** + * Render the component. + */ + public function render(): View + { + $this->updateAuthFollowIds(); + + return view('livewire.user.follow-cards'); + } + /** * Called when the user follows or unfollows a user. */ @@ -38,12 +48,4 @@ class FollowCards extends Component $this->dispatch('auth-follow-change'); } - - /** - * Render the component. - */ - public function render(): View - { - return view('livewire.user.follow-cards'); - } } diff --git a/app/Models/License.php b/app/Models/License.php index 5c013b6..4f967ae 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -22,4 +22,17 @@ class License extends Model return $this->hasMany(Mod::class) ->chaperone(); } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'hub_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + } } diff --git a/app/Models/Mod.php b/app/Models/Mod.php index 2d0738f..b7a9877 100644 --- a/app/Models/Mod.php +++ b/app/Models/Mod.php @@ -243,6 +243,7 @@ class Mod extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', + 'published_at' => 'datetime', ]; } diff --git a/app/Models/ModDependency.php b/app/Models/ModDependency.php index 31de493..8ab885c 100644 --- a/app/Models/ModDependency.php +++ b/app/Models/ModDependency.php @@ -41,4 +41,15 @@ class ModDependency extends Model { return $this->belongsTo(Mod::class, 'dependent_mod_id'); } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + } } diff --git a/app/Models/ModResolvedDependency.php b/app/Models/ModResolvedDependency.php index 41d47c8..4bdc02b 100644 --- a/app/Models/ModResolvedDependency.php +++ b/app/Models/ModResolvedDependency.php @@ -36,4 +36,15 @@ class ModResolvedDependency extends Model { return $this->belongsTo(ModVersion::class, 'resolved_mod_version_id'); } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + } } diff --git a/app/Models/ModVersion.php b/app/Models/ModVersion.php index 406cfaa..100d99c 100644 --- a/app/Models/ModVersion.php +++ b/app/Models/ModVersion.php @@ -152,4 +152,23 @@ class ModVersion extends Model return $this->downloads; } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'hub_id' => 'integer', + 'version_major' => 'integer', + 'version_minor' => 'integer', + 'version_patch' => 'integer', + 'downloads' => 'integer', + 'disabled' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'published_at' => 'datetime', + ]; + } } diff --git a/app/Models/OAuthConnection.php b/app/Models/OAuthConnection.php index e406fb4..4864dff 100644 --- a/app/Models/OAuthConnection.php +++ b/app/Models/OAuthConnection.php @@ -16,4 +16,15 @@ class OAuthConnection extends Model { return $this->belongsTo(User::class); } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + } } diff --git a/app/Models/SptVersion.php b/app/Models/SptVersion.php index dfb7086..9f97a69 100644 --- a/app/Models/SptVersion.php +++ b/app/Models/SptVersion.php @@ -172,4 +172,21 @@ class SptVersion extends Model ->first(); }); } + + /** + * The attributes that should be cast to native types. + */ + protected function casts(): array + { + return [ + 'hub_id' => 'integer', + 'version_major' => 'integer', + 'version_minor' => 'integer', + 'version_patch' => 'integer', + 'mod_count' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index f90525c..5830a4b 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -32,7 +32,10 @@ class UserFactory extends Factory 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), + + // TODO: Does Faker have a markdown plugin? 'about' => fake()->paragraphs(random_int(1, 10), true), + 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'remember_token' => Str::random(10), diff --git a/resources/views/user/show.blade.php b/resources/views/user/show.blade.php index fb29e5a..426b409 100644 --- a/resources/views/user/show.blade.php +++ b/resources/views/user/show.blade.php @@ -43,7 +43,7 @@ {{-- About --}} @if ($user->about)
- {{ $user->about }} + {!! Str::markdown($user->about) !!}
@endif