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 normalizeInput(text: string): string {
122 let normalized = text.toLowerCase();
123 normalized = normalized.replace(/([a-z])\1{2,}/g, '$1');
124 normalized = normalized
125 .replace(/[@4]/g, 'a')
126 .replace(/[3]/g, 'e')
127 .replace(/[1!]/g, 'i')
128 .replace(/[0]/g, 'o')
129 .replace(/[5$]/g, 's')
130 .replace(/[7]/g, 't');
131 return normalized;
132}
133
134export function getUnallowedWordCategory(text: string): string | null {
135 const normalized = normalizeInput(text);
136 for (const [category, words] of Object.entries(UNALLOWED_WORDS)) {
137 for (const word of words as string[]) {
138 if (category === 'slurs') {
139 if (normalized.includes(word)) {
140 return category;
141 }
142 } else {
143 const pattern = new RegExp(`(?:^|\\W)${word}[a-z]{0,2}(?:\\W|$)`, 'i');
144 if (pattern.test(normalized)) {
145 return category;
146 }
147 }
148 }
149 }
150 return null;
151}
152
153export {
154 validateCommandOptions,
155 sanitizeInput,
156 validateTimeString,
157 parseTimeString,
158 formatTimeString,
159 isValidUrl,
160 isValidDomain,
161};