mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
🔥 **Remove Highscore and Bindle features**
- 🗑️ Deleted `Highscore` feature (Model, Controller, Factory, Tests, Routes, Migrations) and associated logic. - 🗑️ Removed `BindleController` and its related test. - 🧹 Cleaned up unused routes, database seeders, and localization references. - 🚫 Deprecated inactive book rental guide component and associated views.
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LibraryItem;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
#[Group(name: 'Community', weight: 7)]
|
||||
class BindleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Bindles (Bibliotheks-Einträge) auflisten
|
||||
*
|
||||
* Liefert die Bibliothekseinträge vom Typ 'bindle' mit id, name, link und image.
|
||||
*
|
||||
* @return Collection<int, array{id: int, name: string, link: string, image: string}>
|
||||
*/
|
||||
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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreHighscoreRequest;
|
||||
use App\Models\Highscore;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Dedoc\Scramble\Attributes\Response;
|
||||
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;
|
||||
|
||||
#[Group(name: 'Highscores', weight: 6)]
|
||||
class HighscoreController extends Controller
|
||||
{
|
||||
/**
|
||||
* Highscore-Bestenliste abrufen
|
||||
*
|
||||
* Öffentliche Bestenliste des Spiels, absteigend nach Satoshis (dann nach Zeitpunkt).
|
||||
* Die Antwort hat die Form { data: [ { npub, name, satoshis, blocks, datetime } ] }.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6
|
||||
$highscores = Highscore::query()
|
||||
->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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreHighscoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function expectsJson(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\BooksForPlebs;
|
||||
|
||||
use App\Traits\SeoTrait;
|
||||
use Livewire\Component;
|
||||
use RalphJSmit\Laravel\SEO\Support\SEOData;
|
||||
|
||||
class BookRentalGuide extends Component
|
||||
{
|
||||
use SeoTrait;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.books-for-plebs.book-rental-guide')->with( [
|
||||
'SEOData' => new SEOData(
|
||||
title: __('BooksForPlebs'),
|
||||
description: __('Lokale Buchausleihe für Bitcoin-Meetups.'),
|
||||
image: asset('img/book-rental.jpg')
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Highscore extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'satoshis' => 'integer',
|
||||
'blocks' => 'integer',
|
||||
'achieved_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user