@20syldev/api 2.9.0 → 3.2.0
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.
- package/README.md +4 -4
- package/app.js +238 -19
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://api.sylvain.pro"><img src="https://api.sylvain.pro/favicon.ico" alt="Logo" width="25%" height="auto"/></a>
|
|
3
3
|
|
|
4
4
|
# API Personnelle
|
|
5
|
-
[](https://github.com/20syldev/api/releases/latest)
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## À propos de l'API
|
|
11
11
|
Voici mon API personnelle, disponible sur le domaine [api.sylvain.pro](https://api.sylvain.pro).
|
|
12
12
|
L'API est développée avec Node.js et hébergée **24h/7j**. Elle est **simple d'utilisation** et a une **documentation** disponible sur [docs.sylvain.pro](https://docs.sylvain.pro) !
|
|
13
|
-
> *Une limite de **1000** requêtes maximum chaque **10 secondes** est fixée.*
|
|
13
|
+
> *Une limite de **1000** requêtes maximum chaque **10 secondes** est fixée. Elle est baissé pour certains endpoints nécessitant plus de ressources.*
|
|
14
14
|
|
|
15
15
|
## Installer le paquet de l'API sur votre machine
|
|
16
16
|
```console
|
|
@@ -23,7 +23,7 @@ Pour utiliser le **paquet** dans votre projet, **créez** un fichier **JavaScrip
|
|
|
23
23
|
require('@20syldev/api');
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Puis, **démarrez** un serveur [Node.js](
|
|
26
|
+
Puis, **démarrez** un serveur [Node.js](https://nodejs.org) pour utiliser l'**API** :
|
|
27
27
|
```console
|
|
28
28
|
$ node index.js
|
|
29
29
|
API is running on
|
|
@@ -44,7 +44,7 @@ Enfin, **exécutez** le script de build, qui installera les **dépendances** et
|
|
|
44
44
|
$ npm run build
|
|
45
45
|
```
|
|
46
46
|
```console
|
|
47
|
-
> @20syldev/api@2.
|
|
47
|
+
> @20syldev/api@3.2.0 build
|
|
48
48
|
> npm install && node app.js
|
|
49
49
|
|
|
50
50
|
[...]
|
package/app.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { createCanvas } from 'canvas';
|
|
2
1
|
import cors from 'cors';
|
|
3
2
|
import dotenv from 'dotenv';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import ical from 'ical.js';
|
|
6
|
+
import { createCanvas } from 'canvas';
|
|
4
7
|
import { randomBytes, getHashes, createHash } from 'crypto';
|
|
5
|
-
import
|
|
8
|
+
import { urlencoded, json } from 'express';
|
|
6
9
|
import { factorial } from 'mathjs';
|
|
7
|
-
import fetch from 'node-fetch';
|
|
8
10
|
import { dirname, join } from 'path';
|
|
9
11
|
import { toDataURL } from 'qrcode';
|
|
10
12
|
import { fileURLToPath } from 'url';
|
|
@@ -15,13 +17,15 @@ const __dirname = dirname(__filename);
|
|
|
15
17
|
const app = express();
|
|
16
18
|
|
|
17
19
|
// Define allowed versions & endpoints for each version
|
|
18
|
-
const versions = ['v1', 'v2'];
|
|
20
|
+
const versions = ['v1', 'v2', 'v3'];
|
|
19
21
|
const endpoints = {
|
|
20
22
|
v1: ['algorithms', 'captcha', 'color', 'convert', 'domain', 'infos', 'personal', 'qrcode', 'token', 'username', 'website'],
|
|
21
|
-
v2: ['algorithms', 'captcha', 'chat', 'color', 'convert', 'domain', 'hash', 'infos', 'personal', 'qrcode', 'tic-tac-toe', 'token', 'username', 'website']
|
|
23
|
+
v2: ['algorithms', 'captcha', 'chat', 'color', 'convert', 'domain', 'hash', 'infos', 'personal', 'qrcode', 'tic-tac-toe', 'token', 'username', 'website'],
|
|
24
|
+
v3: ['algorithms', 'captcha', 'chat', 'color', 'convert', 'date', 'domain', 'hash', 'hyperplanning', 'infos', 'levenshtein', 'personal', 'qrcode', 'tic-tac-toe', 'time', 'token', 'username', 'website']
|
|
22
25
|
};
|
|
23
26
|
|
|
24
|
-
// Arrowed functions (math & random)
|
|
27
|
+
// Arrowed functions (formatting, math & random)
|
|
28
|
+
const formatDate = d => new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
|
|
25
29
|
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
26
30
|
const genID = () => {
|
|
27
31
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
@@ -35,7 +39,46 @@ const random = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
|
35
39
|
const logs = [], chat = [], privateChats = {}, sessions = {}, rateLimits = {}, games = {};
|
|
36
40
|
|
|
37
41
|
// Define global variables
|
|
38
|
-
let lastFetch = 0, requests = 0, resetTime = Date.now() + 10000;
|
|
42
|
+
let contributions, lastFetch = 0, requests = 0, resetTime = Date.now() + 10000;
|
|
43
|
+
|
|
44
|
+
// ----------- ----------- MAIN FUNCTIONS ----------- ----------- //
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check the game result of a Tic-Tac-Toe game.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array} moves - The moves of the game.
|
|
50
|
+
* @returns {Object} - The result of the game.
|
|
51
|
+
*/
|
|
52
|
+
function checkGame(moves) {
|
|
53
|
+
let board = Array(3).fill().map(() => Array(3).fill(null));
|
|
54
|
+
let playerSymbols = {};
|
|
55
|
+
let playersOrder = [];
|
|
56
|
+
|
|
57
|
+
moves.forEach(({ username, move }) => {
|
|
58
|
+
if (!playerSymbols[username]) {
|
|
59
|
+
playersOrder.push(username);
|
|
60
|
+
playerSymbols[username] = playersOrder.length === 1 ? 'X' : 'O';
|
|
61
|
+
}
|
|
62
|
+
let [row, col] = move.split('-').map(Number);
|
|
63
|
+
board[row - 1][col - 1] = playerSymbols[username];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const checkWinner = (symbol) => {
|
|
67
|
+
for (let i = 0; i < 3; i++) {
|
|
68
|
+
if (board[i][0] === symbol && board[i][1] === symbol && board[i][2] === symbol) return true;
|
|
69
|
+
if (board[0][i] === symbol && board[1][i] === symbol && board[2][i] === symbol) return true;
|
|
70
|
+
}
|
|
71
|
+
if (board[0][0] === symbol && board[1][1] === symbol && board[2][2] === symbol) return true;
|
|
72
|
+
if (board[0][2] === symbol && board[1][1] === symbol && board[2][0] === symbol) return true;
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let winner = Object.keys(playerSymbols).find(player => checkWinner(playerSymbols[player]));
|
|
77
|
+
let isTie = !winner && moves.length === 9;
|
|
78
|
+
let loser = winner && playersOrder.length === 2 ? playersOrder.find(player => player !== winner) : null;
|
|
79
|
+
|
|
80
|
+
return { winner, loser, tie: isTie };
|
|
81
|
+
}
|
|
39
82
|
|
|
40
83
|
// ----------- ----------- MIDDLEWARES SETUP ----------- ----------- //
|
|
41
84
|
|
|
@@ -93,7 +136,7 @@ app.use((req, res, next) => {
|
|
|
93
136
|
// Internal Server Error
|
|
94
137
|
app.use((err, req, res, next) => {
|
|
95
138
|
console.error(err.stack);
|
|
96
|
-
res.status(500).
|
|
139
|
+
res.status(500).jsonResponse({
|
|
97
140
|
message: 'Internal Server Error',
|
|
98
141
|
error: err.message,
|
|
99
142
|
documentation: 'https://docs.sylvain.pro',
|
|
@@ -148,7 +191,8 @@ app.get('/', (req, res) => {
|
|
|
148
191
|
logs: 'https://api.sylvain.pro/logs',
|
|
149
192
|
versions: {
|
|
150
193
|
v1: 'https://api.sylvain.pro/v1',
|
|
151
|
-
v2: 'https://api.sylvain.pro/v2'
|
|
194
|
+
v2: 'https://api.sylvain.pro/v2',
|
|
195
|
+
v3: 'https://api.sylvain.pro/v3'
|
|
152
196
|
}
|
|
153
197
|
});
|
|
154
198
|
});
|
|
@@ -157,6 +201,7 @@ app.get('/', (req, res) => {
|
|
|
157
201
|
app.get('/v1', (req, res) => {
|
|
158
202
|
res.jsonResponse({
|
|
159
203
|
version: 'v1',
|
|
204
|
+
documentation: 'https://docs.sylvain.pro/v1',
|
|
160
205
|
endpoints: {
|
|
161
206
|
get: {
|
|
162
207
|
algorithm: '/v1/algorithms?method={algorithm}&value={value}(&value2={value2})',
|
|
@@ -180,6 +225,7 @@ app.get('/v1', (req, res) => {
|
|
|
180
225
|
app.get('/v2', (req, res) => {
|
|
181
226
|
res.jsonResponse({
|
|
182
227
|
version: 'v2',
|
|
228
|
+
documentation: 'https://docs.sylvain.pro/v2',
|
|
183
229
|
endpoints: {
|
|
184
230
|
get: {
|
|
185
231
|
algorithm: '/v2/algorithms?method={algorithm}&value={value}(&value2={value2})',
|
|
@@ -209,6 +255,43 @@ app.get('/v2', (req, res) => {
|
|
|
209
255
|
});
|
|
210
256
|
});
|
|
211
257
|
|
|
258
|
+
// Display v3 endpoints
|
|
259
|
+
app.get('/v3', (req, res) => {
|
|
260
|
+
res.jsonResponse({
|
|
261
|
+
version: 'v3',
|
|
262
|
+
documentation: 'https://docs.sylvain.pro/v3',
|
|
263
|
+
endpoints: {
|
|
264
|
+
get: {
|
|
265
|
+
algorithm: '/v3/algorithms?method={algorithm}&value={value}(&value2={value2})',
|
|
266
|
+
captcha: '/v3/captcha?text={text}',
|
|
267
|
+
chat: '/v3/chat',
|
|
268
|
+
color: '/v3/color',
|
|
269
|
+
convert: '/v3/convert?value={value}&from={unit}&to={unit}',
|
|
270
|
+
domain: '/v3/domain',
|
|
271
|
+
infos: '/v3/infos',
|
|
272
|
+
levenshtein: '/v3/levenshtein?str1={string}&str2={string}',
|
|
273
|
+
personal: '/v3/personal',
|
|
274
|
+
qrcode: '/v3/qrcode?url={URL}',
|
|
275
|
+
time: '/v3/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})',
|
|
276
|
+
username: '/v3/username'
|
|
277
|
+
},
|
|
278
|
+
post: {
|
|
279
|
+
chat: {
|
|
280
|
+
chat: '/v3/chat',
|
|
281
|
+
private: '/v3/chat/private'
|
|
282
|
+
},
|
|
283
|
+
hash: '/v3/hash',
|
|
284
|
+
hyperplanning: '/v3/hyperplanning',
|
|
285
|
+
tic_tac_toe: {
|
|
286
|
+
tic_tac_toe: '/v3/tic-tac-toe',
|
|
287
|
+
fetch: '/v3/tic-tac-toe/fetch'
|
|
288
|
+
},
|
|
289
|
+
token: '/v3/token'
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
212
295
|
// Display logs
|
|
213
296
|
app.get('/logs', (req, res) => res.jsonResponse(logs));
|
|
214
297
|
|
|
@@ -450,6 +533,11 @@ app.get('/:version/domain', (req, res) => {
|
|
|
450
533
|
});
|
|
451
534
|
});
|
|
452
535
|
|
|
536
|
+
// GET planning error
|
|
537
|
+
app.get('/:version/hyperplanning', (req, res) => {
|
|
538
|
+
res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
|
|
539
|
+
});
|
|
540
|
+
|
|
453
541
|
// GET hash error
|
|
454
542
|
app.get('/:version/hash', (req, res) => {
|
|
455
543
|
res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
|
|
@@ -466,6 +554,29 @@ app.get('/:version/infos', (req, res) => {
|
|
|
466
554
|
});
|
|
467
555
|
});
|
|
468
556
|
|
|
557
|
+
// Calculate Levenshtein distance
|
|
558
|
+
app.get('/:version/levenshtein', (req, res) => {
|
|
559
|
+
const { str1, str2 } = req.query;
|
|
560
|
+
|
|
561
|
+
if (!str1 || typeof str1 !== 'string') return res.jsonResponse({ error: 'Please provide a first string (?str1={string})' });
|
|
562
|
+
if (!str2 || typeof str2 !== 'string') return res.jsonResponse({ error: 'Please provide a second string (&str2={string})' });
|
|
563
|
+
|
|
564
|
+
if (str1.length > 1000) return res.jsonResponse({ error: 'First string exceeds 1000 characters.' });
|
|
565
|
+
if (str2.length > 1000) return res.jsonResponse({ error: 'Second string exceeds 1000 characters.' });
|
|
566
|
+
|
|
567
|
+
const lev = (a, b) => {
|
|
568
|
+
const m = Array.from({ length: a.length + 1 }, (_, i) => [i]);
|
|
569
|
+
for (let j = 0; j <= b.length; j++) m[0][j] = j;
|
|
570
|
+
for (let i = 1; i <= a.length; i++)
|
|
571
|
+
for (let j = 1; j <= b.length; j++)
|
|
572
|
+
m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] !== b[j - 1]));
|
|
573
|
+
|
|
574
|
+
return m[a.length][b.length];
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
res.jsonResponse({ str1, str2, distance: lev(str1, str2) });
|
|
578
|
+
});
|
|
579
|
+
|
|
469
580
|
// Generate personal data
|
|
470
581
|
app.get('/:version/personal', (req, res) => {
|
|
471
582
|
const people = [
|
|
@@ -600,6 +711,59 @@ app.get('/:version/tic-tac-toe/fetch', (req, res) => {
|
|
|
600
711
|
res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
|
|
601
712
|
});
|
|
602
713
|
|
|
714
|
+
// Display or generate time informations
|
|
715
|
+
app.get('/:version/time', (req, res) => {
|
|
716
|
+
const { type = 'live', start, end, format, timezone } = req.query;
|
|
717
|
+
|
|
718
|
+
const validFormats = ['iso', 'utc', 'timestamp', 'locale', 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'second', 'ms', 'dayOfWeek', 'dayOfYear', 'weekNumber', 'timezone', 'timezoneOffset'];
|
|
719
|
+
const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
|
|
720
|
+
|
|
721
|
+
if (type !== 'live' && type !== 'random') return res.jsonResponse({ error: 'Please provide a valid type (?type={type})' });
|
|
722
|
+
if (start && !Date.parse(start)) return res.jsonResponse({ error: 'Please provide a valid start date (?start={YYYY-MM-DD})' });
|
|
723
|
+
if (end && !Date.parse(end)) return res.jsonResponse({ error: 'Please provide a valid end date (?end={YYYY-MM-DD})' });
|
|
724
|
+
if (format && !validFormats.includes(format)) return res.jsonResponse({ error: 'Please provide a valid format (?format={format})' });
|
|
725
|
+
if (timezone && !validTimezones.includes(timezone)) return res.jsonResponse({ error: 'Please provide a valid timezone (?timezone={timezone})' });
|
|
726
|
+
|
|
727
|
+
const getTimeFormats = (date, timezoneOption) => {
|
|
728
|
+
return {
|
|
729
|
+
iso: date.toISOString(),
|
|
730
|
+
utc: date.toUTCString(),
|
|
731
|
+
timestamp: date.getTime(),
|
|
732
|
+
locale: date.toLocaleString('en-US', { timeZone: timezoneOption, timeZoneName: 'long' }),
|
|
733
|
+
date: date.toLocaleDateString('en-US', { timeZone: timezoneOption }),
|
|
734
|
+
time: date.toLocaleTimeString('en-US', { timeZone: timezoneOption }),
|
|
735
|
+
year: date.getFullYear(),
|
|
736
|
+
month: date.getMonth() + 1,
|
|
737
|
+
day: date.getDate(),
|
|
738
|
+
hour: date.getHours(),
|
|
739
|
+
minute: date.getMinutes(),
|
|
740
|
+
second: date.getSeconds(),
|
|
741
|
+
ms: date.getMilliseconds(),
|
|
742
|
+
dayOfWeek: date.getDay(),
|
|
743
|
+
dayOfYear: Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000),
|
|
744
|
+
weekNumber: Math.ceil((((date - new Date(date.getFullYear(), 0, 0)) / 86400000) + 1) / 7),
|
|
745
|
+
timezone: timezoneOption,
|
|
746
|
+
timezoneOffset: date.getTimezoneOffset()
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
if (type === 'random') {
|
|
751
|
+
const startDate = new Date(start || '1900-01-01').getTime();
|
|
752
|
+
const endDate = new Date(end || '2100-12-31').getTime();
|
|
753
|
+
const randomDate = new Date(start ? startDate : startDate + Math.random() * (endDate - startDate));
|
|
754
|
+
const timezoneOption = timezone || validTimezones[Math.floor(Math.random() * 5)];
|
|
755
|
+
const formats = getTimeFormats(randomDate, timezoneOption);
|
|
756
|
+
|
|
757
|
+
return res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const now = new Date();
|
|
761
|
+
const timezoneOption = timezone || 'UTC';
|
|
762
|
+
const formats = getTimeFormats(now, timezoneOption);
|
|
763
|
+
|
|
764
|
+
res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
|
|
765
|
+
});
|
|
766
|
+
|
|
603
767
|
// GET token error
|
|
604
768
|
app.get('/:version/token', (req, res) => {
|
|
605
769
|
res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
|
|
@@ -684,6 +848,7 @@ app.get('/:version/website', async (req, res) => {
|
|
|
684
848
|
res.jsonResponse({
|
|
685
849
|
versions: {
|
|
686
850
|
api: process.env.API,
|
|
851
|
+
cdn: process.env.CDN,
|
|
687
852
|
coop_api: process.env.COOP_API,
|
|
688
853
|
coop_status: process.env.COOP_STATUS,
|
|
689
854
|
chat: process.env.CHAT,
|
|
@@ -696,6 +861,7 @@ app.get('/:version/website', async (req, res) => {
|
|
|
696
861
|
gemsync: process.env.GEMSYNC,
|
|
697
862
|
gitsite: process.env.GITSITE,
|
|
698
863
|
logs: process.env.LOGS,
|
|
864
|
+
minify: process.env.MINIFY,
|
|
699
865
|
morpion: process.env.MORPION,
|
|
700
866
|
nitrogen: process.env.NITROGEN,
|
|
701
867
|
old_database: process.env.OLD_DATABASE,
|
|
@@ -708,8 +874,9 @@ app.get('/:version/website', async (req, res) => {
|
|
|
708
874
|
wrkit: process.env.WRKIT,
|
|
709
875
|
zpki: process.env.ZPKI
|
|
710
876
|
},
|
|
711
|
-
updated_projects: process.env.RECENT.split(' '),
|
|
712
|
-
new_projects: process.env.NEW.split(' '),
|
|
877
|
+
updated_projects: process.env.RECENT !== undefined ? process.env.RECENT.split(' ') : [],
|
|
878
|
+
new_projects: process.env.NEW !== undefined ? process.env.NEW.split(' ') : [],
|
|
879
|
+
sub_domains: process.env.DOMAINS !== undefined ? process.env.DOMAINS.split(' ') : [],
|
|
713
880
|
stats: {
|
|
714
881
|
os: process.env.STATS1,
|
|
715
882
|
front: process.env.STATS2,
|
|
@@ -784,6 +951,49 @@ app.post('/:version/chat/private', (req, res) => {
|
|
|
784
951
|
return res.jsonResponse({ error: 'Invalid or expired token.' });
|
|
785
952
|
});
|
|
786
953
|
|
|
954
|
+
// Display a planning from an ICS file
|
|
955
|
+
app.post('/:version/hyperplanning', async (req, res) => {
|
|
956
|
+
const { url, detail } = req.body;
|
|
957
|
+
|
|
958
|
+
if (!url) res.jsonResponse({ error: 'Please provide a valid ICS file URL.' });
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
const response = await fetch(url);
|
|
962
|
+
if (!response.ok || !(response.headers.get('content-type') || '').includes('text/calendar')) return res.jsonResponse({ error: 'Invalid ICS file format.' });
|
|
963
|
+
|
|
964
|
+
const events = new ical.Component(ical.parse(await response.text()))
|
|
965
|
+
.getAllSubcomponents('vevent')
|
|
966
|
+
.map(e => {
|
|
967
|
+
const evt = new ical.Event(e);
|
|
968
|
+
const summary = evt.summary.split(' ').filter(part => part !== '-');
|
|
969
|
+
const start = formatDate(evt.startDate.toJSDate());
|
|
970
|
+
const end = formatDate(evt.endDate.toJSDate());
|
|
971
|
+
|
|
972
|
+
if (detail === 'full') {
|
|
973
|
+
const desc = evt.description.split('\n').map(l => l.trim());
|
|
974
|
+
const extract = (p) => (desc.find(l => l.startsWith(p)) || '').replace(p, '').trim();
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
summary,
|
|
978
|
+
subject: extract('Matière :'),
|
|
979
|
+
teacher: extract('Enseignant :'),
|
|
980
|
+
classes: extract('Promotions :').split(', ').map(c => c.trim()),
|
|
981
|
+
type: extract('Salle :') || undefined,
|
|
982
|
+
start,
|
|
983
|
+
end
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (detail === 'list') return { summary, start, end };
|
|
987
|
+
|
|
988
|
+
return { summary: evt.summary, start, end };
|
|
989
|
+
})
|
|
990
|
+
.sort((a, b) => new Date(a.start) - new Date(b.start))
|
|
991
|
+
.filter(e => new Date(e.start) >= new Date());
|
|
992
|
+
|
|
993
|
+
res.jsonResponse(events);
|
|
994
|
+
} catch { res.jsonResponse({ error: 'Failed to parse ICS file.' }); }
|
|
995
|
+
});
|
|
996
|
+
|
|
787
997
|
// Store tic tac toe games
|
|
788
998
|
app.post('/:version/tic-tac-toe', (req, res) => {
|
|
789
999
|
const { username, move, session, game } = req.body;
|
|
@@ -811,15 +1021,21 @@ app.post('/:version/tic-tac-toe', (req, res) => {
|
|
|
811
1021
|
games[game] = games[game] || [];
|
|
812
1022
|
|
|
813
1023
|
const players = [...new Set(games[game].map(play => play.username))];
|
|
814
|
-
if (players.length >= 2 && !players.includes(username)) {
|
|
815
|
-
return res.jsonResponse({ error: 'Game is full, you can only watch.' });
|
|
816
|
-
}
|
|
817
|
-
|
|
1024
|
+
if (players.length >= 2 && !players.includes(username)) return res.jsonResponse({ error: 'Game is full, you can only watch.' });
|
|
818
1025
|
if (games[game].length > 0 && games[game][games[game].length - 1].username === username) return res.jsonResponse({ error: 'Please wait for the other player to make a move.' });
|
|
819
1026
|
if (games[game].some(play => play.move === move)) return res.jsonResponse({ error: 'Move already made. Please choose a different move.' });
|
|
820
1027
|
|
|
821
1028
|
games[game].push(play);
|
|
822
|
-
|
|
1029
|
+
|
|
1030
|
+
const result = checkGame(games[game]);
|
|
1031
|
+
if (result.winner || result.tie) {
|
|
1032
|
+
setTimeout(() => delete games[game], 600000);
|
|
1033
|
+
return res.jsonResponse({
|
|
1034
|
+
message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : 'It\'s a tie.'}`,
|
|
1035
|
+
...result
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (!result.winner && !result.tie) setTimeout(() => delete games[game], 3600000);
|
|
823
1039
|
|
|
824
1040
|
sessions[u] = sessions[u] || { user: session, last: now };
|
|
825
1041
|
sessions[u].last = now;
|
|
@@ -847,10 +1063,13 @@ app.post('/:version/tic-tac-toe/fetch', (req, res) => {
|
|
|
847
1063
|
|
|
848
1064
|
if (!games[ID]) games[ID] = [];
|
|
849
1065
|
|
|
850
|
-
const data = games[ID]
|
|
851
|
-
const
|
|
1066
|
+
const data = games[ID];
|
|
1067
|
+
const last = data.length ? data[data.length - 1].username : null;
|
|
1068
|
+
const players = [...new Set(data.map(p => p.username))];
|
|
1069
|
+
const turn = players.find(p => p !== last);
|
|
1070
|
+
const result = data.length ? checkGame(data) : {};
|
|
852
1071
|
|
|
853
|
-
res.jsonResponse({ game: data, turn, ID });
|
|
1072
|
+
res.jsonResponse({ game: data, turn, ID, ...result });
|
|
854
1073
|
});
|
|
855
1074
|
|
|
856
1075
|
// Generate hash
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "3.2.0",
|
|
3
3
|
"name": "@20syldev/api",
|
|
4
4
|
"description": "Node.js API with multiple features. Check the documentation at https://docs.sylvain.pro",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node app.js",
|
|
9
|
+
"dev": "nodemon app.js",
|
|
9
10
|
"build": "npm install && node app.js",
|
|
11
|
+
"lint": "prettier --write .",
|
|
10
12
|
"upgrade:minor": "npm upgrade",
|
|
11
13
|
"upgrade:major": "npx npm-check-updates -u && npm install",
|
|
12
14
|
"upgrade:build": "npm upgrade && npm install && node app.js"
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
"cors": "latest",
|
|
17
19
|
"dotenv": "latest",
|
|
18
20
|
"express": "latest",
|
|
21
|
+
"ical.js": "latest",
|
|
19
22
|
"mathjs": "latest",
|
|
20
23
|
"node-fetch": "latest",
|
|
21
24
|
"prettier": "latest",
|