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:
- remplacez ./lib/main.dart par le nouveau fichier d'exemple ./lib/example_advanced_chat.dart.
- compilez ensuite l'application. cela peut prendre plusieurs minutes
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/
- Lancez un première fois l'application discret_flutter et créez un nouveau compte. Par rapport au précédent exemple, vous noterez qu'un nouveau champ nommé displayed name est apparu. Il s'agit du nom que les autres Pairs verront.
- Gardez la première application ouverte, lancez l'application une seconde fois, et créez un second compte.
- Sur la première fenêtres, cliquez sur le bouton Invite, copiez l'invitation en cliquant sur Copy to Clipboard et fermez la fenêtre en cliquant sur Ok
- Sur la seconde fenêtre, cliquez sur le bouton Accept Invite, collez l'invitation dans le champs prévu à cet effet et appuyez sur Ok.
- Vous devriez voir les noms des comptes apparaître dans les deux fenêtres, indiquant que l'invitation a été acceptée.
- Vous devriez pouvoir discuter entre les deux comptes.
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 //...
19 void 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:
- error
- warn
- info
- debug
- trace
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:
- la connections
- les invitations
- la gestion de la liste des groupes de discussion.
class ChatState extends ChangeNotifier {
final List<ChatRoom> chatRooms = [];
//..
}
La classe ChatRoom contient tous les champs et fonctions pour:
- envoyer des message
- recevoir les messages.
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:
- une fois dans le champ admin, indiquant que vous être administrateur de cette Room,
- une fois dans le champ user de l'autorisation. Ce n'est pas vraiment nécessaire car les administrateurs ont tous les droits, mais cela simplifie le code de cet exemple. Dans la liste des groupe de chat nous n'afficheront que les Rooms possédant deux entrées dans le champs user, car cela indique que l'invitation a été acceptée.
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:
- l'id de la Room,
- l'id d'une autorisation interne à cette Room.
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 }