読んでくれ
Hello Nostr! 先住民が教えるNostrの歩き方
この記事は、だいたいこの本の02章「Nostrの仕組み」に書いてあることをシェルスクリプトでトレースしようという試みである。Nostr 全般と Nostr のリレーサーバとクライアントが行うことの概略を理解するのに大変良い本だと思うので一読を勧める。
リレーサーバがやること
クライアントがサーバへ送ってくるのは3つだけである。
- EVENT -- ユーザーの作成・記事の投稿・フォローなどその他すべて
- REQ -- EVENT データを返してほしいというリクエスト。「どんなデータ」かはフィルタ(条件)で指定
- CLOSE -- 2 のリクエストに対して、現状以上の送信を断る
それに対してリレーサーバはただ
- EVENT を記録・保存して
- クライアントのリクエストに応じそれらを返し
- 断られたらそのリクエストを切る
これだけだ。流れを擬似 json で書くと
(クライアント)["EVENT",{サーバに登録したい情報}] => (サーバ)情報を記録・保存する
(クライアント)["REQ",ID,フィルタ(条件)] => (サーバ)["EVENT",ID,フィルタに応じたデータ] => クライアント
(クライアント)["CLOSE",ID] => (サーバ)IDが同じリクエストに応じるのをやめる
である。署名もその検証もクライアントが行う。何故 CLOSE が必要かが分かりにくいかもしれない。クライアントはリクエストを送りっぱなしにして、送った後にサーバに登録された条件に合うデータをも求めることが出来るので、それをやめるときに CLOSE を送ってリクエストをやめる。
目標
NIP-01 と 02 くらいまで。ともかくクライアントでそれなりの遣り取りができるくらいで満足する。とはいえ全てのクライアントに対応するのは無理。Rabbit ( https://rabbit.syusui.net/ ) では何とか応答できる、程度で満足しとく。
名前
大体 Bash のスクリプトと SQLite3 でやってしまうので、名前は「BaSQ」(バスク)にする。
実装を考える
Nostr は HTTP ではなく Websocket で情報のやりとりをするので、websocketd を使う。
sudo apt install websocketd
でインストールするか、github ( https://github.com/joewalnes/websocketd ) からバイナリをダウンロードしてパスを指定し実行すれば良い。
websocketd --port=適当なポート bash シェルスクリプト
で実行する。書かなくてはいけないのはこのシェルスクリプトだ。
クライアントが送ってくるのは EVENT/REQ/CLOSE だけなので、この3つで条件分岐すれば良い。簡単に外側を書くと
while read -r line; do
first_element=$(echo $line | jq .[0])
second_element=$(echo $line | jq .[1])
third_element=$(echo $line | jq .[2])
if [ "$first_element" = '"EVENT"' ]; then
記録・保存
elif [ "$first_element" = '"REQ"' ]; then
求められたデータを流す
elif [ "$first_element" = '"CLOSE"' ]; then
止めるよう求められたリクエストを止める
else echo "[NOTICE]" # おかしなものが送られてきたらこれで誤魔化す
fi
done
この程度で誤魔化すことにする。
EVENT で送られてきたデータのうち
- id
- pubkey
- kind
- created_at
- tags
- EVENT の次に入っている内容そのもの
を記録・保存することにする。大体このくらいで良くない?
タグは e/p/r/t/... と色々あるそうだが( https://nostr.how/jp/the-protocol )、e と p だけを捌くことにする。が、e タグは一つの EVENT で2つ以上付けられて送られて来ることがある。(会話の始まり root と 直接の返答の対象 reply)
なのでここは手抜きをして、tags の記録は種類を問わずに行う。EVENT の id も pubkey も重複することはほぼ有り得ないし(あったら id として機能しないはずだ)、狙って作ることもほぼ無理だろうからおそらくは問題ないだろう。(と言っても絶対に無理ではないらしい ( https://zenn.dev/koalaonsen/articles/d6841265df1aac ))
また tags は幾つも付加してもいいはずだが4つ以上の tag が付加された EVENT が送られてきたことが今のところ筆者はないので(送ったことならある)、多く見積もって tag は4つまで記録保存することにする。5つ以上のタグが付けられていたら諦める。(数が読めないデータをみんなどうやって RDB に入れて処理してるんだろう?)
そして EVENT に続く内容そのものはそのままの json を、 created_at を名前としたファイルで jsons ディレクトリに保存することにする。created_at を名前にするのは、時間でソートするときに名前をそのまま使えるようにするため。それ以外は SQLite3 でデータベースに突っ込む。
なのでまず
mkdir jsons
touch test.db
sqlite3 test.db "sqlite3 test.db "create table forRelay (created_at integer, id, author, kind integer, tag1, tag2, tag3, tag4)""
とデータベースを作る。あとは以下でいいはずだ。多分。
#!/bin/bash
while read -r line; do
first_element=$(echo $line | jq -c .[0])
second_element=$(echo $line | jq -c .[1])
third_element=$(echo $line | jq -c .[2:][])
## 3つめ以降はまとめて third_elemnt に収める
if [ "$first_element" = '"EVENT"' ]; then
## 記録・保存
id=$(echo $second_element | jq '.id' | sed 's/"//g')
pubkey=$(echo $second_element | jq '.pubkey' | sed 's/"//g')
kind=$(echo $second_element | jq '.kind')
created_at=$(echo $second_element | jq '.created_at')
tags=( $(echo $second_element | jq -c .tags[][1] | tr -d '"' |
while read -r tag; do
echo "$tag"
done) ) # 配列化
tag1=${tags[0]}
tag2=${tags[1]}
tag3=${tags[2]}
tag4=${tags[3]}
sqlite3 test.db "insert into forRelay (created_at, id, author, kind, tag1, tag2, tag3, tag4) values ('$created_at', '$id', '$pubkey', '$kind', '$tag1', '$tag2','$tag3','$tag4');" # シングルクォート大事
echo $second_element > ./jsons/$created_at
echo "[\"OK\",\"$id\",true,\"\"]"
## 記録・保存 ここまで
elif [ "$first_element" = '"REQ"' ]; then
求められたデータを流す
elif [ "$first_element" = '"CLOSE"' ]; then
止めるよう求められたリクエストを止める
else echo "[NOTICE]" # これで誤魔化す
fi
done