From 4b62d72e7e3afc18a90176791e9c042ced751ed3 Mon Sep 17 00:00:00 2001 From: kriptonovac Date: Tue, 11 Jun 2024 13:08:44 +0000 Subject: [PATCH 1/2] Add read from relay feature --- src/Filter/Filter.php | 193 ++++++++++++++++++++++++++++++ src/FilterInterface.php | 82 +++++++++++++ src/Message/CloseMessage.php | 28 +++++ src/Message/RequestMessage.php | 47 ++++++++ src/Request/Request.php | 85 +++++++++++++ src/RequestInterface.php | 15 +++ src/Subscription/Subscription.php | 19 +++ src/SubscriptionInterface.php | 10 ++ tests/RequestTest.php | 42 +++++++ 9 files changed, 521 insertions(+) create mode 100644 src/Filter/Filter.php create mode 100644 src/FilterInterface.php create mode 100644 src/Message/CloseMessage.php create mode 100644 src/Message/RequestMessage.php create mode 100644 src/Request/Request.php create mode 100644 src/RequestInterface.php create mode 100644 src/Subscription/Subscription.php create mode 100644 src/SubscriptionInterface.php create mode 100644 tests/RequestTest.php diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php new file mode 100644 index 0000000..f09be12 --- /dev/null +++ b/src/Filter/Filter.php @@ -0,0 +1,193 @@ +isLowercaseHex($key)){ + throw new \RuntimeException("Author pubkeys must be an array of 64-character lowercase hex values"); + } + } + $this->authors = $pubkeys; + return $this; + } + + /** + * Set the kinds for the Filter object. + * + * @param array $kinds The array of kinds to set. + */ + public function setKinds(array $kinds): static + { + $this->kinds = $kinds; + return $this; + } + + /** + * Set the #e tag for the Filter object. + * + * @param array $etag The array of tag to set. + */ + public function setETag(array $etags): static + { + foreach($etags as $tag){ + if(!$this->isLowercaseHex($tag)){ + throw new \RuntimeException("#e tags must be an array of 64-character lowercase hex values"); + } + } + $this->etags = $etags; + return $this; + } + + /** + * Set the #p tag for the Filter object. + * + * @param array $ptag The array of tag to set. + */ + public function setPTag(array $ptags): static + { + // Check IF array contain exact 64-character lowercase hex values + foreach($ptags as $tag){ + if(!$this->isLowercaseHex($tag)){ + throw new \RuntimeException("#e tags must be an array of 64-character lowercase hex values"); + } + } + $this->ptags = $ptags; + return $this; + } + + /** + * Set the since for the Filter object. + * + * @param int $since The limit to set. + */ + public function setSince(int $since): static + { + $this->since = $since; + return $this; + } + + /** + * Set the until for the Filter object. + * + * @param int $until The limit to set. + */ + public function setUntil(int $until): static + { + $this->until = $until; + return $this; + } + + /** + * Set the limit for the Filter object. + * + * @param int $limit The limit to set. + */ + public function setLimit(int $limit): static + { + $this->limit = $limit; + return $this; + } + + /** + * Check if a given string is a 64-character lowercase hexadecimal value. + * + * @param string $string The string to check. + * @return bool True if the string is a 64-character lowercase hexadecimal value, false otherwise. + */ + public function isLowercaseHex($string): bool + { + // Regular expression to match 64-character lowercase hexadecimal value + $pattern = '/^[a-f0-9]{64}$/'; + + // Check if the string matches the pattern + return preg_match($pattern, $string) === 1; + } + + /** + * Check if a given timestamp is valid. + * + * @param mixed $timestamp The timestamp to check. + * @return bool True if the timestamp is valid, false otherwise. + */ + public function isValidTimestamp($timestamp): bool + { + // Convert the timestamp to seconds + $timestamp = (int) $timestamp; + + // Check if the timestamp is valid + return ($timestamp !== 0 && $timestamp !== false && $timestamp !== -1); + } + + /** + * Return an array representation of the object by iterating through its properties. + * + * @return array The array representation of the object. + */ + public function toArray(): array + { + $array = []; + foreach (get_object_vars($this) as $key => $val) { + if($key === 'etags'){ + $array['#e'] = $val; + }elseif($key === 'ptags'){ + $array['#p'] = $val; + }else{ + $array[$key] = $val; + } + } + return $array; + } +} \ No newline at end of file diff --git a/src/FilterInterface.php b/src/FilterInterface.php new file mode 100644 index 0000000..feaa1d3 --- /dev/null +++ b/src/FilterInterface.php @@ -0,0 +1,82 @@ +subscriptionId = $subscriptionId; + } + + /** + * {@inheritdoc} + */ + public function generate(): string + { + return '["CLOSE", "' . $this->subscriptionId . '"]'; + } +} diff --git a/src/Message/RequestMessage.php b/src/Message/RequestMessage.php new file mode 100644 index 0000000..8abfd17 --- /dev/null +++ b/src/Message/RequestMessage.php @@ -0,0 +1,47 @@ +subscriptionId = $subscriptionId; + foreach($filters as $filter){ + $this->filters[] = $filter->toArray(); + } + } + + /** + * Generates a JSON-encoded request array by merging the subscription ID and filters array. + * + * @return string The JSON-encoded request array + */ + public function generate(): string + { + $requestArray = array_merge(["REQ", $this->subscriptionId], $this->filters); + return json_encode($requestArray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } +} \ No newline at end of file diff --git a/src/Request/Request.php b/src/Request/Request.php new file mode 100644 index 0000000..95c4507 --- /dev/null +++ b/src/Request/Request.php @@ -0,0 +1,85 @@ +url = $websocket; + $this->payload = $message->generate(); + } + + /** + * Method to send a request using WebSocket client, receive responses, and handle errors. + * + * @return array The array of responses received or an error message if connection fails. + */ + public function send(): array + { + try { + $client = new WebSocket\Client($this->url); + $client->text($this->payload); + + $result = []; + + /** + * When sending 'CLOSE' request to close a subscription, it is not guaranteed that we + * will receive a response confirming that the subscription with the given ID is closed + * as the protocol does not mandate a specific response for a "CLOSE" request + * + * We can handle this either by: + * - closing connection upon sending the request + * - waiting for a certain period to see if further events are received for that subscription ID + * - waiting for ping from server to close connection (in which case the server indicates the + * connection is still alive, but it does not confirm the closure of the subscription) + */ + while ($response = $client->receive()) { + if ($response instanceof WebSocket\Message\Ping) { + $client->disconnect(); + return $result; + } elseif ($response instanceof WebSocket\Message\Text) { + $response = json_decode($response->getContent()); + if ($response[0] === 'NOTICE' || $response[0] === 'CLOSED') { + $client->disconnect(); + throw new \RuntimeException($response[0] === 'NOTICE' ? $response[1] : $response[2]); + } elseif ($response[0] === 'EOSE') { + break; + } else { + $result[] = $response; + } + } + } + $client->disconnect(); + } catch (WebSocket\ConnectionException $e) { + $result = [ + 'ERROR', + '', + false, + $e->getMessage() + ]; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/RequestInterface.php b/src/RequestInterface.php new file mode 100644 index 0000000..c59cff9 --- /dev/null +++ b/src/RequestInterface.php @@ -0,0 +1,15 @@ +setId(); + + $filter = new Filter(); + $filter->setKinds([1]); + $filter->setLimit(3); + + $filters = [$filter]; + + // Mocking the WebSocket\Client + $mockClient = $this->getMockBuilder(Client::class) + ->setConstructorArgs([$relayUrl]) + ->getMock(); + + $requestMessage = new RequestMessage($subscriptionId, $filters); + $request = new Request($relayUrl, $requestMessage, $mockClient); + + $result = $request->send(); + + $this->assertNotEmpty($result, 'Request send result should not be empty'); + } +} From 5601f87e2f17c0af47581f96f7d0eb978fd70047 Mon Sep 17 00:00:00 2001 From: kriptonovac Date: Tue, 18 Jun 2024 09:30:17 +0000 Subject: [PATCH 2/2] Update PR: removed unused statement, corrected typos, changed names of setPTag method to setLowercasePTags, changed name of setETags method to setLowercaseETags --- src/Filter/Filter.php | 32 +++++++++++++++---------------- src/FilterInterface.php | 12 +++++------- src/Message/RequestMessage.php | 4 ++-- src/Request/Request.php | 18 ++++++++--------- src/Subscription/Subscription.php | 3 +-- src/SubscriptionInterface.php | 2 +- 6 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index f09be12..870a908 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -39,7 +39,7 @@ class Filter implements FilterInterface public int $since; /** - * An integer unix timestamp in seconds, events must be older than this to pass + * An integer unix timestamp in seconds, events must be older than this to pass */ public int $until; @@ -55,8 +55,8 @@ class Filter implements FilterInterface */ public function setAuthors(array $pubkeys): static { - foreach($pubkeys as $key){ - if(!$this->isLowercaseHex($key)){ + foreach($pubkeys as $key) { + if(!$this->isLowercaseHex($key)) { throw new \RuntimeException("Author pubkeys must be an array of 64-character lowercase hex values"); } } @@ -80,10 +80,10 @@ public function setKinds(array $kinds): static * * @param array $etag The array of tag to set. */ - public function setETag(array $etags): static + public function setLowercaseETags(array $etags): static { - foreach($etags as $tag){ - if(!$this->isLowercaseHex($tag)){ + foreach($etags as $tag) { + if(!$this->isLowercaseHex($tag)) { throw new \RuntimeException("#e tags must be an array of 64-character lowercase hex values"); } } @@ -96,12 +96,12 @@ public function setETag(array $etags): static * * @param array $ptag The array of tag to set. */ - public function setPTag(array $ptags): static + public function setLowercasePTags(array $ptags): static { // Check IF array contain exact 64-character lowercase hex values - foreach($ptags as $tag){ - if(!$this->isLowercaseHex($tag)){ - throw new \RuntimeException("#e tags must be an array of 64-character lowercase hex values"); + foreach($ptags as $tag) { + if(!$this->isLowercaseHex($tag)) { + throw new \RuntimeException("#p tags must be an array of 64-character lowercase hex values"); } } $this->ptags = $ptags; @@ -147,11 +147,10 @@ public function setLimit(int $limit): static * @param string $string The string to check. * @return bool True if the string is a 64-character lowercase hexadecimal value, false otherwise. */ - public function isLowercaseHex($string): bool + public function isLowercaseHex($string): bool { // Regular expression to match 64-character lowercase hexadecimal value $pattern = '/^[a-f0-9]{64}$/'; - // Check if the string matches the pattern return preg_match($pattern, $string) === 1; } @@ -166,7 +165,6 @@ public function isValidTimestamp($timestamp): bool { // Convert the timestamp to seconds $timestamp = (int) $timestamp; - // Check if the timestamp is valid return ($timestamp !== 0 && $timestamp !== false && $timestamp !== -1); } @@ -180,14 +178,14 @@ public function toArray(): array { $array = []; foreach (get_object_vars($this) as $key => $val) { - if($key === 'etags'){ + if($key === 'etags') { $array['#e'] = $val; - }elseif($key === 'ptags'){ + } elseif($key === 'ptags') { $array['#p'] = $val; - }else{ + } else { $array[$key] = $val; } } return $array; } -} \ No newline at end of file +} diff --git a/src/FilterInterface.php b/src/FilterInterface.php index feaa1d3..45a4f6d 100644 --- a/src/FilterInterface.php +++ b/src/FilterInterface.php @@ -25,15 +25,14 @@ public function setKinds(array $kinds): static; * * @param array $tag The array of tag to set. */ - public function setETag(array $tag): static; - + public function setLowercaseETags(array $tag): static; /** * Set the #p tag for the Filter object. * * @param array $ptag The array of tag to set. */ - public function setPTag(array $ptag): static; + public function setLowercasePTags(array $ptag): static; /** * Set the since for the Filter object. @@ -48,13 +47,13 @@ public function setSince(int $since): static; * @param int $until The limit to set. */ public function setUntil(int $until): static; - + /** * Set the limit for the Filter object. * * @param int $limit The limit to set. */ - public function setLimit(int $limit):static; + public function setLimit(int $limit): static; /** * Check if a given string is a 64-character lowercase hexadecimal value. @@ -78,5 +77,4 @@ public function isValidTimestamp($timestamp): bool; * @return array The array representation of the object. */ public function toArray(): array; - -} \ No newline at end of file +} diff --git a/src/Message/RequestMessage.php b/src/Message/RequestMessage.php index 8abfd17..20b9a53 100644 --- a/src/Message/RequestMessage.php +++ b/src/Message/RequestMessage.php @@ -29,7 +29,7 @@ class RequestMessage implements MessageInterface public function __construct(string $subscriptionId, array $filters) { $this->subscriptionId = $subscriptionId; - foreach($filters as $filter){ + foreach($filters as $filter) { $this->filters[] = $filter->toArray(); } } @@ -44,4 +44,4 @@ public function generate(): string $requestArray = array_merge(["REQ", $this->subscriptionId], $this->filters); return json_encode($requestArray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } -} \ No newline at end of file +} diff --git a/src/Request/Request.php b/src/Request/Request.php index 95c4507..6f2281b 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -4,7 +4,6 @@ namespace swentel\nostr\Request; -use swentel\nostr\Message\RequestMessage; use swentel\nostr\RequestInterface; use WebSocket; @@ -45,13 +44,12 @@ public function send(): array /** * When sending 'CLOSE' request to close a subscription, it is not guaranteed that we - * will receive a response confirming that the subscription with the given ID is closed + * will receive a response confirming that the subscription with the given ID is closed * as the protocol does not mandate a specific response for a "CLOSE" request - * - * We can handle this either by: + * We can handle this either by: * - closing connection upon sending the request * - waiting for a certain period to see if further events are received for that subscription ID - * - waiting for ping from server to close connection (in which case the server indicates the + * - waiting for ping from server to close connection (in which case the server indicates the * connection is still alive, but it does not confirm the closure of the subscription) */ while ($response = $client->receive()) { @@ -73,13 +71,13 @@ public function send(): array $client->disconnect(); } catch (WebSocket\ConnectionException $e) { $result = [ - 'ERROR', - '', - false, - $e->getMessage() + 'ERROR', + '', + false, + $e->getMessage(), ]; } return $result; } -} \ No newline at end of file +} diff --git a/src/Subscription/Subscription.php b/src/Subscription/Subscription.php index 6a6cba5..0742422 100644 --- a/src/Subscription/Subscription.php +++ b/src/Subscription/Subscription.php @@ -12,8 +12,7 @@ public function setId($length = 64): string { // String of all alphanumeric character $str_result = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - // Shuffle the $str_result and returns substring of specified length return substr(str_shuffle($str_result), 0, $length); } -} \ No newline at end of file +} diff --git a/src/SubscriptionInterface.php b/src/SubscriptionInterface.php index 872dccd..922ccb9 100644 --- a/src/SubscriptionInterface.php +++ b/src/SubscriptionInterface.php @@ -7,4 +7,4 @@ interface SubscriptionInterface { public function setId($length): string; -} \ No newline at end of file +}