LoginSignup
0
0

More than 1 year has passed since last update.

Laravel/PHPでNostrの読み書き

Posted at

PHPではシュノア署名が難しそうなので代わりの手段

node.jsのnostr-toolsを使って必要な機能を提供するAPIをVercelに用意した。
https://github.com/kawax/nostr-vercel-api

これなら自分で署名できなくても何でも使える。PHPネイティブで署名できるようになれば変えるかもしれないけどしばらくはこれで対応。

「読み」だけなら簡単

署名が不要なのでこういうWebSocketクライアントだけで可能。
https://github.com/Textalk/websocket-php

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から使うパッケージ

https://github.com/kawax/laravel-nostr
今回は最初から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);
// LaravelのHTTPクラアントはこの時点ではリクエストを送信していないはずなので$responseに対して何らかの対処が必要。

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\Tag\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は今後どういう使われ方になるか分からないのでしばらく様子を見る。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0