Chat Minimaliste

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.

Préparation

Pour commencer, nous allons créer un nouveau projet rust nommé rust_simple_chat:

cargo new rust_simple_chat
cd rust_simple_chat

Éditez le fichier Cargo.toml pour rajouter les dépendances suivantes:

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

Initialiser Discret

Discret nécessite un certain nombre de paramètres pour être lancé:

Remplacer le fichier src/main.rs par défaut par le suivant

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//identifiant unique de l'application
10const APPLICATION_KEY: &str = "github.com/discretlib/rust_example_simple_chat";
11
12#[tokio::main]
13async fn main() {
14 //definition du modèle de données
15 let model = "chat {
16 Message{
17 content:String
18 }
19 }";
20
21 let path: PathBuf = "test_data".into(); //où les donnée sont stockées
22
23 //utilisé pour dériver les tous les secrets nécessaires au fonctionnement de Discret
24 let key_material: [u8; 32] = derive_pass_phrase("login", "password");
25
26 //démarre Discret
27 let app: Discret = Discret::new(
28 model,
29 APPLICATION_KEY,
30 &key_material,
31 path,
32 Configuration::default(),
33 ).await.unwrap();
34}

La ligne 10 définit un nom unique de l'application:

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

Les lignes 15 à 19 définissent le modèle de données qui pourra être utilisé dans l'application. Ce modèle définit un espace de nom nommé chat qui contient l'entité Message, qui elle même contient un unique champ nommé content qui peut contenir une chaine de caractères.

Vous pourrez en apprendre plus dans la section Schémas et Entités de la documentation.

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

La Ligne 24 crée le secret maitre. Ce secret est utilisé par Discret pour créer, en autre:

la fonction derive_pass_phrase utilise la fonction de dérivation Argon2id avec les paramètres recommandés par owasp.org

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

L'application ne fait rien encore, mais Discret est maintenant prêt à être utilisé.

Insérer des messages

Nous allons maintenant pouvoir insérer des données. Le programme va lire les messages écrit dans la console et les insérer en base de données.

Insérer les lignes suivantes apres la ligne 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 }

La ligne 34 récupère la Room privée de l'utilisateur. Le concept de Room définit le système d'autorisations utilisé par Discret pour synchroniser les données avec ses Pairs. Les données insérées dans une Room ne seront synchronisées qu'avec les Pairs ayant accès à cette Room.

Nous allons utiliser la Room Privée de l'utilisateur, donc lui seul sera capable d'y acceder. Si l'utilisateur se connecte sur plusieurs machines, les données seront synchronisées entre les machines.

Vous pourrez en apprendre plus dans la section Room de la documentation.


Les lignes 47 à 57 définissent la requête utilisée pour insérer des 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 //...

Nous utilisons une requête de type mutation pour insérer un tuple de l'entité chat.Message définie dans le modèle de données.

Vous noterez que room_id n'a pas été défini dans le modèle de données. C'est un champ système disponible pour toutes les entités. $room_id et $message sont des paramètres de la requête qui sont passés par l'objet Some(params).

Vous pourrez en apprendre plus sur l'insertion et la modification de données dans la section Mutations et Suppression de la documentation.

Si vous lancez l'application, vous devriez pouvoir écrire des messages

cargo run

Lire les données synchronisées

A ce stade, les données que vous insérez seront synchronisées entre chaque instance de l'application. Si vous copiez le programme dans deux répertoires ou appareils différents, les données seront synchronisées. Par contre vous ne verrez pas les messages des autres.

Pour récupérer les messages synchronisés, nous allons écouter les évènements envoyé par Discret, et lire les messages quand un évènements indiquera que de nouvelles données sont disponibles. Pour ce faire, insérez le code suivant entre la ligne 33 ).await.unwrap(); et la ligne 34: let private_room: String = app.private_room();

33//cette structure permet de désérialiser les messages
34#[derive(Deserialize)]
35struct Chat {
36 pub id: String,
37 pub mdate: i64,
38 pub content: String,
39}
40
41//écoute les évènements créés par Discret
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 //déclenché quand des données sont insérées ou modifées
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 //obtiens les dernières données dans le format JSON
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 _ => {} //on ignore les autres évènements
83 }
84 }
85});

Les lignes 42 à 86 définissent la boucle qui va écouter les événements Discret. Elle est lancée dans une tache asynchrone pour que l'application ait ainsi deux boucles indépendantes:

Lors qu'un événement de type DataChanged est détecté, nous effectuons une requête pour récupérer les données nouvellement reçues:

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//...

Vous pourrez en apprendre plus sur les requêtes de selection de données dans la section Requête de la documentation.


Les lignes 74 et 75 permettent de lire le résultat JSON de la requête et de les transformer en liste d'objects Chat

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

Si vous lancez ce code dans deux repertoires ou sur deux machines différentes en réseau local, vous pourrez voir les messages s'afficher sur les différentes consoles.

Félicitation! vous pouvez désormais discuter avec vous même en communicant en Peer to Peer!

Aller plus loin

Ce tutorial vous a donné un aperçu des bases nécessaires à l'utilisation de Discret. Ces connaissances sont suffisantes pour créer des applications ne nécessitant la synchronisation qu'avec un seul utilisateur.

Par exemple, vous pouvez créer un gestionnaire de mot de passes qui se synchronisera entre vos différent appareils en réseau local.

Pour en apprendre plus, vous pouvez suivre les tutorials Flutter, en particulier le chat multi utilisateurs qui introduit la gestion des invitations et la création de Room.

La section Apprendre vous permettra d'approfondir vos connaissances.

Le code source complet

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

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

//identifiant unique de l'application
const APPLICATION_KEY: &str = "github.com/discretlib/rust_example_simple_chat";

#[tokio::main]
async fn main() {
  //definition du modèle de données
  let model = "chat {
    Message{
      content:String
    }
  }";

  let path: PathBuf = "test_data".into(); //où les donnée sont stockées

  //utilisé pour dériver les tous les secrets nécessaires au fonctionnement de Discret
  let key_material: [u8; 32] = derive_pass_phrase("login", "password");

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

  //cette structure permet de désérialiser les messages
  #[derive(Deserialize)]
  struct Chat {
    pub id: String,
    pub mdate: i64,
    pub content: String,
  }

  //écoute les évènements créés par Discret
  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 {
        //déclenché quand des données sont insérées ou modifées
        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();

          //obtiens les dernières données dans le format JSON 
          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!("Vous: {}", msg.content);
          }
        }
        _ => {} //on ignore les autres évènements
      }
    }
  });

  //les données sont insérées dans votre Room privée
  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();
  }
}