@20syldev/api 3.5.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +32 -33
  3. package/dist/app.js +36 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/config/env.js +73 -0
  6. package/dist/config/env.js.map +1 -0
  7. package/dist/config/versions.js +72 -0
  8. package/dist/config/versions.js.map +1 -0
  9. package/dist/constants.js +42 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/middleware/cors.js +15 -0
  12. package/dist/middleware/cors.js.map +1 -0
  13. package/dist/middleware/error.js +6 -0
  14. package/dist/middleware/error.js.map +1 -0
  15. package/dist/middleware/json.js +8 -0
  16. package/dist/middleware/json.js.map +1 -0
  17. package/dist/middleware/logger.js +26 -0
  18. package/dist/middleware/logger.js.map +1 -0
  19. package/dist/middleware/ratelimit.js +58 -0
  20. package/dist/middleware/ratelimit.js.map +1 -0
  21. package/dist/middleware/version.js +30 -0
  22. package/dist/middleware/version.js.map +1 -0
  23. package/dist/modules/v3/algorithms.js +221 -0
  24. package/dist/modules/v3/algorithms.js.map +1 -0
  25. package/{modules → dist/modules}/v3/captcha.js +4 -13
  26. package/dist/modules/v3/captcha.js.map +1 -0
  27. package/dist/modules/v3/chat.js +103 -0
  28. package/dist/modules/v3/chat.js.map +1 -0
  29. package/{modules → dist/modules}/v3/color.js +12 -13
  30. package/dist/modules/v3/color.js.map +1 -0
  31. package/dist/modules/v3/convert.js +41 -0
  32. package/dist/modules/v3/convert.js.map +1 -0
  33. package/dist/modules/v3/domain.js +83 -0
  34. package/dist/modules/v3/domain.js.map +1 -0
  35. package/dist/modules/v3/hash.js +16 -0
  36. package/dist/modules/v3/hash.js.map +1 -0
  37. package/dist/modules/v3/hyperplanning.js +50 -0
  38. package/dist/modules/v3/hyperplanning.js.map +1 -0
  39. package/dist/modules/v3/levenshtein.js +38 -0
  40. package/dist/modules/v3/levenshtein.js.map +1 -0
  41. package/dist/modules/v3/personal.js +129 -0
  42. package/dist/modules/v3/personal.js.map +1 -0
  43. package/dist/modules/v3/qrcode.js +21 -0
  44. package/dist/modules/v3/qrcode.js.map +1 -0
  45. package/dist/modules/v3/tic_tac_toe.js +221 -0
  46. package/dist/modules/v3/tic_tac_toe.js.map +1 -0
  47. package/dist/modules/v3/time.js +88 -0
  48. package/dist/modules/v3/time.js.map +1 -0
  49. package/{modules → dist/modules}/v3/token.js +10 -11
  50. package/dist/modules/v3/token.js.map +1 -0
  51. package/dist/modules/v3/username.js +92 -0
  52. package/dist/modules/v3/username.js.map +1 -0
  53. package/dist/modules/v3/utils.js +70 -0
  54. package/dist/modules/v3/utils.js.map +1 -0
  55. package/dist/modules/v3.js +16 -0
  56. package/dist/modules/v3.js.map +1 -0
  57. package/dist/modules/v4/algorithms.js +200 -0
  58. package/dist/modules/v4/algorithms.js.map +1 -0
  59. package/dist/modules/v4/captcha.js +35 -0
  60. package/dist/modules/v4/captcha.js.map +1 -0
  61. package/dist/modules/v4/chat.js +92 -0
  62. package/dist/modules/v4/chat.js.map +1 -0
  63. package/dist/modules/v4/color.js +43 -0
  64. package/dist/modules/v4/color.js.map +1 -0
  65. package/dist/modules/v4/convert.js +33 -0
  66. package/dist/modules/v4/convert.js.map +1 -0
  67. package/dist/modules/v4/dice.js +30 -0
  68. package/dist/modules/v4/dice.js.map +1 -0
  69. package/dist/modules/v4/domain.js +78 -0
  70. package/dist/modules/v4/domain.js.map +1 -0
  71. package/dist/modules/v4/encode.js +165 -0
  72. package/dist/modules/v4/encode.js.map +1 -0
  73. package/dist/modules/v4/geo.js +41 -0
  74. package/dist/modules/v4/geo.js.map +1 -0
  75. package/dist/modules/v4/hash.js +9 -0
  76. package/dist/modules/v4/hash.js.map +1 -0
  77. package/dist/modules/v4/hyperplanning.js +42 -0
  78. package/dist/modules/v4/hyperplanning.js.map +1 -0
  79. package/dist/modules/v4/levenshtein.js +29 -0
  80. package/dist/modules/v4/levenshtein.js.map +1 -0
  81. package/dist/modules/v4/palette.js +89 -0
  82. package/dist/modules/v4/palette.js.map +1 -0
  83. package/dist/modules/v4/personal.js +125 -0
  84. package/dist/modules/v4/personal.js.map +1 -0
  85. package/dist/modules/v4/placeholder.js +96 -0
  86. package/dist/modules/v4/placeholder.js.map +1 -0
  87. package/dist/modules/v4/qrcode.js +12 -0
  88. package/dist/modules/v4/qrcode.js.map +1 -0
  89. package/dist/modules/v4/statistics.js +41 -0
  90. package/dist/modules/v4/statistics.js.map +1 -0
  91. package/dist/modules/v4/text.js +293 -0
  92. package/dist/modules/v4/text.js.map +1 -0
  93. package/dist/modules/v4/tic_tac_toe.js +206 -0
  94. package/dist/modules/v4/tic_tac_toe.js.map +1 -0
  95. package/dist/modules/v4/time.js +73 -0
  96. package/dist/modules/v4/time.js.map +1 -0
  97. package/dist/modules/v4/token.js +36 -0
  98. package/dist/modules/v4/token.js.map +1 -0
  99. package/dist/modules/v4/username.js +87 -0
  100. package/dist/modules/v4/username.js.map +1 -0
  101. package/dist/modules/v4/utils.js +26 -0
  102. package/dist/modules/v4/utils.js.map +1 -0
  103. package/dist/modules/v4/validate.js +54 -0
  104. package/dist/modules/v4/validate.js.map +1 -0
  105. package/dist/modules/v4.js +18 -0
  106. package/dist/modules/v4.js.map +1 -0
  107. package/dist/routes/delete.js +65 -0
  108. package/dist/routes/delete.js.map +1 -0
  109. package/dist/routes/get.js +441 -0
  110. package/dist/routes/get.js.map +1 -0
  111. package/dist/routes/index.js +50 -0
  112. package/dist/routes/index.js.map +1 -0
  113. package/dist/routes/patch.js +41 -0
  114. package/dist/routes/patch.js.map +1 -0
  115. package/dist/routes/post.js +185 -0
  116. package/dist/routes/post.js.map +1 -0
  117. package/dist/storage/index.js +13 -0
  118. package/dist/storage/index.js.map +1 -0
  119. package/dist/types/storage.js +2 -0
  120. package/dist/types/storage.js.map +1 -0
  121. package/dist/utils/response.js +10 -0
  122. package/dist/utils/response.js.map +1 -0
  123. package/dist/utils/version.js +3 -0
  124. package/dist/utils/version.js.map +1 -0
  125. package/docs/changelog.md +333 -0
  126. package/eslint.config.js +20 -0
  127. package/package.json +31 -15
  128. package/src/app.ts +48 -0
  129. package/src/config/env.ts +81 -0
  130. package/src/config/versions.ts +92 -0
  131. package/src/constants.ts +47 -0
  132. package/src/middleware/cors.ts +17 -0
  133. package/src/middleware/error.ts +7 -0
  134. package/src/middleware/json.ts +9 -0
  135. package/src/middleware/logger.ts +31 -0
  136. package/src/middleware/ratelimit.ts +66 -0
  137. package/src/middleware/version.ts +39 -0
  138. package/{modules → src/modules}/v3/algorithms.js +1 -1
  139. package/src/modules/v3/captcha.js +60 -0
  140. package/{modules → src/modules}/v3/chat.js +6 -4
  141. package/src/modules/v3/color.js +60 -0
  142. package/{modules → src/modules}/v3/convert.js +5 -5
  143. package/{modules → src/modules}/v3/domain.js +51 -4
  144. package/{modules → src/modules}/v3/hash.js +1 -1
  145. package/{modules → src/modules}/v3/hyperplanning.js +11 -10
  146. package/{modules → src/modules}/v3/levenshtein.js +3 -3
  147. package/{modules → src/modules}/v3/personal.js +20 -8
  148. package/{modules → src/modules}/v3/qrcode.js +1 -1
  149. package/{modules → src/modules}/v3/tic_tac_toe.js +24 -18
  150. package/{modules → src/modules}/v3/time.js +22 -11
  151. package/src/modules/v3/token.js +54 -0
  152. package/src/modules/v3/username.js +94 -0
  153. package/{modules → src/modules}/v3/utils.js +2 -2
  154. package/{modules → src/modules}/v3.js +1 -1
  155. package/src/modules/v4/algorithms.ts +197 -0
  156. package/src/modules/v4/captcha.ts +49 -0
  157. package/src/modules/v4/chat.ts +122 -0
  158. package/src/modules/v4/color.ts +64 -0
  159. package/src/modules/v4/convert.ts +38 -0
  160. package/src/modules/v4/dice.ts +39 -0
  161. package/src/modules/v4/domain.ts +81 -0
  162. package/src/modules/v4/encode.ts +170 -0
  163. package/src/modules/v4/geo.ts +53 -0
  164. package/src/modules/v4/hash.ts +9 -0
  165. package/src/modules/v4/hyperplanning.ts +58 -0
  166. package/src/modules/v4/levenshtein.ts +38 -0
  167. package/src/modules/v4/palette.ts +103 -0
  168. package/src/modules/v4/personal.ts +147 -0
  169. package/src/modules/v4/placeholder.ts +122 -0
  170. package/src/modules/v4/qrcode.ts +11 -0
  171. package/src/modules/v4/statistics.ts +55 -0
  172. package/src/modules/v4/text.ts +300 -0
  173. package/src/modules/v4/tic_tac_toe.ts +255 -0
  174. package/src/modules/v4/time.ts +92 -0
  175. package/src/modules/v4/token.ts +43 -0
  176. package/src/modules/v4/username.ts +89 -0
  177. package/src/modules/v4/utils.ts +38 -0
  178. package/src/modules/v4/validate.ts +55 -0
  179. package/src/modules/v4.ts +17 -0
  180. package/src/routes/delete.ts +72 -0
  181. package/src/routes/get.ts +508 -0
  182. package/src/routes/index.ts +58 -0
  183. package/src/routes/patch.ts +45 -0
  184. package/src/routes/post.ts +200 -0
  185. package/src/storage/index.ts +16 -0
  186. package/src/types/express.d.ts +17 -0
  187. package/src/types/storage.ts +48 -0
  188. package/src/utils/response.ts +11 -0
  189. package/src/utils/version.ts +5 -0
  190. package/tests/integration/api.test.ts +395 -0
  191. package/tests/tsconfig.json +3 -0
  192. package/tests/unit/algorithms.test.ts +120 -0
  193. package/tests/unit/color.test.ts +33 -0
  194. package/tests/unit/convert.test.ts +42 -0
  195. package/tests/unit/dice.test.ts +57 -0
  196. package/tests/unit/domain.test.ts +47 -0
  197. package/tests/unit/encode.test.ts +108 -0
  198. package/tests/unit/geo.test.ts +45 -0
  199. package/tests/unit/hash.test.ts +37 -0
  200. package/tests/unit/hyperplanning.test.ts +77 -0
  201. package/tests/unit/levenshtein.test.ts +34 -0
  202. package/tests/unit/palette.test.ts +53 -0
  203. package/tests/unit/personal.test.ts +65 -0
  204. package/tests/unit/statistics.test.ts +57 -0
  205. package/tests/unit/text.test.ts +107 -0
  206. package/tests/unit/time.test.ts +67 -0
  207. package/tests/unit/token.test.ts +61 -0
  208. package/tests/unit/username.test.ts +34 -0
  209. package/tests/unit/validate.test.ts +71 -0
  210. package/tsconfig.json +17 -0
  211. package/tsconfig.test.json +9 -0
  212. package/app.js +0 -865
  213. package/modules/v3/username.js +0 -32
@@ -0,0 +1,508 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+ import { versions } from '../config/versions.js';
3
+ import { env } from '../config/env.js';
4
+ import { ipLimits } from '../storage/index.js';
5
+ import { chatStorage } from '../storage/index.js';
6
+ import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
7
+ import { error } from '../utils/response.js';
8
+
9
+ const router = Router();
10
+
11
+ let activity: Record<string, unknown>[] = [];
12
+ let lastFetch = 0;
13
+
14
+ // Display version information
15
+ router.get('/:version', (req: Request, res: Response) => {
16
+ const version = req.params.version as string;
17
+ const versionConfig = versions[version]!;
18
+
19
+ const endpoints = Object.keys(versionConfig.endpoints).reduce<Record<string, unknown>>((acc, method) => {
20
+ const endpointList = versionConfig.endpoints[method as keyof typeof versionConfig.endpoints];
21
+ if (!endpointList) return acc;
22
+ acc[method] = endpointList
23
+ .filter(({ name }: { name: string }) => name !== 'website')
24
+ .sort((a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name))
25
+ .reduce<Record<string, unknown>>(
26
+ (
27
+ group: Record<string, unknown>,
28
+ endpoint: { name: string; path?: string; children?: Record<string, string> },
29
+ ) => {
30
+ if (endpoint.children) {
31
+ group[endpoint.name] = Object.keys(endpoint.children)
32
+ .sort((a: string, b: string) => a.localeCompare(b))
33
+ .reduce<Record<string, string>>((childGroup, childName) => {
34
+ childGroup[childName] = `/${version}${endpoint.children![childName]}`;
35
+ return childGroup;
36
+ }, {});
37
+ } else {
38
+ group[endpoint.name] = `/${version}${endpoint.path}`;
39
+ }
40
+ return group;
41
+ },
42
+ {},
43
+ );
44
+ return acc;
45
+ }, {});
46
+
47
+ res.jsonResponse({
48
+ version,
49
+ documentation: `${DOCS_URL}/${version}`,
50
+ endpoints,
51
+ });
52
+ });
53
+
54
+ // Algorithms
55
+ router.get('/:version/algorithms', (req: Request, res: Response) => {
56
+ const { method, value, value2 } = req.query;
57
+ const { version } = req.params;
58
+
59
+ const algorithms = req.module.algorithms as Record<string, (v: string, v2?: string) => unknown>;
60
+ if (!algorithms || !algorithms[method as string]) {
61
+ error(res, 400, 'Please provide a valid algorithm (?method={algorithm})', `${version}/algorithms`);
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const answer = algorithms[method as string]!(value as string, value2 as string);
67
+ res.jsonResponse({ answer });
68
+ } catch (err) {
69
+ error(res, 400, (err as Error).message, `${req.version}/algorithms`);
70
+ }
71
+ });
72
+
73
+ // Generate captcha
74
+ router.get('/:version/captcha', (req: Request, res: Response) => {
75
+ const text = req.query.text as string;
76
+
77
+ if (!text) {
78
+ error(res, 400, 'Please provide a valid argument (?text={text})', `${req.version}/captcha`);
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const result = req.module.captcha(text);
84
+ res.type('png').send(result);
85
+ } catch (err) {
86
+ error(res, 400, (err as Error).message, `${req.version}/captcha`);
87
+ }
88
+ });
89
+
90
+ // Display stored data
91
+ router.get('/:version/chat', (req: Request, res: Response) => {
92
+ try {
93
+ const messages = req.module.chat('fetch', {
94
+ username: 'system',
95
+ storage: chatStorage,
96
+ });
97
+ res.jsonResponse(messages);
98
+ } catch (err) {
99
+ error(res, 400, (err as Error).message);
100
+ }
101
+ });
102
+
103
+ // GET private chat error
104
+ router.get('/:version/chat/private', (_req: Request, res: Response) => {
105
+ error(res, 405, 'This endpoint only supports POST requests.');
106
+ });
107
+
108
+ // Generate color
109
+ router.get('/:version/color', (req: Request, res: Response) => {
110
+ try {
111
+ const result = req.module.color();
112
+ res.jsonResponse(result);
113
+ } catch (err) {
114
+ error(res, 400, (err as Error).message, `${req.version}/color`);
115
+ }
116
+ });
117
+
118
+ // Convert units
119
+ router.get('/:version/convert', (req: Request, res: Response) => {
120
+ const { value, from, to } = req.query;
121
+
122
+ if (!value || isNaN(Number(value))) {
123
+ error(res, 400, 'Please provide a valid value (?value={value})', `${req.version}/convert`);
124
+ return;
125
+ }
126
+ if (!from) {
127
+ error(res, 400, 'Please provide a valid source unit (&from={unit})', `${req.version}/convert`);
128
+ return;
129
+ }
130
+ if (!to) {
131
+ error(res, 400, 'Please provide a valid target unit (&to={unit})', `${req.version}/convert`);
132
+ return;
133
+ }
134
+
135
+ try {
136
+ const result = req.module.convert(value as string, from as string, to as string);
137
+ res.jsonResponse(result);
138
+ } catch (err) {
139
+ error(res, 400, (err as Error).message, `${req.version}/convert`);
140
+ }
141
+ });
142
+
143
+ // Generate domain informations
144
+ router.get('/:version/domain', (req: Request, res: Response) => {
145
+ try {
146
+ const result = req.module.domain();
147
+ res.jsonResponse(result);
148
+ } catch (err) {
149
+ error(res, 400, (err as Error).message, `${req.version}/domain`);
150
+ }
151
+ });
152
+
153
+ // RPG Dice roller
154
+ router.get('/:version/dice', (req: Request, res: Response) => {
155
+ const { roll } = req.query;
156
+ const { version } = req.params;
157
+
158
+ const dice = (req.module as { dice?: (r: string) => unknown }).dice;
159
+ if (!dice) {
160
+ error(res, 404, `Endpoint not available in ${version}.`, `${version}/dice`);
161
+ return;
162
+ }
163
+ if (!roll) {
164
+ error(res, 400, 'Please provide a roll notation (?roll=2d6+3)', `${version}/dice`);
165
+ return;
166
+ }
167
+
168
+ try {
169
+ const result = dice(roll as string);
170
+ res.jsonResponse(result);
171
+ } catch (err) {
172
+ error(res, 400, (err as Error).message, `${req.version}/dice`);
173
+ }
174
+ });
175
+
176
+ // GET planning error
177
+ router.get('/:version/hyperplanning', (_req: Request, res: Response) => {
178
+ error(res, 405, 'This endpoint only supports POST requests.');
179
+ });
180
+
181
+ // GET hash error
182
+ router.get('/:version/hash', (_req: Request, res: Response) => {
183
+ error(res, 405, 'This endpoint only supports POST requests.');
184
+ });
185
+
186
+ // Display API informations
187
+ router.get('/:version/infos', (req: Request, res: Response) => {
188
+ const endpoints = Object.values(versions[req.version]!.endpoints).flat();
189
+
190
+ const paths = endpoints.flatMap((e) => (e.children ? Object.values(e.children) : e.path ? [e.path] : []));
191
+
192
+ res.jsonResponse({
193
+ endpoints: new Set(paths).size,
194
+ last_version: Object.keys(versions).pop(),
195
+ documentation: DOCS_URL,
196
+ github: 'https://github.com/20syldev/api',
197
+ creation: 'November 25th 2024',
198
+ });
199
+ });
200
+
201
+ // Calculate Levenshtein distance
202
+ router.get('/:version/levenshtein', (req: Request, res: Response) => {
203
+ const { str1, str2 } = req.query;
204
+
205
+ if (!str1 || typeof str1 !== 'string') {
206
+ error(res, 400, 'Please provide a first string (?str1={string})', `${req.version}/levenshtein`);
207
+ return;
208
+ }
209
+ if (!str2 || typeof str2 !== 'string') {
210
+ error(res, 400, 'Please provide a second string (&str2={string})', `${req.version}/levenshtein`);
211
+ return;
212
+ }
213
+
214
+ try {
215
+ const result = req.module.levenshtein(str1, str2);
216
+ res.jsonResponse(result);
217
+ } catch (err) {
218
+ error(res, 400, (err as Error).message, `${req.version}/levenshtein`);
219
+ }
220
+ });
221
+
222
+ // Generate personal data
223
+ router.get('/:version/personal', (req: Request, res: Response) => {
224
+ try {
225
+ const result = req.module.personal();
226
+ res.jsonResponse(result);
227
+ } catch (err) {
228
+ error(res, 400, (err as Error).message, `${req.version}/personal`);
229
+ }
230
+ });
231
+
232
+ // Generate QR Code
233
+ router.get('/:version/qrcode', async (req: Request, res: Response) => {
234
+ const { url } = req.query;
235
+
236
+ if (!url) {
237
+ error(res, 400, 'Please provide a valid url (?url={URL})', `${req.version}/qrcode`);
238
+ return;
239
+ }
240
+
241
+ try {
242
+ const result = await req.module.qrcode(url as string);
243
+ res.jsonResponse(result);
244
+ } catch (err) {
245
+ error(res, 400, (err as Error).message, `${req.version}/qrcode`);
246
+ }
247
+ });
248
+
249
+ // Statistics on a list of numbers
250
+ router.get('/:version/statistics', (req: Request, res: Response) => {
251
+ const { values } = req.query;
252
+ const { version } = req.params;
253
+
254
+ const statistics = (req.module as { statistics?: (v: string) => unknown }).statistics;
255
+ if (!statistics) {
256
+ error(res, 404, `Endpoint not available in ${version}.`, `${version}/statistics`);
257
+ return;
258
+ }
259
+ if (!values) {
260
+ error(res, 400, 'Please provide a list of values (?values=1,2,3)', `${version}/statistics`);
261
+ return;
262
+ }
263
+
264
+ try {
265
+ const result = statistics(values as string);
266
+ res.jsonResponse(result);
267
+ } catch (err) {
268
+ error(res, 400, (err as Error).message, `${req.version}/statistics`);
269
+ }
270
+ });
271
+
272
+ // GET tic-tac-toe errors
273
+ router.get('/:version/tic-tac-toe', (_req: Request, res: Response) => {
274
+ error(res, 405, 'This endpoint only supports POST requests.');
275
+ });
276
+
277
+ router.get('/:version/tic-tac-toe/fetch', (_req: Request, res: Response) => {
278
+ error(res, 405, 'This endpoint only supports POST requests.');
279
+ });
280
+
281
+ router.get('/:version/tic-tac-toe/list', (_req: Request, res: Response) => {
282
+ error(res, 405, 'This endpoint only supports POST requests.');
283
+ });
284
+
285
+ // Display or generate time informations
286
+ router.get('/:version/time', (req: Request, res: Response) => {
287
+ const { type = 'live', start, end, format, timezone } = req.query;
288
+
289
+ const validFormats = [
290
+ 'iso',
291
+ 'utc',
292
+ 'timestamp',
293
+ 'locale',
294
+ 'date',
295
+ 'time',
296
+ 'year',
297
+ 'month',
298
+ 'day',
299
+ 'hour',
300
+ 'minute',
301
+ 'second',
302
+ 'ms',
303
+ 'dayOfWeek',
304
+ 'dayOfYear',
305
+ 'weekNumber',
306
+ 'timezone',
307
+ 'timezoneOffset',
308
+ ];
309
+ const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'];
310
+
311
+ if (type !== 'live' && type !== 'random') {
312
+ error(res, 400, 'Please provide a valid type (?type={type})', `${req.version}/time`);
313
+ return;
314
+ }
315
+ if (start && !Date.parse(start as string)) {
316
+ error(res, 400, 'Please provide a valid start date (?start={YYYY-MM-DD})', `${req.version}/time`);
317
+ return;
318
+ }
319
+ if (end && !Date.parse(end as string)) {
320
+ error(res, 400, 'Please provide a valid end date (?end={YYYY-MM-DD})', `${req.version}/time`);
321
+ return;
322
+ }
323
+ if (format && !validFormats.includes(format as string)) {
324
+ error(res, 400, 'Please provide a valid format (?format={format})', `${req.version}/time`);
325
+ return;
326
+ }
327
+ if (timezone && !validTimezones.includes(timezone as string)) {
328
+ error(res, 400, 'Please provide a valid timezone (?timezone={timezone})', `${req.version}/time`);
329
+ return;
330
+ }
331
+
332
+ try {
333
+ const time = req.module.time(
334
+ type as string,
335
+ start as string,
336
+ end as string,
337
+ format as string,
338
+ timezone as string,
339
+ );
340
+ res.jsonResponse(time);
341
+ } catch (err) {
342
+ error(res, 400, (err as Error).message, `${req.version}/time`);
343
+ }
344
+ });
345
+
346
+ // GET token error
347
+ router.get('/:version/token', (_req: Request, res: Response) => {
348
+ error(res, 405, 'This endpoint only supports POST requests.');
349
+ });
350
+
351
+ // Generate username
352
+ router.get('/:version/username', (req: Request, res: Response) => {
353
+ try {
354
+ const result = req.module.username();
355
+ res.jsonResponse(result);
356
+ } catch (err) {
357
+ error(res, 400, (err as Error).message, `${req.version}/username`);
358
+ }
359
+ });
360
+
361
+ // Display informations for owner's website
362
+ router.get('/:version/website', async (req: Request, res: Response) => {
363
+ const currentTime = Date.now();
364
+
365
+ if (currentTime - lastFetch >= GITHUB_CACHE_TTL) {
366
+ try {
367
+ const username = '20syldev';
368
+ const token = env.GITHUB_TOKEN;
369
+ const lastYear = new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split('T')[0];
370
+
371
+ const query = `
372
+ {
373
+ user(login: "${username}") {
374
+ contributionsCollection(from: "${lastYear}T00:00:00Z") {
375
+ contributionCalendar {
376
+ totalContributions
377
+ weeks {
378
+ firstDay
379
+ contributionDays {
380
+ date
381
+ contributionCount
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }`;
388
+
389
+ const apiResponse = await fetch('https://api.github.com/graphql', {
390
+ method: 'POST',
391
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
392
+ body: JSON.stringify({ query }),
393
+ });
394
+
395
+ if (!apiResponse.ok) throw new Error('Error fetching data.');
396
+
397
+ const data = (await apiResponse.json()) as {
398
+ data?: {
399
+ user?: {
400
+ contributionsCollection?: {
401
+ contributionCalendar?: {
402
+ weeks?: {
403
+ firstDay: string;
404
+ contributionDays: { date: string; contributionCount: number }[];
405
+ }[];
406
+ };
407
+ };
408
+ };
409
+ };
410
+ };
411
+ const user = data?.data?.user;
412
+
413
+ const weeks = user?.contributionsCollection?.contributionCalendar?.weeks || [];
414
+ activity = weeks.map((w) => ({
415
+ week: w.firstDay,
416
+ total: w.contributionDays.reduce((sum, d) => sum + d.contributionCount, 0),
417
+ days: w.contributionDays.map((d) => ({ date: d.date, count: d.contributionCount })),
418
+ }));
419
+
420
+ lastFetch = currentTime;
421
+ } catch {
422
+ activity = [];
423
+ }
424
+ }
425
+
426
+ const response: Record<string, unknown> = {
427
+ versions: {
428
+ 2048: env.G_2048,
429
+ api: env.API,
430
+ cdn: env.CDN,
431
+ coop_api: env.COOP_API,
432
+ coop_status: env.COOP_STATUS,
433
+ chat: env.CHAT,
434
+ digit: env.DIGIT,
435
+ doc_coopbot: env.DOC_COOPBOT,
436
+ docs: env.DOCS,
437
+ donut: env.DONUT,
438
+ drawio_plugin: env.DRAWIO_PLUGIN,
439
+ flowers: env.FLOWERS,
440
+ gemsync: env.GEMSYNC,
441
+ gft: env.GFT,
442
+ gitsite: env.GITSITE,
443
+ lebonchar: env.LEBONCHAR,
444
+ logger: env.LOGGER,
445
+ logs: env.LOGS,
446
+ logvault: env.LOGVAULT,
447
+ lyah: env.LYAH,
448
+ minify: env.MINIFY,
449
+ mn: env.MN,
450
+ monitoring: env.MONITORING,
451
+ morpion: env.MORPION,
452
+ nitrogen: env.NITROGEN,
453
+ old_database: env.OLD_DATABASE,
454
+ password: env.PASSWORD,
455
+ php: env.PHP,
456
+ planning: env.PLANNING,
457
+ ping: env.PING,
458
+ portfolio: env.PORTFOLIO,
459
+ python_api: env.PYTHON_API,
460
+ readme: env.README,
461
+ timestamp: env.TIMESTAMP,
462
+ terminal: env.TERMINAL,
463
+ valentine: env.VALENTINE,
464
+ wrkit: env.WRKIT,
465
+ zpki: env.ZPKI,
466
+ },
467
+ patched_projects: env.PATCH,
468
+ updated_projects: env.RECENT,
469
+ new_projects: env.NEW,
470
+ sub_domains: env.DOMAINS,
471
+ stats: {
472
+ 1: env.STATS1,
473
+ 2: env.STATS2,
474
+ 3: env.STATS3,
475
+ 4: env.STATS4,
476
+ 5: Object.keys(ipLimits).length,
477
+ activity,
478
+ },
479
+ tag: env.TAG,
480
+ active: env.ACTIVE,
481
+ };
482
+
483
+ const key = req.query.key as string | undefined;
484
+ if (key) {
485
+ if (['__proto__', 'constructor', 'prototype'].some((p) => key.includes(p))) {
486
+ error(res, 400, 'Invalid key.');
487
+ return;
488
+ }
489
+
490
+ const keys = key.split('.');
491
+ let result: unknown = response;
492
+
493
+ for (const k of keys) {
494
+ if (result == null || typeof result !== 'object' || !(k in result)) {
495
+ error(res, 404, `Key '${key}' not found.`);
496
+ return;
497
+ }
498
+ result = (result as Record<string, unknown>)[k];
499
+ }
500
+
501
+ res.jsonResponse({ [key]: result });
502
+ return;
503
+ }
504
+
505
+ res.jsonResponse(response);
506
+ });
507
+
508
+ export default router;
@@ -0,0 +1,58 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+ import { versions } from '../config/versions.js';
3
+ import { ipLimits } from '../storage/index.js';
4
+ import { logger } from '../middleware/logger.js';
5
+ import { APP_VERSION, DOCS_URL, START_TIME } from '../constants.js';
6
+
7
+ const router = Router();
8
+
9
+ router.get('/', (req: Request, res: Response) => {
10
+ const base = `${req.protocol}://${req.get('host')}`;
11
+ const links = Object.keys(versions).reduce<Record<string, string>>((link, version) => {
12
+ link[version] = `${base}/${version}`;
13
+ return link;
14
+ }, {});
15
+
16
+ res.jsonResponse({
17
+ documentation: DOCS_URL,
18
+ latest: `${base}/latest`,
19
+ health: `${base}/health`,
20
+ logs: `${base}/logs`,
21
+ versions: links,
22
+ });
23
+ });
24
+
25
+ router.get('/health', (_req: Request, res: Response) => {
26
+ const mem = process.memoryUsage();
27
+
28
+ res.jsonResponse({
29
+ status: 'ok',
30
+ uptime: Math.floor((Date.now() - START_TIME) / 1000),
31
+ version: APP_VERSION,
32
+ node: process.version,
33
+ memory: {
34
+ rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MB`,
35
+ heap_used: `${(mem.heapUsed / 1024 / 1024).toFixed(1)} MB`,
36
+ heap_total: `${(mem.heapTotal / 1024 / 1024).toFixed(1)} MB`,
37
+ },
38
+ connections: Object.keys(ipLimits).length,
39
+ logs: logger.entries().length,
40
+ });
41
+ });
42
+
43
+ router.get('/logs', (_req: Request, res: Response) => {
44
+ res.jsonResponse(logger.entries());
45
+ });
46
+
47
+ router.get('/latest', (_req: Request, res: Response) => {
48
+ const latest = Object.keys(versions).pop()!;
49
+ res.redirect(`/${latest}`);
50
+ });
51
+
52
+ router.get('/latest/{*rest}', (req: Request, res: Response) => {
53
+ const latest = Object.keys(versions).pop()!;
54
+ const rest = (req.params as Record<string, string>).rest;
55
+ res.redirect(`/${latest}/${rest}`);
56
+ });
57
+
58
+ export default router;
@@ -0,0 +1,45 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+ import { ticTacToeStorage } from '../storage/index.js';
3
+ import { error } from '../utils/response.js';
4
+ import { supportsRest } from '../utils/version.js';
5
+
6
+ const router = Router();
7
+
8
+ // Play a tic-tac-toe move (REST: PATCH /tic-tac-toe/:game)
9
+ router.patch('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
10
+ if (!supportsRest(req)) {
11
+ error(res, 405, 'PATCH is only supported in v4+.', `${req.version}/tic-tac-toe`);
12
+ return;
13
+ }
14
+
15
+ const game = req.params.game as string;
16
+ const { username, move, session } = (req.body as Record<string, string>) || {};
17
+
18
+ if (!username) {
19
+ error(res, 400, 'Please provide a username (?username={username})');
20
+ return;
21
+ }
22
+ if (!move) {
23
+ error(res, 400, 'Please provide a valid move (&move={move})');
24
+ return;
25
+ }
26
+ if (!session) {
27
+ error(res, 400, 'Please provide a valid session ID (&session={ID})');
28
+ return;
29
+ }
30
+
31
+ try {
32
+ const result = req.module.tic_tac_toe('play', {
33
+ username,
34
+ move,
35
+ session,
36
+ game,
37
+ storage: ticTacToeStorage,
38
+ });
39
+ res.jsonResponse(result);
40
+ } catch (err) {
41
+ error(res, 400, (err as Error).message);
42
+ }
43
+ });
44
+
45
+ export default router;