@20syldev/api 4.7.0 → 4.9.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 +84 -47
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/config/env.js.map +1 -1
- package/dist/config/versions.js +22 -3
- package/dist/config/versions.js.map +1 -1
- package/dist/constants.js +27 -12
- package/dist/constants.js.map +1 -1
- package/dist/middleware/error.js +2 -2
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/ratelimit.js +2 -2
- package/dist/middleware/ratelimit.js.map +1 -1
- package/dist/middleware/version.js.map +1 -1
- package/dist/modules/v3.js +1 -1
- package/dist/modules/v3.js.map +1 -1
- package/{src/modules/v4/address.ts → dist/modules/v4/address.js} +14 -46
- package/dist/modules/v4/address.js.map +1 -0
- package/{src/modules/v4/agent.ts → dist/modules/v4/agent.js} +26 -38
- package/dist/modules/v4/agent.js.map +1 -0
- package/dist/modules/v4/algorithms.js +79 -0
- package/dist/modules/v4/algorithms.js.map +1 -1
- package/dist/modules/v4/avatar.js +136 -0
- package/dist/modules/v4/avatar.js.map +1 -0
- package/dist/modules/v4/barcode.js +440 -0
- package/dist/modules/v4/barcode.js.map +1 -0
- package/dist/modules/v4/captcha.js +7 -0
- package/dist/modules/v4/captcha.js.map +1 -1
- package/dist/modules/v4/chat.js +9 -1
- package/dist/modules/v4/chat.js.map +1 -1
- package/dist/modules/v4/color.js +8 -1
- package/dist/modules/v4/color.js.map +1 -1
- package/dist/modules/v4/convert.js +9 -0
- package/dist/modules/v4/convert.js.map +1 -1
- package/dist/modules/v4/credit.js +104 -0
- package/dist/modules/v4/credit.js.map +1 -0
- package/dist/modules/v4/cron.js +202 -0
- package/dist/modules/v4/cron.js.map +1 -0
- package/dist/modules/v4/dice.js +7 -0
- package/dist/modules/v4/dice.js.map +1 -1
- package/dist/modules/v4/domain.js +6 -1
- package/dist/modules/v4/domain.js.map +1 -1
- package/dist/modules/v4/encode.js +71 -0
- package/dist/modules/v4/encode.js.map +1 -1
- package/dist/modules/v4/geo.js +11 -1
- package/dist/modules/v4/geo.js.map +1 -1
- package/dist/modules/v4/hash.js +10 -1
- package/dist/modules/v4/hash.js.map +1 -1
- package/dist/modules/v4/hyperplanning.js +9 -1
- package/dist/modules/v4/hyperplanning.js.map +1 -1
- package/{src/modules/v4/ip.ts → dist/modules/v4/ip.js} +58 -60
- package/dist/modules/v4/ip.js.map +1 -0
- package/dist/modules/v4/levenshtein.js +8 -0
- package/dist/modules/v4/levenshtein.js.map +1 -1
- package/dist/modules/v4/palette.js +9 -1
- package/dist/modules/v4/palette.js.map +1 -1
- package/{src/modules/v4/password.ts → dist/modules/v4/password.js} +25 -63
- package/dist/modules/v4/password.js.map +1 -0
- package/dist/modules/v4/personal.js +14 -18
- package/dist/modules/v4/personal.js.map +1 -1
- package/dist/modules/v4/placeholder.js +8 -0
- package/dist/modules/v4/placeholder.js.map +1 -1
- package/dist/modules/v4/qrcode.js +10 -3
- package/dist/modules/v4/qrcode.js.map +1 -1
- package/dist/modules/v4/regex.js +48 -0
- package/dist/modules/v4/regex.js.map +1 -0
- package/dist/modules/v4/statistics.js +7 -0
- package/dist/modules/v4/statistics.js.map +1 -1
- package/dist/modules/v4/text.js +30 -0
- package/dist/modules/v4/text.js.map +1 -1
- package/dist/modules/v4/tic_tac_toe.js +9 -1
- package/dist/modules/v4/tic_tac_toe.js.map +1 -1
- package/dist/modules/v4/time.js +62 -11
- package/dist/modules/v4/time.js.map +1 -1
- package/dist/modules/v4/token.js +17 -8
- package/dist/modules/v4/token.js.map +1 -1
- package/dist/modules/v4/username.js +5 -0
- package/dist/modules/v4/username.js.map +1 -1
- package/dist/modules/v4/validate.js +21 -0
- package/dist/modules/v4/validate.js.map +1 -1
- package/dist/modules/v4.js +10 -1
- package/dist/modules/v4.js.map +1 -1
- package/dist/routes/delete.js.map +1 -1
- package/dist/routes/get.js +208 -48
- 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.map +1 -1
- package/dist/routes/post.js +2 -2
- package/dist/routes/post.js.map +1 -1
- package/dist/utils/response.js.map +1 -1
- package/package.json +2 -1
- package/.github/FUNDING.yml +0 -1
- package/.prettierrc +0 -7
- package/eslint.config.js +0 -24
- package/robots.txt +0 -74
- package/src/app.ts +0 -53
- package/src/config/env.ts +0 -87
- package/src/config/plans.ts +0 -61
- package/src/config/versions.ts +0 -119
- package/src/constants.ts +0 -46
- package/src/favicon.ico +0 -0
- package/src/middleware/cors.ts +0 -23
- package/src/middleware/error.ts +0 -16
- package/src/middleware/json.ts +0 -9
- package/src/middleware/logger.ts +0 -32
- package/src/middleware/ratelimit.ts +0 -89
- package/src/middleware/version.ts +0 -40
- package/src/modules/v3/algorithms.js +0 -218
- package/src/modules/v3/captcha.js +0 -60
- package/src/modules/v3/chat.js +0 -111
- package/src/modules/v3/color.js +0 -60
- package/src/modules/v3/convert.js +0 -39
- package/src/modules/v3/domain.js +0 -86
- package/src/modules/v3/hash.js +0 -16
- package/src/modules/v3/hyperplanning.js +0 -56
- package/src/modules/v3/levenshtein.js +0 -46
- package/src/modules/v3/personal.js +0 -139
- package/src/modules/v3/qrcode.js +0 -20
- package/src/modules/v3/tic_tac_toe.js +0 -242
- package/src/modules/v3/time.js +0 -98
- package/src/modules/v3/token.js +0 -54
- package/src/modules/v3/username.js +0 -94
- package/src/modules/v3/utils.js +0 -77
- package/src/modules/v3.js +0 -15
- package/src/modules/v4/algorithms.ts +0 -276
- package/src/modules/v4/captcha.ts +0 -114
- package/src/modules/v4/chat.ts +0 -130
- package/src/modules/v4/color.ts +0 -75
- package/src/modules/v4/convert.ts +0 -127
- package/src/modules/v4/dice.ts +0 -46
- package/src/modules/v4/domain.ts +0 -86
- package/src/modules/v4/encode.ts +0 -241
- package/src/modules/v4/geo.ts +0 -63
- package/src/modules/v4/hash.ts +0 -32
- package/src/modules/v4/hyperplanning.ts +0 -93
- package/src/modules/v4/levenshtein.ts +0 -46
- package/src/modules/v4/palette.ts +0 -71
- package/src/modules/v4/personal.ts +0 -140
- package/src/modules/v4/placeholder.ts +0 -197
- package/src/modules/v4/qrcode.ts +0 -121
- package/src/modules/v4/statistics.ts +0 -62
- package/src/modules/v4/text.ts +0 -330
- package/src/modules/v4/tic_tac_toe.ts +0 -264
- package/src/modules/v4/time.ts +0 -103
- package/src/modules/v4/token.ts +0 -53
- package/src/modules/v4/username.ts +0 -94
- package/src/modules/v4/validate.ts +0 -76
- package/src/modules/v4.ts +0 -27
- package/src/routes/delete.ts +0 -72
- package/src/routes/get.ts +0 -835
- package/src/routes/index.ts +0 -59
- package/src/routes/patch.ts +0 -45
- package/src/routes/post.ts +0 -209
- package/src/storage/index.ts +0 -16
- package/src/types/express.d.ts +0 -17
- package/src/types/storage.ts +0 -48
- package/src/utils/colors.ts +0 -91
- package/src/utils/helpers.ts +0 -90
- package/src/utils/response.ts +0 -28
- package/tests/integration/api.test.ts +0 -716
- package/tests/tsconfig.json +0 -3
- package/tests/unit/address.test.ts +0 -84
- package/tests/unit/agent.test.ts +0 -113
- package/tests/unit/algorithms.test.ts +0 -121
- package/tests/unit/captcha.test.ts +0 -62
- package/tests/unit/chat.test.ts +0 -54
- package/tests/unit/color.test.ts +0 -54
- package/tests/unit/convert.test.ts +0 -68
- package/tests/unit/dice.test.ts +0 -58
- package/tests/unit/domain.test.ts +0 -48
- package/tests/unit/encode.test.ts +0 -109
- package/tests/unit/geo.test.ts +0 -46
- package/tests/unit/hash.test.ts +0 -45
- package/tests/unit/hyperplanning.test.ts +0 -97
- package/tests/unit/ip.test.ts +0 -140
- package/tests/unit/levenshtein.test.ts +0 -35
- package/tests/unit/palette.test.ts +0 -54
- package/tests/unit/password.test.ts +0 -86
- package/tests/unit/personal.test.ts +0 -66
- package/tests/unit/placeholder.test.ts +0 -58
- package/tests/unit/qrcode.test.ts +0 -83
- package/tests/unit/statistics.test.ts +0 -58
- package/tests/unit/text.test.ts +0 -108
- package/tests/unit/tic_tac_toe.test.ts +0 -56
- package/tests/unit/time.test.ts +0 -68
- package/tests/unit/token.test.ts +0 -62
- package/tests/unit/username.test.ts +0 -35
- package/tests/unit/validate.test.ts +0 -72
- package/tsconfig.json +0 -17
- package/tsconfig.test.json +0 -9
package/src/modules/v4/text.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { MAX_STRING_LENGTH } from '../../constants.js';
|
|
2
|
-
|
|
3
|
-
const LOREM_WORDS = [
|
|
4
|
-
'lorem',
|
|
5
|
-
'ipsum',
|
|
6
|
-
'dolor',
|
|
7
|
-
'sit',
|
|
8
|
-
'amet',
|
|
9
|
-
'consectetur',
|
|
10
|
-
'adipiscing',
|
|
11
|
-
'elit',
|
|
12
|
-
'sed',
|
|
13
|
-
'do',
|
|
14
|
-
'eiusmod',
|
|
15
|
-
'tempor',
|
|
16
|
-
'incididunt',
|
|
17
|
-
'ut',
|
|
18
|
-
'labore',
|
|
19
|
-
'et',
|
|
20
|
-
'dolore',
|
|
21
|
-
'magna',
|
|
22
|
-
'aliqua',
|
|
23
|
-
'enim',
|
|
24
|
-
'ad',
|
|
25
|
-
'minim',
|
|
26
|
-
'veniam',
|
|
27
|
-
'quis',
|
|
28
|
-
'nostrud',
|
|
29
|
-
'exercitation',
|
|
30
|
-
'ullamco',
|
|
31
|
-
'laboris',
|
|
32
|
-
'nisi',
|
|
33
|
-
'aliquip',
|
|
34
|
-
'ex',
|
|
35
|
-
'ea',
|
|
36
|
-
'commodo',
|
|
37
|
-
'consequat',
|
|
38
|
-
'duis',
|
|
39
|
-
'aute',
|
|
40
|
-
'irure',
|
|
41
|
-
'in',
|
|
42
|
-
'reprehenderit',
|
|
43
|
-
'voluptate',
|
|
44
|
-
'velit',
|
|
45
|
-
'esse',
|
|
46
|
-
'cillum',
|
|
47
|
-
'eu',
|
|
48
|
-
'fugiat',
|
|
49
|
-
'nulla',
|
|
50
|
-
'pariatur',
|
|
51
|
-
'excepteur',
|
|
52
|
-
'sint',
|
|
53
|
-
'occaecat',
|
|
54
|
-
'cupidatat',
|
|
55
|
-
'non',
|
|
56
|
-
'proident',
|
|
57
|
-
'sunt',
|
|
58
|
-
'culpa',
|
|
59
|
-
'qui',
|
|
60
|
-
'officia',
|
|
61
|
-
'deserunt',
|
|
62
|
-
'mollit',
|
|
63
|
-
'anim',
|
|
64
|
-
'id',
|
|
65
|
-
'est',
|
|
66
|
-
'laborum',
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
const FR_UNITS = [
|
|
70
|
-
'zéro',
|
|
71
|
-
'un',
|
|
72
|
-
'deux',
|
|
73
|
-
'trois',
|
|
74
|
-
'quatre',
|
|
75
|
-
'cinq',
|
|
76
|
-
'six',
|
|
77
|
-
'sept',
|
|
78
|
-
'huit',
|
|
79
|
-
'neuf',
|
|
80
|
-
'dix',
|
|
81
|
-
'onze',
|
|
82
|
-
'douze',
|
|
83
|
-
'treize',
|
|
84
|
-
'quatorze',
|
|
85
|
-
'quinze',
|
|
86
|
-
'seize',
|
|
87
|
-
'dix-sept',
|
|
88
|
-
'dix-huit',
|
|
89
|
-
'dix-neuf',
|
|
90
|
-
];
|
|
91
|
-
const FR_TENS = [
|
|
92
|
-
'',
|
|
93
|
-
'',
|
|
94
|
-
'vingt',
|
|
95
|
-
'trente',
|
|
96
|
-
'quarante',
|
|
97
|
-
'cinquante',
|
|
98
|
-
'soixante',
|
|
99
|
-
'soixante',
|
|
100
|
-
'quatre-vingt',
|
|
101
|
-
'quatre-vingt',
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
const EN_UNITS = [
|
|
105
|
-
'zero',
|
|
106
|
-
'one',
|
|
107
|
-
'two',
|
|
108
|
-
'three',
|
|
109
|
-
'four',
|
|
110
|
-
'five',
|
|
111
|
-
'six',
|
|
112
|
-
'seven',
|
|
113
|
-
'eight',
|
|
114
|
-
'nine',
|
|
115
|
-
'ten',
|
|
116
|
-
'eleven',
|
|
117
|
-
'twelve',
|
|
118
|
-
'thirteen',
|
|
119
|
-
'fourteen',
|
|
120
|
-
'fifteen',
|
|
121
|
-
'sixteen',
|
|
122
|
-
'seventeen',
|
|
123
|
-
'eighteen',
|
|
124
|
-
'nineteen',
|
|
125
|
-
];
|
|
126
|
-
const EN_TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
|
127
|
-
|
|
128
|
-
function checkText(value: string): void {
|
|
129
|
-
if (!value) throw new Error('A value is required');
|
|
130
|
-
if (typeof value !== 'string') throw new Error('Value must be a string');
|
|
131
|
-
if (value.length > MAX_STRING_LENGTH)
|
|
132
|
-
throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface TextStats {
|
|
136
|
-
characters: number;
|
|
137
|
-
charactersNoSpaces: number;
|
|
138
|
-
words: number;
|
|
139
|
-
sentences: number;
|
|
140
|
-
paragraphs: number;
|
|
141
|
-
readingTime: string;
|
|
142
|
-
mostFrequentChar: string;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Analyzes a text string and returns character, word, sentence, and reading time statistics.
|
|
147
|
-
*
|
|
148
|
-
* @param value - The text to analyze
|
|
149
|
-
* @returns Object containing character counts, word/sentence/paragraph counts, reading time, and most frequent character
|
|
150
|
-
* @throws Error if value is missing, not a string, or too long
|
|
151
|
-
*/
|
|
152
|
-
export function stats(value: string): TextStats {
|
|
153
|
-
checkText(value);
|
|
154
|
-
|
|
155
|
-
const characters = value.length;
|
|
156
|
-
const charactersNoSpaces = value.replace(/\s/g, '').length;
|
|
157
|
-
const words = value.trim().split(/\s+/).filter(Boolean).length;
|
|
158
|
-
const sentences = value.split(/[.!?]+/).filter((s) => s.trim().length > 0).length;
|
|
159
|
-
const paragraphs = value.split(/\n\s*\n/).filter((p) => p.trim().length > 0).length || 1;
|
|
160
|
-
|
|
161
|
-
const minutes = words / 200;
|
|
162
|
-
const readingTime = minutes < 1 ? `${Math.ceil(minutes * 60)}s` : `${Math.round(minutes)}min`;
|
|
163
|
-
|
|
164
|
-
const counts = new Map<string, number>();
|
|
165
|
-
for (const c of value.replace(/\s/g, '').toLowerCase()) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
166
|
-
let mostFrequentChar = '';
|
|
167
|
-
let maxCount = 0;
|
|
168
|
-
for (const [char, count] of counts) {
|
|
169
|
-
if (count > maxCount) {
|
|
170
|
-
mostFrequentChar = char;
|
|
171
|
-
maxCount = count;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, mostFrequentChar };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Converts a string to a URL-friendly slug.
|
|
180
|
-
*
|
|
181
|
-
* @param value - The string to slugify
|
|
182
|
-
* @returns Lowercase hyphenated slug with diacritics and special characters removed
|
|
183
|
-
* @throws Error if value is missing, not a string, or too long
|
|
184
|
-
*/
|
|
185
|
-
export function slug(value: string): string {
|
|
186
|
-
checkText(value);
|
|
187
|
-
return value
|
|
188
|
-
.normalize('NFD')
|
|
189
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
190
|
-
.toLowerCase()
|
|
191
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
192
|
-
.trim()
|
|
193
|
-
.replace(/[\s-]+/g, '-');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Generates Lorem Ipsum placeholder text.
|
|
198
|
-
*
|
|
199
|
-
* @param type - Content type: "words", "sentences", or "paragraphs"
|
|
200
|
-
* @param count - Number of words, sentences, or paragraphs to generate
|
|
201
|
-
* @returns Generated Lorem Ipsum text
|
|
202
|
-
* @throws Error if count is out of range or type is not one of the accepted values
|
|
203
|
-
*/
|
|
204
|
-
export function lorem(type: string, count: string): string {
|
|
205
|
-
const n = Number(count) || 5;
|
|
206
|
-
if (n < 1 || n > 500) throw new Error('Count must be between 1 and 500');
|
|
207
|
-
|
|
208
|
-
const randomWord = (): string => LOREM_WORDS[Math.floor(Math.random() * LOREM_WORDS.length)]!;
|
|
209
|
-
const randomSentence = (): string => {
|
|
210
|
-
const length = 5 + Math.floor(Math.random() * 10);
|
|
211
|
-
const words = Array.from({ length }, randomWord);
|
|
212
|
-
words[0] = words[0]!.charAt(0).toUpperCase() + words[0]!.slice(1);
|
|
213
|
-
return words.join(' ') + '.';
|
|
214
|
-
};
|
|
215
|
-
const randomParagraph = (): string => {
|
|
216
|
-
const length = 3 + Math.floor(Math.random() * 5);
|
|
217
|
-
return Array.from({ length }, randomSentence).join(' ');
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
switch (type) {
|
|
221
|
-
case 'words':
|
|
222
|
-
return Array.from({ length: n }, randomWord).join(' ');
|
|
223
|
-
case 'sentences':
|
|
224
|
-
return Array.from({ length: n }, randomSentence).join(' ');
|
|
225
|
-
case 'paragraphs':
|
|
226
|
-
return Array.from({ length: n }, randomParagraph).join('\n\n');
|
|
227
|
-
default:
|
|
228
|
-
throw new Error('Type must be one of: words, sentences, paragraphs');
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function frBelowHundred(n: number): string {
|
|
233
|
-
if (n < 20) return FR_UNITS[n]!;
|
|
234
|
-
const t = Math.floor(n / 10);
|
|
235
|
-
const u = n % 10;
|
|
236
|
-
if (t === 7 || t === 9) {
|
|
237
|
-
const base = FR_TENS[t]!;
|
|
238
|
-
const sub = 10 + u;
|
|
239
|
-
return base + (base.endsWith('-') ? '' : '-') + FR_UNITS[sub];
|
|
240
|
-
}
|
|
241
|
-
if (t === 8 && u === 0) return 'quatre-vingts';
|
|
242
|
-
if (u === 0) return FR_TENS[t]!;
|
|
243
|
-
if (u === 1 && t < 8) return FR_TENS[t] + ' et un';
|
|
244
|
-
return FR_TENS[t] + '-' + FR_UNITS[u];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function frBelowThousand(n: number): string {
|
|
248
|
-
if (n < 100) return frBelowHundred(n);
|
|
249
|
-
const h = Math.floor(n / 100);
|
|
250
|
-
const r = n % 100;
|
|
251
|
-
let result = '';
|
|
252
|
-
if (h === 1) result = 'cent';
|
|
253
|
-
else result = FR_UNITS[h] + ' cent' + (r === 0 ? 's' : '');
|
|
254
|
-
if (r > 0) result += ' ' + frBelowHundred(r);
|
|
255
|
-
return result;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function numberToFrench(n: number): string {
|
|
259
|
-
if (n === 0) return 'zéro';
|
|
260
|
-
if (n < 0) return 'moins ' + numberToFrench(-n);
|
|
261
|
-
if (n < 1000) return frBelowThousand(n);
|
|
262
|
-
if (n < 1_000_000) {
|
|
263
|
-
const t = Math.floor(n / 1000);
|
|
264
|
-
const r = n % 1000;
|
|
265
|
-
const thousands = t === 1 ? 'mille' : frBelowThousand(t) + ' mille';
|
|
266
|
-
return r === 0 ? thousands : thousands + ' ' + frBelowThousand(r);
|
|
267
|
-
}
|
|
268
|
-
if (n < 1_000_000_000) {
|
|
269
|
-
const m = Math.floor(n / 1_000_000);
|
|
270
|
-
const r = n % 1_000_000;
|
|
271
|
-
const millions = m === 1 ? 'un million' : frBelowThousand(m) + ' millions';
|
|
272
|
-
return r === 0 ? millions : millions + ' ' + numberToFrench(r);
|
|
273
|
-
}
|
|
274
|
-
throw new Error('Number must be less than 1 billion');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function enBelowHundred(n: number): string {
|
|
278
|
-
if (n < 20) return EN_UNITS[n]!;
|
|
279
|
-
const t = Math.floor(n / 10);
|
|
280
|
-
const u = n % 10;
|
|
281
|
-
return u === 0 ? EN_TENS[t]! : EN_TENS[t] + '-' + EN_UNITS[u];
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function enBelowThousand(n: number): string {
|
|
285
|
-
if (n < 100) return enBelowHundred(n);
|
|
286
|
-
const h = Math.floor(n / 100);
|
|
287
|
-
const r = n % 100;
|
|
288
|
-
return r === 0 ? EN_UNITS[h] + ' hundred' : EN_UNITS[h] + ' hundred ' + enBelowHundred(r);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function numberToEnglish(n: number): string {
|
|
292
|
-
if (n === 0) return 'zero';
|
|
293
|
-
if (n < 0) return 'minus ' + numberToEnglish(-n);
|
|
294
|
-
if (n < 1000) return enBelowThousand(n);
|
|
295
|
-
if (n < 1_000_000) {
|
|
296
|
-
const t = Math.floor(n / 1000);
|
|
297
|
-
const r = n % 1000;
|
|
298
|
-
return r === 0 ? enBelowThousand(t) + ' thousand' : enBelowThousand(t) + ' thousand ' + enBelowThousand(r);
|
|
299
|
-
}
|
|
300
|
-
if (n < 1_000_000_000) {
|
|
301
|
-
const m = Math.floor(n / 1_000_000);
|
|
302
|
-
const r = n % 1_000_000;
|
|
303
|
-
return r === 0 ? enBelowThousand(m) + ' million' : enBelowThousand(m) + ' million ' + numberToEnglish(r);
|
|
304
|
-
}
|
|
305
|
-
throw new Error('Number must be less than 1 billion');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Converts an integer to its written-out word form in French or English.
|
|
310
|
-
*
|
|
311
|
-
* @param value - The integer to convert (must be less than 1 billion)
|
|
312
|
-
* @param lang - Language: "fr" for French or "en" for English
|
|
313
|
-
* @returns The number written out in words
|
|
314
|
-
* @throws Error if value is not an integer, exceeds the maximum, or lang is not supported
|
|
315
|
-
*/
|
|
316
|
-
export function number(value: string, lang: string): string {
|
|
317
|
-
const n = Number(value);
|
|
318
|
-
if (isNaN(n)) throw new Error('Value must be a number');
|
|
319
|
-
if (!Number.isInteger(n)) throw new Error('Value must be an integer');
|
|
320
|
-
if (Math.abs(n) >= 1_000_000_000) throw new Error('Number must be less than 1 billion');
|
|
321
|
-
|
|
322
|
-
switch (lang) {
|
|
323
|
-
case 'fr':
|
|
324
|
-
return numberToFrench(n);
|
|
325
|
-
case 'en':
|
|
326
|
-
return numberToEnglish(n);
|
|
327
|
-
default:
|
|
328
|
-
throw new Error('Lang must be one of: fr, en');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'crypto';
|
|
2
|
-
|
|
3
|
-
import { GAME_CLEANUP_TTL, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, SESSION_TTL } from '../../constants.js';
|
|
4
|
-
import type { TicTacToeGame, TicTacToeMove, TicTacToeStorage } from '../../types/storage.js';
|
|
5
|
-
|
|
6
|
-
interface TicTacToeParams {
|
|
7
|
-
username?: string;
|
|
8
|
-
move?: string;
|
|
9
|
-
session?: string;
|
|
10
|
-
game?: string;
|
|
11
|
-
private?: boolean;
|
|
12
|
-
storage: TicTacToeStorage;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface GameResult {
|
|
16
|
-
winner?: string | null;
|
|
17
|
-
loser?: string | null;
|
|
18
|
-
tie?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Handles Tic-Tac-Toe game actions including playing a move, fetching game state, listing games, and forfeiting.
|
|
23
|
-
*
|
|
24
|
-
* @param action - The action to perform: "play", "fetch", "list", or "forfeit"
|
|
25
|
-
* @param params - Game parameters including username, move, session, game ID, and shared storage
|
|
26
|
-
* @returns Game state or action result depending on the action
|
|
27
|
-
* @throws Error if a required parameter is missing, the action is invalid, or a rate limit is exceeded
|
|
28
|
-
*/
|
|
29
|
-
export default function tic_tac_toe(action: string, params: TicTacToeParams): Record<string, unknown> {
|
|
30
|
-
const storage = params.storage;
|
|
31
|
-
|
|
32
|
-
storage.games ??= {};
|
|
33
|
-
storage.sessions ??= {};
|
|
34
|
-
storage.rateLimits ??= {};
|
|
35
|
-
|
|
36
|
-
const { games, sessions, rateLimits } = storage;
|
|
37
|
-
|
|
38
|
-
if (action === 'list') return listGames(games);
|
|
39
|
-
|
|
40
|
-
if (!params.username) throw new Error('Please provide a username');
|
|
41
|
-
|
|
42
|
-
const u = params.username.toLowerCase();
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
|
|
45
|
-
rateLimits[u] = (rateLimits[u] ?? []).filter((ts) => now - ts < RATE_LIMIT_WINDOW);
|
|
46
|
-
if (rateLimits[u]!.length > RATE_LIMIT_MAX) {
|
|
47
|
-
const remainingTime = Math.ceil((rateLimits[u]![0]! + RATE_LIMIT_WINDOW - now) / 1000);
|
|
48
|
-
throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
|
|
49
|
-
}
|
|
50
|
-
rateLimits[u]!.push(now);
|
|
51
|
-
|
|
52
|
-
if (action === 'play') {
|
|
53
|
-
return playMove(params, games, sessions, u, now);
|
|
54
|
-
} else if (action === 'fetch') {
|
|
55
|
-
return fetchGame(params, games, u);
|
|
56
|
-
} else if (action === 'forfeit') {
|
|
57
|
-
return forfeitGame(params, games, sessions);
|
|
58
|
-
} else {
|
|
59
|
-
throw new Error('Invalid action. Use "play", "fetch", "list", or "forfeit"');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function forfeitGame(
|
|
64
|
-
params: TicTacToeParams,
|
|
65
|
-
games: Record<string, TicTacToeGame>,
|
|
66
|
-
sessions: Record<string, { user: string; last: number }>,
|
|
67
|
-
): Record<string, unknown> {
|
|
68
|
-
const { game, session } = params;
|
|
69
|
-
|
|
70
|
-
if (!game) throw new Error('Please provide a valid game ID');
|
|
71
|
-
if (!session) throw new Error('Please provide a valid session ID');
|
|
72
|
-
if (!games[game]) throw new Error('Game not found');
|
|
73
|
-
|
|
74
|
-
const u = params.username!.toLowerCase();
|
|
75
|
-
if (sessions[u] && sessions[u].user !== session) {
|
|
76
|
-
throw new Error('Session ID mismatch');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const players = games[game]!.players;
|
|
80
|
-
if (!players.includes(u)) throw new Error('You are not a player in this game');
|
|
81
|
-
|
|
82
|
-
const winner = players.find((p) => p !== u) ?? null;
|
|
83
|
-
delete games[game];
|
|
84
|
-
delete sessions[u];
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
message: `${params.username} forfeited the game.${winner ? ` ${winner} wins.` : ''}`,
|
|
88
|
-
winner,
|
|
89
|
-
loser: params.username,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function playMove(
|
|
94
|
-
params: TicTacToeParams,
|
|
95
|
-
games: Record<string, TicTacToeGame>,
|
|
96
|
-
sessions: Record<string, { user: string; last: number }>,
|
|
97
|
-
u: string,
|
|
98
|
-
now: number,
|
|
99
|
-
): Record<string, unknown> {
|
|
100
|
-
const { move, session, game } = params;
|
|
101
|
-
const validMoves = ['1-1', '1-2', '1-3', '2-1', '2-2', '2-3', '3-1', '3-2', '3-3'];
|
|
102
|
-
|
|
103
|
-
if (!move) throw new Error('Please provide a valid move');
|
|
104
|
-
if (!session) throw new Error('Please provide a valid session ID');
|
|
105
|
-
if (!game) throw new Error('Please provide a valid game ID');
|
|
106
|
-
if (!validMoves.includes(move)) throw new Error('Invalid move. Please provide a valid move (e.g., 1-1, 2-2, 3-3).');
|
|
107
|
-
|
|
108
|
-
if (sessions[u] && sessions[u].user !== session) {
|
|
109
|
-
throw new Error('Session ID mismatch');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
games[game] = games[game] ?? {
|
|
113
|
-
moves: [],
|
|
114
|
-
players: [],
|
|
115
|
-
private: params.private || false,
|
|
116
|
-
creation: Date.now(),
|
|
117
|
-
};
|
|
118
|
-
const moves = games[game]!.moves;
|
|
119
|
-
|
|
120
|
-
const players = [...new Set(moves.map((play) => play.username))];
|
|
121
|
-
if (players.length >= 2 && !players.includes(params.username!)) {
|
|
122
|
-
throw new Error('Game is full, you can only watch.');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (moves.length > 0 && moves[moves.length - 1]!.username === params.username) {
|
|
126
|
-
throw new Error('Please wait for the other player to make a move.');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (moves.some((play) => play.move === move)) {
|
|
130
|
-
throw new Error('Move already made. Please choose a different move.');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const play: TicTacToeMove = { username: params.username!, move, session };
|
|
134
|
-
moves.push(play);
|
|
135
|
-
|
|
136
|
-
const result = checkGame(moves);
|
|
137
|
-
if (result.winner || result.tie) {
|
|
138
|
-
setTimeout(() => delete games[game!], GAME_CLEANUP_TTL);
|
|
139
|
-
return {
|
|
140
|
-
message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : "It's a tie."}`,
|
|
141
|
-
...result,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
setTimeout(() => delete games[game!], SESSION_TTL);
|
|
146
|
-
|
|
147
|
-
sessions[u] = sessions[u] ?? { user: session, last: now };
|
|
148
|
-
sessions[u]!.last = now;
|
|
149
|
-
setTimeout(() => {
|
|
150
|
-
if (sessions[u] && now - sessions[u].last >= SESSION_TTL) delete sessions[u];
|
|
151
|
-
}, SESSION_TTL);
|
|
152
|
-
|
|
153
|
-
return { message: 'Move sent successfully' };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function fetchGame(params: TicTacToeParams, games: Record<string, TicTacToeGame>, u: string): Record<string, unknown> {
|
|
157
|
-
const id = params.game || generateGameId();
|
|
158
|
-
if (!games[id]) {
|
|
159
|
-
games[id] = {
|
|
160
|
-
moves: [],
|
|
161
|
-
players: [],
|
|
162
|
-
private: params.private || false,
|
|
163
|
-
creation: Date.now(),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
if (!games[id]!.players.includes(u)) {
|
|
167
|
-
games[id]!.players.push(u);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const moves = games[id]!.moves;
|
|
171
|
-
const players = games[id]!.players;
|
|
172
|
-
const privateGame = games[id]!.private;
|
|
173
|
-
const lastPlayer = moves.length ? moves[moves.length - 1]!.username : null;
|
|
174
|
-
const turn = players.find((p) => p !== lastPlayer) || players[0];
|
|
175
|
-
const status = players.length >= 2 ? 'ready' : 'waiting';
|
|
176
|
-
const result = moves.length ? checkGame(moves) : {};
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
id,
|
|
180
|
-
moves,
|
|
181
|
-
players,
|
|
182
|
-
turn,
|
|
183
|
-
status,
|
|
184
|
-
private: privateGame,
|
|
185
|
-
...result,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function listGames(games: Record<string, TicTacToeGame>): Record<string, unknown> {
|
|
190
|
-
const publicGames: Record<string, unknown>[] = [];
|
|
191
|
-
const now = Date.now();
|
|
192
|
-
|
|
193
|
-
for (const [gameId, game] of Object.entries(games)) {
|
|
194
|
-
if (!game.private) {
|
|
195
|
-
const result = game.moves.length ? checkGame(game.moves) : {};
|
|
196
|
-
const isFinished = result.winner || result.tie;
|
|
197
|
-
|
|
198
|
-
if (!isFinished) {
|
|
199
|
-
const lastPlayer = game.moves.length ? game.moves[game.moves.length - 1]!.username : null;
|
|
200
|
-
const turn = game.players.find((p) => p !== lastPlayer) || game.players[0];
|
|
201
|
-
const status = game.players.length >= 2 ? 'ready' : 'waiting';
|
|
202
|
-
|
|
203
|
-
publicGames.push({
|
|
204
|
-
id: gameId,
|
|
205
|
-
players: game.players,
|
|
206
|
-
playersCount: game.players.length,
|
|
207
|
-
moves: game.moves.length,
|
|
208
|
-
turn,
|
|
209
|
-
status,
|
|
210
|
-
creation: game.creation || now,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
publicGames.sort((a, b) => (b.creation as number) - (a.creation as number));
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
message: 'Public games available',
|
|
220
|
-
count: publicGames.length,
|
|
221
|
-
games: publicGames,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function generateGameId(): string {
|
|
226
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
227
|
-
return Array.from(randomBytes(5))
|
|
228
|
-
.map((b) => chars[b % chars.length])
|
|
229
|
-
.join('');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function checkGame(moves: TicTacToeMove[]): GameResult {
|
|
233
|
-
const board: (string | null)[][] = Array(3)
|
|
234
|
-
.fill(null)
|
|
235
|
-
.map(() => Array(3).fill(null) as (string | null)[]);
|
|
236
|
-
const playerSymbols: Record<string, string> = {};
|
|
237
|
-
const playersOrder: string[] = [];
|
|
238
|
-
|
|
239
|
-
moves.forEach(({ username, move }) => {
|
|
240
|
-
if (!playerSymbols[username]) {
|
|
241
|
-
playersOrder.push(username);
|
|
242
|
-
playerSymbols[username] = playersOrder.length === 1 ? 'X' : 'O';
|
|
243
|
-
}
|
|
244
|
-
const [row, col] = move.split('-').map(Number) as [number, number];
|
|
245
|
-
board[row - 1]![col - 1] = playerSymbols[username]!;
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const checkWinner = (symbol: string): boolean => {
|
|
249
|
-
for (let i = 0; i < 3; i++) {
|
|
250
|
-
if (board[i]![0] === symbol && board[i]![1] === symbol && board[i]![2] === symbol) return true;
|
|
251
|
-
if (board[0]![i] === symbol && board[1]![i] === symbol && board[2]![i] === symbol) return true;
|
|
252
|
-
}
|
|
253
|
-
if (board[0]![0] === symbol && board[1]![1] === symbol && board[2]![2] === symbol) return true;
|
|
254
|
-
if (board[0]![2] === symbol && board[1]![1] === symbol && board[2]![0] === symbol) return true;
|
|
255
|
-
return false;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const winner = Object.keys(playerSymbols).find((player) => checkWinner(playerSymbols[player]!)) ?? null;
|
|
259
|
-
const isTie = !winner && moves.length === 9;
|
|
260
|
-
const loser =
|
|
261
|
-
winner && playersOrder.length === 2 ? (playersOrder.find((player) => player !== winner) ?? null) : null;
|
|
262
|
-
|
|
263
|
-
return { winner, loser, tie: isTie };
|
|
264
|
-
}
|
package/src/modules/v4/time.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
const validFormats = [
|
|
2
|
-
'iso',
|
|
3
|
-
'utc',
|
|
4
|
-
'timestamp',
|
|
5
|
-
'locale',
|
|
6
|
-
'date',
|
|
7
|
-
'time',
|
|
8
|
-
'year',
|
|
9
|
-
'month',
|
|
10
|
-
'day',
|
|
11
|
-
'hour',
|
|
12
|
-
'minute',
|
|
13
|
-
'second',
|
|
14
|
-
'ms',
|
|
15
|
-
'dayOfWeek',
|
|
16
|
-
'dayOfYear',
|
|
17
|
-
'weekNumber',
|
|
18
|
-
'timezone',
|
|
19
|
-
'timezoneOffset',
|
|
20
|
-
] as const;
|
|
21
|
-
|
|
22
|
-
const validTimezones = ['UTC', 'America/New_York', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'] as const;
|
|
23
|
-
|
|
24
|
-
type TimeFormat = (typeof validFormats)[number];
|
|
25
|
-
type Timezone = (typeof validTimezones)[number];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Returns the current or a random date/time in various formats and timezones.
|
|
29
|
-
*
|
|
30
|
-
* @param type - "live" for the current time or "random" for a random date within a range
|
|
31
|
-
* @param start - Optional start date for random mode (YYYY-MM-DD)
|
|
32
|
-
* @param end - Optional end date for random mode (YYYY-MM-DD)
|
|
33
|
-
* @param format - Optional specific format to return (e.g. "iso", "timestamp", "year")
|
|
34
|
-
* @param timezone - Optional timezone (e.g. "UTC", "Europe/Paris")
|
|
35
|
-
* @returns Object containing all time formats, or a single format if specified
|
|
36
|
-
* @throws Error if type, format, or timezone is invalid
|
|
37
|
-
*/
|
|
38
|
-
export default function time(
|
|
39
|
-
type: string = 'live',
|
|
40
|
-
start?: string,
|
|
41
|
-
end?: string,
|
|
42
|
-
format?: string,
|
|
43
|
-
timezone?: string,
|
|
44
|
-
): Record<string, unknown> {
|
|
45
|
-
if (type !== 'live' && type !== 'random') {
|
|
46
|
-
throw new Error('Please provide a valid type (live or random)');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (start && !Date.parse(start)) {
|
|
50
|
-
throw new Error('Please provide a valid start date (YYYY-MM-DD)');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (end && !Date.parse(end)) {
|
|
54
|
-
throw new Error('Please provide a valid end date (YYYY-MM-DD)');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (format && !validFormats.includes(format as TimeFormat)) {
|
|
58
|
-
throw new Error(`Please provide a valid format. Options: ${validFormats.join(', ')}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (timezone && !validTimezones.includes(timezone as Timezone)) {
|
|
62
|
-
throw new Error(`Please provide a valid timezone. Options: ${validTimezones.join(', ')}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const getTimeFormats = (date: Date, tz: string): Record<string, unknown> => {
|
|
66
|
-
return {
|
|
67
|
-
iso: date.toISOString(),
|
|
68
|
-
utc: date.toUTCString(),
|
|
69
|
-
timestamp: date.getTime(),
|
|
70
|
-
locale: date.toLocaleString('en-US', { timeZone: tz, timeZoneName: 'long' }),
|
|
71
|
-
date: date.toLocaleDateString('en-US', { timeZone: tz }),
|
|
72
|
-
time: date.toLocaleTimeString('en-US', { timeZone: tz }),
|
|
73
|
-
year: date.getFullYear(),
|
|
74
|
-
month: date.getMonth() + 1,
|
|
75
|
-
day: date.getDate(),
|
|
76
|
-
hour: date.getHours(),
|
|
77
|
-
minute: date.getMinutes(),
|
|
78
|
-
second: date.getSeconds(),
|
|
79
|
-
ms: date.getMilliseconds(),
|
|
80
|
-
dayOfWeek: date.getDay(),
|
|
81
|
-
dayOfYear: Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000),
|
|
82
|
-
weekNumber: Math.ceil(((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 + 1) / 7),
|
|
83
|
-
timezone: tz,
|
|
84
|
-
timezoneOffset: date.getTimezoneOffset(),
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (type === 'random') {
|
|
89
|
-
const startDate = start ? new Date(start).getTime() : new Date('1900-01-01').getTime();
|
|
90
|
-
const endDate = end ? new Date(end).getTime() : new Date('2100-12-31').getTime();
|
|
91
|
-
const randomDate = new Date(startDate + Math.random() * (endDate - startDate));
|
|
92
|
-
const tz = timezone || validTimezones[Math.floor(Math.random() * validTimezones.length)]!;
|
|
93
|
-
const formats = getTimeFormats(randomDate, tz);
|
|
94
|
-
|
|
95
|
-
return format ? { date: formats[format] } : formats;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const now = new Date();
|
|
99
|
-
const tz = timezone || 'UTC';
|
|
100
|
-
const formats = getTimeFormats(now, tz);
|
|
101
|
-
|
|
102
|
-
return format ? { date: formats[format] } : formats;
|
|
103
|
-
}
|