Browse Source

Streamline nostr scheme extension

imwald
Nuša Pukšič 8 months ago
parent
commit
3d82e3ea81
  1. 12
      src/Util/CommonMark/Converter.php
  2. 11
      src/Util/CommonMark/NostrSchemeExtension/NostrEventRenderer.php
  3. 15
      src/Util/CommonMark/NostrSchemeExtension/NostrMentionParser.php
  4. 11
      src/Util/CommonMark/NostrSchemeExtension/NostrRawNpubParser.php
  5. 10
      src/Util/CommonMark/NostrSchemeExtension/NostrSchemeExtension.php
  6. 74
      src/Util/CommonMark/NostrSchemeExtension/NostrSchemeParser.php

12
src/Util/CommonMark/Converter.php

@ -2,7 +2,7 @@
namespace App\Util\CommonMark; namespace App\Util\CommonMark;
use App\Util\Bech32\Bech32Decoder; use App\Service\RedisCacheService;
use App\Util\CommonMark\ImagesExtension\RawImageLinkExtension; use App\Util\CommonMark\ImagesExtension\RawImageLinkExtension;
use App\Util\CommonMark\NostrSchemeExtension\NostrSchemeExtension; use App\Util\CommonMark\NostrSchemeExtension\NostrSchemeExtension;
use League\CommonMark\Environment\Environment; use League\CommonMark\Environment\Environment;
@ -22,11 +22,11 @@ use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter; use League\CommonMark\MarkdownConverter;
use League\CommonMark\Renderer\HtmlDecorator; use League\CommonMark\Renderer\HtmlDecorator;
class Converter readonly class Converter
{ {
public function __construct(private readonly Bech32Decoder $bech32Decoder) public function __construct(
{ private RedisCacheService $redisCacheService
} ){}
/** /**
* @throws CommonMarkException * @throws CommonMarkException
@ -64,7 +64,7 @@ class Converter
$environment->addExtension(new TableExtension()); $environment->addExtension(new TableExtension());
$environment->addExtension(new StrikethroughExtension()); $environment->addExtension(new StrikethroughExtension());
// create a custom extension, that handles nostr mentions // create a custom extension, that handles nostr mentions
$environment->addExtension(new NostrSchemeExtension($this->bech32Decoder)); $environment->addExtension(new NostrSchemeExtension($this->redisCacheService));
$environment->addExtension(new SmartPunctExtension()); $environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new EmbedExtension()); $environment->addExtension(new EmbedExtension());
$environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embedded-content'])); $environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embedded-content']));

11
src/Util/CommonMark/NostrSchemeExtension/NostrEventRenderer.php

@ -22,15 +22,22 @@ class NostrEventRenderer implements NodeRendererInterface
} else if ($node->getType() === 'naddr') { } else if ($node->getType() === 'naddr') {
// dump($node); // dump($node);
// Construct the local link URL from the special part // Construct the local link URL from the special part
$url = '/' . $node->getSpecial(); $url = '/article/' . $node->getSpecial();
} }
if (isset($url)) { if (isset($url)) {
// Create the anchor element // Create the anchor element
return new HtmlElement('a', ['href' => $url], '@' . $node->getSpecial()); return new HtmlElement('a', ['href' => $url], '@' . $this->labelFromKey($node->getSpecial()));
} }
return false; return false;
} }
private function labelFromKey($key): string
{
$start = substr($key, 0, 8);
$end = substr($key, -8);
return $start . '…' . $end;
}
} }

15
src/Util/CommonMark/NostrSchemeExtension/NostrMentionParser.php

@ -2,6 +2,7 @@
namespace App\Util\CommonMark\NostrSchemeExtension; namespace App\Util\CommonMark\NostrSchemeExtension;
use App\Service\RedisCacheService;
use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Parser\InlineParserContext;
@ -13,8 +14,11 @@ use swentel\nostr\Key\Key;
* but have npub1XXXX instead of a URL * but have npub1XXXX instead of a URL
* @package App\Util\CommonMark * @package App\Util\CommonMark
*/ */
class NostrMentionParser implements InlineParserInterface readonly class NostrMentionParser implements InlineParserInterface
{ {
public function __construct(
private RedisCacheService $redisCacheService
){}
public function getMatchDefinition(): InlineParserMatch public function getMatchDefinition(): InlineParserMatch
{ {
@ -34,13 +38,14 @@ class NostrMentionParser implements InlineParserInterface
// Extract "npub" part from fullMatch // Extract "npub" part from fullMatch
$npubLink = substr($fullMatch, strpos($fullMatch, 'npub1'), -1); // e.g., "npubXXXX" $npubLink = substr($fullMatch, strpos($fullMatch, 'npub1'), -1); // e.g., "npubXXXX"
$npubPart = substr($npubLink, 5); // Extract the part after "npub1", i.e., "XXXX"
$key = new Key(); if (empty($label)) {
$hex = $key->convertToHex($npubLink); $metadata = $this->redisCacheService->getMetadata($npubLink);
$label = $metadata->display_name ?? $metadata->name;
}
// Create a new inline node for the custom link // Create a new inline node for the custom link
$inlineContext->getContainer()->appendChild(new NostrMentionLink($label, $hex)); $inlineContext->getContainer()->appendChild(new NostrMentionLink($label, $npubLink));
// Advance the cursor to consume the matched part (important!) // Advance the cursor to consume the matched part (important!)
$cursor->advanceBy(strlen($fullMatch)); $cursor->advanceBy(strlen($fullMatch));

11
src/Util/CommonMark/NostrSchemeExtension/NostrRawNpubParser.php

@ -2,19 +2,19 @@
namespace App\Util\CommonMark\NostrSchemeExtension; namespace App\Util\CommonMark\NostrSchemeExtension;
use App\Service\RedisCacheService;
use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Parser\InlineParserContext;
use swentel\nostr\Key\Key;
/** /**
* Class NostrRawNpubParser * Class NostrRawNpubParser
* Looks for raw nostr mentions formatted as npub1XXXX * Looks for raw nostr mentions formatted as npub1XXXX
*/ */
class NostrRawNpubParser implements InlineParserInterface readonly class NostrRawNpubParser implements InlineParserInterface
{ {
public function __construct() public function __construct(private RedisCacheService $redisCacheService)
{ {
} }
@ -28,11 +28,10 @@ class NostrRawNpubParser implements InlineParserInterface
$cursor = $inlineContext->getCursor(); $cursor = $inlineContext->getCursor();
// Get the match and extract relevant parts // Get the match and extract relevant parts
$fullMatch = $inlineContext->getFullMatch(); $fullMatch = $inlineContext->getFullMatch();
$key = new Key(); $meta = $this->redisCacheService->getMetadata($fullMatch);
$hex = $key->convertToHex($fullMatch);
// Create a new inline node for the custom link // Create a new inline node for the custom link
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $hex)); $inlineContext->getContainer()->appendChild(new NostrMentionLink($meta->name, $fullMatch));
// Advance the cursor to consume the matched part (important!) // Advance the cursor to consume the matched part (important!)
$cursor->advanceBy(strlen($fullMatch)); $cursor->advanceBy(strlen($fullMatch));

10
src/Util/CommonMark/NostrSchemeExtension/NostrSchemeExtension.php

@ -2,23 +2,23 @@
namespace App\Util\CommonMark\NostrSchemeExtension; namespace App\Util\CommonMark\NostrSchemeExtension;
use App\Util\Bech32\Bech32Decoder; use App\Service\RedisCacheService;
use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Extension\ExtensionInterface;
class NostrSchemeExtension implements ExtensionInterface class NostrSchemeExtension implements ExtensionInterface
{ {
public function __construct(private readonly Bech32Decoder $bech32Decoder) public function __construct(private readonly RedisCacheService $redisCacheService)
{ {
} }
public function register(EnvironmentBuilderInterface $environment): void public function register(EnvironmentBuilderInterface $environment): void
{ {
$environment $environment
->addInlineParser(new NostrMentionParser(), 200) ->addInlineParser(new NostrMentionParser($this->redisCacheService), 200)
->addInlineParser(new NostrSchemeParser($this->bech32Decoder), 199) ->addInlineParser(new NostrSchemeParser(), 199)
->addInlineParser(new NostrRawNpubParser(), 198) ->addInlineParser(new NostrRawNpubParser($this->redisCacheService), 198)
->addRenderer(NostrSchemeData::class, new NostrEventRenderer(), 2) ->addRenderer(NostrSchemeData::class, new NostrEventRenderer(), 2)
->addRenderer(NostrMentionLink::class, new NostrMentionRenderer(), 1) ->addRenderer(NostrMentionLink::class, new NostrMentionRenderer(), 1)

74
src/Util/CommonMark/NostrSchemeExtension/NostrSchemeParser.php

@ -2,18 +2,18 @@
namespace App\Util\CommonMark\NostrSchemeExtension; namespace App\Util\CommonMark\NostrSchemeExtension;
use App\Util\Bech32\Bech32Decoder;
use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Parser\InlineParserContext;
use function BitWasp\Bech32\convertBits; use nostriphant\NIP19\Bech32;
use function BitWasp\Bech32\decode; use nostriphant\NIP19\Data\NAddr;
use nostriphant\NIP19\Data\NEvent;
use nostriphant\NIP19\Data\NProfile;
class NostrSchemeParser implements InlineParserInterface class NostrSchemeParser implements InlineParserInterface
{ {
public function __construct(private Bech32Decoder $bech32Decoder) public function __construct()
{ {
} }
@ -30,58 +30,36 @@ class NostrSchemeParser implements InlineParserInterface
// The match is a Bech32 encoded string // The match is a Bech32 encoded string
// decode it to get the parts // decode it to get the parts
$bechEncoded = substr($fullMatch, 6); // Extract the part after "nostr:", i.e., "XXXX" $bechEncoded = substr($fullMatch, 6); // Extract the part after "nostr:", i.e., "XXXX"
// dump($bechEncoded);
try { try {
list($hrp, $tlv) = $this->bech32Decoder->decodeAndParseNostrBech32($bechEncoded); $decoded = new Bech32($bechEncoded);
// dump($hrp);
// dump($tlv); switch ($decoded->type) {
switch ($hrp) {
case 'npub': case 'npub':
$str = ''; $inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $decoded->data));
list($hrp, $data) = decode($bechEncoded);
$bytes = convertBits($data, count($data), 5, 8, false);
foreach ($bytes as $item) {
$str .= str_pad(dechex($item), 2, '0', STR_PAD_LEFT);
}
$npubPart = $str;
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $npubPart));
break; break;
case 'nprofile': case 'nprofile':
$type = 0; // npub /** @var NProfile $decodedProfile */
foreach ($tlv as $item) { $decodedProfile = $decoded->data;
if ($item['type'] === $type) { $inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $decodedProfile->getPubkey()));
// from array of integers to string
$str = '';
foreach ($item['value'] as $byte) {
$str .= str_pad(dechex($byte), 2, '0', STR_PAD_LEFT);
}
$npubPart = $str;
break;
}
}
if (isset($npubPart)) {
$inlineContext->getContainer()->appendChild(new NostrMentionLink(null, $npubPart));
}
break; break;
case 'nevent': case 'nevent':
foreach ($tlv as $item) { /** @var NEvent $decodedNpub */
// event id $decodedEvent = $decoded->data;
if ($item['type'] === 0) { $eventId = $decodedEvent->getId();
$eventId = implode('', array_map(fn($byte) => sprintf('%02x', $byte), $item['value'])); $relays = $decodedEvent->getRelays();
break; $author = $decodedEvent->getAuthor();
} $kind = $decodedEvent->getKind();
// relays $inlineContext->getContainer()->appendChild(new NostrSchemeData('nevent', $eventId, $relays, $author, $kind));
if ($item['type'] === 1) {
$relays[] = implode('', array_map('chr', $item['value']));
}
}
// dump($relays ?? null);
// TODO also potentially contains relays, author, and kind
$inlineContext->getContainer()->appendChild(new NostrSchemeData('nevent', $eventId, $relays ?? null, null, null));
break; break;
case 'naddr': case 'naddr':
$inlineContext->getContainer()->appendChild(new NostrSchemeData('naddr', $bechEncoded, null, null, null)); /** @var NAddr $decodedNpub */
$decodedEvent = $decoded->data;
$identifier = $decodedEvent->getIdentifier();
$pubkey = $decodedEvent->getPubkey();
$kind = $decodedEvent->getKind();
$relays = $decodedEvent->getRelays();
$inlineContext->getContainer()->appendChild(new NostrSchemeData('naddr', $identifier, $relays, $pubkey, $kind));
break; break;
case 'nrelay': case 'nrelay':
// deprecated // deprecated

Loading…
Cancel
Save