@20syldev/api 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/app.js +4 -0
- package/dist/app.js.map +1 -1
- package/dist/config/versions.js +19 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/modules/v3/chat.js +1 -1
- package/dist/modules/v3/chat.js.map +1 -1
- package/dist/modules/v3/domain.js +1 -1
- package/dist/modules/v3/domain.js.map +1 -1
- package/dist/modules/v3/hyperplanning.js +1 -1
- package/dist/modules/v3/hyperplanning.js.map +1 -1
- package/dist/modules/v3/personal.js +1 -1
- package/dist/modules/v3/personal.js.map +1 -1
- package/dist/modules/v3/username.js +1 -1
- package/dist/modules/v3/username.js.map +1 -1
- package/dist/modules/v4/captcha.js +60 -23
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +1 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +44 -35
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/colors.js +54 -0
- package/dist/modules/v4/colors.js.map +1 -0
- package/dist/modules/v4/convert.js +93 -19
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/domain.js +1 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/hash.js +11 -4
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +1 -1
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/dist/modules/v4/palette.js +2 -43
- package/dist/modules/v4/palette.js.map +1 -1
- package/dist/modules/v4/personal.js +1 -1
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +108 -52
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +79 -6
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/username.js +1 -1
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/modules/v4.js +6 -0
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js +16 -17
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +212 -13
- package/dist/routes/get.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/patch.js +2 -3
- package/dist/routes/patch.js.map +1 -1
- package/dist/routes/post.js +10 -3
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/colors.js +54 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/helpers.js +26 -0
- package/dist/utils/helpers.js.map +1 -0
- package/package.json +1 -1
- package/src/app.ts +4 -0
- package/src/config/versions.ts +16 -0
- package/src/constants.ts +0 -2
- package/src/middleware/cors.ts +1 -1
- package/src/modules/v4/captcha.ts +84 -27
- package/src/modules/v4/chat.ts +25 -2
- package/src/modules/v4/color.ts +56 -52
- package/src/modules/v4/convert.ts +100 -20
- package/src/modules/v4/domain.ts +1 -1
- package/src/modules/v4/geo.ts +53 -0
- package/src/modules/v4/hash.ts +18 -4
- package/src/modules/v4/hyperplanning.ts +1 -1
- package/src/modules/v4/palette.ts +63 -0
- package/src/modules/v4/personal.ts +1 -1
- package/src/modules/v4/placeholder.ts +189 -0
- package/src/modules/v4/qrcode.ts +108 -6
- package/src/modules/v4/tic_tac_toe.ts +33 -1
- package/src/modules/v4/username.ts +1 -1
- package/src/modules/v4.ts +3 -0
- package/src/routes/delete.ts +71 -0
- package/src/routes/get.ts +147 -15
- package/src/routes/patch.ts +44 -0
- package/src/routes/post.ts +11 -3
- package/src/utils/colors.ts +91 -0
- package/src/utils/helpers.ts +90 -0
- package/src/utils/response.ts +8 -0
- package/tests/integration/api.test.ts +113 -25
- package/tests/unit/captcha.test.ts +61 -0
- package/tests/unit/chat.test.ts +53 -0
- package/tests/unit/color.test.ts +24 -4
- package/tests/unit/convert.test.ts +26 -1
- package/tests/unit/geo.test.ts +45 -0
- package/tests/unit/hash.test.ts +17 -10
- package/tests/unit/palette.test.ts +53 -0
- package/tests/unit/placeholder.test.ts +57 -0
- package/tests/unit/qrcode.test.ts +82 -0
- package/tests/unit/tic_tac_toe.test.ts +55 -0
- package/src/modules/v4/utils.ts +0 -38
package/src/modules/v4.ts
CHANGED
|
@@ -6,10 +6,13 @@ export { default as convert } from './v4/convert.js';
|
|
|
6
6
|
export { default as dice } from './v4/dice.js';
|
|
7
7
|
export { default as domain } from './v4/domain.js';
|
|
8
8
|
export * as encode from './v4/encode.js';
|
|
9
|
+
export { default as geo } from './v4/geo.js';
|
|
9
10
|
export { default as hash } from './v4/hash.js';
|
|
10
11
|
export { default as hyperplanning } from './v4/hyperplanning.js';
|
|
11
12
|
export { default as levenshtein } from './v4/levenshtein.js';
|
|
13
|
+
export { default as palette } from './v4/palette.js';
|
|
12
14
|
export { default as personal } from './v4/personal.js';
|
|
15
|
+
export { default as placeholder } from './v4/placeholder.js';
|
|
13
16
|
export { default as qrcode } from './v4/qrcode.js';
|
|
14
17
|
export { default as statistics } from './v4/statistics.js';
|
|
15
18
|
export * as text from './v4/text.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Router, type Request, type Response } from 'express';
|
|
2
|
+
import { chatStorage, ticTacToeStorage } from '../storage/index.js';
|
|
3
|
+
import { error } from '../utils/response.js';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// Clear a private chat
|
|
8
|
+
router.delete('/:version/chat/:token', (req: Request, res: Response) => {
|
|
9
|
+
if (req.version !== 'v4') {
|
|
10
|
+
error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/chat`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const token = req.params.token as string;
|
|
15
|
+
const { username, session } = (req.body as Record<string, string>) || {};
|
|
16
|
+
|
|
17
|
+
if (!username) {
|
|
18
|
+
error(res, 400, 'Please provide a username (?username={username})');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!session) {
|
|
22
|
+
error(res, 400, 'Please provide a valid session ID (&session={ID})');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = req.module.chat('clear', {
|
|
28
|
+
username,
|
|
29
|
+
token,
|
|
30
|
+
session,
|
|
31
|
+
storage: chatStorage,
|
|
32
|
+
});
|
|
33
|
+
res.jsonResponse(result);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error(res, 400, (err as Error).message);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Forfeit a tic-tac-toe game
|
|
40
|
+
router.delete('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
|
|
41
|
+
if (req.version !== 'v4') {
|
|
42
|
+
error(res, 405, 'DELETE is only supported in v4+.', `${req.version}/tic-tac-toe`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const game = req.params.game as string;
|
|
47
|
+
const { username, session } = (req.body as Record<string, string>) || {};
|
|
48
|
+
|
|
49
|
+
if (!username) {
|
|
50
|
+
error(res, 400, 'Please provide a username (?username={username})');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!session) {
|
|
54
|
+
error(res, 400, 'Please provide a valid session ID (&session={ID})');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = req.module.tic_tac_toe('forfeit', {
|
|
60
|
+
username,
|
|
61
|
+
session,
|
|
62
|
+
game,
|
|
63
|
+
storage: ticTacToeStorage,
|
|
64
|
+
});
|
|
65
|
+
res.jsonResponse(result);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
error(res, 400, (err as Error).message);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export default router;
|
package/src/routes/get.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { ipLimits } from '../storage/index.js';
|
|
|
5
5
|
import { chatStorage } from '../storage/index.js';
|
|
6
6
|
import { DOCS_URL, GITHUB_CACHE_TTL } from '../constants.js';
|
|
7
7
|
import { error } from '../utils/response.js';
|
|
8
|
+
import { since } from '../utils/helpers.js';
|
|
9
|
+
import type { QRCodeOptions, QRCodeResult } from '../modules/v4/qrcode.js';
|
|
10
|
+
import type { CaptchaOptions, CaptchaResult } from '../modules/v4/captcha.js';
|
|
11
|
+
import type { ColorResult } from '../modules/v4/color.js';
|
|
8
12
|
|
|
9
13
|
const router = Router();
|
|
10
14
|
|
|
@@ -72,16 +76,29 @@ router.get('/:version/algorithms', (req: Request, res: Response) => {
|
|
|
72
76
|
|
|
73
77
|
// Generate captcha
|
|
74
78
|
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
79
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
if (since(req.version, 4)) {
|
|
81
|
+
const captchaFn = req.module.captcha as (o: CaptchaOptions) => CaptchaResult;
|
|
82
|
+
const result = captchaFn({
|
|
83
|
+
text: req.query.text as string | undefined,
|
|
84
|
+
length: req.query.length ? Number(req.query.length) : undefined,
|
|
85
|
+
width: req.query.width ? Number(req.query.width) : undefined,
|
|
86
|
+
height: req.query.height ? Number(req.query.height) : undefined,
|
|
87
|
+
noise: req.query.noise as CaptchaOptions['noise'],
|
|
88
|
+
bg: req.query.bg as string | undefined,
|
|
89
|
+
color: req.query.color as string | undefined,
|
|
90
|
+
});
|
|
91
|
+
res.set('X-Captcha-Text', result.text);
|
|
92
|
+
res.type('png').send(result.body);
|
|
93
|
+
} else {
|
|
94
|
+
const text = req.query.text as string;
|
|
95
|
+
if (!text) {
|
|
96
|
+
error(res, 400, 'Please provide a valid argument (?text={text})', `${req.version}/captcha`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const result = (req.module.captcha as (t: string) => Buffer)(text);
|
|
100
|
+
res.type('png').send(result);
|
|
101
|
+
}
|
|
85
102
|
} catch (err) {
|
|
86
103
|
error(res, 400, (err as Error).message, `${req.version}/captcha`);
|
|
87
104
|
}
|
|
@@ -108,8 +125,15 @@ router.get('/:version/chat/private', (_req: Request, res: Response) => {
|
|
|
108
125
|
// Generate color
|
|
109
126
|
router.get('/:version/color', (req: Request, res: Response) => {
|
|
110
127
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
if (since(req.version, 4)) {
|
|
129
|
+
const colorFn = req.module.color as (hex?: string) => ColorResult;
|
|
130
|
+
const hex = req.query.hex as string | undefined;
|
|
131
|
+
const result = colorFn(hex || undefined);
|
|
132
|
+
res.jsonResponse(result);
|
|
133
|
+
} else {
|
|
134
|
+
const result = (req.module.color as () => Record<string, string>)();
|
|
135
|
+
res.jsonResponse(result);
|
|
136
|
+
}
|
|
113
137
|
} catch (err) {
|
|
114
138
|
error(res, 400, (err as Error).message, `${req.version}/color`);
|
|
115
139
|
}
|
|
@@ -133,8 +157,18 @@ router.get('/:version/convert', (req: Request, res: Response) => {
|
|
|
133
157
|
}
|
|
134
158
|
|
|
135
159
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
if (since(req.version, 4)) {
|
|
161
|
+
const convertFn = req.module.convert as (v: number, f: string, t: string) => Record<string, unknown>;
|
|
162
|
+
const result = convertFn(Number(value), from as string, to as string);
|
|
163
|
+
res.jsonResponse(result);
|
|
164
|
+
} else {
|
|
165
|
+
const result = (req.module.convert as (v: string, f: string, t: string) => Record<string, unknown>)(
|
|
166
|
+
value as string,
|
|
167
|
+
from as string,
|
|
168
|
+
to as string,
|
|
169
|
+
);
|
|
170
|
+
res.jsonResponse(result);
|
|
171
|
+
}
|
|
138
172
|
} catch (err) {
|
|
139
173
|
error(res, 400, (err as Error).message, `${req.version}/convert`);
|
|
140
174
|
}
|
|
@@ -196,6 +230,29 @@ router.get('/:version/encode', (req: Request, res: Response) => {
|
|
|
196
230
|
}
|
|
197
231
|
});
|
|
198
232
|
|
|
233
|
+
// Geographic distance and bearing between two coordinates
|
|
234
|
+
router.get('/:version/geo', (req: Request, res: Response) => {
|
|
235
|
+
const { lat1, lon1, lat2, lon2 } = req.query;
|
|
236
|
+
const { version } = req.params;
|
|
237
|
+
|
|
238
|
+
const geo = (req.module as { geo?: (a: string, b: string, c: string, d: string) => unknown }).geo;
|
|
239
|
+
if (!geo) {
|
|
240
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/geo`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (lat1 === undefined || lon1 === undefined || lat2 === undefined || lon2 === undefined) {
|
|
244
|
+
error(res, 400, 'Please provide lat1, lon1, lat2 and lon2', `${version}/geo`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const result = geo(lat1 as string, lon1 as string, lat2 as string, lon2 as string);
|
|
250
|
+
res.jsonResponse(result);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
error(res, 400, (err as Error).message, `${req.version}/geo`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
199
256
|
// GET planning error
|
|
200
257
|
router.get('/:version/hyperplanning', (_req: Request, res: Response) => {
|
|
201
258
|
error(res, 405, 'This endpoint only supports POST requests.');
|
|
@@ -242,6 +299,33 @@ router.get('/:version/levenshtein', (req: Request, res: Response) => {
|
|
|
242
299
|
}
|
|
243
300
|
});
|
|
244
301
|
|
|
302
|
+
// Generate a color palette from a base color
|
|
303
|
+
router.get('/:version/palette', (req: Request, res: Response) => {
|
|
304
|
+
const { color, type } = req.query;
|
|
305
|
+
const { version } = req.params;
|
|
306
|
+
|
|
307
|
+
const palette = (req.module as { palette?: (c: string, t: string) => unknown }).palette;
|
|
308
|
+
if (!palette) {
|
|
309
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/palette`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!color) {
|
|
313
|
+
error(res, 400, 'Please provide a base color (?color=#ff6600)', `${version}/palette`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!type) {
|
|
317
|
+
error(res, 400, 'Please provide a palette type (&type=complementary)', `${version}/palette`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = palette(color as string, type as string);
|
|
323
|
+
res.jsonResponse(result);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
error(res, 400, (err as Error).message, `${req.version}/palette`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
245
329
|
// Generate personal data
|
|
246
330
|
router.get('/:version/personal', (req: Request, res: Response) => {
|
|
247
331
|
try {
|
|
@@ -252,6 +336,32 @@ router.get('/:version/personal', (req: Request, res: Response) => {
|
|
|
252
336
|
}
|
|
253
337
|
});
|
|
254
338
|
|
|
339
|
+
// Generate a placeholder image or skeleton
|
|
340
|
+
router.get('/:version/placeholder', (req: Request, res: Response) => {
|
|
341
|
+
const { type = 'image' } = req.query;
|
|
342
|
+
const { version } = req.params;
|
|
343
|
+
|
|
344
|
+
const placeholder = (
|
|
345
|
+
req.module as {
|
|
346
|
+
placeholder?: (
|
|
347
|
+
t: string,
|
|
348
|
+
q: Record<string, string | undefined>,
|
|
349
|
+
) => { type: string; contentType: string; body: Buffer | string };
|
|
350
|
+
}
|
|
351
|
+
).placeholder;
|
|
352
|
+
if (!placeholder) {
|
|
353
|
+
error(res, 404, `Endpoint not available in ${version}.`, `${version}/placeholder`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = placeholder(type as string, req.query as Record<string, string | undefined>);
|
|
359
|
+
res.type(result.contentType).send(result.body);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
error(res, 400, (err as Error).message, `${req.version}/placeholder`);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
255
365
|
// Generate QR Code
|
|
256
366
|
router.get('/:version/qrcode', async (req: Request, res: Response) => {
|
|
257
367
|
const { url } = req.query;
|
|
@@ -262,8 +372,30 @@ router.get('/:version/qrcode', async (req: Request, res: Response) => {
|
|
|
262
372
|
}
|
|
263
373
|
|
|
264
374
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
375
|
+
if (since(req.version, 4)) {
|
|
376
|
+
const qrcodeFn = req.module.qrcode as (o: QRCodeOptions) => Promise<QRCodeResult>;
|
|
377
|
+
const result = await qrcodeFn({
|
|
378
|
+
url: url as string,
|
|
379
|
+
size: req.query.size ? Number(req.query.size) : undefined,
|
|
380
|
+
margin: req.query.margin ? Number(req.query.margin) : undefined,
|
|
381
|
+
correction: req.query.correction as QRCodeOptions['correction'],
|
|
382
|
+
dark: req.query.dark as string | undefined,
|
|
383
|
+
light: req.query.light as string | undefined,
|
|
384
|
+
icon: req.query.icon as string | undefined,
|
|
385
|
+
iconSize: req.query.iconSize ? Number(req.query.iconSize) : undefined,
|
|
386
|
+
iconPadding: req.query.iconPadding ? Number(req.query.iconPadding) : undefined,
|
|
387
|
+
iconRadius: req.query.iconRadius ? Number(req.query.iconRadius) : undefined,
|
|
388
|
+
format: req.query.format as QRCodeOptions['format'],
|
|
389
|
+
});
|
|
390
|
+
if (result.contentType === 'application/json') {
|
|
391
|
+
res.jsonResponse(result.body);
|
|
392
|
+
} else {
|
|
393
|
+
res.type(result.contentType).send(result.body);
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
const result = await (req.module.qrcode as (u: string) => Promise<string>)(url as string);
|
|
397
|
+
res.jsonResponse(result);
|
|
398
|
+
}
|
|
267
399
|
} catch (err) {
|
|
268
400
|
error(res, 400, (err as Error).message, `${req.version}/qrcode`);
|
|
269
401
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Router, type Request, type Response } from 'express';
|
|
2
|
+
import { ticTacToeStorage } from '../storage/index.js';
|
|
3
|
+
import { error } from '../utils/response.js';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// Play a tic-tac-toe move
|
|
8
|
+
router.patch('/:version/tic-tac-toe/:game', (req: Request, res: Response) => {
|
|
9
|
+
if (req.version !== 'v4') {
|
|
10
|
+
error(res, 405, 'PATCH is only supported in v4+.', `${req.version}/tic-tac-toe`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const game = req.params.game as string;
|
|
15
|
+
const { username, move, session } = (req.body as Record<string, string>) || {};
|
|
16
|
+
|
|
17
|
+
if (!username) {
|
|
18
|
+
error(res, 400, 'Please provide a username (?username={username})');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!move) {
|
|
22
|
+
error(res, 400, 'Please provide a valid move (&move={move})');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!session) {
|
|
26
|
+
error(res, 400, 'Please provide a valid session ID (&session={ID})');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = req.module.tic_tac_toe('play', {
|
|
32
|
+
username,
|
|
33
|
+
move,
|
|
34
|
+
session,
|
|
35
|
+
game,
|
|
36
|
+
storage: ticTacToeStorage,
|
|
37
|
+
});
|
|
38
|
+
res.jsonResponse(result);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
error(res, 400, (err as Error).message);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default router;
|
package/src/routes/post.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Router, type Request, type Response } from 'express';
|
|
|
2
2
|
import { chatStorage, ticTacToeStorage } from '../storage/index.js';
|
|
3
3
|
import { MIN_TOKEN_LENGTH, MAX_TOKEN_LENGTH } from '../constants.js';
|
|
4
4
|
import { error } from '../utils/response.js';
|
|
5
|
+
import { since } from '../utils/helpers.js';
|
|
6
|
+
import type { HashResult } from '../modules/v4/hash.js';
|
|
5
7
|
|
|
6
8
|
const router = Router();
|
|
7
9
|
|
|
@@ -64,7 +66,7 @@ router.post('/:version/chat/private', (req: Request, res: Response) => {
|
|
|
64
66
|
|
|
65
67
|
// Generate hash
|
|
66
68
|
router.post('/:version/hash', (req: Request, res: Response) => {
|
|
67
|
-
const { text, method } = (req.body as Record<string, string>) || {};
|
|
69
|
+
const { text, method, encoding } = (req.body as Record<string, string>) || {};
|
|
68
70
|
|
|
69
71
|
if (!text) {
|
|
70
72
|
error(res, 400, 'Please provide a text (?text={text})', `${req.version}/hash`);
|
|
@@ -76,8 +78,14 @@ router.post('/:version/hash', (req: Request, res: Response) => {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
if (since(req.version, 4)) {
|
|
82
|
+
const hashFn = req.module.hash as (t: string, m: string, e?: string) => HashResult;
|
|
83
|
+
const result = hashFn(text, method, encoding);
|
|
84
|
+
res.jsonResponse(result);
|
|
85
|
+
} else {
|
|
86
|
+
const result = (req.module.hash as (t: string, m: string) => Record<string, string>)(text, method);
|
|
87
|
+
res.jsonResponse(result);
|
|
88
|
+
}
|
|
81
89
|
} catch (err) {
|
|
82
90
|
error(res, 400, (err as Error).message, `${req.version}/hash`);
|
|
83
91
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a hex color string into an RGB tuple.
|
|
3
|
+
*
|
|
4
|
+
* @param hex - Hex color string (#RRGGBB or RRGGBB)
|
|
5
|
+
* @returns RGB tuple [r, g, b] with values 0-255
|
|
6
|
+
* @throws Error if the hex string is invalid
|
|
7
|
+
*/
|
|
8
|
+
export function hexToRgb(hex: string): [number, number, number] {
|
|
9
|
+
const clean = hex.replace('#', '');
|
|
10
|
+
if (!/^[0-9a-fA-F]{6}$/.test(clean)) throw new Error('Invalid HEX color (use #RRGGBB)');
|
|
11
|
+
return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Converts HSL values to an RGB tuple.
|
|
16
|
+
*
|
|
17
|
+
* @param h - Hue in degrees (0-360)
|
|
18
|
+
* @param s - Saturation (0-1)
|
|
19
|
+
* @param l - Lightness (0-1)
|
|
20
|
+
* @returns RGB tuple [r, g, b] with values 0-255
|
|
21
|
+
*/
|
|
22
|
+
export function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
23
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
24
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
25
|
+
const m = l - c / 2;
|
|
26
|
+
let r1 = 0,
|
|
27
|
+
g1 = 0,
|
|
28
|
+
b1 = 0;
|
|
29
|
+
if (h < 60) [r1, g1, b1] = [c, x, 0];
|
|
30
|
+
else if (h < 120) [r1, g1, b1] = [x, c, 0];
|
|
31
|
+
else if (h < 180) [r1, g1, b1] = [0, c, x];
|
|
32
|
+
else if (h < 240) [r1, g1, b1] = [0, x, c];
|
|
33
|
+
else if (h < 300) [r1, g1, b1] = [x, 0, c];
|
|
34
|
+
else [r1, g1, b1] = [c, 0, x];
|
|
35
|
+
return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validates and normalizes a hex color string, adding # if missing.
|
|
40
|
+
*
|
|
41
|
+
* @param value - Raw color value (with or without #)
|
|
42
|
+
* @param def - Default color if value is empty
|
|
43
|
+
* @returns Normalized hex color string with #
|
|
44
|
+
* @throws Error if the color format is invalid
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeColor(value: string | undefined, def: string): string {
|
|
47
|
+
if (!value) return def;
|
|
48
|
+
const clean = value.startsWith('#') ? value : `#${value}`;
|
|
49
|
+
if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(clean)) throw new Error('Invalid color (use hex like ff6600)');
|
|
50
|
+
return clean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Formats an RGB tuple as a hex color string.
|
|
55
|
+
*
|
|
56
|
+
* @param r - Red channel (0-255)
|
|
57
|
+
* @param g - Green channel (0-255)
|
|
58
|
+
* @param b - Blue channel (0-255)
|
|
59
|
+
* @returns Hex color string (#rrggbb)
|
|
60
|
+
*/
|
|
61
|
+
export function rgbToHex(r: number, g: number, b: number): string {
|
|
62
|
+
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Converts RGB values to an HSL tuple.
|
|
67
|
+
*
|
|
68
|
+
* @param r - Red channel (0-255)
|
|
69
|
+
* @param g - Green channel (0-255)
|
|
70
|
+
* @param b - Blue channel (0-255)
|
|
71
|
+
* @returns HSL tuple [h (0-360), s (0-1), l (0-1)]
|
|
72
|
+
*/
|
|
73
|
+
export function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
|
74
|
+
const r1 = r / 255,
|
|
75
|
+
g1 = g / 255,
|
|
76
|
+
b1 = b / 255;
|
|
77
|
+
const max = Math.max(r1, g1, b1),
|
|
78
|
+
min = Math.min(r1, g1, b1);
|
|
79
|
+
const l = (max + min) / 2;
|
|
80
|
+
|
|
81
|
+
if (max === min) return [0, 0, l];
|
|
82
|
+
|
|
83
|
+
const d = max - min;
|
|
84
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
85
|
+
let h = 0;
|
|
86
|
+
if (max === r1) h = (g1 - b1) / d + (g1 < b1 ? 6 : 0);
|
|
87
|
+
else if (max === g1) h = (b1 - r1) / d + 2;
|
|
88
|
+
else h = (r1 - g1) / d + 4;
|
|
89
|
+
|
|
90
|
+
return [(h * 60 + 360) % 360, s, l];
|
|
91
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks requests per user and throws if the rate limit is exceeded.
|
|
3
|
+
*
|
|
4
|
+
* @param rateLimits - Store of timestamps per user
|
|
5
|
+
* @param userId - Identifier of the requesting user
|
|
6
|
+
* @param timestamp - Current timestamp in milliseconds
|
|
7
|
+
* @param window - Time window in milliseconds
|
|
8
|
+
* @param limit - Maximum requests allowed in the window
|
|
9
|
+
* @returns false if within limits
|
|
10
|
+
* @throws Error if rate limit is exceeded
|
|
11
|
+
*/
|
|
12
|
+
export function checkRateLimit(
|
|
13
|
+
rateLimits: Record<string, number[]>,
|
|
14
|
+
userId: string,
|
|
15
|
+
timestamp: number,
|
|
16
|
+
window = 10000,
|
|
17
|
+
limit = 50,
|
|
18
|
+
): boolean {
|
|
19
|
+
rateLimits[userId] = (rateLimits[userId] ?? []).filter((ts) => timestamp - ts < window);
|
|
20
|
+
|
|
21
|
+
if (rateLimits[userId]!.length > limit) {
|
|
22
|
+
const remainingTime = Math.ceil((rateLimits[userId]![0]! + window - timestamp) / 1000);
|
|
23
|
+
throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
rateLimits[userId]!.push(timestamp);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses a space-separated environment variable into an array.
|
|
32
|
+
*
|
|
33
|
+
* @param key - The environment variable name
|
|
34
|
+
* @returns The parsed array, or null if not set
|
|
35
|
+
*/
|
|
36
|
+
export function envList(key: string): string[] | null {
|
|
37
|
+
const v = process.env[key];
|
|
38
|
+
return v && v !== 'undefined' ? v.split(' ') : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Formats a Date to ISO string without timezone suffix.
|
|
43
|
+
*
|
|
44
|
+
* @param date - The date to format
|
|
45
|
+
* @returns ISO-formatted string without trailing Z
|
|
46
|
+
*/
|
|
47
|
+
export function formatDate(date: Date): string {
|
|
48
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().replace('Z', '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generates a random IPv4 address.
|
|
53
|
+
*
|
|
54
|
+
* @returns A string in the format "X.X.X.X"
|
|
55
|
+
*/
|
|
56
|
+
export function genIP(): string {
|
|
57
|
+
return `${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a random element from an array.
|
|
62
|
+
*
|
|
63
|
+
* @param arr - The source array
|
|
64
|
+
* @returns A random element
|
|
65
|
+
*/
|
|
66
|
+
export function random<T>(arr: T[]): T {
|
|
67
|
+
return arr[Math.floor(Math.random() * arr.length)]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns a random integer between min and max (inclusive).
|
|
72
|
+
*
|
|
73
|
+
* @param min - Lower bound
|
|
74
|
+
* @param max - Upper bound
|
|
75
|
+
* @returns A random integer in [min, max]
|
|
76
|
+
*/
|
|
77
|
+
export function randomNumber(min: number, max: number): number {
|
|
78
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a version string is at least the given minimum.
|
|
83
|
+
*
|
|
84
|
+
* @param version - Version string (e.g. "v4")
|
|
85
|
+
* @param min - Minimum version number
|
|
86
|
+
* @returns true if the version is >= min
|
|
87
|
+
*/
|
|
88
|
+
export function since(version: string, min: number): boolean {
|
|
89
|
+
return parseInt(version.replace('v', '')) >= min;
|
|
90
|
+
}
|
package/src/utils/response.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Response } from 'express';
|
|
2
2
|
import { DOCS_URL, STATUS_MESSAGES } from '../constants.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Sends a standardized JSON error response.
|
|
6
|
+
*
|
|
7
|
+
* @param res - Express response object
|
|
8
|
+
* @param status - HTTP status code
|
|
9
|
+
* @param message - Error description
|
|
10
|
+
* @param docPath - Optional documentation path appended to the base URL
|
|
11
|
+
*/
|
|
4
12
|
export function error(res: Response, status: number, message: string, docPath?: string): void {
|
|
5
13
|
res.status(status).jsonResponse({
|
|
6
14
|
message: STATUS_MESSAGES[status] ?? 'Error',
|