Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import validator from 'validator';
2import { ChatInputCommandInteraction } from 'discord.js';
3import { UNALLOWED_WORDS } from '../constants/unallowedWords';
4
5interface ValidationResult {
6 isValid: boolean;
7 message?: string;
8}
9
10function validateCommandOptions(
11 interaction: ChatInputCommandInteraction,
12 requiredOptions: string[] = [],
13): ValidationResult {
14 for (const option of requiredOptions) {
15 const value = interaction.options.getString(option);
16 if (!value || value.trim() === '') {
17 return {
18 isValid: false,
19 message: `Missing required option: ${option}`,
20 };
21 }
22 }
23 return { isValid: true };
24}
25
26function sanitizeInput(input?: string | null): string {
27 if (!input) return '';
28
29 let sanitized = input
30 .replace(/[<>"'&]/g, '')
31 .replace(/javascript:/gi, '')
32 .replace(/data:/gi, '')
33 .replace(/vbscript:/gi, '')
34 .trim();
35
36 sanitized = sanitized
37 .split('')
38 .filter((char) => {
39 const code = char.charCodeAt(0);
40 return code >= 32 && code !== 127 && (code < 128 || code > 159);
41 })
42 .join('');
43
44 return sanitized.substring(0, 1000);
45}
46
47function isValidUrl(url: string): boolean {
48 if (!url || typeof url !== 'string') return false;
49
50 try {
51 const parsedUrl = new URL(url);
52
53 if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
54 return false;
55 }
56
57 const hostname = parsedUrl.hostname.toLowerCase();
58 if (
59 hostname === 'localhost' ||
60 hostname === '127.0.0.1' ||
61 hostname.startsWith('192.168.') ||
62 hostname.startsWith('10.') ||
63 hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
64 ) {
65 return false;
66 }
67
68 return true;
69 } catch {
70 return false;
71 }
72}
73
74function validateTimeString(timeStr: string): boolean {
75 if (typeof timeStr !== 'string' || timeStr.length > 10) return false;
76
77 const timeRegex = /^(\d{1,3}h)?(\d{1,4}m)?$|^(\d{1,4}m)?(\d{1,3}h)?$/i;
78
79 if (!timeRegex.test(timeStr)) return false;
80
81 const hoursMatch = timeStr.match(/(\d+)h/i);
82 const minsMatch = timeStr.match(/(\d+)m/i);
83
84 const hours = hoursMatch ? parseInt(hoursMatch[1], 10) : 0;
85 const minutes = minsMatch ? parseInt(minsMatch[1], 10) : 0;
86
87 return hours <= 168 && minutes <= 10080;
88}
89
90function parseTimeString(timeStr: string): number | null {
91 if (!validateTimeString(timeStr)) return null;
92
93 let minutes = 0;
94 const hoursMatch = timeStr.match(/(\d+)h/i);
95 const minsMatch = timeStr.match(/(\d+)m/i);
96
97 if (hoursMatch) minutes += parseInt(hoursMatch[1], 10) * 60;
98 if (minsMatch) minutes += parseInt(minsMatch[1], 10);
99
100 return minutes > 0 ? minutes : null;
101}
102
103function formatTimeString(minutes: number): string {
104 if (!minutes || minutes < 0) return '0m';
105
106 const hours = Math.floor(minutes / 60);
107 const mins = minutes % 60;
108
109 const parts: string[] = [];
110 if (hours > 0) parts.push(`${hours}h`);
111 if (mins > 0 || hours === 0) parts.push(`${mins}m`);
112
113 return parts.join(' ');
114}
115
116function isValidDomain(domain: string): boolean {
117 if (typeof domain !== 'string') return false;
118 return validator.isFQDN(domain, { require_tld: true });
119}
120
121function _normalizeText(text: string): string {
122 if (!text) return '';
123
124 return text
125 .toLowerCase()
126 .replace(/([a-z])\1{2,}/g, '$1')
127 .replace(/[^\w\s]/g, '')
128 .replace(/@/g, 'a')
129 .replace(/4/g, 'a')
130 .replace(/3/g, 'e')
131 .replace(/1|!/g, 'i')
132 .replace(/0/g, 'o')
133 .replace(/[5$]/g, 's')
134 .replace(/7/g, 't')
135 .trim();
136}
137
138export function getUnallowedWordCategory(text: string): string | null {
139 if (!text || typeof text !== 'string') return null;
140
141 const words = text
142 .toLowerCase()
143 .split(/[\s"'.,?!;:]+/)
144 .map((word) => word.replace(/[^\w\s]/g, ''))
145 .filter((word) => word.length > 0);
146
147 for (const word of words) {
148 if (word.length <= 2) continue;
149
150 for (const [category, wordList] of Object.entries(UNALLOWED_WORDS)) {
151 if ((wordList as string[]).some((badWord) => word.toLowerCase() === badWord.toLowerCase())) {
152 return category;
153 }
154 }
155 }
156
157 return null;
158}
159
160export {
161 validateCommandOptions,
162 sanitizeInput,
163 validateTimeString,
164 parseTimeString,
165 formatTimeString,
166 isValidUrl,
167 isValidDomain,
168};