20250201_nostr_python.rst (11397B)
1 Hello nostr 2 ########### 3 4 :date: 2025-03-20 19:29:35 5 :category: Coding 6 :author: Louis Holbrook 7 :tags: python,nostr,bitcoin,websockets,rust 8 :slug: hello-nostr-with-python 9 :summary: Assembling and publishing a simple text message to a nostr relay. 10 :lang: en 11 :status: published 12 13 14 Why nostr?! 15 =========== 16 17 Because it is simple. 18 19 And by that I mean the base layer is simple. There is no black magic that needs to happen to get admitted, synced and discovered. 20 21 At its core, it goes something like this: 22 23 1. Set up a server. 24 2. Start putting content there. 25 3. Point people to your server to find your content. 26 4. They know it's you because you signed your content with a key. 27 28 Of course, apart from 4, that's not really that different of a description from any communications infrastructure anyone would use. 29 30 I understand that many think 4 is a big deal. But let's face it; most people couldn't care less about their digital sovereignity. The vast majority of crypto holders defer to others to keep their key safe, one way or another. It is a big deal because you *can*, but not because people *do*. 31 32 However, for those who *do* care, who *want* to take responsiblity, nostr offers some things I haven't seen in any other project of its kind so far: 33 34 1. The `core protocol <nostrcore_>`_ is a couple of pages, elegant and simple, and expressed in clear language. 35 2. I can easily execute that same protocol with the most basic of tools. 36 3. There exists server software that is easy to build, easy to set up and that does not require much resources to run. 37 4. `"It does not rely on P2P techniques, and therefore it works." <https://github.com/nostr-protocol/nostr?tab=readme-ov-file#nostr---notes-and-other-stuff-transmitted-by-relays>`_ 38 39 In other words. To do something with nostr does not involve connecting to some infernal machine that abstracts all its complexity. It is *in itself not complex*. 40 41 It is simple. 42 43 I hope this post works as a demonstration of that. 44 45 At the time of writing I am using: 46 47 * ``linux 6.12.10`` 48 * ``python 3.13.1`` 49 * ``rust 1.81 (2dbb1af80 2024-08-20)`` 50 51 All rust applications below are built simply with ``cargo build --release``. 52 53 54 The message 55 =========== 56 57 Please meet the script that will generate our scratch nostr message. All it took to write it was to read through the first `"Nostr Improvement Proposal" (NIP-01) <nostrcore_>`_. [1]_ 58 59 .. include:: code/hello-nostr-with-python/msg.py 60 :code: python 61 :name: msg.py 62 63 To run the script, you will need the coincurve_ python module. As always, a virtual environment is recommended as usual, while you are playing around: 64 65 .. code-block:: bash 66 67 $ python -m venv .venv 68 $ source .venv/bin/activate 69 # let's install bech32, too, we will need it later 70 $ pip install coincurve bech32 71 $ python msg.py 72 73 Once run, the script will produce the signed message ready to be sent to the relay. 74 75 It will resemble the data below, but with a different value for the ``created_at``, which in turn results in different values for ``id`` and ``sig``. 76 77 78 .. code-block:: json 79 80 ["EVENT", {"id": "63b43ae8d74b5df17659a4663f256c6829994970ca6b08a5d068e0c01a460461", "pubkey": "cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01", "created_at": 1738407317, "kind": 1, "tags": [], "content": "hello nostr ¶", "sig": "c2ccf3524908c54a53e5fc1405b56b514dcf1d80baa7c135514fce3dcafb6646920c560f612e3769d2682b318f44f880c7147cb810ca88ffea0496cda087c583"}] 81 82 83 The runnning 84 ============ 85 86 To share this with the world we need some service to read and write from. 87 88 Starting out I am using the nostr-rs-relay_ (``v0.9.0``, ``git:ff65ec2acd781150a585a78e1c60b0cdb104698e``). It gives you enough debug output to follow what's going on, and it's easy to build [2]_ and configure: 89 90 .. code-block:: ini 91 92 # nostr-rs-relay configuration toml file 93 # limited to the fields that should be changed 94 95 [info] 96 relay_url = "ws://localhost" 97 name = "my_first_nostr_node" 98 description = "act local, think local" 99 # same pubkey as the script above 100 pubkey = cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01 101 102 [network] 103 bind = 127.0.0.1 104 port = 8081 105 106 In the repository, make a new directory ``db`` and run it. The process starts a websocket server which is used to send and receive events. One of the log lines in the terminal should show you where it is listening. 107 108 .. code-block:: bash 109 110 $ RUST_LOG=debug ./target/release/nostr-rs-relay -c config.toml -d db 111 [...] 112 2025-02-01T11:05:55.223024Z INFO nostr_rs_relay::server: listening on: 127.0.0.1:8081 113 114 115 The sending 116 =========== 117 118 We use websocat_ (``v1.14.0``, ``git:83c4375ac9a0475b7a8f3b75e49290f4486a4914``) to provide the terminal connection to the node. 119 120 Add a bit of fanciness by piping the output of our message generator python script to the command: 121 122 .. code-block:: bash 123 124 $ python msg.py | tee /dev/stderr | websocat -v ws://localhost:8080 125 [INFO websocat::lints] Auto-inserting the line mode 126 [INFO websocat::stdio_threaded_peer] get_stdio_peer (threaded) 127 [INFO websocat::ws_client_peer] get_ws_client_peer 128 ["EVENT", {"id": "bac1d459b39ac0ba91951491e382b8b5648b149b509ea8585a369d0a84101447", "pubkey": "cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01", "created_at": 1738408544, "kind": 1, "tags": [], "content": "hello nostr ¶", "sig": "322470a022f21fe4eae6b3652f4cca59a71d2e865791880117aba7bdb069bfed8780eea7874e0d475db71beef4c498b5a559bb4dd3ef87ecce68f157a7b64213"}] 129 [INFO websocat::net_peer] Failure during connecting TCP: Connection refused (os error 111) 130 [INFO websocat::net_peer] Connected to TCP 127.0.0.1:8081 131 [INFO websocat::ws_client_peer] Connected to ws 132 ["OK","bac1d459b39ac0ba91951491e382b8b5648b149b509ea8585a369d0a84101447",true,""] 133 [INFO websocat::sessionserve] Forward finished 134 [INFO websocat::ws_peer] incoming None 135 [INFO websocat::sessionserve] Reverse finished 136 [INFO websocat::sessionserve] Both directions finished 137 138 As we see, the response from the node (the line with ``"OK"`` as first value) indicates a success, and cites the ``id`` matching the ``id`` in the message json contents we emitted to ``stderr``. 139 140 The message has now been published. 141 142 143 The viewing 144 =========== 145 146 Of course we can use a similar trick to get the message back out. More on that a bit later. 147 148 But for now, let's use a proper nostr client to view the published message. 149 150 My introduction to nostr has been through gossip_ (``v0.13.0``, ``git:90712385f6f79b60c01ae588464be4c960e76836``), a rust-based graphical client with plenty of levers to pull, aswell as an easy way of running clients with different states and keys simultaneously. 151 152 The only annoying thing about gossip I have found so far is that it will not accept localhost as a value when adding relays to use. So we will use our local hostname instead. Make sure you have an entry in your ``/etc/hosts`` and that it resolves: 153 154 .. code-block:: bash 155 156 $ cat /etc/hosts 157 [...] 158 127.0.0.1 localhost myhost 159 160 $ getent ahostsv4 myhost 161 127.0.0.1 STREAM localhost 162 [...] 163 164 Once built, create an empty data directory, and run gossip on it: 165 166 .. code-block:: bash 167 168 $ mkdir foo 169 $ RUST_LOG=debug GOSSIP_DIR=foo ./target/release/gossip 170 171 You will go through a setup wizard, prompting you to create a new account, and then ask you for relays to use and people to follow. 172 173 On the relay page, remove the one suggested and add instead ``ws://myhost:8081`` to the *Outbox* and *InBox* categories. 174 175 .. image:: {static}/images/gossip_relay_init.png 176 177 On the follow page, follow the public key of the message we already have published: ``cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01``. [3]_ 178 179 Completing this step will take you to the main application page. Here, navigate to ``Relays -> My relays``, which should list the one relay added using the wizard. Expand the entry, activate the *Global Feed* switch. 180 181 .. image:: {static}/images/gossip_relay_global.png 182 183 Navigate back to the *Global* menu item. In a short while, the post made with the script should appear [4]_. 184 185 .. image:: {static}/images/gossip_feed.png 186 187 With any luck, it should appear under the **Following** menu item, too. 188 189 Getting hooked 190 ============== 191 192 Let us now read a response from the gossip client directly in the websocket connection. 193 194 While in gossip, click the reply button on the post, write a message and send it. 195 196 Then, choose the menu item *Settings* and copy the value in the *Public Key* entry (starts with ``npub...``) 197 198 Back in the terminal, using a new script, we will now generate a subscription request for text message events signed with this public key. 199 200 .. include:: code/hello-nostr-with-python/sub.py 201 :code: python 202 203 For the public key ``npub1p2vekljvajp4d3kqdwvha54d6ywh2j360xpl32ug70r8y009h86q7g3tr4`` we would do: 204 205 .. code-block:: bash 206 207 $ python sub.py npub1p2vekljvajp4d3kqdwvha54d6ywh2j360xpl32ug70r8y009h86q7g3tr4 | tee /dev/stderr | websocat ws://alto:8081 208 ["REQ", "2c626a2ee46a1751b271447264b1eb524b4ed0f834d35a94d3d74d21ed003fde", {"authors": ["0a999b7e4cec8356c6c06b997ed2add11d754a3a7983f8ab88f3c6723de5b9f4"], "kinds": [1]}] 209 ["EVENT","873a3ce558e0690c4d831bea2c74b24f40407ef2bc42e251792c0bf80e94eb28",{"id":"b1474751a1799c4785aa8272fba5f20034abe3312d38335ae5553c95045e03b3","pubkey":"0a999b7e4cec8356c6c06b997ed2add11d754a3a7983f8ab88f3c6723de5b9f4","created_at":1738415174,"kind":1,"tags":[["p","cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01"],["e","55befa55c97b59fb4f206455246c6d14f0711c429502dc814036fd271a544ea6","ws://alto:8081/","root","cc9519ba6fb1cb0cca53743dc90c2418440cf637f8b891ce2f0e2dc5c5b3cf01"]],"content":"Hello back🙏🚀","sig":"1288e42970e746e6908350e434db27e408b40ff0290e7d54e32740a5dd6ceeca74cea410694eb5f1347099372fb2fdf8d58369c565fe2cc57a64be49c05dfa75"}] 210 ["EOSE","873a3ce558e0690c4d831bea2c74b24f40407ef2bc42e251792c0bf80e94eb28"] 211 212 213 And of course, while this connection is kept open, any other messages posted by the same key will appear in the terminal. 214 215 .. _nostrcore: https://github.com/nostr-protocol/nips/blob/master/01.md 216 217 .. _coincurve: https://github.com/ofek/coincurve 218 219 .. _gossip: https://github.com/mikedilger/gossip 220 221 .. _websocat: https://github.com/vi/websocat 222 223 .. _nostr-rs-relay: https://github.com/scsibug/nostr-rs-relay 224 225 .. 226 227 .. [1] To be fair, in reality NIP-10 also, which is referenced by NIP-01, although the meat of it is already spelled out in the latter. 228 229 .. [2] Simply ``cargo build --release``. If you are using the `rustup <https://rustup.rs/>`_ toolchain manager, you can set up provisions with ``rustup default 1.81`` first. 230 231 .. [3] The public key will be encoded to the nostr `npub` encoding scheme, a `bech32` encoded address. It's really just semantics, and represents the same value. Have a look at `python_nostr <https://github.com/jeffthibault/python-nostr/blob/main/nostr/key.py#L30>`_. 232 233 .. [4] The synchronization logic in gossip still seems to be slightly lazy, so you may have to move back and forth between menu items to trigger redraws that will list the message.