diff --git a/app/Console/Commands/BackfillIdenticons.php b/app/Console/Commands/BackfillIdenticons.php new file mode 100644 index 000000000..ed7709f65 --- /dev/null +++ b/app/Console/Commands/BackfillIdenticons.php @@ -0,0 +1,39 @@ +chunk(100, function ($users) { + foreach ($users as $user) { + UpdateUserIdenticonStatus::dispatch($user); + } + + sleep(2); + }); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 3f7e292f6..97d415547 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\RegisterRequest; use App\Jobs\RegisterUser; +use App\Jobs\UpdateUserIdenticonStatus; use App\Models\User; use App\Providers\AppServiceProvider; use Illuminate\Auth\Events\Registered; @@ -71,6 +72,10 @@ protected function create(RegisterRequest $request): User { $this->dispatchSync(RegisterUser::fromRequest($request)); - return User::findByEmailAddress($request->emailAddress()); + $user = User::findByEmailAddress($request->emailAddress()); + + $this->dispatch(new UpdateUserIdenticonStatus($user)); + + return $user; } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 3a844c5b7..269dcb9c1 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -16,7 +16,7 @@ public function show() $communityMembers = Cache::remember( 'communityMembers', now()->addMinutes(5), - fn () => User::notBanned()->inRandomOrder()->take(100)->get()->chunk(20) + fn () => User::notBanned()->withAvatar()->inRandomOrder()->take(100)->get()->chunk(20) ); $totalUsers = Cache::remember('totalUsers', now()->addDay(), fn () => number_format(User::notBanned()->count())); diff --git a/app/Jobs/UnVerifyAuthor.php b/app/Jobs/UnVerifyAuthor.php index ec742a952..9ab35b19c 100644 --- a/app/Jobs/UnVerifyAuthor.php +++ b/app/Jobs/UnVerifyAuthor.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; -class UnVerifyAuthor implements ShouldQueue +final class UnVerifyAuthor implements ShouldQueue { use Queueable; diff --git a/app/Jobs/UpdateUserIdenticonStatus.php b/app/Jobs/UpdateUserIdenticonStatus.php new file mode 100644 index 000000000..b863e52aa --- /dev/null +++ b/app/Jobs/UpdateUserIdenticonStatus.php @@ -0,0 +1,25 @@ +hasIdenticon($this->user->githubId()); + + $this->user->update(['github_has_identicon' => $hasIdenticon]); + } +} \ No newline at end of file diff --git a/app/Jobs/VerifyAuthor.php b/app/Jobs/VerifyAuthor.php index 89dbf332a..6ffc319a3 100644 --- a/app/Jobs/VerifyAuthor.php +++ b/app/Jobs/VerifyAuthor.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; -class VerifyAuthor implements ShouldQueue +final class VerifyAuthor implements ShouldQueue { use Queueable; diff --git a/app/Models/User.php b/app/Models/User.php index c43b35dad..ab8ef3fac 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -58,6 +58,7 @@ final class User extends Authenticatable implements MustVerifyEmail 'remember_token', 'bio', 'banned_reason', + 'github_has_identicon' ]; /** @@ -75,6 +76,7 @@ protected function casts(): array return [ 'allowed_notifications' => 'array', 'author_verified_at' => 'datetime', + 'github_has_identicon' => 'boolean', ]; } @@ -113,6 +115,11 @@ public function githubUsername(): string return $this->github_username ?? ''; } + public function hasIdenticon(): bool + { + return (bool) $this->github_has_identicon; + } + public function twitter(): ?string { return $this->twitter; @@ -411,6 +418,11 @@ public function scopeModerators(Builder $query) ]); } + public function scopeWithAvatar(Builder $query) + { + return $query->where('github_has_identicon', false); + } + public function scopeNotBanned(Builder $query) { return $query->whereNull('banned_at'); diff --git a/app/Social/GithubUserApi.php b/app/Social/GithubUserApi.php index e7580ca21..21cf9ee0e 100644 --- a/app/Social/GithubUserApi.php +++ b/app/Social/GithubUserApi.php @@ -5,7 +5,7 @@ use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; -class GithubUserApi +final class GithubUserApi { public function find(int|string $id): ?GitHubUser { @@ -14,4 +14,22 @@ public function find(int|string $id): ?GitHubUser return $response->failed() ? null : new GitHubUser($response->json()); } + + public function hasIdenticon(int|string $id): bool + { + $response = Http::retry(3, 300, fn ($exception) => $exception instanceof ConnectionException) + ->get("https://avatars.githubusercontent.com/u/{$id}?v=4&s=40"); + + if ($response->failed()) { + return true; + } + + if (! $info = getimagesizefromstring($response->body())) { + return true; + } + + [$width, $height] = $info; + + return ! ($width === 420 && $height === 420); + } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 8f5c3609b..406393cb4 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -23,6 +23,7 @@ public function definition(): array 'remember_token' => Str::random(10), 'github_id' => $this->faker->unique()->numberBetween(10000, 99999), 'github_username' => $this->faker->unique()->userName(), + 'github_has_identicon' => $this->faker->boolean(), 'twitter' => $this->faker->unique()->userName(), 'bluesky' => $this->faker->unique()->userName(), 'website' => 'https://laravel.io', diff --git a/database/migrations/2025_09_11_152525_add_has_identicon_to_users_table.php b/database/migrations/2025_09_11_152525_add_has_identicon_to_users_table.php new file mode 100644 index 000000000..dedb4763d --- /dev/null +++ b/database/migrations/2025_09_11_152525_add_has_identicon_to_users_table.php @@ -0,0 +1,15 @@ +boolean('github_has_identicon')->after('github_username')->default(false); + }); + } +}; diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 5c3fe18a4..d30dfdd4d 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -34,7 +34,7 @@ public function run(): void DB::beginTransaction(); User::factory() - ->count(100) + ->count(300) ->has(Thread::factory()->count(2), 'threadsRelation') ->has( Article::factory() diff --git a/resources/views/components/avatar.blade.php b/resources/views/components/avatar.blade.php index ac27d4f23..db261e951 100644 --- a/resources/views/components/avatar.blade.php +++ b/resources/views/components/avatar.blade.php @@ -4,7 +4,7 @@ ]) githubId() +$src = $user->githubId() && ! $user->hasIdenticon() ? sprintf('https://avatars.githubusercontent.com/u/%s', $user->githubId()) : asset('https://laravel.io/images/laravelio-icon-gray.svg'); ?>