@20syldev/api 3.4.0 → 3.4.1

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,817 +1,818 @@
1
- // Import modules
2
- import * as apiv3 from './modules/v3.js';
3
-
4
- // Import dependencies
5
- import cors from 'cors';
6
- import dotenv from 'dotenv';
7
- import express from 'express';
8
- import fetch from 'node-fetch';
9
- import { urlencoded, json } from 'express';
10
- import { dirname, join } from 'path';
11
- import { fileURLToPath } from 'url';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const app = express();
16
-
17
- // Define allowed versions & endpoints for each version
18
- const v1 = {
19
- get: [
20
- { name: 'algorithms', path: '/algorithms?method={algorithm}&value={value}(&value2={value2})' },
21
- { name: 'captcha', path: '/captcha?text={text}' },
22
- { name: 'color', path: '/color' },
23
- { name: 'convert', path: '/convert?value={value}&from={unit}&to={unit}' },
24
- { name: 'domain', path: '/domain' },
25
- { name: 'infos', path: '/infos' },
26
- { name: 'personal', path: '/personal' },
27
- { name: 'qrcode', path: '/qrcode?url={URL}' },
28
- { name: 'username', path: '/username' },
29
- { name: 'website', path: '/website' }
30
- ],
31
- post: [
32
- { name: 'token', path: '/token' }
33
- ]
34
- };
35
- const v2 = {
36
- get: [
37
- ...v1.get,
38
- { name: 'chat', path: '/chat' }
39
- ],
40
- post: [
41
- ...v1.post,
42
- {
43
- name: 'chat',
44
- children: {
45
- chat: '/chat',
46
- private: '/chat/private'
47
- }
48
- },
49
- { name: 'hash', path: '/hash' },
50
- {
51
- name: 'tic_tac_toe',
52
- children: {
53
- tic_tac_toe: '/tic-tac-toe',
54
- fetch: '/tic-tac-toe/fetch'
55
- }
56
- },
57
- { name: 'token', path: '/token' }
58
- ]
59
- };
60
- const v3 = {
61
- get: [
62
- ...v2.get,
63
- { name: 'levenshtein', path: '/levenshtein?str1={string}&str2={string}' },
64
- { name: 'time', path: '/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})' },
65
- ],
66
- post: [
67
- ...v2.post,
68
- { name: 'hyperplanning', path: '/hyperplanning' }
69
- ]
70
- };
71
- const versions = {
72
- v1: {
73
- endpoints: v1,
74
- modules: apiv3
75
- },
76
- v2: {
77
- endpoints: v2,
78
- modules: apiv3
79
- },
80
- v3: {
81
- endpoints: v3,
82
- modules: apiv3
83
- }
84
- };
85
-
86
- // API storage
87
- const logs = [], ipLimits = {};
88
-
89
- // Chat storage
90
- const chatStorage = {
91
- messages: [],
92
- privateChats: {},
93
- sessions: {},
94
- rateLimits: {}
95
- };
96
-
97
- // Tic-Tac-Toe storage
98
- const ticTacToeStorage = {
99
- games: {},
100
- sessions: {},
101
- rateLimits: {}
102
- };
103
-
104
- // Define global variables
105
- let contributions, lastFetch = 0, requests = 0, requestLimit, resetTime = Date.now() + 3600000;
106
-
107
- // ----------- ----------- MIDDLEWARES SETUP ----------- ----------- //
108
-
109
- dotenv.config();
110
-
111
- // CORS & Express setup
112
- app.set('trust proxy', 1);
113
- app.use(cors({ methods: ['GET', 'POST'] }));
114
- app.use(urlencoded({ extended: true }));
115
- app.use(json());
116
-
117
- // Set favicon for API
118
- app.use('/favicon.ico', express.static(join(__dirname, 'src', 'favicon.ico')));
119
-
120
- // Display robots.txt
121
- app.use('/robots.txt', express.static(join(__dirname, 'robots.txt')));
122
-
123
- // Return formatted JSON
124
- app.use((req, res, next) => {
125
- res.setHeader('Content-Type', 'application/json');
126
- res.jsonResponse = (data) => {
127
- res.send(JSON.stringify(data, null, 2));
128
- };
129
- next();
130
- });
131
-
132
- // Too many requests
133
- app.use((req, res, next) => {
134
- const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
135
- const token = req.headers.authorization?.split(' ')[1] || '';
136
-
137
- const now = Date.now();
138
- const minute = Math.floor(now / 60000) % 60;
139
- const hour = Math.floor(now / 3600000) % 24;
140
-
141
- const business = process.env.BUSINESS_TOKEN_LIST?.split(' ') || [];
142
- const pro = process.env.PRO_TOKEN_LIST?.split(' ') || [];
143
- const advanced = process.env.ADVANCED_TOKEN_LIST?.split(' ') || [];
144
-
145
- if (token && ![...business, ...pro, ...advanced].includes(token) || token === 'undefined') {
146
- return res.status(401).jsonResponse({
147
- message: 'Unauthorized',
148
- error: 'Invalid token.',
149
- status: '401'
150
- });
151
- }
152
-
153
- if (business.includes(token) && !business.includes('undefined')) requestLimit = process.env.BUSINESS_LIMIT;
154
- else if (pro.includes(token) && !pro.includes('undefined')) requestLimit = process.env.PRO_LIMIT;
155
- else if (advanced.includes(token) && !advanced.includes('undefined')) requestLimit = process.env.ADVANCED_LIMIT;
156
- else requestLimit = process.env.DEFAULT_LIMIT;
157
-
158
- if (now > resetTime) requests = 0, resetTime = now + 3600000;
159
- if (++requests > Math.max(process.env.GLOBAL_LIMIT, requestLimit)) {
160
- return res.status(429).jsonResponse({ message: 'Too Many Requests' });
161
- }
162
-
163
- if (['__proto__', 'constructor', 'prototype'].includes(ip)) {
164
- return res.status(400).jsonResponse({ message: 'Invalid IP address' });
165
- }
166
-
167
- if (!ipLimits[ip]) ipLimits[ip] = {};
168
- if (!ipLimits[ip][hour]) ipLimits[ip][hour] = {};
169
- ipLimits[ip][hour][minute] = (ipLimits[ip][hour][minute] || 0) + 1;
170
-
171
- if (ipLimits[ip][hour][minute] > requestLimit) {
172
- return res.status(429).jsonResponse({
173
- message: 'Too Many Requests (IP limited), authenticate to increase the limit',
174
- error: `You have exceeded the limit of ${requestLimit} requests per hour.`,
175
- reset: `Reset in ${((resetTime - now) / 60000).toFixed(0)} minutes.`,
176
- status: '429'
177
- });
178
- }
179
-
180
- Object.keys(ipLimits[ip]).forEach(h => { if (h != hour) delete ipLimits[ip][h]; });
181
-
182
- next();
183
- });
184
-
185
- // Save and send logs
186
- app.use((req, res, next) => {
187
- if (req.method === 'HEAD') return next();
188
- if (req.originalUrl === '/logs') return next();
189
-
190
- const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
191
-
192
- const startTime = Date.now();
193
- const timestamp = new Date().toISOString();
194
- const method = req.method;
195
- const url = req.originalUrl;
196
- const platform = req.headers['sec-ch-ua-platform']?.replace(/"/g, '');
197
-
198
- res.on('finish', () => {
199
- const status = res.statusCode === 304 ? 200 : res.statusCode;
200
- const duration = `${Date.now() - startTime}ms`;
201
-
202
- logs.push({ timestamp, method, url, status, duration, platform });
203
- console.log(`[${new Date().toISOString()}] ${method} ${url} ${res.statusCode} - ${duration} - ${ip}`);
204
- if (logs.length > 1000) logs.shift();
205
- });
206
- next();
207
- });
208
-
209
- // Internal Server Error
210
- app.use((err, req, res, next) => {
211
- console.error(err.stack);
212
- res.status(500).jsonResponse({
213
- message: 'Internal Server Error',
214
- error: err.message,
215
- documentation: 'https://docs.sylvain.pro',
216
- status: '500'
217
- });
218
- });
219
-
220
- // Check if version exists
221
- app.use('/:version', (req, res, next) => {
222
- const { version } = req.params;
223
- const latest = Object.keys(versions).pop();
224
- const endpoint = req.originalUrl.split('/').slice(2).join('/');
225
-
226
- req.version = version;
227
- req.latest = latest;
228
-
229
- if (['latest', 'fr', 'en'].includes(version)) {
230
- return res.redirect(endpoint ? `/${latest}/${endpoint}` : `/${latest}`);
231
- }
232
-
233
- if (version === 'logs') return res.jsonResponse(logs);
234
-
235
- if (!versions[version] && version !== 'logs') {
236
- return res.status(404).jsonResponse({
237
- message: 'Not Found',
238
- error: `Invalid API version (${version}).`,
239
- documentation: `https://docs.sylvain.pro/${latest}`,
240
- status: '404'
241
- });
242
- }
243
-
244
- req.module = versions[version].modules;
245
-
246
- if (!req.module) {
247
- return res.status(404).jsonResponse({
248
- message: 'Not Found',
249
- error: `Module not found for version ${version}.`,
250
- documentation: `https://docs.sylvain.pro/${latest}`,
251
- status: '404'
252
- });
253
- }
254
-
255
- next();
256
- });
257
-
258
- // Check if endpoint exists
259
- app.use('/:version/:endpoint', (req, res, next) => {
260
- const { version, endpoint } = req.params;
261
-
262
- req.version = version;
263
- req.endpoint = endpoint;
264
-
265
- if (!req.endpoint) {
266
- return res.status(404).jsonResponse({
267
- message: 'Not Found',
268
- error: `Endpoint '${endpoint}' does not exist in ${req.version}.`,
269
- documentation: `https://docs.sylvain.pro/${versions[version.length - 1]}`,
270
- status: '404'
271
- });
272
- }
273
- next();
274
- });
275
-
276
- // ----------- ----------- MAIN ENDPOINTS ----------- ----------- //
277
-
278
- // Main route
279
- app.get('/', (req, res) => {
280
- const base = `${req.protocol}://${req.get('host')}`;
281
- const links = Object.keys(versions).reduce((link, version) => {
282
- link[version] = `${base}/${version}`;
283
- return link;
284
- }, {});
285
-
286
- res.jsonResponse({
287
- documentation: 'https://docs.sylvain.pro',
288
- latest: `${base}/latest`,
289
- logs: `${base}/logs`,
290
- versions: links
291
- });
292
- });
293
-
294
- // Display version information
295
- app.get('/:version', (req, res) => {
296
- const { version } = req.params;
297
-
298
- if (!versions[version]) {
299
- return res.status(404).jsonResponse({
300
- message: 'Not Found',
301
- error: `Invalid API version (${version}).`,
302
- documentation: `https://docs.sylvain.pro/${versions[versions.length - 1]}`,
303
- status: '404'
304
- });
305
- }
306
-
307
- const endpoints = Object.keys(versions[version].endpoints).reduce((acc, method) => {
308
- acc[method] = versions[version].endpoints[method]
309
- .filter(({ name }) => name !== 'website')
310
- .sort((a, b) => a.name.localeCompare(b.name))
311
- .reduce((group, endpoint) => {
312
- if (endpoint.children) {
313
- group[endpoint.name] = Object.keys(endpoint.children)
314
- .sort((a, b) => a.localeCompare(b))
315
- .reduce((childGroup, childName) => {
316
- childGroup[childName] = `/${version}${endpoint.children[childName]}`;
317
- return childGroup;
318
- }, {});
319
- } else {
320
- group[endpoint.name] = `/${version}${endpoint.path}`;
321
- }
322
- return group;
323
- }, {});
324
- return acc;
325
- }, {});
326
-
327
- res.jsonResponse({
328
- version,
329
- documentation: `https://docs.sylvain.pro/${version}`,
330
- endpoints
331
- });
332
- });
333
-
334
- // ----------- ----------- GET ENDPOINTS ----------- ----------- //
335
-
336
- // Algorithms
337
- app.get('/:version/algorithms', (req, res) => {
338
- const { method, value, value2 } = req.query;
339
- const { version } = req.params;
340
-
341
- if (!req.module.algorithms || !req.module.algorithms[method]) {
342
- return res.jsonResponse({
343
- error: 'Please provide a valid algorithm (?method={algorithm})',
344
- documentation: `https://docs.sylvain.pro/${version}/en/algorithms`
345
- });
346
- }
347
-
348
- try {
349
- const answer = req.module.algorithms[method](value, value2);
350
- res.jsonResponse({ answer });
351
- } catch (err) {
352
- res.jsonResponse({
353
- error: err.message,
354
- documentation: `https://docs.sylvain.pro/${version}/en/algorithms`
355
- });
356
- }
357
- });
358
-
359
- // Generate captcha
360
- app.get('/:version/captcha', (req, res) => {
361
- const text = req.query.text;
362
-
363
- if (!text) return res.jsonResponse({ error: 'Please provide a valid argument (?text={text})' });
364
-
365
- try {
366
- const result = req.module.captcha(text);
367
- res.type('png').send(result);
368
- } catch (err) {
369
- res.jsonResponse({
370
- error: err.message,
371
- documentation: `https://docs.sylvain.pro/${req.version}/en/captcha`
372
- });
373
- }
374
- });
375
-
376
- // Display stored data
377
- app.get('/:version/chat', (req, res) => {
378
- try {
379
- const messages = req.module.chat('fetch', {
380
- username: 'system',
381
- storage: chatStorage
382
- });
383
- res.jsonResponse(messages);
384
- } catch (err) {
385
- res.jsonResponse({ error: err.message });
386
- }
387
- });
388
-
389
- // GET private chat error
390
- app.get('/:version/chat/private', (req, res) => {
391
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
392
- });
393
-
394
- // Generate color
395
- app.get('/:version/color', (req, res) => {
396
- try {
397
- const result = req.module.color();
398
- res.jsonResponse(result);
399
- } catch (err) {
400
- res.jsonResponse({
401
- error: err.message,
402
- documentation: `https://docs.sylvain.pro/${req.version}/en/color`
403
- });
404
- }
405
- });
406
-
407
- // Convert units
408
- app.get('/:version/convert', (req, res) => {
409
- const { value, from, to } = req.query;
410
-
411
- if (!value || isNaN(value)) return res.jsonResponse({ error: 'Please provide a valid value (?value={value})' });
412
- if (!from) return res.jsonResponse({ error: 'Please provide a valid source unit (&from={unit})' });
413
- if (!to) return res.jsonResponse({ error: 'Please provide a valid target unit (&to={unit})' });
414
-
415
- try {
416
- const result = req.module.convert(value, from, to);
417
- res.jsonResponse(result);
418
- } catch (err) {
419
- res.jsonResponse({
420
- error: err.message,
421
- documentation: `https://docs.sylvain.pro/${req.version}/en/convert`
422
- });
423
- }
424
- });
425
-
426
- // Generate domain informations
427
- app.get('/:version/domain', (req, res) => {
428
- try {
429
- const result = req.module.domain();
430
- res.jsonResponse(result);
431
- } catch (err) {
432
- res.jsonResponse({
433
- error: err.message,
434
- documentation: `https://docs.sylvain.pro/${req.version}/en/domain`
435
- });
436
- }
437
- });
438
-
439
- // GET planning error
440
- app.get('/:version/hyperplanning', (req, res) => {
441
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
442
- });
443
-
444
- // GET hash error
445
- app.get('/:version/hash', (req, res) => {
446
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
447
- });
448
-
449
- // Display API informations
450
- app.get('/:version/infos', (req, res) => {
451
- const endpoints = Object.values(versions[req.version].endpoints).flat().reduce((total, endpoint) => {
452
- if (endpoint.children) return total + Object.keys(endpoint.children).length;
453
- return total + 1;
454
- }, 0);
455
-
456
- res.jsonResponse({
457
- endpoints,
458
- last_version: Object.keys(versions).pop(),
459
- documentation: 'https://docs.sylvain.pro',
460
- github: 'https://github.com/20syldev/api',
461
- creation: 'November 25th 2024',
462
- });
463
- });
464
-
465
- // Calculate Levenshtein distance
466
- app.get('/:version/levenshtein', (req, res) => {
467
- const { str1, str2 } = req.query;
468
-
469
- if (!str1 || typeof str1 !== 'string') return res.jsonResponse({ error: 'Please provide a first string (?str1={string})' });
470
- if (!str2 || typeof str2 !== 'string') return res.jsonResponse({ error: 'Please provide a second string (&str2={string})' });
471
-
472
- try {
473
- const result = req.module.levenshtein(str1, str2);
474
- res.jsonResponse(result);
475
- } catch (err) {
476
- res.jsonResponse({
477
- error: err.message,
478
- documentation: `https://docs.sylvain.pro/${req.version}/en/levenshtein`
479
- });
480
- }
481
- });
482
-
483
- // Generate personal data
484
- app.get('/:version/personal', (req, res) => {
485
- try {
486
- const result = req.module.personal();
487
- res.jsonResponse(result);
488
- } catch (err) {
489
- res.jsonResponse({
490
- error: err.message,
491
- documentation: `https://docs.sylvain.pro/${req.version}/en/personal`
492
- });
493
- }
494
- });
495
-
496
- // Generate QR Code
497
- app.get('/:version/qrcode', async (req, res) => {
498
- const { url } = req.query;
499
-
500
- if (!url) return res.jsonResponse({ error: 'Please provide a valid url (?url={URL})' });
501
-
502
- try {
503
- const result = await req.module.qrcode(url);
504
- res.jsonResponse(result);
505
- } catch (err) {
506
- res.jsonResponse({
507
- error: err.message,
508
- documentation: `https://docs.sylvain.pro/${req.version}/en/qrcode`
509
- });
510
- }
511
- });
512
-
513
- // GET tic-tac-toe game error
514
- app.get('/:version/tic-tac-toe', (req, res) => {
515
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
516
- });
517
-
518
- // GET tic-tac-toe fetch error
519
- app.get('/:version/tic-tac-toe/fetch', (req, res) => {
520
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
521
- });
522
-
523
- // Display or generate time informations
524
- app.get('/:version/time', (req, res) => {
525
- const { type = 'live', start, end, format, timezone } = req.query;
526
-
527
- const validFormats = ['iso', 'utc', 'timestamp', 'locale', 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'second', 'ms', 'dayOfWeek', 'dayOfYear', 'weekNumber', 'timezone', 'timezoneOffset'];
528
- const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
529
-
530
- if (type !== 'live' && type !== 'random') return res.jsonResponse({ error: 'Please provide a valid type (?type={type})' });
531
- if (start && !Date.parse(start)) return res.jsonResponse({ error: 'Please provide a valid start date (?start={YYYY-MM-DD})' });
532
- if (end && !Date.parse(end)) return res.jsonResponse({ error: 'Please provide a valid end date (?end={YYYY-MM-DD})' });
533
- if (format && !validFormats.includes(format)) return res.jsonResponse({ error: 'Please provide a valid format (?format={format})' });
534
- if (timezone && !validTimezones.includes(timezone)) return res.jsonResponse({ error: 'Please provide a valid timezone (?timezone={timezone})' });
535
-
536
- try {
537
- const time = req.module.time(type, start, end, format, timezone);
538
- res.jsonResponse(time);
539
- } catch (err) {
540
- res.jsonResponse({
541
- error: err.message,
542
- documentation: `https://docs.sylvain.pro/${req.version}/en/time`
543
- });
544
- }
545
- });
546
-
547
- // GET token error
548
- app.get('/:version/token', (req, res) => {
549
- res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
550
- });
551
-
552
- // Generate username
553
- app.get('/:version/username', (req, res) => {
554
- try {
555
- const result = req.module.username();
556
- res.jsonResponse(result);
557
- } catch (err) {
558
- res.jsonResponse({
559
- error: err.message,
560
- documentation: `https://docs.sylvain.pro/${req.version}/en/username`
561
- });
562
- }
563
- });
564
-
565
- // Display informations for owner's website
566
- app.get('/:version/website', async (req, res) => {
567
- const currentTime = Date.now();
568
-
569
- if (currentTime - lastFetch >= 10 * 60 * 1000) {
570
- try {
571
- const username = '20syldev';
572
- const token = process.env.GITHUB_TOKEN;
573
- const today = new Date().toISOString().split('T')[0];
574
- const monthFirst = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
575
- const lastYear = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split('T')[0];
576
-
577
- const query = `
578
- {
579
- user(login: "${username}") {
580
- contributionsCollection(from: "${today}T00:00:00Z") {
581
- contributionCalendar {
582
- totalContributions
583
- }
584
- }
585
- contributions_month: contributionsCollection(from: "${monthFirst}T00:00:00Z") {
586
- contributionCalendar {
587
- totalContributions
588
- }
589
- }
590
- contributions_year: contributionsCollection(from: "${lastYear}T00:00:00Z") {
591
- contributionCalendar {
592
- totalContributions
593
- }
594
- }
595
- }
596
- }`;
597
-
598
- const apiResponse = await fetch('https://api.github.com/graphql', {
599
- method: 'POST',
600
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
601
- body: JSON.stringify({ query })
602
- });
603
-
604
- if (!apiResponse.ok) throw new Error('Error fetching data.');
605
-
606
- const data = await apiResponse.json();
607
- const user = data?.data?.user;
608
-
609
- contributions = {
610
- today: user?.contributionsCollection?.contributionCalendar?.totalContributions || 0,
611
- month: user?.contributions_month?.contributionCalendar?.totalContributions || 0,
612
- year: user?.contributions_year?.contributionCalendar?.totalContributions || 0,
613
- };
614
-
615
- lastFetch = currentTime;
616
- } catch { contributions = { today: 0, month: 0, year: 0 }; }
617
- }
618
-
619
- res.jsonResponse({
620
- versions: {
621
- api: process.env.API,
622
- cdn: process.env.CDN,
623
- coop_api: process.env.COOP_API,
624
- coop_status: process.env.COOP_STATUS,
625
- chat: process.env.CHAT,
626
- digit: process.env.DIGIT,
627
- doc_coopbot: process.env.DOC_COOPBOT,
628
- docs: process.env.DOCS,
629
- donut: process.env.DONUT,
630
- drawio_plugin: process.env.DRAWIO_PLUGIN,
631
- flowers: process.env.FLOWERS,
632
- gemsync: process.env.GEMSYNC,
633
- gitsite: process.env.GITSITE,
634
- lebonchar: process.env.LEBONCHAR,
635
- logs: process.env.LOGS,
636
- logvault: process.env.LOGVAULT,
637
- lyah: process.env.LYAH,
638
- minify: process.env.MINIFY,
639
- morpion: process.env.MORPION,
640
- nitrogen: process.env.NITROGEN,
641
- old_database: process.env.OLD_DATABASE,
642
- php: process.env.PHP,
643
- ping: process.env.PING,
644
- portfolio: process.env.PORTFOLIO,
645
- python_api: process.env.PYTHON_API,
646
- readme: process.env.README,
647
- terminal: process.env.TERMINAL,
648
- wrkit: process.env.WRKIT,
649
- zpki: process.env.ZPKI
650
- },
651
- patched_projects: process.env.PATCH?.split(' ') || [],
652
- updated_projects: process.env.RECENT?.split(' ') || [],
653
- new_projects: process.env.NEW?.split(' ') || [],
654
- sub_domains: process.env.DOMAINS?.split(' ') || [],
655
- stats: {
656
- 1: process.env.STATS1,
657
- 2: process.env.STATS2,
658
- 3: process.env.STATS3,
659
- 4: process.env.STATS4,
660
- 5: Object.keys(ipLimits).length,
661
- today: contributions.today.toString(),
662
- this_month: contributions.month.toString(),
663
- last_year: contributions.year.toString(),
664
- },
665
- notif_tag: process.env.TAG,
666
- active: process.env.ACTIVE
667
- });
668
- });
669
-
670
- // ----------- ----------- POST ENDPOINTS ----------- ----------- //
671
-
672
- // Store chat messages
673
- app.post('/:version/chat', (req, res) => {
674
- const { username, message, timestamp, session, token } = req.body;
675
-
676
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
677
- if (!message) return res.jsonResponse({ error: 'Please provide a message (&message={message})' });
678
- if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
679
-
680
- try {
681
- const result = req.module.chat('message', {
682
- username,
683
- message,
684
- timestamp,
685
- session,
686
- token,
687
- storage: chatStorage
688
- });
689
- res.jsonResponse(result);
690
- } catch (err) {
691
- res.jsonResponse({ error: err.message });
692
- }
693
- });
694
-
695
- // Display a private chat with a token
696
- app.post('/:version/chat/private', (req, res) => {
697
- const { username, token } = req.body;
698
-
699
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
700
- if (!token) return res.jsonResponse({ error: 'Please provide a valid token (&token={key}).' });
701
-
702
- try {
703
- const messages = req.module.chat('private', {
704
- username,
705
- token,
706
- storage: chatStorage
707
- });
708
- res.jsonResponse(messages);
709
- } catch (err) {
710
- res.jsonResponse({ error: err.message });
711
- }
712
- });
713
-
714
- // Generate hash
715
- app.post('/:version/hash', (req, res) => {
716
- const { text, method } = req.body;
717
-
718
- if (!text) return res.jsonResponse({ error: 'Please provide a text (?text={text})' });
719
- if (!method) return res.jsonResponse({
720
- error: 'Please provide a valid hash algorithm (&method={algorithm})',
721
- documentation: `https://docs.sylvain.pro/${req.version}/en/hash`
722
- });
723
-
724
- try {
725
- const result = req.module.hash(text, method);
726
- res.jsonResponse(result);
727
- } catch (err) {
728
- res.jsonResponse({
729
- error: err.message,
730
- documentation: `https://docs.sylvain.pro/${req.version}/en/hash`
731
- })
732
- }
733
- });
734
-
735
- // Display a planning from an ICS file
736
- app.post('/:version/hyperplanning', async (req, res) => {
737
- const { url, detail } = req.body;
738
-
739
- if (!url) return res.jsonResponse({ error: 'Please provide a valid ICS file URL (?url={URL})' });
740
-
741
- try {
742
- const hyperplanning = req.module.hyperplanning(url, detail);
743
- res.jsonResponse(hyperplanning);
744
- } catch (err){
745
- res.jsonResponse({
746
- error: 'Failed to parse ICS file.',
747
- documentation: `https://docs.sylvain.pro/${req.version}/en/hyperplanning`
748
- });
749
- }
750
- });
751
-
752
- // Store tic tac toe games
753
- app.post('/:version/tic-tac-toe', (req, res) => {
754
- const { username, move, session, game } = req.body;
755
-
756
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
757
- if (!move) return res.jsonResponse({ error: 'Please provide a valid move (&move={move})' });
758
- if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
759
- if (!game) return res.jsonResponse({ error: 'Please provide a valid game ID (&game={ID})' });
760
-
761
- try {
762
- const result = req.module.tic_tac_toe('play', {
763
- username,
764
- move,
765
- session,
766
- game,
767
- storage: ticTacToeStorage
768
- });
769
- res.jsonResponse(result);
770
- } catch (err) {
771
- res.jsonResponse({ error: err.message });
772
- }
773
- });
774
-
775
- // Display a tic tac toe game with a token
776
- app.post('/:version/tic-tac-toe/fetch', (req, res) => {
777
- const { username, game } = req.body;
778
-
779
- if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
780
-
781
- try {
782
- const result = req.module.tic_tac_toe('fetch', {
783
- username,
784
- game,
785
- storage: ticTacToeStorage
786
- });
787
- res.jsonResponse(result);
788
- } catch (err) {
789
- res.jsonResponse({ error: err.message });
790
- }
791
- });
792
-
793
- // Generate Token
794
- app.post('/:version/token', (req, res) => {
795
- let { len, type } = req.body;
796
-
797
- len = parseInt(len || 24, 10);
798
- type = type ? type.toLowerCase() : 'alpha';
799
-
800
- if (isNaN(len) || len < 0) return res.jsonResponse({ error: 'Invalid number.' });
801
- if (len > 4096) return res.jsonResponse({ error: 'Length cannot exceed 4096.' });
802
- if (len < 12) return res.jsonResponse({ error: 'Length cannot be less than 12.' });
803
-
804
- try {
805
- const token = req.module.token(len, type);
806
- res.jsonResponse({ token });
807
- } catch (err) {
808
- return res.jsonResponse({
809
- error: err.message,
810
- documentation: `https://docs.sylvain.pro/${req.version}/en/token`
811
- });
812
- }
813
- });
814
-
815
- // ----------- ----------- SERVER SETUP ----------- ----------- //
816
-
817
- app.listen(3000, () => console.log('API is running on\n - http://127.0.0.1:3000\n - http://localhost:3000'));
1
+ // Import modules
2
+ import * as apiv3 from './modules/v3.js';
3
+
4
+ // Import dependencies
5
+ import cors from 'cors';
6
+ import dotenv from 'dotenv';
7
+ import express from 'express';
8
+ import fetch from 'node-fetch';
9
+ import { urlencoded, json } from 'express';
10
+ import { dirname, join } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const app = express();
16
+
17
+ // Define allowed versions & endpoints for each version
18
+ const v1 = {
19
+ get: [
20
+ { name: 'algorithms', path: '/algorithms?method={algorithm}&value={value}(&value2={value2})' },
21
+ { name: 'captcha', path: '/captcha?text={text}' },
22
+ { name: 'color', path: '/color' },
23
+ { name: 'convert', path: '/convert?value={value}&from={unit}&to={unit}' },
24
+ { name: 'domain', path: '/domain' },
25
+ { name: 'infos', path: '/infos' },
26
+ { name: 'personal', path: '/personal' },
27
+ { name: 'qrcode', path: '/qrcode?url={URL}' },
28
+ { name: 'username', path: '/username' },
29
+ { name: 'website', path: '/website' }
30
+ ],
31
+ post: [
32
+ { name: 'token', path: '/token' }
33
+ ]
34
+ };
35
+ const v2 = {
36
+ get: [
37
+ ...v1.get,
38
+ { name: 'chat', path: '/chat' }
39
+ ],
40
+ post: [
41
+ ...v1.post,
42
+ {
43
+ name: 'chat',
44
+ children: {
45
+ chat: '/chat',
46
+ private: '/chat/private'
47
+ }
48
+ },
49
+ { name: 'hash', path: '/hash' },
50
+ {
51
+ name: 'tic_tac_toe',
52
+ children: {
53
+ tic_tac_toe: '/tic-tac-toe',
54
+ fetch: '/tic-tac-toe/fetch'
55
+ }
56
+ },
57
+ { name: 'token', path: '/token' }
58
+ ]
59
+ };
60
+ const v3 = {
61
+ get: [
62
+ ...v2.get,
63
+ { name: 'levenshtein', path: '/levenshtein?str1={string}&str2={string}' },
64
+ { name: 'time', path: '/time(?type={type}&start={timestamp}&end={timestamp}&format={format}&timezone={timezone})' },
65
+ ],
66
+ post: [
67
+ ...v2.post,
68
+ { name: 'hyperplanning', path: '/hyperplanning' }
69
+ ]
70
+ };
71
+ const versions = {
72
+ v1: {
73
+ endpoints: v1,
74
+ modules: apiv3
75
+ },
76
+ v2: {
77
+ endpoints: v2,
78
+ modules: apiv3
79
+ },
80
+ v3: {
81
+ endpoints: v3,
82
+ modules: apiv3
83
+ }
84
+ };
85
+
86
+ // API storage
87
+ const logs = [], ipLimits = {};
88
+
89
+ // Chat storage
90
+ const chatStorage = {
91
+ messages: [],
92
+ privateChats: {},
93
+ sessions: {},
94
+ rateLimits: {}
95
+ };
96
+
97
+ // Tic-Tac-Toe storage
98
+ const ticTacToeStorage = {
99
+ games: {},
100
+ sessions: {},
101
+ rateLimits: {}
102
+ };
103
+
104
+ // Define global variables
105
+ let contributions, lastFetch = 0, requests = 0, requestLimit, resetTime = Date.now() + 3600000;
106
+
107
+ // ----------- ----------- MIDDLEWARES SETUP ----------- ----------- //
108
+
109
+ dotenv.config();
110
+
111
+ // CORS & Express setup
112
+ app.set('trust proxy', 1);
113
+ app.use(cors({ methods: ['GET', 'POST'] }));
114
+ app.use(urlencoded({ extended: true }));
115
+ app.use(json());
116
+
117
+ // Set favicon for API
118
+ app.use('/favicon.ico', express.static(join(__dirname, 'src', 'favicon.ico')));
119
+
120
+ // Display robots.txt
121
+ app.use('/robots.txt', express.static(join(__dirname, 'robots.txt')));
122
+
123
+ // Return formatted JSON
124
+ app.use((req, res, next) => {
125
+ res.setHeader('Content-Type', 'application/json');
126
+ res.jsonResponse = (data) => {
127
+ res.send(JSON.stringify(data, null, 2));
128
+ };
129
+ next();
130
+ });
131
+
132
+ // Too many requests
133
+ app.use((req, res, next) => {
134
+ const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
135
+ const token = req.headers.authorization?.split(' ')[1] || '';
136
+
137
+ const now = Date.now();
138
+ const minute = Math.floor(now / 60000) % 60;
139
+ const hour = Math.floor(now / 3600000) % 24;
140
+
141
+ const business = process.env.BUSINESS_TOKEN_LIST?.split(' ') || [];
142
+ const pro = process.env.PRO_TOKEN_LIST?.split(' ') || [];
143
+ const advanced = process.env.ADVANCED_TOKEN_LIST?.split(' ') || [];
144
+
145
+ if (token && ![...business, ...pro, ...advanced].includes(token) || token === 'undefined') {
146
+ return res.status(401).jsonResponse({
147
+ message: 'Unauthorized',
148
+ error: 'Invalid token.',
149
+ status: '401'
150
+ });
151
+ }
152
+
153
+ if (business.includes(token) && !business.includes('undefined')) requestLimit = process.env.BUSINESS_LIMIT;
154
+ else if (pro.includes(token) && !pro.includes('undefined')) requestLimit = process.env.PRO_LIMIT;
155
+ else if (advanced.includes(token) && !advanced.includes('undefined')) requestLimit = process.env.ADVANCED_LIMIT;
156
+ else requestLimit = process.env.DEFAULT_LIMIT;
157
+
158
+ if (now > resetTime) requests = 0, resetTime = now + 3600000;
159
+ if (++requests > Math.max(process.env.GLOBAL_LIMIT, requestLimit)) {
160
+ return res.status(429).jsonResponse({ message: 'Too Many Requests' });
161
+ }
162
+
163
+ if (['__proto__', 'constructor', 'prototype'].includes(ip)) {
164
+ return res.status(400).jsonResponse({ message: 'Invalid IP address' });
165
+ }
166
+
167
+ if (!ipLimits[ip]) ipLimits[ip] = {};
168
+ if (!ipLimits[ip][hour]) ipLimits[ip][hour] = {};
169
+ ipLimits[ip][hour][minute] = (ipLimits[ip][hour][minute] || 0) + 1;
170
+
171
+ if (ipLimits[ip][hour][minute] > requestLimit) {
172
+ return res.status(429).jsonResponse({
173
+ message: 'Too Many Requests (IP limited), authenticate to increase the limit',
174
+ error: `You have exceeded the limit of ${requestLimit} requests per hour.`,
175
+ reset: `Reset in ${((resetTime - now) / 60000).toFixed(0)} minutes.`,
176
+ status: '429'
177
+ });
178
+ }
179
+
180
+ Object.keys(ipLimits[ip]).forEach(h => { if (h != hour) delete ipLimits[ip][h]; });
181
+
182
+ next();
183
+ });
184
+
185
+ // Save and send logs
186
+ app.use((req, res, next) => {
187
+ if (req.method === 'HEAD') return next();
188
+ if (req.originalUrl === '/logs') return next();
189
+
190
+ const ip = req.headers['cf-connecting-ip'] || req.socket.remoteAddress;
191
+
192
+ const startTime = Date.now();
193
+ const timestamp = new Date().toISOString();
194
+ const method = req.method;
195
+ const url = req.originalUrl;
196
+ const platform = req.headers['sec-ch-ua-platform']?.replace(/"/g, '');
197
+
198
+ res.on('finish', () => {
199
+ const status = res.statusCode === 304 ? 200 : res.statusCode;
200
+ const duration = `${Date.now() - startTime}ms`;
201
+
202
+ logs.push({ timestamp, method, url, status, duration, platform });
203
+ console.log(`[${new Date().toISOString()}] ${method} ${url} ${res.statusCode} - ${duration} - ${ip}`);
204
+ if (logs.length > 1000) logs.shift();
205
+ });
206
+ next();
207
+ });
208
+
209
+ // Internal Server Error
210
+ app.use((err, req, res, next) => {
211
+ console.error(err.stack);
212
+ res.status(500).jsonResponse({
213
+ message: 'Internal Server Error',
214
+ error: err.message,
215
+ documentation: 'https://docs.sylvain.pro',
216
+ status: '500'
217
+ });
218
+ });
219
+
220
+ // Check if version exists
221
+ app.use('/:version', (req, res, next) => {
222
+ const { version } = req.params;
223
+ const latest = Object.keys(versions).pop();
224
+ const endpoint = req.originalUrl.split('/').slice(2).join('/');
225
+
226
+ req.version = version;
227
+ req.latest = latest;
228
+
229
+ if (['latest', 'fr', 'en'].includes(version)) {
230
+ return res.redirect(endpoint ? `/${latest}/${endpoint}` : `/${latest}`);
231
+ }
232
+
233
+ if (version === 'logs') return res.jsonResponse(logs);
234
+
235
+ if (!versions[version] && version !== 'logs') {
236
+ return res.status(404).jsonResponse({
237
+ message: 'Not Found',
238
+ error: `Invalid API version (${version}).`,
239
+ documentation: `https://docs.sylvain.pro/${latest}`,
240
+ status: '404'
241
+ });
242
+ }
243
+
244
+ req.module = versions[version].modules;
245
+
246
+ if (!req.module) {
247
+ return res.status(404).jsonResponse({
248
+ message: 'Not Found',
249
+ error: `Module not found for version ${version}.`,
250
+ documentation: `https://docs.sylvain.pro/${latest}`,
251
+ status: '404'
252
+ });
253
+ }
254
+
255
+ next();
256
+ });
257
+
258
+ // Check if endpoint exists
259
+ app.use('/:version/:endpoint', (req, res, next) => {
260
+ const { version, endpoint } = req.params;
261
+
262
+ req.version = version;
263
+ req.endpoint = endpoint;
264
+
265
+ if (!req.endpoint) {
266
+ return res.status(404).jsonResponse({
267
+ message: 'Not Found',
268
+ error: `Endpoint '${endpoint}' does not exist in ${req.version}.`,
269
+ documentation: `https://docs.sylvain.pro/${versions[version.length - 1]}`,
270
+ status: '404'
271
+ });
272
+ }
273
+ next();
274
+ });
275
+
276
+ // ----------- ----------- MAIN ENDPOINTS ----------- ----------- //
277
+
278
+ // Main route
279
+ app.get('/', (req, res) => {
280
+ const base = `${req.protocol}://${req.get('host')}`;
281
+ const links = Object.keys(versions).reduce((link, version) => {
282
+ link[version] = `${base}/${version}`;
283
+ return link;
284
+ }, {});
285
+
286
+ res.jsonResponse({
287
+ documentation: 'https://docs.sylvain.pro',
288
+ latest: `${base}/latest`,
289
+ logs: `${base}/logs`,
290
+ versions: links
291
+ });
292
+ });
293
+
294
+ // Display version information
295
+ app.get('/:version', (req, res) => {
296
+ const { version } = req.params;
297
+
298
+ if (!versions[version]) {
299
+ return res.status(404).jsonResponse({
300
+ message: 'Not Found',
301
+ error: `Invalid API version (${version}).`,
302
+ documentation: `https://docs.sylvain.pro/${Object.keys(versions).pop()}`,
303
+ status: '404'
304
+ });
305
+ }
306
+
307
+ const endpoints = Object.keys(versions[version].endpoints).reduce((acc, method) => {
308
+ acc[method] = versions[version].endpoints[method]
309
+ .filter(({ name }) => name !== 'website')
310
+ .sort((a, b) => a.name.localeCompare(b.name))
311
+ .reduce((group, endpoint) => {
312
+ if (endpoint.children) {
313
+ group[endpoint.name] = Object.keys(endpoint.children)
314
+ .sort((a, b) => a.localeCompare(b))
315
+ .reduce((childGroup, childName) => {
316
+ childGroup[childName] = `/${version}${endpoint.children[childName]}`;
317
+ return childGroup;
318
+ }, {});
319
+ } else {
320
+ group[endpoint.name] = `/${version}${endpoint.path}`;
321
+ }
322
+ return group;
323
+ }, {});
324
+ return acc;
325
+ }, {});
326
+
327
+ res.jsonResponse({
328
+ version,
329
+ documentation: `https://docs.sylvain.pro/${version}`,
330
+ endpoints
331
+ });
332
+ });
333
+
334
+ // ----------- ----------- GET ENDPOINTS ----------- ----------- //
335
+
336
+ // Algorithms
337
+ app.get('/:version/algorithms', (req, res) => {
338
+ const { method, value, value2 } = req.query;
339
+ const { version } = req.params;
340
+
341
+ if (!req.module.algorithms || !req.module.algorithms[method]) {
342
+ return res.jsonResponse({
343
+ error: 'Please provide a valid algorithm (?method={algorithm})',
344
+ documentation: `https://docs.sylvain.pro/${version}/en/algorithms`
345
+ });
346
+ }
347
+
348
+ try {
349
+ const answer = req.module.algorithms[method](value, value2);
350
+ res.jsonResponse({ answer });
351
+ } catch (err) {
352
+ res.jsonResponse({
353
+ error: err.message,
354
+ documentation: `https://docs.sylvain.pro/${version}/en/algorithms`
355
+ });
356
+ }
357
+ });
358
+
359
+ // Generate captcha
360
+ app.get('/:version/captcha', (req, res) => {
361
+ const text = req.query.text;
362
+
363
+ if (!text) return res.jsonResponse({ error: 'Please provide a valid argument (?text={text})' });
364
+
365
+ try {
366
+ const result = req.module.captcha(text);
367
+ res.type('png').send(result);
368
+ } catch (err) {
369
+ res.jsonResponse({
370
+ error: err.message,
371
+ documentation: `https://docs.sylvain.pro/${req.version}/en/captcha`
372
+ });
373
+ }
374
+ });
375
+
376
+ // Display stored data
377
+ app.get('/:version/chat', (req, res) => {
378
+ try {
379
+ const messages = req.module.chat('fetch', {
380
+ username: 'system',
381
+ storage: chatStorage
382
+ });
383
+ res.jsonResponse(messages);
384
+ } catch (err) {
385
+ res.jsonResponse({ error: err.message });
386
+ }
387
+ });
388
+
389
+ // GET private chat error
390
+ app.get('/:version/chat/private', (req, res) => {
391
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
392
+ });
393
+
394
+ // Generate color
395
+ app.get('/:version/color', (req, res) => {
396
+ try {
397
+ const result = req.module.color();
398
+ res.jsonResponse(result);
399
+ } catch (err) {
400
+ res.jsonResponse({
401
+ error: err.message,
402
+ documentation: `https://docs.sylvain.pro/${req.version}/en/color`
403
+ });
404
+ }
405
+ });
406
+
407
+ // Convert units
408
+ app.get('/:version/convert', (req, res) => {
409
+ const { value, from, to } = req.query;
410
+
411
+ if (!value || isNaN(value)) return res.jsonResponse({ error: 'Please provide a valid value (?value={value})' });
412
+ if (!from) return res.jsonResponse({ error: 'Please provide a valid source unit (&from={unit})' });
413
+ if (!to) return res.jsonResponse({ error: 'Please provide a valid target unit (&to={unit})' });
414
+
415
+ try {
416
+ const result = req.module.convert(value, from, to);
417
+ res.jsonResponse(result);
418
+ } catch (err) {
419
+ res.jsonResponse({
420
+ error: err.message,
421
+ documentation: `https://docs.sylvain.pro/${req.version}/en/convert`
422
+ });
423
+ }
424
+ });
425
+
426
+ // Generate domain informations
427
+ app.get('/:version/domain', (req, res) => {
428
+ try {
429
+ const result = req.module.domain();
430
+ res.jsonResponse(result);
431
+ } catch (err) {
432
+ res.jsonResponse({
433
+ error: err.message,
434
+ documentation: `https://docs.sylvain.pro/${req.version}/en/domain`
435
+ });
436
+ }
437
+ });
438
+
439
+ // GET planning error
440
+ app.get('/:version/hyperplanning', (req, res) => {
441
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
442
+ });
443
+
444
+ // GET hash error
445
+ app.get('/:version/hash', (req, res) => {
446
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
447
+ });
448
+
449
+ // Display API informations
450
+ app.get('/:version/infos', (req, res) => {
451
+ const endpoints = Object.values(versions[req.version].endpoints).flat();
452
+
453
+ const paths = endpoints.flatMap(e => e.children
454
+ ? Object.values(e.children)
455
+ : (e.path ? [e.path] : []));
456
+
457
+ res.jsonResponse({
458
+ endpoints: new Set(paths).size,
459
+ last_version: Object.keys(versions).pop(),
460
+ documentation: 'https://docs.sylvain.pro',
461
+ github: 'https://github.com/20syldev/api',
462
+ creation: 'November 25th 2024',
463
+ });
464
+ });
465
+
466
+ // Calculate Levenshtein distance
467
+ app.get('/:version/levenshtein', (req, res) => {
468
+ const { str1, str2 } = req.query;
469
+
470
+ if (!str1 || typeof str1 !== 'string') return res.jsonResponse({ error: 'Please provide a first string (?str1={string})' });
471
+ if (!str2 || typeof str2 !== 'string') return res.jsonResponse({ error: 'Please provide a second string (&str2={string})' });
472
+
473
+ try {
474
+ const result = req.module.levenshtein(str1, str2);
475
+ res.jsonResponse(result);
476
+ } catch (err) {
477
+ res.jsonResponse({
478
+ error: err.message,
479
+ documentation: `https://docs.sylvain.pro/${req.version}/en/levenshtein`
480
+ });
481
+ }
482
+ });
483
+
484
+ // Generate personal data
485
+ app.get('/:version/personal', (req, res) => {
486
+ try {
487
+ const result = req.module.personal();
488
+ res.jsonResponse(result);
489
+ } catch (err) {
490
+ res.jsonResponse({
491
+ error: err.message,
492
+ documentation: `https://docs.sylvain.pro/${req.version}/en/personal`
493
+ });
494
+ }
495
+ });
496
+
497
+ // Generate QR Code
498
+ app.get('/:version/qrcode', async (req, res) => {
499
+ const { url } = req.query;
500
+
501
+ if (!url) return res.jsonResponse({ error: 'Please provide a valid url (?url={URL})' });
502
+
503
+ try {
504
+ const result = await req.module.qrcode(url);
505
+ res.jsonResponse(result);
506
+ } catch (err) {
507
+ res.jsonResponse({
508
+ error: err.message,
509
+ documentation: `https://docs.sylvain.pro/${req.version}/en/qrcode`
510
+ });
511
+ }
512
+ });
513
+
514
+ // GET tic-tac-toe game error
515
+ app.get('/:version/tic-tac-toe', (req, res) => {
516
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
517
+ });
518
+
519
+ // GET tic-tac-toe fetch error
520
+ app.get('/:version/tic-tac-toe/fetch', (req, res) => {
521
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
522
+ });
523
+
524
+ // Display or generate time informations
525
+ app.get('/:version/time', (req, res) => {
526
+ const { type = 'live', start, end, format, timezone } = req.query;
527
+
528
+ const validFormats = ['iso', 'utc', 'timestamp', 'locale', 'date', 'time', 'year', 'month', 'day', 'hour', 'minute', 'second', 'ms', 'dayOfWeek', 'dayOfYear', 'weekNumber', 'timezone', 'timezoneOffset'];
529
+ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
530
+
531
+ if (type !== 'live' && type !== 'random') return res.jsonResponse({ error: 'Please provide a valid type (?type={type})' });
532
+ if (start && !Date.parse(start)) return res.jsonResponse({ error: 'Please provide a valid start date (?start={YYYY-MM-DD})' });
533
+ if (end && !Date.parse(end)) return res.jsonResponse({ error: 'Please provide a valid end date (?end={YYYY-MM-DD})' });
534
+ if (format && !validFormats.includes(format)) return res.jsonResponse({ error: 'Please provide a valid format (?format={format})' });
535
+ if (timezone && !validTimezones.includes(timezone)) return res.jsonResponse({ error: 'Please provide a valid timezone (?timezone={timezone})' });
536
+
537
+ try {
538
+ const time = req.module.time(type, start, end, format, timezone);
539
+ res.jsonResponse(time);
540
+ } catch (err) {
541
+ res.jsonResponse({
542
+ error: err.message,
543
+ documentation: `https://docs.sylvain.pro/${req.version}/en/time`
544
+ });
545
+ }
546
+ });
547
+
548
+ // GET token error
549
+ app.get('/:version/token', (req, res) => {
550
+ res.jsonResponse({ error: 'This endpoint only supports POST requests.' });
551
+ });
552
+
553
+ // Generate username
554
+ app.get('/:version/username', (req, res) => {
555
+ try {
556
+ const result = req.module.username();
557
+ res.jsonResponse(result);
558
+ } catch (err) {
559
+ res.jsonResponse({
560
+ error: err.message,
561
+ documentation: `https://docs.sylvain.pro/${req.version}/en/username`
562
+ });
563
+ }
564
+ });
565
+
566
+ // Display informations for owner's website
567
+ app.get('/:version/website', async (req, res) => {
568
+ const currentTime = Date.now();
569
+
570
+ if (currentTime - lastFetch >= 10 * 60 * 1000) {
571
+ try {
572
+ const username = '20syldev';
573
+ const token = process.env.GITHUB_TOKEN;
574
+ const today = new Date().toISOString().split('T')[0];
575
+ const monthFirst = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
576
+ const lastYear = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split('T')[0];
577
+
578
+ const query = `
579
+ {
580
+ user(login: "${username}") {
581
+ contributionsCollection(from: "${today}T00:00:00Z") {
582
+ contributionCalendar {
583
+ totalContributions
584
+ }
585
+ }
586
+ contributions_month: contributionsCollection(from: "${monthFirst}T00:00:00Z") {
587
+ contributionCalendar {
588
+ totalContributions
589
+ }
590
+ }
591
+ contributions_year: contributionsCollection(from: "${lastYear}T00:00:00Z") {
592
+ contributionCalendar {
593
+ totalContributions
594
+ }
595
+ }
596
+ }
597
+ }`;
598
+
599
+ const apiResponse = await fetch('https://api.github.com/graphql', {
600
+ method: 'POST',
601
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
602
+ body: JSON.stringify({ query })
603
+ });
604
+
605
+ if (!apiResponse.ok) throw new Error('Error fetching data.');
606
+
607
+ const data = await apiResponse.json();
608
+ const user = data?.data?.user;
609
+
610
+ contributions = {
611
+ today: user?.contributionsCollection?.contributionCalendar?.totalContributions || 0,
612
+ month: user?.contributions_month?.contributionCalendar?.totalContributions || 0,
613
+ year: user?.contributions_year?.contributionCalendar?.totalContributions || 0,
614
+ };
615
+
616
+ lastFetch = currentTime;
617
+ } catch { contributions = { today: 0, month: 0, year: 0 }; }
618
+ }
619
+
620
+ res.jsonResponse({
621
+ versions: {
622
+ api: process.env.API,
623
+ cdn: process.env.CDN,
624
+ coop_api: process.env.COOP_API,
625
+ coop_status: process.env.COOP_STATUS,
626
+ chat: process.env.CHAT,
627
+ digit: process.env.DIGIT,
628
+ doc_coopbot: process.env.DOC_COOPBOT,
629
+ docs: process.env.DOCS,
630
+ donut: process.env.DONUT,
631
+ drawio_plugin: process.env.DRAWIO_PLUGIN,
632
+ flowers: process.env.FLOWERS,
633
+ gemsync: process.env.GEMSYNC,
634
+ gitsite: process.env.GITSITE,
635
+ lebonchar: process.env.LEBONCHAR,
636
+ logs: process.env.LOGS,
637
+ logvault: process.env.LOGVAULT,
638
+ lyah: process.env.LYAH,
639
+ minify: process.env.MINIFY,
640
+ morpion: process.env.MORPION,
641
+ nitrogen: process.env.NITROGEN,
642
+ old_database: process.env.OLD_DATABASE,
643
+ php: process.env.PHP,
644
+ ping: process.env.PING,
645
+ portfolio: process.env.PORTFOLIO,
646
+ python_api: process.env.PYTHON_API,
647
+ readme: process.env.README,
648
+ terminal: process.env.TERMINAL,
649
+ wrkit: process.env.WRKIT,
650
+ zpki: process.env.ZPKI
651
+ },
652
+ patched_projects: process.env.PATCH?.split(' ') || [],
653
+ updated_projects: process.env.RECENT?.split(' ') || [],
654
+ new_projects: process.env.NEW?.split(' ') || [],
655
+ sub_domains: process.env.DOMAINS?.split(' ') || [],
656
+ stats: {
657
+ 1: process.env.STATS1,
658
+ 2: process.env.STATS2,
659
+ 3: process.env.STATS3,
660
+ 4: process.env.STATS4,
661
+ 5: Object.keys(ipLimits).length,
662
+ today: contributions.today.toString(),
663
+ this_month: contributions.month.toString(),
664
+ last_year: contributions.year.toString(),
665
+ },
666
+ notif_tag: process.env.TAG,
667
+ active: process.env.ACTIVE
668
+ });
669
+ });
670
+
671
+ // ----------- ----------- POST ENDPOINTS ----------- ----------- //
672
+
673
+ // Store chat messages
674
+ app.post('/:version/chat', (req, res) => {
675
+ const { username, message, timestamp, session, token } = req.body;
676
+
677
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
678
+ if (!message) return res.jsonResponse({ error: 'Please provide a message (&message={message})' });
679
+ if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
680
+
681
+ try {
682
+ const result = req.module.chat('message', {
683
+ username,
684
+ message,
685
+ timestamp,
686
+ session,
687
+ token,
688
+ storage: chatStorage
689
+ });
690
+ res.jsonResponse(result);
691
+ } catch (err) {
692
+ res.jsonResponse({ error: err.message });
693
+ }
694
+ });
695
+
696
+ // Display a private chat with a token
697
+ app.post('/:version/chat/private', (req, res) => {
698
+ const { username, token } = req.body;
699
+
700
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
701
+ if (!token) return res.jsonResponse({ error: 'Please provide a valid token (&token={key}).' });
702
+
703
+ try {
704
+ const messages = req.module.chat('private', {
705
+ username,
706
+ token,
707
+ storage: chatStorage
708
+ });
709
+ res.jsonResponse(messages);
710
+ } catch (err) {
711
+ res.jsonResponse({ error: err.message });
712
+ }
713
+ });
714
+
715
+ // Generate hash
716
+ app.post('/:version/hash', (req, res) => {
717
+ const { text, method } = req.body;
718
+
719
+ if (!text) return res.jsonResponse({ error: 'Please provide a text (?text={text})' });
720
+ if (!method) return res.jsonResponse({
721
+ error: 'Please provide a valid hash algorithm (&method={algorithm})',
722
+ documentation: `https://docs.sylvain.pro/${req.version}/en/hash`
723
+ });
724
+
725
+ try {
726
+ const result = req.module.hash(text, method);
727
+ res.jsonResponse(result);
728
+ } catch (err) {
729
+ res.jsonResponse({
730
+ error: err.message,
731
+ documentation: `https://docs.sylvain.pro/${req.version}/en/hash`
732
+ })
733
+ }
734
+ });
735
+
736
+ // Display a planning from an ICS file
737
+ app.post('/:version/hyperplanning', async (req, res) => {
738
+ const { url, detail } = req.body;
739
+
740
+ if (!url) return res.jsonResponse({ error: 'Please provide a valid ICS file URL (?url={URL})' });
741
+
742
+ try {
743
+ const hyperplanning = req.module.hyperplanning(url, detail);
744
+ res.jsonResponse(hyperplanning);
745
+ } catch (err){
746
+ res.jsonResponse({
747
+ error: 'Failed to parse ICS file.',
748
+ documentation: `https://docs.sylvain.pro/${req.version}/en/hyperplanning`
749
+ });
750
+ }
751
+ });
752
+
753
+ // Store tic tac toe games
754
+ app.post('/:version/tic-tac-toe', (req, res) => {
755
+ const { username, move, session, game } = req.body;
756
+
757
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
758
+ if (!move) return res.jsonResponse({ error: 'Please provide a valid move (&move={move})' });
759
+ if (!session) return res.jsonResponse({ error: 'Please provide a valid session ID (&session={ID})' });
760
+ if (!game) return res.jsonResponse({ error: 'Please provide a valid game ID (&game={ID})' });
761
+
762
+ try {
763
+ const result = req.module.tic_tac_toe('play', {
764
+ username,
765
+ move,
766
+ session,
767
+ game,
768
+ storage: ticTacToeStorage
769
+ });
770
+ res.jsonResponse(result);
771
+ } catch (err) {
772
+ res.jsonResponse({ error: err.message });
773
+ }
774
+ });
775
+
776
+ // Display a tic tac toe game with a token
777
+ app.post('/:version/tic-tac-toe/fetch', (req, res) => {
778
+ const { username, game } = req.body;
779
+
780
+ if (!username) return res.jsonResponse({ error: 'Please provide a username (?username={username})' });
781
+
782
+ try {
783
+ const result = req.module.tic_tac_toe('fetch', {
784
+ username,
785
+ game,
786
+ storage: ticTacToeStorage
787
+ });
788
+ res.jsonResponse(result);
789
+ } catch (err) {
790
+ res.jsonResponse({ error: err.message });
791
+ }
792
+ });
793
+
794
+ // Generate Token
795
+ app.post('/:version/token', (req, res) => {
796
+ let { len, type } = req.body;
797
+
798
+ len = parseInt(len || 24, 10);
799
+ type = type ? type.toLowerCase() : 'alpha';
800
+
801
+ if (isNaN(len) || len < 0) return res.jsonResponse({ error: 'Invalid number.' });
802
+ if (len > 4096) return res.jsonResponse({ error: 'Length cannot exceed 4096.' });
803
+ if (len < 12) return res.jsonResponse({ error: 'Length cannot be less than 12.' });
804
+
805
+ try {
806
+ const token = req.module.token(len, type);
807
+ res.jsonResponse({ token });
808
+ } catch (err) {
809
+ return res.jsonResponse({
810
+ error: err.message,
811
+ documentation: `https://docs.sylvain.pro/${req.version}/en/token`
812
+ });
813
+ }
814
+ });
815
+
816
+ // ----------- ----------- SERVER SETUP ----------- ----------- //
817
+
818
+ app.listen(3000, () => console.log('API is running on\n - http://127.0.0.1:3000\n - http://localhost:3000'));