diff --git a/assets/styles/article.css b/assets/styles/article.css index 3357bc4..847a89f 100644 --- a/assets/styles/article.css +++ b/assets/styles/article.css @@ -72,3 +72,9 @@ blockquote p { text-decoration: none; font-weight: bold; } + +.embedded-content iframe { + width: 100%; + height: auto; + aspect-ratio: 16/9; +} diff --git a/composer.json b/composer.json index f66a46b..ed674fe 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,11 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.3", + "embed/embed": "^4.4", "endroid/qr-code": "^6.0", "endroid/qr-code-bundle": "^6.0", "friendsofsymfony/elastica-bundle": "^6.5", + "laminas/laminas-diactoros": "^3.6", "league/commonmark": "^2.7", "league/html-to-markdown": "*", "phpdocumentor/reflection-docblock": "^5.6", diff --git a/composer.lock b/composer.lock index 733f33c..d870050 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad306101014c19a923d27979dcdeb2cd", + "content-hash": "caedc29506c52d87b3e4e4217fe34eb8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -106,6 +106,82 @@ }, "time": "2018-02-05T22:23:47+00:00" }, + { + "name": "composer/ca-bundle", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-03-06T14:30:56+00:00" + }, { "name": "composer/semver", "version": "3.4.3", @@ -1498,6 +1574,95 @@ }, "time": "2023-04-21T15:31:12+00:00" }, + { + "name": "embed/embed", + "version": "v4.4.17", + "source": { + "type": "git", + "url": "https://github.com/php-embed/Embed.git", + "reference": "b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-embed/Embed/zipball/b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87", + "reference": "b2ea091a5586c14ea5f2c5bf52fb0ef38e5aef87", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "ext-curl": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ml/json-ld": "^1.1", + "oscarotero/html-parser": "^0.1.4", + "php": "^7.4|^8", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0|^2.0" + }, + "require-dev": { + "brick/varexporter": "^0.3.1", + "friendsofphp/php-cs-fixer": "^2.0", + "nyholm/psr7": "^1.2", + "oscarotero/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^9.0", + "symfony/css-selector": "^5.0" + }, + "suggest": { + "symfony/css-selector": "If you want to get elements using css selectors" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Embed\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP library to retrieve page info using oembed, opengraph, etc", + "homepage": "https://github.com/oscarotero/Embed", + "keywords": [ + "embed", + "embedly", + "oembed", + "opengraph", + "twitter cards" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/Embed/issues", + "source": "https://github.com/php-embed/Embed/tree/v4.4.17" + }, + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2025-05-13T12:42:29+00:00" + }, { "name": "endroid/installer", "version": "1.5.0", @@ -1992,6 +2157,94 @@ }, "time": "2025-02-12T20:20:53+00:00" }, + { + "name": "laminas/laminas-diactoros", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/b068eac123f21c0e592de41deeb7403b88e0a89f", + "reference": "b068eac123f21c0e592de41deeb7403b88e0a89f", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.0.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-05-05T16:03:34+00:00" + }, { "name": "lcobucci/jwt", "version": "5.5.0", @@ -2584,6 +2837,110 @@ }, "time": "2024-03-31T07:05:07+00:00" }, + { + "name": "ml/iri", + "version": "1.1.4", + "target-dir": "ML/IRI", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/IRI.git", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341", + "shasum": "" + }, + "require": { + "lib-pcre": ">=4.0", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "ML\\IRI": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "IRI handling for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "URN", + "iri", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/lanthaler/IRI/issues", + "source": "https://github.com/lanthaler/IRI/tree/master" + }, + "time": "2014-01-21T13:43:39+00:00" + }, + { + "name": "ml/json-ld", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/JsonLD.git", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ml/iri": "^1.1.1", + "php": ">=5.3.0" + }, + "require-dev": { + "json-ld/tests": "1.0", + "phpunit/phpunit": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ML\\JsonLD\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "JSON-LD Processor for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "JSON-LD", + "jsonld" + ], + "support": { + "issues": "https://github.com/lanthaler/JsonLD/issues", + "source": "https://github.com/lanthaler/JsonLD/tree/1.2.1" + }, + "time": "2022-09-29T08:45:17+00:00" + }, { "name": "nette/schema", "version": "v1.3.2", @@ -2793,6 +3150,59 @@ ], "time": "2021-11-18T09:23:29+00:00" }, + { + "name": "oscarotero/html-parser", + "version": "v0.1.8", + "source": { + "type": "git", + "url": "https://github.com/oscarotero/html-parser.git", + "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oscarotero/html-parser/zipball/10f3219267a365d9433f2f7d1694209c9d436c8d", + "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.11", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HtmlParser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "Parse html strings to DOMDocument", + "homepage": "https://github.com/oscarotero/html-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/html-parser/issues", + "source": "https://github.com/oscarotero/html-parser/tree/v0.1.8" + }, + "time": "2023-11-29T20:28:41+00:00" + }, { "name": "pagerfanta/pagerfanta", "version": "v4.7.1", @@ -3729,6 +4139,58 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, { "name": "psr/http-factory", "version": "1.1.0", diff --git a/config/packages/fos_elastica.yaml b/config/packages/fos_elastica.yaml index 372974e..016e43e 100644 --- a/config/packages/fos_elastica.yaml +++ b/config/packages/fos_elastica.yaml @@ -10,11 +10,14 @@ fos_elastica: articles: indexable_callback: [ 'App\Util\IndexableArticleChecker', 'isIndexable' ] properties: + createdAt: ~ title: ~ summary: ~ content: ~ slug: type: keyword + pubkey: + type: keyword topics: ~ persistence: driver: orm diff --git a/config/packages/security.yaml b/config/packages/security.yaml index c1df357..b32a818 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -9,7 +9,7 @@ security: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: - lazy: true + lazy: false stateless: false provider: user_dto_provider custom_authenticators: diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index e454232..5e69a18 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -6,6 +6,7 @@ use App\Entity\Article; use App\Enum\KindsEnum; use App\Form\EditorType; use App\Service\NostrClient; +use App\Service\RedisCacheService; use App\Util\Bech32\Bech32Decoder; use App\Util\CommonMark\Converter; use Doctrine\ORM\EntityManagerInterface; @@ -83,8 +84,13 @@ class ArticleController extends AbstractController * @throws InvalidArgumentException|CommonMarkException */ #[Route('/article/d/{slug}', name: 'article-slug')] - public function article(EntityManagerInterface $entityManager, CacheItemPoolInterface $articlesCache, - NostrClient $nostrClient, Converter $converter, $slug): Response + public function article( + $slug, + EntityManagerInterface $entityManager, + RedisCacheService $redisCacheService, + CacheItemPoolInterface $articlesCache, + Converter $converter + ): Response { $article = null; // check if an item with same eventId already exists in the db @@ -114,37 +120,16 @@ class ArticleController extends AbstractController $articlesCache->save($cacheItem); } -// // suggestions -// $suggestions = $repository->findBy(['pubkey' => $article->getPubkey()], ['createdAt' => 'DESC'], 3); -// // skip current, if listed in suggestions -// $suggestions = array_filter($suggestions, function ($s) use ($article) { -// return $s->getId() !== $article->getId(); -// }); -// $suggestions = array_merge($suggestions, $repository->findBy([], ['createdAt' => 'DESC'], 6 - count($suggestions))); -// // sort by date -// usort($suggestions, function ($a, $b) { -// return $b->getCreatedAt() <=> $a->getCreatedAt(); -// }); - - try { - $meta = $nostrClient->getNpubMetadata($article->getPubkey()); - if ($meta?->content) { - $author = (array) json_decode($meta->content); - } else { - $author = [ - 'name' => '' - ]; - } - } catch (\Exception $e) { - // Whatever? - } + $key = new Key(); + $npub = $key->convertPublicKeyToBech32($article->getPubkey()); + $author = $redisCacheService->getMetadata($npub); return $this->render('Pages/article.html.twig', [ 'article' => $article, - 'author' => $author ?? null, + 'author' => $author, + 'npub' => $npub, 'content' => $cacheItem->get(), - //'suggestions' => $suggestions ]); } diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php index adff1af..71d8eb6 100644 --- a/src/Controller/AuthorController.php +++ b/src/Controller/AuthorController.php @@ -4,18 +4,14 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Article; -use App\Entity\Event; -use App\Enum\KindsEnum; -use App\Service\NostrClient; -use Doctrine\ORM\EntityManagerInterface; +use App\Service\RedisCacheService; +use Elastica\Query\Terms; +use FOS\ElasticaBundle\Finder\FinderInterface; use Psr\Cache\InvalidArgumentException; use swentel\nostr\Key\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; class AuthorController extends AbstractController { @@ -24,39 +20,18 @@ class AuthorController extends AbstractController * @throws InvalidArgumentException */ #[Route('/p/{npub}', name: 'author-profile', requirements: ['npub' => '^npub1.*'])] - public function index($npub, CacheInterface $redisCache, EntityManagerInterface $entityManager, NostrClient $client): Response + public function index($npub, RedisCacheService $redisCacheService, FinderInterface $finder): Response { $keys = new Key(); $pubkey = $keys->convertToHex($npub); - $relays = []; - try { - $cacheKey = '0_' . $pubkey; + $author = $redisCacheService->getMetadata($npub); + $relays = $redisCacheService->getRelays($npub); - $author = $redisCache->get($cacheKey, function (ItemInterface $item) use ($pubkey, $client) { - $item->expiresAfter(3600); // 1 hour, adjust as needed - - $meta = $client->getNpubMetadata($pubkey); - return (array) json_decode($meta->content ?? '{}'); - }); - - } catch (InvalidArgumentException | \Exception $e) { - // nothing to do - } - - try { - $cacheKey = '10002_' . $pubkey; - - $relays = $redisCache->get($cacheKey, function (ItemInterface $item) use ($pubkey, $client) { - $item->expiresAfter(3600); // 1 hour, adjust as needed - - return $client->getNpubRelays($pubkey); - }); - } catch (InvalidArgumentException | \Exception $e) { - // nothing to do - } - - $list = $client->getLongFormContentForPubkey($pubkey); + // Look for articles in index, assume indexing is done regularly + // TODO give users an option to reindex + $query = new Terms('pubkey', [$pubkey]); + $list = $finder->find($query, 25); // deduplicate by slugs $articles = []; @@ -66,19 +41,10 @@ class AuthorController extends AbstractController } } - // $indices = $entityManager->getRepository(Event::class)->findBy(['pubkey' => $pubkey, 'kind' => KindsEnum::PUBLICATION_INDEX]); - - // $nzines = $entityManager->getRepository(Nzine::class)->findBy(['editor' => $pubkey]); - - // $nzine = $entityManager->getRepository(Nzine::class)->findBy(['npub' => $npub]); - return $this->render('Pages/author.html.twig', [ 'author' => $author, 'npub' => $npub, 'articles' => $articles, - 'nzine' => null, - 'nzines' => null, - 'idx' => null, 'relays' => $relays ]); } @@ -90,9 +56,7 @@ class AuthorController extends AbstractController public function authorRedirect($pubkey): Response { $keys = new Key(); - $npub = $keys->convertPublicKeyToBech32($pubkey); - return $this->redirectToRoute('author-profile', ['npub' => $npub]); } } diff --git a/src/Entity/User.php b/src/Entity/User.php index 64bbc8b..ce4d311 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -15,6 +15,8 @@ use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Table(name: "app_user")] class User implements UserInterface, EquatableInterface { + private static array $sessionData = []; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -84,38 +86,48 @@ class User implements UserInterface, EquatableInterface return $this->getNpub(); } - public function setMetadata($metadata) + public function setMetadata(?object $metadata): void { - $this->metadata = $metadata; + self::$sessionData[$this->getNpub()]['metadata'] = $metadata; } - public function getMetadata() + public function getMetadata(): ?object { - return $this->metadata; - } - - public function getDisplayName() { - return $this->metadata->name; + return self::$sessionData[$this->getNpub()]['metadata'] ?? null; } - /** - * @param mixed $relays - */ - public function setRelays($relays): void + public function setRelays(?array $relays): void { - $this->relays = $relays; + self::$sessionData[$this->getNpub()]['relays'] = $relays; } - /** - * @return null|array - */ public function getRelays(): ?array { - return $this->relays; + return self::$sessionData[$this->getNpub()]['relays'] ?? null; } public function isEqualTo(UserInterface $user): bool { return $this->getUserIdentifier() === $user->getUserIdentifier(); } + + public function __serialize(): array + { + return [ + 'id' => $this->id, + 'npub' => $this->npub, + 'roles' => $this->roles, + 'metadata' => $this->metadata, + 'relays' => $this->relays + ]; + } + + public function __unserialize(array $data): void + { + $this->id = $data['id']; + $this->npub = $data['npub']; + $this->roles = $data['roles']; + $this->metadata = $data['metadata']; + $this->relays = $data['relays']; + } } diff --git a/src/Security/UserDTOProvider.php b/src/Security/UserDTOProvider.php index b6357b1..c4e5d0c 100644 --- a/src/Security/UserDTOProvider.php +++ b/src/Security/UserDTOProvider.php @@ -3,11 +3,9 @@ namespace App\Security; use App\Entity\User; -use App\Enum\KindsEnum; -use App\Service\NostrClient; +use App\Service\RedisCacheService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; -use swentel\nostr\Key\Key; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -15,9 +13,11 @@ readonly class UserDTOProvider implements UserProviderInterface { public function __construct( private EntityManagerInterface $entityManager, - private NostrClient $nostrClient, + private RedisCacheService $redisCacheService, private LoggerInterface $logger - ) {} + ) + { + } /** * @inheritDoc @@ -27,8 +27,12 @@ readonly class UserDTOProvider implements UserProviderInterface if (!$user instanceof User) { throw new \InvalidArgumentException('Invalid user type.'); } - - return $this->loadUserByIdentifier($user->getUserIdentifier()); + $this->logger->info('Refresh user.', ['user' => $user->getUserIdentifier()]); + $freshUser = $this->entityManager->getRepository(User::class) + ->findOneBy(['npub' => $user->getUserIdentifier()]); + $metadata = $this->redisCacheService->getMetadata($user->getUserIdentifier()); + $freshUser->setMetadata($metadata); + return $freshUser; } /** @@ -44,39 +48,7 @@ readonly class UserDTOProvider implements UserProviderInterface */ public function loadUserByIdentifier(string $identifier): UserInterface { - try { - $key = new Key(); - $pubkey = $key->convertToHex($identifier); - $data = $this->nostrClient->getLoginData($pubkey); - - $this->logger->info('Load user by identifier.', ['data' => $data]); - - $metadata = null; - $relays = null; - - foreach ($data as $d) { - $ev = $d->event; - $this->logger->info('Load user by identifier event.', ['event' => $ev]); - if ($ev->kind === KindsEnum::METADATA->value) { - $metadata = json_decode($ev->content); - $this->logger->info('Load user by identifier event.', ['metadata' => $metadata]); - } - if ($ev->kind === KindsEnum::RELAY_LIST->value) { - $relays = $ev->tags; - } - } - } catch (\Exception $e) { - $this->logger->error('Error getting user data.', ['exception' => $e]); - $metadata = null; - $relays = null; - } - - // Fallback metadata if none fetched - if (is_null($metadata)) { - $metadata = new \stdClass(); - $metadata->name = substr($identifier, 0, 8) . '…' . substr($identifier, -4); - } - + $this->logger->info('Load user by identifier.'); // Get or create user $user = $this->entityManager->getRepository(User::class)->findOneBy(['npub' => $identifier]); @@ -84,15 +56,13 @@ readonly class UserDTOProvider implements UserProviderInterface $user = new User(); $user->setNpub($identifier); $this->entityManager->persist($user); + $this->entityManager->flush(); } - // Update with fresh metadata/relays + $metadata = $this->redisCacheService->getMetadata($identifier); $user->setMetadata($metadata); - $user->setRelays($relays); - - $this->entityManager->flush(); + $this->logger->debug('User metadata set.', ['metadata' => json_encode($user->getMetadata())]); return $user; - } } diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index 656e529..e03c814 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -12,6 +12,7 @@ use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use swentel\nostr\Event\Event; use swentel\nostr\Filter\Filter; +use swentel\nostr\Key\Key; use swentel\nostr\Message\EventMessage; use swentel\nostr\Message\RequestMessage; use swentel\nostr\Relay\Relay; @@ -24,23 +25,81 @@ use Symfony\Component\Serializer\SerializerInterface; class NostrClient { - private $defaultRelaySet; + private RelaySet $defaultRelaySet; + + /** + * List of reputable relays in descending order of reputation + */ + private const array REPUTABLE_RELAYS = [ + 'wss://theforest.nostr1.com', + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://nos.lol', + 'wss://relay.snort.social', + 'wss://nostr.land', + 'wss://purplepag.es', + ]; + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ManagerRegistry $managerRegistry, private readonly UserEntityRepository $userEntityRepository, private readonly ArticleFactory $articleFactory, - private readonly SerializerInterface $serializer, + private readonly SerializerInterface $serializer, private readonly TokenStorageInterface $tokenStorage, - private readonly LoggerInterface $logger) + private readonly LoggerInterface $logger) { - // TODO configure read and write relays for logged in users from their 10002 events $this->defaultRelaySet = new RelaySet(); - $this->defaultRelaySet->addRelay(new Relay('wss://relay.damus.io')); // public relay - // $this->defaultRelaySet->addRelay(new Relay('wss://relay.primal.net')); // public relay - // $this->defaultRelaySet->addRelay(new Relay('wss://nos.lol')); // public relay - // $this->defaultRelaySet->addRelay(new Relay('wss://relay.snort.social')); // public relay - $this->defaultRelaySet->addRelay(new Relay('wss://theforest.nostr1.com')); // public relay - // $this->defaultRelaySet->addRelay(new Relay('wss://purplepag.es')); // public relay + $this->defaultRelaySet->addRelay(new Relay('wss://theforest.nostr1.com')); // public aggregator relay + } + + /** + * Creates a RelaySet from a list of relay URLs + */ + private function createRelaySet(array $relayUrls): RelaySet + { + $relaySet = new RelaySet(); + foreach ($relayUrls as $relayUrl) { + $relaySet->addRelay(new Relay($relayUrl)); + } + return $relaySet; + } + + /** + * Get top 3 reputable relays from an author's relay list + */ + private function getTopReputableRelaysForAuthor(string $pubkey, int $limit = 3): array + { + try { + $authorRelays = $this->getNpubRelays($pubkey); + } catch (\Exception $e) { + $this->logger->error('Error getting author relays', [ + 'pubkey' => $pubkey, + 'error' => $e->getMessage() + ]); + // fall through + $authorRelays = []; + } + if (empty($authorRelays)) { + return [self::REPUTABLE_RELAYS[0]]; // Default to theforest if no author relays + } + + $reputableAuthorRelays = []; + foreach (self::REPUTABLE_RELAYS as $relay) { + if (in_array($relay, $authorRelays) && count($reputableAuthorRelays) < $limit) { + $reputableAuthorRelays[] = $relay; + } + } + + // If no reputable relays found in author's list, take the top 3 from author's list + // But make sure they start with wss: and are not localhost + if (empty($reputableAuthorRelays)) { + $authorRelays = array_filter($authorRelays, function ($relay) { + return str_starts_with($relay, 'wss:') && !str_contains($relay, 'localhost'); + }); + return array_slice($authorRelays, 0, $limit); + } + + return $reputableAuthorRelays; } public function getLoginData($npub) @@ -54,6 +113,8 @@ class NostrClient $request = new Request($this->defaultRelaySet, $requestMessage); $response = $request->send(); + $this->logger->info('Login data.', ['response' => $response]); + // response is an n-dimensional array, where n is the number of relays in the set // check that response has events in the results foreach ($response as $relayRes) { @@ -68,56 +129,33 @@ class NostrClient return null; } - /** - * @throws \Exception - */ - public function getNpubMetadata($npub) + public function getNpubMetadata($npub): \stdClass { - $filter = new Filter(); - $filter->setKinds([KindsEnum::METADATA]); - $filter->setAuthors([$npub]); - $filters = [$filter]; - $subscription = new Subscription(); - $requestMessage = new RequestMessage($subscription->getId(), $filters); - $relays = [ - new Relay('wss://purplepag.es'), - new Relay('wss://theforest.nostr1.com'), - ]; - $relaySet = new RelaySet(); - $relaySet->setRelays($relays); - - $request = new Request($relaySet, $requestMessage); - $response = $request->send(); - - $meta = []; - // response is an array of arrays - foreach ($response as $value) { - foreach ($value as $item) { - switch ($item->type) { - case 'EVENT': - $meta[] = $item->event; - break; - case 'AUTH': - throw new UnauthorizedHttpException('', 'Relay requires authentication'); - case 'ERROR': - case 'NOTICE': - throw new \Exception('An error occurred'); - default: - // skip - } - } + $this->logger->info('Getting metadata for npub', ['npub' => $npub]); + // Convert npub to hex + $keys = new Key(); + $pubkey = $keys->convertToHex($npub); + $request = $this->createNostrRequest( + kinds: [KindsEnum::METADATA], + filters: ['authors' => [$pubkey]], + relaySet: $this->defaultRelaySet + ); + + $events = $this->processResponse($request->send(), function($received) { + return $received; + }); + + if (empty($events)) { + $meta = new \stdClass(); + $content = new \stdClass(); + $content->name = substr($npub, 0, 8) . '…' . substr($npub, -4); + $meta->content = json_encode($content); + return $meta; } - if (count($meta) > 0) { - if (count($meta) > 1) { - // sort by date and pick newest - usort($meta, function($a, $b) { - return $b->created_at <=> $a->created_at; - }); - } - return $meta[0]; - } - return []; + // Sort by date and return newest + usort($events, fn($a, $b) => $b->created_at <=> $a->created_at); + return $events[0]; } public function getNpubLongForm($npub): void @@ -159,7 +197,6 @@ class NostrClient // TODO handle relays that require auth } - public function publishEvent(Event $event, array $relays): array { $eventMessage = new EventMessage($event); @@ -192,11 +229,7 @@ class NostrClient } $requestMessage = new RequestMessage($subscriptionId, [$filter]); - // if user is logged in, use their settings - $user = $this->tokenStorage->getToken()?->getUser(); - $relays = $this->defaultRelaySet; - - $request = new Request($relays, $requestMessage); + $request = new Request($this->defaultRelaySet, $requestMessage); $response = $request->send(); // response is an n-dimensional array, where n is the number of relays in the set @@ -209,11 +242,8 @@ class NostrClient $this->saveLongFormContent($filtered); } } - // TODO handle relays that require auth } - - public function getLongFormFromNaddr($slug, $relayList, $author, $kind): void { $subscription = new Subscription(); @@ -222,38 +252,74 @@ class NostrClient $filter->setKinds([$kind]); $filter->setAuthors([$author]); $filter->setTag('#d', [$slug]); - $requestMessage = new RequestMessage($subscriptionId, [$filter]); - $relays = $this->defaultRelaySet; - if (!empty($relayList)) { - // $relays->addRelay(new Relay($relayList[0])); + // First try with theforest relay and any relays in $relayList + // Add theforest relay to the list, if not already present + if (!in_array('wss://theforest.nostr1.com', $relayList)) { + $relayList[] = 'wss://theforest.nostr1.com'; } - + $forestRelaySet = $this->createRelaySet($relayList); + $response = null; + $hasEvents = false; try { - $request = new Request($this->defaultRelaySet, $requestMessage); + $request = new Request($forestRelaySet, $requestMessage); $response = $request->send(); + + // Check if we got any events + foreach ($response as $relayRes) { + $filtered = array_filter($relayRes, function ($item) { + return $item->type === 'EVENT'; + }); + if (count($filtered) > 0) { + $this->saveLongFormContent($filtered); + $hasEvents = true; + break; + } + } + + // If no events found in theforest, try author's reputable relays + if (!$hasEvents) { + $topAuthorRelays = $this->getTopReputableRelaysForAuthor($author); + $authorRelaySet = $this->createRelaySet($topAuthorRelays); + + $this->logger->info('No results from theforest, trying author relays', [ + 'relays' => $topAuthorRelays + ]); + + $request = new Request($authorRelaySet, $requestMessage); + $response = $request->send(); + + foreach ($response as $relayRes) { + $filtered = array_filter($relayRes, function ($item) { + return $item->type === 'EVENT'; + }); + if (count($filtered) > 0) { + $this->saveLongFormContent($filtered); + break; + } + } + } } catch (\Exception $e) { - // likely a problem with user's relays, go to defaults only + // If any error occurs, fall back to default relay set + $this->logger->error('Error querying relays, falling back to defaults', [ + 'error' => $e->getMessage() + ]); $request = new Request($this->defaultRelaySet, $requestMessage); $response = $request->send(); - } - // response is an n-dimensional array, where n is the number of relays in the set - // check that response has events in the results - foreach ($response as $relayRes) { - $filtered = array_filter($relayRes, function ($item) { - return $item->type === 'EVENT'; - }); - if (count($filtered) > 0) { - $this->saveLongFormContent($filtered); + foreach ($response as $relayRes) { + $filtered = array_filter($relayRes, function ($item) { + return $item->type === 'EVENT'; + }); + if (count($filtered) > 0) { + $this->saveLongFormContent($filtered); + } } } - // TODO handle relays that require auth } - /** * User metadata * NIP-01 @@ -292,7 +358,6 @@ class NostrClient } } } - } /** @@ -316,7 +381,6 @@ class NostrClient $this->logger->error($e->getMessage()); $this->managerRegistry->resetManager(); } - } private function saveLongFormContent(mixed $filtered): void @@ -338,54 +402,39 @@ class NostrClient } } - - public function getNpubRelays($pubkey): array + /** + * @throws \Exception + */ + public function getNpubRelays($npub): array { - $subscription = new Subscription(); - $subscriptionId = $subscription->setId(); - $filter = new Filter(); - $filter->setKinds([KindsEnum::RELAY_LIST]); - $filter->setAuthors([$pubkey]); - $requestMessage = new RequestMessage($subscriptionId, [$filter]); - $request = new Request($this->defaultRelaySet, $requestMessage); - - $response = $request->send(); - - // response is an array of arrays - foreach ($response as $value) { - foreach ($value as $item) { - switch ($item->type) { - case 'EVENT': - $event = $item->event; - $relays = []; - foreach ($event->tags as $tag) { - if ($tag[0] === 'r') { - $this->logger->info('Relay: ' . $tag[1]); - // if not already listed - // is wss: - // not localhost - if (!in_array($tag[1], $relays) - && str_starts_with('wss:',$tag[1]) - && !str_contains('localhost',$tag[1])) { - $relays[] = $tag[1]; - } - } - } - if (!empty($relays)) { - return $relays; - } - break; - case 'AUTH': - throw new UnauthorizedHttpException('', 'Relay requires authentication'); - case 'ERROR': - case 'NOTICE': - throw new \Exception('An error occurred'); - default: - // nothing to do here - } + // Convert npub to hex + $keys = new Key(); + $pubkey = $keys->convertToHex($npub); + // Get relays + $request = $this->createNostrRequest( + kinds: [KindsEnum::RELAY_LIST], + filters: ['authors' => [$pubkey]], + relaySet: $this->defaultRelaySet + ); + $response = $this->processResponse($request->send(), function($received) { + return $received; + }); + if (empty($response)) { + return []; + } + // Sort by date and use newest + usort($response, fn($a, $b) => $b->created_at <=> $a->created_at); + // Process tags of the $response[0] and extract relays + $relays = []; + foreach ($response[0]->tags as $tag) { + if ($tag[0] === 'r') { + $relays[] = $tag[1]; } } - return []; + // Remove duplicates, localhost and any non-wss relays + return array_filter(array_unique($relays), function ($relay) { + return str_starts_with($relay, 'wss:') && !str_contains($relay, 'localhost'); + }); } /** @@ -415,10 +464,10 @@ class NostrClient $list[] = $item; break; case 'AUTH': - throw new UnauthorizedHttpException('', 'Relay requires authentication'); + // throw new UnauthorizedHttpException('', 'Relay requires authentication'); case 'ERROR': case 'NOTICE': - throw new \Exception('An error occurred'); + // throw new \Exception('An error occurred'); default: // nothing to do here } @@ -430,10 +479,9 @@ class NostrClient /** * @throws \Exception */ - public function getLongFormContentForPubkey(string $pubkey) + public function getLongFormContentForPubkey(string $pubkey): array { $articles = []; - $relaySet = $this->defaultRelaySet; // look for last months long-form notes @@ -480,28 +528,111 @@ class NostrClient $filter->setTag('#d', $slugs); $requestMessage = new RequestMessage($subscriptionId, [$filter]); - $request = new Request($this->defaultRelaySet, $requestMessage); + try { + $request = new Request($this->defaultRelaySet, $requestMessage); + $response = $request->send(); + $hasEvents = false; - $response = $request->send(); - // response is an array of arrays - foreach ($response as $value) { - foreach ($value as $item) { - switch ($item->type) { - case 'EVENT': + // Check if we got any events + foreach ($response as $value) { + foreach ($value as $item) { + if ($item->type === 'EVENT') { if (!isset($articles[$item->event->id])) { $articles[$item->event->id] = $item->event; + $hasEvents = true; } - break; - case 'AUTH': - throw new UnauthorizedHttpException('', 'Relay requires authentication'); - case 'ERROR': - case 'NOTICE': - $this->logger->error('An error while getting articles.', $item); - default: - // nothing to do here + } + } + } + + // If no articles found, try the default relay set + if (!$hasEvents && !empty($slugs)) { + $this->logger->info('No results from theforest, trying default relays'); + + $request = new Request($this->defaultRelaySet, $requestMessage); + $response = $request->send(); + + foreach ($response as $value) { + foreach ($value as $item) { + if ($item->type === 'EVENT') { + if (!isset($articles[$item->event->id])) { + $articles[$item->event->id] = $item->event; + } + } elseif (in_array($item->type, ['AUTH', 'ERROR', 'NOTICE'])) { + $this->logger->error('An error while getting articles.', ['response' => $item]); + } + } + } + } + } catch (\Exception $e) { + $this->logger->error('Error querying relays', [ + 'error' => $e->getMessage() + ]); + + // Fall back to default relay set + $request = new Request($this->defaultRelaySet, $requestMessage); + $response = $request->send(); + + foreach ($response as $value) { + foreach ($value as $item) { + if ($item->type === 'EVENT') { + if (!isset($articles[$item->event->id])) { + $articles[$item->event->id] = $item->event; + } + } } } } + return $articles; } + + private function createNostrRequest(array $kinds, array $filters = [], ?RelaySet $relaySet = null): Request + { + $subscription = new Subscription(); + $filter = new Filter(); + $filter->setKinds($kinds); + + foreach ($filters as $key => $value) { + $method = 'set' . ucfirst($key); + if (method_exists($filter, $method)) { + $filter->$method($value); + } + } + + $requestMessage = new RequestMessage($subscription->getId(), [$filter]); + return new Request($relaySet ?? $this->defaultRelaySet, $requestMessage); + } + + private function processResponse(array $response, callable $eventHandler): array + { + $results = []; + foreach ($response as $relayRes) { + foreach ($relayRes as $item) { + try { + switch ($item->type) { + case 'EVENT': + $result = $eventHandler($item->event); + if ($result !== null) { + $results[] = $result; + } + break; + case 'AUTH': + $this->logger->warning('Relay requires authentication', ['response' => $item]); + break; + case 'ERROR': + case 'NOTICE': + $this->logger->error('Relay error/notice', ['response' => $item]); + break; + } + } catch (\Exception $e) { + $this->logger->error('Error processing event', [ + 'exception' => $e->getMessage(), + 'event' => $item + ]); + } + } + } + return $results; + } } diff --git a/src/Service/RedisCacheService.php b/src/Service/RedisCacheService.php new file mode 100644 index 0000000..d2a6ea8 --- /dev/null +++ b/src/Service/RedisCacheService.php @@ -0,0 +1,71 @@ +redisCache->get($cacheKey, function (ItemInterface $item) use ($npub) { + $item->expiresAfter(3600); // 1 hour, adjust as needed + try { + $meta = $this->nostrClient->getNpubMetadata($npub); + } catch (\Exception $e) { + $this->logger->error('Error getting user data.', ['exception' => $e]); + $meta = new \stdClass(); + $content = new \stdClass(); + $meta->name = substr($npub, 0, 8) . '…' . substr($npub, -4); + $meta->content = json_encode($content); + } + $this->logger->info('Metadata:', ['meta' => json_encode($meta)]); + return json_decode($meta->content); + }); + } catch (InvalidArgumentException $e) { + $this->logger->error('Error getting user data.', ['exception' => $e]); + $content = new \stdClass(); + $content->name = substr($npub, 0, 8) . '…' . substr($npub, -4); + return $content; + } + } + + public function getRelays($npub) + { + $cacheKey = '10002_' . $npub; + + try { + return $this->redisCache->get($cacheKey, function (ItemInterface $item) use ($npub) { + $item->expiresAfter(3600); // 1 hour, adjust as needed + try { + $relays = $this->nostrClient->getNpubRelays($npub); + } catch (\Exception $e) { + $this->logger->error('Error getting user relays.', ['exception' => $e]); + } + return $relays ?? []; + }); + } catch (InvalidArgumentException $e) { + $this->logger->error('Error getting user relays.', ['exception' => $e]); + return []; + } + } +} diff --git a/src/Twig/Components/Molecules/UserFromNpub.php b/src/Twig/Components/Molecules/UserFromNpub.php index 769f83d..56875f7 100644 --- a/src/Twig/Components/Molecules/UserFromNpub.php +++ b/src/Twig/Components/Molecules/UserFromNpub.php @@ -2,11 +2,8 @@ namespace App\Twig\Components\Molecules; -use App\Service\NostrClient; -use Psr\Cache\InvalidArgumentException; +use App\Service\RedisCacheService; use swentel\nostr\Key\Key; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] @@ -16,31 +13,15 @@ final class UserFromNpub public string $npub; public $user = null; - public function __construct( - private readonly CacheInterface $redisCache, - private readonly NostrClient $nostrClient) + public function __construct(private readonly RedisCacheService $redisCacheService) { } public function mount(string $pubkey): void { - $keys = new Key(); $this->pubkey = $pubkey; $this->npub = $keys->convertPublicKeyToBech32($pubkey); - - try { - $cacheKey = '0_' . $this->pubkey; - - $this->user = $this->redisCache->get($cacheKey, function (ItemInterface $item) use ($pubkey) { - $item->expiresAfter(3600); // 1 hour, adjust as needed - - $meta = $this->nostrClient->getNpubMetadata($pubkey); - return (array) json_decode($meta->content); - }); - - } catch (InvalidArgumentException | \Exception $e) { - // nothing to do - } + $this->user = $this->redisCacheService->getMetadata($this->npub); } } diff --git a/src/Util/CommonMark/Converter.php b/src/Util/CommonMark/Converter.php index bccf9f1..e4faae9 100644 --- a/src/Util/CommonMark/Converter.php +++ b/src/Util/CommonMark/Converter.php @@ -9,6 +9,10 @@ use League\CommonMark\Environment\Environment; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\Extension\Embed\Bridge\OscaroteroEmbedAdapter; +use League\CommonMark\Extension\Embed\Embed; +use League\CommonMark\Extension\Embed\EmbedExtension; +use League\CommonMark\Extension\Embed\EmbedRenderer; use League\CommonMark\Extension\Footnote\FootnoteExtension; use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension; use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; @@ -16,6 +20,7 @@ use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension; use League\CommonMark\MarkdownConverter; +use League\CommonMark\Renderer\HtmlDecorator; class Converter { @@ -33,7 +38,6 @@ class Converter preg_match_all('/^#+\s.*$/m', $markdown, $matches); $headingsCount = count($matches[0]); - // Configure the Environment with all the CommonMark parsers/renderers $config = [ 'table_of_contents' => [ @@ -47,6 +51,11 @@ class Converter 'allowed_protocols' => ['https'], // defaults to ['https', 'http', 'ftp'] 'default_protocol' => 'https', // defaults to 'http' ], + 'embed' => [ + 'adapter' => new OscaroteroEmbedAdapter(), // See the "Adapter" documentation below + 'allowed_domains' => ['youtube.com', 'twitter.com', 'github.com'], + 'fallback' => 'link' + ], ]; $environment = new Environment($config); // Add the extensions @@ -57,6 +66,8 @@ class Converter // create a custom extension, that handles nostr mentions $environment->addExtension(new NostrSchemeExtension($this->bech32Decoder)); $environment->addExtension(new SmartPunctExtension()); + $environment->addExtension(new EmbedExtension()); + $environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embedded-content'])); $environment->addExtension(new RawImageLinkExtension()); $environment->addExtension(new AutolinkExtension()); if ($headingsCount > 3) { diff --git a/templates/components/Atoms/NameOrNpub.html.twig b/templates/components/Atoms/NameOrNpub.html.twig index c6a81f6..4a6b97c 100644 --- a/templates/components/Atoms/NameOrNpub.html.twig +++ b/templates/components/Atoms/NameOrNpub.html.twig @@ -3,5 +3,7 @@ {{ author.display_name }} {% elseif author.name is defined and author.name is not empty %} {{ author.name }} + {% else %} + {{ npub|shortenNpub }} {% endif %} diff --git a/templates/components/Molecules/UserFromNpub.html.twig b/templates/components/Molecules/UserFromNpub.html.twig index 4638148..2f48145 100644 --- a/templates/components/Molecules/UserFromNpub.html.twig +++ b/templates/components/Molecules/UserFromNpub.html.twig @@ -1,5 +1,5 @@ {% if user %} - + {% else %} {{ npub|shortenNpub }} {% endif %} diff --git a/templates/components/UserMenu.html.twig b/templates/components/UserMenu.html.twig index 8120a2e..94e2ea5 100644 --- a/templates/components/UserMenu.html.twig +++ b/templates/components/UserMenu.html.twig @@ -1,6 +1,6 @@
{% if app.user %} -

Hello, {{ app.user.displayName }}

+

Hello, {{ app.user.name }}

{% if is_granted('ROLE_ADMIN') %} {#