aotrautils-srv 0.0.1478 → 0.0.1480
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aotrautils-srv/aotrautils-srv.build.js +171 -129
- aotrautils-srv/package.json +1 -1
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
|
3
|
-
/*utils COMMONS library associated with aotra version : «1_29072022-2359 (
|
3
|
+
/*utils COMMONS library associated with aotra version : «1_29072022-2359 (27/04/2025-01:10:16)»*/
|
4
4
|
/*-----------------------------------------------------------------------------*/
|
5
5
|
|
6
6
|
|
@@ -4894,7 +4894,7 @@ AOTRAUTILS_LIB_IS_LOADED=true;
|
|
4894
4894
|
|
4895
4895
|
|
4896
4896
|
|
4897
|
-
/*utils AI library associated with aotra version : «1_29072022-2359 (
|
4897
|
+
/*utils AI library associated with aotra version : «1_29072022-2359 (27/04/2025-01:10:16)»*/
|
4898
4898
|
/*-----------------------------------------------------------------------------*/
|
4899
4899
|
|
4900
4900
|
|
@@ -5040,7 +5040,7 @@ getOpenAIAPIClient=(modelName, apiURL, agentRole, defaultPrompt)=>{
|
|
5040
5040
|
|
5041
5041
|
|
5042
5042
|
|
5043
|
-
/*utils CONSOLE library associated with aotra version : «1_29072022-2359 (
|
5043
|
+
/*utils CONSOLE library associated with aotra version : «1_29072022-2359 (27/04/2025-01:10:16)»*/
|
5044
5044
|
/*-----------------------------------------------------------------------------*/
|
5045
5045
|
|
5046
5046
|
|
@@ -5294,7 +5294,25 @@ WebsocketImplementation={
|
|
5294
5294
|
WebsocketImplementation.isNodeContext=isNodeContext;
|
5295
5295
|
WebsocketImplementation.useSocketIOImplementation=useSocketIOImplementation;
|
5296
5296
|
|
5297
|
-
if(WebsocketImplementation.useSocketIOImplementation){
|
5297
|
+
if(!WebsocketImplementation.useSocketIOImplementation){
|
5298
|
+
// TRACE
|
5299
|
+
lognow("INFO : (SERVER/CLIENT) Using native WebSocket implementation.");
|
5300
|
+
|
5301
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications
|
5302
|
+
if(isNodeContext){
|
5303
|
+
if(typeof(WebSocket)==="undefined"){
|
5304
|
+
// TRACE
|
5305
|
+
console.log("«ws» SERVER library not called yet, calling it now.");
|
5306
|
+
|
5307
|
+
WebSocket=require("ws");
|
5308
|
+
if(typeof(WebSocket)==="undefined"){
|
5309
|
+
// TRACE
|
5310
|
+
console.log("ERROR : «ws» CONSOLE/BROWSER CLIENT/SERVER library not found. Cannot launch nodejs server. Aborting.");
|
5311
|
+
}
|
5312
|
+
}
|
5313
|
+
}
|
5314
|
+
|
5315
|
+
}else{
|
5298
5316
|
// TRACE
|
5299
5317
|
lognow("INFO : (SERVER/CLIENT) Using socket.io websocket implementation.");
|
5300
5318
|
|
@@ -5327,23 +5345,6 @@ WebsocketImplementation={
|
|
5327
5345
|
}
|
5328
5346
|
}
|
5329
5347
|
|
5330
|
-
}else{
|
5331
|
-
// TRACE
|
5332
|
-
lognow("INFO : (SERVER/CLIENT) Using native WebSocket implementation.");
|
5333
|
-
|
5334
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications
|
5335
|
-
if(isNodeContext){
|
5336
|
-
if(typeof(WebSocket)==="undefined"){
|
5337
|
-
// TRACE
|
5338
|
-
console.log("«ws» SERVER library not called yet, calling it now.");
|
5339
|
-
|
5340
|
-
WebSocket=require("ws");
|
5341
|
-
if(typeof(WebSocket)==="undefined"){
|
5342
|
-
// TRACE
|
5343
|
-
console.log("ERROR : «ws» CONSOLE/BROWSER CLIENT/SERVER library not found. Cannot launch nodejs server. Aborting.");
|
5344
|
-
}
|
5345
|
-
}
|
5346
|
-
}
|
5347
5348
|
|
5348
5349
|
}
|
5349
5350
|
|
@@ -7118,6 +7119,48 @@ getAORTACNode=function(nodeId=getUUID(), selfOrigin="ws://127.0.0.1:40000", outc
|
|
7118
7119
|
|
7119
7120
|
// ==================================================================================================================
|
7120
7121
|
|
7122
|
+
class ServerReceptionEntryPoint{
|
7123
|
+
|
7124
|
+
constructor(channelNameParam, clientsRoomsTag, doOnIncomingMessage){
|
7125
|
+
this.channelName=channelNameParam;
|
7126
|
+
this.clientsRoomsTag=clientsRoomsTag;
|
7127
|
+
this.doOnIncomingMessage=doOnIncomingMessage;
|
7128
|
+
}
|
7129
|
+
|
7130
|
+
execute(eventOrMessage, clientSocketParam){
|
7131
|
+
|
7132
|
+
// With «ws» library we have no choive than register message events inside a «connection» event !
|
7133
|
+
|
7134
|
+
const dataWrapped=WebsocketImplementation.getMessageDataBothImplementations(eventOrMessage);
|
7135
|
+
|
7136
|
+
// Channel information is stored in exchanged data :
|
7137
|
+
if(dataWrapped.channelName!==this.channelName) return;
|
7138
|
+
|
7139
|
+
// TODO : FIXME : Use one single interface !
|
7140
|
+
// Room information is stored in client socket object :
|
7141
|
+
const isClientInRoom=WebsocketImplementation.isInRoom(clientSocketParam, this.clientsRoomsTag);
|
7142
|
+
|
7143
|
+
// DBG
|
7144
|
+
lognow("(SERVER) isClientInRoom:",isClientInRoom);
|
7145
|
+
|
7146
|
+
if(!isClientInRoom) return;
|
7147
|
+
|
7148
|
+
if(this.doOnIncomingMessage){
|
7149
|
+
// DBG
|
7150
|
+
lognow("(SERVER) this.doOnIncomingMessage:");
|
7151
|
+
this.doOnIncomingMessage(dataWrapped.data, clientSocketParam);
|
7152
|
+
}
|
7153
|
+
|
7154
|
+
}
|
7155
|
+
}
|
7156
|
+
|
7157
|
+
|
7158
|
+
|
7159
|
+
|
7160
|
+
|
7161
|
+
|
7162
|
+
|
7163
|
+
|
7121
7164
|
class NodeServerInstance{
|
7122
7165
|
|
7123
7166
|
constructor(serverSocket, listenableServer){
|
@@ -7127,7 +7170,7 @@ class NodeServerInstance{
|
|
7127
7170
|
this.clientPingIntervalMillis=5000;
|
7128
7171
|
this.serverSocket=serverSocket;
|
7129
7172
|
this.listenableServer=listenableServer;
|
7130
|
-
this.
|
7173
|
+
this.serverReceptionEntryPoints=[];
|
7131
7174
|
|
7132
7175
|
}
|
7133
7176
|
|
@@ -7150,54 +7193,28 @@ class NodeServerInstance{
|
|
7150
7193
|
// DBG
|
7151
7194
|
lognow("(SERVER) Registering receive for «"+channelNameParam+"»...");
|
7152
7195
|
|
7153
|
-
|
7154
|
-
const receptionEntryPoint={
|
7155
|
-
channelName:channelNameParam,
|
7156
|
-
clientsRoomsTag:clientsRoomsTag,
|
7157
|
-
execute:(eventOrMessage, clientSocketParam)=>{
|
7158
|
-
|
7159
|
-
// With «ws» library we have no choive than register message events inside a «connection» event !
|
7160
|
-
|
7161
|
-
const dataWrapped=WebsocketImplementation.getMessageDataBothImplementations(eventOrMessage);
|
7162
|
-
|
7163
|
-
// Channel information is stored in exchanged data :
|
7164
|
-
if(dataWrapped.channelName!==receptionEntryPoint.channelName) return;
|
7165
|
-
|
7166
|
-
|
7167
|
-
// TODO : FIXME : Use one single interface !
|
7168
|
-
// Room information is stored in client socket object :
|
7169
|
-
const isClientInRoom=WebsocketImplementation.isInRoom(clientSocketParam, receptionEntryPoint.clientsRoomsTag);
|
7170
|
-
|
7171
|
-
// DBG
|
7172
|
-
lognow("(SERVER) isClientInRoom:",isClientInRoom);
|
7173
|
-
|
7174
|
-
if(!isClientInRoom) return;
|
7175
|
-
|
7176
|
-
if(doOnIncomingMessage){
|
7177
|
-
// DBG
|
7178
|
-
lognow("(SERVER) doOnIncomingMessage:");
|
7179
|
-
doOnIncomingMessage(dataWrapped.data, clientSocketParam);
|
7180
|
-
}
|
7181
|
-
|
7182
|
-
},
|
7183
|
-
};
|
7184
|
-
|
7196
|
+
const serverReceptionEntryPoint=new ServerReceptionEntryPoint(channelNameParam, clientsRoomsTag, doOnIncomingMessage);
|
7185
7197
|
|
7186
7198
|
///!!!
|
7187
|
-
|
7199
|
+
this.serverReceptionEntryPoints.push(serverReceptionEntryPoint);
|
7188
7200
|
|
7189
7201
|
// SPECIAL FOR THE SOCKETIO IMPLEMENTATION :
|
7190
7202
|
if(WebsocketImplementation.useSocketIOImplementation){
|
7191
|
-
const channelName=
|
7203
|
+
const channelName=serverReceptionEntryPoint.channelName;
|
7192
7204
|
clientSocket.on(channelName, (eventOrMessage)=>{
|
7193
|
-
|
7205
|
+
serverReceptionEntryPoint.execute(eventOrMessage, clientSocket);
|
7194
7206
|
});
|
7195
7207
|
}
|
7196
7208
|
|
7197
7209
|
return this;
|
7198
7210
|
}
|
7199
|
-
|
7200
|
-
|
7211
|
+
|
7212
|
+
|
7213
|
+
// TODO : DEVELOP...
|
7214
|
+
//sendChainable(channelNameParam, data, clientsRoomsTag=null){
|
7215
|
+
//}
|
7216
|
+
|
7217
|
+
|
7201
7218
|
send(channelName, data, clientsRoomsTag=null, clientSocketParam=null){
|
7202
7219
|
|
7203
7220
|
// DBG
|
@@ -7289,13 +7306,14 @@ class NodeServerInstance{
|
|
7289
7306
|
|
7290
7307
|
const doOnMessage=(eventOrMessage)=>{
|
7291
7308
|
// We execute the events registration listeners entry points:
|
7292
|
-
foreach(self.
|
7293
|
-
|
7309
|
+
foreach(self.serverReceptionEntryPoints,(serverReceptionEntryPoint,i)=>{
|
7310
|
+
serverReceptionEntryPoint.execute(eventOrMessage, clientSocket);
|
7294
7311
|
});
|
7295
7312
|
};
|
7296
7313
|
|
7297
|
-
if(!WebsocketImplementation.useSocketIOImplementation)
|
7314
|
+
if(!WebsocketImplementation.useSocketIOImplementation){
|
7298
7315
|
clientSocket.addEventListener("message", doOnMessage);
|
7316
|
+
}
|
7299
7317
|
|
7300
7318
|
doOnConnection(this, clientSocket);
|
7301
7319
|
|
@@ -7335,19 +7353,26 @@ class NodeServerInstance{
|
|
7335
7353
|
|
7336
7354
|
clientSocket.isConnectionAlive=false;
|
7337
7355
|
|
7338
|
-
if(!WebsocketImplementation.useSocketIOImplementation)
|
7339
|
-
|
7356
|
+
if(!WebsocketImplementation.useSocketIOImplementation){
|
7357
|
+
// FOR THE WEBSOCKET IMPLEMENTATION :
|
7358
|
+
clientSocket.ping();
|
7359
|
+
}else{
|
7360
|
+
// FOR THE SOCKETIO IMPLEMENTATION :
|
7361
|
+
self.send("protocol",{type:"ping"}, null, clientSocket);
|
7362
|
+
}
|
7340
7363
|
|
7341
7364
|
|
7342
7365
|
}, this.clientPingIntervalMillis);
|
7343
7366
|
|
7344
|
-
|
7367
|
+
|
7368
|
+
// PING-PONG :
|
7345
7369
|
if(!WebsocketImplementation.useSocketIOImplementation){
|
7370
|
+
// FOR THE WEBSOCKET IMPLEMENTATION :
|
7346
7371
|
clientSocket.on("pong",()=>{
|
7347
7372
|
clientSocket.isConnectionAlive=true;
|
7348
7373
|
});
|
7349
7374
|
}else{
|
7350
|
-
|
7375
|
+
// FOR THE SOCKETIO IMPLEMENTATION :
|
7351
7376
|
this.receive("protocol",(message)=>{
|
7352
7377
|
if(message.type!=="pong") return;
|
7353
7378
|
clientSocket.isConnectionAlive=true;
|
@@ -7410,6 +7435,47 @@ if(typeof(window)==="undefined") window=global;
|
|
7410
7435
|
|
7411
7436
|
// ==================================================================================================================
|
7412
7437
|
|
7438
|
+
|
7439
|
+
class ClientReceptionEntryPoint{
|
7440
|
+
|
7441
|
+
constructor(channelNameParam, entryPointId, clientsRoomsTag, listenerConfig, doOnIncomingMessage){
|
7442
|
+
this.channelName=channelNameParam;
|
7443
|
+
this.entryPointId=entryPointId;
|
7444
|
+
this.clientsRoomsTag=clientsRoomsTag;
|
7445
|
+
this.listenerConfig=listenerConfig;
|
7446
|
+
this.doOnIncomingMessage=doOnIncomingMessage;
|
7447
|
+
}
|
7448
|
+
|
7449
|
+
execute(eventOrMessage, clientSocket, clientReceptionEntryPoints){
|
7450
|
+
|
7451
|
+
const channelName=this.channelName;
|
7452
|
+
|
7453
|
+
|
7454
|
+
const dataWrapped=WebsocketImplementation.getMessageDataBothImplementations(eventOrMessage);
|
7455
|
+
|
7456
|
+
// Channel information is stored in exchanged data :
|
7457
|
+
if(dataWrapped.channelName && dataWrapped.channelName!==this.channelName) return;
|
7458
|
+
|
7459
|
+
// Room information is stored in client socket object :
|
7460
|
+
if(!WebsocketImplementation.isInRoom(clientSocket, this.clientsRoomsTag)) return;
|
7461
|
+
|
7462
|
+
|
7463
|
+
if( this.listenerConfig && this.listenerConfig.messageType && dataWrapped.type
|
7464
|
+
&& this.listenerConfig.messageType===dataWrapped.type) return;
|
7465
|
+
|
7466
|
+
// We remove one-time usage listeners :
|
7467
|
+
// We remove BEFORE executing doOnIncomingMessage(), because in this reception entry point function we can have other listener registration UNDER THE SAME ID !
|
7468
|
+
if(this.listenerConfig && this.listenerConfig.destroyListenerAfterReceiving)
|
7469
|
+
remove(this, clientReceptionEntryPoints);
|
7470
|
+
|
7471
|
+
if(this.doOnIncomingMessage)
|
7472
|
+
this.doOnIncomingMessage(dataWrapped.data, clientSocket);
|
7473
|
+
|
7474
|
+
}
|
7475
|
+
|
7476
|
+
}
|
7477
|
+
|
7478
|
+
|
7413
7479
|
//
|
7414
7480
|
// CLIENT INSTANCE :
|
7415
7481
|
//
|
@@ -7417,59 +7483,33 @@ class ClientInstance{
|
|
7417
7483
|
|
7418
7484
|
constructor(clientSocket){
|
7419
7485
|
this.clientSocket=clientSocket;
|
7420
|
-
this.
|
7486
|
+
this.clientReceptionEntryPoints=[];
|
7421
7487
|
|
7422
7488
|
}
|
7423
7489
|
|
7424
|
-
receive(channelNameParam, doOnIncomingMessage, clientsRoomsTag=null,
|
7490
|
+
receive(channelNameParam, doOnIncomingMessage, clientsRoomsTag=null, entryPointId=null, listenerConfig={destroyListenerAfterReceiving:false}){
|
7425
7491
|
const self=this;
|
7426
7492
|
|
7427
7493
|
// DBG
|
7428
|
-
lognow("INFO : (CLIENT
|
7429
|
-
|
7430
|
-
const receptionEntryPoint={
|
7431
|
-
channelName:channelNameParam,
|
7432
|
-
clientsRoomsTag:clientsRoomsTag,
|
7433
|
-
// TODO : ADD TO ALL OTHER SUBSYSTEMS !
|
7434
|
-
id:receptionEntryPointId,
|
7435
|
-
listenerConfig:listenerConfig,
|
7436
|
-
doOnIncomingMessage:doOnIncomingMessage,
|
7437
|
-
execute:(eventOrMessage)=>{
|
7438
|
-
|
7439
|
-
const dataWrapped=WebsocketImplementation.getMessageDataBothImplementations(eventOrMessage);
|
7440
|
-
|
7441
|
-
// Channel information is stored in exchanged data :
|
7442
|
-
if(dataWrapped.channelName && dataWrapped.channelName!==receptionEntryPoint.channelName) return;
|
7443
|
-
|
7444
|
-
// Room information is stored in client socket object :
|
7445
|
-
const clientSocket=self.clientSocket;
|
7446
|
-
if(!WebsocketImplementation.isInRoom(clientSocket, receptionEntryPoint.clientsRoomsTag)) return;
|
7447
|
-
|
7448
|
-
// We remove one-time usage listeners :
|
7449
|
-
// We remove BEFORE executing doOnIncomingMessage(), because in receptionEntryPoint function we can have other listener registration UNDER THE SAME ID !
|
7450
|
-
if(receptionEntryPoint.listenerConfig && receptionEntryPoint.listenerConfig.destroyListenerAfterReceiving)
|
7451
|
-
remove(self.receptionEntryPoints, receptionEntryPoint);
|
7452
|
-
|
7453
|
-
if(receptionEntryPoint.doOnIncomingMessage)
|
7454
|
-
receptionEntryPoint.doOnIncomingMessage(dataWrapped.data, clientSocket);
|
7455
|
-
|
7456
|
-
}
|
7457
|
-
};
|
7458
|
-
|
7459
|
-
///!!!
|
7460
|
-
// TODO : ADD TO ALL OTHER SUBSYSTEMS !
|
7461
|
-
if(!contains.filter((l)=>(l.id && receptionEntryPoint.id && l.id===receptionEntryPoint.id))(this.receptionEntryPoints))
|
7462
|
-
this.receptionEntryPoints.push(receptionEntryPoint);
|
7494
|
+
lognow("INFO : (CLIENT) SETTING UP RECEIVE for :",channelNameParam);
|
7463
7495
|
|
7464
|
-
|
7465
|
-
if(
|
7466
|
-
|
7467
|
-
|
7468
|
-
|
7496
|
+
const clientReceptionEntryPoint=new ClientReceptionEntryPoint(channelNameParam, entryPointId, clientsRoomsTag, listenerConfig, doOnIncomingMessage);
|
7497
|
+
if(!contains.filter((l)=>(
|
7498
|
+
l.entryPointId && clientReceptionEntryPoint.entryPointId
|
7499
|
+
&& l.entryPointId===clientReceptionEntryPoint.entryPointId
|
7500
|
+
))(this.clientReceptionEntryPoints)){
|
7501
|
+
|
7502
|
+
this.clientReceptionEntryPoints.push(clientReceptionEntryPoint);
|
7503
|
+
}
|
7504
|
+
|
7505
|
+
const clientSocket=this.clientSocket;
|
7506
|
+
if(WebsocketImplementation.useSocketIOImplementation){
|
7507
|
+
// FOR THE SOCKETIO IMPLEMENTATION :
|
7508
|
+
clientSocket.on(channelNameParam, (eventOrMessage)=>{
|
7509
|
+
clientReceptionEntryPoint.execute(eventOrMessage, clientSocket, clientReceptionEntryPoint);
|
7469
7510
|
});
|
7470
7511
|
}
|
7471
|
-
|
7472
|
-
|
7512
|
+
|
7473
7513
|
return this;
|
7474
7514
|
}
|
7475
7515
|
|
@@ -7505,6 +7545,7 @@ class ClientInstance{
|
|
7505
7545
|
}
|
7506
7546
|
|
7507
7547
|
doOnIncomingMessageForResponse(dataLocal, clientSocket);
|
7548
|
+
|
7508
7549
|
}, resultPromise.clientsRoomsTag, listenerId, {destroyListenerAfterReceiving:true});
|
7509
7550
|
//
|
7510
7551
|
|
@@ -7567,39 +7608,40 @@ class ClientInstance{
|
|
7567
7608
|
// DBG
|
7568
7609
|
lognow("DEBUG : CLIENT : doOnConnection !");
|
7569
7610
|
|
7570
|
-
const doOnMessage=(eventOrMessage)=>{
|
7571
|
-
|
7572
|
-
// We execute the listeners entry points registration :
|
7573
|
-
foreach(self.receptionEntryPoints,(receptionEntryPoint)=>{
|
7574
|
-
receptionEntryPoint.execute(eventOrMessage);
|
7575
|
-
});
|
7576
|
-
|
7577
|
-
};
|
7578
|
-
|
7579
7611
|
const clientSocket=self.clientSocket;
|
7580
|
-
if(!WebsocketImplementation.useSocketIOImplementation){
|
7612
|
+
if(!WebsocketImplementation.useSocketIOImplementation){
|
7613
|
+
// FOR THE WEBSOCKET IMPLEMENTATION :
|
7614
|
+
clientSocket.addEventListener("message", (eventOrMessage)=>{
|
7615
|
+
foreach(self.clientReceptionEntryPoints,(clientReceptionEntryPoint)=>{
|
7616
|
+
clientReceptionEntryPoint.execute(eventOrMessage, clientSocket, self.clientReceptionEntryPoints);
|
7617
|
+
});
|
7618
|
+
});
|
7581
7619
|
}
|
7582
7620
|
|
7583
7621
|
doOnConnection(self, clientSocket);
|
7584
|
-
|
7585
7622
|
};
|
7586
7623
|
|
7587
|
-
if(!WebsocketImplementation.useSocketIOImplementation) this.clientSocket.addEventListener("open",doAllOnConnection);
|
7588
|
-
else this.clientSocket.on("connect",doAllOnConnection);
|
7589
|
-
|
7590
7624
|
|
7591
|
-
|
7592
|
-
|
7625
|
+
if(!WebsocketImplementation.useSocketIOImplementation){
|
7626
|
+
// FOR THE WEBSOCKET IMPLEMENTATION :
|
7627
|
+
this.clientSocket.addEventListener("open",doAllOnConnection);
|
7628
|
+
}else{
|
7629
|
+
// FOR THE SOCKETIO IMPLEMENTATION :
|
7630
|
+
this.clientSocket.on("connect",doAllOnConnection);
|
7631
|
+
|
7632
|
+
// Client ping handling :
|
7633
|
+
// (SocketIO implementation only, the websocket implementation sends back a hidden pong !)
|
7634
|
+
// PING-PONG :
|
7593
7635
|
this.receive("protocol",(message)=>{
|
7594
7636
|
if(message.type!=="ping") return;
|
7595
7637
|
self.send("protocol",{type:"pong"});
|
7596
|
-
|
7597
|
-
//
|
7598
|
-
lognow("DEBUG : CLIENT : Pong sent.");
|
7638
|
+
// // DBG
|
7639
|
+
// lognow("DEBUG : CLIENT : Pong sent.");
|
7599
7640
|
});
|
7600
7641
|
}
|
7601
7642
|
|
7602
7643
|
|
7644
|
+
|
7603
7645
|
}
|
7604
7646
|
|
7605
7647
|
}
|
aotrautils-srv/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "aotrautils-srv",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.1480",
|
4
4
|
"main": "aotrautils-srv.build.js",
|
5
5
|
"description": "A library for vanilla javascript utils (server-side) used in aotra javascript CMS",
|
6
6
|
"author": "Jeremie Ratomposon <info@alqemia.com> (https://alqemia.com)",
|