@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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/app.js +238 -19
  3. 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
- [![Version](https://custom-icon-badges.demolab.com/badge/Version%20:-v2.9.0-6479ee?logo=api.sylvain.pro&labelColor=23272A)](https://github.com/20syldev/api/releases/latest)
5
+ [![Version](https://custom-icon-badges.demolab.com/badge/Version%20:-v3.2.0-6479ee?logo=api.sylvain.pro&labelColor=23272A)](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](http://nodejs.org) pour utiliser l'**API** :
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.9.0 build
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 express, { urlencoded, json } from 'express';
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).json({
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
- setTimeout(() => { delete games[game]; }, 3600000);
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], last = data.length ? data[data.length - 1].username : null;
851
- const players = [...new Set(data.map(p => p.username))], turn = players.find(p => p !== last);
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.9.0",
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",