Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read events from relays #57

Merged
merged 9 commits into from
Jun 20, 2024
120 changes: 116 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ $eventMessage = new EventMessage($note);
$message_string = $eventMessage->generate();
```

## Interacting with a relay
## Publish an event to a relay

Publish an event with a note that has been prepared for sending to a relay.

Expand All @@ -93,10 +93,115 @@ $signer->signEvent($note, $private_key);
$eventMessage = new EventMessage($note);

$relayUrl = 'wss://nostr-websocket.tld';
$relay = new Relay($relayUrl, $eventMessage);
$relay = new Relay($relayUrl);
$relay->setMessage($eventMessage);
$result = $relay->send();
```

If you would like to publish the event to multiple relays, you can use the `RelaySet` class.

```php
$relay1 = new Relay(''wss://nostr-websocket1.tld'');
$relay2 = new Relay(''wss://nostr-websocket2.tld'');
$relay3 = new Relay(''wss://nostr-websocket3.tld'');
$relay4 = new Relay(''wss://nostr-websocket4.tld'');
$relaySet = new RelaySet();
$relaySet->setRelays([$relay1, $relay2, $relay3, $relay4]);
$relaySet->setMessage($eventMessage);
$result = $relay->send();
```

## Read events from a relay

Fetch events from a relay.

```php
$subscription = new Subscription();
$subscriptionId = $subscription->setId();

$filter1 = new Filter();
$filter1->setKinds([1, 3]); // You can add multiple kind numbers
$filter1->setLimit(25); // Limit to fetch only a maximum of 25 events
$filters = [$filter1]; // You can add multiple filters.

$requestMessage = new RequestMessage($subscriptionId, $filters);

$relayUrl = 'wss://nostr-websocket.tld';
$relay = new Relay($relayUrl);
$relay->setMessage($requestMessage);

$request = new Request($relay, $requestMessage);
$response = $request->send();
```

`$response` is an multidimensional array with elements containing each a response message (JSON string) decoded to an array from the relay and sorted by the relay.
Output example:
```php
[
'wss://nostr-websocket.tld' => [
0 => [
"EVENT",
"A8kWzjCVUHSD1rmuwGqyK2PxsolZMO9YXditbg05fch6p3Q4eT7vRFLEJINBna",
[
'id' => '1e8534623845629d40f7761c0577edf10f778c490e7b95a524845d9280c7c25a',
'kind' => 1,
'pubkey' => '06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71',
'created_at' => 1718723787,
'content' => 'Losing your social graph can feel the same for some I think 😮 ',
'tags' => [
['e', 'f754a238947b7f32168f872650a8dd0b9376493e58005d7e0b8be52f6f229364', 'wss://nos.lol/', 'root'],
['e', 'fe7dd6ba22fa0aa39370aa160226b8bc2413460621c8d67ce862205ad5a02c24', 'wss://nos.lol/', 'reply'],
['p', 'fb1366abd5e4c92a8a950791bc72d51bde291a83555cb2c629a92fedd78068ac', '', 'mention']
],
'sig' => '888c9b5d9e0b69eba3510dd2b5d03eddcf0a680ab0e7673820fb36a56448ad80701042a669c7ef9918593c5a41c8b3ccc1d82ade50f32b62dd843144f32df403'
],
1 => [
"EVENT",
"A8kWzjCVUHSD1rmuwGqyK2PxsolZMO9YXditbg05fch6p3Q4eT7vRFLEJINBna",
[
...Nostr event
]
],
2 => [
...
],
3 => [
...
],
4 => [
...
]
]
]

```

## Read events from a set of relays

Read events from a set of relays with the `RelaySet` class.
It's basically the same snippet as above with the difference you create a `RelaySet` class and pass it through the `Request` object.

```php
$subscription = new Subscription();
$subscriptionId = $subscription->setId();

$filter1 = new Filter();
$filter1->setKinds([1]);
$filter1->setLimit(5);
$filters = [$filter1];
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relays = [
new Relay('wss://nostr-websocket-1.tld'),
new Relay('wss://nostr-websocket-2.tld'),
new Relay('wss://nostr-websocket-3.tld'),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);

$request = new Request($relaySet, $requestMessage);
$response = $request->send();
```

## Generating a private key and a public key

```php
Expand Down Expand Up @@ -161,13 +266,20 @@ private key on command line.
- [x] Event validation (issue [#17](https://github.com/swentel/nostr-php/issues/17))
- [ ] Support NIP-01 basic protocol flow description
- [x] Publish events
- [ ] Request events (pr [#48](https://github.com/swentel/nostr-php/pull/48))
- [x] Request events (issue [#55](https://github.com/nostrver-se/nostr-php/pull/55) credits to [kriptonix](https://github.com/kriptonix))
- [ ] Implement all types of relay responses
- [ ] EVENT - sends events requested by the client
- [ ] OK - indicate an acceptance or denial of an EVENT message
- [ ] EOSE - end of stored events
- [ ] CLOSED - subscription is ended on the server side
- [ ] NOTICE - used to send human-readable messages (like errors) to clients
- [ ] Improve handling relay responses
- [ ] Support NIP-19 bech32-encoded identifiers
- [ ] Support NIP-42 authentication of clients to relays
- [ ] Support NIP-45 event counts
- [ ] Support NIP-50 search capability
- [ ] Support multi-threading for handling requests simultaneously
- [ ] Support multi-threading (async concurrency) for handling requests simultaneously
- [ ] Support realtime (runtime) subscriptions with the `bin/nostr-php` CLI client to listen to new events from relays

## Community

Expand Down
191 changes: 191 additions & 0 deletions src/Filter/Filter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

declare(strict_types=1);

namespace swentel\nostr\Filter;

use swentel\nostr\FilterInterface;

class Filter implements FilterInterface
{
/**
* A list of event ids
*/
public array $id;

/**
* A list of lowercase pubkeys, the pubkey of an event must be one of these
*/
public array $authors;

/**
* A list of a kind numbers
*/
public array $kinds;

/**
* A list of #e tag values (list of event ids)
*/
public array $etags;

/**
* A list of #p tag values (list of pubkeys).
*/
public array $ptags;

/**
* An integer unix timestamp in seconds, events must be newer than this to pass
*/
public int $since;

/**
* An integer unix timestamp in seconds, events must be older than this to pass
*/
public int $until;

/**
* Maximum number of events relays SHOULD return in the initial query
*/
public int $limit;

/**
* Set the authors for the Filter object.
*
* @param array $pubkey The array of authors to set.
*/
public function setAuthors(array $pubkeys): static
{
foreach($pubkeys as $key) {
if(!$this->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 setLowercaseETags(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 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("#p 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;
}
}
Loading