2 changed files with 243 additions and 0 deletions
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Controller; |
||||
|
||||
use App\Service\NostrClient; |
||||
use App\Service\RedisCacheService; |
||||
use Exception; |
||||
use nostriphant\NIP19\Bech32; |
||||
use nostriphant\NIP19\Data; |
||||
use Psr\Log\LoggerInterface; |
||||
use swentel\nostr\Key\Key; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
||||
use Symfony\Component\Routing\Attribute\Route; |
||||
|
||||
class EventController extends AbstractController |
||||
{ |
||||
/** |
||||
* @throws Exception |
||||
*/ |
||||
#[Route('/e/{nevent}', name: 'nevent', requirements: ['nevent' => '^nevent1.*'])] |
||||
public function index($nevent, NostrClient $nostrClient, RedisCacheService $redisCacheService, LoggerInterface $logger): Response |
||||
{ |
||||
$logger->info('Accessing event page', ['nevent' => $nevent]); |
||||
|
||||
try { |
||||
// Decode nevent - nevent1... is a NIP-19 encoded event identifier |
||||
$decoded = new Bech32($nevent); |
||||
$logger->info('Decoded event', ['decoded' => json_encode($decoded)]); |
||||
|
||||
// Get the event using the event ID |
||||
/** @var Data $data */ |
||||
$data = $decoded->data; |
||||
$logger->info('Event data', ['data' => json_encode($data)]); |
||||
|
||||
// Sort which event type this is using $data->type |
||||
switch ($decoded->type) { |
||||
case 'note': |
||||
// Handle note (regular event) |
||||
$relays = $data->relays ?? []; |
||||
$event = $nostrClient->getEventById($data->identifier, $relays); |
||||
break; |
||||
|
||||
case 'nprofile': |
||||
// Redirect to author profile if it's a profile identifier |
||||
$logger->info('Redirecting to author profile', ['pubkey' => $data->pubkey]); |
||||
return $this->redirectToRoute('author-redirect', ['pubkey' => $data->pubkey]); |
||||
|
||||
case 'nevent': |
||||
// Handle nevent identifier (event with additional metadata) |
||||
$relays = $data->relays ?? []; |
||||
$event = $nostrClient->getEventById($data->id, $relays); |
||||
break; |
||||
|
||||
case 'naddr': |
||||
// Handle naddr (parameterized replaceable event) |
||||
$decodedData = [ |
||||
'kind' => $data->kind, |
||||
'pubkey' => $data->pubkey, |
||||
'identifier' => $data->identifier, |
||||
'relays' => $data->relays ?? [] |
||||
]; |
||||
$event = $nostrClient->getEventByNaddr($decodedData); |
||||
break; |
||||
|
||||
default: |
||||
$logger->error('Unsupported event type', ['type' => $decoded->type]); |
||||
throw new NotFoundHttpException('Unsupported event type: ' . $decoded->type); |
||||
} |
||||
|
||||
if (!$event) { |
||||
$logger->warning('Event not found', ['data' => $data]); |
||||
throw new NotFoundHttpException('Event not found'); |
||||
} |
||||
|
||||
// If author is included in the event, get metadata |
||||
$authorMetadata = null; |
||||
if (isset($event->pubkey)) { |
||||
$key = new Key(); |
||||
$npub = $key->convertPublicKeyToBech32($event->pubkey); |
||||
$authorMetadata = $redisCacheService->getMetadata($npub); |
||||
} |
||||
// Render template with the event data |
||||
return $this->render('event/index.html.twig', [ |
||||
'event' => $event, |
||||
'author' => $authorMetadata |
||||
]); |
||||
|
||||
} catch (Exception $e) { |
||||
$logger->error('Error processing event', ['error' => $e->getMessage()]); |
||||
throw $e; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,145 @@
@@ -0,0 +1,145 @@
|
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block title %}Nostr Event{% endblock %} |
||||
|
||||
{% block body %} |
||||
<div class="container"> |
||||
<div class="event-container"> |
||||
<div class="event-header"> |
||||
{% if author %} |
||||
{% if author.image is defined %} |
||||
<img src="{{ author.image }}" class="avatar" alt="{{ author.name }}" onerror="this.style.display = 'none'" /> |
||||
{% endif %} |
||||
|
||||
<twig:Molecules:UserFromNpub ident="{{ event.pubkey }}" /> |
||||
<div> |
||||
{% if author.about is defined %} |
||||
{{ author.about|markdown_to_html|mentionify }} |
||||
{% endif %} |
||||
</div> |
||||
<hr /> |
||||
{% endif %} |
||||
<div class="event-meta"> |
||||
<span class="event-date">{{ event.created_at|date('F j, Y - H:i') }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="event-content"> |
||||
<twig:Atoms:Content :content="event.content" /> |
||||
</div> |
||||
|
||||
<div class="event-footer"> |
||||
<div class="event-tags"> |
||||
{% if event.tags is defined and event.tags|length > 0 %} |
||||
<h4>Tags:</h4> |
||||
<ul> |
||||
{% for tag in event.tags %} |
||||
{% if tag[0] != 'e' and tag[0] != 'p' %} |
||||
<li> |
||||
<strong>{{ tag[0] }}:</strong> {{ tag[1] }} |
||||
</li> |
||||
{% endif %} |
||||
{% endfor %} |
||||
</ul> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="event-references"> |
||||
{% if event.tags is defined %} |
||||
{% set references = [] %} |
||||
{% for tag in event.tags %} |
||||
{% if tag[0] == 'e' %} |
||||
{% set references = references|merge([tag[1]]) %} |
||||
{% endif %} |
||||
{% endfor %} |
||||
|
||||
{% if references|length > 0 %} |
||||
<h4>References:</h4> |
||||
<ul> |
||||
{% for ref in references %} |
||||
<li> |
||||
<a href="{{ path('nevent', {nevent: 'nevent1' ~ ref}) }}">{{ ref|slice(0, 8) }}...</a> |
||||
</li> |
||||
{% endfor %} |
||||
</ul> |
||||
{% endif %} |
||||
{% endif %} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block stylesheets %} |
||||
{{ parent() }} |
||||
<style> |
||||
.event-container { |
||||
max-width: 800px; |
||||
margin: 2rem auto; |
||||
padding: 1.5rem; |
||||
background: #fff; |
||||
border-radius: 8px; |
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.event-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: flex-start; |
||||
margin-bottom: 1.5rem; |
||||
border-bottom: 1px solid #eee; |
||||
padding-bottom: 1rem; |
||||
} |
||||
|
||||
.author-info { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 1rem; |
||||
} |
||||
|
||||
.author-picture { |
||||
width: 50px; |
||||
height: 50px; |
||||
border-radius: 50%; |
||||
object-fit: cover; |
||||
} |
||||
|
||||
.event-content { |
||||
font-size: 1.1rem; |
||||
line-height: 1.6; |
||||
margin-bottom: 2rem; |
||||
white-space: pre-wrap; |
||||
} |
||||
|
||||
.event-footer { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-top: 1.5rem; |
||||
padding-top: 1rem; |
||||
border-top: 1px solid #eee; |
||||
} |
||||
|
||||
.event-tags, .event-references { |
||||
flex: 1; |
||||
} |
||||
|
||||
.event-tags ul, .event-references ul { |
||||
list-style-type: none; |
||||
padding-left: 0; |
||||
} |
||||
|
||||
.event-tags li, .event-references li { |
||||
margin-bottom: 0.5rem; |
||||
} |
||||
|
||||
.event-id { |
||||
margin-top: 1.5rem; |
||||
color: #888; |
||||
text-align: right; |
||||
} |
||||
|
||||
.nip05 { |
||||
color: #0066cc; |
||||
font-size: 0.9rem; |
||||
} |
||||
</style> |
||||
{% endblock %} |
||||
Loading…
Reference in new issue