diff --git a/src/Examples/publish-event.php b/src/Examples/publish-event.php index 3194e1f..b465613 100644 --- a/src/Examples/publish-event.php +++ b/src/Examples/publish-event.php @@ -8,6 +8,7 @@ use swentel\nostr\Key\Key; use swentel\nostr\Message\EventMessage; use swentel\nostr\Relay\Relay; +use swentel\nostr\Request\Request; use swentel\nostr\Sign\Sign; try { @@ -28,13 +29,18 @@ $relay = new Relay('wss://relay.nostr.band'); $eventMessage = new EventMessage($note); $relay->setMessage($eventMessage); - /** @var \swentel\nostr\RelayResponse\RelayResponse $response */ - $response = $relay->send(); + $request = new Request($relay, $eventMessage); + $response = $request->send(); // Handle response. - if ($response->isSuccess) { - print 'The event has been transmitted to the relay' . PHP_EOL; - $eventId = $response->eventId; - // Now we could request the event with this id. + foreach ($response as $relayUrl => $relayResponses) { + foreach ($relayResponses as $relayResponse) { + if ($relayResponse->isSuccess) { + print 'The event has been transmitted to the relay ' . $relayUrl . PHP_EOL; + $eventId = $relayResponse->eventId; + print 'The received event id from the relay: ' . $relayResponse->eventId; + // Now we could request the event with this id. + } + } } } catch (Exception $e) { print 'Exception error: ' . $e->getMessage() . PHP_EOL; diff --git a/src/Examples/request-events-filtered-with-tags.php b/src/Examples/request-events-filtered-with-tags.php new file mode 100644 index 0000000..fbc5242 --- /dev/null +++ b/src/Examples/request-events-filtered-with-tags.php @@ -0,0 +1,73 @@ +setId(); + + $filter1 = new Filter(); + $filter1->setKinds([1]); + $filter1->setLimit(25); + /** + * Please have a look at this overview with tags you can use according to the Nostr NIPs: + * https://nips.nostr.com/#standardized-tags + */ + // Apply multiple tags to the filter. + $filter1->setTags( + [ + '#t' => ['PHP', 'Drupal'], + '#p' => ['06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71'], + ], + ); + // Apply a single tag to the filter which in this case will be appended to the #t tag. + $filter1->setTag('#t', ['Wordpress']); + // Apply an e-tag. + //$filter1->setLowercaseETags([ + // '06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71' + //]); + // Apply a p-tag. + //$filter1->setLowercasePTags([ + // '06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71' + //]); + /** + * If you're using multiple conditions in one filter, these conditions are interpreted as && (AND) conditions. + * If you would like to use || (OR) conditions, you should use multiple filters. + */ + $filters = [$filter1]; + $requestMessage = new RequestMessage($subscriptionId, $filters); + $relay = new Relay('wss://relay.nostr.band'); + $request = new Request($relay, $requestMessage); + $response = $request->send(); + + /** + * @var string $relayUrl + * The relay URL. + * @var object $relayResponse + * RelayResponse which will contain the messages returned by the relay. + * Each message will also contain the event. + */ + foreach ($response as $relayUrl => $relayResponses) { + print 'Received ' . count($response[$relayUrl]) . ' message(s) received from relay ' . $relayUrl . PHP_EOL; + /** @var RelayResponse $relayResponse */ + foreach ($relayResponses as $relayResponse) { + if ($relayResponse instanceof RelayResponseEvent) { + if (isset($relayResponse->event->content)) { + print $relayResponse->event->content . PHP_EOL; + } + } + } + } +} catch (Exception $e) { + print 'Exception error: ' . $e->getMessage() . PHP_EOL; +} diff --git a/src/Examples/request-events-from-multiple-relays.php b/src/Examples/request-events-from-multiple-relays.php index 4e653ef..64571ee 100644 --- a/src/Examples/request-events-from-multiple-relays.php +++ b/src/Examples/request-events-from-multiple-relays.php @@ -48,5 +48,5 @@ } } } catch (Exception $e) { - echo $e->getMessage(); + print $e->getMessage() . PHP_EOL; } diff --git a/src/Examples/request-events-with-auth.php b/src/Examples/request-events-with-auth.php index 8da60ac..437e165 100644 --- a/src/Examples/request-events-with-auth.php +++ b/src/Examples/request-events-with-auth.php @@ -28,12 +28,12 @@ $request = new Request($relay, $requestMessage); $response = $request->send(); - foreach ($response as $relay => $messages) { + foreach ($response as $relay => $relayResponses) { print 'Received ' . count($response[$relay]) . ' message(s) received from relay ' . $relay . PHP_EOL; - foreach ($messages as $message) { - print $message->type . ': ' . $message->message . PHP_EOL; - if ($message instanceof RelayResponseEvent) { - $rawEvent = $message->event; + foreach ($relayResponses as $relayResponse) { + print 'Relay response ' . $relayResponse->type . ': ' . $relayResponse->message . PHP_EOL; + if ($relayResponse instanceof RelayResponseEvent) { + $rawEvent = $relayResponse->event; $event = new Event(); $event->setId($rawEvent->id); $event->setPublicKey($rawEvent->pubkey); diff --git a/src/Examples/request-events.php b/src/Examples/request-events.php index b560a5b..db8f109 100644 --- a/src/Examples/request-events.php +++ b/src/Examples/request-events.php @@ -26,19 +26,19 @@ /** * @var string $relayUrl * The relay URL. - * @var object $relayResponses - * RelayResponses which will contain the messages returned by the relay. + * @var object $relayResponse + * RelayResponse which will contain the messages returned by the relay. * Each message will also contain the event. */ foreach ($response as $relayUrl => $relayResponses) { print 'Received ' . count($response[$relayUrl]) . ' message(s) received from relay ' . $relayUrl . PHP_EOL; /** @var \swentel\nostr\RelayResponse\RelayResponseEvent $message */ - foreach ($relayResponses as $message) { - if (isset($message->event->content)) { - print $message->event->content . PHP_EOL; + foreach ($relayResponses as $relayResponse) { + if (isset($relayResponse->event->content)) { + print $relayResponse->event->content . PHP_EOL; } } } } catch (Exception $e) { - print 'Exception error: ' . $e->getMessage() . '\n'; + print 'Exception error: ' . $e->getMessage() . PHP_EOL; } diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index 1bc2543..a67f8eb 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -7,6 +7,8 @@ use swentel\nostr\FilterInterface; use swentel\nostr\Key\Key; +use function PHPUnit\Framework\throwException; + class Filter implements FilterInterface { /** @@ -24,6 +26,11 @@ class Filter implements FilterInterface */ public array $kinds; + /** + * A list of tags values starting with a # followed by single letters (format: #). + */ + public array $tags; + /** * A list of #e tag values (list of event ids) */ @@ -101,6 +108,68 @@ public function setKinds(array $kinds): static return $this; } + /** + * Set tags for the Filter object. + * Every tag in this filter property needs to start with a #. + * + * @param array $tags + * The array of tags to set. + * @return Filter + */ + public function setTags(array $tags): static + { + foreach ($tags as $name => $value) { + if (!is_array($value)) { + $message = sprintf('Provided tag value for %s must be an array', $name); + throw new \RuntimeException($message); + } + $this->validateTagName($name); + $this->setTag($name, $value); + } + return $this; + } + + /** + * Set a single tag value for the Filter object. + * + * @param string $name + * Tag name. + * @param string $value + * Tag value. + * @return Filter + */ + public function setTag(string $name, array $value): static + { + $this->validateTagName($name); + if (isset($this->{$name})) { + $this->{$name} = array_merge($this->{$name}, $value); + } else { + $this->{$name} = $value; + } + return $this; + } + + /** + * Validate standardized tag. + * + * @param $tag + * Provided tag name to be validated. + * @return void + */ + private function validateTagName($tag): void + { + // Check if tag starts with #. + if (!str_starts_with($tag, '#')) { + throw new \RuntimeException('All tags must start with #'); + } + // Check if tag has valid value. + $pattern = '/^#[a-z_-]+$/i'; + if (!preg_match($pattern, $tag)) { + $message = sprintf('Invalid tag provided: %s', $tag); + throw new \RuntimeException($message); + } + } + /** * Set the #e tag for the Filter object. * @@ -146,6 +215,9 @@ public function setLowercasePTags(array $ptags): static */ public function setSince(int $since): static { + if (!$this->isValidTimestamp($since)) { + throw new \RuntimeException("The provided since filter is not a valid timestamp"); + } $this->since = $since; return $this; } @@ -158,6 +230,9 @@ public function setSince(int $since): static */ public function setUntil(int $until): static { + if (!$this->isValidTimestamp($until)) { + throw new \RuntimeException("The provided until filter is not a valid timestamp"); + } $this->until = $until; return $this; } diff --git a/src/Message/RequestMessage.php b/src/Message/RequestMessage.php index a5d48fa..25b174f 100644 --- a/src/Message/RequestMessage.php +++ b/src/Message/RequestMessage.php @@ -37,9 +37,7 @@ public function __construct(string $subscriptionId, array $filters) { $this->subscriptionId = $subscriptionId; $this->setType(MessageTypeEnum::REQUEST); - foreach ($filters as $filter) { - $this->filters[] = $filter->toArray(); - } + $this->processFilters($filters); } /** @@ -63,4 +61,24 @@ public function generate(): string $requestArray = array_merge([$this->type, $this->subscriptionId], $this->filters); return json_encode($requestArray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } + + /** + * @param array $filters + * Process filters for the request message. + * @return void + */ + private function processFilters(array $filters): void + { + /** @var Filter\Filter $filter */ + foreach ($filters as $filter) { + // Process tag values from $filter->tags. + if (isset($filter->tags)) { + foreach ($filter->tags as $key => $tag) { + $filter->{$key} = [$tag]; + } + unset($filter->tags); + } + $this->filters[] = $filter->toArray(); + } + } } diff --git a/src/Relay/Relay.php b/src/Relay/Relay.php index d915e7d..e8ee74a 100644 --- a/src/Relay/Relay.php +++ b/src/Relay/Relay.php @@ -97,6 +97,7 @@ private function setPayload(string $payload): void */ public function send(): RelayResponse { + // TODO: deprecate this and replace with $request->send($relay, $message) logic. $this->validateUrl(); try { diff --git a/src/Relay/RelaySet.php b/src/Relay/RelaySet.php index 7fce751..43f1d19 100644 --- a/src/Relay/RelaySet.php +++ b/src/Relay/RelaySet.php @@ -114,6 +114,7 @@ public function isConnected(): bool */ public function send(): array { + // TODO: deprecate this and replace with $request->send($relaySet, $message) logic. try { // Send message to each relay defined in this set. /** @var Relay $relay */ diff --git a/src/Request/Request.php b/src/Request/Request.php index 737cbee..b7a3afd 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -6,6 +6,7 @@ use swentel\nostr\Event\Event; use swentel\nostr\Message\AuthMessage; +use swentel\nostr\MessageInterface; use swentel\nostr\Nip42\AuthEvent; use swentel\nostr\Relay\Relay; use swentel\nostr\Relay\RelaySet; @@ -44,7 +45,7 @@ class Request implements RequestInterface * Constructor for the Request class. * Initializes the url and payload properties based on the provided websocket and message. */ - public function __construct(Relay|RelaySet $relay, $message) + public function __construct(Relay|RelaySet $relay, MessageInterface $message) { if ($relay instanceof RelaySet) { $this->relays = $relay; @@ -105,6 +106,7 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse $client->setTimeout(60); $client->text($this->payload); + // The Nostr subscription lifecycle within a websocket connection lifecycle. while ($response = $client->receive()) { if ($response === null) { $response = [ @@ -119,19 +121,41 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse } elseif ($response instanceof Text) { $relayResponse = RelayResponse::create(json_decode($response->getContent())); $this->responses[] = $relayResponse; + // NIP-01 - Response OK from the relay. + if ($relayResponse->type === 'OK' && $relayResponse->status === false) { + // Something went wrong, see message from the relay why. + $client->disconnect(); + throw new \Exception($relayResponse->message); + } + if ($relayResponse->type === 'OK' && $relayResponse->status === true) { + if (isset($relayResponse->eventId) && $relayResponse->eventId !== '') { + // Event is transmitted to the relay. + // TODO: send closeMessage to relay. + $client->disconnect(); + break; + } + } + // NIP-01 - Response EVENT from the relay. + if ($relayResponse->type === 'EVENT') { + // Do nothing. + } + // NIP-01 - Response EOSE from the relay. if ($relayResponse->type === 'EOSE') { + // We should send closeMessage to the relay here. $client->disconnect(); break; } if ($relayResponse->type === 'OK' && $relayResponse->status === false) { + // We should send closeMessage to the relay here. $client->disconnect(); throw new \Exception($relayResponse->message); } - // NIP-42 + // NIP-42 - Response AUTH from the relay. if ($relayResponse->type === 'AUTH') { // Save challenge string in session. $_SESSION['challenge'] = $relayResponse->message; } + // NIP-01 - Response CLOSED from the relay. if ($relayResponse->type === 'CLOSED') { // NIP-42 // We do need to broadcast a signed event verification here to the relay. @@ -167,7 +191,9 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse /** @var RelayResponse $response */ $response = RelayResponse::create(json_decode($message->getContent())); $this->responses[] = $response; + $client->stop(); if ($response->type === 'EOSE') { + // We should send closeMessage to the relay here. $client->disconnect(); } })->start(); @@ -179,10 +205,14 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse $client->disconnect(); throw new \Exception($relayResponse->message); } + $client->disconnect(); + break; } } } - $client->disconnect(); + if ($client->isConnected()) { + $client->disconnect(); + } $client->close(); return $this->responses; }