diff --git a/.github/workflows/deploy_on_forge.yml b/.github/workflows/deploy_on_forge.yml deleted file mode 100644 index 561d1b8..0000000 --- a/.github/workflows/deploy_on_forge.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: 'Deploy on push' - -on: - push: - branches: - - master - -jobs: - forge-deploy: - name: 'Laravel Forge Deploy' - runs-on: ubuntu-latest - environment: production - - steps: - # Trigger Laravel Forge Deploy - - name: Deploy - uses: jbrooksuk/laravel-forge-action@v1.0.2 - with: - trigger_url: ${{ secrets.TRIGGER_URL }} diff --git a/app/Http/Controllers/Api/HighscoreController.php b/app/Http/Controllers/Api/HighscoreController.php index c712342..9541e9c 100644 --- a/app/Http/Controllers/Api/HighscoreController.php +++ b/app/Http/Controllers/Api/HighscoreController.php @@ -8,6 +8,12 @@ use App\Models\Highscore; use Carbon\CarbonImmutable; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; +use swentel\nostr\Filter\Filter; +use swentel\nostr\Message\RequestMessage; +use swentel\nostr\Relay\Relay; +use swentel\nostr\Relay\RelaySet; +use swentel\nostr\Request\Request; +use swentel\nostr\Subscription\Subscription; class HighscoreController extends Controller { @@ -50,6 +56,14 @@ class HighscoreController extends Controller $highscore->save(); + if (empty($highscore->name)) { + $fetchedName = $this->fetchNostrName($highscore->npub); + if ($fetchedName) { + $highscore->name = $fetchedName; + $highscore->save(); + } + } + Log::info('Highscore submission received', [ 'npub' => $highscore->npub, 'name' => $highscore->name, @@ -69,4 +83,64 @@ class HighscoreController extends Controller ], ], 202); } + + protected function fetchNostrName(string $npub): ?string + { + $author = trim($npub); + + if (! str_starts_with($author, 'npub1')) { + return null; + } + + $subscription = new Subscription; + $filter = new Filter; + $filter->setAuthors([$author]); + $filter->setKinds([0]); + + $requestMessage = new RequestMessage($subscription->getId(), [$filter]); + $relaySet = new RelaySet; + $relaySet->setRelays([ + new Relay('wss://nos.lol'), + ]); + + $request = new Request($relaySet, $requestMessage); + + try { + $response = $request->send(); + + foreach ($response as $relayUrl => $relayResponses) { + foreach ($relayResponses as $message) { + if (! isset($message->event)) { + continue; + } + + try { + $profile = json_decode($message->event->content, true, 512, JSON_THROW_ON_ERROR); + + if (isset($profile['name']) && is_string($profile['name']) && $profile['name'] !== '') { + Log::info('Fetched nostr profile name for highscore', [ + 'npub' => $author, + 'relay' => $relayUrl, + ]); + + return $profile['name']; + } + } catch (\JsonException $e) { + Log::warning('Failed to decode nostr profile for highscore', [ + 'npub' => $author, + 'relay' => $relayUrl, + 'error' => $e->getMessage(), + ]); + } + } + } + } catch (\Throwable $e) { + Log::warning('Failed to fetch nostr profile for highscore', [ + 'npub' => $author, + 'error' => $e->getMessage(), + ]); + } + + return null; + } } diff --git a/routes/api.php b/routes/api.php index 3963db9..84aa625 100644 --- a/routes/api.php +++ b/routes/api.php @@ -21,6 +21,8 @@ Route::middleware([]) Route::resource('courses', CourseController::class); Route::resource('cities', CityController::class); Route::resource('venues', VenueController::class); + Route::get('highscores', [HighscoreController::class, 'index'])->name('highscores.index'); + Route::post('highscores', [HighscoreController::class, 'store'])->name('highscores.store'); Route::get('nostrplebs', function () { return User::query() ->select([ @@ -185,6 +187,3 @@ Route::get('/lnurl-auth-callback', [\App\Http\Controllers\LnurlAuthController::c Route::post('/check-auth-error', [\App\Http\Controllers\LnurlAuthController::class, 'checkError']) ->name('auth.check-error'); - -Route::get('highscores', [HighscoreController::class, 'index'])->name('highscores.index'); -Route::post('highscores', [HighscoreController::class, 'store'])->name('highscores.store'); diff --git a/tests/Feature/HighscoreApiTest.php b/tests/Feature/HighscoreApiTest.php index a7c9431..7c75c3d 100644 --- a/tests/Feature/HighscoreApiTest.php +++ b/tests/Feature/HighscoreApiTest.php @@ -1,5 +1,6 @@ assertSame(1, Highscore::query()->count()); }); +test('missing name is fetched from nostr when available', function () { + $fetchedName = 'Fetched Player'; + + $controllerMock = \Mockery::mock(HighscoreController::class)->makePartial(); + $controllerMock->shouldAllowMockingProtectedMethods(); + $controllerMock->shouldReceive('fetchNostrName')->once()->andReturn($fetchedName); + + app()->instance(HighscoreController::class, $controllerMock); + + $payload = [ + 'npub' => 'npub1fetchnamevalue', + 'satoshis' => 1337, + 'blocks' => 2, + 'datetime' => CarbonImmutable::now()->subMinute()->toIso8601String(), + ]; + + $response = $this->postJson(route('api.highscores.store'), $payload); + + $response->assertAccepted() + ->assertJsonPath('data.name', $fetchedName); + + $this->assertDatabaseHas('highscores', [ + 'npub' => $payload['npub'], + 'name' => $fetchedName, + 'satoshis' => $payload['satoshis'], + 'blocks' => $payload['blocks'], + ]); +}); + test('highscore submission does not clear existing name when omitted', function () { $datetime = CarbonImmutable::now()->subMinutes(15);