From c79b7928c1b64ea0ef8f722178a46eb359eb4f6e Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 11 Sep 2025 17:28:11 -0500 Subject: [PATCH 1/2] Async Github identicon avatars detection on user signup --- .../Controllers/Auth/RegisterController.php | 3 +++ app/Jobs/UpdateUserIdenticonStatus.php | 24 +++++++++++++++++++ app/Models/User.php | 6 +++++ app/Social/GithubUserApi.php | 21 ++++++++++++++++ ...52525_add_has_identicon_to_users_table.php | 16 +++++++++++++ resources/views/components/avatar.blade.php | 2 +- 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 app/Jobs/UpdateUserIdenticonStatus.php create mode 100644 database/migrations/2025_09_11_152525_add_has_identicon_to_users_table.php diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 3f7e292f6..5dd5b70c3 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; @@ -70,6 +71,8 @@ public function register(RegisterRequest $request) protected function create(RegisterRequest $request): User { $this->dispatchSync(RegisterUser::fromRequest($request)); + $user = User::findByEmailAddress($request->emailAddress()); + $this->dispatch(new UpdateUserIdenticonStatus($user)); return User::findByEmailAddress($request->emailAddress()); } diff --git a/app/Jobs/UpdateUserIdenticonStatus.php b/app/Jobs/UpdateUserIdenticonStatus.php new file mode 100644 index 000000000..ebfd92fe9 --- /dev/null +++ b/app/Jobs/UpdateUserIdenticonStatus.php @@ -0,0 +1,24 @@ +hasIdenticon($this->user->githubId()); + $this->user->update(['has_identicon' => $hasIdenticon]); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index c43b35dad..7efbd3f33 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', + 'has_identicon' ]; /** @@ -113,6 +114,11 @@ public function githubUsername(): string return $this->github_username ?? ''; } + public function hasIdenticon(): bool + { + return (bool) $this->has_identicon; + } + public function twitter(): ?string { return $this->twitter; diff --git a/app/Social/GithubUserApi.php b/app/Social/GithubUserApi.php index e7580ca21..2dae363bb 100644 --- a/app/Social/GithubUserApi.php +++ b/app/Social/GithubUserApi.php @@ -14,4 +14,25 @@ public function find(int|string $id): ?GitHubUser return $response->failed() ? null : new GitHubUser($response->json()); } + + public function hasIdenticon(int|string $id): bool + { + $detectionSize = 40; + $response = Http::retry(3, 300, fn ($exception) => $exception instanceof ConnectionException) + ->get("https://avatars.githubusercontent.com/u/{$id}?v=4&s={$detectionSize}"); + + if ($response->failed()) { + return true; + } + + $info = getimagesizefromstring($response->body()); + + if (!$info) { + return true; + } + + [$width, $height] = $info; + + return !($width === 420 && $height === 420); + } } 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..af5ed07a0 --- /dev/null +++ b/database/migrations/2025_09_11_152525_add_has_identicon_to_users_table.php @@ -0,0 +1,16 @@ +boolean('has_identicon')->after('bio')->default(false); + }); + } + +}; diff --git a/resources/views/components/avatar.blade.php b/resources/views/components/avatar.blade.php index ac27d4f23..4ead5a2cd 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'); ?> From 2419ca02b43a76ac5ee021e61d016a6b21f59eb8 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Fri, 12 Sep 2025 10:49:37 +0200 Subject: [PATCH 2/2] wip --- app/Console/Commands/BackfillIdenticons.php | 39 +++++++++++++++++++ .../Controllers/Auth/RegisterController.php | 4 +- app/Http/Controllers/HomeController.php | 2 +- app/Jobs/UnVerifyAuthor.php | 2 +- app/Jobs/UpdateUserIdenticonStatus.php | 5 ++- app/Jobs/VerifyAuthor.php | 2 +- app/Models/User.php | 10 ++++- app/Social/GithubUserApi.php | 11 ++---- database/factories/UserFactory.php | 1 + ...52525_add_has_identicon_to_users_table.php | 3 +- database/seeders/UserSeeder.php | 2 +- resources/views/components/avatar.blade.php | 2 +- 12 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 app/Console/Commands/BackfillIdenticons.php 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 5dd5b70c3..97d415547 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -71,9 +71,11 @@ public function register(RegisterRequest $request) protected function create(RegisterRequest $request): User { $this->dispatchSync(RegisterUser::fromRequest($request)); + $user = User::findByEmailAddress($request->emailAddress()); + $this->dispatch(new UpdateUserIdenticonStatus($user)); - return User::findByEmailAddress($request->emailAddress()); + 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 index ebfd92fe9..b863e52aa 100644 --- a/app/Jobs/UpdateUserIdenticonStatus.php +++ b/app/Jobs/UpdateUserIdenticonStatus.php @@ -14,11 +14,12 @@ final class UpdateUserIdenticonStatus implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(private User $user) {} + public function __construct(protected User $user) {} public function handle(GithubUserApi $github): void { $hasIdenticon = $github->hasIdenticon($this->user->githubId()); - $this->user->update(['has_identicon' => $hasIdenticon]); + + $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 7efbd3f33..ab8ef3fac 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -58,7 +58,7 @@ final class User extends Authenticatable implements MustVerifyEmail 'remember_token', 'bio', 'banned_reason', - 'has_identicon' + 'github_has_identicon' ]; /** @@ -76,6 +76,7 @@ protected function casts(): array return [ 'allowed_notifications' => 'array', 'author_verified_at' => 'datetime', + 'github_has_identicon' => 'boolean', ]; } @@ -116,7 +117,7 @@ public function githubUsername(): string public function hasIdenticon(): bool { - return (bool) $this->has_identicon; + return (bool) $this->github_has_identicon; } public function twitter(): ?string @@ -417,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 2dae363bb..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 { @@ -17,22 +17,19 @@ public function find(int|string $id): ?GitHubUser public function hasIdenticon(int|string $id): bool { - $detectionSize = 40; $response = Http::retry(3, 300, fn ($exception) => $exception instanceof ConnectionException) - ->get("https://avatars.githubusercontent.com/u/{$id}?v=4&s={$detectionSize}"); + ->get("https://avatars.githubusercontent.com/u/{$id}?v=4&s=40"); if ($response->failed()) { return true; } - $info = getimagesizefromstring($response->body()); - - if (!$info) { + if (! $info = getimagesizefromstring($response->body())) { return true; } [$width, $height] = $info; - return !($width === 420 && $height === 420); + 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 index af5ed07a0..dedb4763d 100644 --- 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 @@ -9,8 +9,7 @@ public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->boolean('has_identicon')->after('bio')->default(false); + $table->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 4ead5a2cd..db261e951 100644 --- a/resources/views/components/avatar.blade.php +++ b/resources/views/components/avatar.blade.php @@ -4,7 +4,7 @@ ]) githubId() && !$user->hasIdenticon() +$src = $user->githubId() && ! $user->hasIdenticon() ? sprintf('https://avatars.githubusercontent.com/u/%s', $user->githubId()) : asset('https://laravel.io/images/laravelio-icon-gray.svg'); ?>