LoginSignup
5
5

Arduino(ESP32)から次世代SNSプロトコル Nostrにアクセスしてみる

Last updated at Posted at 2024-01-03

はじめに

Arduino(ESP32)から次世代SNSネットワーク Nostrにアクセスしてみます。

かつては、Twitterとマイコンを繋いで遊ぶのが主流でしたが、
それと同程度以上のことが色々とできます。

かつてのUserStream並の高速な通信と、サーバー落ちに対する耐性を持ったNostrプロトコルで遊んでみましょう。

image.png

Nostrについては、以下を参照ください。

事前準備

  • まず、Nostrのアカウント(鍵ペア)を作成してください。
  • nostterや、Damusなどアプリを使用して、Timelineなどの見え方を確認しておいてください。
  • 必要に応じて、実験用のアカウント(鍵ペア)を作成し、そちらもログインしておいてください。

環境構築

マイコンにはWi-Fi搭載ESP32を積んでいるこちらを使います。(ATOM Lite)

開発環境はこちらを参考に構築してください。

正しくセットアップできているとこうなります。

image.png

下記の画像を参考に、No OTAの設定をしておいてください。

image.png

この状態で、ライブラリマネージャ(本棚のマーク)をクリックし、nostrを入れてArduino-nostrをインストールしてください。

image.png

ArduinoJsonもインストールしておいてください。

image.png

リファレンスは下記にあります。

原理の説明

Nostrの通信には、Websocketを使用します。
ほぼすべてのリレーはwss(Encrypted WebSocket connections)を使用しますので、TLS対応している必要があります。

書き込みにはさらに、ビットコインでおなじみのSchnorr署名(BIP 340)と、正確な時刻が必要になります。

これらを自前で実装するのは用意ではないため、Arduino-nostrライブラリを使用しています。
また、電文はJson形式のため、ArduinoJsonも使用します。

このあたりの通信仕様に関する詳細は、下記のnostr仕様を確認してください。

原文

日本語訳版

TLを取得する

ではタイムラインを取得してみましょう。

#include <NostrEvent.h>
#include <NostrQueueProcessor.h>
#include <NostrRelayManager.h>
#include <NostrRequestOptions.h>
#include <aes.h>
#include <aes.hpp>

NostrRelayManager nostrRelayManager;
StaticJsonDocument<4096> doc;

// イベント受信時コールバック
void nip01Event(const std::string& key, const char* payload) {
  DeserializationError error = deserializeJson(doc, payload);

  if (error) {
    Serial.print("deserializeJson() failed: ");
    Serial.println(error.c_str());
    return;
  }

  // Jsonパース結果を取得
  JsonObject root_2 = doc[2];
  const char* root_2_content = root_2["content"];
  Serial.print("> 受信した本文: ");
  Serial.print(root_2_content);
  Serial.println();
}

void setup() {
  Serial.begin(115200);

  // 無線LANに接続
  Serial.println("> Wifi Connect");
  WiFi.begin("SSIDをここに入れます", "パスワードをここに入れます");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // 接続先リレー 一覧(日本語コミュニティで有名なもの)
  // 3個程度が限界らしく、5個入れるとheapエラーで落ちる。
  std::vector<String> relays = {
    "relay-jp.nostr.wirednet.jp",
    "nostr.fediverse.jp",
    "yabu.me",
//    "nrelay-jp.c-stellar.net",
//    "r.kojira.io",
  };
  nostrRelayManager.setRelays(relays);

  // 最低1接続必要とする。10秒経つと最低数に達していなくてもブロードキャストする。
  nostrRelayManager.setMinRelaysAndTimeout(1, 10000);

  // コールバックハンドラを設定
  nostrRelayManager.setEventCallback(1, nip01Event);

  // リレーに接続
  Serial.println("> Relay connect");
  nostrRelayManager.connect();

  delay(1000);

  // Subscribeを実施する
  NostrRequestOptions req{};

  // 受信対象はkind 1
  int kinds[] = { 1 };
  req.kinds = kinds;
  req.kinds_count = 1;

  // 初回受信個数は5
  req.limit = 5;

  // イベントキューに登録
  nostrRelayManager.requestEvents(&req);

  Serial.println("> OK");
}

void loop() {
  // Websocket処理を実施
  nostrRelayManager.loop();

  // キューに溜まっているイベントの処理を実行する
  nostrRelayManager.broadcastEvents();
}

書込み後、シリアルモニタを起動します。

image.png

うまく接続できると、このようになります。
放置しておくと、次々と誰ともなくNostr上の発言が次々流れていきます。

entry 0x400805e4
> Wifi Connect
..> Relay connect
Relays are:
relay-jp.nostr.wirednet.jp
nostr.fediverse.jp
yabu.me
Connecting to relay: relay-jp.nostr.wirednet.jp
Connecting to relay: nostr.fediverse.jp
Connecting to relay: yabu.me
REQ looks like this
["REQ", "SQDBbnkdJvq2Je4Vep6SjWGCTfhrRipE92u9uz634XV6Bxw6pOqhCfd0Ziiaeu6d",{"kinds":[1],"limit":5}]
> OK
Message from relay index:2
[WSc] Disconnected from relay: .
Message from relay index:1
[WSc] Connected to relay: .
Broadcasting event: ["REQ", "SQDBbnkdJvq2Je4Vep6SjWGCTfhrRipE92u9uz634XV6Bxw6pOqhCfd0Ziiaeu6d",{"kinds":[1],"limit":5}]
Message from relay index:1
[WSc]: Received text from relay .: ["EVENT","SQDBbnkdJvq2Je4Vep6SjWGCTfhrRipE92u9uz634XV6Bxw6pOqhCfd0Ziiaeu6d",{"id":"b18f6008b83fa1ab98c111ae0c7a59e29c2844e65c9857831b8c288830068c53","kind":1,"pubkey":"b707d6be7fd9cc9e1aee83e81c3994156cfcf74ded5b09111930fdeeeb5a0c20","created_at":1704281720,"content":"色褪せたカニ","tags":[],"sig":"2be00da6bfb240584a294e2924d748b5820a86169d75b6bafbcadbd2b1dc8b224501e9f6f44c2bdcae1affa6de4a6958d9c8fa7a2e684bdcb23454dea17d1e79"}]
> 受信した本文: 色褪せたカニ

特定のユーザーの情報を取得する

このままでは、無数のNostr住民の声を垂れ流すだけになってしまいます。
なにか有用な使い方はできないか、ということで、緊急地震速報を提供してくれている「なまずくん」を購読してみましょう。

image.png

このアカウントのID(公開鍵)は以下になります。

npub1namazu7um9xvgfpax6yrk9tl3segxpgac67jx7cuttzqp7usem9sqavlhz

このnpub表記は、人間にわかりやすくするためのものなので、HEX表記に変換する必要があります。
簡単には、下記のサイトで変換することができます。

変換すると以下のようになります。

9f77d173dcd94cc4243d36883b157f8c3283051dc6bd237b1c5ac400fb90cecb

受信条件に、これを追加したものが以下になります。

#include <NostrEvent.h>
#include <NostrQueueProcessor.h>
#include <NostrRelayManager.h>
#include <NostrRequestOptions.h>
#include <aes.h>
#include <aes.hpp>

NostrRelayManager nostrRelayManager;
StaticJsonDocument<4096> doc;

// イベント受信時コールバック
void nip01Event(const std::string& key, const char* payload) {
  DeserializationError error = deserializeJson(doc, payload);

  if (error) {
    Serial.print("deserializeJson() failed: ");
    Serial.println(error.c_str());
    return;
  }

  // Jsonパース結果を取得
  JsonObject root_2 = doc[2];
  const char* root_2_content = root_2["content"];
  Serial.print("> 受信した本文: ");
  Serial.print(root_2_content);
  Serial.println();
}

void setup() {
  Serial.begin(115200);

  // 無線LANに接続
  Serial.println("> Wifi Connect");
  WiFi.begin("SSIDをここに入れます", "パスワードをここに入れます");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // 接続先リレー 一覧(日本語コミュニティで有名なもの)
  // 3個程度が限界らしく、5個入れるとheapエラーで落ちる。
  std::vector<String> relays = {
    "relay-jp.nostr.wirednet.jp",
    "nostr.fediverse.jp",
    "yabu.me",
//    "nrelay-jp.c-stellar.net",
//    "r.kojira.io",
  };
  nostrRelayManager.setRelays(relays);

  // 最低1接続必要とする。10秒経つと最低数に達していなくてもブロードキャストする。
  nostrRelayManager.setMinRelaysAndTimeout(1, 10000);

  // コールバックハンドラを設定
  nostrRelayManager.setEventCallback(1, nip01Event);

  // リレーに接続
  Serial.println("> Relay connect");
  nostrRelayManager.connect();

  delay(1000);

  // Subscribeを実施する
  NostrRequestOptions req{};

  // 受信対象はkind 1
  int kinds[] = { 1 };
  req.kinds = kinds;
  req.kinds_count = 1;

  // 発信者を絞る (※※※追加箇所※※※)
  String authors[] = { "9f77d173dcd94cc4243d36883b157f8c3283051dc6bd237b1c5ac400fb90cecb" };
  req.authors = authors;
  req.authors_count = 1;

  // 初回受信個数は1
  req.limit = 1;

  // イベントキューに登録
  nostrRelayManager.requestEvents(&req);

  Serial.println("> OK");
}

void loop() {
  // Websocket処理を実施
  nostrRelayManager.loop();

  // キューに溜まっているイベントの処理を実行する
  nostrRelayManager.broadcastEvents();
}

以下のような出力が得られます。
地震が発生するとリアルタイムに追加で出力されるはずです。

Message from relay index:1
[WSc]: Received text from relay .: ["EVENT","fNuynsc3TYVA89uKJXv6MANmeWbKjl9EzJHHTnP0qINUE7pP9JKktD6vswErbWbe",{"id":"9e7cbbded06479eec4d07f97b30474ccfed17d4cf478dc5d719d7d880c89a9da","kind":1,"pubkey":"9f77d173dcd94cc4243d36883b157f8c3283051dc6bd237b1c5ac400fb90cecb","created_at":1703892336,"content":"**緊急地震速報(予報)** 第1報\n30日08時25分ごろ、地震がありました。\n震源地は宮古島近海(北緯25.2度、東経125.6度)で震源の深さは約20km、地震の規模(マグニチュード)は3.6、この地震による最大震度は震度2と推定されます。\nhttps://earthquake.tenki.jp/bousai/earthquake/detail/2023/12/30/2023-12-30-08-25-25.html","tags":[],"sig":"1292b4a5db6c6b05182eab9e6b5f65d2a63506e9fcc09b6b59762d2b4a1a0eb852c7507acdb2a16e364281cb8a50c45c3319591cc98e83ed7a3bdf74b8b78f14"}]
> 受信した本文: **緊急地震速報(予報)** 第1報
30日08時25分ごろ、地震がありました。
震源地は宮古島近海(北緯25.2度、東経125.6度)で震源の深さは約20km、地震の規模(マグニチュード)は3.6、この地震による最大震度は震度2と推定されます。
https://earthquake.tenki.jp/bousai/earthquake/detail/2023/12/30/2023-12-30-08-25-25.html

書き込む

読み込みだけでは面白くありません。書き込みもやりましょう。
前述した通り、書き込みには、電子署名と正確な時刻が必要になります。

image.png

コードは以下です。

#include <NostrEvent.h>
#include <NostrQueueProcessor.h>
#include <NostrRelayManager.h>
#include <NostrRequestOptions.h>
#include <aes.h>
#include <aes.hpp>

NostrEvent nostr;
NostrRelayManager nostrRelayManager;

void setup() {
  Serial.begin(115200);

  // 無線LANに接続
  Serial.println("> Wifi Connect");
  WiFi.begin("SSIDをここに入れます", "パスワードをここに入れます");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // 接続先リレー 一覧(日本語コミュニティで有名なもの)
  // 3個程度が限界らしく、5個入れるとheapエラーで落ちる。
  std::vector<String> relays = {
    "relay-jp.nostr.wirednet.jp",
    "nostr.fediverse.jp",
    "yabu.me",
//    "nrelay-jp.c-stellar.net",
//    "r.kojira.io",
  };
  nostrRelayManager.setRelays(relays);

  // 最低1接続必要とする。10秒経つと最低数に達していなくてもブロードキャストする。
  nostrRelayManager.setMinRelaysAndTimeout(1, 10000);

  // リレーに接続
  Serial.println("> Relay connect");
  nostrRelayManager.connect();

  delay(1000);

  // 時刻調整を実施(UTC)
  configTime(0, 0, "ntp.nict.jp");
  struct tm lt;
  getLocalTime(&lt); 

  Serial.println("> OK");

  // ----------------------------
  // 投稿を実施
  const char* nsec = "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"; // あなたの秘密鍵HEX
  const char* npub = "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp"; // あなたの公開鍵HEX
  const char* message = "こんにちは。これはEPS32からの投稿テストです。"; // 本文
  time_t t;
  time(&t); // 現在時刻

  String s = nostr.getNote(nsec, npub, t, message); // 署名処理を実行
  nostrRelayManager.enqueueMessage(s.c_str()); // 処理キューに押し込む
  // ----------------------------

  Serial.println("> Sent");
}

void loop() {
  // Websocket処理を実施
  nostrRelayManager.loop();

  // キューに溜まっているイベントの処理を実行する
  nostrRelayManager.broadcastEvents();
}

秘密鍵は外部に漏らさないように注意してください。
可能な限り、常用のアカウント(鍵ペア)ではなく、実験用のアカウント(鍵ペア)を作成して使用することをお勧めします。

NostrにはAPI制限が明確になく、それ故に送信処理を無限ループにすると、大量の投稿がリレーに向かって送信され、Timelineを埋め尽くします。
(rate-limited: slow down there chiefの応答が帰る場合もあります。)

そのまま放置した場合、接続中のリレーから秘密鍵あるいはIPアドレスごとBANされることになります。

Nostrの性質上、海外含めた多数のリレーが使用できるため、BANの影響は少ないですが、日本リレーからBANされた場合、日本語コミュニティとの交流に支障をきたすことになりますので、ご注意ください。

必要に応じて、自前で試験用のリレーを立てるなど、影響を少なくする方法を検討してください。

また、リレーごとのポリシーについては、各リレーが提供するNIP-11情報(リレーメタ情報)を参照してください。対応しているアプリケーションではGUIで確認することができます。

おわりに

TwitterのAPIが色々と不便になり、結構な時間が経ちました。
MastodonのAPIで遊ぶなど、代替手段は色々とありますが、
その1つとしてnostrで遊んでみるのも楽しいです。

汎用クライアントとして使用するには、kind 0のプロフィール情報や、kind 3フォローリストなどを扱う必要があり、そこをやり始めると少々手間ではあります。
一方でWebsocketの速報性もあり、楽しく利用できる使い方もあると思いますので、是非試してみてください。

冒頭の写真は、M5GFXとM5AtomDisplayを使用して、HDMIモニターに地震情報を出力しています。

注意
Nostr Assets ProtocolおよびNostrトークンは、Nostrの名前を勝手に使用している無関係の(おそらく詐欺)通貨です。混同しないようにご注意ください。

5
5
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
5
5