(その1はこちら

はい、続きです。演習1-3から。

【演習1】

☆演習1-3☆

1-1と1-2はリレーからイベントを取得する――つまり、「リレーに存在している投稿を読む」課題でした。
1-3はいよいよ「リレーに投稿する」課題です。

ソースコードの1行目2行目に
const { currUnixtime, getCliArg } = require("./utils.js");
const { relayInit, getPublicKey, getEventHash, getSignature } = require("nostr-tools");
というのがあります。1-2でも見たやつですが、1-2より増えてますね。「nostr-tools」から引っ張ってくるものが3つも増えているし、「utils.js」からは新たに「getCliArg」というのを取ってきている。

この「getCliArg」、ソースコードの下から2行目にも出てきます。
const content = getCliArg("error: 投稿内容をコマンドライン引数として設定してください");
はい出た、「コマンドライン引数」!
何のことかわかんなくてひゅうがが泣いたやつです。
要は、このプログラムを起動させる時に、「node 1-2.js こんにちはショッカー」のように、内部で使う引数を一緒に入力するのです。

もしコマンドライン引数を使わない場合、「こんにちはショッカー」という投稿内容を直にプログラム内に書くことになり、投稿内容を変えたい場合はいちいちプログラムを書き直さなくてはいけなくなります。

コマンドライン引数にしてあることによって、プログラムを書き換えずに投稿内容だけをいくらでも好きに変えて実行できるわけです。

はぁー、なるほど。

ちなみにプログラム名と引数の間は「半角スペース」。半角スペースで区切ることによって、引数を複数渡すことができます。(その場合もちろんプログラム内で2つめ3つめの引数を受け取る処理を書かなければいけません)

1-3のソースコードでは上記のように、受け取ったコマンドライン引数を「content」という変数に入れて使います。
この「content」(投稿内容)を「composePost」という関数に渡して「post」内容を組み立て、その「post」を「relay.publish」というメソッドでリレーに送信してやる。
無事送信が成功すれば「Nostrリレーに投稿できた=つぶやけた!」ということになります。
  const post = composePost(content);
  const pub = relay.publish(post);
ちなみに「conposePost」関数の中身はこんなふうになっていて、「content: 」に「content」をセットするところでちょっと混乱します(^^;)
 const composePost = (content) => {
  const pubkey = getPublicKey(PRIVATE_KEY_HEX); // 公開鍵は秘密鍵から導出できる
  const ev = {
    /* Q-2: イベントの pubkey, kind, content を設定してみよう */
    pubkey: pubkey,
    kind: 1,
    content: content,
    tags: [],
    created_at: currUnixtime(),
  } 
これ、1-2で見たデータ構造ですよね。「ev.content」で投稿本文が参照できるやつ。投稿する時にこういうふうにセットしてるから、ああいうデータになってるわけですね。

☆演習1-4☆

1-4は誰かの投稿にリプライする課題です。
1-3の「composePost」の中に「tags」という項目がありますね? そこに何も入れなければ普通の投稿、返信先の「公開鍵」と「イベントID」を入れれば、そのイベントIDを持つ投稿へのリプライになります。
    tags: [
      /* Q-1: リプライ対象の公開鍵を指すpタグを書いてみよう */
      [ "p",  targetPubkey, ""],
      /* Q-2: リプライ対象の投稿を指すeタグを書いてみよう */
      [ "e",  targetEventId, ""],
    ],

これは「composeReplyPost」の関数の中身なんですけど、「composeReplyPost」は以下のように3つの引数を受け取って処理を行います。
const composeReplyPost = (content, targetPubkey, targetEventId) => {
1つめの「content」は1-3と同じ、コマンドライン引数から入力する「返信内容」です。で、2つめが"p"タグに入れたい返信先の公開鍵、3つめが"e"タグに入れたいイベントID。
この関数を呼ぶ時に、2つめと3つめもセットすれば良いので、以下のようになるわけですが。
    const replyPost = composeReplyPost(
    content,
     "???(リプライ対象の公開鍵)",
    "???(リプライ対象の投稿のイベントID)"
  );
1-4をやっている時は「関数への引数の受け渡し」の部分を理解できていなくて、「composeReplyPost」の中にも直接「1234567~」のような長い公開鍵とイベントIDを書き込んで、「なんで2箇所に書き込むんだろ???」と思ってました。

関数を呼ぶ時に引数をセットすれば、関数側ではその引数を用いて処理を行ってくれる。その際受け取った値は(   )の中の変数にセットされているので、関数内部ではその変数を指定する。
だからQ-1の答えは「targetPubkey」になり、Q-2は「targetEventId」になるわけです。

(※参考→「その1」の「JavaScriptの基礎の基礎」の最後、「add」関数の使い方を見てみましょう)

【演習2 botと遊ぼう!】

☆演習2-1☆

2-1はbotのプロフィール情報をリレーに投稿する課題。1-3や1-4では「kind1」のデータを投稿していましたが、プロフィール情報は「kind0」で流します。

Q-2部分はヒントに従ってbotのID(@~で表示される部分)、表示名、説明をセットすればOK。
  /* Q-2: Botアカウントのプロフィールを設定しよう  */
  const profile = {
    name: "", // スクリーンネーム
    display_name: "", // 表示名
    about: "", // 説明欄(bio)
ちなみにアイコンも設定したいなら「picture: ""」、バナー(ヘッダ画像)は「banner: ""」、「NIP-05認証」は「nip05: ""」で設定できます。

問題はQ-3。
  /* Q-3: メタデータ(プロフィール)イベントのフィールドを埋めよう */
   const ev = {
    kind: ???,
    content: ???,
    tags: [],
    created_at: currUnixtime(),
  };
「kind」は「0」ですね。「content」のところはさっきQ-2部分でセットした「profile」という変数を突っ込めばいいように思いますが、Nostr本の演習課題の説明に「JSONオブジェクトを文字列化した上で設定する」という注意書きがあります。

JSONオブジェクト? 何それ美味しいの???

アンチョコその1の「演習1-1」部分を思い出してみましょう。
リレーにリクエストを送る時には「JSON.stringify」というのを使って「JavaScriptオブジェクトをJSON文字列に変換」
具体的なことはよくわかりませんが、ともかくそのままではダメで、「JSON.stringify」という呪文で変換してからセットしなければならないということです。
なのでQ-3の答えは
 const ev = {
    kind: 0,
    content: JSON.stringify(profile),
    tags: [],
    created_at: currUnixtime(),
  };
となります。

《発展》

2-1が正しく動けばそれでbotは存在を開始したはずですが、私の場合、公開鍵で検索しても出てこなくて「2-1が成功したのかどうか」がすぐにはわかりませんでした。(結局「日本人ユーザーbot」のフォロー一覧から発見した)

・演習1-3を使って何か投稿させてみる
 たぶん一番手っ取り早いです。Rabbitのぞき窓きりのさんリレー(wss://relay-jp.nostr.wirednet.jp)を監視しつつ投稿すれば発見できるはず。

・他のリレーにもプロフィールを投げてみる
 演習はすべてきりのさんリレーを使っていて、2-1もbotのプロフィールをきりのさんリレーに投げています。つまり、botはきりのさんリレーにしか存在しません。ソースコードの「relayUrl」部分に「wss://nostr.band」や「wss://relay.damus.io」を入れて、他のリレーにもプロフィールを投げてみましょう。検索で出てくるようになるかも。

・カスタム絵文字を使ってみよう
 なんとプロフィールにもカスタム絵文字が使えます。(表示対応しているクライアントはまだnostterぐらいだけど)。
 Q-2の説明欄や表示名部分に「:shocker: 」と入れ、Q-3の「tags」部分に絵文字の設定を入れます。
tags: [ [ "emoji", "shocker", "画像のURL"]],  
 次の演習2-2でカスタム絵文字リアクションをしたい場合も、同じように「tags」で絵文字指定をすればOKです。

☆演習2-2☆

2-2は作成したbotに「いいね」させるプログラム。なんでもかんでもいいねしてたら大変なので、コマンドライン引数で「対象となるキーワード」を設定します。
コマンドライン引数、演習1-3と1-4で使ったやつですね。「getCliArg」です。1-3では「content」という変数にコマンドライン引数が受け渡されました。今回は「targetWord」という変数にセットされます。(ソースコードの下から2行目)

それを踏まえてQ-3の「キーワードを含む投稿だけを引っかける」ロジックを書くわけですが。
sub.on("event", (ev) => {
    /* Q-3: 「受信した投稿のcontentに対象の単語が含まれていたら、
            その投稿イベントにリアクションする」ロジックを完成させよう */
    // ヒント: ある文字列に指定の単語が含まれているかを判定するには、includes()メソッドを使うとよいでしょう
    ???;
  });

いきなり難易度が上がってます。「???;」って、そこ絶対1行で書けないよね?
まず「if」で「投稿本文に対象の単語が含まれているかどうか」という条件を書かなければいけない。それには「includesメソッドを使え」というヒント。

ヒントそれだけなんですかぁぁぁぁぁ!

ググると「includesメソッド」というのは「対象となるデータ.includes(調べたい語句)」という使い方をするそう。
ここで調べたい語句というのはコマンドライン引数で入力した単語、つまり「targetWord」。そしてそれが含まれているかどうかを調べる対象データは受信した投稿の「content」部分。つまり演習1-2でも使った「ev.content」。
なのでif文は
 if(ev.content.includes(targetWord)){
となる。うん、ここまではわかる。でもその次は?
includesでチェックして、見事投稿本文に対象となる単語が含まれていた場合、「いいねする」という処理を書きたいわけだけど……。

ソースコードを眺めると、リアクションイベントを組み立てる「composeReaction」という関数と、リレーにイベントを送信する「publishToRelay」という関数があります。「if=true」の場合に呼ぶべきなのはさてどっち?

いや、どっちというか、「組み立ててから」「送信する」んじゃないかと思うよね? 送信する内容を先に作らないと、と。しかし「if」の中に書くのは「publishToRelay」です。「publishToRelay」の引数として「composeReaction」を使います。
なので正解はこう。
publishToRelay(relay, composeReaction(ev));
ええええ、関数の引数に関数書けるんだ? 聞いてないよ、そんなの。

関数というのは「何らかの戻り値を返してくるもの」です。また例の「足し算する関数」を思い返してみましょう。
const add = (a, b) => {
     return a + b;
     };
const sum = add(15, 20);
「add」という関数はまさしく「a+b」を「return」しています。なので「sum」に代入されるのは関数内の処理を経て戻ってきた「35」です。

同様に、「publishToRelay」の2つめの引数のところに「composeReaction(ev)」を指定すると、「composeReaction」という関数で処理された内容(つまりは組み立てられたリアクションイベント)が「publishToRelay」に渡されるわけです。

良かったですね!

(こうして振り返ると「あ~、なるほど」という気がしますが、実際に2-2に取り組んでいる間はさっぱりで、マンツーマンで教えてもらってやっと正解にたどり着けました)

ちなみに「publishToRelay」の1つめの引数の「relay」もくせもので、私はそこに「relayUrl」をセットするのかと思いました。URLそのものを渡してもダメで、「relayInit」でちゃんと使える状態にした「relay」じゃないとダメ、ってことだそう。

で、Q-1が「composeReaction」を組み立てる部分。ここは演習1-4とだいたい同じです。「kind」が「7」なのと、「content」が「いいね」を表す「"+"」(もしくは好きな絵文字)になるだけ。
「tags」の中の「"e"」と「"p"」には、引数で受け取った「targetEvent」に含まれている「id」と「pubkey」を指定すればよい。

☆演習2-3☆

最後です!
botに返事をさせてみよう!!!
話しかけられたら、「イーっ!」とか、「変身、ぶいすりゃー!」とか言ってみるのだ、頑張れ、俺のbot!

返信するのは演習1-4でやったから、あれをコピーすればいいだけ。コピーすれば……。

はい、ここで罠があります。1-4のソースコードの2行目付近はこう。
const {
  relayInit,
  getPublicKey,
  getEventHash,
  getSignature,
  nip19
} = require("nostr-tools");

一方、2-3の同じ部分はこう。
const {
  relayInit,
  getPublicKey,
  finishEvent,
  nip19
} = require("nostr-tools");

なんか行数が少ないですね?
そうです、演習1ではイベントの署名に「getEventHash」「getSignature」という2つの関数を使っていましたが、演習2では同じことを「finishEvent」一つでやっています。

演習1ではいちいちイベントデータに「pubkey」をセットしていましたが、演習2ではそこも省略できてしまいます。
演習2-1のQ-3部分に「// pubkeyは以下の処理で自動で設定されるため、ここで設定する必要はありません」というただし書きがあるのですが、「以下の処理」=「finishEvent」です。

なので演習1-4をコピーしてしまうと署名部分で齟齬が起きてうまく動きません。コピーするのは2-2です! 2-2の「kind7」を「kind1」に変更してゴニョゴニョするのです!

できた?
できたよね???

はい、おつかれっしたーーー!

(※botに色々させるための覚え書き「その3」にたぶん続きます)


【関連記事】
Nostrでbotを作った話
Nostr本演習のアンチョコ(JavaScript覚え書き)~その1~
Nostr本演習のアンチョコ(JavaScript覚え書き)~その3・延長戦~