first copies from portal

This commit is contained in:
fsociety
2024-09-04 19:37:46 +02:00
parent b38f3f8bed
commit a0ef037b2d
741 changed files with 78623 additions and 387 deletions

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands\Einundzwanzig;
use App\Models\EinundzwanzigPleb;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use swentel\nostr\Key\Key;
class SyncPlebs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sync:plebs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$response = Http::get('https://portal.einundzwanzig.space/api/nostrplebs');
$plebs = $response->json();
foreach ($plebs as $pleb) {
$npub = str($pleb)->trim();
EinundzwanzigPleb::updateOrCreate(
['npub' => $npub],
['pubkey' => (new Key())->convertToHex($npub)]
);
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Console\Commands\Nostr;
use App\Models\Event;
use App\Traits\NostrEventRendererTrait;
use Illuminate\Console\Command;
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 FetchEvents extends Command
{
use NostrEventRendererTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'fetch:events';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$plebs = \App\Models\EinundzwanzigPleb::query()
->get();
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setKinds([1]); // You can add multiple kind numbers
$filter1->setAuthors($plebs->pluck('pubkey')->toArray()); // You can add multiple authors
$filter1->setLimit(25); // Limit to fetch only a maximum of 25 events
$filters = [$filter1]; // You can add multiple filters.
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relays = [
new Relay('wss://nostr.einundzwanzig.space'),
new Relay('wss://nostr.wine'),
new Relay('wss://nos.lol'),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);
$request = new Request($relaySet, $requestMessage);
$response = $request->send();
$uniqueEvents = [];
foreach ($response as $relay => $events) {
foreach ($events as $event) {
if (!isset($uniqueEvents[$event->event->id])) {
$uniqueEvents[$event->event->id] = $event;
}
}
}
foreach ($uniqueEvents as $id => $uniqueEvent) {
$type = $this->isReplyOrRoot($uniqueEvent->event);
$parentEventId = $this->getParentEventId($uniqueEvent->event);
$event = Event::query()->updateOrCreate(
['event_id' => $id],
[
'pubkey' => $uniqueEvent->event->pubkey,
'parent_event_id' => $parentEventId,
'json' => json_encode($uniqueEvent->event, JSON_THROW_ON_ERROR),
'type' => $type,
]
);
$this->renderContentToHtml($event);
}
}
private function getParentEventId($event)
{
foreach ($event->tags as $tag) {
if ($tag[0] === 'e') {
if ((isset($tag[2]) && $tag[2] === '') || (isset($tag[3]) && $tag[3] === 'reply')) {
return $tag[1];
}
}
}
return null;
}
private function isReplyOrRoot($event)
{
foreach ($event->tags as $tag) {
if ($tag[0] === 'e') {
if ((isset($tag[3]) && $tag[3] === 'reply') || (!isset($tag[3]) && isset($tag[2]) && $tag[2] === '')) {
return 'reply';
}
}
}
return 'root';
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands\Nostr;
use App\Traits\NostrEventRendererTrait;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Broadcast;
class RenderAllEvents extends Command
{
use NostrEventRendererTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'render';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$events = \App\Models\Event::query()
->get();
foreach ($events as $event) {
$this->renderContentToHtml($event);
}
Broadcast::on('events')
->as('newEvents')
->with([
'test' => 'test',
])
->sendNow();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands\Nostr;
use App\Models\EinundzwanzigPleb;
use App\Traits\NostrFetcherTrait;
use Illuminate\Console\Command;
use swentel\nostr\Subscription\Subscription;
class SyncProfiles extends Command
{
use NostrFetcherTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sync:profiles';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$plebs = EinundzwanzigPleb::query()
->whereDoesntHave('profile')
->get();
$this->fetchProfile($plebs->pluck('npub')->toArray());
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Livewire;
use App\Models\Meetup;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column;
use PowerComponents\LivewirePowerGrid\Exportable;
use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\Rule;
use PowerComponents\LivewirePowerGrid\Footer;
use PowerComponents\LivewirePowerGrid\Header;
use PowerComponents\LivewirePowerGrid\Lazy;
use PowerComponents\LivewirePowerGrid\PowerGrid;
use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
final class MeetupTable extends PowerGridComponent
{
public function setUp(): array
{
$this->showCheckBox();
return [
Exportable::make('export')
->striped()
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV),
Footer::make()
->showPerPage(perPage: 25)
->showRecordCount(),
];
}
public function datasource(): Builder
{
return Meetup::query();
}
public function fields(): PowerGridFields
{
return PowerGrid::fields()
->add('name')
->add('name_lower', fn (Meetup $model) => strtolower(e($model->name)))
->add('created_at')
->add('created_at_formatted', fn (Meetup $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s'));
}
public function columns(): array
{
return [
Column::make('Name', 'name')
->searchable()
->sortable(),
Column::action('Action')
];
}
public function filters(): array
{
return [
Filter::inputText('name'),
Filter::datepicker('created_at_formatted', 'created_at'),
];
}
#[\Livewire\Attributes\On('edit')]
public function edit($rowId): void
{
$this->js('alert('.$rowId.')');
}
public function actions(Meetup $row): array
{
return [
Button::add('edit')
->slot('Edit: '.$row->id)
->id()
->class('pg-btn-white dark:ring-pg-primary-600 dark:border-pg-primary-600 dark:hover:bg-pg-primary-700 dark:ring-offset-pg-primary-800 dark:text-pg-primary-300 dark:bg-pg-primary-700')
->dispatch('edit', ['rowId' => $row->id])
];
}
/*
public function actionRules(Meetup $row): array
{
return [
// Hide button edit for ID 1
Rule::button('edit')
->when(fn($row) => $row->id === 1)
->hide(),
];
}
*/
}

32
app/Models/Category.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Category extends Model
{
protected $connection = 'einundzwanzig';
/**
* 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',
];
public function courses(): BelongsToMany
{
return $this->belongsToMany(Course::class);
}
}

83
app/Models/City.php Normal file
View File

@@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Akuechler\Geoly;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Cookie;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class City extends Model
{
use HasSlug;
use Geoly;
protected $connection = 'einundzwanzig';
/**
* 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',
'country_id' => 'integer',
'osm_relation' => 'json',
'simplified_geojson' => 'json',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
/**
* Get the options for generating the slug.
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['country.code', 'name'])
->saveSlugsTo('slug')
->usingLanguage(Cookie::get('lang', config('app.locale')));
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function country(): BelongsTo
{
return $this->belongsTo(Country::class);
}
public function venues(): HasMany
{
return $this->hasMany(Venue::class);
}
public function courseEvents()
{
return $this->hasManyThrough(CourseEvent::class, Venue::class);
}
public function meetups()
{
return $this->hasMany(Meetup::class);
}
}

33
app/Models/Country.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Country extends Model
{
protected $connection = 'einundzwanzig';
/**
* 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',
'language_codes' => 'array',
];
public function cities(): HasMany
{
return $this->hasMany(City::class);
}
}

89
app/Models/Course.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Image\Enums\Fit;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Tags\HasTags;
class Course extends Model implements HasMedia
{
use InteractsWithMedia;
use HasTags;
protected $connection = 'einundzwanzig';
/**
* 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',
'lecturer_id' => 'integer',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Fit::Crop, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Fit::Crop, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('logo')
->singleFile()
->useFallbackUrl(asset('img/einundzwanzig.png'));
$this->addMediaCollection('images')
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
public function lecturer(): BelongsTo
{
return $this->belongsTo(Lecturer::class);
}
public function courseEvents(): HasMany
{
return $this->hasMany(CourseEvent::class);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class CourseEvent extends Model
{
protected $connection = 'einundzwanzig';
/**
* 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',
'course_id' => 'integer',
'venue_id' => 'integer',
'from' => 'datetime',
'to' => 'datetime',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function course(): BelongsTo
{
return $this->belongsTo(Course::class);
}
public function venue(): BelongsTo
{
return $this->belongsTo(Venue::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EinundzwanzigPleb extends Model
{
protected $guarded = [];
public function profile()
{
return $this->hasOne(Profile::class, 'pubkey', 'pubkey');
}
}

17
app/Models/Event.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
protected $guarded = [];
public function renderedEvent()
{
return $this->hasOne(RenderedEvent::class, 'event_id', 'event_id');
}
}

99
app/Models/Lecturer.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Support\Facades\Cookie;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Lecturer extends Model implements HasMedia
{
use HasSlug;
use InteractsWithMedia;
protected $connection = 'einundzwanzig';
/**
* 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',
'active' => 'boolean',
];
protected static function booted()
{
static::creating(function ($model) {
if (!$model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Fit::Crop, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Fit::Crop, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('avatar')
->singleFile()
->useFallbackUrl(asset('img/einundzwanzig.png'));
$this->addMediaCollection('images')
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
/**
* Get the options for generating the slug.
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['name'])
->saveSlugsTo('slug')
->usingLanguage(Cookie::get('lang', config('app.locale')));
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function courses(): HasMany
{
return $this->hasMany(Course::class);
}
public function coursesEvents(): HasManyThrough
{
return $this->hasManyThrough(CourseEvent::class, Course::class);
}
public function libraryItems(): HasMany
{
return $this->hasMany(LibraryItem::class);
}
}

136
app/Models/Meetup.php Normal file
View File

@@ -0,0 +1,136 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Cookie;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Meetup extends Model implements HasMedia
{
use InteractsWithMedia;
use HasSlug;
protected $connection = 'einundzwanzig';
/**
* 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',
'city_id' => 'integer',
'github_data' => 'json',
'simplified_geojson' => 'array',
];
protected static function booted()
{
static::creating(function ($model) {
if (!$model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['name'])
->saveSlugsTo('slug')
->usingLanguage(Cookie::get('lang', config('app.locale')));
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Fit::Crop, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Fit::Crop, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('logo')
->singleFile()
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function users()
{
return $this->belongsToMany(User::class);
}
public function city(): BelongsTo
{
return $this->belongsTo(City::class);
}
protected function logoSquare(): Attribute
{
$media = $this->getFirstMedia('logo');
if ($media) {
$path = str($media->getPath())->after('storage/app/');
} else {
$path = 'img/einundzwanzig.png';
}
return Attribute::make(
get: fn() => url()->route('img',
[
'path' => $path,
'w' => 900,
'h' => 900,
'fit' => 'crop',
'fm' => 'webp',
]),
);
}
protected function nextEvent(): Attribute
{
$nextEvent = $this->meetupEvents()->where('start', '>=', now())->orderBy('start')->first();
return Attribute::make(
get: fn() => $nextEvent ? [
'start' => $nextEvent->start->toDateTimeString(),
'portalLink' => url()->route('meetup.event.landing', ['country' => $this->city->country, 'meetupEvent' => $nextEvent]),
'location' => $nextEvent->location,
'description' => $nextEvent->description,
'link' => $nextEvent->link,
'attendees' => count($nextEvent->attendees ?? []),
'nostr_note' => str($nextEvent->nostr_status)->after('Sent event ')->before(' to '),
] : null,
);
}
public function meetupEvents(): HasMany
{
return $this->hasMany(MeetupEvent::class);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MeetupEvent extends Model
{
protected $connection = 'einundzwanzig';
/**
* 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',
'meetup_id' => 'integer',
'start' => 'datetime',
'attendees' => 'array',
'might_attendees' => 'array',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function meetup(): BelongsTo
{
return $this->belongsTo(Meetup::class);
}
}

12
app/Models/Profile.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Profile extends Model
{
protected $guarded = [];
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class RenderedEvent extends Model
{
protected $guarded = [];
public function event()
{
return $this->belongsTo(Event::class, 'event_id', 'event_id');
}
}

104
app/Models/Venue.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Cookie;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
class Venue extends Model implements HasMedia
{
use HasSlug;
use HasRelationships;
use InteractsWithMedia;
protected $connection = 'einundzwanzig';
/**
* 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',
'city_id' => 'integer',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Fit::Crop, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Fit::Crop, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('images')
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
/**
* Get the options for generating the slug.
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['city.slug', 'name'])
->saveSlugsTo('slug')
->usingLanguage(Cookie::get('lang', config('app.locale')));
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function city(): BelongsTo
{
return $this->belongsTo(City::class);
}
public function lecturers()
{
return $this->hasManyDeepFromRelations($this->courses(), (new Course())->lecturer());
}
public function courses()
{
return $this->hasManyDeepFromRelations($this->events(), (new CourseEvent())->course());
}
public function courseEvents(): HasMany
{
return $this->hasMany(CourseEvent::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Folio\Folio;
class FolioServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Folio::path(resource_path('views/pages'))->middleware([
'*' => [
//
],
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Traits;
use App\Models\Event;
use App\Models\Profile;
use App\Models\RenderedEvent;
use swentel\nostr\Key\Key;
trait NostrEventRendererTrait
{
public function renderContentToHtml(Event $event): void
{
$content = json_decode($event->json, true, 512, JSON_THROW_ON_ERROR)['content'];
$profile = Profile::query()->where('pubkey', $event->pubkey)->first();
if ($profile && $profile->name) {
$name = $profile->name;
} elseif ($profile && !empty($profile->display_name)) {
$name = $profile->display_name;
} else {
$name = 'Anonymous';
}
$content = $this->nprofile1($content);
$content = $this->images($content);
$content = $this->youtube($content);
$content = $this->npub1($content);
RenderedEvent::query()->updateOrCreate([
'event_id' => $event->event_id,
], [
'html' => $content,
'profile_image' => $profile && $profile->picture !== '' ? $profile->picture : 'https://robohash.org/' . $profile->pubkey,
'profile_name' => $name,
]);
}
protected function images($content): string
{
// we need to find all image urls by looking for the extension
// and replace them with the img tag
$pattern = '/(https?:\/\/.*\.(?:png|jpg|jpeg|gif|webp))/';
$replacement = '<div class="w-96 group aspect-h-7 aspect-w-10"><img class="pointer-events-none object-cover" src="$1" alt="image" /></div>';
return preg_replace($pattern, $replacement, $content);
}
protected function npub1($content): string
{
// Pattern to match nostr:npub1 elements, optionally followed by a non-alphanumeric character
$pattern = '/(nostr:npub1[a-zA-Z0-9]+)(\W?)/';
// find all matches of the pattern
preg_match_all($pattern, $content, $matches);
// loop through all matches
foreach ($matches[1] as $match) {
$pubkey = (new Key)->convertToHex(str($match)->after('nostr:'));
$profile = Profile::query()->where('pubkey', $pubkey)->first();
if ($profile && $profile->name) {
$name = $profile->name;
} elseif ($profile && !empty($profile->display_name)) {
$name = $profile->display_name;
} else {
$name = 'Anonymous';
}
// replace the match with the profile name
$content = str_replace($match, $name, $content);
}
return $content;
}
protected function nprofile1($content): string
{
// todo: implement this
return $content;
}
protected function youtube($content): string
{
// Pattern to match YouTube short URLs like https://youtu.be/ddvHagjmRJY?feature=shared
$pattern1 = '/https:\/\/youtu.be\/([a-zA-Z0-9-_]+)\??.*/';
$replacement1 = '<iframe width="560" height="315" src="https://www.youtube.com/embed/$1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
// Pattern to match YouTube long URLs like https://www.youtube.com/watch?v=tiNZoDBGhdo
$pattern2 = '/https:\/\/www.youtube.com\/watch\?v=([a-zA-Z0-9-_]+)\??.*/';
$replacement2 = '<iframe width="560" height="315" src="https://www.youtube.com/embed/$1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
// Replace both patterns in the content
$content = preg_replace($pattern1, $replacement1, $content);
$content = preg_replace($pattern2, $replacement2, $content);
return $content;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Traits;
use App\Models\Profile;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key;
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;
trait NostrFetcherTrait
{
public function fetchProfile($npubs)
{
$hex = collect([]);
foreach ($npubs as $item) {
$hex->push([
'hex' => (new Key)->convertToHex($item),
'npub' => $item,
]);
}
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setKinds([0]); // You can add multiple kind numbers
$filter1->setAuthors($hex->pluck('hex')->toArray()); // You can add multiple author ids
$filters = [$filter1]; // You can add multiple filters.
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relayUrl = 'wss://relay.nostr.band/';
$relay = new Relay($relayUrl);
$relay->setMessage($requestMessage);
$request = new Request($relay, $requestMessage);
$response = $request->send();
foreach ($response['wss://relay.nostr.band/'] as $item) {
try {
$result = json_decode($item->event->content, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException('Error decoding JSON: ' . $e->getMessage());
}
Profile::query()->updateOrCreate(
['pubkey' => $item->event->pubkey],
[
'name' => $result['name'] ?? null,
'display_name' => $result['display_name'] ?? null,
'picture' => $result['picture'] ?? null,
'banner' => $result['banner'] ?? null,
'website' => $result['website'] ?? null,
'about' => $result['about'] ?? null,
'nip05' => $result['nip05'] ?? null,
'lud16' => $result['lud16'] ?? null,
'lud06' => $result['lud06'] ?? null,
'deleted' => $result['deleted'] ?? false,
]
);
}
}
}