Browse Source

Follows

imwald
Nuša Pukšič 1 month ago
parent
commit
e6ce1e7a99
  1. 18
      src/Controller/EventController.php
  2. 149
      src/Controller/FollowsController.php
  3. 56
      src/Service/NostrClient.php

18
src/Controller/EventController.php

@ -94,11 +94,27 @@ class EventController extends AbstractController @@ -94,11 +94,27 @@ class EventController extends AbstractController
$authorMetadata = $redisCacheService->getMetadata($event->pubkey);
// Batch fetch profiles for follow pack events (kind 39089)
$followPackProfiles = [];
if (isset($event->kind) && $event->kind == 39089 && isset($event->tags)) {
$pubkeys = [];
foreach ($event->tags as $tag) {
if (is_array($tag) && $tag[0] === 'p' && isset($tag[1])) {
$pubkeys[] = $tag[1];
}
}
if (!empty($pubkeys)) {
$logger->info('Batch fetching follow pack profiles', ['count' => count($pubkeys)]);
$followPackProfiles = $redisCacheService->getMultipleMetadata($pubkeys);
}
}
// Render template with the event data and extracted Nostr links
$response = $this->render('event/index.html.twig', [
'event' => $event,
'author' => $authorMetadata,
'nostrLinks' => $nostrLinks
'nostrLinks' => $nostrLinks,
'followPackProfiles' => $followPackProfiles
]);
// Add HTTP caching headers for request-level caching

149
src/Controller/FollowsController.php

@ -0,0 +1,149 @@ @@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Article;
use App\Service\NostrClient;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use swentel\nostr\Key\Key;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class FollowsController extends AbstractController
{
#[Route('/follows', name: 'follows')]
public function index(
EntityManagerInterface $em,
NostrClient $nostrClient,
LoggerInterface $logger
): Response
{
$user = $this->getUser();
// If user is not logged in, show a notice
if (!$user) {
return $this->render('follows/index.html.twig', [
'isLoggedIn' => false,
'articles' => [],
'authorsMetadata' => [],
]);
}
// Get user's pubkey in hex format
$pubkeyHex = null;
try {
$key = new Key();
$pubkeyHex = $key->convertToHex($user->getUserIdentifier());
} catch (\Throwable $e) {
$logger->error('Failed to convert user npub to hex', [
'error' => $e->getMessage(),
'npub' => $user->getUserIdentifier()
]);
return $this->render('follows/index.html.twig', [
'isLoggedIn' => true,
'articles' => [],
'authorsMetadata' => [],
'error' => 'Unable to process user credentials'
]);
}
// Fetch the user's follow list from relays using NostrClient
$followedPubkeys = [];
try {
$followedPubkeys = $nostrClient->getUserFollows($pubkeyHex);
$logger->info('Fetched follow list from relays', [
'user_pubkey' => $pubkeyHex,
'follows_count' => count($followedPubkeys)
]);
} catch (\Throwable $e) {
$logger->error('Failed to fetch follow list from relays', [
'error' => $e->getMessage(),
'pubkey' => $pubkeyHex
]);
return $this->render('follows/index.html.twig', [
'isLoggedIn' => true,
'articles' => [],
'authorsMetadata' => [],
'error' => 'Unable to fetch your follow list from relays'
]);
}
$articles = [];
$authorsMetadata = [];
// If user follows people, get their articles
if (!empty($followedPubkeys)) {
$articleRepo = $em->getRepository(Article::class);
// Query articles from followed authors, ordered by creation date
$qb = $articleRepo->createQueryBuilder('a');
$qb->where($qb->expr()->in('a.pubkey', ':pubkeys'))
->setParameter('pubkeys', $followedPubkeys)
->orderBy('a.createdAt', 'DESC')
->setMaxResults(50); // Limit to latest 50 articles
$articles = $qb->getQuery()->getResult();
// Collect unique author pubkeys
$authorPubkeys = [];
foreach ($articles as $article) {
$authorPubkeys[] = $article->getPubkey();
}
$authorPubkeys = array_unique($authorPubkeys);
// Batch fetch metadata for all authors using NostrClient
try {
$metadataEvents = $nostrClient->getMetadataForPubkeys($authorPubkeys);
foreach ($metadataEvents as $pubkey => $event) {
try {
$metadata = json_decode($event->content, false);
$authorsMetadata[$pubkey] = $metadata;
} catch (\Throwable $e) {
$logger->warning('Failed to decode author metadata', [
'pubkey' => $pubkey,
'error' => $e->getMessage()
]);
// Create basic metadata object
$authorsMetadata[$pubkey] = (object)[
'name' => substr($pubkey, 0, 8) . '...'
];
}
}
// Add fallback metadata for any authors not found
foreach ($authorPubkeys as $pubkey) {
if (!isset($authorsMetadata[$pubkey])) {
$authorsMetadata[$pubkey] = (object)[
'name' => substr($pubkey, 0, 8) . '...'
];
}
}
} catch (\Throwable $e) {
$logger->error('Failed to fetch author metadata', [
'error' => $e->getMessage()
]);
// Create basic metadata for all authors as fallback
foreach ($authorPubkeys as $pubkey) {
$authorsMetadata[$pubkey] = (object)[
'name' => substr($pubkey, 0, 8) . '...'
];
}
}
}
return $this->render('follows/index.html.twig', [
'isLoggedIn' => true,
'articles' => $articles,
'authorsMetadata' => $authorsMetadata,
'followCount' => count($followedPubkeys),
]);
}
}

56
src/Service/NostrClient.php

@ -1411,6 +1411,62 @@ class NostrClient @@ -1411,6 +1411,62 @@ class NostrClient
return $tTags;
}
/**
* Fetch user's follow list (kind 3 event) from relays
*
* @param string $pubkey Hex-encoded pubkey
* @return array Array of followed pubkeys extracted from 'p' tags, or empty array if not found
* @throws \Exception
*/
public function getUserFollows(string $pubkey): array
{
$this->logger->info('Fetching follow list for pubkey', ['pubkey' => $pubkey]);
// Use relay pool with local relay prioritized
$relayUrls = $this->relayPool->ensureLocalRelayInList([
'wss://theforest.nostr1.com',
'wss://nostr.land',
'wss://relay.primal.net',
'wss://purplepag.es'
]);
$relaySet = $this->createRelaySet($relayUrls);
$request = $this->createNostrRequest(
kinds: [KindsEnum::FOLLOWS->value],
filters: ['authors' => [$pubkey], 'limit' => 1],
relaySet: $relaySet
);
$events = $this->processResponse($request->send(), function($received) {
$this->logger->info('Received follow list event', ['event' => $received]);
return $received;
});
if (empty($events)) {
$this->logger->info('No follow list found for pubkey', ['pubkey' => $pubkey]);
return [];
}
// Sort by created_at descending and take the most recent
usort($events, fn($a, $b) => $b->created_at <=> $a->created_at);
$latestFollowEvent = $events[0];
// Extract followed pubkeys from 'p' tags
$followedPubkeys = [];
foreach ($latestFollowEvent->tags as $tag) {
if (is_array($tag) && isset($tag[0]) && $tag[0] === 'p' && isset($tag[1])) {
$followedPubkeys[] = $tag[1];
}
}
$this->logger->info('Follow list fetched successfully', [
'pubkey' => $pubkey,
'follows_count' => count($followedPubkeys)
]);
return $followedPubkeys;
}
/**
* Batch fetch metadata for multiple pubkeys
* Queries local relay first, then fallback to reputable relays for any missing metadata.

Loading…
Cancel
Save