Chat Multi Utilisateurs

Ce document décrit l'exemple avancé de chat nommé example_advanced_chat.dart disponible dans le répertoire ./lib/, et considère que vous avez déjà suivi le tutoriel Chat Flutter Minimaliste. Seules les fonctions introduisant de nouvelles parties de l'API seront décrites.

Pour commencer:

rm ./lib/main.dart
cp ./lib/example_advanced_chat.dart ./lib/main.dart
flutter build linux

Ensuite, allez dans le répertoire indiqué à la fin de la compilation, pour Linux:

cd build/linux/x64/release/bundle/

Si vous copiez le programme sur une autre machine du réseau et que vous re-créez un des comptes, vous devriez pouvoir discuter entre vos deux machines, avec deux comptes différents.

Pour trois utilisateurs, vous devriez voir l'écran:

Préparation de l'API

L'initialisation de l'API ne change pas beaucoup par rapport à l'exemple précédent.

Seul le répertoire où stocker des données est modifié pour être compatible avec les smartphone et tablet.

18//...
19void main() async {
20 WidgetsFlutterBinding.ensureInitialized();
21 final path = await _localPath;
22 String dataPath = '$path/.discret_data';

Le niveau de log est mis au niveau info. ce niveau peut être changé n'importe quand en passant une de ces valeurs:

27 await Discret().setLogLevel("info");

Architecture

Pour gérer les différents groupes de discussion, la classe ChatState contient une liste de ChatRoom ainsi que tous les champs et fonctions pour gérer:

class ChatState extends ChangeNotifier {
  final List<ChatRoom> chatRooms = [];
//..
}

La classe ChatRoom contient tous les champs et fonctions pour:

class ChatRoom {
  final List<ChatEntry> chat = [];
  String roomId;
  String name;
  int mdate;
}

Initialisation

L'initialisation du ChatState est plus complexe que la précédente.

Pendant la création de l'objet, on définit la gestion du flux de logs envoyé par Discret. Ces logs sont principalement utilisés pour renvoyer des erreurs techniques qui ne sont pas particulièrement liées à un appel de l'API.

Le code suivant ne fait que renvoyer ces logs dans le log Flutter de deboggage.

50 final StreamSubscription<LogMsg> loglistener =
51 Discret().logStream().listen((event) async {
52 var message = event.data;
53 var level = event.level;
54 logger.log('$level: $message');
55 });

--

La ligne 68 récupère la verifyingKey de l'utilisateur connecté. Cela représente l'identité publique de l'utilisateur.

Toutes les données insérés sont signées avec la clé de signature associée, et les autres utilisateurs vérifieront cette signature lors de la synchronisation des données.

//...
    msg = await Discret().verifyingKey();
    myVerifyingKey = msg.data;
//..

La gestion du flux d'évènements envoyé par Discret est plus complète.

eventlistener = Discret().eventStream().listen((event) async {
    switch (event.type) {
      case EventType.dataChanged:
        {
          DataModification modif = event.data as DataModification;
          modif.rooms?.forEach((key, value) {
            if (chatMap.containsKey(key)) {
              ChatRoom chatRoom = chatMap[key]!;
              chatRoom.refresh();
            }
          });
          chatRooms.sort((a, b) => b.mdate.compareTo(a.mdate));
          notifyListeners();
        }
      case EventType.roomModified:
        {
          String roomId = event.data as String;
          await loadRoom(roomId);
          notifyListeners();
        }
      case EventType.peerConnected:
        {
          PeerConnection peer = event.data as PeerConnection;
          var key = peer.verifyingKey;
          logger.log('Peer connected: $key');
        }
      case EventType.peerDisconnected:
        {
          PeerConnection peer = event.data as PeerConnection;
          var key = peer.verifyingKey;
          logger.log('Peer disconnected: $key');
        }
      default:
    }
  }, onDone: () {}, onError: (error) {});

L'évènement EventType.dataChanged doit maintenant prendre en compte les différent groupe de discussion et ne rafraîchira que les Rooms qui ont de nouveaux messages.

Un nouvel évènement est traité EventType.roomModified. Cet évènement sera envoyé pendant le traitements des invitations et permet de détecter qu'un nouveau groupe de discussion est disponible.

Les évènements EventType.peerConnected, EventType.peerDisconnected permettent de détecter qu'un Pair vient de se connecter ou de se déconnecter. Ils ne sont pas utilisés dans le code et ne font qu'écrire dans le log.

Créer un Compte

La fonction de login n'a pas changée mais la fonction de création de compte est plus complexe car elle doit désormais gérer le champ Display Name.

Ce champ est utilisé pour associer un nom à votre identité publique (votre verifying_key). Votre identité ainsi que ce nom seront envoyés aux Pairs lors du traitement des invitations.

Cette association se fait dans l'entité sys.Peer qui contient tous les Pairs que vous connaissez, vous y compris.

128 Future<ResultMsg> createAccount(
129 String userName, String password, String displayName) async {
130 ResultMsg msg = await Discret().login(userName, password, true);
131 if (msg.successful) {
132 await initialise();
133 String query = """query{
134 sys.Peer(verifying_key=\$verifyingKey){
135 id
136 }
137 }""";
138
139 ResultMsg res =
140 await Discret().query(query, {"verifyingKey": myVerifyingKey});
141 if (!res.successful) {
142 logger.log(res.error);
143 return res;
144 }
145
146 final json = jsonDecode(res.data) as Map<String, dynamic>;
147 final msgArray = json["sys.Peer"] as List<dynamic>;
148 var result = msgArray[0] as Map<String, dynamic>;
149 var id = result["id"];
150 String mutate = """mutate {
151 sys.Peer{
152 id:\$id
153 name:\$name
154 }
155 }""";
156 res = await Discret().mutate(mutate, {"id": id, "name": displayName});
157 if (!res.successful) {
158 logger.log(res.error);
159 return res;
160 }
161 return res;
162 }
163 return msg;
164 }

La première requête ligne 133 va récupérer l'identifiant id du tuple correspondant à votre identité.

Chaque tuple en base de données possède un identifiant unique généré lors de l'insertion.

String query = """query{
  sys.Peer(verifying_key=\$verifyingKey){
    id
  }
}""";

La seconde requête ligne 150 va mettre à jour le champ name avec la valeur de Display Name.

Toute requête de type mutate qui fournit un id est une requête de mise à jours. Si l'identifiant n'existe pas en base de données, une erreur est retournée.

String mutate = """mutate {
  sys.Peer{
    id:\$id
    name:\$name
  }
}""";

Créer une invitation

Chaque demande d'invitation va créer une nouvelle Room qui va permettre de limiter les droits d'accès au groupe de discussion.

273 Future<String> createInvite() async {
274 String query = """mutate {
275 sys.Room{
276 admin: [{
277 verif_key:\$verifyingKey
278 }]
279 authorisations:[{
280 name:"chat"
281 rights:[{
282 entity:"chat.Message"
283 mutate_self:true
284 mutate_all:false
285 }]
286 users:[{
287 verif_key:\$verifyingKey
288 }]
289 }]
290 }
291 }""";
292
293 ResultMsg res =
294 await Discret().mutate(query, {"verifyingKey": myVerifyingKey});
295
296 if (!res.successful) {
297 logger.log(res.error);
298 return "";
299 }
300
301 final json = jsonDecode(res.data) as Map<String, dynamic>;
302 final msgArray = json["sys.Room"] as Map<String, dynamic>;
303 String roomId = msgArray["id"];
304
305 final auths = msgArray["authorisations"] as List<dynamic>;
306 final authorisation = auths[0] as Map<String, dynamic>;
307 final authId = authorisation["id"];
308
309 ResultMsg invite = await Discret().invite(roomId, authId);
310 if (!res.successful) {
311 logger.log(res.error);
312 return "";
313 }
314 return invite.data;
315 }

La requête ligne 274 va créer une nouvelle Room pour accueillir le nouvel invité.

String query = """mutate {
  sys.Room{
    admin: [{
      verif_key:\$verifyingKey
    }]
    authorisations:[{
      name:"chat"
      rights:[{
        entity:"chat.Message"
        mutate_self:true
        mutate_all:false
      }]
      users:[{
          verif_key:\$verifyingKey
      }]
    }]
  }
}""";

Lors de la création de la Room, l'identité (verifying_key) du pair invité est encore inconnue, vous n'insérez donc que votre propre identité.

Vous pourrez noter que votre identité est utilisée deux fois:

Le champ rights de l'autorisation indique que seul les tuples de type chat.Message sont acceptés. Tenter d'insérer d'autres données renverra une erreur.


L'API de creation d'invitation requiers:

Ces identifiants sont récupérés en lisant le résultat de la requête de création (lignes 293 à 307), qui contient les identifiants générés lors de la création.

Lorsque l'invitation sera acceptée, l'identité du Pair sera rajoutée automatiquement à l'autorisation fournie, vous pourrez commencer à communiquer en insérant des messages dans cette Room.


L'appel de la fonction de l'API va retourner une grande chaîne de caractères qui devra être transmise au pair que vous voulez inviter par des moyens externe à l'application.

 ResultMsg invite = await Discret().invite(roomId, authId);
  //..
 return invite.data;

Accepter une invitation

Accepter une invitation se résume à appeler la fonction de l'API avec l'invitation reçue. L'invitation va permettre aux deux pairs de se retrouver sur le réseau et se connecter.

Une invitation ne peut être utilisée qu'une seule fois.

320 Future<void> acceptInvite(String invite) async {
321 ResultMsg msg = await Discret().acceptInvite(invite);
322 if (!msg.successful) {
323 logger.log(msg.error);
324 }
325 }