Nostrの読み書き
1年前の記事です。
Laravelは古い情報は全く役に立たないので絶対に参考にしないでください。
コメントに新しい情報がないか確認してください。
- PHPではシュノア署名が難しそうなので代わりの手段
- 「読み」だけなら簡単
- Laravelから使うパッケージ
- NostrClient
- SocialClient
- Laravel Notifications
- おわり
PHPではシュノア署名が難しそうなので代わりの手段
node.jsのnostr-toolsを使って必要な機能を提供するAPIをVercelに用意した。
これなら自分で署名できなくても何でも使える。PHPネイティブで署名できるようになれば変えるかもしれないけどしばらくはこれで対応。
「読み」だけなら簡単
署名が不要なのでこういうWebSocketクライアントだけで可能。
Pure PHP版。nostr.phpを以下の内容で作ってphp nostr.php
で実行。
<?php
require_once './vendor/autoload.php';
use WebSocket\Client;
use WebSocket\ConnectionException;
$relay = 'wss://relay.damus.io';
$sub_id = bin2hex(random_bytes(5));
echo $sub_id.PHP_EOL;
$client = new Client($relay);
$client->send(json_encode([
'REQ',
$sub_id,
[
'limit' => 10,
'kinds' => [1],
],
]));
while (true) {
try {
$response = $client->receive();
echo $response;
echo PHP_EOL.'----'.PHP_EOL;
$event = json_decode($response, true);
if ($event[0] === 'EOSE') {
break;
}
} catch (ConnectionException $e) {
echo $e->getMessage();
}
}
$client->send(json_encode([
'CLOSE',
$sub_id,
]));
$client->close();
kind1=Noteを10件取得。EOSEが来たら終了。EOSEで終了してるのでWebサーバーでもこれで動かせるはず。EOSEで終了しなければずっと起動しつつ新しいイベントが来たらすぐに何かするような処理もできる。
余計なものに依存したくなく読みだけならこれでもいいけど使いにくいので読み部分もnostr-api
を使っていく。
Laravelから使うパッケージ
今回は最初からPHP8.1以上用なので名前付き引数やEnumを遠慮なく使う前提の設計。
まだ引数名は容赦なく変わる。ここで使い方を書いても役に立たなくなるので影響がなさそうな範囲だけ書く。
NostrClient
nostr-api
を使うための基本的なクライアント。
上の例と同じ最近の10件取得
use Illuminate\Http\Client\Response;
use Revolution\Nostr\Facades\Nostr;
use Revolution\Nostr\Filter;
use Revolution\Nostr\Kind;
$filter = new Filter(
kinds: [Kind::Text],
limit: 10,
);
/** @var Response $response */
$response = Nostr::event()->list([$filter]);
// $responseはLaravelのHTTPクライアントのResponseなので後の使い方は同じ。
$events = $response->json('events');
// $eventsはこんな配列
// [
// [
// 'id' => '...1',
// 'kind' => 1,
// 'content' => '...',
// ],
// [
// 'id' => '...2',
// 'kind' => 1,
// 'content' => '...',
// ],
// ]
リレーサーバーはconfig/nostr.php
のrelaysの一つ目が自動的に使われる。別のリレーを指定してもいい。
$response = Nostr::event()->withRelay('wss://')->list([$filter]);
$response = Nostr::event()->list([$filter], 'wss://');
authorsで自分のpubkeyを指定すれば自分のノートを取得できる。
$filter = new Filter(
authors: [$pk],
kinds: [Kind::Text],
limit: 10,
);
新しいノートの投稿
「読み」では何もいらないけど「書き」では署名のための秘密鍵skが必須。
use Revolution\Nostr\Facades\Nostr;
use Revolution\Nostr\Event;
use Revolution\Nostr\Kind;
$event = new Event(
kind: Kind::Text,
content: 'test',
created_at: now()->timestamp,
tags: [],
);
// secret key. Userモデルなどに保存しておく。
$sk = $user->nostr_sk;
$response = Nostr::event()->publish(event: $event, sk: $sk);
if($response->successful()) {
// 送信した署名済みeventもresponseに含めているので何かに使えるかも。
$event = $response->json('event');
}
//エラー時はステータス500
これは一つのリレーにしか送信してない。
新しいノートを複数のリレーに投稿
Nostr::pool()
を使うとconfig/nostr.php
のrelays全部に送信。nostr-api
のVercelに対して複数のリクエストを送っているので大量に送りすぎると良くないかもしれない。
use Revolution\Nostr\Facades\Nostr;
use Revolution\Nostr\Event;
use Revolution\Nostr\Kind;
$event = new Event(
kind: Kind::Text,
content: 'test',
created_at: now()->timestamp,
tags: [],
);
$sk = $user->nostr_sk;
$responses = Nostr::pool()->publish(event: $event, sk: $sk);
// $responsesはリレーがキーのarray
// [
// 'wss://relay1' => $response,
// 'wss://relay2' => $response,
// ]
foreach ($responses as $relay => $response) {
if ($response->failed()) {
dump($relay.' : '.$response->body());
}
}
pool()の場合のrelaysの指定。
$responses = Nostr::pool()->withRelays(['relay1', 'relay2'])->publish(event: $event, sk: $sk);
$responses = Nostr::pool()->publish(event: $event, sk: $sk, relays: ['relay1', 'relay2']);
ここまでのまとめ
「読み」はFilterで条件を指定してlistかgetでイベントを取得。
「書き」はEventを作ってpublish。秘密鍵skが必須。
これだけ覚えておけば十分。
SocialClient
NostrClientはFilterやEventの組み立てが面倒だったのでもう一段抽象化してSNS用のSocialClientも用意。
新規ユーザー作成
Nostrにはそもそも「ユーザー作成」なんてものはないけど鍵を作ってリレーにプロフィールを登録すればユーザー作成。
use Revolution\Nostr\Facades\Social;
use Revolution\Nostr\Profile;
$profile = new Profile(
name: 'test',
display_name: 'test',
about: 'about',
);
$user = Social::createNewUser($profile);
//$userはarray。これを元にLaravelのUserなどに保存。
// [
// 'keys' => [
// 'sk' => '',
// 'nsec' => '',
// 'pk' => '',
// 'npub' => '',
// ],
// 'profile' => [
// 'name' => '',
// ]
// ]
SocialClientでもリレーは自動的に決まるので指定するには。
$user = Social::withRelay('wss://')->createNewUser($profile);
SocialClientは一つのリレーとのやり取りのみ。pool()はない。
ユーザー認証=鍵の指定
どのユーザーとして操作するかはこれだけ。
use Revolution\Nostr\Facades\Social;
Social::withKey(sk: $sk, pk: $pk);
Social Facadeはsingletonなので毎回指定する必要はない。使う箇所の最初でwithKey()。以降はskとpkが指定された状態。
Social::withKey(sk: $sk, pk: $pk);
$follows = Social::follows();
自分のノートを10件取得
use Revolution\Nostr\Facades\Social;
$notes = Social::notes(authors: ['my pk'], limit: 10);
//$notesはarray。NostrClientの例と同じだけどcreated_atの降順でソート済み。
自分のフォローしている人のpubkeyを取得
Nostrでは「フォローしている」に自分も含めるようだ。
$follows = Social::follows();
//$followsはarray。pkのみ。
自分のフォローしている人のプロフィールを取得
上の$followsを使用。
$profiles = Social::profiles(authors: $follows);
//$profilesはarray。
自分のフォローしている人のノートを取得
上の$followsを使用。自分のノートの場合からauthorsの条件を変えているだけ。
$notes = Social::notes(authors: $follows);
ノートとプロフィール情報を合わせる
$notes
にはpubkeyしかないのでユーザーの情報がない。pubkeyを元にして2つを合わせる。
$notes = Social::mergeNotesAndProfiles($notes, $profiles);
// [
// [
// 'id' => '1',
// 'kind' => 1,
// 'content' => '...',
// 'pubkey' => '...',
// 'name' => 'name',
// 'display_name' => 'name',
// ]
// ]
タイムライン
自分のフォローしている人のノートを見るだけでも面倒なので全部まとめてタイムライン。
$notes = Social::timeline(limit: 20);
新しいノートの投稿
$response = Social::createNote('test');
//$responseはHTTPクライアントのResponse
if ($response->failed()) {
dump($response->body());
}
ここまでのまとめ
SocialClientは実装サンプルみたいなものなのでこの辺でいいか。SocialClientの使い方は多分変わる。NostrはあまりSNSとは見てないので別の使い方できる実装もいずれ作りたい。
Laravel Notifications
何か新しい投稿先が出てきたらとりあえずLaravelの通知先として使う。
Notificationクラス
use Illuminate\Notifications\Notification;
use Revolution\Nostr\Notifications\NostrChannel;
use Revolution\Nostr\Notifications\NostrMessage;
use Revolution\Nostr\Tags\HashTag;
class TestNotification extends Notification
{
public function via($notifiable): array
{
return [
'mail',
NostrChannel::class
];
}
public function toNostr(mixed $notifiable): NostrMessage
{
return NostrMessage::create(
content: 'test #laravel',
tags: [
HashTag::make(t: 'laravel'),
],
);
}
}
オンデマンド通知
use Illuminate\Support\Facades\Notification;
use Revolution\Nostr\Notifications\NostrRoute;
Notification::route('nostr', NostrRoute::to(sk: 'sk'))
->notify(new TestNotification());
Nostr::pool()でconfig/nostr.php
のrelays全部に通知している。通知先のリレーを変更するにはNostrRouteで指定。
NostrRoute::to(sk: 'sk', relays: [])
「どの秘密鍵でどのリレーに通知するか」をNostrRouteで指定。オンデマンド通知ならskを.envとconfigで保存。
NostrRoute::to(sk: config('nostr.sk'))
この方法で通知してる例。
https://iris.to/invokable.net
ユーザーから通知
use Illuminate\Notifications\Notifiable;
use Revolution\Nostr\Notifications\NostrRoute;
class User
{
use Notifiable;
public function routeNotificationForNostr($notification): NostrRoute
{
return NostrRoute::to(sk: $this->sk, relays: ['wss://']);
}
}
$user->notify(new TestNotification());
おわり
読み書きできるくらい理解すると「SNSじゃない」って認識になる。Nostrは今後どういう使われ方になるか分からないのでしばらく様子を見る。