diff --git a/app/Http/Controllers/Api/BindleController.php b/app/Http/Controllers/Api/BindleController.php deleted file mode 100644 index a2e084f..0000000 --- a/app/Http/Controllers/Api/BindleController.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public function __invoke(): Collection - { - return LibraryItem::query() - ->where('type', 'bindle') - ->with([ - 'media', - ]) - ->orderByDesc('id') - ->get() - ->map(fn ($item) => [ - 'id' => $item->id, - 'name' => $item->name, - 'link' => strtok($item->value, '?'), - 'image' => $item->getFirstMediaUrl('main'), - ]); - } -} diff --git a/app/Http/Controllers/Api/HighscoreController.php b/app/Http/Controllers/Api/HighscoreController.php deleted file mode 100644 index f71bd68..0000000 --- a/app/Http/Controllers/Api/HighscoreController.php +++ /dev/null @@ -1,164 +0,0 @@ -orderByDesc('satoshis') - ->orderBy('achieved_at') - ->get() - ->map(fn (Highscore $highscore) => [ - 'npub' => $highscore->npub, - 'name' => $highscore->name, - 'satoshis' => $highscore->satoshis, - 'blocks' => $highscore->blocks, - 'datetime' => $highscore->achieved_at->toIso8601String(), - ]); - - return response()->json([ - 'data' => $highscores, - ]); - } - - /** - * Highscore einreichen - * - * Reicht einen Highscore ein (idempotent pro npub und Zeitpunkt). - * Zusätzlich auf 10 Anfragen pro Minute begrenzt. - * Fehlt ein Name, versucht der Server, ihn über das Nostr-Profil zu ergänzen. - * Antwortet mit HTTP 202. - */ - #[Response(status: 429, description: 'Zu viele Anfragen (Limit: 10 pro Minute überschritten).')] - public function store(StoreHighscoreRequest $request): JsonResponse - { - $validated = $request->validated(); - $achievedAt = CarbonImmutable::parse($validated['datetime']); - - $highscore = Highscore::query()->firstOrNew([ - 'npub' => $validated['npub'], - 'achieved_at' => $achievedAt, - ]); - - $highscore->satoshis = (int) $validated['satoshis']; - $highscore->blocks = (int) $validated['blocks']; - - if (array_key_exists('name', $validated)) { - $highscore->name = $validated['name']; - } - - $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, - 'satoshis' => $highscore->satoshis, - 'blocks' => $highscore->blocks, - 'datetime' => $highscore->achieved_at->toIso8601String(), - ]); - - return response()->json([ - 'message' => 'Highscore received', - 'data' => [ - 'npub' => $highscore->npub, - 'name' => $highscore->name, - 'satoshis' => $highscore->satoshis, - 'blocks' => $highscore->blocks, - 'datetime' => $highscore->achieved_at->toIso8601String(), - ], - ], 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/app/Http/Requests/StoreHighscoreRequest.php b/app/Http/Requests/StoreHighscoreRequest.php deleted file mode 100644 index cef6520..0000000 --- a/app/Http/Requests/StoreHighscoreRequest.php +++ /dev/null @@ -1,52 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'npub' => ['required', 'string', 'starts_with:npub1', 'max:100'], - 'name' => ['nullable', 'string', 'max:255'], - 'satoshis' => ['required', 'integer', 'min:0'], - 'blocks' => ['required', 'integer', 'min:0'], - 'datetime' => ['required', 'date'], - ]; - } - - public function messages(): array - { - return [ - 'npub.required' => 'An npub is required to record the highscore.', - 'npub.starts_with' => 'The npub must start with npub1.', - 'name.string' => 'The name must be a valid string.', - 'satoshis.required' => 'Please provide the earned satoshis amount.', - 'satoshis.integer' => 'Satoshis must be a whole number.', - 'blocks.required' => 'Please provide the number of blocks.', - 'blocks.integer' => 'Blocks must be a whole number.', - 'datetime.required' => 'Please provide when the score was achieved.', - 'datetime.date' => 'The datetime value must be a valid date.', - ]; - } -} diff --git a/app/Livewire/BooksForPlebs/BookRentalGuide.php b/app/Livewire/BooksForPlebs/BookRentalGuide.php deleted file mode 100644 index f3f18c4..0000000 --- a/app/Livewire/BooksForPlebs/BookRentalGuide.php +++ /dev/null @@ -1,23 +0,0 @@ -with( [ - 'SEOData' => new SEOData( - title: __('BooksForPlebs'), - description: __('Lokale Buchausleihe für Bitcoin-Meetups.'), - image: asset('img/book-rental.jpg') - ), - ]); - } -} diff --git a/app/Models/Highscore.php b/app/Models/Highscore.php deleted file mode 100644 index 19b6cfb..0000000 --- a/app/Models/Highscore.php +++ /dev/null @@ -1,30 +0,0 @@ - 'integer', - 'satoshis' => 'integer', - 'blocks' => 'integer', - 'achieved_at' => 'datetime', - ]; -} diff --git a/config/scramble.php b/config/scramble.php index 3caf5ed..17f4823 100644 --- a/config/scramble.php +++ b/config/scramble.php @@ -64,8 +64,7 @@ return [ ## Rate Limiting - Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt, das Einreichen von - Highscores zusätzlich auf **10 Anfragen/Minute**. + Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt. MARKDOWN, ], @@ -96,7 +95,7 @@ return [ 'view' => 'scramble::scalar', 'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference', 'theme' => 'laravel', - 'proxyUrl' => 'https://proxy.scalar.com', + 'proxyUrl' => '', 'darkMode' => true, 'showDeveloperTools' => 'never', 'agent' => ['disabled' => true], diff --git a/database/factories/HighscoreFactory.php b/database/factories/HighscoreFactory.php deleted file mode 100644 index 956ad6b..0000000 --- a/database/factories/HighscoreFactory.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ -class HighscoreFactory extends Factory -{ - protected $model = Highscore::class; - - public function definition(): array - { - return [ - 'npub' => NostrHelper::randomNpub(), - 'name' => fake()->name(), - 'satoshis' => fake()->numberBetween(0, 100000), - 'blocks' => fake()->numberBetween(0, 1000), - 'achieved_at' => fake()->dateTimeBetween('-1 year', 'now'), - ]; - } -} diff --git a/database/migrations/2026_02_02_105837_create_highscores_table.php b/database/migrations/2026_02_02_105837_create_highscores_table.php deleted file mode 100644 index ab8ddf7..0000000 --- a/database/migrations/2026_02_02_105837_create_highscores_table.php +++ /dev/null @@ -1,35 +0,0 @@ -id(); - $table->string('npub', 100); - $table->string('name')->nullable(); - $table->unsignedBigInteger('satoshis'); - $table->unsignedInteger('blocks'); - $table->dateTime('achieved_at'); - $table->timestamps(); - - $table->unique(['npub', 'achieved_at']); - $table->index('satoshis'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('highscores'); - } -}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index aa82402..ef8abad 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -11,7 +11,6 @@ use App\Models\CourseEvent; use App\Models\EmailCampaign; use App\Models\EmailTexts; use App\Models\Episode; -use App\Models\Highscore; use App\Models\Lecturer; use App\Models\Library; use App\Models\LibraryItem; @@ -28,7 +27,6 @@ use App\Models\TwitterAccount; use App\Models\User; use App\Models\Venue; use App\Models\Vote; -use Database\Factories\Helpers\NostrHelper; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\DB; @@ -167,7 +165,7 @@ class DatabaseSeeder extends Seeder } }); - $this->command->info('Phase 6: Voting & Highscores'); + $this->command->info('Phase 6: Voting'); $proposals->each(function (ProjectProposal $proposal) use ($users) { foreach ($users->random(min(8, $users->count())) as $voter) { Vote::create([ @@ -180,17 +178,6 @@ class DatabaseSeeder extends Seeder } }); - foreach (NostrHelper::realNpubs() as $i => $npub) { - for ($d = 0; $d < 5; $d++) { - Highscore::factory()->create([ - 'npub' => $npub, - 'achieved_at' => now()->subDays(($i * 10) + $d), - 'satoshis' => fake()->numberBetween(1000, 1_000_000), - 'blocks' => fake()->numberBetween(1, 5000), - ]); - } - } - $this->command->info('Phase 7: LoginKeys'); LoginKey::factory()->count(5)->recycle($users)->create(); diff --git a/resources/views/livewire/books-for-plebs/book-rental-guide.blade.php b/resources/views/livewire/books-for-plebs/book-rental-guide.blade.php deleted file mode 100644 index 41e087b..0000000 --- a/resources/views/livewire/books-for-plebs/book-rental-guide.blade.php +++ /dev/null @@ -1,213 +0,0 @@ -
-
- -
-

- Anleitung zum Bücherverleih -

- Buch Etiketten -
- -

- Hallo Pleb, -

- -

- Vielen Dank, dass du dich dazu entschieden hast, deine - - ₿itcoin-Bücher - - zur Verfügung zu stellen. Mit dieser Anleitung kannst du eine Bezahladresse - generieren und hast auch alle Materialien, die du benötigst. Wir haben - darauf geachtet, dass es für jedes Meetup geeignet ist. Deshalb stellen - wir dir die Quelldateien zur Verfügung, damit du deinen eigenen QR-Code - einfügen und das Logo eures Meetups verwenden kannst. -
-

- Du hast keine ₿itcoin Wallet oder kein Programm zum Bearbeiten der Dateien? - Kein Problem! Schreib uns einfach und wir helfen dir. -

-
- - - - @Awesomo12 - -
- - - - @LottiTheFuchs - -
- - - - @Robin_Hodl21 - -

-
- - -
-
-

- Bücheretiketten -

-

- (Zum Editieren brauchst du - - Adobe Illustrator) - -

- Buch Etiketten - -
- - -
-

- Flyer -

-

- (Zum Editieren brauchst du - - Adobe Illustrator) - -

- Flyer - -
- - - - -
-

- Lesezeichen -

-

- (Nicht editierbar) -

- Flyer - -
- - Download .jpg - -
-
- - -
-

- Bookring4Sats -

-

- (Nicht editierbar) -

- Flyer - -
- - Download .jpg - -
-
-
- -

- Um deinen - - ₿itcoin - - QR-Code zu erstellen, kopiere einfach die Empfangsadresse aus der Wallet - deiner Wahl und füge sie hier ein: -
- - www.qr-code-generator.com - -

- -

- Der QR-Code-Generator akzeptiert sowohl Lightning als auch Onchain-Empfangsadressen. - Wir empfehlen dir jedoch Lightning zu verwenden, da es schneller und zumeist deutlich günstiger als Onchain ist. -
- - - - Vorsicht: Bitte KEINE Lightning-Adressen,verwenden, da diese nach einem bestimmten Zeitraum ablaufen! - Stattdessen nutzt Ihr bitte eine LNURL, da diese Statisch sind. - - - Tipp: Der Lightning TipBot "LN.tips" erzeugt euch eine. Dazu müsst Ihr diesen nur aktivieren und mit dem Befehl - "/advanced" eure LNURL anzeigen lassen und diese copy-pasten. - Alternativ kann dies auch die WalletOfSatoshi, dort sehen Sie aus wie E-Mail Adessen, wie zum Beispiel: - "BitcoinKalle@walletofsatoshi.com" - -

- -

- Für die sichere Lagerung deiner Bücher empfehlen wir einen Meetup-Ort, - an dem du regelmäßig bist und die Bücher auch sicher verstaut werden können. - Falls dem nicht der Fall ist, solltet Ihr die Bücher lieber jedes mal separat mit zum Meetup - nehmen und die nicht verliehenen Bücher auch wieder mit zurück. -

- -

- Du willst deine Bücher nicht nur deinem lokalen Meetup zur Verfügung stellen, - sondern online an die gesamte Community verschicken, dann komm in die Gruppe: -

- -

- -
- - - @BOOKRING4SATS -
-

- -

- - - Vielen Dank, dass du deine Bücher zur Verfügung stellst und uns dabei - hilfst, das Wissen über ₿itcoin zu verbreiten! -

- -
- - - Happy Stacking - -
- -
-
diff --git a/routes/api.php b/routes/api.php index 01bb577..a25c74f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,12 +1,10 @@ only(['index', 'show']); 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']) - ->middleware('throttle:10,1') - ->name('highscores.store'); Route::get('nostrplebs', NostrPlebController::class); - Route::get('bindles', BindleController::class); Route::get('meetups', MeetupMapController::class); Route::get('meetup-events/{date?}', MeetupEventController::class); Route::get('btc-map-communities', BtcMapCommunityController::class); diff --git a/routes/web.php b/routes/web.php index 9aa1704..1ab4af1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,27 +16,6 @@ Route::get('error/{code}', function (string $code) { abort($code >= 400 && $code <= 599 ? $code : 404); })->where('code', '[0-9]{3}'); -/* - * Commented out routes related to book rental download and display - * These are currently inactive but can be enabled if needed - */ -/*Route::get('/download-buecherverleih', function (Request $request) { - $filename = $request->input('filename'); - // Get the file path from the storage folder - $filePath = storage_path('app/'.$filename); - dd($filePath); - // Check if the file exists - if (!file_exists($filePath)) { - abort(404); - } - // Generate a response with the file for download - return response()->download($filePath, $filename); -})->name('buecherverleih.download'); - -Route::middleware([]) - ->get('/buecherverleih', \App\Livewire\BooksForPlebs\BookRentalGuide::class) - ->name('buecherverleih');*/ - // Route for rabbit following helper page - Updated for Livewire v4 Route::livewire('/kaninchenbau', FollowTheRabbit::class) ->name('kaninchenbau'); diff --git a/tests/Feature/Api/HighscoreApiTest.php b/tests/Feature/Api/HighscoreApiTest.php deleted file mode 100644 index e22f7c4..0000000 --- a/tests/Feature/Api/HighscoreApiTest.php +++ /dev/null @@ -1,71 +0,0 @@ -create(['satoshis' => 100, 'achieved_at' => now()->subHours(1)]); - Highscore::factory()->create(['satoshis' => 5000, 'achieved_at' => now()->subHours(2)]); - Highscore::factory()->create(['satoshis' => 1000, 'achieved_at' => now()->subHours(3)]); - - $response = $this->getJson('/api/highscores'); - - $response->assertSuccessful(); - $data = $response->json('data'); - expect(collect($data)->pluck('satoshis')->all())->toBe([5000, 1000, 100]); -}); - -it('accepts a valid highscore submission', function () { - $payload = [ - 'npub' => 'npub1'.str_repeat('a', 58), - 'name' => 'Tester', - 'satoshis' => 1234, - 'blocks' => 5, - 'datetime' => now()->subDay()->toIso8601String(), - ]; - - $this->postJson('/api/highscores', $payload) - ->assertStatus(202) - ->assertJsonPath('data.satoshis', 1234) - ->assertJsonPath('data.name', 'Tester'); - - expect(Highscore::query()->where('npub', $payload['npub'])->exists())->toBeTrue(); -}); - -it('rejects a highscore submission missing npub', function () { - $this->postJson('/api/highscores', [ - 'satoshis' => 1234, - 'blocks' => 5, - 'datetime' => now()->toIso8601String(), - ])->assertUnprocessable() - ->assertJsonValidationErrors(['npub']); -}); - -it('rejects a highscore submission with an npub that does not start with npub1', function () { - $this->postJson('/api/highscores', [ - 'npub' => 'nsec1'.str_repeat('a', 58), - 'satoshis' => 1234, - 'blocks' => 5, - 'datetime' => now()->toIso8601String(), - ])->assertUnprocessable() - ->assertJsonValidationErrors(['npub']); -}); - -it('rejects a highscore submission with negative satoshis', function () { - $this->postJson('/api/highscores', [ - 'npub' => 'npub1'.str_repeat('b', 58), - 'satoshis' => -10, - 'blocks' => 5, - 'datetime' => now()->toIso8601String(), - ])->assertUnprocessable() - ->assertJsonValidationErrors(['satoshis']); -}); - -it('rejects a highscore submission with an invalid datetime', function () { - $this->postJson('/api/highscores', [ - 'npub' => 'npub1'.str_repeat('c', 58), - 'satoshis' => 100, - 'blocks' => 5, - 'datetime' => 'not-a-date', - ])->assertUnprocessable() - ->assertJsonValidationErrors(['datetime']); -}); diff --git a/tests/Feature/Api/JsonFeedTest.php b/tests/Feature/Api/JsonFeedTest.php index 25548c6..a1d39f5 100644 --- a/tests/Feature/Api/JsonFeedTest.php +++ b/tests/Feature/Api/JsonFeedTest.php @@ -1,6 +1,5 @@ toHaveCount(2) ->each->toStartWith('npub1'); }); - -it('returns bindle-type library items in /api/bindles', function () { - LibraryItem::factory()->create(['type' => 'bindle', 'name' => 'My Bindle']); - LibraryItem::factory()->create(['type' => 'article', 'name' => 'My Article']); - - $response = $this->getJson('/api/bindles'); - - $response->assertSuccessful(); - $names = collect($response->json())->pluck('name'); - expect($names->all()) - ->toContain('My Bindle') - ->not->toContain('My Article'); -}); diff --git a/tests/Feature/Livewire/BooksForPlebs/BookRentalGuideTest.php b/tests/Feature/Livewire/BooksForPlebs/BookRentalGuideTest.php deleted file mode 100644 index 2644a62..0000000 --- a/tests/Feature/Livewire/BooksForPlebs/BookRentalGuideTest.php +++ /dev/null @@ -1,14 +0,0 @@ - Livewire::test(BookRentalGuide::class)->assertStatus(200)) - ->toThrow(ViewException::class, 'Route [buecherverleih.download] not defined.'); -})->skip('Component is unreachable: /buecherverleih route is commented out in routes/web.php — view references the missing buecherverleih.download route.'); - -it('confirms the BookRentalGuide component class still exists', function () { - expect(class_exists(BookRentalGuide::class))->toBeTrue(); -}); diff --git a/tests/Feature/Models/FactoriesTest.php b/tests/Feature/Models/FactoriesTest.php index 3e3797f..b61e796 100644 --- a/tests/Feature/Models/FactoriesTest.php +++ b/tests/Feature/Models/FactoriesTest.php @@ -9,7 +9,6 @@ use App\Models\CourseEvent; use App\Models\EmailCampaign; use App\Models\EmailTexts; use App\Models\Episode; -use App\Models\Highscore; use App\Models\Lecturer; use App\Models\Library; use App\Models\LibraryItem; @@ -62,7 +61,6 @@ it('creates a valid persisted record via the factory', function (string $modelCl 'Participant' => Participant::class, 'EmailCampaign' => EmailCampaign::class, 'EmailTexts' => EmailTexts::class, - 'Highscore' => Highscore::class, 'LoginKey' => LoginKey::class, 'Tag' => Tag::class, ]); diff --git a/tests/Feature/Smoke/ApiRoutesTest.php b/tests/Feature/Smoke/ApiRoutesTest.php index 53a5b3c..8699522 100644 --- a/tests/Feature/Smoke/ApiRoutesTest.php +++ b/tests/Feature/Smoke/ApiRoutesTest.php @@ -4,9 +4,7 @@ use App\Models\City; use App\Models\Country; use App\Models\Course; use App\Models\CourseEvent; -use App\Models\Highscore; use App\Models\Lecturer; -use App\Models\LibraryItem; use App\Models\Meetup; use App\Models\MeetupEvent; use App\Models\User; @@ -21,8 +19,6 @@ beforeEach(function () { Course::factory()->create(); CourseEvent::factory()->create(); Lecturer::factory()->create(); - Highscore::factory()->create(); - LibraryItem::factory()->create(['type' => 'bindle']); User::factory()->create(['nostr' => 'npub1'.str_repeat('a', 58)]); }); @@ -34,12 +30,10 @@ it('returns a JSON response for the API GET endpoint', function (string $path) { 'meetup events' => '/api/meetup-events', 'btc-map communities' => '/api/btc-map-communities', 'nostrplebs' => '/api/nostrplebs', - 'bindles' => '/api/bindles', 'lecturers' => '/api/lecturers', 'courses' => '/api/courses', 'cities' => '/api/cities', 'venues' => '/api/venues', - 'highscores' => '/api/highscores', ]); it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () {