Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: final bug fixes

+56 -14
+4 -5
src/events/messageCreate.ts
··· 173 173 174 174 if (config.usingDefaultKey) { 175 175 const exemptUserId = process.env.AI_EXEMPT_USER_ID; 176 - if ( 177 - conversationKey !== `dm:${exemptUserId}` && 178 - !conversationKey.startsWith(`guild:${exemptUserId}`) 179 - ) { 180 - const allowed = await incrementAndCheckDailyLimit(conversationKey, 10); 176 + const actorId = message.author.id; 177 + 178 + if (actorId !== exemptUserId) { 179 + const allowed = await incrementAndCheckDailyLimit(actorId, 10); 181 180 if (!allowed) { 182 181 await message.reply( 183 182 "❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.",
+52 -9
src/services/social/fetchers/UnifiedFetcher.ts
··· 77 77 } 78 78 } 79 79 80 - isValidAccount(account: string): boolean { 80 + isValidAccount(account: string | null | undefined): boolean { 81 81 if (!account) return false; 82 - const handle = this.normalizeHandle(account); 83 - return /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(handle); 82 + 83 + if (account.startsWith('did:')) { 84 + return /^did:[a-z0-9]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(account); 85 + } 86 + 87 + const parts = account.split('@').filter(Boolean); 88 + if (parts.length < 2) return false; 89 + 90 + const handle = parts[0]; 91 + const domain = parts.slice(1).join('@'); 92 + 93 + const isValidHandle = /^[a-zA-Z0-9-]+$/.test(handle); 94 + const isValidDomain = /^[a-zA-Z0-9.-]+$/.test(domain); 95 + 96 + return isValidHandle && isValidDomain; 84 97 } 85 98 86 99 private normalizeHandle(handle: string): string { 100 + if (handle.startsWith('did:')) { 101 + return handle; 102 + } 103 + 87 104 handle = handle.startsWith('@') ? handle.slice(1) : handle; 88 105 if (!handle.includes('.')) { 89 106 return `${handle}.bsky.social`.toLowerCase(); ··· 198 215 export class FediverseFetcher implements SocialMediaFetcher { 199 216 platform: SocialPlatform = 'fediverse'; 200 217 201 - async fetchLatestPost(account: string): Promise<SocialMediaPost | null> { 202 - const [username, domain] = this.parseAccount(account); 218 + private validateFediverseAccount(username: string, domain: string | null): void { 203 219 if (!domain) { 204 220 throw new Error('Fediverse account must include a domain (e.g., user@instance.social)'); 205 221 } 206 222 207 - const apiUrl = `https://${domain}/api/v1/accounts/lookup?acct=${username}@${domain}`; 223 + if (/^https?:\/\//i.test(username) || /^https?:\/\//i.test(domain)) { 224 + throw new Error('URL schemes (http/https) are not allowed in Fediverse accounts'); 225 + } 226 + 227 + if (/[?#]/.test(username) || /[?#]/.test(domain)) { 228 + throw new Error('URL paths and query parameters are not allowed in Fediverse accounts'); 229 + } 230 + 231 + const domainStr = domain as string; 232 + if (!/^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(domainStr)) { 233 + throw new Error('Invalid domain format in Fediverse account'); 234 + } 235 + 236 + if (!/^[a-z0-9_.-]+$/i.test(username)) { 237 + throw new Error('Invalid username format in Fediverse account'); 238 + } 239 + } 240 + 241 + async fetchLatestPost(account: string): Promise<SocialMediaPost | null> { 242 + const [username, domain] = this.parseAccount(account); 243 + this.validateFediverseAccount(username, domain); 244 + 245 + const domainStr = domain as string; 246 + const apiUrl = `https://${domainStr}/api/v1/accounts/lookup?acct=${username}@${domainStr}`; 208 247 209 248 try { 210 249 const accountResponse = await fetchWithTimeout(apiUrl); ··· 213 252 } 214 253 215 254 const accountData = await accountResponse.json(); 255 + 256 + if (!accountData?.id) { 257 + throw new Error('Invalid account data: missing account ID in response'); 258 + } 259 + 216 260 const accountId = accountData.id; 217 - 218 - const statusesUrl = `https://${domain}/api/v1/accounts/${accountId}/statuses?limit=1&exclude_replies=true&exclude_reblogs=true`; 261 + const statusesUrl = `https://${domainStr}/api/v1/accounts/${accountId}/statuses?limit=1&exclude_replies=true&exclude_reblogs=true`; 219 262 const statusResponse = await fetchWithTimeout(statusesUrl); 220 263 221 264 if (!statusResponse.ok) { ··· 227 270 return null; 228 271 } 229 272 230 - const post = this.mapToSocialMediaPost(statuses[0], domain); 273 + const post = this.mapToSocialMediaPost(statuses[0], domainStr); 231 274 return post; 232 275 } catch (error: unknown) { 233 276 const errorMessage = error instanceof Error ? error.message : 'Unknown error';