Continue writing documentation
This commit is contained in:
parent
b10c627ec2
commit
1058841451
|
@ -6,4 +6,5 @@ doc/
|
|||
pigeon.bundle
|
||||
*.gem
|
||||
bundle/
|
||||
pigeon.db
|
||||
pigeon.db
|
||||
.vscode
|
|
@ -87,7 +87,7 @@ module Pigeon
|
|||
|
||||
def new_draft(kind:, body: {})
|
||||
old = _get_config(CURRENT_DRAFT)
|
||||
raise "PUBLISH OR RESET CURRENT DRAFT (#{old.kind}) FIRST" if old
|
||||
raise "PUBLISH OR RESET CURRENT DRAFT (#{old.kind}) FIRST (call db.delete_current_draft)" if old
|
||||
|
||||
_replace_draft(Draft.new(kind: kind, body: body))
|
||||
end
|
||||
|
@ -100,7 +100,7 @@ module Pigeon
|
|||
def get_draft
|
||||
draft = store._get_config(CURRENT_DRAFT)
|
||||
unless draft
|
||||
raise "THERE IS NO DRAFT. CREATE ONE FIRST."
|
||||
raise "THERE IS NO DRAFT. CREATE ONE FIRST. Call db.new_draft(kind, body)"
|
||||
end
|
||||
draft
|
||||
end
|
||||
|
|
254
ruby_tutorial.md
254
ruby_tutorial.md
|
@ -46,52 +46,256 @@ One note about the `pigeon.db` file before moving to the next section: Do not sh
|
|||
|
||||
## Working with Drafts
|
||||
|
||||
```
|
||||
### ##### ### #### #### ##### #### # # ##### #### #####
|
||||
# # # # # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # # #
|
||||
### # # # #### #### #### # # ##### #### #### ####
|
||||
# # # # # # # # # # # # # # #
|
||||
# # # # # # # # # # # # # # # #
|
||||
### # ### # # ##### #### # # ##### # # #####
|
||||
```
|
||||
A `message` is the basic building block of a Pigeon database.
|
||||
A `message` is the basic building block of a Pigeon database. As mentioned in the [protocol spec](https://tildegit.org/PigeonProtocolConsortium/protocol_spec), there are three parts to a message:
|
||||
|
||||
* A header containing a `kind` field (similar to an email subject line) plus some additional meta data used by protocol clients.
|
||||
* A body containing user definable header fields.
|
||||
* A footer containing a Crockford Base32 encoded ED25119 signature to prevent forgery.
|
||||
|
||||
As a convenience, the Pigeon Ruby client allows developers to keep zero or one "draft" messages. A draft message is a log message that has not been signed and has not been committed to the database. Think of it as your own personal scratchpad.
|
||||
|
||||
A draft is not part of the protocol spec. It is a convinience provided to users of this library. You could absolutely write messages by hand, calculate their signatures, convert everything to Base32 and manually add them to the database. This would be extremely tedious, however, so the draft functionality was added for convenience.
|
||||
|
||||
Let's see if we have a draft to work with:
|
||||
|
||||
```ruby
|
||||
db.get_draft
|
||||
# => #<Pigeon::Draft:0x000056160b2e64a0 @author="NONE", @body={"a"=>"\"bar\"", "b"=>"&CH...QG.sha256"}, @depth=-1, @kind="unit_test", @lipmaa="NONE", @prev="NONE", @signature="NONE">
|
||||
```
|
||||
db.new_draft
|
||||
|
||||
It appears that my database has a draft. I don't actually remember what this draft was, so I will just delete it before proceeding.
|
||||
|
||||
```ruby
|
||||
db.delete_current_draft
|
||||
db.update_draft
|
||||
# => nil
|
||||
```
|
||||
|
||||
Now I can create a new draft. I am going to create a new `garden_diary` for a fictitious gardening app. In my gardening app, I expect every `garden_diary` message to have a `message_text` entry in its body. We can add that now, also.
|
||||
|
||||
```ruby
|
||||
db.new_draft(kind: "garden_diary", body: {"message_text" => "Tomato plant looking healthy."})
|
||||
# => #<Pigeon::Draft:0x000056160b63da68 @author="NONE", @body={"message_text"=>"\"Tomato plant looking healthy.\""}, @depth=-1, @kind="garden_diary", @lipmaa="NONE", @prev="NONE", @signature="NONE">
|
||||
```
|
||||
|
||||
A couple notes here:
|
||||
|
||||
* `"garden_diary` is the message `kind`. This is definable by application developers and helps determine the type of message we are dealing with. A fictitious diary app might have other entries such as `"status_update"` or `"photo_entry"`. It really just depends on the application you are building.
|
||||
* Notice that my hash used string keys for the `"message_text"` body entry. You can only use strings for key / value pairs (no symbols or numbers). Later on we will learn how to attach files to messages.
|
||||
* The `body:` part is optional. I could have called `db.new_draft(kind: "garden_diary")` and added key / value pairs to the body later.
|
||||
|
||||
Oops! Speaking of adding entries to a draft's body, it looks like I forgot something. In my fictitious gardening app, a `garden_diary` entry doesn't just have a `"message_text"`, it also has a `"current_mood"` entry. Luckily, it is easy to add keys to unpublished drafts. Let's add the key now:
|
||||
|
||||
```ruby
|
||||
db.update_draft("current_mood", "Feeling great")
|
||||
# => "\"Feeling great\""
|
||||
```
|
||||
|
||||
OK, I think our draft message is looking better. Let's take a look:
|
||||
|
||||
```ruby
|
||||
db.get_draft
|
||||
# => => #<Pigeon::Draft:0x000056160b3e6be8 @author="NONE", @body={"message_text"=>"\"Tomato plant looking healthy.\"", "current_mood"=>"\"Feeling great\""}, @depth=-1, @kind="garden_diary", @lipmaa="NONE", @prev="NONE", @signature="NONE">
|
||||
```
|
||||
|
||||
I can see the status of my current draft message using `db.get_draft`. It's not very human readable though. To get a more human readable version, I can use the `render_as_draft` method on a `Draft` object:
|
||||
|
||||
```ruby
|
||||
human_readable_string = db.get_draft.render_as_draft
|
||||
# => "author DRAFT\nkind garden_dia...."
|
||||
puts human_readable_string
|
||||
# => author DRAFT
|
||||
# kind garden_diary
|
||||
# prev DRAFT
|
||||
# depth DRAFT
|
||||
# lipmaa DRAFT
|
||||
#
|
||||
# message_text:"Tomato plant looking healthy."
|
||||
# current_mood:"Feeling great"
|
||||
```
|
||||
|
||||
Some interesting things about the message we just rendered:
|
||||
|
||||
* Unlike a message, a draft has no signature (yet).
|
||||
* The `author`, `kind`, `prev`, `depth`, `lipmaa` properties are all set to `"DRAFT"`. Real values will be populated when we finally publish the draft.
|
||||
|
||||
## Turning Drafts Into Messages
|
||||
|
||||
```
|
||||
db.get_draft
|
||||
db.publish_draft
|
||||
```
|
||||
## Working With Messages
|
||||
Now that I am happy with my draft, I can publish it. Once published, my message cannot be modified, so it is very important to visually inspect a draft with `db.get_draft` before proceeding.
|
||||
Since we did that in the last step, I will go ahead and publish the message:
|
||||
|
||||
```
|
||||
db.add_message
|
||||
my_message = db.publish_draft
|
||||
# => #<Pigeon::Message:0x000056160b50dd00
|
||||
# @author=#<Pigeon::RemoteIdentity:0x000056160b50dd78 @multihash="@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519">,
|
||||
# @body={"message_text"=>"\"Tomato plant looking healthy.\"", "current_mood"=>"\"Feeling great\""},
|
||||
# @depth=0,
|
||||
# @kind="garden_diary",
|
||||
# @lipmaa=0,
|
||||
# @prev="NONE",
|
||||
# @signature="2ZHC8TX3P2SQVQTMFYXTAT4S02RN43JNZNECRJDA7QMSJNE5G7NV7GTRK3PGFHFY9MBE1Q95BCKBSJH4V0PTX6945A34Z1CARTGH230.sig.ed25519">
|
||||
```
|
||||
|
||||
Let's look at our new message in a human-readable way:
|
||||
|
||||
```ruby
|
||||
puts my_message.render
|
||||
# => author @753...T6G.ed25519
|
||||
# kind garden_diary
|
||||
# prev NONE
|
||||
# depth 0
|
||||
# lipmaa 0
|
||||
#
|
||||
# message_text:"Tomato plant looking healthy."
|
||||
# current_mood:"Feeling great"
|
||||
#
|
||||
# signature 2ZH...230.sig.ed25519
|
||||
```
|
||||
|
||||
We see that unlike our draft, the message has a signature. The header fields are also populated.
|
||||
|
||||
In the next section, we will learn more about messages.
|
||||
|
||||
## Working With Messages
|
||||
|
||||
Drafts can be helpful when you are building a message incrementally and need a place to temporarily store things between application restarts.
|
||||
What about when you have all the information you need and want to publish immediately?
|
||||
|
||||
In those cases, you can call `db.add_message` and your message will be published to your database immediately. No intermediate steps:
|
||||
|
||||
```ruby
|
||||
message = db.add_message("garden_entry", {"message_text" => "The basil is just OK", "current_mood" => "content"})
|
||||
# => #<Pigeon::Message:0x000056160b5cb558
|
||||
# @author=#<Pigeon::RemoteIdentity:0x000056160b5cb5a8 @multihash="@753...T6G.ed25519">,
|
||||
# @body={"message_text"=>"\"The basil is just OK\"", "current_mood"=>"\"content\""},
|
||||
# @depth=1,
|
||||
# @kind="garden_entry",
|
||||
# @lipmaa=0,
|
||||
# @prev="%EM7...260.sha256",
|
||||
# @signature="J59...238.sig.ed25519">
|
||||
|
||||
puts message.render
|
||||
# => author @753...T6G.ed25519
|
||||
# kind garden_entry
|
||||
# prev %EM7...260.sha256
|
||||
# depth 1
|
||||
# lipmaa 0
|
||||
#
|
||||
# message_text:"The basil is just OK"
|
||||
# current_mood:"content"
|
||||
#
|
||||
# signature J59...238.sig.ed25519
|
||||
```
|
||||
|
||||
We should now have 2 messages in the local database.
|
||||
Let's take a look using the `db.all_messages` method:
|
||||
|
||||
```ruby
|
||||
db.all_messages
|
||||
db.read_message
|
||||
db.have_message?
|
||||
# => ["%EM749647YHD3CBEC19TJJ7YME7BDXJ2KZ38ZZKS6E3VA0JHAM260.sha256", "%0HTM1H6ETBMKCPP5JMN2XEM060RYQHJ8P5KY09WRPTTVZ20N3EFG.sha256"]
|
||||
```
|
||||
|
||||
The `#all_messages` method returns an array containing every message multihash in the database. We can then pass the multihash to the `db.read_message` method to retrieve the corresponding `Pigeon::Message` object.
|
||||
|
||||
Let's look at the old log message we created from a draft previously:
|
||||
|
||||
```ruby
|
||||
old_message = db.read_message("%EM749647YHD3CBEC19TJJ7YME7BDXJ2KZ38ZZKS6E3VA0JHAM260.sha256")
|
||||
# => #<Pigeon::Message:0x000056160b35f580
|
||||
# @author=#<Pigeon::RemoteIdentity:0x000056160b35ee50 @multihash="@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519">,
|
||||
# @body={"message_text"=>"\"Tomato plant looking healthy.\"", "current_mood"=>"\"Feeling great\""},
|
||||
# @depth=0,
|
||||
# @kind="garden_diary",
|
||||
# @lipmaa=0,
|
||||
# @prev="NONE",
|
||||
# @signature="2ZHC8TX3P2SQVQTMFYXTAT4S02RN43JNZNECRJDA7QMSJNE5G7NV7GTRK3PGFHFY9MBE1Q95BCKBSJH4V0PTX6945A34Z1CARTGH230.sig.ed25519">
|
||||
|
||||
puts old_message.render
|
||||
# author @753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519
|
||||
# kind garden_diary
|
||||
# prev NONE
|
||||
# depth 0
|
||||
# lipmaa 0
|
||||
#
|
||||
# message_text:"Tomato plant looking healthy."
|
||||
# current_mood:"Feeling great"
|
||||
#
|
||||
# signature 2ZHC8TX3P2SQVQTMFYXTAT4S02RN43JNZNECRJDA7QMSJNE5G7NV7GTRK3PGFHFY9MBE1Q95BCKBSJH4V0PTX6945A34Z1CARTGH230.sig.ed25519
|
||||
```
|
||||
|
||||
Additionally, there is a `have_message?` helper that let's us know if we have a message in the local DB. It will return a `Pigeon::Message` (if found) or `false`:
|
||||
|
||||
```
|
||||
db.have_message?("%AAAM1H6ETBBBCPP5JMN2XEM060RYQCCCP5KY09WRPTTVZ20N3FFF.sha256")
|
||||
# => false
|
||||
```
|
||||
|
||||
## Working with Peers
|
||||
|
||||
Building a gardening diary is not very fun unless there is a way of sharing your work. Pigeon supports data transfer through the use of peers.
|
||||
|
||||
Every Pigeon database (including ours) has a unique identifier to identify itself.
|
||||
|
||||
Let's call `db.who_am_i` to find out what our database multihash is:
|
||||
|
||||
```ruby
|
||||
me = db.who_am_i
|
||||
# => #<Pigeon::LocalIdentity:0x000056160b5ca658
|
||||
# @multihash="@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519",
|
||||
# @seed="REDACTED",
|
||||
# @signing_key=#<Ed25519::SigningKey:REDACTED>>
|
||||
```
|
||||
db.who_am_i
|
||||
db.add_peer
|
||||
|
||||
Calling `db.who_am_i` returned a `Pigeon::LocalIdentity`. To get results in a more copy/pastable format, call `#multihash` on the `LocalIdentity`:
|
||||
|
||||
```ruby
|
||||
me.multihash
|
||||
# => "@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"
|
||||
```
|
||||
|
||||
You can send this string to all your friends so they can add you as a peer to their respective databases.
|
||||
Let's add a friend to our database now.
|
||||
My friend has informed me her Pigeon identity is `"@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"`:
|
||||
|
||||
```ruby
|
||||
db.add_peer("@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519")
|
||||
# => "@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"
|
||||
```
|
||||
|
||||
My client will now keep a local copy of my friend's DB on disk at all times. Since Pigeon is an offline-only protocol, she will need to mail me an SD Card with her files. We will cover this later in the "Bundles" section.
|
||||
|
||||
If you ever lose track of who your peers are, you can call `db.all_peers` to get a list:
|
||||
|
||||
```ruby
|
||||
db.all_peers
|
||||
db.remove_peer
|
||||
db.block_peer
|
||||
# => ["@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"]
|
||||
```
|
||||
|
||||
You can also remove peers if you no longer need to replicate their messages:
|
||||
|
||||
```ruby
|
||||
db.remove_peer("@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519")
|
||||
# => "@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"
|
||||
db.all_peers
|
||||
# => []
|
||||
```
|
||||
|
||||
It is also possible to block peers as needed via `db.block_peer`. `block_peer` is _not_ the same as `remove_peer`. Blocking a peer will prevent gossip from flowing through your database. All of their messages will be ignored and none of your peers will be able to retrieve their messages through you via gossip:
|
||||
|
||||
```ruby
|
||||
db.block_peer("@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519")
|
||||
# => "@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"
|
||||
|
||||
db.all_blocks
|
||||
db.peer_blocked?
|
||||
# => ["@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519"]
|
||||
|
||||
db.peer_blocked?("@753FT97S1FD3SRYPTVPQQ64F7HCEAZMWVBKG0C2MYMS5MJ3SBT6G.ed25519")
|
||||
# => true
|
||||
```
|
||||
|
||||
## Querying the Database
|
||||
|
||||
I stopped here. The remainder of the file is incomplete and will be completed later.
|
||||
|
||||
```
|
||||
db.get_message_by_depth
|
||||
db.get_message_count_for
|
||||
|
|
Loading…
Reference in New Issue