@20syldev/api 4.3.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/config/versions.js +10 -0
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +0 -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 +5 -10
- 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/routes/delete.js +14 -14
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +68 -13
- package/dist/routes/get.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/config/versions.ts +10 -0
- package/src/constants.ts +0 -2
- package/src/modules/v4/captcha.ts +84 -27
- package/src/modules/v4/chat.ts +1 -1
- 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/hash.ts +18 -4
- package/src/modules/v4/hyperplanning.ts +1 -1
- package/src/modules/v4/palette.ts +3 -43
- package/src/modules/v4/personal.ts +1 -1
- package/src/modules/v4/placeholder.ts +6 -9
- package/src/modules/v4/qrcode.ts +108 -6
- package/src/modules/v4/username.ts +1 -1
- package/src/routes/get.ts +71 -15
- 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 +51 -7
- package/tests/unit/captcha.test.ts +50 -12
- package/tests/unit/chat.test.ts +4 -1
- package/tests/unit/color.test.ts +24 -4
- package/tests/unit/convert.test.ts +26 -1
- package/tests/unit/hash.test.ts +17 -10
- package/tests/unit/qrcode.test.ts +72 -4
- package/tests/unit/tic_tac_toe.test.ts +4 -1
- package/src/modules/v4/utils.ts +0 -38
package/src/modules/v4/color.ts
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { hexToRgb, rgbToHsl, rgbToHex } from '../../utils/colors.js';
|
|
2
|
+
|
|
3
|
+
export interface ColorResult {
|
|
4
|
+
hex: string;
|
|
5
|
+
rgb: string;
|
|
6
|
+
hsl: string;
|
|
7
|
+
hsv: string;
|
|
8
|
+
hwb: string;
|
|
9
|
+
cmyk: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
|
13
|
+
const max = Math.max(r, g, b),
|
|
14
|
+
min = Math.min(r, g, b),
|
|
15
|
+
v = max / 255,
|
|
16
|
+
s = max ? (max - min) / max : 0;
|
|
17
|
+
|
|
18
|
+
if (max === min) return [0, s * 100, v * 100];
|
|
19
|
+
|
|
20
|
+
let h = 0;
|
|
21
|
+
const d = max - min;
|
|
22
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
23
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
24
|
+
else h = (r - g) / d + 4;
|
|
5
25
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
16
|
-
const h =
|
|
17
|
-
{
|
|
18
|
-
[r1]: (g1 - b1) / d + (g1 < b1 ? 6 : 0),
|
|
19
|
-
[g1]: (b1 - r1) / d + 2,
|
|
20
|
-
[b1]: (r1 - g1) / d + 4,
|
|
21
|
-
}[max] ?? 0;
|
|
22
|
-
return [(h * 60) % 360, s * 100, l * 100];
|
|
23
|
-
})();
|
|
26
|
+
return [(h * 60) % 360, s * 100, v * 100];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rgbToHwb(r: number, g: number, b: number): [number, number, number] {
|
|
30
|
+
const [h] = rgbToHsv(r, g, b);
|
|
31
|
+
const whiteness = Math.min(r, g, b) / 255;
|
|
32
|
+
const blackness = 1 - Math.max(r, g, b) / 255;
|
|
33
|
+
return [h, whiteness * 100, blackness * 100];
|
|
34
|
+
}
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[g]: 2 + (b - r) / (max - min),
|
|
36
|
-
[b]: 4 + (r - g) / (max - min),
|
|
37
|
-
}[max] ?? 0);
|
|
38
|
-
return [(h * 60) % 360, s * 100, v * 100];
|
|
39
|
-
})();
|
|
36
|
+
function rgbToCmyk(r: number, g: number, b: number): [number, number, number, number] {
|
|
37
|
+
const k = 1 - Math.max(r, g, b) / 255;
|
|
38
|
+
const c = (1 - r / 255 - k) / (1 - k) || 0;
|
|
39
|
+
const m = (1 - g / 255 - k) / (1 - k) || 0;
|
|
40
|
+
const y = (1 - b / 255 - k) / (1 - k) || 0;
|
|
41
|
+
return [c * 100, m * 100, y * 100, k * 100];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function color(hex?: string): ColorResult {
|
|
45
|
+
let r: number, g: number, b: number;
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
if (hex) {
|
|
48
|
+
[r, g, b] = hexToRgb(hex);
|
|
49
|
+
} else {
|
|
50
|
+
r = Math.floor(Math.random() * 256);
|
|
51
|
+
g = Math.floor(Math.random() * 256);
|
|
52
|
+
b = Math.floor(Math.random() * 256);
|
|
53
|
+
}
|
|
47
54
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
y = (1 - b / 255 - k) / (1 - k) || 0;
|
|
53
|
-
return [c * 100, m * 100, y * 100, k * 100];
|
|
54
|
-
})();
|
|
55
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
56
|
+
const hsv = rgbToHsv(r, g, b);
|
|
57
|
+
const hwb = rgbToHwb(r, g, b);
|
|
58
|
+
const cmyk = rgbToCmyk(r, g, b);
|
|
55
59
|
|
|
56
60
|
return {
|
|
57
|
-
hex:
|
|
61
|
+
hex: rgbToHex(r, g, b),
|
|
58
62
|
rgb: `rgb(${r}, ${g}, ${b})`,
|
|
59
|
-
hsl: `hsl(${
|
|
63
|
+
hsl: `hsl(${h.toFixed(1)}, ${(s * 100).toFixed(1)}%, ${(l * 100).toFixed(1)}%)`,
|
|
60
64
|
hsv: `hsv(${hsv[0].toFixed(1)}, ${hsv[1].toFixed(1)}%, ${hsv[2].toFixed(1)}%)`,
|
|
61
65
|
hwb: `hwb(${hwb[0].toFixed(1)}, ${hwb[1].toFixed(1)}%, ${hwb[2].toFixed(1)}%)`,
|
|
62
66
|
cmyk: `cmyk(${cmyk.map((x) => x.toFixed(1)).join('%, ')}%)`,
|
|
@@ -1,38 +1,118 @@
|
|
|
1
|
-
import { MIN_TEMPERATURE, MAX_TEMPERATURE } from '../../constants.js';
|
|
2
|
-
|
|
3
1
|
type ConversionFn = (val: number) => number;
|
|
4
2
|
|
|
3
|
+
const TEMPERATURE_UNITS = new Set(['celsius', 'fahrenheit', 'kelvin']);
|
|
4
|
+
const ABSOLUTE_ZERO: Record<string, number> = { celsius: -273.15, fahrenheit: -459.67, kelvin: 0 };
|
|
5
|
+
|
|
5
6
|
const conversions: Record<string, Record<string, ConversionFn>> = {
|
|
7
|
+
// Temperature
|
|
6
8
|
celsius: {
|
|
7
|
-
fahrenheit: (
|
|
8
|
-
kelvin: (
|
|
9
|
+
fahrenheit: (v) => (v * 9) / 5 + 32,
|
|
10
|
+
kelvin: (v) => v + 273.15,
|
|
9
11
|
},
|
|
10
12
|
fahrenheit: {
|
|
11
|
-
celsius: (
|
|
12
|
-
kelvin: (
|
|
13
|
+
celsius: (v) => ((v - 32) * 5) / 9,
|
|
14
|
+
kelvin: (v) => ((v - 32) * 5) / 9 + 273.15,
|
|
13
15
|
},
|
|
14
16
|
kelvin: {
|
|
15
|
-
celsius: (
|
|
16
|
-
fahrenheit: (
|
|
17
|
+
celsius: (v) => v - 273.15,
|
|
18
|
+
fahrenheit: (v) => ((v - 273.15) * 9) / 5 + 32,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Length
|
|
22
|
+
km: {
|
|
23
|
+
mi: (v) => v * 0.621371,
|
|
24
|
+
m: (v) => v * 1000,
|
|
25
|
+
ft: (v) => v * 3280.84,
|
|
26
|
+
cm: (v) => v * 100000,
|
|
27
|
+
in: (v) => v * 39370.1,
|
|
28
|
+
yd: (v) => v * 1093.61,
|
|
29
|
+
},
|
|
30
|
+
mi: {
|
|
31
|
+
km: (v) => v * 1.60934,
|
|
32
|
+
m: (v) => v * 1609.34,
|
|
33
|
+
ft: (v) => v * 5280,
|
|
34
|
+
cm: (v) => v * 160934,
|
|
35
|
+
in: (v) => v * 63360,
|
|
36
|
+
yd: (v) => v * 1760,
|
|
37
|
+
},
|
|
38
|
+
m: {
|
|
39
|
+
km: (v) => v / 1000,
|
|
40
|
+
mi: (v) => v * 0.000621371,
|
|
41
|
+
ft: (v) => v * 3.28084,
|
|
42
|
+
cm: (v) => v * 100,
|
|
43
|
+
in: (v) => v * 39.3701,
|
|
44
|
+
yd: (v) => v * 1.09361,
|
|
45
|
+
},
|
|
46
|
+
ft: {
|
|
47
|
+
km: (v) => v * 0.0003048,
|
|
48
|
+
mi: (v) => v / 5280,
|
|
49
|
+
m: (v) => v * 0.3048,
|
|
50
|
+
cm: (v) => v * 30.48,
|
|
51
|
+
in: (v) => v * 12,
|
|
52
|
+
yd: (v) => v / 3,
|
|
53
|
+
},
|
|
54
|
+
cm: {
|
|
55
|
+
km: (v) => v / 100000,
|
|
56
|
+
mi: (v) => v / 160934,
|
|
57
|
+
m: (v) => v / 100,
|
|
58
|
+
ft: (v) => v / 30.48,
|
|
59
|
+
in: (v) => v / 2.54,
|
|
60
|
+
yd: (v) => v / 91.44,
|
|
17
61
|
},
|
|
62
|
+
in: {
|
|
63
|
+
km: (v) => v / 39370.1,
|
|
64
|
+
mi: (v) => v / 63360,
|
|
65
|
+
m: (v) => v * 0.0254,
|
|
66
|
+
ft: (v) => v / 12,
|
|
67
|
+
cm: (v) => v * 2.54,
|
|
68
|
+
yd: (v) => v / 36,
|
|
69
|
+
},
|
|
70
|
+
yd: {
|
|
71
|
+
km: (v) => v * 0.0009144,
|
|
72
|
+
mi: (v) => v / 1760,
|
|
73
|
+
m: (v) => v * 0.9144,
|
|
74
|
+
ft: (v) => v * 3,
|
|
75
|
+
cm: (v) => v * 91.44,
|
|
76
|
+
in: (v) => v * 36,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Weight
|
|
80
|
+
kg: { lb: (v) => v * 2.20462, oz: (v) => v * 35.274, g: (v) => v * 1000, ton: (v) => v / 1000 },
|
|
81
|
+
lb: { kg: (v) => v * 0.453592, oz: (v) => v * 16, g: (v) => v * 453.592, ton: (v) => v * 0.000453592 },
|
|
82
|
+
oz: { kg: (v) => v * 0.0283495, lb: (v) => v / 16, g: (v) => v * 28.3495, ton: (v) => v * 0.0000283495 },
|
|
83
|
+
g: { kg: (v) => v / 1000, lb: (v) => v * 0.00220462, oz: (v) => v * 0.035274, ton: (v) => v / 1000000 },
|
|
84
|
+
ton: { kg: (v) => v * 1000, lb: (v) => v * 2204.62, oz: (v) => v * 35274, g: (v) => v * 1000000 },
|
|
85
|
+
|
|
86
|
+
// Data
|
|
87
|
+
b: { kb: (v) => v / 1024, mb: (v) => v / 1048576, gb: (v) => v / 1073741824, tb: (v) => v / 1099511627776 },
|
|
88
|
+
kb: { b: (v) => v * 1024, mb: (v) => v / 1024, gb: (v) => v / 1048576, tb: (v) => v / 1073741824 },
|
|
89
|
+
mb: { b: (v) => v * 1048576, kb: (v) => v * 1024, gb: (v) => v / 1024, tb: (v) => v / 1048576 },
|
|
90
|
+
gb: { b: (v) => v * 1073741824, kb: (v) => v * 1048576, mb: (v) => v * 1024, tb: (v) => v / 1024 },
|
|
91
|
+
tb: { b: (v) => v * 1099511627776, kb: (v) => v * 1073741824, mb: (v) => v * 1048576, gb: (v) => v * 1024 },
|
|
92
|
+
|
|
93
|
+
// Speed
|
|
94
|
+
'km/h': { mph: (v) => v * 0.621371, 'm/s': (v) => v / 3.6, knots: (v) => v * 0.539957 },
|
|
95
|
+
mph: { 'km/h': (v) => v * 1.60934, 'm/s': (v) => v * 0.44704, knots: (v) => v * 0.868976 },
|
|
96
|
+
'm/s': { 'km/h': (v) => v * 3.6, mph: (v) => v * 2.23694, knots: (v) => v * 1.94384 },
|
|
97
|
+
knots: { 'km/h': (v) => v * 1.852, mph: (v) => v * 1.15078, 'm/s': (v) => v * 0.514444 },
|
|
18
98
|
};
|
|
19
99
|
|
|
20
100
|
export default function convert(
|
|
21
|
-
value:
|
|
101
|
+
value: number,
|
|
22
102
|
from: string,
|
|
23
103
|
to: string,
|
|
24
104
|
): { from: string; to: string; value: number; result: number } {
|
|
25
|
-
const
|
|
105
|
+
const fromKey = from.toLowerCase();
|
|
106
|
+
const toKey = to.toLowerCase();
|
|
107
|
+
const convertFn = conversions[fromKey]?.[toKey];
|
|
26
108
|
|
|
27
109
|
if (!convertFn) throw new Error('Invalid conversion unit');
|
|
28
|
-
if (isNaN(
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
result: convertFn(parseFloat(String(value))),
|
|
37
|
-
};
|
|
110
|
+
if (isNaN(value)) throw new Error('Value must be a number');
|
|
111
|
+
|
|
112
|
+
if (TEMPERATURE_UNITS.has(fromKey)) {
|
|
113
|
+
const minValue = ABSOLUTE_ZERO[fromKey]!;
|
|
114
|
+
if (value < minValue) throw new Error('Value must be greater than absolute zero');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { from, to, value, result: convertFn(value) };
|
|
38
118
|
}
|
package/src/modules/v4/domain.ts
CHANGED
package/src/modules/v4/hash.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { getHashes, createHash } from 'crypto';
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface HashResult {
|
|
4
|
+
method: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
encoding: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ENCODINGS = new Set(['hex', 'base64']);
|
|
10
|
+
|
|
11
|
+
export default function hash(text: string, method: string, encoding: string = 'hex'): HashResult {
|
|
12
|
+
if (!text) throw new Error('Text is required');
|
|
13
|
+
|
|
4
14
|
const methods = getHashes();
|
|
5
|
-
if (!methods.includes(method))
|
|
15
|
+
if (!methods.includes(method)) throw new Error(`Unsupported method. Use one of: ${methods.join(', ')}`);
|
|
16
|
+
|
|
17
|
+
if (!ENCODINGS.has(encoding)) throw new Error('Encoding must be one of: hex, base64');
|
|
6
18
|
|
|
7
|
-
const result = createHash(method)
|
|
8
|
-
|
|
19
|
+
const result = createHash(method)
|
|
20
|
+
.update(text)
|
|
21
|
+
.digest(encoding as 'hex' | 'base64');
|
|
22
|
+
return { method, hash: result, encoding };
|
|
9
23
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hexToRgb, rgbToHsl, hslToRgb, rgbToHex } from '../../utils/colors.js';
|
|
2
|
+
|
|
1
3
|
export interface PaletteColor {
|
|
2
4
|
hex: string;
|
|
3
5
|
rgb: string;
|
|
@@ -10,52 +12,10 @@ export interface PaletteResult {
|
|
|
10
12
|
colors: PaletteColor[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function hexToRgb(hex: string): [number, number, number] {
|
|
14
|
-
const clean = hex.replace('#', '');
|
|
15
|
-
if (!/^[0-9a-fA-F]{6}$/.test(clean)) throw new Error('Invalid HEX color (use #RRGGBB)');
|
|
16
|
-
return [parseInt(clean.slice(0, 2), 16), parseInt(clean.slice(2, 4), 16), parseInt(clean.slice(4, 6), 16)];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
|
20
|
-
const r1 = r / 255,
|
|
21
|
-
g1 = g / 255,
|
|
22
|
-
b1 = b / 255;
|
|
23
|
-
const max = Math.max(r1, g1, b1),
|
|
24
|
-
min = Math.min(r1, g1, b1);
|
|
25
|
-
const l = (max + min) / 2;
|
|
26
|
-
|
|
27
|
-
if (max === min) return [0, 0, l];
|
|
28
|
-
|
|
29
|
-
const d = max - min;
|
|
30
|
-
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
31
|
-
let h = 0;
|
|
32
|
-
if (max === r1) h = (g1 - b1) / d + (g1 < b1 ? 6 : 0);
|
|
33
|
-
else if (max === g1) h = (b1 - r1) / d + 2;
|
|
34
|
-
else h = (r1 - g1) / d + 4;
|
|
35
|
-
|
|
36
|
-
return [(h * 60 + 360) % 360, s, l];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
40
|
-
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
41
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
42
|
-
const m = l - c / 2;
|
|
43
|
-
let r1 = 0,
|
|
44
|
-
g1 = 0,
|
|
45
|
-
b1 = 0;
|
|
46
|
-
if (h < 60) [r1, g1, b1] = [c, x, 0];
|
|
47
|
-
else if (h < 120) [r1, g1, b1] = [x, c, 0];
|
|
48
|
-
else if (h < 180) [r1, g1, b1] = [0, c, x];
|
|
49
|
-
else if (h < 240) [r1, g1, b1] = [0, x, c];
|
|
50
|
-
else if (h < 300) [r1, g1, b1] = [x, 0, c];
|
|
51
|
-
else [r1, g1, b1] = [c, 0, x];
|
|
52
|
-
return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
15
|
function format(r: number, g: number, b: number): PaletteColor {
|
|
56
16
|
const [h, s, l] = rgbToHsl(r, g, b);
|
|
57
17
|
return {
|
|
58
|
-
hex:
|
|
18
|
+
hex: rgbToHex(r, g, b),
|
|
59
19
|
rgb: `rgb(${r}, ${g}, ${b})`,
|
|
60
20
|
hsl: `hsl(${h.toFixed(1)}, ${(s * 100).toFixed(1)}%, ${(l * 100).toFixed(1)}%)`,
|
|
61
21
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeColor } from '../../utils/colors.js';
|
|
2
|
+
|
|
1
3
|
type AvatarShape = 'circle' | 'rounded' | 'square';
|
|
2
4
|
type AnimateMode = 'shimmer' | 'pulse' | 'none';
|
|
3
5
|
|
|
@@ -23,13 +25,6 @@ function parseSize(value: string | undefined, name: string, def: number): number
|
|
|
23
25
|
return Math.floor(n);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
function normalizeColor(value: string | undefined, def: string): string {
|
|
27
|
-
if (!value) return def;
|
|
28
|
-
const clean = value.startsWith('#') ? value : `#${value}`;
|
|
29
|
-
if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(clean)) throw new Error('Invalid color (use hex like #ff6600)');
|
|
30
|
-
return clean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
28
|
function escapeXml(str: string): string {
|
|
34
29
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
35
30
|
}
|
|
@@ -88,8 +83,10 @@ function generateSkeleton(opts: PlaceholderOptions): string {
|
|
|
88
83
|
const rx = opts.radius ?? 4;
|
|
89
84
|
|
|
90
85
|
const pad = Math.round(width * 0.06);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
86
|
+
const slots = rows + (avatar ? 4 : 0);
|
|
87
|
+
const available = height - pad * 2;
|
|
88
|
+
const lineH = Math.round(Math.max(8, (available / Math.max(1, slots)) * 0.65));
|
|
89
|
+
const gap = Math.round(Math.max(6, (available / Math.max(1, slots)) * 0.35));
|
|
93
90
|
|
|
94
91
|
const shapes: string[] = [];
|
|
95
92
|
let y = pad;
|
package/src/modules/v4/qrcode.ts
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
|
-
import { toDataURL } from 'qrcode';
|
|
1
|
+
import { toBuffer, toDataURL, toCanvas as qrToCanvas } from 'qrcode';
|
|
2
|
+
import { createCanvas, loadImage } from 'canvas';
|
|
3
|
+
import { normalizeColor } from '../../utils/colors.js';
|
|
2
4
|
|
|
3
|
-
export
|
|
5
|
+
export interface QRCodeOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
margin?: number;
|
|
9
|
+
correction?: 'L' | 'M' | 'Q' | 'H';
|
|
10
|
+
dark?: string;
|
|
11
|
+
light?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
iconSize?: number;
|
|
14
|
+
iconPadding?: number;
|
|
15
|
+
iconRadius?: number;
|
|
16
|
+
format?: 'png' | 'base64';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QRCodeResult {
|
|
20
|
+
contentType: string;
|
|
21
|
+
body: Buffer | string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CORRECTIONS = new Set(['L', 'M', 'Q', 'H']);
|
|
25
|
+
const FORMATS = new Set(['png', 'base64']);
|
|
26
|
+
const MAX_LOGO_BYTES = 2 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
function clamp(value: number | undefined, name: string, def: number, min: number, max: number): number {
|
|
29
|
+
if (value === undefined) return def;
|
|
30
|
+
if (isNaN(value)) throw new Error(`${name} must be a number`);
|
|
31
|
+
if (value < min || value > max) throw new Error(`${name} must be between ${min} and ${max}`);
|
|
32
|
+
return Math.floor(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchIcon(url: string): Promise<Buffer> {
|
|
36
|
+
if (!/^https:\/\//i.test(url)) throw new Error('Icon URL must use HTTPS');
|
|
37
|
+
|
|
38
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
39
|
+
|
|
40
|
+
if (!response.ok) throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
|
|
41
|
+
|
|
42
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
43
|
+
if (!contentType.startsWith('image/')) throw new Error('Icon URL must point to an image');
|
|
44
|
+
|
|
45
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
46
|
+
if (buffer.length > MAX_LOGO_BYTES) throw new Error('Icon must be under 2MB');
|
|
47
|
+
|
|
48
|
+
return buffer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default async function qrcode(options: QRCodeOptions): Promise<QRCodeResult> {
|
|
52
|
+
const { url } = options;
|
|
4
53
|
if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
|
|
5
54
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
55
|
+
const format = options.format ?? 'png';
|
|
56
|
+
if (!FORMATS.has(format)) throw new Error('format must be one of: png, base64');
|
|
57
|
+
|
|
58
|
+
const size = clamp(options.size, 'size', 200, 50, 2000);
|
|
59
|
+
const margin = clamp(options.margin, 'margin', 4, 0, 10);
|
|
60
|
+
const dark = normalizeColor(options.dark, '#000000');
|
|
61
|
+
const light = normalizeColor(options.light, '#ffffff');
|
|
62
|
+
|
|
63
|
+
let correction = options.correction ?? 'M';
|
|
64
|
+
if (!CORRECTIONS.has(correction)) throw new Error('correction must be one of: L, M, Q, H');
|
|
65
|
+
if (options.icon) correction = 'H';
|
|
66
|
+
|
|
67
|
+
const qrOpts = {
|
|
68
|
+
width: size,
|
|
69
|
+
margin,
|
|
70
|
+
errorCorrectionLevel: correction as 'L' | 'M' | 'Q' | 'H',
|
|
71
|
+
color: { dark, light },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!options.icon) {
|
|
75
|
+
if (format === 'base64') {
|
|
76
|
+
return { contentType: 'application/json', body: await toDataURL(url, qrOpts) };
|
|
77
|
+
}
|
|
78
|
+
return { contentType: 'image/png', body: await toBuffer(url, qrOpts) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const canvas = createCanvas(size, size);
|
|
82
|
+
await qrToCanvas(canvas as unknown as HTMLCanvasElement, url, qrOpts);
|
|
83
|
+
|
|
84
|
+
const iconBuffer = await fetchIcon(options.icon);
|
|
85
|
+
const iconImg = await loadImage(iconBuffer);
|
|
86
|
+
|
|
87
|
+
const iconSize = clamp(options.iconSize, 'iconSize', Math.round(size * 0.2), 10, Math.round(size * 0.4));
|
|
88
|
+
const iconPadding = clamp(options.iconPadding, 'iconPadding', 5, 0, 50);
|
|
89
|
+
const maxRadius = Math.floor((iconSize + iconPadding * 2) / 2);
|
|
90
|
+
const iconRadius = clamp(options.iconRadius, 'iconRadius', 0, 0, maxRadius);
|
|
91
|
+
|
|
92
|
+
const ctx = canvas.getContext('2d');
|
|
93
|
+
const totalSize = iconSize + iconPadding * 2;
|
|
94
|
+
const x = (canvas.width - totalSize) / 2;
|
|
95
|
+
const y = (canvas.height - totalSize) / 2;
|
|
96
|
+
|
|
97
|
+
ctx.beginPath();
|
|
98
|
+
ctx.roundRect(x, y, totalSize, totalSize, iconRadius);
|
|
99
|
+
ctx.fillStyle = light;
|
|
100
|
+
ctx.fill();
|
|
101
|
+
|
|
102
|
+
ctx.save();
|
|
103
|
+
ctx.beginPath();
|
|
104
|
+
ctx.roundRect(x + iconPadding, y + iconPadding, iconSize, iconSize, Math.max(0, iconRadius - iconPadding));
|
|
105
|
+
ctx.clip();
|
|
106
|
+
ctx.drawImage(iconImg, x + iconPadding, y + iconPadding, iconSize, iconSize);
|
|
107
|
+
ctx.restore();
|
|
108
|
+
|
|
109
|
+
if (format === 'base64') {
|
|
110
|
+
return { contentType: 'application/json', body: canvas.toDataURL('image/png') };
|
|
10
111
|
}
|
|
112
|
+
return { contentType: 'image/png', body: canvas.toBuffer('image/png') };
|
|
11
113
|
}
|
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
|
}
|
|
@@ -338,8 +372,30 @@ router.get('/:version/qrcode', async (req: Request, res: Response) => {
|
|
|
338
372
|
}
|
|
339
373
|
|
|
340
374
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
399
|
} catch (err) {
|
|
344
400
|
error(res, 400, (err as Error).message, `${req.version}/qrcode`);
|
|
345
401
|
}
|