📝

JavaでNostrのリレーに1件だけテキストイベントを投稿するコードを書いてみた

2023/06/07に公開

JavaでのWebSocketClientの情報は少ない上に、Nostrが絡むとさらに少ないので忘備録代わりに書いてみました、第二弾。まだ、なぜその設定が必要なのかわからない点もありますが、動作したので記事にします。

  • 前提
    • このコードの利用により、損害を被ったとしても執筆者は一切の責任を負わないものとします。
    • エラーハンドリングなどがほとんどされていません。
    • Jettyのwebsocket-jetty-client、http2-client、jetty-client、http2-http-client-transportを使用しています。今回は11.0.15(一部11.0.14)を使用しています。執筆者はMavenを使用しましたが、手動で導入する時は、slf4jも必要になるかもしれません。
    • 今回は更に、nostr-javaからnostr-java-base、nostr-java-crypto、nostr-java-utilを使用しています。
    • Javaは17を使用しました。
    • 実行時にNo SLF4J providers were found.の警告が出ます。気になる場合は適切な設定を行ってください。
    • RELAY_URLにはwssで始まるリレーのアドレスを記入してください。
    • CONTNTには投稿したい内容を記入してください。
    • タグを設定する場合には更にコードの追記が必要になります。
SmallClientToPostToNostr.java
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;

@WebSocket
public class SmallClientToPostToNostr {

    private static final String CONTENT = "ここに投稿したい内容を記入する。";

    private static final String RELAY_URL = "wssで始まるリレーのアドレスを記入する";

    private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);


    @OnWebSocketConnect
    public void onConnect(Session session) {

        session.setMaxTextMessageSize(16 * 1024);
        System.out.printf("onConnect()---%sと接続しました%n",session.getRemoteAddress());

    }

    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {

        System.out.printf("onClose()---%sを理由として、コード%dで接続が終了しました%n", reason,statusCode);

    }

    @OnWebSocketError
    public void onError(Throwable cause) {

        cause.printStackTrace();

    }

    @OnWebSocketMessage
    public void onTextMessage(Session session, String message)
    {
        if ("close".equalsIgnoreCase(message)) {
            session.close(StatusCode.NORMAL, "bye");
            return;
        }

        if(!queue.offer(message)){
            System.err.println("メッセージ受け取りに失敗しました。");
        }

        System.out.printf("onTextMessage()---%sからメッセージを受け取りました。内容:%s%n",session.getRemoteAddress(),message);

    }

    @OnWebSocketMessage
    public void onBinaryMessage(byte[] payload, int offset, int length) {

        //バイナリーメッセージのときにここを使う

    }
    public static void main(String[] args) throws Exception{

        PrivateKey secKey = new PrivateKey(Schnorr.generatePrivateKey());
        PublicKey publicKey = new PublicKey(Schnorr.genPubKey(secKey.getRawData()));

        System.out.println("SecKey(HEX) = " + secKey);
        System.out.println("publicKey(HEX) = " + publicKey);

        //SSLの場合の設定ここから
        SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
        sslContextFactory.setIncludeProtocols("TLSv1.3");
        ClientConnector clientConnector = new ClientConnector();
        clientConnector.setSslContextFactory(sslContextFactory);

        ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11;
        ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector));

        HttpClientTransport transport = new HttpClientTransportDynamic(clientConnector, h1, h2);
        HttpClient httpClient = new HttpClient(transport);
        //SSLの場合の設定ここまで

        WebSocketClient webSocketClient = new WebSocketClient(httpClient);
        webSocketClient.start();

        SmallClientToPostToNostr clientEndPoint = new SmallClientToPostToNostr();

        URI serverURI = URI.create(RELAY_URL);

        //カスタムリクエストの作成(何のため)?
        ClientUpgradeRequest customRequest = new ClientUpgradeRequest();
        customRequest.setHeader(HttpHeader.UPGRADE.asString(), "h2c");
        customRequest.setHeader(HttpHeader.CONNECTION.asString(), "Upgrade, HTTP2-Settings");

        JettyUpgradeListener listener = new JettyUpgradeListener() {

            @Override
            public void onHandshakeRequest(HttpRequest request) {}

            @Override
            public void onHandshakeResponse(HttpRequest request, HttpResponse response) {}
        };

        CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI, customRequest, listener);

        //接続時間稼ぎ
        Thread.sleep(1500);

        try(Session session = clientSessionPromise.join()) {

            if (!session.isOpen()) {
                System.err.println("接続できませんでした");
                System.exit(-1000);
            }

            String publicKeyHex = publicKey.toString();
            long created_at = Instant.now().getEpochSecond();
            int kind = 1;
            List<List<String>> tags = new ArrayList<>();

            //id計算に使う要素を集める
            // [0,"公開鍵(HEX)",投稿時間,kind(プレーンテキスト投稿は1),タグ群,"投稿内容"]
            String partsForId ="[0,\""+publicKeyHex+"\","+created_at+","+kind+","+ tags +",\""+ CONTENT +"\"]";
            byte[] idSourceBytes = partsForId.getBytes(StandardCharsets.UTF_8);
            String id = NostrUtil.bytesToHex(NostrUtil.sha256(idSourceBytes));
            byte[] signedHashedSerializedEvent =
                            Schnorr.sign(NostrUtil.sha256(idSourceBytes), secKey.getRawData(), NostrUtil.createRandomByteArray(32));
            String sig = NostrUtil.bytesToHex(signedHashedSerializedEvent);

            //メッセージは["EVENT",{
            // "id":"id",
            //"pubkey":"公開鍵(Hex)",
            //"created_at":作成時間(UnixTimeStamp),
            //"kind":kind(プレーンテキストは1),
            //"tags":[],
            //"content":"投稿内容",
            //"sig":"署名"
            // }]
            String message = "[\"EVENT\",{\"id\":\""+id+"\",\"pubkey\":\""+publicKeyHex+"\","
                    + "\"created_at\":"+created_at+",\"kind\":"+kind+",\"tags\":"+tags+","
                    +"\"content\":\""+ CONTENT +"\",\"sig\":\""+sig+"\"}]";

            String displayMessage = message.replace(",",",\n");
            System.out.println("組み立てたメッセージ:\n"+displayMessage);

            session.getRemote().sendString(message);

            //受信時間稼ぎ
            Thread.sleep(1500);

            webSocketClient.stop();

            for (String str : queue) {

                System.out.println("メッセージを受信:" + str);

            }
        }
    }
}

参考資料:
https://github.com/nostr-protocol/nips/blob/master/01.md
https://github.com/tcheeric/nostr-java

Discussion