@20syldev/api 3.3.5 → 3.3.6

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/app.js CHANGED
@@ -1,1172 +1,1172 @@
1
- import cors from 'cors';
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';
7
- import { randomBytes, getHashes, createHash } from 'crypto';
8
- import { urlencoded, json } from 'express';
9
- import { factorial } from 'mathjs';
10
- import { dirname, join } from 'path';
11
- import { toDataURL } from 'qrcode';
12
- import { fileURLToPath } from 'url';
13
- import { v4 } from 'uuid';
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
- const app = express();
18
-
19
- // Define allowed versions & endpoints for each version
20
- const versions = ['v1', 'v2', 'v3'];
21
- const endpoints = {
22
- v1: ['algorithms', 'captcha', 'color', 'convert', 'domain', 'infos', 'personal', 'qrcode', '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', 'domain', 'hash', 'hyperplanning', 'infos', 'levenshtein', 'personal', 'qrcode', 'tic-tac-toe', 'time', 'token', 'username', 'website']
25
- };
26
-
27
- // Arrowed functions (formatting, math & random)
28
- const formatDate = d => new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
29
- const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
30
- const genID = () => {
31
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
32
- return Array.from(randomBytes(5)).map(b => chars[b % chars.length]).join('');
33
- };
34
- const genIP = () => `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
35
- const genToken = (chars, length) => Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
36
- const random = (arr) => arr[Math.floor(Math.random() * arr.length)];
37
-
38
- // Store data
39
- const logs = [], chat = [], privateChats = {}, sessions = {}, rateLimits = {}, ipLimits = {}, games = {};
40
-
41
- // Define global variables
42
- let contributions, lastFetch = 0, requests = 0, requestLimit, resetTime = Date.now() + 3600000;
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
- }
82
-
83
- // ----------- ----------- MIDDLEWARES SETUP ----------- ----------- //
84
-
85
- dotenv.config();
86
-
87
- // CORS & Express setup
88
- app.set('trust proxy', 1);
89
- app.use(cors({ methods: ['GET', 'POST'] }));
90
- app.use(urlencoded({ extended: true }));
91
- app.use(json());
92
-
93
- // Set favicon for API
94
- app.use('/favicon.ico', express.static(join(__dirname, 'src', 'favicon.ico')));
95
-
96
- // Display robots.txt
97
- app.use('/robots.txt', express.static(join(__dirname, 'robots.txt')));
98
-
99
- // Return formatted JSON
100
- app.use((req, res, next) => {
101
- res.setHeader('Content-Type', 'application/json');
102
- res.jsonResponse = (data) => {
103
- res.send(JSON.stringify(data, null, 2));
104
- };
105
- next();
106
- });
107
-
108
- // Too many requests
109
- app.use((req, res, next) => {
110
- const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
111
- const token = req.headers.authorization?.split(' ')[1] || '';
112
-
113
- const now = Date.now();
114
- const minute = Math.floor(now / 60000) % 60;
115
- const hour = Math.floor(now / 3600000) % 24;
116
-
117
- const business = process.env.BUSINESS_TOKEN_LIST?.split(' ') || [];
118
- const pro = process.env.PRO_TOKEN_LIST?.split(' ') || [];
119
- const advanced = process.env.ADVANCED_TOKEN_LIST?.split(' ') || [];
120
-
121
- if (token && ![...business, ...pro, ...advanced].includes(token) || token === 'undefined') {
122
- return res.status(401).jsonResponse({
123
- message: 'Unauthorized',
124
- error: 'Invalid token.',
125
- status: '401'
126
- });
127
- }
128
-
129
- if (business.includes(token) && !business.includes('undefined')) requestLimit = process.env.BUSINESS_LIMIT;
130
- else if (pro.includes(token) && !pro.includes('undefined')) requestLimit = process.env.PRO_LIMIT;
131
- else if (advanced.includes(token) && !advanced.includes('undefined')) requestLimit = process.env.ADVANCED_LIMIT;
132
- else requestLimit = process.env.DEFAULT_LIMIT;
133
-
134
- if (now > resetTime) requests = 0, resetTime = now + 3600000;
135
- if (++requests > Math.max(process.env.GLOBAL_LIMIT, requestLimit)) {
136
- return res.status(429).jsonResponse({ message: 'Too Many Requests' });
137
- }
138
-
139
- if (['__proto__', 'constructor', 'prototype'].includes(ip)) {
140
- return res.status(400).jsonResponse({ message: 'Invalid IP address' });
141
- }
142
-
143
- if (!ipLimits[ip]) ipLimits[ip] = {};
144
- if (!ipLimits[ip][hour]) ipLimits[ip][hour] = {};
145
- ipLimits[ip][hour][minute] = (ipLimits[ip][hour][minute] || 0) + 1;
146
-
147
- if (ipLimits[ip][hour][minute] > requestLimit) {
148
- return res.status(429).jsonResponse({
149
- message: 'Too Many Requests (IP limited), authenticate to increase the limit',
150
- error: `You have exceeded the limit of ${requestLimit} requests per hour.`,
151
- reset: `Reset in ${((resetTime - now) / 60000).toFixed(0)} minutes.`,
152
- status: '429'
153
- });
154
- }
155
-
156
- Object.keys(ipLimits[ip]).forEach(h => { if (h != hour) delete ipLimits[ip][h]; });
157
-
158
- next();
159
- });
160
-
161
- // Save and send logs
162
- app.use((req, res, next) => {
163
- if (req.method === 'HEAD') return next();
164
- if (req.originalUrl === '/logs') return next();
165
-
166
- const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
167
-
168
- const startTime = Date.now();
169
- const timestamp = new Date().toISOString();
170
- const method = req.method;
171
- const url = req.originalUrl;
172
- const platform = req.headers['sec-ch-ua-platform']?.replace(/"/g, '');
173
-
174
- res.on('finish', () => {
175
- const status = res.statusCode === 304 ? 200 : res.statusCode;
176
- const duration = `${Date.now() - startTime}ms`;
177
-
178
- logs.push({ timestamp, method, url, status, duration, platform });
179
- console.log(`[${new Date().toISOString()}] ${method} ${url} ${res.statusCode} - ${duration} - ${ip}`);
180
- if (logs.length > 1000) logs.shift();
181
- });
182
- next();
183
- });
184
-
185
- // Internal Server Error
186
- app.use((err, req, res, next) => {
187
- console.error(err.stack);
188
- res.status(500).jsonResponse({
189
- message: 'Internal Server Error',
190
- error: err.message,
191
- documentation: 'https://docs.sylvain.pro',
192
- status: '500'
193
- });
194
- });
195
-
196
- // Check if version exists
197
- app.use('/:version', (req, res, next) => {
198
- const { version } = req.params;
199
- const latest = versions[versions.length - 1];
200
- const endpoint = req.originalUrl.split('/').slice(2).join('/');
201
-
202
- if (['latest', 'fr', 'en'].includes(version)) {
203
- return res.redirect(endpoint ? `/${latest}/${endpoint}` : `/${latest}`);
204
- }
205
-
206
- if (!versions.includes(version) && version !== 'logs') {
207
- return res.status(404).jsonResponse({
208
- message: 'Not Found',
209
- error: `Invalid API version (${version}).`,
210
- documentation: `https://docs.sylvain.pro/${versions.at(-1)}`,
211
- status: '404'
212
- });
213
- }
214
- next();
215
- });
216
-
217
- // Check if endpoint exists
218
- app.use('/:version/:endpoint', (req, res, next) => {
219
- const { version, endpoint } = req.params;
220
-
221
- if (!versions.includes(version) || !endpoints[version].includes(endpoint)) {
222
- return res.status(404).jsonResponse({
223
- message: 'Not Found',
224
- error: `Endpoint '${endpoint}' does not exist in ${version}.`,
225
- documentation: `https://docs.sylvain.pro/${versions.at(-1)}`,
226
- status: '404'
227
- });
228
- }
229
- next();
230
- });
231
-
232
- // ----------- ----------- MAIN ENDPOINTS ----------- ----------- //
233
-
234
- // Main route
235
- app.get('/', (req, res) => {
236
- res.setHeader('Content-Type', 'application/json');
237
- res.jsonResponse({
238
- documentation: 'https://docs.sylvain.pro',
239
- latest: 'https://api.sylvain.pro/latest',
240
- logs: 'https://api.sylvain.pro/logs',
241
- versions: {
242
- v1: 'https://api.sylvain.pro/v1',
243
- v2: 'https://api.sylvain.pro/v2',
244
- v3: 'https://api.sylvain.pro/v3'
245
- }
246
- });
247
- });
248
-
249
- // Display v1 endpoints
250
- app.get('/v1', (req, res) => {
251
- res.jsonResponse({
252
- version: 'v1',
253
- documentation: 'https://docs.sylvain.pro/v1',
254
- endpoints: {
255
- get: {
256
- algorithm: '/v1/algorithms?method={algorithm}&value={value}(&value2={value2})',
257
- captcha: '/v1/captcha?text={text}',
258
- color: '/v1/color',
259
- convert: '/v1/convert?value={value}&from={unit}&to={unit}',
260
- domain: '/v1/domain',
261
- infos: '/v1/infos',
262
- personal: '/v1/personal',
263
- qrcode: '/v1/qrcode?url={URL}',
264
- username: '/v1/username'
265
- },
266
- post: {
267
- token: '/v1/token'
268
- }
269
- }
270
- });
271
- });
272
-
273
- // Display v2 endpoints
274
- app.get('/v2', (req, res) => {
275
- res.jsonResponse({
276
- version: 'v2',
277
- documentation: 'https://docs.sylvain.pro/v2',
278
- endpoints: {
279
- get: {
280
- algorithm: '/v2/algorithms?method={algorithm}&value={value}(&value2={value2})',
281
- captcha: '/v2/captcha?text={text}',
282
- chat: '/v2/chat',
283
- color: '/v2/color',
284
- convert: '/v2/convert?value={value}&from={unit}&to={unit}',
285
- domain: '/v2/domain',
286
- infos: '/v2/infos',
287
- personal: '/v2/personal',
288
- qrcode: '/v2/qrcode?url={URL}',
289
- username: '/v2/username'
290
- },
291
- post: {
292
- chat: {
293
- chat: '/v2/chat',
294
- private: '/v2/chat/private'
295
- },
296
- hash: '/v2/hash',
297
- tic_tac_toe: {
298
- tic_tac_toe: '/v2/tic-tac-toe',
299
- fetch: '/v2/tic-tac-toe/fetch'
300
- },
301
- token: '/v2/token'
302
- }
303
- }
304
- });
305
- });
306
-
307
- // Display v3 endpoints
308
- app.get('/v3', (req, res) => {
309
- res.jsonResponse({
310
- version: 'v3',
311
- documentation: 'https://docs.sylvain.pro/v3',
312
- endpoints: {
313
- get: {
314
- algorithm: '/v3/algorithms?method={algorithm}&value={value}(&value2={value2})',
315
- captcha: '/v3/captcha?text={text}',
316
- chat: '/v3/chat',
317
- color: '/v3/color',
318
- convert: '/v3/convert?value={value}&from={unit}&to={unit}',
319
- domain: '/v3/domain',
320
- infos: '/v3/infos',
321
- levenshtein: '/v3/levenshtein?str1={string}&str2={string}',
322
- personal: '/v3/personal',
323
- qrcode: '/v3/qrcode?url={URL}',
324
- time: '/v3/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})',
325
- username: '/v3/username'
326
- },
327
- post: {
328
- chat: {
329
- chat: '/v3/chat',
330
- private: '/v3/chat/private'
331
- },
332
- hash: '/v3/hash',
333
- hyperplanning: '/v3/hyperplanning',
334
- tic_tac_toe: {
335
- tic_tac_toe: '/v3/tic-tac-toe',
336
- fetch: '/v3/tic-tac-toe/fetch'
337
- },
338
- token: '/v3/token'
339
- }
340
- }
341
- });
342
- });
343
-
344
- // Display logs
345
- app.get('/logs', (req, res) => res.jsonResponse(logs));
346
-
347
- // ----------- ----------- GET ENDPOINTS ----------- ----------- //
348
-
349
- // Algorithms
350
- app.get('/:version/algorithms', (req, res) => {
351
- const { method, value, value2 } = req.query;
352
- const { version } = req.params;
353
-
354
- if (!['anagram', 'bubblesort', 'factorial', 'fibonacci', 'gcd', 'isprime', 'palindrome', 'primefactors', 'primelist', 'reverse'].includes(method)) {
355
- return res.jsonResponse({
356
- error: 'Please provide a valid algorithm (?method={algorithm})',
357
- documentation: `https://docs.sylvain.pro/${version}/algorithms`
358
- });
359
- }
360
- if (!value) return res.jsonResponse({ error: 'Please provide a valid value (&value={value})' });
361
-
362
- if (method === 'anagram') {
363
- if (!value2) return res.jsonResponse({ error: 'Please provide a second value (&value2={value})' });
364
- return res.jsonResponse({ answer: value.split('').sort().join('') === value2.split('').sort().join('') });
365
- }
366
-
367
- if (method === 'bubblesort') {
368
- const arr = value.split(',').map(Number);
369
- const n = arr.length;
370
- for (let i = 0; i < n-1; i++) {
371
- for (let j = 0; j < n-i-1; j++) {
372
- if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
373
- }
374
- }
375
- return res.jsonResponse({ answer: arr });
376
- }
377
-
378
- if (method === 'factorial') {
379
- if (isNaN(value) || value < 0 || value > 170) return res.jsonResponse({ error: 'Please provide a valid number between 0 and 170.' });
380
- return res.jsonResponse({ answer: factorial(value) });
381
- }
382
-
383
- if (method === 'fibonacci') {
384
- let fib = [0, 1];
385
- if (isNaN(value) || value < 0 || value > 1000) return res.jsonResponse({ error: 'Please provide a valid number between 0 and 1000.' });
386
- for (let i = 2; i < parseInt(value); i++) fib.push(fib[i - 1] + fib[i - 2]);
387
- return res.jsonResponse({ answer: fib.slice(0, parseInt(value)) });
388
- }
389
-
390
- if (method === 'gcd') {
391
- if (!value2) return res.jsonResponse({ error: 'Please provide a second value (&value2={value})' });
392
- if (isNaN(value) || isNaN(value2)) return res.jsonResponse({ error: 'Invalid numbers.' });
393
- return res.jsonResponse({ answer: gcd(value, value2) });
394
- }
395
-
396
- if (method === 'isprime') {
397
- let isPrime = true;
398
- if (isNaN(value) || value < 1) return res.jsonResponse({ error: 'Please provide a valid number greater than or equal to 1.' });
399
- for (let i = 2; i <= Math.sqrt(value); i++) {
400
- if (value % i === 0) {
401
- isPrime = false;
402
- break;
403
- }
404
- }
405
- return res.jsonResponse({ answer: isPrime });
406
- }
407
-
408
- if (method === 'palindrome') return res.jsonResponse({ answer: value === value.split('').reverse().join('') });
409
-
410
- if (method === 'primefactors') {
411
- let num = value;
412
- let factors = [];
413
- if (isNaN(num) || num < 2 || num > 100000) return res.jsonResponse({ error: 'Please provide a valid number between 2 and 100 000.' });
414
- for (let i = 2; i <= num; i++) {
415
- while (num % i === 0) {
416
- factors.push(i);
417
- num /= i;
418
- }
419
- }
420
- return res.jsonResponse({ answer: factors });
421
- }
422
-
423
- if (method === 'primelist') {
424
- const primes = [];
425
- if (isNaN(value) || value < 2 || value > 10000) return res.jsonResponse({ error: 'Please provide a valid number between 2 and 10 000.' });
426
- for (let i = 2; i <= value; i++) {
427
- let isPrime = true;
428
- for (let j = 2; j <= Math.sqrt(i); j++) {
429
- if (i % j === 0) {
430
- isPrime = false;
431
- break;
432
- }
433
- }
434
- if (isPrime) primes.push(i);
435
- }
436
- return res.jsonResponse({ answer: primes });
437
- }
438
-
439
- if (method === 'reverse') return res.jsonResponse({ answer: value.split('').reverse().join('') });
440
- });
441
-
442
- // Generate captcha
443
- app.get('/:version/captcha', (req, res) => {
444
- const captcha = req.query.text;
445
-
446
- if (!captcha) return res.jsonResponse({ error: 'Please provide a valid argument (?text={text})' });
447
-
448
- const size = 60, font = '60px Comic Sans Ms', width = captcha.length * size, height = 120;
449
- const canvas = createCanvas(width, height), ctx = canvas.getContext('2d');
450
-
451
- ctx.fillStyle = 'white';
452
- ctx.fillRect(0, 0, width, height);
453
-
454
- for (let i = 0; i < 20; i++) {
455
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
456
- ctx.beginPath();
457
- ctx.moveTo(Math.random() * width, Math.random() * height);
458
- ctx.lineTo(Math.random() * width, Math.random() * height);
459
- ctx.lineWidth = Math.random() * 2;
460
- ctx.stroke();
461
- }
462
-
463
- let x = (canvas.width + 20 - width) / 2;
464
- for (let i = 0; i < captcha.length; i++) {
465
- const offsetX = Math.cos(i * 0.3) * 10, y = height / 2.5 + Math.floor(Math.random() * (height / 2));
466
-
467
- ctx.font = font;
468
- ctx.fillStyle = `rgb(${Math.floor(Math.random() * 192)}, ${Math.floor(Math.random() * 192)}, ${Math.floor(Math.random() * 192)})`;
469
-
470
- ctx.save();
471
- ctx.translate(x + size / 2, y);
472
- ctx.rotate((Math.random() - 0.5) * 0.5);
473
- ctx.fillText(captcha[i], -size / 2 + offsetX, 0);
474
- ctx.restore();
475
-
476
- x += size;
477
- }
478
-
479
- for (let i = 0; i < 200; i++) {
480
- ctx.fillStyle = 'black';
481
- ctx.fillRect(Math.floor(Math.random() * width), Math.floor(Math.random() * height), 1.2, 1.2);
482
- }
483
-
484
- res.set('Content-Type', 'image/png');
485
- res.send(canvas.toBuffer('image/png'));
486
- });
487
-
488
- // Display stored data
489
- app.get('/:version/chat', (req, res) => {
490
- if (chat.length > 0) res.jsonResponse(chat);
491
- else res.jsonResponse({ error: 'No messages stored.' });
492
- });
493
-
494
- // GET private chat error
495
- app.get('/:version/chat/private', (req, res) => {
496
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
497
- });
498
-
499
- // Generate color
500
- app.get('/:version/color', (req, res) => {
501
- const r = Math.floor(Math.random() * 256), g = Math.floor(Math.random() * 256), b = Math.floor(Math.random() * 256);
502
- const hsl = (() => {
503
- const r1 = r / 255, g1 = g / 255, b1 = b / 255, max = Math.max(r1, g1, b1), min = Math.min(r1, g1, b1), l = (max + min) / 2;
504
- if (max === min) return [0, 0, l * 100];
505
- const d = max - min, s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
506
- let h = { [r1]: (g1 - b1) / d + (g1 < b1 ? 6 : 0), [g1]: (b1 - r1) / d + 2, [b1]: (r1 - g1) / d + 4 }[max];
507
- return [h * 60 % 360, s * 100, l * 100];
508
- })();
509
- const hsv = (() => {
510
- const max = Math.max(r, g, b), min = Math.min(r, g, b), v = max / 255, s = max ? (max - min) / max : 0;
511
- let h = max === min ? 0 : { [r]: (g - b) / (max - min), [g]: 2 + (b - r) / (max - min), [b]: 4 + (r - g) / (max - min) }[max];
512
- return [h * 60 % 360, s * 100, v * 100];
513
- })();
514
- const hwb = (() => {
515
- const [h] = hsv, whiteness = Math.min(r, g, b) / 255, blackness = 1 - Math.max(r, g, b) / 255;
516
- return [h, whiteness * 100, blackness * 100];
517
- })();
518
- const cmyk = (() => {
519
- const k = 1 - Math.max(r, g, b) / 255, c = (1 - r / 255 - k) / (1 - k) || 0, m = (1 - g / 255 - k) / (1 - k) || 0, y = (1 - b / 255 - k) / (1 - k) || 0;
520
- return [c, m, y, k].map(x => x * 100);
521
- })();
522
- res.jsonResponse({
523
- hex: `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`,
524
- rgb: `rgb(${r}, ${g}, ${b})`,
525
- hsl: `hsl(${hsl[0].toFixed(1)}, ${hsl[1].toFixed(1)}%, ${hsl[2].toFixed(1)}%)`,
526
- hsv: `hsv(${hsv[0].toFixed(1)}, ${hsv[1].toFixed(1)}%, ${hsv[2].toFixed(1)}%)`,
527
- hwb: `hwb(${hwb[0].toFixed(1)}, ${hwb[1].toFixed(1)}%, ${hwb[2].toFixed(1)}%)`,
528
- cmyk: `cmyk(${cmyk.map(x => x.toFixed(1)).join('%, ')}%)`
529
- });
530
- });
531
-
532
- // Convert units
533
- app.get('/:version/convert', (req, res) => {
534
- const { value, from, to } = req.query;
535
-
536
- if (!value || isNaN(value)) return res.jsonResponse({ error: 'Please provide a valid value (?value={value})' });
537
- if (!from) return res.jsonResponse({ error: 'Please provide a valid source unit (&from={unit})' });
538
- if (!to) return res.jsonResponse({ error: 'Please provide a valid target unit (&to={unit})' });
539
-
540
- const conversions = {
541
- celsius: { fahrenheit: (val) => (val * 9) / 5 + 32, kelvin: (val) => val + 273.15 },
542
- fahrenheit: { celsius: (val) => ((val - 32) * 5) / 9, kelvin: (val) => ((val - 32) * 5) / 9 + 273.15 },
543
- kelvin: { celsius: (val) => val - 273.15, fahrenheit: (val) => ((val - 273.15) * 9) / 5 + 32 },
544
- };
545
-
546
- const convert = conversions[from.toLowerCase()]?.[to.toLowerCase()];
547
- if (!convert) return res.jsonResponse({ error: 'Invalid conversion units.' });
548
-
549
- res.jsonResponse({ from, to, value: parseFloat(value), result: convert(parseFloat(value)) });
550
- });
551
-
552
- // Generate domain informations
553
- app.get('/:version/domain', (req, res) => {
554
- const subdomains = ['fr.', 'en.', 'docs.', 'api.', 'projects.', 'app.', 'web.', 'info.', 'dev.', 'shop.', 'blog.', 'support.', 'mail.', 'forum.'];
555
- const domains = ['example', 'site', 'test', 'demo', 'page', 'store', 'portfolio', 'platform', 'hub', 'network', 'service', 'cloud', 'solutions', 'company'];
556
- const tlds = ['.com', '.fr', '.eu', '.dev', '.net', '.org', '.io', '.tech', '.biz', '.info', '.co', '.app', '.store', '.online', '.shop', '.tv'];
557
-
558
- const domain = `${random(domains)}${random(tlds)}`;
559
- const fulldomain = `${random(subdomains)}${domain}`;
560
-
561
- const ips = Array.from({ length: Math.floor(Math.random() * 3) + 1 }, genIP);
562
- const dns = Array.from({ length: Math.floor(Math.random() * 5) + 1 }, genIP);
563
-
564
- res.jsonResponse({
565
- domain,
566
- full_domain: fulldomain,
567
- ip_address: ips,
568
- ssl_certified: Math.random() > 0.5,
569
- hosting_provider: random(['AWS', 'Bluehost', 'DigitalOcean', 'GitHub', 'HostGator', 'Render', 'SiteGround']),
570
- dns_servers: dns,
571
- dns_provider: random(['AWS Route 53', 'Cloudflare', 'GoDaddy', 'Google DNS', 'Namecheap']),
572
- traffic: `${Math.floor(Math.random() * 10000)} visits/day`,
573
- seo_score: Math.floor(Math.random() * 100),
574
- page_rank: Math.floor(Math.random() * 10),
575
- country: random(['Australia', 'Canada', 'France', 'Germany', 'India', 'Japan', 'UK', 'USA']),
576
- website_type: random(['Blog', 'Community', 'Corporate', 'Educational', 'E-commerce', 'Personal', 'Portfolio']),
577
- random_name: domain.split('.')[0],
578
- random_subdomain: fulldomain.split('.')[0],
579
- random_tld: domain.split('.').pop(),
580
- backlinks_count: Math.floor(Math.random() * 1000),
581
- creation_date: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString(),
582
- expiration_date: new Date(Date.now() + Math.floor(Math.random() * 10000000000)).toISOString(),
583
- });
584
- });
585
-
586
- // GET planning error
587
- app.get('/:version/hyperplanning', (req, res) => {
588
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
589
- });
590
-
591
- // GET hash error
592
- app.get('/:version/hash', (req, res) => {
593
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
594
- });
595
-
596
- // Display API informations
597
- app.get('/:version/infos', (req, res) => {
598
- res.jsonResponse({
599
- endpoints: endpoints[versions.at(-1)].length,
600
- last_version: versions.at(-1),
601
- documentation: 'https://docs.sylvain.pro',
602
- github: 'https://github.com/20syldev/api',
603
- creation: 'November 25th 2024',
604
- });
605
- });
606
-
607
- // Calculate Levenshtein distance
608
- app.get('/:version/levenshtein', (req, res) => {
609
- const { str1, str2 } = req.query;
610
-
611
- if (!str1 || typeof str1 !== 'string') return res.jsonResponse({ error: 'Please provide a first string (?str1={string})' });
612
- if (!str2 || typeof str2 !== 'string') return res.jsonResponse({ error: 'Please provide a second string (&str2={string})' });
613
-
614
- if (str1.length > 1000) return res.jsonResponse({ error: 'First string exceeds 1000 characters.' });
615
- if (str2.length > 1000) return res.jsonResponse({ error: 'Second string exceeds 1000 characters.' });
616
-
617
- const lev = (a, b) => {
618
- const m = Array.from({ length: a.length + 1 }, (_, i) => [i]);
619
- for (let j = 0; j <= b.length; j++) m[0][j] = j;
620
- for (let i = 1; i <= a.length; i++)
621
- for (let j = 1; j <= b.length; j++)
622
- 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]));
623
-
624
- return m[a.length][b.length];
625
- };
626
-
627
- res.jsonResponse({ str1, str2, distance: lev(str1, str2) });
628
- });
629
-
630
- // Generate personal data
631
- app.get('/:version/personal', (req, res) => {
632
- const people = [
633
- { name: 'John Doe', social: 'john_doe', email: 'john@example.com', country: 'US' },
634
- { name: 'Jane Martin', social: 'jane_martin', email: 'jane@example.com', country: 'FR' },
635
- { name: 'Michael Johnson', social: 'mike_johnson', email: 'michael@example.com', country: 'UK' },
636
- { name: 'Emily Davis', social: 'emily_davis', email: 'emily@example.com', country: 'ES' },
637
- { name: 'Alexis Barbos', social: 'alexis_barbos', email: 'alexis@example.com', country: 'DE' },
638
- { name: 'Sarah Williams', social: 'sarah_williams', email: 'sarah@example.com', country: 'IT' },
639
- { name: 'Daniel Brown', social: 'daniel_brown', email: 'daniel@example.com', country: 'JP' },
640
- { name: 'Sophia Wilson', social: 'sophia_wilson', email: 'sophia@example.com', country: 'BR' },
641
- { name: 'James Taylor', social: 'james_taylor', email: 'james@example.com', country: 'CA' },
642
- { name: 'Olivia Thomas', social: 'olivia_thomas', email: 'olivia@example.com', country: 'AU' }
643
- ];
644
-
645
- const countries = {
646
- US: { tel: '123-456-7890', code: '1', lang: 'English' },
647
- FR: { tel: '06 78 90 12 34', code: '33', lang: 'French' },
648
- UK: { tel: '7911 123456', code: '44', lang: 'English' },
649
- ES: { tel: '678 901 234', code: '34', lang: 'Spanish' },
650
- DE: { tel: '163 555 1584', code: '49', lang: 'German' },
651
- IT: { tel: '345 678 9012', code: '39', lang: 'Italian' },
652
- JP: { tel: '080-1234-5678', code: '81', lang: 'Japanese' },
653
- BR: { tel: '(11) 98765-4321', code: '55', lang: 'Portuguese' },
654
- CA: { tel: '416-123-4567', code: '1', lang: 'English' },
655
- AU: { tel: '0412 345 678', code: '61', lang: 'English' }
656
- };
657
-
658
- const jobs = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Doctor'];
659
- const hobbies = ['Reading', 'Traveling', 'Gaming', 'Cooking', 'Fitness', 'Music', 'Photography', 'Writing'];
660
- const cities = ['New York', 'Paris', 'London', 'Madrid', 'Berlin', 'Rome', 'Tokyo', 'Los Angeles', 'Sydney', 'São Paulo', 'Toronto'];
661
- const streets = ['Main St', '2nd Ave', 'Broadway', 'Park Lane', 'Elm St', 'Sunset Blvd', 'Maple St', 'Highland Rd'];
662
-
663
- const card = Array.from({ length: 4 }, () => Math.floor(Math.random() * 9000) + 1000).join(' ');
664
- const cvc = Math.floor(Math.random() * 900) + 100;
665
- const expiration = `${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}/${(new Date().getFullYear() + Math.floor(Math.random() * 3)).toString().slice(-2)}`;
666
-
667
- const person = random(people);
668
- const social = person.social;
669
- const country = person.country;
670
- const phone = countries[country].tel;
671
- const lang = countries[country].lang;
672
-
673
- const age = Math.floor(Math.random() * 50) + 18;
674
- const birthday = new Date(Date.now() - Math.floor((Math.random() * 50 + 18) * 365.25 * 24 * 60 * 60 * 1000)).toISOString();
675
-
676
- let emergencyContacts = [], yearIncome = Math.floor(Math.random() * 100000), subscriptions = [], pets = [], vehicles = [];
677
- let civilStatus = 'Single';
678
- let children = 0;
679
-
680
- if (age >= 21 && Math.random() > 0.7) civilStatus = 'Married';
681
-
682
- if (civilStatus === 'Married' && age >= 25) children = Math.floor(Math.random() * 4);
683
-
684
- while (emergencyContacts.length < Math.floor(Math.random() * 3) + 1) {
685
- let emergencyContact = random(people);
686
- while (emergencyContact.email === person.email || emergencyContacts.some(e => e.email === emergencyContact.email)) {
687
- emergencyContact = random(people);
688
- }
689
- emergencyContacts.push({
690
- name: emergencyContact.name,
691
- relationship: random(['Spouse', 'Parent', 'Sibling', 'Friend']),
692
- phone: `+${countries[country].code} ${countries[country].tel}`
693
- });
694
- }
695
-
696
- while (subscriptions.length < Math.floor(Math.random() * 3) + 1) {
697
- let subscription = random(['Netflix', 'Spotify', 'Amazon Prime', 'Disney+', 'Hulu']);
698
- if (!subscriptions.includes(subscription)) subscriptions.push(subscription);
699
- }
700
-
701
- while (pets.length < Math.floor(Math.random() * 3) + 1) {
702
- let pet = random(['Dog', 'Cat', 'Fish', 'Bird', 'None']);
703
- if (!pets.includes(pet)) pets.push(pet);
704
- }
705
-
706
- while (vehicles.length < Math.floor(Math.random() * 3) + 1) {
707
- let vehicle = random(['Car', 'Bike', 'Motorcycle', 'Bus', 'None']);
708
- if (!vehicles.includes(vehicle)) vehicles.push(vehicle);
709
- }
710
-
711
- res.jsonResponse({
712
- name: person.name,
713
- email: person.email,
714
- localisation: country,
715
- phone: `+${countries[country].code} ${phone}`,
716
- job: random(jobs),
717
- hobbies: random(hobbies),
718
- language: lang,
719
- card,
720
- cvc,
721
- expiration,
722
- address: `${Math.floor(Math.random() * 9999)} ${random(streets)}, ${random(cities)}`,
723
- birthday,
724
- civil_status: civilStatus,
725
- children,
726
- vehicle: vehicles,
727
- social_profiles: {
728
- twitter: `@${social}`,
729
- facebook: `facebook.com/${social}`,
730
- linkedin: `linkedin.com/in/${social}`,
731
- instagram: `instagram.com/${social}`
732
- },
733
- year_income: `${yearIncome} USD/year`,
734
- month_income: `${(yearIncome / 12).toFixed(2)} USD/month`,
735
- education: random(['High School', 'Bachelor\'s', 'Master\'s', 'PhD']),
736
- work_experience: `${Math.floor(Math.random() * 20)} years`,
737
- health_status: random(['Healthy', 'Minor Issues', 'Chronic Conditions']),
738
- emergency_contacts: emergencyContacts,
739
- subscriptions,
740
- pets,
741
- });
742
- });
743
-
744
- // Generate QR Code
745
- app.get('/:version/qrcode', async (req, res) => {
746
- const { url } = req.query;
747
-
748
- if (!url) return res.jsonResponse({ error: 'Please provide a valid url (?url={URL})' });
749
-
750
- try { res.jsonResponse({ qr: await toDataURL(url) }); }
751
- catch { res.jsonResponse({ error: 'Error generating QR code.' }); }
752
- });
753
-
754
- // GET tic-tac-toe game error
755
- app.get('/:version/tic-tac-toe', (req, res) => {
756
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
757
- });
758
-
759
- // GET tic-tac-toe fetch error
760
- app.get('/:version/tic-tac-toe/fetch', (req, res) => {
761
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
762
- });
763
-
764
- // Display or generate time informations
765
- app.get('/:version/time', (req, res) => {
766
- const { type = 'live', start, end, format, timezone } = req.query;
767
-
768
- const validFormats = ['iso', 'utc', 'timestamp', 'locale', 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'second', 'ms', 'dayOfWeek', 'dayOfYear', 'weekNumber', 'timezone', 'timezoneOffset'];
769
- const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
770
-
771
- if (type !== 'live' && type !== 'random') return res.jsonResponse({ error: 'Please provide a valid type (?type={type})' });
772
- if (start && !Date.parse(start)) return res.jsonResponse({ error: 'Please provide a valid start date (?start={YYYY-MM-DD})' });
773
- if (end && !Date.parse(end)) return res.jsonResponse({ error: 'Please provide a valid end date (?end={YYYY-MM-DD})' });
774
- if (format && !validFormats.includes(format)) return res.jsonResponse({ error: 'Please provide a valid format (?format={format})' });
775
- if (timezone && !validTimezones.includes(timezone)) return res.jsonResponse({ error: 'Please provide a valid timezone (?timezone={timezone})' });
776
-
777
- const getTimeFormats = (date, timezoneOption) => {
778
- return {
779
- iso: date.toISOString(),
780
- utc: date.toUTCString(),
781
- timestamp: date.getTime(),
782
- locale: date.toLocaleString('en-US', { timeZone: timezoneOption, timeZoneName: 'long' }),
783
- date: date.toLocaleDateString('en-US', { timeZone: timezoneOption }),
784
- time: date.toLocaleTimeString('en-US', { timeZone: timezoneOption }),
785
- year: date.getFullYear(),
786
- month: date.getMonth() + 1,
787
- day: date.getDate(),
788
- hour: date.getHours(),
789
- minute: date.getMinutes(),
790
- second: date.getSeconds(),
791
- ms: date.getMilliseconds(),
792
- dayOfWeek: date.getDay(),
793
- dayOfYear: Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000),
794
- weekNumber: Math.ceil((((date - new Date(date.getFullYear(), 0, 0)) / 86400000) + 1) / 7),
795
- timezone: timezoneOption,
796
- timezoneOffset: date.getTimezoneOffset()
797
- };
798
- };
799
-
800
- if (type === 'random') {
801
- const startDate = new Date(start || '1900-01-01').getTime();
802
- const endDate = new Date(end || '2100-12-31').getTime();
803
- const randomDate = new Date(start ? startDate : startDate + Math.random() * (endDate - startDate));
804
- const timezoneOption = timezone || validTimezones[Math.floor(Math.random() * 5)];
805
- const formats = getTimeFormats(randomDate, timezoneOption);
806
-
807
- return res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
808
- }
809
-
810
- const now = new Date();
811
- const timezoneOption = timezone || 'UTC';
812
- const formats = getTimeFormats(now, timezoneOption);
813
-
814
- res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
815
- });
816
-
817
- // GET token error
818
- app.get('/:version/token', (req, res) => {
819
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
820
- });
821
-
822
- // Generate username
823
- app.get('/:version/username', (req, res) => {
824
- const adj = ['Happy', 'Silly', 'Clever', 'Creative', 'Brave', 'Gentle', 'Kind', 'Funny', 'Wise', 'Charming', 'Sincere', 'Resourceful', 'Patient', 'Energetic', 'Adventurous', 'Ambitious', 'Courageous', 'Courteous', 'Determined'];
825
- const ani = ['Cat', 'Dog', 'Tiger', 'Elephant', 'Monkey', 'Penguin', 'Dolphin', 'Lion', 'Bear', 'Fox', 'Owl', 'Giraffe', 'Zebra', 'Koala', 'Rabbit', 'Squirrel', 'Panda', 'Horse', 'Wolf', 'Eagle'];
826
- const job = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Chef', 'Doctor', 'Teacher', 'Lawyer', 'Entrepreneur', 'Actor', 'Dancer', 'Photographer', 'Architect', 'Pilot', 'Designer', 'Journalist', 'Veterinarian'];
827
-
828
- const nombre = Math.floor(Math.random() * 100);
829
- const choix = {
830
- adj_num: () => random(adj) + nombre,
831
- ani_num: () => random(ani) + nombre,
832
- pro_num: () => random(job) + nombre,
833
- adj_ani: () => random(adj) + random(ani),
834
- adj_ani_num: () => random(adj) + random(ani) + nombre,
835
- adj_pro: () => random(adj) + random(job),
836
- pro_ani: () => random(job) + random(ani),
837
- pro_ani_num: () => random(job) + random(ani) + nombre
838
- };
839
-
840
- const username = choix[random(Object.keys(choix))]();
841
- res.jsonResponse({ adjective: adj, animal: ani, job, number: nombre, username });
842
- });
843
-
844
- // Display informations for owner's website
845
- app.get('/:version/website', async (req, res) => {
846
- const currentTime = Date.now();
847
-
848
- if (currentTime - lastFetch >= 10 * 60 * 1000) {
849
- try {
850
- const username = '20syldev';
851
- const token = process.env.STATS5;
852
- const today = new Date().toISOString().split('T')[0];
853
- const monthFirst = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
854
- const lastYear = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split('T')[0];
855
-
856
- const query = `
857
- {
858
- user(login: "${username}") {
859
- contributionsCollection(from: "${today}T00:00:00Z") {
860
- contributionCalendar {
861
- totalContributions
862
- }
863
- }
864
- contributions_month: contributionsCollection(from: "${monthFirst}T00:00:00Z") {
865
- contributionCalendar {
866
- totalContributions
867
- }
868
- }
869
- contributions_year: contributionsCollection(from: "${lastYear}T00:00:00Z") {
870
- contributionCalendar {
871
- totalContributions
872
- }
873
- }
874
- }
875
- }`;
876
-
877
- const apiResponse = await fetch('https://api.github.com/graphql', {
878
- method: 'POST',
879
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
880
- body: JSON.stringify({ query })
881
- });
882
-
883
- if (!apiResponse.ok) throw new Error('Error fetching data.');
884
-
885
- const data = await apiResponse.json();
886
- const user = data?.data?.user;
887
-
888
- contributions = {
889
- today: user?.contributionsCollection?.contributionCalendar?.totalContributions || 0,
890
- month: user?.contributions_month?.contributionCalendar?.totalContributions || 0,
891
- year: user?.contributions_year?.contributionCalendar?.totalContributions || 0,
892
- };
893
-
894
- lastFetch = currentTime;
895
- } catch { contributions = { today: 0, month: 0, year: 0 }; }
896
- }
897
-
898
- res.jsonResponse({
899
- versions: {
900
- api: process.env.API,
901
- cdn: process.env.CDN,
902
- coop_api: process.env.COOP_API,
903
- coop_status: process.env.COOP_STATUS,
904
- chat: process.env.CHAT,
905
- digit: process.env.DIGIT,
906
- doc_coopbot: process.env.DOC_COOPBOT,
907
- docs: process.env.DOCS,
908
- donut: process.env.DONUT,
909
- drawio_plugin: process.env.DRAWIO_PLUGIN,
910
- flowers: process.env.FLOWERS,
911
- gemsync: process.env.GEMSYNC,
912
- gitsite: process.env.GITSITE,
913
- logs: process.env.LOGS,
914
- logvault: process.env.LOGVAULT,
915
- minify: process.env.MINIFY,
916
- morpion: process.env.MORPION,
917
- nitrogen: process.env.NITROGEN,
918
- old_database: process.env.OLD_DATABASE,
919
- php: process.env.PHP,
920
- ping: process.env.PING,
921
- portfolio: process.env.PORTFOLIO,
922
- python_api: process.env.PYTHON_API,
923
- readme: process.env.README,
924
- terminal: process.env.TERMINAL,
925
- wrkit: process.env.WRKIT,
926
- zpki: process.env.ZPKI
927
- },
928
- patched_projects: process.env.PATCH?.split(' ') || [],
929
- updated_projects: process.env.RECENT?.split(' ') || [],
930
- new_projects: process.env.NEW?.split(' ') || [],
931
- sub_domains: process.env.DOMAINS?.split(' ') || [],
932
- stats: {
933
- os: process.env.STATS1,
934
- front: process.env.STATS2,
935
- back: process.env.STATS3,
936
- projects: process.env.STATS4,
937
- today: contributions.today.toString(),
938
- this_month: contributions.month.toString(),
939
- last_year: contributions.year.toString(),
940
- },
941
- notif_tag: process.env.TAG,
942
- active: process.env.ACTIVE
943
- });
944
- });
945
-
946
- // ----------- ----------- POST ENDPOINTS ----------- ----------- //
947
-
948
- // Store chat messages
949
- app.post('/:version/chat', (req, res) => {
950
- const { username, message, timestamp, session, token } = req.body;
951
-
952
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
953
- if (!message) return res.jsonResponse({ error: 'Please provide a message (&message={message})' });
954
- if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
955
-
956
- const u = username.toLowerCase(), now = Date.now();
957
- const msg = { username, message, timestamp: timestamp || new Date().toISOString() };
958
-
959
- rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
960
- if (rateLimits[u].length > 50) {
961
- const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
962
- return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
963
- }
964
- rateLimits[u].push(now);
965
-
966
- if (sessions[u] && sessions[u].user !== session) return res.jsonResponse({ error: 'Session ID mismatch' });
967
-
968
- if (token) {
969
- privateChats[token] = privateChats[token] || [];
970
- privateChats[token].push(msg);
971
- setTimeout(() => { delete privateChats[token]; }, 3600000);
972
- } else {
973
- chat.push(msg);
974
- setTimeout(() => chat.splice(chat.indexOf(msg), 1), 3600000);
975
- }
976
-
977
- sessions[u] = sessions[u] || { user: session, last: now };
978
- sessions[u].last = now;
979
-
980
- setTimeout(() => { if (now - sessions[u].last >= 3600000) delete sessions[u]; }, 3600000);
981
-
982
- res.jsonResponse({ message: 'Message sent successfully' });
983
- });
984
-
985
- // Display a private chat with a token
986
- app.post('/:version/chat/private', (req, res) => {
987
- const { username, token } = req.body;
988
-
989
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
990
- if (!token) return res.jsonResponse({ error: 'Please provide a valid token (&token={key}).' });
991
-
992
- const u = username.toLowerCase(), now = Date.now();
993
-
994
- rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
995
- if (rateLimits[u].length > 50) {
996
- const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
997
- return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
998
- }
999
- rateLimits[u].push(now);
1000
-
1001
- if (privateChats[token]) return res.jsonResponse(privateChats[token]);
1002
-
1003
- return res.jsonResponse({ error: 'Invalid or expired token.' });
1004
- });
1005
-
1006
- // Generate hash
1007
- app.post('/:version/hash', (req, res) => {
1008
- const { text, method } = req.body;
1009
- const { version } = req.params;
1010
-
1011
- if (!text) return res.jsonResponse({ error: 'Please provide a text (?text={text})' });
1012
- if (!method) return res.jsonResponse({
1013
- error: 'Please provide a valid hash algorithm (?method={algorithm})',
1014
- documentation: `https://docs.sylvain.pro/${version}/hash`
1015
- });
1016
-
1017
- const methods = getHashes();
1018
- if (!methods.includes(method)) return res.jsonResponse({ error: `Unsupported method. Use one of: ${methods.join(', ')}` });
1019
-
1020
- const hash = createHash(method).update(text).digest('hex');
1021
- res.jsonResponse({ method, hash });
1022
- });
1023
-
1024
- // Display a planning from an ICS file
1025
- app.post('/:version/hyperplanning', async (req, res) => {
1026
- const { url, detail } = req.body;
1027
-
1028
- if (!url) return res.jsonResponse({ error: 'Please provide a valid ICS file URL.' });
1029
-
1030
- try {
1031
- const response = await fetch(url);
1032
- if (!response.ok || !(response.headers.get('content-type') || '').includes('text/calendar')) return res.jsonResponse({ error: 'Invalid ICS file format.' });
1033
-
1034
- const events = new ical.Component(ical.parse(await response.text()))
1035
- .getAllSubcomponents('vevent')
1036
- .map(e => {
1037
- const evt = new ical.Event(e);
1038
- const summary = evt.summary.split(' ').filter(part => part !== '-');
1039
- const start = formatDate(evt.startDate.toJSDate());
1040
- const end = formatDate(evt.endDate.toJSDate());
1041
-
1042
- if (detail === 'full') {
1043
- const desc = evt.description.split('\n').map(l => l.trim());
1044
- const extract = (p) => (desc.find(l => l.startsWith(p)) || '').replace(p, '').trim();
1045
-
1046
- return {
1047
- summary,
1048
- subject: extract('Matière :'),
1049
- teacher: extract('Enseignant :'),
1050
- classes: extract('Promotions :').split(', ').map(c => c.trim()),
1051
- type: extract('Salle :') || undefined,
1052
- start,
1053
- end
1054
- };
1055
- }
1056
- if (detail === 'list') return { summary, start, end };
1057
-
1058
- return { summary: evt.summary, start, end };
1059
- })
1060
- .sort((a, b) => new Date(a.start) - new Date(b.start))
1061
- .filter(e => new Date(e.end) >= new Date());
1062
-
1063
- res.jsonResponse(events);
1064
- } catch { res.jsonResponse({ error: 'Failed to parse ICS file.' }); }
1065
- });
1066
-
1067
- // Store tic tac toe games
1068
- app.post('/:version/tic-tac-toe', (req, res) => {
1069
- const { username, move, session, game } = req.body;
1070
-
1071
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
1072
- if (!move) return res.jsonResponse({ error: 'Please provide a valid move (&move={move})' });
1073
- if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
1074
- if (!game) return res.jsonResponse({ error: 'Please provide a valid game ID (&game={ID})' });
1075
-
1076
- const u = username.toLowerCase(), now = Date.now();
1077
- const play = { username, move, session };
1078
- const validMoves = ['1-1', '1-2', '1-3', '2-1', '2-2', '2-3', '3-1', '3-2', '3-3'];
1079
-
1080
- if (!validMoves.includes(move)) return res.jsonResponse({ error: 'Invalid move. Please provide a valid move (e.g., 1-1, 2-2, 3-3).' });
1081
-
1082
- rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
1083
- if (rateLimits[u].length > 50) {
1084
- const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
1085
- return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
1086
- }
1087
- rateLimits[u].push(now);
1088
-
1089
- if (sessions[u] && sessions[u].user !== session) return res.jsonResponse({ error: 'Session ID mismatch' });
1090
-
1091
- games[game] = games[game] || [];
1092
-
1093
- const players = [...new Set(games[game].map(play => play.username))];
1094
- if (players.length >= 2 && !players.includes(username)) return res.jsonResponse({ error: 'Game is full, you can only watch.' });
1095
- 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.' });
1096
- if (games[game].some(play => play.move === move)) return res.jsonResponse({ error: 'Move already made. Please choose a different move.' });
1097
-
1098
- games[game].push(play);
1099
-
1100
- const result = checkGame(games[game]);
1101
- if (result.winner || result.tie) {
1102
- setTimeout(() => delete games[game], 600000);
1103
- return res.jsonResponse({
1104
- message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : 'It\'s a tie.'}`,
1105
- ...result
1106
- });
1107
- }
1108
- if (!result.winner && !result.tie) setTimeout(() => delete games[game], 3600000);
1109
-
1110
- sessions[u] = sessions[u] || { user: session, last: now };
1111
- sessions[u].last = now;
1112
-
1113
- setTimeout(() => { if (now - sessions[u].last >= 3600000) delete sessions[u]; }, 3600000);
1114
-
1115
- res.jsonResponse({ message: 'Move sent successfully' });
1116
- });
1117
-
1118
- // Display a tic tac toe game with a token
1119
- app.post('/:version/tic-tac-toe/fetch', (req, res) => {
1120
- const { username, game } = req.body;
1121
-
1122
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
1123
-
1124
- const ID = game || genID();
1125
- const u = username.toLowerCase(), now = Date.now();
1126
-
1127
- rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
1128
- if (rateLimits[u].length > 50) {
1129
- const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
1130
- return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
1131
- }
1132
- rateLimits[u].push(now);
1133
-
1134
- if (!games[ID]) games[ID] = [];
1135
-
1136
- const data = games[ID];
1137
- const last = data.length ? data[data.length - 1].username : null;
1138
- const players = [...new Set(data.map(p => p.username))];
1139
- const turn = players.find(p => p !== last);
1140
- const result = data.length ? checkGame(data) : {};
1141
-
1142
- res.jsonResponse({ game: data, turn, ID, ...result });
1143
- });
1144
-
1145
- // Generate Token
1146
- app.post('/:version/token', (req, res) => {
1147
- let { len, type } = req.body;
1148
-
1149
- len = parseInt(len || 24, 10);
1150
- type = type ? type.toLowerCase() : 'alpha';
1151
-
1152
- if (isNaN(len) || len < 0) return res.jsonResponse({ error: 'Invalid number.' });
1153
- if (len > 4096) return res.jsonResponse({ error: 'Length cannot exceed 4096.' });
1154
- if (len < 12) return res.jsonResponse({ error: 'Length cannot be less than 12.' });
1155
-
1156
- const token = {
1157
- alpha: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
1158
- alphanum: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len),
1159
- base64: randomBytes(len).toString('base64').slice(0, len),
1160
- hex: randomBytes(len).toString('hex').slice(0, len),
1161
- num: genToken('0123456789', len),
1162
- punct: genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
1163
- urlsafe: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
1164
- uuid: v4().replace(/-/g, '').slice(0, len)
1165
- }[type] || genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len);
1166
-
1167
- res.jsonResponse({ token });
1168
- });
1169
-
1170
- // ----------- ----------- SERVER SETUP ----------- ----------- //
1171
-
1172
- app.listen(3000, () => console.log('API is running on\n - http://127.0.0.1:3000\n - http://localhost:3000'));
1
+ import cors from 'cors';
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';
7
+ import { randomBytes, getHashes, createHash } from 'crypto';
8
+ import { urlencoded, json } from 'express';
9
+ import { factorial } from 'mathjs';
10
+ import { dirname, join } from 'path';
11
+ import { toDataURL } from 'qrcode';
12
+ import { fileURLToPath } from 'url';
13
+ import { v4 } from 'uuid';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const app = express();
18
+
19
+ // Define allowed versions & endpoints for each version
20
+ const versions = ['v1', 'v2', 'v3'];
21
+ const endpoints = {
22
+ v1: ['algorithms', 'captcha', 'color', 'convert', 'domain', 'infos', 'personal', 'qrcode', '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', 'domain', 'hash', 'hyperplanning', 'infos', 'levenshtein', 'personal', 'qrcode', 'tic-tac-toe', 'time', 'token', 'username', 'website']
25
+ };
26
+
27
+ // Arrowed functions (formatting, math & random)
28
+ const formatDate = d => new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
29
+ const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
30
+ const genID = () => {
31
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
32
+ return Array.from(randomBytes(5)).map(b => chars[b % chars.length]).join('');
33
+ };
34
+ const genIP = () => `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
35
+ const genToken = (chars, length) => Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
36
+ const random = (arr) => arr[Math.floor(Math.random() * arr.length)];
37
+
38
+ // Store data
39
+ const logs = [], chat = [], privateChats = {}, sessions = {}, rateLimits = {}, ipLimits = {}, games = {};
40
+
41
+ // Define global variables
42
+ let contributions, lastFetch = 0, requests = 0, requestLimit, resetTime = Date.now() + 3600000;
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
+ }
82
+
83
+ // ----------- ----------- MIDDLEWARES SETUP ----------- ----------- //
84
+
85
+ dotenv.config();
86
+
87
+ // CORS & Express setup
88
+ app.set('trust proxy', 1);
89
+ app.use(cors({ methods: ['GET', 'POST'] }));
90
+ app.use(urlencoded({ extended: true }));
91
+ app.use(json());
92
+
93
+ // Set favicon for API
94
+ app.use('/favicon.ico', express.static(join(__dirname, 'src', 'favicon.ico')));
95
+
96
+ // Display robots.txt
97
+ app.use('/robots.txt', express.static(join(__dirname, 'robots.txt')));
98
+
99
+ // Return formatted JSON
100
+ app.use((req, res, next) => {
101
+ res.setHeader('Content-Type', 'application/json');
102
+ res.jsonResponse = (data) => {
103
+ res.send(JSON.stringify(data, null, 2));
104
+ };
105
+ next();
106
+ });
107
+
108
+ // Too many requests
109
+ app.use((req, res, next) => {
110
+ const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
111
+ const token = req.headers.authorization?.split(' ')[1] || '';
112
+
113
+ const now = Date.now();
114
+ const minute = Math.floor(now / 60000) % 60;
115
+ const hour = Math.floor(now / 3600000) % 24;
116
+
117
+ const business = process.env.BUSINESS_TOKEN_LIST?.split(' ') || [];
118
+ const pro = process.env.PRO_TOKEN_LIST?.split(' ') || [];
119
+ const advanced = process.env.ADVANCED_TOKEN_LIST?.split(' ') || [];
120
+
121
+ if (token && ![...business, ...pro, ...advanced].includes(token) || token === 'undefined') {
122
+ return res.status(401).jsonResponse({
123
+ message: 'Unauthorized',
124
+ error: 'Invalid token.',
125
+ status: '401'
126
+ });
127
+ }
128
+
129
+ if (business.includes(token) && !business.includes('undefined')) requestLimit = process.env.BUSINESS_LIMIT;
130
+ else if (pro.includes(token) && !pro.includes('undefined')) requestLimit = process.env.PRO_LIMIT;
131
+ else if (advanced.includes(token) && !advanced.includes('undefined')) requestLimit = process.env.ADVANCED_LIMIT;
132
+ else requestLimit = process.env.DEFAULT_LIMIT;
133
+
134
+ if (now > resetTime) requests = 0, resetTime = now + 3600000;
135
+ if (++requests > Math.max(process.env.GLOBAL_LIMIT, requestLimit)) {
136
+ return res.status(429).jsonResponse({ message: 'Too Many Requests' });
137
+ }
138
+
139
+ if (['__proto__', 'constructor', 'prototype'].includes(ip)) {
140
+ return res.status(400).jsonResponse({ message: 'Invalid IP address' });
141
+ }
142
+
143
+ if (!ipLimits[ip]) ipLimits[ip] = {};
144
+ if (!ipLimits[ip][hour]) ipLimits[ip][hour] = {};
145
+ ipLimits[ip][hour][minute] = (ipLimits[ip][hour][minute] || 0) + 1;
146
+
147
+ if (ipLimits[ip][hour][minute] > requestLimit) {
148
+ return res.status(429).jsonResponse({
149
+ message: 'Too Many Requests (IP limited), authenticate to increase the limit',
150
+ error: `You have exceeded the limit of ${requestLimit} requests per hour.`,
151
+ reset: `Reset in ${((resetTime - now) / 60000).toFixed(0)} minutes.`,
152
+ status: '429'
153
+ });
154
+ }
155
+
156
+ Object.keys(ipLimits[ip]).forEach(h => { if (h != hour) delete ipLimits[ip][h]; });
157
+
158
+ next();
159
+ });
160
+
161
+ // Save and send logs
162
+ app.use((req, res, next) => {
163
+ if (req.method === 'HEAD') return next();
164
+ if (req.originalUrl === '/logs') return next();
165
+
166
+ const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
167
+
168
+ const startTime = Date.now();
169
+ const timestamp = new Date().toISOString();
170
+ const method = req.method;
171
+ const url = req.originalUrl;
172
+ const platform = req.headers['sec-ch-ua-platform']?.replace(/"/g, '');
173
+
174
+ res.on('finish', () => {
175
+ const status = res.statusCode === 304 ? 200 : res.statusCode;
176
+ const duration = `${Date.now() - startTime}ms`;
177
+
178
+ logs.push({ timestamp, method, url, status, duration, platform });
179
+ console.log(`[${new Date().toISOString()}] ${method} ${url} ${res.statusCode} - ${duration} - ${ip}`);
180
+ if (logs.length > 1000) logs.shift();
181
+ });
182
+ next();
183
+ });
184
+
185
+ // Internal Server Error
186
+ app.use((err, req, res, next) => {
187
+ console.error(err.stack);
188
+ res.status(500).jsonResponse({
189
+ message: 'Internal Server Error',
190
+ error: err.message,
191
+ documentation: 'https://docs.sylvain.pro',
192
+ status: '500'
193
+ });
194
+ });
195
+
196
+ // Check if version exists
197
+ app.use('/:version', (req, res, next) => {
198
+ const { version } = req.params;
199
+ const latest = versions[versions.length - 1];
200
+ const endpoint = req.originalUrl.split('/').slice(2).join('/');
201
+
202
+ if (['latest', 'fr', 'en'].includes(version)) {
203
+ return res.redirect(endpoint ? `/${latest}/${endpoint}` : `/${latest}`);
204
+ }
205
+
206
+ if (!versions.includes(version) && version !== 'logs') {
207
+ return res.status(404).jsonResponse({
208
+ message: 'Not Found',
209
+ error: `Invalid API version (${version}).`,
210
+ documentation: `https://docs.sylvain.pro/${versions.at(-1)}`,
211
+ status: '404'
212
+ });
213
+ }
214
+ next();
215
+ });
216
+
217
+ // Check if endpoint exists
218
+ app.use('/:version/:endpoint', (req, res, next) => {
219
+ const { version, endpoint } = req.params;
220
+
221
+ if (!versions.includes(version) || !endpoints[version].includes(endpoint)) {
222
+ return res.status(404).jsonResponse({
223
+ message: 'Not Found',
224
+ error: `Endpoint '${endpoint}' does not exist in ${version}.`,
225
+ documentation: `https://docs.sylvain.pro/${versions.at(-1)}`,
226
+ status: '404'
227
+ });
228
+ }
229
+ next();
230
+ });
231
+
232
+ // ----------- ----------- MAIN ENDPOINTS ----------- ----------- //
233
+
234
+ // Main route
235
+ app.get('/', (req, res) => {
236
+ res.setHeader('Content-Type', 'application/json');
237
+ res.jsonResponse({
238
+ documentation: 'https://docs.sylvain.pro',
239
+ latest: 'https://api.sylvain.pro/latest',
240
+ logs: 'https://api.sylvain.pro/logs',
241
+ versions: {
242
+ v1: 'https://api.sylvain.pro/v1',
243
+ v2: 'https://api.sylvain.pro/v2',
244
+ v3: 'https://api.sylvain.pro/v3'
245
+ }
246
+ });
247
+ });
248
+
249
+ // Display v1 endpoints
250
+ app.get('/v1', (req, res) => {
251
+ res.jsonResponse({
252
+ version: 'v1',
253
+ documentation: 'https://docs.sylvain.pro/v1',
254
+ endpoints: {
255
+ get: {
256
+ algorithm: '/v1/algorithms?method={algorithm}&value={value}(&value2={value2})',
257
+ captcha: '/v1/captcha?text={text}',
258
+ color: '/v1/color',
259
+ convert: '/v1/convert?value={value}&from={unit}&to={unit}',
260
+ domain: '/v1/domain',
261
+ infos: '/v1/infos',
262
+ personal: '/v1/personal',
263
+ qrcode: '/v1/qrcode?url={URL}',
264
+ username: '/v1/username'
265
+ },
266
+ post: {
267
+ token: '/v1/token'
268
+ }
269
+ }
270
+ });
271
+ });
272
+
273
+ // Display v2 endpoints
274
+ app.get('/v2', (req, res) => {
275
+ res.jsonResponse({
276
+ version: 'v2',
277
+ documentation: 'https://docs.sylvain.pro/v2',
278
+ endpoints: {
279
+ get: {
280
+ algorithm: '/v2/algorithms?method={algorithm}&value={value}(&value2={value2})',
281
+ captcha: '/v2/captcha?text={text}',
282
+ chat: '/v2/chat',
283
+ color: '/v2/color',
284
+ convert: '/v2/convert?value={value}&from={unit}&to={unit}',
285
+ domain: '/v2/domain',
286
+ infos: '/v2/infos',
287
+ personal: '/v2/personal',
288
+ qrcode: '/v2/qrcode?url={URL}',
289
+ username: '/v2/username'
290
+ },
291
+ post: {
292
+ chat: {
293
+ chat: '/v2/chat',
294
+ private: '/v2/chat/private'
295
+ },
296
+ hash: '/v2/hash',
297
+ tic_tac_toe: {
298
+ tic_tac_toe: '/v2/tic-tac-toe',
299
+ fetch: '/v2/tic-tac-toe/fetch'
300
+ },
301
+ token: '/v2/token'
302
+ }
303
+ }
304
+ });
305
+ });
306
+
307
+ // Display v3 endpoints
308
+ app.get('/v3', (req, res) => {
309
+ res.jsonResponse({
310
+ version: 'v3',
311
+ documentation: 'https://docs.sylvain.pro/v3',
312
+ endpoints: {
313
+ get: {
314
+ algorithm: '/v3/algorithms?method={algorithm}&value={value}(&value2={value2})',
315
+ captcha: '/v3/captcha?text={text}',
316
+ chat: '/v3/chat',
317
+ color: '/v3/color',
318
+ convert: '/v3/convert?value={value}&from={unit}&to={unit}',
319
+ domain: '/v3/domain',
320
+ infos: '/v3/infos',
321
+ levenshtein: '/v3/levenshtein?str1={string}&str2={string}',
322
+ personal: '/v3/personal',
323
+ qrcode: '/v3/qrcode?url={URL}',
324
+ time: '/v3/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})',
325
+ username: '/v3/username'
326
+ },
327
+ post: {
328
+ chat: {
329
+ chat: '/v3/chat',
330
+ private: '/v3/chat/private'
331
+ },
332
+ hash: '/v3/hash',
333
+ hyperplanning: '/v3/hyperplanning',
334
+ tic_tac_toe: {
335
+ tic_tac_toe: '/v3/tic-tac-toe',
336
+ fetch: '/v3/tic-tac-toe/fetch'
337
+ },
338
+ token: '/v3/token'
339
+ }
340
+ }
341
+ });
342
+ });
343
+
344
+ // Display logs
345
+ app.get('/logs', (req, res) => res.jsonResponse(logs));
346
+
347
+ // ----------- ----------- GET ENDPOINTS ----------- ----------- //
348
+
349
+ // Algorithms
350
+ app.get('/:version/algorithms', (req, res) => {
351
+ const { method, value, value2 } = req.query;
352
+ const { version } = req.params;
353
+
354
+ if (!['anagram', 'bubblesort', 'factorial', 'fibonacci', 'gcd', 'isprime', 'palindrome', 'primefactors', 'primelist', 'reverse'].includes(method)) {
355
+ return res.jsonResponse({
356
+ error: 'Please provide a valid algorithm (?method={algorithm})',
357
+ documentation: `https://docs.sylvain.pro/${version}/en/algorithms`
358
+ });
359
+ }
360
+ if (!value) return res.jsonResponse({ error: 'Please provide a valid value (&value={value})' });
361
+
362
+ if (method === 'anagram') {
363
+ if (!value2) return res.jsonResponse({ error: 'Please provide a second value (&value2={value})' });
364
+ return res.jsonResponse({ answer: value.split('').sort().join('') === value2.split('').sort().join('') });
365
+ }
366
+
367
+ if (method === 'bubblesort') {
368
+ const arr = value.split(',').map(Number);
369
+ const n = arr.length;
370
+ for (let i = 0; i < n-1; i++) {
371
+ for (let j = 0; j < n-i-1; j++) {
372
+ if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
373
+ }
374
+ }
375
+ return res.jsonResponse({ answer: arr });
376
+ }
377
+
378
+ if (method === 'factorial') {
379
+ if (isNaN(value) || value < 0 || value > 170) return res.jsonResponse({ error: 'Please provide a valid number between 0 and 170.' });
380
+ return res.jsonResponse({ answer: factorial(value) });
381
+ }
382
+
383
+ if (method === 'fibonacci') {
384
+ let fib = [0, 1];
385
+ if (isNaN(value) || value < 0 || value > 1000) return res.jsonResponse({ error: 'Please provide a valid number between 0 and 1000.' });
386
+ for (let i = 2; i < parseInt(value); i++) fib.push(fib[i - 1] + fib[i - 2]);
387
+ return res.jsonResponse({ answer: fib.slice(0, parseInt(value)) });
388
+ }
389
+
390
+ if (method === 'gcd') {
391
+ if (!value2) return res.jsonResponse({ error: 'Please provide a second value (&value2={value})' });
392
+ if (isNaN(value) || isNaN(value2)) return res.jsonResponse({ error: 'Invalid numbers.' });
393
+ return res.jsonResponse({ answer: gcd(value, value2) });
394
+ }
395
+
396
+ if (method === 'isprime') {
397
+ let isPrime = true;
398
+ if (isNaN(value) || value < 1) return res.jsonResponse({ error: 'Please provide a valid number greater than or equal to 1.' });
399
+ for (let i = 2; i <= Math.sqrt(value); i++) {
400
+ if (value % i === 0) {
401
+ isPrime = false;
402
+ break;
403
+ }
404
+ }
405
+ return res.jsonResponse({ answer: isPrime });
406
+ }
407
+
408
+ if (method === 'palindrome') return res.jsonResponse({ answer: value === value.split('').reverse().join('') });
409
+
410
+ if (method === 'primefactors') {
411
+ let num = value;
412
+ let factors = [];
413
+ if (isNaN(num) || num < 2 || num > 100000) return res.jsonResponse({ error: 'Please provide a valid number between 2 and 100 000.' });
414
+ for (let i = 2; i <= num; i++) {
415
+ while (num % i === 0) {
416
+ factors.push(i);
417
+ num /= i;
418
+ }
419
+ }
420
+ return res.jsonResponse({ answer: factors });
421
+ }
422
+
423
+ if (method === 'primelist') {
424
+ const primes = [];
425
+ if (isNaN(value) || value < 2 || value > 10000) return res.jsonResponse({ error: 'Please provide a valid number between 2 and 10 000.' });
426
+ for (let i = 2; i <= value; i++) {
427
+ let isPrime = true;
428
+ for (let j = 2; j <= Math.sqrt(i); j++) {
429
+ if (i % j === 0) {
430
+ isPrime = false;
431
+ break;
432
+ }
433
+ }
434
+ if (isPrime) primes.push(i);
435
+ }
436
+ return res.jsonResponse({ answer: primes });
437
+ }
438
+
439
+ if (method === 'reverse') return res.jsonResponse({ answer: value.split('').reverse().join('') });
440
+ });
441
+
442
+ // Generate captcha
443
+ app.get('/:version/captcha', (req, res) => {
444
+ const captcha = req.query.text;
445
+
446
+ if (!captcha) return res.jsonResponse({ error: 'Please provide a valid argument (?text={text})' });
447
+
448
+ const size = 60, font = '60px Comic Sans Ms', width = captcha.length * size, height = 120;
449
+ const canvas = createCanvas(width, height), ctx = canvas.getContext('2d');
450
+
451
+ ctx.fillStyle = 'white';
452
+ ctx.fillRect(0, 0, width, height);
453
+
454
+ for (let i = 0; i < 20; i++) {
455
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
456
+ ctx.beginPath();
457
+ ctx.moveTo(Math.random() * width, Math.random() * height);
458
+ ctx.lineTo(Math.random() * width, Math.random() * height);
459
+ ctx.lineWidth = Math.random() * 2;
460
+ ctx.stroke();
461
+ }
462
+
463
+ let x = (canvas.width + 20 - width) / 2;
464
+ for (let i = 0; i < captcha.length; i++) {
465
+ const offsetX = Math.cos(i * 0.3) * 10, y = height / 2.5 + Math.floor(Math.random() * (height / 2));
466
+
467
+ ctx.font = font;
468
+ ctx.fillStyle = `rgb(${Math.floor(Math.random() * 192)}, ${Math.floor(Math.random() * 192)}, ${Math.floor(Math.random() * 192)})`;
469
+
470
+ ctx.save();
471
+ ctx.translate(x + size / 2, y);
472
+ ctx.rotate((Math.random() - 0.5) * 0.5);
473
+ ctx.fillText(captcha[i], -size / 2 + offsetX, 0);
474
+ ctx.restore();
475
+
476
+ x += size;
477
+ }
478
+
479
+ for (let i = 0; i < 200; i++) {
480
+ ctx.fillStyle = 'black';
481
+ ctx.fillRect(Math.floor(Math.random() * width), Math.floor(Math.random() * height), 1.2, 1.2);
482
+ }
483
+
484
+ res.set('Content-Type', 'image/png');
485
+ res.send(canvas.toBuffer('image/png'));
486
+ });
487
+
488
+ // Display stored data
489
+ app.get('/:version/chat', (req, res) => {
490
+ if (chat.length > 0) res.jsonResponse(chat);
491
+ else res.jsonResponse({ error: 'No messages stored.' });
492
+ });
493
+
494
+ // GET private chat error
495
+ app.get('/:version/chat/private', (req, res) => {
496
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
497
+ });
498
+
499
+ // Generate color
500
+ app.get('/:version/color', (req, res) => {
501
+ const r = Math.floor(Math.random() * 256), g = Math.floor(Math.random() * 256), b = Math.floor(Math.random() * 256);
502
+ const hsl = (() => {
503
+ const r1 = r / 255, g1 = g / 255, b1 = b / 255, max = Math.max(r1, g1, b1), min = Math.min(r1, g1, b1), l = (max + min) / 2;
504
+ if (max === min) return [0, 0, l * 100];
505
+ const d = max - min, s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
506
+ let h = { [r1]: (g1 - b1) / d + (g1 < b1 ? 6 : 0), [g1]: (b1 - r1) / d + 2, [b1]: (r1 - g1) / d + 4 }[max];
507
+ return [h * 60 % 360, s * 100, l * 100];
508
+ })();
509
+ const hsv = (() => {
510
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), v = max / 255, s = max ? (max - min) / max : 0;
511
+ let h = max === min ? 0 : { [r]: (g - b) / (max - min), [g]: 2 + (b - r) / (max - min), [b]: 4 + (r - g) / (max - min) }[max];
512
+ return [h * 60 % 360, s * 100, v * 100];
513
+ })();
514
+ const hwb = (() => {
515
+ const [h] = hsv, whiteness = Math.min(r, g, b) / 255, blackness = 1 - Math.max(r, g, b) / 255;
516
+ return [h, whiteness * 100, blackness * 100];
517
+ })();
518
+ const cmyk = (() => {
519
+ const k = 1 - Math.max(r, g, b) / 255, c = (1 - r / 255 - k) / (1 - k) || 0, m = (1 - g / 255 - k) / (1 - k) || 0, y = (1 - b / 255 - k) / (1 - k) || 0;
520
+ return [c, m, y, k].map(x => x * 100);
521
+ })();
522
+ res.jsonResponse({
523
+ hex: `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`,
524
+ rgb: `rgb(${r}, ${g}, ${b})`,
525
+ hsl: `hsl(${hsl[0].toFixed(1)}, ${hsl[1].toFixed(1)}%, ${hsl[2].toFixed(1)}%)`,
526
+ hsv: `hsv(${hsv[0].toFixed(1)}, ${hsv[1].toFixed(1)}%, ${hsv[2].toFixed(1)}%)`,
527
+ hwb: `hwb(${hwb[0].toFixed(1)}, ${hwb[1].toFixed(1)}%, ${hwb[2].toFixed(1)}%)`,
528
+ cmyk: `cmyk(${cmyk.map(x => x.toFixed(1)).join('%, ')}%)`
529
+ });
530
+ });
531
+
532
+ // Convert units
533
+ app.get('/:version/convert', (req, res) => {
534
+ const { value, from, to } = req.query;
535
+
536
+ if (!value || isNaN(value)) return res.jsonResponse({ error: 'Please provide a valid value (?value={value})' });
537
+ if (!from) return res.jsonResponse({ error: 'Please provide a valid source unit (&from={unit})' });
538
+ if (!to) return res.jsonResponse({ error: 'Please provide a valid target unit (&to={unit})' });
539
+
540
+ const conversions = {
541
+ celsius: { fahrenheit: (val) => (val * 9) / 5 + 32, kelvin: (val) => val + 273.15 },
542
+ fahrenheit: { celsius: (val) => ((val - 32) * 5) / 9, kelvin: (val) => ((val - 32) * 5) / 9 + 273.15 },
543
+ kelvin: { celsius: (val) => val - 273.15, fahrenheit: (val) => ((val - 273.15) * 9) / 5 + 32 },
544
+ };
545
+
546
+ const convert = conversions[from.toLowerCase()]?.[to.toLowerCase()];
547
+ if (!convert) return res.jsonResponse({ error: 'Invalid conversion units.' });
548
+
549
+ res.jsonResponse({ from, to, value: parseFloat(value), result: convert(parseFloat(value)) });
550
+ });
551
+
552
+ // Generate domain informations
553
+ app.get('/:version/domain', (req, res) => {
554
+ const subdomains = ['fr.', 'en.', 'docs.', 'api.', 'projects.', 'app.', 'web.', 'info.', 'dev.', 'shop.', 'blog.', 'support.', 'mail.', 'forum.'];
555
+ const domains = ['example', 'site', 'test', 'demo', 'page', 'store', 'portfolio', 'platform', 'hub', 'network', 'service', 'cloud', 'solutions', 'company'];
556
+ const tlds = ['.com', '.fr', '.eu', '.dev', '.net', '.org', '.io', '.tech', '.biz', '.info', '.co', '.app', '.store', '.online', '.shop', '.tv'];
557
+
558
+ const domain = `${random(domains)}${random(tlds)}`;
559
+ const fulldomain = `${random(subdomains)}${domain}`;
560
+
561
+ const ips = Array.from({ length: Math.floor(Math.random() * 3) + 1 }, genIP);
562
+ const dns = Array.from({ length: Math.floor(Math.random() * 5) + 1 }, genIP);
563
+
564
+ res.jsonResponse({
565
+ domain,
566
+ full_domain: fulldomain,
567
+ ip_address: ips,
568
+ ssl_certified: Math.random() > 0.5,
569
+ hosting_provider: random(['AWS', 'Bluehost', 'DigitalOcean', 'GitHub', 'HostGator', 'Render', 'SiteGround']),
570
+ dns_servers: dns,
571
+ dns_provider: random(['AWS Route 53', 'Cloudflare', 'GoDaddy', 'Google DNS', 'Namecheap']),
572
+ traffic: `${Math.floor(Math.random() * 10000)} visits/day`,
573
+ seo_score: Math.floor(Math.random() * 100),
574
+ page_rank: Math.floor(Math.random() * 10),
575
+ country: random(['Australia', 'Canada', 'France', 'Germany', 'India', 'Japan', 'UK', 'USA']),
576
+ website_type: random(['Blog', 'Community', 'Corporate', 'Educational', 'E-commerce', 'Personal', 'Portfolio']),
577
+ random_name: domain.split('.')[0],
578
+ random_subdomain: fulldomain.split('.')[0],
579
+ random_tld: domain.split('.').pop(),
580
+ backlinks_count: Math.floor(Math.random() * 1000),
581
+ creation_date: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString(),
582
+ expiration_date: new Date(Date.now() + Math.floor(Math.random() * 10000000000)).toISOString(),
583
+ });
584
+ });
585
+
586
+ // GET planning error
587
+ app.get('/:version/hyperplanning', (req, res) => {
588
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
589
+ });
590
+
591
+ // GET hash error
592
+ app.get('/:version/hash', (req, res) => {
593
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
594
+ });
595
+
596
+ // Display API informations
597
+ app.get('/:version/infos', (req, res) => {
598
+ res.jsonResponse({
599
+ endpoints: endpoints[versions.at(-1)].length,
600
+ last_version: versions.at(-1),
601
+ documentation: 'https://docs.sylvain.pro',
602
+ github: 'https://github.com/20syldev/api',
603
+ creation: 'November 25th 2024',
604
+ });
605
+ });
606
+
607
+ // Calculate Levenshtein distance
608
+ app.get('/:version/levenshtein', (req, res) => {
609
+ const { str1, str2 } = req.query;
610
+
611
+ if (!str1 || typeof str1 !== 'string') return res.jsonResponse({ error: 'Please provide a first string (?str1={string})' });
612
+ if (!str2 || typeof str2 !== 'string') return res.jsonResponse({ error: 'Please provide a second string (&str2={string})' });
613
+
614
+ if (str1.length > 1000) return res.jsonResponse({ error: 'First string exceeds 1000 characters.' });
615
+ if (str2.length > 1000) return res.jsonResponse({ error: 'Second string exceeds 1000 characters.' });
616
+
617
+ const lev = (a, b) => {
618
+ const m = Array.from({ length: a.length + 1 }, (_, i) => [i]);
619
+ for (let j = 0; j <= b.length; j++) m[0][j] = j;
620
+ for (let i = 1; i <= a.length; i++)
621
+ for (let j = 1; j <= b.length; j++)
622
+ 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]));
623
+
624
+ return m[a.length][b.length];
625
+ };
626
+
627
+ res.jsonResponse({ str1, str2, distance: lev(str1, str2) });
628
+ });
629
+
630
+ // Generate personal data
631
+ app.get('/:version/personal', (req, res) => {
632
+ const people = [
633
+ { name: 'John Doe', social: 'john_doe', email: 'john@example.com', country: 'US' },
634
+ { name: 'Jane Martin', social: 'jane_martin', email: 'jane@example.com', country: 'FR' },
635
+ { name: 'Michael Johnson', social: 'mike_johnson', email: 'michael@example.com', country: 'UK' },
636
+ { name: 'Emily Davis', social: 'emily_davis', email: 'emily@example.com', country: 'ES' },
637
+ { name: 'Alexis Barbos', social: 'alexis_barbos', email: 'alexis@example.com', country: 'DE' },
638
+ { name: 'Sarah Williams', social: 'sarah_williams', email: 'sarah@example.com', country: 'IT' },
639
+ { name: 'Daniel Brown', social: 'daniel_brown', email: 'daniel@example.com', country: 'JP' },
640
+ { name: 'Sophia Wilson', social: 'sophia_wilson', email: 'sophia@example.com', country: 'BR' },
641
+ { name: 'James Taylor', social: 'james_taylor', email: 'james@example.com', country: 'CA' },
642
+ { name: 'Olivia Thomas', social: 'olivia_thomas', email: 'olivia@example.com', country: 'AU' }
643
+ ];
644
+
645
+ const countries = {
646
+ US: { tel: '123-456-7890', code: '1', lang: 'English' },
647
+ FR: { tel: '06 78 90 12 34', code: '33', lang: 'French' },
648
+ UK: { tel: '7911 123456', code: '44', lang: 'English' },
649
+ ES: { tel: '678 901 234', code: '34', lang: 'Spanish' },
650
+ DE: { tel: '163 555 1584', code: '49', lang: 'German' },
651
+ IT: { tel: '345 678 9012', code: '39', lang: 'Italian' },
652
+ JP: { tel: '080-1234-5678', code: '81', lang: 'Japanese' },
653
+ BR: { tel: '(11) 98765-4321', code: '55', lang: 'Portuguese' },
654
+ CA: { tel: '416-123-4567', code: '1', lang: 'English' },
655
+ AU: { tel: '0412 345 678', code: '61', lang: 'English' }
656
+ };
657
+
658
+ const jobs = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Doctor'];
659
+ const hobbies = ['Reading', 'Traveling', 'Gaming', 'Cooking', 'Fitness', 'Music', 'Photography', 'Writing'];
660
+ const cities = ['New York', 'Paris', 'London', 'Madrid', 'Berlin', 'Rome', 'Tokyo', 'Los Angeles', 'Sydney', 'São Paulo', 'Toronto'];
661
+ const streets = ['Main St', '2nd Ave', 'Broadway', 'Park Lane', 'Elm St', 'Sunset Blvd', 'Maple St', 'Highland Rd'];
662
+
663
+ const card = Array.from({ length: 4 }, () => Math.floor(Math.random() * 9000) + 1000).join(' ');
664
+ const cvc = Math.floor(Math.random() * 900) + 100;
665
+ const expiration = `${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}/${(new Date().getFullYear() + Math.floor(Math.random() * 3)).toString().slice(-2)}`;
666
+
667
+ const person = random(people);
668
+ const social = person.social;
669
+ const country = person.country;
670
+ const phone = countries[country].tel;
671
+ const lang = countries[country].lang;
672
+
673
+ const age = Math.floor(Math.random() * 50) + 18;
674
+ const birthday = new Date(Date.now() - Math.floor((Math.random() * 50 + 18) * 365.25 * 24 * 60 * 60 * 1000)).toISOString();
675
+
676
+ let emergencyContacts = [], yearIncome = Math.floor(Math.random() * 100000), subscriptions = [], pets = [], vehicles = [];
677
+ let civilStatus = 'Single';
678
+ let children = 0;
679
+
680
+ if (age >= 21 && Math.random() > 0.7) civilStatus = 'Married';
681
+
682
+ if (civilStatus === 'Married' && age >= 25) children = Math.floor(Math.random() * 4);
683
+
684
+ while (emergencyContacts.length < Math.floor(Math.random() * 3) + 1) {
685
+ let emergencyContact = random(people);
686
+ while (emergencyContact.email === person.email || emergencyContacts.some(e => e.email === emergencyContact.email)) {
687
+ emergencyContact = random(people);
688
+ }
689
+ emergencyContacts.push({
690
+ name: emergencyContact.name,
691
+ relationship: random(['Spouse', 'Parent', 'Sibling', 'Friend']),
692
+ phone: `+${countries[country].code} ${countries[country].tel}`
693
+ });
694
+ }
695
+
696
+ while (subscriptions.length < Math.floor(Math.random() * 3) + 1) {
697
+ let subscription = random(['Netflix', 'Spotify', 'Amazon Prime', 'Disney+', 'Hulu']);
698
+ if (!subscriptions.includes(subscription)) subscriptions.push(subscription);
699
+ }
700
+
701
+ while (pets.length < Math.floor(Math.random() * 3) + 1) {
702
+ let pet = random(['Dog', 'Cat', 'Fish', 'Bird', 'None']);
703
+ if (!pets.includes(pet)) pets.push(pet);
704
+ }
705
+
706
+ while (vehicles.length < Math.floor(Math.random() * 3) + 1) {
707
+ let vehicle = random(['Car', 'Bike', 'Motorcycle', 'Bus', 'None']);
708
+ if (!vehicles.includes(vehicle)) vehicles.push(vehicle);
709
+ }
710
+
711
+ res.jsonResponse({
712
+ name: person.name,
713
+ email: person.email,
714
+ localisation: country,
715
+ phone: `+${countries[country].code} ${phone}`,
716
+ job: random(jobs),
717
+ hobbies: random(hobbies),
718
+ language: lang,
719
+ card,
720
+ cvc,
721
+ expiration,
722
+ address: `${Math.floor(Math.random() * 9999)} ${random(streets)}, ${random(cities)}`,
723
+ birthday,
724
+ civil_status: civilStatus,
725
+ children,
726
+ vehicle: vehicles,
727
+ social_profiles: {
728
+ twitter: `@${social}`,
729
+ facebook: `facebook.com/${social}`,
730
+ linkedin: `linkedin.com/in/${social}`,
731
+ instagram: `instagram.com/${social}`
732
+ },
733
+ year_income: `${yearIncome} USD/year`,
734
+ month_income: `${(yearIncome / 12).toFixed(2)} USD/month`,
735
+ education: random(['High School', 'Bachelor\'s', 'Master\'s', 'PhD']),
736
+ work_experience: `${Math.floor(Math.random() * 20)} years`,
737
+ health_status: random(['Healthy', 'Minor Issues', 'Chronic Conditions']),
738
+ emergency_contacts: emergencyContacts,
739
+ subscriptions,
740
+ pets,
741
+ });
742
+ });
743
+
744
+ // Generate QR Code
745
+ app.get('/:version/qrcode', async (req, res) => {
746
+ const { url } = req.query;
747
+
748
+ if (!url) return res.jsonResponse({ error: 'Please provide a valid url (?url={URL})' });
749
+
750
+ try { res.jsonResponse({ qr: await toDataURL(url) }); }
751
+ catch { res.jsonResponse({ error: 'Error generating QR code.' }); }
752
+ });
753
+
754
+ // GET tic-tac-toe game error
755
+ app.get('/:version/tic-tac-toe', (req, res) => {
756
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
757
+ });
758
+
759
+ // GET tic-tac-toe fetch error
760
+ app.get('/:version/tic-tac-toe/fetch', (req, res) => {
761
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
762
+ });
763
+
764
+ // Display or generate time informations
765
+ app.get('/:version/time', (req, res) => {
766
+ const { type = 'live', start, end, format, timezone } = req.query;
767
+
768
+ const validFormats = ['iso', 'utc', 'timestamp', 'locale', 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'second', 'ms', 'dayOfWeek', 'dayOfYear', 'weekNumber', 'timezone', 'timezoneOffset'];
769
+ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
770
+
771
+ if (type !== 'live' && type !== 'random') return res.jsonResponse({ error: 'Please provide a valid type (?type={type})' });
772
+ if (start && !Date.parse(start)) return res.jsonResponse({ error: 'Please provide a valid start date (?start={YYYY-MM-DD})' });
773
+ if (end && !Date.parse(end)) return res.jsonResponse({ error: 'Please provide a valid end date (?end={YYYY-MM-DD})' });
774
+ if (format && !validFormats.includes(format)) return res.jsonResponse({ error: 'Please provide a valid format (?format={format})' });
775
+ if (timezone && !validTimezones.includes(timezone)) return res.jsonResponse({ error: 'Please provide a valid timezone (?timezone={timezone})' });
776
+
777
+ const getTimeFormats = (date, timezoneOption) => {
778
+ return {
779
+ iso: date.toISOString(),
780
+ utc: date.toUTCString(),
781
+ timestamp: date.getTime(),
782
+ locale: date.toLocaleString('en-US', { timeZone: timezoneOption, timeZoneName: 'long' }),
783
+ date: date.toLocaleDateString('en-US', { timeZone: timezoneOption }),
784
+ time: date.toLocaleTimeString('en-US', { timeZone: timezoneOption }),
785
+ year: date.getFullYear(),
786
+ month: date.getMonth() + 1,
787
+ day: date.getDate(),
788
+ hour: date.getHours(),
789
+ minute: date.getMinutes(),
790
+ second: date.getSeconds(),
791
+ ms: date.getMilliseconds(),
792
+ dayOfWeek: date.getDay(),
793
+ dayOfYear: Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 86400000),
794
+ weekNumber: Math.ceil((((date - new Date(date.getFullYear(), 0, 0)) / 86400000) + 1) / 7),
795
+ timezone: timezoneOption,
796
+ timezoneOffset: date.getTimezoneOffset()
797
+ };
798
+ };
799
+
800
+ if (type === 'random') {
801
+ const startDate = new Date(start || '1900-01-01').getTime();
802
+ const endDate = new Date(end || '2100-12-31').getTime();
803
+ const randomDate = new Date(start ? startDate : startDate + Math.random() * (endDate - startDate));
804
+ const timezoneOption = timezone || validTimezones[Math.floor(Math.random() * 5)];
805
+ const formats = getTimeFormats(randomDate, timezoneOption);
806
+
807
+ return res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
808
+ }
809
+
810
+ const now = new Date();
811
+ const timezoneOption = timezone || 'UTC';
812
+ const formats = getTimeFormats(now, timezoneOption);
813
+
814
+ res.jsonResponse(format && formats[format] ? { date: formats[format] } : formats);
815
+ });
816
+
817
+ // GET token error
818
+ app.get('/:version/token', (req, res) => {
819
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
820
+ });
821
+
822
+ // Generate username
823
+ app.get('/:version/username', (req, res) => {
824
+ const adj = ['Happy', 'Silly', 'Clever', 'Creative', 'Brave', 'Gentle', 'Kind', 'Funny', 'Wise', 'Charming', 'Sincere', 'Resourceful', 'Patient', 'Energetic', 'Adventurous', 'Ambitious', 'Courageous', 'Courteous', 'Determined'];
825
+ const ani = ['Cat', 'Dog', 'Tiger', 'Elephant', 'Monkey', 'Penguin', 'Dolphin', 'Lion', 'Bear', 'Fox', 'Owl', 'Giraffe', 'Zebra', 'Koala', 'Rabbit', 'Squirrel', 'Panda', 'Horse', 'Wolf', 'Eagle'];
826
+ const job = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Chef', 'Doctor', 'Teacher', 'Lawyer', 'Entrepreneur', 'Actor', 'Dancer', 'Photographer', 'Architect', 'Pilot', 'Designer', 'Journalist', 'Veterinarian'];
827
+
828
+ const nombre = Math.floor(Math.random() * 100);
829
+ const choix = {
830
+ adj_num: () => random(adj) + nombre,
831
+ ani_num: () => random(ani) + nombre,
832
+ pro_num: () => random(job) + nombre,
833
+ adj_ani: () => random(adj) + random(ani),
834
+ adj_ani_num: () => random(adj) + random(ani) + nombre,
835
+ adj_pro: () => random(adj) + random(job),
836
+ pro_ani: () => random(job) + random(ani),
837
+ pro_ani_num: () => random(job) + random(ani) + nombre
838
+ };
839
+
840
+ const username = choix[random(Object.keys(choix))]();
841
+ res.jsonResponse({ adjective: adj, animal: ani, job, number: nombre, username });
842
+ });
843
+
844
+ // Display informations for owner's website
845
+ app.get('/:version/website', async (req, res) => {
846
+ const currentTime = Date.now();
847
+
848
+ if (currentTime - lastFetch >= 10 * 60 * 1000) {
849
+ try {
850
+ const username = '20syldev';
851
+ const token = process.env.STATS5;
852
+ const today = new Date().toISOString().split('T')[0];
853
+ const monthFirst = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
854
+ const lastYear = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split('T')[0];
855
+
856
+ const query = `
857
+ {
858
+ user(login: "${username}") {
859
+ contributionsCollection(from: "${today}T00:00:00Z") {
860
+ contributionCalendar {
861
+ totalContributions
862
+ }
863
+ }
864
+ contributions_month: contributionsCollection(from: "${monthFirst}T00:00:00Z") {
865
+ contributionCalendar {
866
+ totalContributions
867
+ }
868
+ }
869
+ contributions_year: contributionsCollection(from: "${lastYear}T00:00:00Z") {
870
+ contributionCalendar {
871
+ totalContributions
872
+ }
873
+ }
874
+ }
875
+ }`;
876
+
877
+ const apiResponse = await fetch('https://api.github.com/graphql', {
878
+ method: 'POST',
879
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
880
+ body: JSON.stringify({ query })
881
+ });
882
+
883
+ if (!apiResponse.ok) throw new Error('Error fetching data.');
884
+
885
+ const data = await apiResponse.json();
886
+ const user = data?.data?.user;
887
+
888
+ contributions = {
889
+ today: user?.contributionsCollection?.contributionCalendar?.totalContributions || 0,
890
+ month: user?.contributions_month?.contributionCalendar?.totalContributions || 0,
891
+ year: user?.contributions_year?.contributionCalendar?.totalContributions || 0,
892
+ };
893
+
894
+ lastFetch = currentTime;
895
+ } catch { contributions = { today: 0, month: 0, year: 0 }; }
896
+ }
897
+
898
+ res.jsonResponse({
899
+ versions: {
900
+ api: process.env.API,
901
+ cdn: process.env.CDN,
902
+ coop_api: process.env.COOP_API,
903
+ coop_status: process.env.COOP_STATUS,
904
+ chat: process.env.CHAT,
905
+ digit: process.env.DIGIT,
906
+ doc_coopbot: process.env.DOC_COOPBOT,
907
+ docs: process.env.DOCS,
908
+ donut: process.env.DONUT,
909
+ drawio_plugin: process.env.DRAWIO_PLUGIN,
910
+ flowers: process.env.FLOWERS,
911
+ gemsync: process.env.GEMSYNC,
912
+ gitsite: process.env.GITSITE,
913
+ logs: process.env.LOGS,
914
+ logvault: process.env.LOGVAULT,
915
+ minify: process.env.MINIFY,
916
+ morpion: process.env.MORPION,
917
+ nitrogen: process.env.NITROGEN,
918
+ old_database: process.env.OLD_DATABASE,
919
+ php: process.env.PHP,
920
+ ping: process.env.PING,
921
+ portfolio: process.env.PORTFOLIO,
922
+ python_api: process.env.PYTHON_API,
923
+ readme: process.env.README,
924
+ terminal: process.env.TERMINAL,
925
+ wrkit: process.env.WRKIT,
926
+ zpki: process.env.ZPKI
927
+ },
928
+ patched_projects: process.env.PATCH?.split(' ') || [],
929
+ updated_projects: process.env.RECENT?.split(' ') || [],
930
+ new_projects: process.env.NEW?.split(' ') || [],
931
+ sub_domains: process.env.DOMAINS?.split(' ') || [],
932
+ stats: {
933
+ os: process.env.STATS1,
934
+ front: process.env.STATS2,
935
+ back: process.env.STATS3,
936
+ projects: process.env.STATS4,
937
+ today: contributions.today.toString(),
938
+ this_month: contributions.month.toString(),
939
+ last_year: contributions.year.toString(),
940
+ },
941
+ notif_tag: process.env.TAG,
942
+ active: process.env.ACTIVE
943
+ });
944
+ });
945
+
946
+ // ----------- ----------- POST ENDPOINTS ----------- ----------- //
947
+
948
+ // Store chat messages
949
+ app.post('/:version/chat', (req, res) => {
950
+ const { username, message, timestamp, session, token } = req.body;
951
+
952
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
953
+ if (!message) return res.jsonResponse({ error: 'Please provide a message (&message={message})' });
954
+ if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
955
+
956
+ const u = username.toLowerCase(), now = Date.now();
957
+ const msg = { username, message, timestamp: timestamp || new Date().toISOString() };
958
+
959
+ rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
960
+ if (rateLimits[u].length > 50) {
961
+ const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
962
+ return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
963
+ }
964
+ rateLimits[u].push(now);
965
+
966
+ if (sessions[u] && sessions[u].user !== session) return res.jsonResponse({ error: 'Session ID mismatch' });
967
+
968
+ if (token) {
969
+ privateChats[token] = privateChats[token] || [];
970
+ privateChats[token].push(msg);
971
+ setTimeout(() => { delete privateChats[token]; }, 3600000);
972
+ } else {
973
+ chat.push(msg);
974
+ setTimeout(() => chat.splice(chat.indexOf(msg), 1), 3600000);
975
+ }
976
+
977
+ sessions[u] = sessions[u] || { user: session, last: now };
978
+ sessions[u].last = now;
979
+
980
+ setTimeout(() => { if (now - sessions[u].last >= 3600000) delete sessions[u]; }, 3600000);
981
+
982
+ res.jsonResponse({ message: 'Message sent successfully' });
983
+ });
984
+
985
+ // Display a private chat with a token
986
+ app.post('/:version/chat/private', (req, res) => {
987
+ const { username, token } = req.body;
988
+
989
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
990
+ if (!token) return res.jsonResponse({ error: 'Please provide a valid token (&token={key}).' });
991
+
992
+ const u = username.toLowerCase(), now = Date.now();
993
+
994
+ rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
995
+ if (rateLimits[u].length > 50) {
996
+ const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
997
+ return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
998
+ }
999
+ rateLimits[u].push(now);
1000
+
1001
+ if (privateChats[token]) return res.jsonResponse(privateChats[token]);
1002
+
1003
+ return res.jsonResponse({ error: 'Invalid or expired token.' });
1004
+ });
1005
+
1006
+ // Generate hash
1007
+ app.post('/:version/hash', (req, res) => {
1008
+ const { text, method } = req.body;
1009
+ const { version } = req.params;
1010
+
1011
+ if (!text) return res.jsonResponse({ error: 'Please provide a text (?text={text})' });
1012
+ if (!method) return res.jsonResponse({
1013
+ error: 'Please provide a valid hash algorithm (&method={algorithm})',
1014
+ documentation: `https://docs.sylvain.pro/${version}/en/hash`
1015
+ });
1016
+
1017
+ const methods = getHashes();
1018
+ if (!methods.includes(method)) return res.jsonResponse({ error: `Unsupported method. Use one of: ${methods.join(', ')}` });
1019
+
1020
+ const hash = createHash(method).update(text).digest('hex');
1021
+ res.jsonResponse({ method, hash });
1022
+ });
1023
+
1024
+ // Display a planning from an ICS file
1025
+ app.post('/:version/hyperplanning', async (req, res) => {
1026
+ const { url, detail } = req.body;
1027
+
1028
+ if (!url) return res.jsonResponse({ error: 'Please provide a valid ICS file URL.' });
1029
+
1030
+ try {
1031
+ const response = await fetch(url);
1032
+ if (!response.ok || !(response.headers.get('content-type') || '').includes('text/calendar')) return res.jsonResponse({ error: 'Invalid ICS file format.' });
1033
+
1034
+ const events = new ical.Component(ical.parse(await response.text()))
1035
+ .getAllSubcomponents('vevent')
1036
+ .map(e => {
1037
+ const evt = new ical.Event(e);
1038
+ const summary = evt.summary.split(' ').filter(part => part !== '-');
1039
+ const start = formatDate(evt.startDate.toJSDate());
1040
+ const end = formatDate(evt.endDate.toJSDate());
1041
+
1042
+ if (detail === 'full') {
1043
+ const desc = evt.description.split('\n').map(l => l.trim());
1044
+ const extract = (p) => (desc.find(l => l.startsWith(p)) || '').replace(p, '').trim();
1045
+
1046
+ return {
1047
+ summary,
1048
+ subject: extract('Matière :'),
1049
+ teacher: extract('Enseignant :'),
1050
+ classes: extract('Promotions :').split(', ').map(c => c.trim()),
1051
+ type: extract('Salle :') || undefined,
1052
+ start,
1053
+ end
1054
+ };
1055
+ }
1056
+ if (detail === 'list') return { summary, start, end };
1057
+
1058
+ return { summary: evt.summary, start, end };
1059
+ })
1060
+ .sort((a, b) => new Date(a.start) - new Date(b.start))
1061
+ .filter(e => new Date(e.end) >= new Date());
1062
+
1063
+ res.jsonResponse(events);
1064
+ } catch { res.jsonResponse({ error: 'Failed to parse ICS file.' }); }
1065
+ });
1066
+
1067
+ // Store tic tac toe games
1068
+ app.post('/:version/tic-tac-toe', (req, res) => {
1069
+ const { username, move, session, game } = req.body;
1070
+
1071
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
1072
+ if (!move) return res.jsonResponse({ error: 'Please provide a valid move (&move={move})' });
1073
+ if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
1074
+ if (!game) return res.jsonResponse({ error: 'Please provide a valid game ID (&game={ID})' });
1075
+
1076
+ const u = username.toLowerCase(), now = Date.now();
1077
+ const play = { username, move, session };
1078
+ const validMoves = ['1-1', '1-2', '1-3', '2-1', '2-2', '2-3', '3-1', '3-2', '3-3'];
1079
+
1080
+ if (!validMoves.includes(move)) return res.jsonResponse({ error: 'Invalid move. Please provide a valid move (e.g., 1-1, 2-2, 3-3).' });
1081
+
1082
+ rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
1083
+ if (rateLimits[u].length > 50) {
1084
+ const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
1085
+ return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
1086
+ }
1087
+ rateLimits[u].push(now);
1088
+
1089
+ if (sessions[u] && sessions[u].user !== session) return res.jsonResponse({ error: 'Session ID mismatch' });
1090
+
1091
+ games[game] = games[game] || [];
1092
+
1093
+ const players = [...new Set(games[game].map(play => play.username))];
1094
+ if (players.length >= 2 && !players.includes(username)) return res.jsonResponse({ error: 'Game is full, you can only watch.' });
1095
+ 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.' });
1096
+ if (games[game].some(play => play.move === move)) return res.jsonResponse({ error: 'Move already made. Please choose a different move.' });
1097
+
1098
+ games[game].push(play);
1099
+
1100
+ const result = checkGame(games[game]);
1101
+ if (result.winner || result.tie) {
1102
+ setTimeout(() => delete games[game], 600000);
1103
+ return res.jsonResponse({
1104
+ message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : 'It\'s a tie.'}`,
1105
+ ...result
1106
+ });
1107
+ }
1108
+ if (!result.winner && !result.tie) setTimeout(() => delete games[game], 3600000);
1109
+
1110
+ sessions[u] = sessions[u] || { user: session, last: now };
1111
+ sessions[u].last = now;
1112
+
1113
+ setTimeout(() => { if (now - sessions[u].last >= 3600000) delete sessions[u]; }, 3600000);
1114
+
1115
+ res.jsonResponse({ message: 'Move sent successfully' });
1116
+ });
1117
+
1118
+ // Display a tic tac toe game with a token
1119
+ app.post('/:version/tic-tac-toe/fetch', (req, res) => {
1120
+ const { username, game } = req.body;
1121
+
1122
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
1123
+
1124
+ const ID = game || genID();
1125
+ const u = username.toLowerCase(), now = Date.now();
1126
+
1127
+ rateLimits[u] = (rateLimits[u] || []).filter(ts => now - ts < 10000);
1128
+ if (rateLimits[u].length > 50) {
1129
+ const remainingTime = Math.ceil((rateLimits[u][0] + 10000 - now) / 1000);
1130
+ return res.jsonResponse({ error: `Rate limit exceeded. Try again in ${remainingTime} seconds.` });
1131
+ }
1132
+ rateLimits[u].push(now);
1133
+
1134
+ if (!games[ID]) games[ID] = [];
1135
+
1136
+ const data = games[ID];
1137
+ const last = data.length ? data[data.length - 1].username : null;
1138
+ const players = [...new Set(data.map(p => p.username))];
1139
+ const turn = players.find(p => p !== last);
1140
+ const result = data.length ? checkGame(data) : {};
1141
+
1142
+ res.jsonResponse({ game: data, turn, ID, ...result });
1143
+ });
1144
+
1145
+ // Generate Token
1146
+ app.post('/:version/token', (req, res) => {
1147
+ let { len, type } = req.body;
1148
+
1149
+ len = parseInt(len || 24, 10);
1150
+ type = type ? type.toLowerCase() : 'alpha';
1151
+
1152
+ if (isNaN(len) || len < 0) return res.jsonResponse({ error: 'Invalid number.' });
1153
+ if (len > 4096) return res.jsonResponse({ error: 'Length cannot exceed 4096.' });
1154
+ if (len < 12) return res.jsonResponse({ error: 'Length cannot be less than 12.' });
1155
+
1156
+ const token = {
1157
+ alpha: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', len),
1158
+ alphanum: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len),
1159
+ base64: randomBytes(len).toString('base64').slice(0, len),
1160
+ hex: randomBytes(len).toString('hex').slice(0, len),
1161
+ num: genToken('0123456789', len),
1162
+ punct: genToken('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', len),
1163
+ urlsafe: genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_', len),
1164
+ uuid: v4().replace(/-/g, '').slice(0, len)
1165
+ }[type] || genToken('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', len);
1166
+
1167
+ res.jsonResponse({ token });
1168
+ });
1169
+
1170
+ // ----------- ----------- SERVER SETUP ----------- ----------- //
1171
+
1172
+ app.listen(3000, () => console.log('API is running on\n - http://127.0.0.1:3000\n - http://localhost:3000'));