【演習延長戦!】
botが動くと嬉しい。ただ「いいね」したり返信したりするだけでなく、色々やらせてみたくなる。というわけで、演習2-3終了後もちまちまと機能追加したり改修したり。
ここではその過程で学んだことをメモっていきたいと思います。
☆JavaScriptファイルのそもそもの構造☆
最初は「なんでこの処理を先にやるんだろう」などと思っていたけれども、実はずっと「const」で色々定義しているだけで、最後の行のmain().catch((e) => console.error(e));で初めて「main関数」を呼んで、色々な処理が動いていく。気づいた時は「あー、そーゆー!?」となりました。
☆いわゆる一つのスコープ問題☆
botに色々させるべくいじっていると、しょっちゅう「undefined」だの「already declared」などというエラーを吐かれます。「if」で条件分岐して、「当てはまる時にはnameにAAAを入れなさい」とconstで定義して、「if」終了後にその「name」を表示しようとすると「undefined」と言われてしまう。
えー、ちゃんと定義しましたやーん。
これは「変数のスコープ(参照範囲)」の問題で、「if」などの制御構文や関数の「{ }」の内側で定義された変数は、その「{ }」の中でしか有効でない、らしい。
演習のソースコードの中に「ev」が何度か出てきて、「あれ?ここのevとあっちのevはたぶん違うけど、evでいいんだ???」と思ったことがあったのだけど、ブロックが違えば参照されないので、同じ変数名を使い回しても大丈夫らしい。
「let」と「const」には再代入ができるできないとか違いがあって、その辺もまだあまり理解できていない。
☆乱数メソッド☆
返信のバリエーションを増やすために乱数を使いました。「Math.random()」という数学オブジェクト。
0から1の間(ただし1は含まない)のランダムな小数を返してくれます。
「返信する確率」などで使う時はそのままで良いけれど、「5つの返信文のうちの何番目を返すかランダムに決める」ような用途の時は少し工夫が必要ということで。
0~nまでの整数(ただしnは含まない)を返す関数を書きました。 ↓
function getRansu(n) { let ransu = Math.random() * n; return Math.floor(ransu); }図書館で借りた本の丸写し(^^;)。もしconstで定義&途中を省略していきなりMath.floorの引数部分に代入するとたぶんこう ↓
const getRansu = (n) =>{ return Math.floor(Math.random() * n); };たとえばこういう「返信文」の配列を作ったとします。
const phrase = ["おのれディケイド!", "サイクロン、ジョーカー!", "タ、ト、バ、タトバタトバっ!", ];この中からランダムに1つ選んで表示したいとすると、「3つのうちから1つ」なので、乱数関数で戻ってきてほしい数字は「1~3」と言いたいところですが、配列の要素番号は0から数えるので、「0~2」です。
そして乱数関数はうまい具合に「0から」の値を返してくれます。「n」に「3」を入れれば、めでたく「0~2」までの整数をランダムに得ることができます。
しかし直接プログラム内に「getRansu(3)」と書き込んでしまうと、返信文を増やした時にいちいちその「3」部分を書き換えなくてはなりません。
配列の要素がいくつあるかはlengthプロパティで取得できるので、
console.log(phrase[getRansu(phrase.length)]);で、「phrase」配列の中の返信文をランダムに表示させることができます。
最初はswitch文を使って地道に「ランダム関数の戻り値が0ならaをセット」「1ならbを」とやっていたのですが、返信文を増やすたび場合分けを書き足すことになってバカバカしいので配列を使うよう変更しました。
☆ else if ☆
条件分岐として頻出する「if」。普通の(?)「if」はこうですがif(条件式){ //条件式がtrueの場合に行う処理を書く } else { //条件式がfalseの場合に行う処理を書く }最初の条件にあてはまらなかった時さらに「こうだったら」、という条件を書くのが「else if」。
if(条件式1){ //条件式1がtrueの場合に行う処理 } else if (条件式2){ //条件式2がtrueの場合に行う処理 } else { //1にも2にもあてはまらない場合に行う処理 }「1ではない場合だけ2をチェックする」。botに色々やらせているとコードがifの嵐になっていきます……。
☆正規表現☆
「ifの嵐」を少し緩和してくれるのが「正規表現」。詳しいことはよく理解していませんが、「もし対象データ(たとえば「name」という変数に入っているデータ)に“ディケイド”か“ダブル”か“オーズ”が含まれていたら」を少し楽に書けます。これをもし「if」で書くと
if(name.includes("ディケイド") || name.includes("ダブル") || name.includes("オーズ")){となるのだけど(「||」はor条件、「&&」ならand条件)、正規表現を用いて書くとこうなります。↓
if(/ディケイド|ダブル|オーズ/.test(name)){同じ「含まれるかどうか」でも正規表現を用いる時はincludesを使うことはできず、「test」というのを使うのだけど、「調べたい語句」と「対象となるデータ」の配置が逆になるという……。ぐぅぅ、そういうとこだぞ、JavaScript!
そもそも正規表現が文字列を対象にしているものだからか、「ディケイド」を「""」で囲む必要はないもよう。
「/ディケイド|ダブル|オーズ/」の部分を変数にセットすることもできます。もし「riders」という変数にセットしてあったとすると、上記のif文は
if(riders.test(name)){と書ける。ライダーの名前を増やしたい時には変数にセットしておく方がif文がすっきりしますね。
☆曜日、日付関係のメソッド☆
今日が何日か、何曜日か、そして今何時か、といった情報を得る方法。const now = new Date(); console.log(now.getFullYear()); // 西暦4桁が表示される console.log(now.getMonth()); // 月。ただし0始まり console.log(now.getDate()); // 日 console.log(now.getDay()); // 曜日。日曜が0 console.log(now.getHours()); // 時 console.log(now.getMinutes()); // 分 console.log(now.getSeconds()); // 秒 console.log(now.getMilliseconds()); // ミリ秒
ちなみに「now」の中身をそのまま表示させると「Fri Jun 30 2023 13:44:37 GMT+0900 (日本標準時)」という形式のものが出ます。
(※「new Date()」で取得される日付は数字として扱う場合には数字として処理されるので、「console.log(now * 1);」とすると「1689055596048」というUNIX時間を表示します。なので以下のような計算が可能)
Nostrの投稿にはUNIX時間(秒単位)を使うので、演習では「utilis.js」の中に
const currUnixtime = () => Math.floor(new Date().getTime() / 1000);という関数が書かれています。ちなみにUNIX時間というのは「UTC時刻における1970年1月1日午前0時0分0秒からの経過秒数を計算したもの」だそうです。
(※上記の式は「new Date() /1000」でも同じ結果が出るもよう。「getTime()」はたぶん省略できる)
Nostrの投稿イベントに記録されている「UNIX時間」を普通の日時に変換するには
const time1 = new Date(ev.created_at * 1000); console.log(time1.toLocaleString());これで、「2023/6/30 14:21:36」という表示になります。
☆配列操作のバリエーション 「for ~of」 ☆
botが他のbotに反応しないよう、除外設定をする時に使いました。最初はこれも単純に「if」の中にor条件を書いていってたんですが、除外対象が増えるたびに条件を書き込まないといけないし、「if」の条件式がだらだらと長くなってしまうので、「なんとかならんのか?」と思い、方法を模索しました。
最初は単純に「for」文でループを回して検索してたんですが。(「keys」に除外したいアカウントの鍵一覧が配列で入っている)
for (let i = 0; i < keys.length; i++){ if(usrPubkey.includes(keys[i])){それなら「for of」を使った方が楽だよ、と教えてもらいました。
for (let aite of keys){ if(usrPubkey.includes(aite)){自動で配列の中身を「aite」という変数(これは何でも好きな名前を付ければいいもよう)に入れて、配列の要素数分ループを回してくれるらしい。なるほどこれは便利。
☆配列操作のバリエーション 多次元配列の検索 「some」「flat」☆
演習2-3では自分宛の投稿をフィルターを使って抽出していました。演習1-1や1-2では全件取得していたのが、2-3では自分宛のイベントデータしか受け取っていなかったわけです。
「話しかけられたら返事をする」だけのbotならそれでいいですが、「話しかけられなくてもなんか言いたい」「誰かが“ライダー”と言ったら“とうっ!”とエアリプしたい」と思うと、まずは全件読み込まなければなりません。
1. 全件読み込んで投稿本文をキーワードで抽出→エアリプ
2. 全件の中から自分宛の投稿を抽出→返信
「話しかけられたら返事をする」だけのbotならそれでいいですが、「話しかけられなくてもなんか言いたい」「誰かが“ライダー”と言ったら“とうっ!”とエアリプしたい」と思うと、まずは全件読み込まなければなりません。
1. 全件読み込んで投稿本文をキーワードで抽出→エアリプ
2. 全件の中から自分宛の投稿を抽出→返信
の両方をやりたい。
フィルターを使わずに「自分宛の投稿」をチェックするには、受け取ったデータの「tags」の部分を見なければなりません。「tags」の中はこんなふうになっています。
[["e", "a12345~", "", "root"], ["p", "d67890~"]]配列の中に入れ子で配列がある形。「多次元配列」と言うそうです。"e"のところにはイベントID、そして"p"のところに公開鍵。そこに自分の公開鍵が書かれていれば、それは「自分宛の投稿」ということになります。
とりあえず「tags」の中の文字列に自分の公開鍵が存在するかどうかがわかればいいので、「includes」でチェックできるのかなと思ったんですが。
できませんでした/(^o^)\
ググった結果、「some」というのを使えばいいらしい。「配列内のいずれかの要素が条件に合致しているかを判定する」メソッドだそうで、
if(ev.tags.some(v => v.includes(myPubkey))){と書くと、「tags」の中の要素が目的のものを含んでいるかどうかチェックしてくれる。
入れ子を解除する「flat」というメソッドを使う方法もあります。(「flat()」の()内には平坦化する階数を書く。既定は1)
if (tags.flat().includes(myPubkey)) {
☆配列操作のバリエーション 「indexOf」☆
「includes」は含まれているかだけを調べてくれる(戻り値はtrueかfalse)けど、その要素の場所を調べたい時は「indexOf」を使いましょう。最初に合致した位置の要素番号を返してくれます。存在しない場合は「-1」が返ってくる。「===」(厳密等価演算子)で検索されるので、文字と数字は区別されます。
「tags」の中の"e"の次の要素(イベントID)を取得したい場合、
const num = tags.flat().indexOf("e"); if(num >=0){ console.log(tags.flat()[num + 1]); }で表示できる。
リプのリプ、引用の引用など"e"が複数含まれている場合、最初の"e"しか見つけられないけれども…。
(※「indexOf(検索したい要素, 検索開始位置)」という使い方ができるので、検索開始位置をゴニョゴニョしてループで回せば複数の"e"を検出できることはできる、たぶん)
【演習1-2の延長戦】
botだけでなく、1-2や1-3も改造チャレンジしました。1-3は2つのbotとテスト垢の3つを切り替えて使えるように。そして1-2は日時指定と検索キーワードをつけて自分の投稿を引っ張ってくるように。
リポストも取ってこようと思ったけど、リポスト、tagsの中の"e"を見て、そのイベントIDであらためて元の投稿を取ってこないと中身が見られないんですよね…。無理、難しい……。
自分のオリジナルポストを好きに取ってこられるだけで十分便利!うん、すごい!!
ただ、日時と検索語をコマンドライン引数で入力するのがちょっと(かなり)面倒くさい。
ブラウザから入力フォームでサクッと…と思うけれども、画面とやりとりするの全然わからない。HTMLとJavaScript組みあわせるとなるとまた「何もわからない」状態になってしまう。
うーーー。
まぁしょうがないですね。こんなことばっかりしてないで他のこともしないと。
【関連記事】
・Nostrでbotを作った話
・Nostr本演習のアンチョコ(JavaScript覚え書き)~その1~
・Nostr本演習のアンチョコ(JavaScript覚え書き)~その2~
・Nostr本演習のアンチョコ(JavaScript覚え書き)~その4・ファイル出力とか~
・拡張機能で始める簡単(?)Nostr生活
・Nostr初心者のためのFAQ【日本語ユーザーはここにいる!】
【参考文献(図書館で借りたやつ)】
0 Comments
コメントを投稿