Minimalist Rust Chat

In this document, we will create a minimalistic Chat application that will allow you to discuss with yourself. While not super useful, it introduces the different concept used by Discret

Nous allons créer un application de chat vraiment minimaliste qui ne permettra de discuter qu'avec vous même. Bien que peut utile, cela permet d'introduire les différents mécanismes utilisés par Discret.

Setup

Let 's start by creating a new Rust project named rust_simple_chat

cargo new rust_simple_chat
cd rust_simple_chat

Open the Cargo.toml file to add the required dependencies:

[dependencies]
    discret = "0.6.0"
    tokio = { version = "1.38.0", features = ["full"] }
    serde = { version = "1.0.203", features = ["derive"] }
    serde_json = "1.0.117"

Discret Initialisation

Discret requires some parameters:

Replace src/main.rs default file by this one:

1use std::{io, path::PathBuf};
2
3use discret::{
4 derive_pass_phrase, zero_uid, Configuration,
5 Discret, Parameters, ParametersAdd, ResultParser,
6};
7use serde::Deserialize;
8
9//application unique identifier
10const APPLICATION_KEY: &str = "github.com/discretlib/rust_example_simple_chat";
11
12#[tokio::main]
13async fn main() {
14 //the data model
15 let model = "chat {
16 Message{
17 content:String
18 }
19 }";
20
21 let path: PathBuf = "test_data".into(); //where data is stored
22
23 //the user master secret
24 let key_material: [u8; 32] = derive_pass_phrase("login", "password");
25
26 //starts Discret
27 let app: Discret = Discret::new(
28 model,
29 APPLICATION_KEY,
30 &key_material,
31 path,
32 Configuration::default(),
33 ).await.unwrap();
34}

Line 10 defines the application unique identifier:

9//...
10const APPLICATION_KEY: &str = "github.com/discretlib/rust_example_simple_chat";
11//...

Lines 15 to 19 define the data model used by this application. It creates a namespace chat that contains an Entity names Message, which contains an unique field named content of the String type.

You can learn more about data models in the documentation Schema and Entities.

14//...
15 let model = "chat {
16 Message{
17 content:String
18 }
19 }";
20//...

Line 24 creates the user master secret. It will be used by Discret to create:

The derive_pass_phrase uses the Argon2id key derivation function with the parameters recommended by owasp.org

23//...
24let key_material: [u8; 32] = derive_pass_phrase("login", "password");
25//...

Insert Messages

Once Discret initialized, we can insert new data. This example will read the messages from the console and write them into the Discret database.

Insert the following lines after line 33 ).await.unwrap();

34 let private_room: String = app.private_room();
35 let stdin = io::stdin();
36 let mut line = String::new();
37 println!("{}", "Write Something!");
38 loop {
39 stdin.read_line(&mut line).unwrap();
40 if line.starts_with("/q") {
41 break;
42 }
43 line.pop();
44 let mut params = Parameters::new();
45 params.add("message", line.clone()).unwrap();
46 params.add("room_id", private_room.clone()).unwrap();
47 app.mutate(
48 "mutate {
49 chat.Message {
50 room_id:$room_id
51 content: $message
52 }
53 }",
54 Some(params),
55 ).await.unwrap();
56
57 line.clear();
58 }

Line 34 retrieves the user's private Room. Rooms are the access rights system used to synchronized data with other peers. Data inserted in a Room is synchronized only with peers that have access rights to it.

In this example we will only use the user's private Room, meaning that only him will able to access the data. If this user's is connected on several devices, data will be synchronized between devices.

You can learn more about access right in the Room section of the documentation.


Lines 47 to 57 defines the query used to insert messages:

46 //...
47 app.mutate(
48 "mutate {
49 chat.Message {
50 room_id: $room_id
51 content: $message
52 }
53 }",
54 Some(params),
55 ).await.unwrap();
56 //...

This is a mutation query to insert a new tuple of the chat.Message entity defined in the data model.

You can notice that the room_id has not been manually defined in the data model. It is a system field available for every entities. $room_id and $message are query parameters that are passed by the Some(params) object.

You can lean more about data insertion and modification in the Mutations and Deletions section of the documentation.

If you run the application, you should be able to write messages.

cargo run

Read synchronized data

At this point, inserted data will be synchronized between each instance of the application.

To retrieve the synchronized messages, we will listen to the events sent by Discret and read messages from the database when we receive an event saying that new data is available.

Insert the following code between ligne 33 ).await.unwrap(); and line 34: let private_room: String = app.private_room();

33//this struct is used to parse the query result
34#[derive(Deserialize)]
35struct Chat {
36 pub id: String,
37 pub mdate: i64,
38 pub content: String,
39}
40
41 //listen for events
42let mut events = app.subscribe_for_events().await;
43let event_app: Discret = app.clone();
44tokio::spawn(async move {
45 let mut last_date = 0;
46 let mut last_id = zero_uid();
47
48 let private_room: String = event_app.private_room();
49 while let Ok(event) = events.recv().await {
50 match event {
51 //triggered when data is inserted or modified
52 discret::Event::DataChanged(_) => {
53 let mut param = Parameters::new();
54 param.add("mdate", last_date).unwrap();
55 param.add("id", last_id.clone()).unwrap();
56 param.add("room_id", private_room.clone()).unwrap();
57
58 //get the latest data in the JSON format
59 let result: String = event_app.query(
60 "query {
61 res: chat.Message(
62 order_by(mdate asc, id asc),
63 after($mdate, $id),
64 room_id = $room_id
65 ) {
66 id
67 mdate
68 content
69 }
70 }",
71 Some(param),
72 ).await.unwrap();
73
74 let mut query_result = ResultParser::new(&result).unwrap();
75 let res: Vec<Chat> = query_result.take_array("res").unwrap();
76 for msg in res {
77 last_date = msg.mdate;
78 last_id = msg.id;
79 println!("Vous: {}", msg.content);
80 }
81 }
82 _ => {} //ignores other events
83 }
84 }
85});

Lines 42 to 86 Les lignes 42 à 86 define the loop that will listen for the Discret events. it is launched as an asynchronous task to avoid blocking the application.

When a DataChanged event is triggered, we query the database to retrieve the new data:

58//...
59let result: String = event_app.query(
60 "query {
61 res: chat.Message(
62 order_by(mdate asc, id asc),
63 after($mdate, $id),
64 room_id = $room_id
65 ) {
66 id
67 mdate
68 content
69 }
70 }",
71 Some(param),
72).await.unwrap();
73//...

You can learn more about reading database data in the Query section of the documentation.


Lines 74 and 75 parse the JSON result to transform it into a list of Chat object.

73//...
74let mut query_result = ResultParser::new(&result).unwrap();
75let res: Vec<Chat> = query_result.take_array("res").unwrap();
76//...

Now, if you run this example in different folders or different devices on your local network, you should be able to chat with yourself!

Going Further

This tutorial provided the basis to use Discret, you should now be able to create applications that requires only one user to be synchronized. For example you could create a password manager that synchronize data across your devices.

To learn more about discret you can follow the Flutter tutorials. Flutter chat with multiple users creates a multi user chat and introduces you to Room creations and how to create Invitations to be sent to other peers.

The Learn section will provide deeper explanations on how to use Discret

Full Source Code

use std::{io, path::PathBuf};

use discret::{
  derive_pass_phrase, zero_uid, Configuration, 
  Discret, Parameters, ParametersAdd, ResultParser,
};
use serde::Deserialize;

//application unique identifier
const APPLICATION_KEY: &str = "github.com/discretlib/rust_example_simple_chat";

#[tokio::main]
async fn main() {
  //defines the datamodel
  let model = "chat {
    Message{
      content:String
    }
  }";


  let path: PathBuf = "test_data".into(); //where data is stored

  //used to derives all necessary secrets
  let key_material: [u8; 32] = derive_pass_phrase("login", "password");

  //starts Discret
  let app: Discret = Discret::new(
    model,
    APPLICATION_KEY,
    &key_material,
    path,
    Configuration::default(),
  ).await.unwrap();

  //this struct is used to parse the query result
  #[derive(Deserialize)]
  struct Chat {
    pub id: String,
    pub mdate: i64,
    pub content: String,
  }

  //listen for events
  let mut events = app.subscribe_for_events().await;
  let event_app: Discret = app.clone();
  tokio::spawn(async move {
    let mut last_date = 0;
    let mut last_id = zero_uid();

    
    let private_room: String = event_app.private_room();
    while let Ok(event) = events.recv().await {
      match event {
        //triggered when data is inserted or modified
        discret::Event::DataChanged(_) => {
          let mut param = Parameters::new();
          param.add("mdate", last_date).unwrap();
          param.add("id", last_id.clone()).unwrap();
          param.add("room_id", private_room.clone()).unwrap();

          //get the latest data in the JSON format 
          let result: String = event_app.query(
            "query {
                res: chat.Message(
                    order_by(mdate asc, id asc), 
                    after($mdate, $id),
                    room_id = $room_id
                ) {
                        id
                        mdate
                        content
                }
            }",
            Some(param),
          ).await.unwrap();

          let mut query_result = ResultParser::new(&result).unwrap();
          let res: Vec<Chat> = query_result.take_array("res").unwrap();
          for msg in res {
            last_date = msg.mdate;
            last_id = msg.id;
            println!("you said: {}", msg.content);
          }
        }
        _ => {} //ignores other events
      }
    }
  });

  //Message are inserted in your private room
  let private_room: String = app.private_room();
  let stdin = io::stdin();
  let mut line = String::new();
  println!("{}", "Write Something!");
  loop {
    stdin.read_line(&mut line).unwrap();
    if line.starts_with("/q") {
    break;
    }
    line.pop();
    let mut params = Parameters::new();
    params.add("message", line.clone()).unwrap();
    params.add("room_id", private_room.clone()).unwrap();
    app.mutate(
    "mutate {
        chat.Message {
            room_id:$room_id 
            content: $message 
        }
    }",
    Some(params),
    ).await.unwrap();

    line.clear();
  }
}