Simple Micro Blog Tutorial

WIP

This article is currently a work in progress and subject to frequent change.
See changelog for details.

Time & Level

Time: ~3 hours | Level: Beginner

Welcome to the Simple Micro blog tutorial in the Core Concepts tutorial series. The aim of this tutorial is to show how entries can be linked to each other in a Holochain app.
A link is simply a relationship between two entries. It's a useful way to find some data from something you already know. For example, you could link from your user's agent ID entry to their blog posts.

You will be building on the previous Hello World tutorial and making a super simple blog app. The app's users will be able to post a blog post and then retrieve other users' posts.

What will you learn

In this tutorial you will learn how to attach entries to an agents address using links and then retrieve a list of those entries back from another agent.

Why it matters

Links are vital to locating entries. The only way to find an entry is to know it's hash, however most of the time an agent will not know the hash of all the entries it needs. To allow agents to find entries you can create a link between something the agent does know and the unknown entry.

Add a Post

Store your posts as a Post struct that holds a message of type String, a timestamp of type u64, and an author ID of type Address.

Note the timestamp is important because otherwise two posts with the same author and message will be treated as the same data.

Go ahead and change the Person struct into a Post struct:

#[derive(Serialize, Deserialize, Debug, DefaultJson, Clone)]
pub struct Post {
    message: String,
    timestamp: u64,
    author_id: Address,
}

Add the post entry

The post's entry definition starts off very similar to the person so you can modify it. Update the person entry type definition to post:

    #[entry_def]
    fn post_entry_def() -> ValidatingEntryType {
        entry!(
            name: "post",
            description: "A blog post",
            sharing: Sharing::Public,
            validation_package: || {
                hdk::ValidationPackageDefinition::Entry
            },
            validation: | validation_data: hdk::EntryValidationData<Post>| {
Up until this point all the validation has done is check that the data is in the correct shape but a real application will usually need to validate its data a little more than that.

One thing you might like to do is make sure the blog posts cannot be longer then some maximum length.

Use a match statement to check the entry when it's created:

                match validation_data {
                    hdk::EntryValidationData::Create{ entry, .. } => {
Set a MAX_LENGTH for a posts characters:
                        const MAX_LENGTH: usize = 140;
Simply check if the message is less than or equal to the maximum or return an error:
                        if entry.message.len() <= MAX_LENGTH {
                           Ok(())
                        } else {
                           Err("Post too long".into())
                        }
                    },
                    _ => Ok(()),
                }
            },

Can you think of a way a user could still have an entry with more than the maximum length? Hint: What if they Modify?

The user needs some way of finding which posts belong to an agent.
In Holochain we use links to associate data to something known.
The following creates a link from the agents address to a post.
Every agent has a unique address and you will see how to find it later.

Add the link from the %agent_id:

            links: [
                from!(
                   "%agent_id",
Later you will use the link's type to find all the links on this anchor.

Set it to author_post:

                   link_type: "author_post",
The validation_package and validation are similar to the entry except there is no type checking.

Allow this link to be committed without checks:

                   validation_package: || {
                       hdk::ValidationPackageDefinition::Entry
                   },
                   validation: |_validation_data: hdk::LinkValidationData| {
                       Ok(())
                   }
                )
            ]
        )
    }

Create a post

The entry definition tells Holochain what the data looks like and how to validate it. The next piece is a public function that the UI can use to create a new post. Take a moment to think about the ingredients that go into the Post structure: a message, a timestamp, and the author's ID.

The message will come from the UI, that's easy.
For simplicity the timestamp will come from the UI as well.

Question?

If the user changes their machine's system clock back two days will this app be able tell that the post was made with a fake time?

Answer

Actually it cannot. In fact different machines will have different times anyway. User submitted timestamps are not a very reliable source of truth in a decentralized app. Start thinking about if you need reliable time in your future hApp designs. There are other solutions but for now it's valuable just to be aware of that time needs careful planning.

The author's ID comes from a special constant hdk::AGENT_ADDRESS, which you can access from your zome functions.

Why do I have to specify a timestamp and author? Aren't they already in the entry's header?

If two agents publish entries with identical type and content, they'll have the same address on the DHT. That means that, for all purposes, there's only one entry with two authors. This is fine for some cases. But it causes problems in a microblog. When one author wants to delete an existing message, does the other author's copy get deleted too? Adding a timestamp and author ID makes the two posts distinct and gives them their own addresses.

Tip

You can remove the create_person function as it's no longer needed.

Add a public create_post function that takes a message as a String and a timestamp as a u64:

#[zome_fn("hc_public")]
pub fn create_post(message: String, timestamp: u64) -> ZomeApiResult<Address> {

Create the Post using the message, timestamp, and author's address:

    let post = Post {
        message,
        timestamp,
        author_id: hdk::AGENT_ADDRESS.clone(),
    };

Get this agents address:

    let agent_address = hdk::AGENT_ADDRESS.clone().into();

Commit the post entry:

    let entry = Entry::App("post".into(), post.into());
    let address = hdk::commit_entry(&entry)?;
Before you defined the link from the agent's address to the post.

This is where you actually make the link:

    hdk::link_entries(&agent_address, &address, "author_post", "")?;

    Ok(address)
}

Retrieve all of a user's posts

So how do your users find all these posts?
The user will submit an agents address through the UI and then a list of posts will be displayed. Later you will see how they will get hold of an agents address.

Add a public function that takes an author's agent address and returns a vector of posts:

#[zome_fn("hc_public")]
fn retrieve_posts(agent_address: Address) -> ZomeApiResult<Vec<Post>> {

Retrieve all the author_post links from the agent's address that is passed in. This function should return a vector of Post structs. Luckily you can use the convenient get_links_and_load_type function to do all this.

Return a list of links with exactly the type of author_post (instead of a fuzzy regex search) and any tag.

    hdk::utils::get_links_and_load_type(
        &agent_address,
        LinkMatch::Exactly("author_post"),
        LinkMatch::Any,
    )
}

Note that because you've already told Rust that this function is going to return a vector of posts, the compiler will tell get_links_and_load_type what type to use in the conversion.

We're using a new directive, link::LinkMatch. You'll need to add it to your use statements at the top of the file:

use hdk::holochain_core_types::{
    entry::Entry,
    dna::entry_types::Sharing,
    link::LinkMatch,
};

Get the agent's ID

The users need a way to share their agent id with others. For the sake of simplicity this app will rely on user sending their address to others outside of Holochain (although we will cover messaging in a future tutorial).

The user still needs a way to get their own address, so they can give it to their friends.

Add a public function that returns their AGENT_ADDRESS:

#[zome_fn("hc_public")]
fn get_agent_id() -> ZomeApiResult<Address> {
    Ok(hdk::AGENT_ADDRESS.clone())
}

Show the agent's ID in the UI

Now use that function to display their address at the top of the UI.

Go to your gui folder and open up the index.html file.

The id should update when the page loads and when the websocket port that links to the conductor is changed.

Add an onload event to the body that will call the get_agent_id javascript function when the page loads:

  <body onload="get_agent_id()">
Add an element to render the agents id:
    <div id="agent_id"></div>

Open up the hello.js file and add the get_agent_id function.
Call the get_agent_id zome function that updates the agent_id element with the agent's address:

function get_agent_id() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'get_agent_id')({}).then(result =>
      show_output(result, 'agent_id'),
    );
  });
}

Update the create and retrieve elements for posts

Update the html for posts instead of persons:

Update the headings and function calls:

    <h3>Create Post</h3>
    <textarea id="post" placeholder="Enter a message :)"></textarea>
    <button onclick="create_post()" type="button">Submit Post</button>
    <div>Address: <span id="address_output"></span></div>
    <h3>Retrieve Post</h3>
    <input type="text" id="address_in" placeholder="Enter the entry address" />
    <button onclick="retrieve_posts()" type="button">Show Posts</button>
Add an empty list to display the posts:
    <ul id="posts_output"></ul>

Check your code
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>Hello GUI</title>
    <meta name="description" content="GUI for a Holochain app" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/dark.min.css"
    />
  </head>

  <body onload="get_agent_id()">
    <div id="agent_id"></div>
    <button onclick="hello()" type="button">Say Hello</button>
    <div>Response: <span id="output"></span></div>
    <h3>Create Post</h3>
    <textarea id="post" placeholder="Enter a message :)"></textarea>
    <button onclick="create_post()" type="button">Submit Post</button>
    <div>Address: <span id="address_output"></span></div>
    <h3>Retrieve Post</h3>
    <input type="text" id="address_in" placeholder="Enter the entry address" />
    <button onclick="retrieve_posts()" type="button">Show Posts</button>
    <ul id="posts_output"></ul>
    <input type="text" id="port" placeholder="Set websocket port" />
    <button onclick="update_port()" type="button">update port</button>
    <script
      type="text/javascript"
      src="hc-web-client/hc-web-client-0.5.1.browser.min.js"
    ></script>
    <script type="text/javascript" src="hello.js"></script>
  </body>
</html>

Call create_post from JavaScript

Update create_person to create posts instead:

- function create_person() {
+ function create_post() {
-   const name = document.getElementById('name').value;
+   const message = document.getElementById('post').value;
Use the Date object to give the current timestamp:
+   const timestamp = Date.now();
   holochain_connection.then(({callZome, close}) => {
-     callZome('test-instance', 'hello', 'create_person')({
+     callZome('test-instance', 'hello', 'create_post')({
-       person: {name: name},
+       message: message,
+       timestamp: timestamp,
-     }).then(result => show_output(result, id));
+     }).then(result => show_output(result, 'address_output'));
   });
 }

Update the posts list dynamically

Because the number of posts changes at runtime, you can update the empty list element from earlier to display them.

Empty the list element:

function display_posts(result) {
  var list = document.getElementById('posts_output');
  list.innerHTML = "";

Parse the posts JSON data and order them by time:

  var output = JSON.parse(result);
  var posts = output.Ok.sort((a, b) => a.timestamp - b.timestamp);

For each post add a <li> element that contains the post's message:

  for (post of posts) {
    var node = document.createElement("LI");
    var textnode = document.createTextNode(post.message);
    node.appendChild(textnode);
    list.appendChild(node);
  }
}

Update the agent id when the port is changed

When the websocket port changes, the UI will be talking to a different conductor with a different agent id.

Update the agent's id when this happens:

function update_port() {
  const port = document.getElementById('port').value;
  holochain_connection = holochainclient.connect({
    url: 'ws://localhost:' + port,
  });
+  get_agent_id();
}

Retrieve an agent's posts

To retrieve the posts, update the retrieve_person function, and call display_posts:

- function retrieve_person() {
+ function retrieve_posts() {
  var address = document.getElementById('address_in').value;
  holochain_connection.then(({callZome, close}) => {
-    callZome('test-instance', 'hello', 'retrieve_person')({
+    callZome('test-instance', 'hello', 'retrieve_posts')({
      agent_address: address,
-    }).then(result => show_person(result, 'person_output'));
+    }).then(result => display_posts(result));
  });
}

Check your code
// Connect
var holochain_connection = holochainclient.connect({
  url: 'ws://localhost:3401',
});

// Render functions
function show_output(result, id) {
  var el = document.getElementById(id);
  var output = JSON.parse(result);
  if (output.Ok) {
    el.textContent = ' ' + output.Ok;
  } else {
    alert(output.Err.Internal);
  }
}

function show_person(result) {
  var person = document.getElementById('person_output');
  var output = JSON.parse(result);
  person.textContent = ' ' + output.Ok.name;
}

// Zome calls

function hello() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'hello_holo')({args: {}}).then(result =>
      show_output(result, 'output'),
    );
  });
}
function get_agent_id() {
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'get_agent_id')({}).then(result =>
      show_output(result, 'agent_id'),
    );
  });
}
function create_post() {
  const message = document.getElementById('post').value;
  const timestamp = Date.now();
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'create_post')({
      message: message,
      timestamp: timestamp,
    }).then(result => show_output(result, 'address_output'));
  });
}
function display_posts(result) {
  var list = document.getElementById('posts_output');
  list.innerHTML = "";
  var output = JSON.parse(result);
  var posts = output.Ok.sort((a, b) => a.timestamp - b.timestamp);
  for (post of posts) {
    var node = document.createElement("LI");
    var textnode = document.createTextNode(post.message);
    node.appendChild(textnode);
    list.appendChild(node);
  }
}
function update_port() {
  const port = document.getElementById('port').value;
  holochain_connection = holochainclient.connect({
    url: 'ws://localhost:' + port,
  });
  get_agent_id();
}
function retrieve_posts() {
  var address = document.getElementById('address_in').value;
  holochain_connection.then(({callZome, close}) => {
    callZome('test-instance', 'hello', 'retrieve_posts')({
      agent_address: address,
    }).then(result => display_posts(result));
  });
}

Submit a few post and list them

Run the app and two UIs

This is the same setup as the previous tutorial.

Terminal one

Only for local:

Only do this if you are running a local copy of sim2h server.
Otherwise skip this step.

Run the sim2h server

Run in nix-shell https://holochain.love

sim2h_server -p 9001

Terminal two

Package the dna and then update the hash:

Run in nix-shell https://holochain.love

hc package

Copy the DNA's hash:

DNA hash: QmadwZXwcUccmjZGK5pkTzeSLB88NPBKajg3ZZkyE2hKkG

Your hash will be different.

If you're feeling lazy I have provided a sed command to update the config file:

Run in nix-shell https://holochain.love

sed -i "s/hash = '.*/hash = 'QmadwZXwcUccmjZGK5pkTzeSLB88NPBKajg3ZZkyE2hKkG'/g" conductor-config-alice.toml

Run in nix-shell https://holochain.love

holochain -c conductor-config-alice.toml

Terminal three

No need to compile again but you will need to update the hash in Bob's config file:

Run in nix-shell https://holochain.love

sed -i "s/hash = '.*/hash = 'QmadwZXwcUccmjZGK5pkTzeSLB88NPBKajg3ZZkyE2hKkG'/g" conductor-config-bob.toml

Run in nix-shell https://holochain.love

holochain -c conductor-config-bob.toml

Start the second conductor:

Run in nix-shell https://holochain.love

holochain -c conductor-config-bob.toml

Terminal four

Go to the root folder of your GUI:

Run the first UI on port 8001:

Run in nix-shell https://holochain.love

python -m SimpleHTTPServer 8001

Terminal five

Still in the root folder of your GUI:

Run the second UI on port 8002:

Run in nix-shell https://holochain.love

python -m SimpleHTTPServer 8002

Open up the browser

Open two tabs.

Tab Alice

Go to 0.0.0.0:8001.

Tab Bob

Go to 0.0.0.0:8002.
Enter 3402 into the port text box and click update port.

Update the port to 3401

Tab Alice

Create a few posts:

Create posts

Try retrieving them using Alice's agent id:

Retrieve Posts

Be careful of spaces before or after the address.

Tab Bob

Copy Alice's agent id and try retrieving her posts from Bob's conductor:

Retrieve Posts

Bug

There is currently a bug in the links implementation that is preventing this last operation from working.
This is the nature of alpha software. We are working to solve this asap. See this issue for more details.

Congratulations you have created a simple blog hApp running on a decentralized network 😃

Key takeaways

  • Entries are only located via their hash.
  • Two identical entires will have the same hash and be treated as the same entry.
  • Links create a connection between something you know and something you don't.

Was this helpful?