├── .gitignore ├── 1-1_fetch_posts_raw_ws.js ├── 1-2_fetch_posts_nostr_tools.js ├── 1-3_post_text.js ├── 1-4_reply.js ├── 2-1_create_bot_account.js ├── 2-2_react_to_keywords.js ├── 2-3_reply_to_reply.js ├── LICENSE ├── README.md ├── package.json ├── scripts ├── gen_key_pair.js ├── sub_reply.js └── to_hex.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | yarn.lock 4 | package-lock.json 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /1-1_fetch_posts_raw_ws.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime } from "./utils.js"; 2 | 3 | import "websocket-polyfill"; 4 | 5 | /* Q(おまけ): URLを変更して、別のリレーの様子も見てみよう */ 6 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 7 | 8 | const main = () => { 9 | const ws = new WebSocket(relayUrl); 10 | 11 | ws.onopen = () => { 12 | /* Q-1: REQメッセージを書いてみよう */ 13 | const req = [ 14 | ???, 15 | "subscription", // 購読ID。空でない・長すぎない文字列であれば何でもOK 16 | ??? 17 | ]; 18 | ws.send(JSON.stringify(req)); 19 | }; 20 | 21 | ws.onmessage = (e) => { 22 | const msg = JSON.parse(e.data); 23 | 24 | // メッセージタイプによって分岐 25 | /* Q-2: 受信したメッセージからメッセージタイプを取り出そう */ 26 | switch ( ??? ) { 27 | case "EVENT": 28 | /* Q-3: 受信したEVENTメッセージからイベント本体を取り出して表示してみよう */ 29 | console.log( ??? ); 30 | break; 31 | 32 | case "EOSE": 33 | console.log("****** EOSE ******"); 34 | break; 35 | 36 | default: 37 | console.log(msg); 38 | break; 39 | } 40 | }; 41 | 42 | ws.onerror = () => { 43 | console.error("failed to connect"); 44 | } 45 | }; 46 | 47 | main(); 48 | -------------------------------------------------------------------------------- /1-2_fetch_posts_nostr_tools.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime } from "./utils.js"; 2 | 3 | import { relayInit } from "nostr-tools"; 4 | import "websocket-polyfill"; 5 | 6 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 7 | 8 | const main = async () => { 9 | /* Q-1: nostr-toolsのRelayオブジェクトを初期化してみよう */ 10 | const relay = ???; 11 | relay.on("error", () => { 12 | console.error("failed to connect"); 13 | }); 14 | 15 | /* Q-2: Relayオブジェクトのメソッドを呼び出して、リレーに接続してみよう */ 16 | ???; 17 | 18 | /* Q-3: Relayオブジェクトのメソッドを使って、イベントを購読してみよう */ 19 | const sub = ???; 20 | 21 | // メッセージタイプごとにリスナーを設定できる 22 | sub.on("event", (ev) => { 23 | // Nostrイベントのオブジェクトがコールバックに渡る 24 | console.log(ev); 25 | }); 26 | 27 | sub.on("eose", () => { 28 | console.log("****** EOSE ******"); 29 | }); 30 | }; 31 | 32 | main().catch((e) => console.error(e)); 33 | -------------------------------------------------------------------------------- /1-3_post_text.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime, getCliArg } from "./utils.js"; 2 | 3 | import { relayInit, getPublicKey, getEventHash, getSignature } from "nostr-tools"; 4 | import "websocket-polyfill"; 5 | 6 | /* Q-1: 自分の秘密鍵をhex形式に変換して、ここに設定しよう */ 7 | const PRIVATE_KEY_HEX = ???; 8 | 9 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 10 | 11 | /** 12 | * テキスト投稿イベントを組み立てる 13 | * @param {string} content 14 | */ 15 | const composePost = (content) => { 16 | const pubkey = getPublicKey(PRIVATE_KEY_HEX); // 公開鍵は秘密鍵から導出できる 17 | const ev = { 18 | /* Q-2: イベントの pubkey, kind, content を設定してみよう */ 19 | pubkey: ???, 20 | kind: ???, 21 | content: ???, 22 | tags: [], 23 | created_at: currUnixtime(), 24 | } 25 | /* Q-3: イベントのハッシュ値を求めてみよう */ 26 | const id = ??? 27 | /* Q-4: イベントの署名を生成してみよう */ 28 | const sig = ??? 29 | 30 | return {...ev, id, sig} // イベントにID(ハッシュ値)と署名を設定 31 | 32 | /* 33 | * Q(おまけ): nostr-toolsには、イベントのハッシュ値の計算と署名の生成を行って、イベントのオブジェクトに自動で設定してくれる 34 | * (24~29行目の処理を一気にやってくれる)、便利な関数が用意されている。その関数を探してみよう 35 | */ 36 | } 37 | 38 | const main = async (content) => { 39 | const relay = relayInit(relayUrl); 40 | relay.on("error", () => { 41 | console.error("failed to connect"); 42 | }); 43 | 44 | await relay.connect(); 45 | 46 | // テキスト投稿イベントを組み立てて、中身を見てみる 47 | const post = composePost(content); 48 | console.log(post); 49 | 50 | /* Q-5: Relayオブジェクトのメソッドを使って、イベントを発行してみよう */ 51 | const pub = ???; 52 | 53 | pub.on('ok', () => { 54 | console.log("succeess!"); 55 | relay.close(); 56 | }) 57 | pub.on('failed', () => { 58 | console.log("failed to send event") 59 | relay.close(); 60 | }) 61 | }; 62 | 63 | const content = getCliArg("error: 投稿内容をコマンドライン引数として設定してください"); 64 | main(content).catch((e) => console.error(e)); 65 | -------------------------------------------------------------------------------- /1-4_reply.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime, getCliArg } from "./utils.js"; 2 | 3 | import { 4 | relayInit, 5 | getPublicKey, 6 | getEventHash, 7 | getSignature, 8 | nip19 9 | } from "nostr-tools"; 10 | import "websocket-polyfill"; 11 | 12 | /* 自分の秘密鍵をhex形式に変換して、ここに設定*/ 13 | const PRIVATE_KEY_HEX = ???; 14 | 15 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 16 | 17 | /** 18 | * テキスト投稿イベント(リプライ)を組み立てる 19 | * @param {string} content 投稿内容 20 | * @param {string} targetPubkey リプライ対象の公開鍵(hex) 21 | * @param {string} targetEventId リプライ対象の投稿のイベントID(hex) 22 | */ 23 | const composeReplyPost = (content, targetPubkey, targetEventId) => { 24 | const myPubkey = getPublicKey(PRIVATE_KEY_HEX); 25 | 26 | // 発展課題のヒント: NIP-27に準拠するには、ここでcontentに手を加えることになります 27 | 28 | const ev = { 29 | pubkey: myPubkey, 30 | kind: 1, 31 | content, 32 | tags: [ 33 | /* Q-1: リプライ対象の公開鍵を指すpタグを書いてみよう */ 34 | [???], 35 | /* Q-2: リプライ対象の投稿を指すeタグを書いてみよう */ 36 | [???], 37 | ], 38 | created_at: currUnixtime(), 39 | }; 40 | const id = getEventHash(ev); 41 | const sig = getSignature(ev, PRIVATE_KEY_HEX); 42 | 43 | return { ...ev, id, sig }; 44 | }; 45 | 46 | const main = async (content) => { 47 | const relay = relayInit(relayUrl); 48 | relay.on("error", () => { 49 | console.error("failed to connect"); 50 | }); 51 | 52 | await relay.connect(); 53 | 54 | const replyPost = composeReplyPost( 55 | content, 56 | /* Q-3: Nostr上の好きな投稿を選び、その投稿のイベントIDと投稿者の公開鍵を調べて、ここに設定してみよう */ 57 | // ヒント-1: まずは、1-3節の演習で作った投稿にリプライしてみるといいでしょう。必要な2つのデータはログに出力されたイベントの中にあります 58 | // ヒント-2: 「リプライ実装チェッカー(bot)」の投稿にリプライすると、実装が正しいか判定してリプライで結果を教えてくれます。詳しくはREADMEの「ヒント」の項を参照してください 59 | // ヒント-3: npmスクリプト sub-reply を使って自分へのリプライを確認できます。詳しくはREADMEの「npmスクリプト」の項を参照してください 60 | "???(リプライ対象の公開鍵)", 61 | "???(リプライ対象の投稿のイベントID)" 62 | ); 63 | const pub = relay.publish(replyPost); 64 | 65 | pub.on("ok", () => { 66 | console.log("succeess!"); 67 | relay.close(); 68 | }); 69 | pub.on("failed", () => { 70 | console.log("failed to send event"); 71 | relay.close(); 72 | }); 73 | }; 74 | 75 | const content = getCliArg("error: リプライの内容をコマンドライン引数として設定してください"); 76 | main(content).catch((e) => console.error(e)); 77 | -------------------------------------------------------------------------------- /2-1_create_bot_account.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime } from "./utils.js"; 2 | 3 | import { 4 | relayInit, 5 | getPublicKey, 6 | finishEvent, 7 | nip19, 8 | } from "nostr-tools"; 9 | import "websocket-polyfill"; 10 | 11 | /* Q-1: Bot用に新しい秘密鍵を生成して、ここに設定しよう */ 12 | const BOT_PRIVATE_KEY_HEX = ???; 13 | 14 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 15 | 16 | /** 17 | * メタデータ(プロフィール)イベントを組み立てる 18 | */ 19 | const composeMetadata = () => { 20 | /* Q-2: Botアカウントのプロフィールを設定しよう */ 21 | const profile = { 22 | name: "", // スクリーンネーム 23 | display_name: "", // 表示名 24 | about: "", // 説明欄(bio) 25 | }; 26 | 27 | /* Q-3: メタデータ(プロフィール)イベントのフィールドを埋めよう */ 28 | // pubkeyは以下の処理で自動で設定されるため、ここで設定する必要はありません 29 | const ev = { 30 | kind: ???, 31 | content: ???, 32 | tags: [], 33 | created_at: currUnixtime(), 34 | }; 35 | 36 | // イベントID(ハッシュ値)計算・署名 37 | return finishEvent(ev, BOT_PRIVATE_KEY_HEX); 38 | } 39 | 40 | const main = async () => { 41 | const relay = relayInit(relayUrl); 42 | relay.on("error", () => { 43 | console.error("failed to connect"); 44 | }); 45 | 46 | await relay.connect(); 47 | 48 | // メタデータ(プロフィール)イベントを組み立てる 49 | const metadata = composeMetadata(); 50 | 51 | // メタデータイベントを送信 52 | const pub = relay.publish(metadata); 53 | pub.on("ok", () => { 54 | console.log("succeess!"); 55 | relay.close(); 56 | }); 57 | pub.on("failed", () => { 58 | console.log("failed to send event"); 59 | relay.close(); 60 | }); 61 | }; 62 | 63 | main().catch((e) => console.error(e)); 64 | -------------------------------------------------------------------------------- /2-2_react_to_keywords.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime, getCliArg } from "./utils.js"; 2 | 3 | import { 4 | relayInit, 5 | getPublicKey, 6 | finishEvent 7 | } from "nostr-tools"; 8 | import "websocket-polyfill"; 9 | 10 | /* Bot用の秘密鍵をここに設定 */ 11 | const BOT_PRIVATE_KEY_HEX = ???; 12 | 13 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 14 | 15 | /** 16 | * リアクションイベントを組み立てる 17 | * @param {import("nostr-tools").Event} targetEvent リアクション対象のイベント 18 | */ 19 | const composeReaction = (targetEvent) => { 20 | /* Q-1: リアクションイベントのフィールドを埋めよう */ 21 | const ev = { 22 | kind: ???, 23 | content: ???, 24 | tags: ???, 25 | created_at: currUnixtime(), 26 | }; 27 | 28 | // イベントID(ハッシュ値)計算・署名 29 | return finishEvent(ev, BOT_PRIVATE_KEY_HEX); 30 | }; 31 | 32 | // リレーにイベントを送信 33 | const publishToRelay = (relay, ev) => { 34 | const pub = relay.publish(ev); 35 | pub.on("ok", () => { 36 | console.log("succeess!"); 37 | }); 38 | pub.on("failed", () => { 39 | console.log("failed to send event"); 40 | }); 41 | }; 42 | 43 | const main = async (targetWord) => { 44 | const relay = relayInit(relayUrl); 45 | relay.on("error", () => { 46 | console.error("failed to connect"); 47 | }); 48 | 49 | await relay.connect(); 50 | 51 | /* Q-2: すべてのテキスト投稿を購読しよう */ 52 | const sub = ???; 53 | sub.on("event", (ev) => { 54 | try { 55 | /* Q-3: 「受信した投稿のcontentに対象の単語が含まれていたら、 56 | その投稿イベントにリアクションする」ロジックを完成させよう */ 57 | // ヒント: ある文字列に指定の単語が含まれているかを判定するには、includes()メソッドを使うとよいでしょう 58 | ???; 59 | } catch (err) { 60 | console.error(err); 61 | } 62 | }); 63 | }; 64 | 65 | // コマンドライン引数をリアクション対象の単語とする 66 | const targetWord = getCliArg("error: リアクション対象の単語をコマンドライン引数として設定してください"); 67 | main(targetWord).catch((e) => console.error(e)); 68 | -------------------------------------------------------------------------------- /2-3_reply_to_reply.js: -------------------------------------------------------------------------------- 1 | import { currUnixtime } from "./utils.js"; 2 | 3 | import { 4 | relayInit, 5 | getPublicKey, 6 | finishEvent, 7 | nip19 8 | } from "nostr-tools"; 9 | import "websocket-polyfill"; 10 | 11 | /* Bot用の秘密鍵をここに設定 */ 12 | const BOT_PRIVATE_KEY_HEX = ???; 13 | 14 | const relayUrl = "wss://relay-jp.nostr.wirednet.jp"; 15 | 16 | /** 17 | * テキスト投稿イベント(リプライ)を組み立てる 18 | * @param {string} content 投稿内容 19 | * @param {import("nostr-tools").Event} targetEvent リプライ対象のイベント 20 | */ 21 | const composeReplyPost = (content, targetEvent) => { 22 | /* Q-1: これまで学んだことを思い出しながら、 23 | リプライを表現するイベントを組み立てよう */ 24 | ???; 25 | }; 26 | 27 | // リレーにイベントを送信 28 | const publishToRelay = (relay, ev) => { 29 | const pub = relay.publish(ev); 30 | pub.on("ok", () => { 31 | console.log("succeess!"); 32 | }); 33 | pub.on("failed", () => { 34 | console.log("failed to send event"); 35 | }); 36 | }; 37 | 38 | /* 暴走・無限リプライループ対策 */ 39 | // リプライクールタイム 40 | const COOL_TIME_DUR_SEC = 60 41 | 42 | // 公開鍵ごとに、最後にリプライを返した時刻(unixtime)を保持するMap 43 | const lastReplyTimePerPubkey = new Map() 44 | 45 | // 引数のイベントにリプライしても安全か? 46 | // 対象の発行時刻が古すぎる場合・最後にリプライを返した時点からクールタイム分の時間が経過していない場合、安全でない 47 | const isSafeToReply = ({ pubkey, created_at }) => { 48 | const now = currUnixtime() 49 | if (created_at < now - COOL_TIME_DUR_SEC) { 50 | return false; 51 | } 52 | 53 | const lastReplyTime = lastReplyTimePerPubkey.get(pubkey) 54 | if (lastReplyTime !== undefined && now - lastReplyTime < COOL_TIME_DUR_SEC) { 55 | return false 56 | } 57 | lastReplyTimePerPubkey.set(pubkey, now) 58 | return true 59 | } 60 | 61 | // メイン関数 62 | const main = async () => { 63 | const relay = relayInit(relayUrl); 64 | relay.on("error", () => { 65 | console.error("failed to connect"); 66 | }); 67 | 68 | await relay.connect(); 69 | console.log("connected to relay"); 70 | 71 | /* Q-2: 「このBotの公開鍵へのリプライ」を絞り込むフィルタを設定して、イベントを購読しよう */ 72 | // ヒント: nostr-toolsのgetPublicKey()関数を使って、秘密鍵(BOT_PRIVATE_KEY_HEX)から公開鍵を得ることができます 73 | const sub = ???; 74 | 75 | sub.on("event", (ev) => { 76 | try { 77 | // リプライしても安全なら、リプライイベントを組み立てて送信する 78 | if (isSafeToReply(ev)) { 79 | const replyPost = composeReplyPost("こんにちは!", ev); 80 | publishToRelay(relay, replyPost); 81 | } 82 | } catch (err) { 83 | console.error(err); 84 | } 85 | }); 86 | }; 87 | 88 | main().catch((e) => console.error(e)); 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 「手を動かして学ぶ Nostrプロトコル」 演習環境 2 | [Hello Nostr! 先住民が教えるNosteの歩き方](https://nip-book.nostr-jp.org/book/1/) ([技術書典](https://techbookfest.org/)にて頒布) 内の記事「手を動かして学ぶ Nostrプロトコル」(pp.65-75) の演習環境を含むリポジトリです。 3 | 4 | ## 演習環境のセットアップ 5 | 6 | 以下のコマンドで演習環境をセットアップします。実行前に、Node.jsをインストールしておいてください。 7 | 8 | ```bash 9 | git clone https://github.com/nostr-jp/learn-nostr-by-crafting.git 10 | cd learn-nostr-by-crafting 11 | npm install 12 | ``` 13 | 14 | ## 演習の進め方 15 | 16 | 演習環境には、演習の各節に対応するソースファイルが用意されています。ファイル名の先頭(1-1〜2-3)が、対応する節を表します。 17 | 本文の説明に従ってソースコードの /* Q-1: ... */ となっている部分に適切なコードを書いて、目的の機能を実装していきましょう。 18 | 19 | 実装が完了したら、`node 1-1_fetch_posts_raw_ws.js` のようにして実行してください。 20 | 一部のプログラムは、実行時にコマンドライン引数を渡す必要がありますのでご注意ください。 21 | 22 | ## npmスクリプト 23 | 24 | 以下のnpmスクリプトが用意されていますので、必要に応じて利用してください。 25 | 26 | - `gen-key-pair`: 秘密鍵と公開鍵のペアを生成 27 | - `to-hex `: `npub...` `nsec...` のようなフォーマット(bech32形式といいます)のデータを、16進文字列形式に変換 28 | - bech32形式では、先頭の文字列(接頭辞)がデータの種類を表します。以下にNostrで使われている接頭辞の例を示します 29 | - `npub...`: 公開鍵 30 | - `nprofile...`: 公開鍵 + 付加情報 31 | - `nsec...`: 秘密鍵 32 | - `note...`: 投稿のイベントID 33 | - `nevent...`: 一般の(投稿に限らない)イベントID + 付加情報 34 | - `sub-reply <リレーURL> <公開鍵(hex形式)>`: 指定したリレーに接続し、指定した公開鍵を対象とするリプライを購読して表示 35 | - 1-4節の演習にて、リプライ実装チェッカーからのリプライ(チェック結果)を受け取るのに使うとよいでしょう 36 | 37 | ## ヒント 38 | - 1-2節以降で利用する nostr-tools のGitリポジトリは[こちら](https://github.com/nbd-wtf/nostr-tools) 39 | - 1-3節: 自分が普段利用している秘密鍵を調べるには? 40 | - Webブラウザ拡張機能(NIP-07)を利用している場合は、拡張機能のオプションから確認できるはずです 41 | - Amethystの場合: 画面左上のアイコンをタップ →「Backup Keys」→「Copy my secret key」 42 | - Damusの場合: 画面左上のアイコンをタップ→「設定」→「鍵」→「ログイン用秘密鍵」を確認 43 | - 1-4節: 各種Nostrクライアントで投稿のイベントIDを調べる方法。取得できるIDはbech32形式のため、npmスクリプト`to-hex` を使ってhex形式に変換する必要があります 44 | - Snort: 投稿下方の ︙ →「Copy ID」 45 | - Iris: 投稿右上の … →「Copy Note ID」 46 | - Rabbit: 投稿下方の … →「IDをコピー」 47 | - nostter: 投稿右下の {…} →「Note ID」を確認 48 | - 1-4節: 各種Nostrクライアントで投稿者の公開鍵を調べる方法。取得できるIDはbech32形式のため、npmスクリプト`to-hex` を使ってhex形式に変換する必要があります 49 | - Snort: 投稿者のアイコンをクリック → 表示されるプロフィール画面で`npub`から始まる文字列を確認 50 | - Iris: 投稿者のアイコンをクリック → 右上の … → 「Copy User ID」 51 | - Rabbit: 投稿者のアイコンをクリック → 表示されるプロフィール画面で`npub`から始まる文字列を確認 52 | - nostter: 投稿者のアイコンをクリック → 表示されるプロフィール画面で`npub`または`nprofile`から始まる文字列を確認 53 | - 1-4節: リプライの実装が正しいか確認してくれる [リプライ実装チェッカーbot](https://nostx.shino3.net/npub1y75tnycxnpp8z23fkql4xn597x3alnd728xa93ujxtxvxrktvm5qf3rg9u/) を用意しています。公開鍵は以下の通り 54 | 55 | ``` 56 | 27a8b993069842712a29b03f534e85f1a3dfcdbe51cdd2c79232ccc30ecb66e8 57 | ``` 58 | 59 | ## 訂正・更新 60 | 誤字の訂正、ならびに執筆当時から状況が変化したために古くなってしまった内容に関する補足などを以下にまとめます。 61 | 62 | ### 正誤表 63 | 単純な誤字・誤表記の訂正。 64 | 65 | |箇所|誤|正|備考| 66 | |---|--|--|---| 67 | |pp.68 9-11行目|`Relay.connect()`
`Relay.sub()`|`Relay#connect()`
`Relay#sub()`|これらは実際には`Relay`のインスタンスメソッドだが、クラスメソッドのように見える表記となっており、誤解が生じる可能性があるため訂正| 68 | |pp.69 6行目|`Relay.publish()`|`Relay#publish()`|同上| 69 | |pp.73 「フォロー」の節 5行目|...、続いてそれ含まれる...|...、続いてそれ**に**含まれる...|| 70 | 71 | 72 | ### 関数`signEvent`の改称 73 | pp.69 5行目で説明している関数`signEvent`は、nostr-toolsのバージョン1.11.1より`getSignature`という名前に改称されました。 74 | 古い名前の関数は非推奨化されており、早いうちに移行すべきと考えられます。 75 | 76 | > 改称にまつわる詳細な経緯については [こちら](https://github.com/nbd-wtf/nostr-tools/pull/195) を参照のこと 77 | 78 | 79 | 以上を踏まえ、pp.69 5行目の内容を以下の通り訂正します。 80 | 81 | (旧) ・関数 `signEvent()` にイベントと秘密鍵を渡して署名 82 | 83 | (新) ・関数 **`getSignature()`** にイベントと秘密鍵を渡して署名**を得る** 84 | 85 | あわせて、演習問題のコードについても新しい名前の関数を利用するように変更してあります。 86 | 87 | 88 | ## 解答例 89 | [このページ](https://lnbc-answer-gate.jiftechnify.workers.dev/)にアクセスして以下の通りに認証情報を入力すると、演習問題の解答例が掲載されたページへのリンクが表示されます。 90 | 91 | - ユーザー名: learn-nostr 92 | - パスワード: 「手を動かして学ぶ Nostrプロトコル」の最終ページにある挿絵の吹き出しに書かれている内容(記号も含めて正確に!) 93 | 94 | ## ライセンス 95 | このリポジトリ内の**ソフトウェア**は [The Unlicense](https://unlicense.org/) のもとで公開されます。 96 | 97 | 特に、このリポジトリに含まれるソフトフェアを使用・変更・再配布するにあたり、著作権表示は必要ありません。 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-nostr-by-crafting", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "repository": "ssh://git@github.com/jiftechnify/learn-nostr-by-crafting.git", 7 | "author": "jiftechnify", 8 | "license": "Unlicense", 9 | "private": true, 10 | "scripts": { 11 | "to-hex": "node scripts/to_hex.js", 12 | "gen-key-pair": "node scripts/gen_key_pair.js", 13 | "sub-reply": "node scripts/sub_reply.js" 14 | }, 15 | "dependencies": { 16 | "nostr-tools": "1.13.1", 17 | "websocket-polyfill": "^0.0.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/gen_key_pair.js: -------------------------------------------------------------------------------- 1 | import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools"; 2 | 3 | const privkey = generatePrivateKey(); 4 | const pubkey = getPublicKey(privkey); 5 | 6 | console.log("秘密鍵: %s (%s)", privkey, nip19.nsecEncode(privkey)); 7 | console.log("公開鍵: %s (%s)", pubkey, nip19.npubEncode(pubkey)); 8 | -------------------------------------------------------------------------------- /scripts/sub_reply.js: -------------------------------------------------------------------------------- 1 | import{relayInit as r}from"nostr-tools";import"websocket-polyfill";if(process.argv.length<=3){console.error("usage: npm run sub-reply ");process.exit(1)}const[n,c]=process.argv.slice(2,4);const e=async()=>{const e=r(n);e.on("error",()=>{console.error("failed to connect")});await e.connect();console.log("connected to relay");const o=e.sub([{kinds:[1],"#p":[c],since:Math.floor((new Date).getTime()/1e3)}]);o.on("event",e=>{console.log("received reply!");console.log(e)})};e().catch(e=>console.error(e)); 2 | -------------------------------------------------------------------------------- /scripts/to_hex.js: -------------------------------------------------------------------------------- 1 | import { nip19 } from "nostr-tools"; 2 | 3 | if (process.argv.length <= 2) { 4 | console.error( 5 | "usage: npm run to-hex " 6 | ); 7 | process.exit(1); 8 | } 9 | 10 | const { type, data } = nip19.decode(process.argv[2]); 11 | 12 | const out = (() => { 13 | switch (type) { 14 | case "npub": 15 | return `公開鍵: ${data}`; 16 | case "nprofile": 17 | return `公開鍵: ${data.pubkey}`; 18 | case "nsec": 19 | return `!!! 取り扱い注意 !!! 秘密鍵: ${data}`; 20 | case "note": 21 | return `イベントID: ${data}`; 22 | case "nevent": 23 | return `イベントID: ${data.id}, イベント発行者の公開鍵: ${ 24 | data.author ?? "(不明)" 25 | }`; 26 | default: 27 | return `${type}: この種類のIDには対応していません! `; 28 | } 29 | })(); 30 | 31 | console.log(out); 32 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | // 現在のunixtime(秒単位)を取得 2 | const currUnixtime = () => Math.floor(new Date().getTime() / 1000); 3 | 4 | // 1番目のコマンドライン引数を取得 5 | const getCliArg = (errMsg) => { 6 | if (process.argv.length <= 2) { 7 | console.error(errMsg); 8 | process.exit(1); 9 | } 10 | return process.argv[2]; 11 | }; 12 | 13 | module.exports = { 14 | currUnixtime, 15 | getCliArg, 16 | }; 17 | --------------------------------------------------------------------------------