@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.
Files changed (80) hide show
  1. package/README.md +3 -3
  2. package/dist/config/versions.js +10 -0
  3. package/dist/config/versions.js.map +1 -1
  4. package/dist/constants.js +0 -2
  5. package/dist/constants.js.map +1 -1
  6. package/dist/modules/v3/chat.js +1 -1
  7. package/dist/modules/v3/chat.js.map +1 -1
  8. package/dist/modules/v3/domain.js +1 -1
  9. package/dist/modules/v3/domain.js.map +1 -1
  10. package/dist/modules/v3/hyperplanning.js +1 -1
  11. package/dist/modules/v3/hyperplanning.js.map +1 -1
  12. package/dist/modules/v3/personal.js +1 -1
  13. package/dist/modules/v3/personal.js.map +1 -1
  14. package/dist/modules/v3/username.js +1 -1
  15. package/dist/modules/v3/username.js.map +1 -1
  16. package/dist/modules/v4/captcha.js +60 -23
  17. package/dist/modules/v4/captcha.js.map +1 -1
  18. package/dist/modules/v4/chat.js +1 -1
  19. package/dist/modules/v4/chat.js.map +1 -1
  20. package/dist/modules/v4/color.js +44 -35
  21. package/dist/modules/v4/color.js.map +1 -1
  22. package/dist/modules/v4/colors.js +54 -0
  23. package/dist/modules/v4/colors.js.map +1 -0
  24. package/dist/modules/v4/convert.js +93 -19
  25. package/dist/modules/v4/convert.js.map +1 -1
  26. package/dist/modules/v4/domain.js +1 -1
  27. package/dist/modules/v4/domain.js.map +1 -1
  28. package/dist/modules/v4/hash.js +11 -4
  29. package/dist/modules/v4/hash.js.map +1 -1
  30. package/dist/modules/v4/hyperplanning.js +1 -1
  31. package/dist/modules/v4/hyperplanning.js.map +1 -1
  32. package/dist/modules/v4/palette.js +2 -43
  33. package/dist/modules/v4/palette.js.map +1 -1
  34. package/dist/modules/v4/personal.js +1 -1
  35. package/dist/modules/v4/personal.js.map +1 -1
  36. package/dist/modules/v4/placeholder.js +5 -10
  37. package/dist/modules/v4/placeholder.js.map +1 -1
  38. package/dist/modules/v4/qrcode.js +79 -6
  39. package/dist/modules/v4/qrcode.js.map +1 -1
  40. package/dist/modules/v4/username.js +1 -1
  41. package/dist/modules/v4/username.js.map +1 -1
  42. package/dist/routes/delete.js +14 -14
  43. package/dist/routes/delete.js.map +1 -1
  44. package/dist/routes/get.js +68 -13
  45. package/dist/routes/get.js.map +1 -1
  46. package/dist/routes/post.js +10 -3
  47. package/dist/routes/post.js.map +1 -1
  48. package/dist/utils/colors.js +54 -0
  49. package/dist/utils/colors.js.map +1 -0
  50. package/dist/utils/helpers.js +26 -0
  51. package/dist/utils/helpers.js.map +1 -0
  52. package/package.json +1 -1
  53. package/src/config/versions.ts +10 -0
  54. package/src/constants.ts +0 -2
  55. package/src/modules/v4/captcha.ts +84 -27
  56. package/src/modules/v4/chat.ts +1 -1
  57. package/src/modules/v4/color.ts +56 -52
  58. package/src/modules/v4/convert.ts +100 -20
  59. package/src/modules/v4/domain.ts +1 -1
  60. package/src/modules/v4/hash.ts +18 -4
  61. package/src/modules/v4/hyperplanning.ts +1 -1
  62. package/src/modules/v4/palette.ts +3 -43
  63. package/src/modules/v4/personal.ts +1 -1
  64. package/src/modules/v4/placeholder.ts +6 -9
  65. package/src/modules/v4/qrcode.ts +108 -6
  66. package/src/modules/v4/username.ts +1 -1
  67. package/src/routes/get.ts +71 -15
  68. package/src/routes/post.ts +11 -3
  69. package/src/utils/colors.ts +91 -0
  70. package/src/utils/helpers.ts +90 -0
  71. package/src/utils/response.ts +8 -0
  72. package/tests/integration/api.test.ts +51 -7
  73. package/tests/unit/captcha.test.ts +50 -12
  74. package/tests/unit/chat.test.ts +4 -1
  75. package/tests/unit/color.test.ts +24 -4
  76. package/tests/unit/convert.test.ts +26 -1
  77. package/tests/unit/hash.test.ts +17 -10
  78. package/tests/unit/qrcode.test.ts +72 -4
  79. package/tests/unit/tic_tac_toe.test.ts +4 -1
  80. package/src/modules/v4/utils.ts +0 -38
@@ -1,62 +1,66 @@
1
- export default function color(): Record<string, string> {
2
- const r = Math.floor(Math.random() * 256),
3
- g = Math.floor(Math.random() * 256),
4
- b = Math.floor(Math.random() * 256);
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
- const hsl = ((): [number, number, number] => {
7
- const r1 = r / 255,
8
- g1 = g / 255,
9
- b1 = b / 255,
10
- max = Math.max(r1, g1, b1),
11
- min = Math.min(r1, g1, b1),
12
- l = (max + min) / 2;
13
- if (max === min) return [0, 0, l * 100];
14
- const d = max - min,
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
- const hsv = ((): [number, number, number] => {
26
- const max = Math.max(r, g, b),
27
- min = Math.min(r, g, b),
28
- v = max / 255,
29
- s = max ? (max - min) / max : 0;
30
- const h =
31
- max === min
32
- ? 0
33
- : ({
34
- [r]: (g - b) / (max - min),
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
- const hwb = ((): [number, number, number] => {
42
- const [h] = hsv,
43
- whiteness = Math.min(r, g, b) / 255,
44
- blackness = 1 - Math.max(r, g, b) / 255;
45
- return [h, whiteness * 100, blackness * 100];
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 cmyk = ((): [number, number, number, number] => {
49
- const k = 1 - Math.max(r, g, b) / 255,
50
- c = (1 - r / 255 - k) / (1 - k) || 0,
51
- m = (1 - g / 255 - k) / (1 - k) || 0,
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: `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`,
61
+ hex: rgbToHex(r, g, b),
58
62
  rgb: `rgb(${r}, ${g}, ${b})`,
59
- hsl: `hsl(${hsl[0].toFixed(1)}, ${hsl[1].toFixed(1)}%, ${hsl[2].toFixed(1)}%)`,
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: (val) => (val * 9) / 5 + 32,
8
- kelvin: (val) => val + 273.15,
9
+ fahrenheit: (v) => (v * 9) / 5 + 32,
10
+ kelvin: (v) => v + 273.15,
9
11
  },
10
12
  fahrenheit: {
11
- celsius: (val) => ((val - 32) * 5) / 9,
12
- kelvin: (val) => ((val - 32) * 5) / 9 + 273.15,
13
+ celsius: (v) => ((v - 32) * 5) / 9,
14
+ kelvin: (v) => ((v - 32) * 5) / 9 + 273.15,
13
15
  },
14
16
  kelvin: {
15
- celsius: (val) => val - 273.15,
16
- fahrenheit: (val) => ((val - 273.15) * 9) / 5 + 32,
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: string | number,
101
+ value: number,
22
102
  from: string,
23
103
  to: string,
24
104
  ): { from: string; to: string; value: number; result: number } {
25
- const convertFn = conversions[from.toLowerCase()]?.[to.toLowerCase()];
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(Number(value))) throw new Error('Value must be a number');
29
- if (Number(value) < MIN_TEMPERATURE) throw new Error('Value must be greater than absolute zero');
30
- if (Number(value) > MAX_TEMPERATURE) throw new Error('Value must be less than 1,000,000');
31
-
32
- return {
33
- from,
34
- to,
35
- value: parseFloat(String(value)),
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
  }
@@ -1,4 +1,4 @@
1
- import { random, genIP } from './utils.js';
1
+ import { random, genIP } from '../../utils/helpers.js';
2
2
 
3
3
  export default function domain(): Record<string, unknown> {
4
4
  const subdomains = [
@@ -1,9 +1,23 @@
1
1
  import { getHashes, createHash } from 'crypto';
2
2
 
3
- export default function hash(text: string, method: string): { method: string; hash: string } | { error: string } {
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)) return { error: `Unsupported method. Use one of: ${methods.join(', ')}` };
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).update(text).digest('hex');
8
- return { method, hash: result };
19
+ const result = createHash(method)
20
+ .update(text)
21
+ .digest(encoding as 'hex' | 'base64');
22
+ return { method, hash: result, encoding };
9
23
  }
@@ -1,4 +1,4 @@
1
- import { formatDate } from './utils.js';
1
+ import { formatDate } from '../../utils/helpers.js';
2
2
  import ical from 'ical.js';
3
3
 
4
4
  interface CalendarEvent {
@@ -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: `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`,
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,4 +1,4 @@
1
- import { random, randomNumber } from './utils.js';
1
+ import { random, randomNumber } from '../../utils/helpers.js';
2
2
 
3
3
  interface Person {
4
4
  name: string;
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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 gap = Math.round(Math.max(6, Math.min(14, height * 0.04)));
92
- const lineH = Math.round(Math.max(8, Math.min(16, ((height - pad * 2) / (rows + (avatar ? 4 : 0))) * 0.65)));
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;
@@ -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 default async function qrcode(url: string): Promise<string> {
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
- try {
7
- return await toDataURL(url);
8
- } catch (error) {
9
- throw new Error(`Failed to generate QR code: ${(error as Error).message}`);
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
  }
@@ -1,4 +1,4 @@
1
- import { random, randomNumber } from './utils.js';
1
+ import { random, randomNumber } from '../../utils/helpers.js';
2
2
 
3
3
  export default function username(): Record<string, unknown> {
4
4
  const adj = [
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
- const result = req.module.captcha(text);
84
- res.type('png').send(result);
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
- const result = req.module.color();
112
- res.jsonResponse(result);
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
- const result = req.module.convert(value as string, from as string, to as string);
137
- res.jsonResponse(result);
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
- const result = await req.module.qrcode(url as string);
342
- res.jsonResponse(result);
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
  }