You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
18 KiB
503 lines
18 KiB
<?php |
|
|
|
namespace App\Service; |
|
|
|
use App\Entity\Article; |
|
use App\Entity\User; |
|
use App\Enum\KindsEnum; |
|
use App\Factory\ArticleFactory; |
|
use App\Repository\UserEntityRepository; |
|
use Doctrine\ORM\EntityManagerInterface; |
|
use Doctrine\Persistence\ManagerRegistry; |
|
use Psr\Log\LoggerInterface; |
|
use swentel\nostr\Event\Event; |
|
use swentel\nostr\Filter\Filter; |
|
use swentel\nostr\Message\EventMessage; |
|
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; |
|
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; |
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; |
|
use Symfony\Component\Serializer\SerializerInterface; |
|
|
|
class NostrClient |
|
{ |
|
private $defaultRelaySet; |
|
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 TokenStorageInterface $tokenStorage, |
|
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 |
|
} |
|
|
|
public function getLoginData($npub) |
|
{ |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::METADATA, KindsEnum::RELAY_LIST]); |
|
$filter->setAuthors([$npub]); |
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]); |
|
$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) { |
|
return $filtered; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* @throws \Exception |
|
*/ |
|
public function getNpubMetadata($npub) |
|
{ |
|
$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 |
|
} |
|
} |
|
} |
|
|
|
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 []; |
|
} |
|
|
|
public function getNpubLongForm($npub): void |
|
{ |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::LONGFORM]); |
|
$filter->setAuthors([$npub]); |
|
$filter->setSince(strtotime('-6 months')); // too much? |
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]); |
|
|
|
// if user is logged in, use their settings |
|
/* @var $user */ |
|
$user = $this->tokenStorage->getToken()?->getUser(); |
|
$relays = $this->defaultRelaySet; |
|
if ($user && $user->getRelays()) { |
|
$relays = new RelaySet(); |
|
foreach ($user->getRelays() as $relayArr) { |
|
if ($relayArr[2] == 'write') { |
|
$relays->addRelay(new Relay($relayArr[1])); |
|
} |
|
} |
|
} |
|
|
|
$request = new Request($relays, $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); |
|
} |
|
} |
|
// TODO handle relays that require auth |
|
} |
|
|
|
|
|
public function publishEvent(Event $event, array $relays): array |
|
{ |
|
$eventMessage = new EventMessage($event); |
|
$relaySet = new RelaySet(); |
|
foreach ($relays as $relayWss) { |
|
$relay = new Relay($relayWss); |
|
$relaySet->addRelay($relay); |
|
} |
|
$relaySet->setMessage($eventMessage); |
|
// TODO handle responses appropriately |
|
return $relaySet->send(); |
|
} |
|
|
|
/** |
|
* Long-form Content |
|
* NIP-23 |
|
*/ |
|
public function getLongFormContent(): void |
|
{ |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::LONGFORM]); |
|
// TODO make filters configurable |
|
$filter->setSince(strtotime('-1 week')); // |
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]); |
|
|
|
// if user is logged in, use their settings |
|
$user = $this->tokenStorage->getToken()?->getUser(); |
|
$relays = $this->defaultRelaySet; |
|
if ($user) { |
|
//$relays = new RelaySet(); |
|
} |
|
|
|
$request = new Request($relays, $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); |
|
} |
|
} |
|
// TODO handle relays that require auth |
|
} |
|
|
|
|
|
|
|
public function getLongFormFromNaddr($slug, $relayList, $author, $kind): void |
|
{ |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$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])); |
|
} |
|
|
|
|
|
try { |
|
$request = new Request($this->defaultRelaySet, $requestMessage); |
|
$response = $request->send(); |
|
} catch (\Exception $e) { |
|
// likely a problem with user's relays, go to defaults only |
|
$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); |
|
} |
|
} |
|
// TODO handle relays that require auth |
|
} |
|
|
|
|
|
/** |
|
* User metadata |
|
* NIP-01 |
|
* @throws \Exception |
|
*/ |
|
public function getMetadata(array $npubs): void |
|
{ |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::METADATA]); |
|
$filter->setAuthors($npubs); |
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]); |
|
// TODO make relays configurable |
|
$relays = new RelaySet(); |
|
$relays->addRelay(new Relay('wss://purplepag.es')); // default metadata aggregator |
|
// $relays->addRelay(new Relay('wss://nos.lol')); // default metadata aggregator |
|
|
|
$request = new Request($relays, $requestMessage); |
|
|
|
$response = $request->send(); |
|
// response is an array of arrays |
|
foreach ($response as $value) { |
|
foreach ($value as $item) { |
|
switch ($item->type) { |
|
case 'EVENT': |
|
$this->saveMetadata($item->event); |
|
break; |
|
case 'AUTH': |
|
throw new UnauthorizedHttpException('', 'Relay requires authentication'); |
|
case 'ERROR': |
|
case 'NOTICE': |
|
throw new \Exception('An error occurred'); |
|
default: |
|
// nothing to do here |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Save user metadata |
|
*/ |
|
private function saveMetadata($metadata): void |
|
{ |
|
try { |
|
$user = $this->serializer->deserialize($metadata->content, User::class, 'json'); |
|
$user->setNpub($metadata->pubkey); |
|
} catch (\Exception $e) { |
|
$this->logger->error('Deserialization of user data failed.', ['exception' => $e]); |
|
return; |
|
} |
|
|
|
try { |
|
$this->logger->info('Saving user', ['user' => $user]); |
|
$this->userEntityRepository->findOrCreateByUniqueField($user); |
|
$this->entityManager->flush(); |
|
} catch (\Exception $e) { |
|
$this->logger->error($e->getMessage()); |
|
$this->managerRegistry->resetManager(); |
|
} |
|
|
|
} |
|
|
|
private function saveLongFormContent(mixed $filtered): void |
|
{ |
|
foreach ($filtered as $wrapper) { |
|
$article = $this->articleFactory->createFromLongFormContentEvent($wrapper->event); |
|
// check if event with same eventId already in DB |
|
$saved = $this->entityManager->getRepository(Article::class)->findOneBy(['eventId' => $article->getEventId()]); |
|
if (!$saved) { |
|
try { |
|
$this->logger->info('Saving article', ['article' => $article]); |
|
$this->entityManager->persist($article); |
|
$this->entityManager->flush(); |
|
} catch (\Exception $e) { |
|
$this->logger->error($e->getMessage()); |
|
$this->managerRegistry->resetManager(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
public function getNpubRelays($pubkey): 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 |
|
} |
|
} |
|
} |
|
return []; |
|
} |
|
|
|
/** |
|
* @throws \Exception |
|
*/ |
|
public function getComments($coordinate): array |
|
{ |
|
$list = []; |
|
$parts = explode(':', $coordinate); |
|
|
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::COMMENTS, KindsEnum::TEXT_NOTE]); |
|
$filter->setTag('#a', [$coordinate]); |
|
$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': |
|
dump($item); |
|
$list[] = $item; |
|
break; |
|
case 'AUTH': |
|
throw new UnauthorizedHttpException('', 'Relay requires authentication'); |
|
case 'ERROR': |
|
case 'NOTICE': |
|
throw new \Exception('An error occurred'); |
|
default: |
|
// nothing to do here |
|
} |
|
} |
|
} |
|
return $list; |
|
} |
|
|
|
/** |
|
* @throws \Exception |
|
*/ |
|
public function getLongFormContentForPubkey(string $pubkey) |
|
{ |
|
$articles = []; |
|
|
|
$relaySet = $this->defaultRelaySet; |
|
|
|
// look for last months long-form notes |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::LONGFORM]); |
|
$filter->setLimit(10); |
|
$filter->setAuthors([$pubkey]); |
|
$requestMessage = new RequestMessage($subscriptionId, [$filter]); |
|
$request = new Request($relaySet, $requestMessage); |
|
|
|
$response = $request->send(); |
|
|
|
// response is an array of arrays |
|
foreach ($response as $value) { |
|
foreach ($value as $item) { |
|
if (is_array($item)) continue; |
|
switch ($item->type) { |
|
case 'EVENT': |
|
$article = $this->articleFactory->createFromLongFormContentEvent($item->event); |
|
$articles[] = $article; |
|
break; |
|
case 'AUTH': |
|
// throw new UnauthorizedHttpException('', 'Relay requires authentication'); |
|
case 'ERROR': |
|
case 'NOTICE': |
|
// throw new \Exception('An error occurred'); |
|
default: |
|
// nothing to do here |
|
} |
|
} |
|
} |
|
return $articles; |
|
} |
|
|
|
public function getArticles(array $slugs): array |
|
{ |
|
$articles = []; |
|
$subscription = new Subscription(); |
|
$subscriptionId = $subscription->setId(); |
|
$filter = new Filter(); |
|
$filter->setKinds([KindsEnum::LONGFORM]); |
|
$filter->setTag('#d', $slugs); |
|
$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': |
|
$articles[] = $item->event; |
|
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 |
|
} |
|
} |
|
} |
|
return $articles; |
|
} |
|
}
|
|
|