This post, despite posting it first elsewhere, was inspired by the principles of open learning, so I am reposting it here
I want to write a little thing about my very first real SSB hack day the other day.
Goal: to write a minimal SSB thingy that could do two things:
- send private messages
- receive new private messages
There is no requirement to fetch past sessions or conversations.
Hereâs something like how things wentâŚ
I didnât know whether to use ssb-client or ssb-server? At first I tried running ssb-client, and it looked promising, then something made me realize that it was connecting to my open Patchwork ssb-server instance.
So then I realized I definitely needed ssb-server because I want this to be standalone⌠can spin it up on a completely new system easily, and probably independent of other ssb apps.
I think it was in the readme of ssb-server that I hit some confusion⌠it has two chunks of code, one for ssb-server, and then says âelsewhere:â and shows another code snippet with âssb-clientâ. This made me unsure whether I NEEDED ssb-client, or whether it was just an option.
See the bit about âexample usageâ on ssb-server README.
Using ssb-server, there were a lot of references to plugins. This was a sore spot.
I couldnât find a good reference for which plugins existed nor what they did. If someone has such a reference, I would still love to see it. I could see examples of ssb plugins in code samples, but somehow that didnât feel helpful. It seemed a bit arbitrary.
There is this resource that references plugins, but it felt it went deeper than I needed on one particular plugin, so I mostly skipped over it.
Wouldnât this page be a good one to put plugins into?
https://www.scuttlebutt.nz/modules
It was explained to me here
that setting caps.shs
and caps.sign
I could work off a test network, and my experiments wouldnât reach the main SSB network. Fairly easy to grasp and get started with.
One thing Iâm still unsure of is port
in the config. Port for what?
Overall, this âfield guideâ does a nice job of what I think it intends to, a quick and dirty orientation.
I created ~/.ssb-test
empty folder. I created config
file, and dropped in:
{
"caps": {
"shs": "g4rJuU2yHGOcwc8OxrZDaGpmhHAt9r3fenaTIaRfSJo=",
"sign": "JRjK44nw1miRqP65nHCV3qfEFjdpWVoBv7jyJYjN/UQ="
},
"port": 8007,
"ws": {
"port": 8988
},
"ssb_appname": "ssb-test"
}
I generated the shs (secret handshake) and sign
values the recommended way, in a node âreplâ (just type node
) and then run crypto.randomBytes(32).toString('base64')
twice for the two new values.
I am STILL not sure whether I actually needed to perform this step: export ssb_appname="ssb-test"
in the terminal.
Before going for private messages, I went for unencrypted ones. For this, all I needed was some sample code from the ssb-server readme, now that I resolved that the same api available on ssb-client would be available on the ssb-server instance itself directly.
The server code could be pared down to:
const Server = require('ssb-server')
const Config = require('ssb-config/inject')
// ssb-test matches the name of ~/.ssb-test folder where the rest of the `config` is
const ssbconfig = Config('ssb-test')
Server
.use(require('ssb-gossip'))
.use(require('ssb-replicate'))
.use(require('ssb-private'))
const server = Server(ssbconfig)
Once youâve got the server, you can start to play. First, write a message, a-la ssb-server readme.
// publish a message
server.publish({ type: 'post', text: 'My First Post!' }, function (err, msg) {
// msg.key == hash(msg.value)
// msg.value.author == your id
// msg.value.content == { type: 'post', text: 'My First Post!' }
// ...
})
This worked fine.
Next, can I read a message. This gets me into the territory that made it daunting to work with ssb. pull-streams.
I see this code in the readme, and I donât know why itâs done this way or what it means. I have a slight idea from prior reading.
const pull = require('pull-stream')
// ...
// stream all messages in all feeds, ordered by publish time
pull(
server.createFeedStream(),
pull.collect(function (err, msgs) {
// msgs[0].key == hash(msgs[0].value)
// msgs[0].value...
})
)
For me, it is problematic at this point to not know what âpull.collectâ is/does, because what I need is a running (ongoing) stream, and I donât know whether this does that.
As it turns out, by testing with it, I realize that it doesnât. So I need to review more closely, what is pull
, and what is pull.collect
. For this, itâs time to find pull-streams docs.
I think, somehow or other that I canât remember now, this ended being the pull-streams resource that I found the fastest, which seems possibly problematic, since isnât this the outdated documentation?
https://scuttlebot.io/apis/pull-stream/pull-stream.html
Long story short, source
, through
and sink
descriptions on their pages helped⌠a lot of the intro and pre-amble is to philosophical.
âYou must have a source at the start of a pipeline for data to move through.â
âA Through is a stream that both reads and is read by another stream. Through streams are optional. Put through streams in-between sources and sinksâ
âYou must have a sink at the end of a pipeline for data to move towards. You can only use one sink per pipelineâ
Now I see this is a more comprehensive source of docs for it, though still intimidating: https://pull-stream.github.io/
This line kiinda explains the pull
function⌠âpull(a, b, c) is basically the same as a.pipe(b).pipe(c)â
So at this point, itâs obvious that server.createFeedStream()
is creating a source
.
And pull.collect
is creating a sink
. What kind of sink?
This page states: Sink Functions - Scuttlebot
pull.collect(cb)
: Read the stream into an array, then callback
I discovered what I wanted on that page, even though I still kinda donât get the descriptionâŚ
drain (op?, done?)
âDrain the stream, calling op on each data. call done when stream is finished. If op returns ===false, abort the streamâ
A simple way to test, found on that page was:
const pull = require('pull-stream')
// ...
// stream all messages in all feeds, ordered by publish time
// log each to the console
pull(
server.createFeedStream(),
pull.log()
)
pull.drain is really what I want to do something functional though, keeping the stream flowing:
pull(
server.createFeedStream(),
pull.drain((msg) => {
// do whatever I like with the message
})
)
Pretty big time investment gone into this simple goal. But it comes together, and definitely shows promise of its value in other more contexts. I am still interested in learning more about the various source and through functions in pull-streams
. Obviously some classic functional programming concepts in there like filter
and map
.
Just running the file like node index.js
over and over again with my testing at this point.
Have now successfully written and read messages, time to go encrypted.
This one was a bit obvious that Iâd need ssb-private
module/plugin installed. Up to this point everything could work just fine without it. This page was a good guide:
https://www.scuttlebutt.nz/guides/ssb-server/publish-encrypted-messages
Now we have ssb.private
to work with. We can replace both the read and write functions (I only learned after about replacing the read).
For read, replace server.createFeedStream()
with server.private.read({})
I think it errored out without an empty object, this seems like a buggy behaviour.
Docs for ssb-private are minimal: GitHub - ssbc/ssb-private: scuttlebot plugin for indexed private messages
About read
it says:
"read(opts)
(sync) Returns a stream of private messages. Takes query options similar to ssb-query."
ssb-query links to: GitHub - ssbc/ssb-query
That link is oddly misleading, at least unless you dig a layer deeper.
GitHub - ssbc/ssb-query This gives a hint, and follow another link down âsee createLogStreamâ to get useful docs:
GitHub - ssbc/ssb-db: A database of unforgeable append-only feeds, optimized for efficient replication for peer to peer protocols
Most useful to me was the bit about live
and old
option keys.
gt, gte, lt, lte ranges are supported, via ltgt if reverse is set to true, results will be from oldest to newest. if limit is provided, the stream will stop after that many items. old
and live
return wether to include old and live (newly written messages) as via pull-live
I didnât want old
, but did want live
so I updated server.private.read({})
to server.private.read({ old: false, live: true })
.
This left me with two questions:
- did server.private.read dump me ALL encrypted messages of ANYONES? (still encrypted of course)
- did server.private.read decrypt the messages that were for me?
Through testing, and asking (thanks @cel ) I discovered 1. no, and 2. yes
This would be helpful to put in the docs, explaining this. I did a check in the source code and this point and got mystified by a flume query. ssb-private/index.js at master ¡ ssbc/ssb-private ¡ GitHub
Once again, writing a message was easier: ssb-private docs + Publish encrypted messages ¡ GitBook were enough easily.
From what I know, you canât publish encrypted messages from the CLI, so itâs confusing for this to be under âCommand Line Clientâ section in the scuttlebutt.nz guides. Maybe it could come out.
To create a function that takes a string/message and a recipient, the ssb code would be:
function send(stringMessage, id) {
server.private.publish(
{
type: 'post',
text: stringMessage,
recps: [{ link: id }]
},
[id], // those to encrypt for
(err, msg) => {}
)
}
This would mean not including encrypting it for âyourselfâ as the bot. To do that, you would just add the hash/address/id of the server/bot itself to the [] type elements, in the same format as the recipient. I just didnât/donât want to do that.
There we go thatâs my write up this took me longer than I wanted already Iâm done goodnight
So a stripped down version of what I came up with isâŚ
const pull = require('pull-stream')
const Server = require('ssb-server')
const Config = require('ssb-config/inject')
// ssb-test matches the name of ~/.ssb-test folder where the rest of the `config` is
const ssbconfig = Config('ssb-test')
Server
.use(require('ssb-gossip'))
.use(require('ssb-replicate'))
.use(require('ssb-private'))
const server = Server(ssbconfig)
pull(
server.private.read({ old: false, live: true }), // don't bother with old messages, just stream new ones
pull.drain((msg) => {
// already decrypted :)
// this is the part where I receive incoming messages
console.log(`receiving a message from ${msg.value.author}`)
console.log(`message content: ${msg.value.content.text}`)
})
)
// this is a function where I could send private messages
function send(stringMessage, id) {
server.private.publish({
type: 'post',
text: stringMessage,
recps: [{ link: id }]
},
[id], // those to encrypt for
(err, msg) => {})
}
package.json dependencies
"pull-stream": "^3.6.14",
"ssb-config": "^3.4.2",
"ssb-gossip": "^1.1.1",
"ssb-private": "^0.2.3",
"ssb-replicate": "^1.3.0",
"ssb-server": "^15.1.2"
Followups:
I think it will still be useful to be able to customize which folder it points to for the config, and storing of files, which can be tweaked with ssb-config.
Fuller code, in context: