diff --git a/.prettierrc b/.prettierrc index 0256aae..fdd2792 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,9 +3,36 @@ "singleQuote": true, "tabWidth": 2, "useTabs": false, - "trailingComma": "es5", + "trailingComma": "all", "bracketSpacing": true, + "bracketSameLine": false, "arrowParens": "always", "printWidth": 100, - "endOfLine": "auto" + "endOfLine": "lf", + "quoteProps": "as-needed", + "singleAttributePerLine": true, + "embeddedLanguageFormatting": "auto", + "experimentalTernaries": false, + "overrides": [ + { + "files": ["*.md", "*.mdx"], + "options": { + "printWidth": 80, + "proseWrap": "always" + } + }, + { + "files": ["*.json", "*.jsonc"], + "options": { + "trailingComma": "none" + } + }, + { + "files": ["*.yml", "*.yaml"], + "options": { + "tabWidth": 2, + "singleQuote": false + } + } + ] } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index fdd2d1a..ebfe263 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,7 +2,8 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our project and our community a harassment-free experience for everyone. +We as members, contributors, and leaders pledge to make participation in our +project and our community a harassment-free experience for everyone. ## Our Standards @@ -21,4 +22,5 @@ Examples of unacceptable behavior: ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer at scan@scanash.com. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the maintainer at scan@scanash.com. diff --git a/README.md b/README.md index 5283f12..ae7a916 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ This monorepo includes both the frontend and backend of the Aethel Discord bot. [![Node.js](https://img.shields.io/badge/node-%3E=16.9.0-green?logo=node.js)](https://nodejs.org/) -A privacy-conscious, production-ready Discord user-installed bot with AI chat, reminders, and utility commands. Built with Node.js, Discord.js v14, PostgreSQL, and robust security best practices. +A privacy-conscious, production-ready Discord user-installed bot with AI chat, +reminders, and utility commands. Built with Node.js, Discord.js v14, PostgreSQL, +and robust security best practices. --- ## Features -- **AI Chat**: `/ai` command with custom API key support (OpenRouter, OpenAI, Grok) +- **AI Chat**: `/ai` command with custom API key support (OpenRouter, OpenAI, + Grok) - **Reminders**: `/remind` command for scheduling reminders -- **Utilities**: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, `/whois` +- **Utilities**: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, + `/whois` - **Ephemeral Replies** for sensitive commands - **Encrypted API Key Storage** (AES-256-GCM) - **Rate Limiting & Logging** @@ -92,7 +96,8 @@ See [LICENSE](LICENSE) for details. - Use `/ai` for AI chat, with optional custom API key for private usage - Use `/remind` to schedule reminders -- Use utility commands: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, `/whois` +- Use utility commands: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, + `/whois` - Sensitive commands use ephemeral replies for privacy - Use `/ai use_custom_api:true` to set your own (encrypted) API key @@ -105,44 +110,42 @@ See [LICENSE](LICENSE) for details. ## 🌐 Translations & Localization -Aethel supports multiple languages! You can help improve or add new translations for the bot. +Aethel supports multiple languages! You can help improve or add new translations +for the bot. ### Supported Languages -- English (en-US) - +- English (en-US) English translation status -- Spanish (es-ES) - +- Spanish (es-ES) Spanish translation status - Spanish (Latin America) (es-419) Spanish (Latin America) translation status -- German (de-DE) - +- German (de-DE) German translation status -- French (fr-FR) - +- French (fr-FR) French translation status - Portuguese (Brazil) (pt-BR) Portuguese (Brazil) translation status -- Japanese (ja) - +- Japanese (ja) Japanese translation status ### Contribute a Translation -We use [Weblate](https://translate.aethel.xyz/projects/aethel/) for collaborative translation. Anyone can contribute: +We use [Weblate](https://translate.aethel.xyz/projects/aethel/) for +collaborative translation. Anyone can contribute: -- Visit the [Aethel Weblate project](https://translate.aethel.xyz/projects/aethel/) +- Visit the + [Aethel Weblate project](https://translate.aethel.xyz/projects/aethel/) - Sign in or register (free) - Pick your language and start translating or reviewing existing translations diff --git a/SECURITY.md b/SECURITY.md index ce8c1a0..d47b53e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,7 +11,8 @@ We actively maintain and provide security updates for the following versions: ## Reporting a Vulnerability -We take security vulnerabilities seriously. If you discover a security vulnerability in Aethel, please report it responsibly. +We take security vulnerabilities seriously. If you discover a security +vulnerability in Aethel, please report it responsibly. ### How to Report @@ -25,8 +26,10 @@ We take security vulnerabilities seriously. If you discover a security vulnerabi ### What to Expect - **Acknowledgment**: We will acknowledge receipt of your report within 48 hours -- **Initial Assessment**: We will provide an initial assessment within 5 business days -- **Updates**: We will keep you informed of our progress throughout the investigation +- **Initial Assessment**: We will provide an initial assessment within 5 + business days +- **Updates**: We will keep you informed of our progress throughout the + investigation - **Resolution**: We aim to resolve critical vulnerabilities within 30 days ### Responsible Disclosure @@ -35,13 +38,15 @@ We follow responsible disclosure practices: - We will work with you to understand and resolve the issue - We will credit you for the discovery (unless you prefer to remain anonymous) -- We ask that you do not publicly disclose the vulnerability until we have had a chance to address it +- We ask that you do not publicly disclose the vulnerability until we have had a + chance to address it ## Security Measures ### Current Security Implementations -- **SSRF Protection**: API endpoints are restricted to whitelisted hosts to prevent Server-Side Request Forgery attacks +- **SSRF Protection**: API endpoints are restricted to whitelisted hosts to + prevent Server-Side Request Forgery attacks - **Input Validation**: All user inputs are validated and sanitized - **Encryption**: Sensitive data like API keys are encrypted before storage - **Authentication**: Secure token-based authentication for API access @@ -49,7 +54,8 @@ We follow responsible disclosure practices: ### Allowed API Hosts -For security reasons, custom API endpoints are restricted to the following trusted hosts: +For security reasons, custom API endpoints are restricted to the following +trusted hosts: - `api.openai.com` - `openrouter.ai` @@ -59,15 +65,19 @@ For security reasons, custom API endpoints are restricted to the following trust When contributing to or using Aethel: -1. **Never commit secrets**: Do not include API keys, passwords, or other sensitive information in code -2. **Use environment variables**: Store sensitive configuration in environment variables +1. **Never commit secrets**: Do not include API keys, passwords, or other + sensitive information in code +2. **Use environment variables**: Store sensitive configuration in environment + variables 3. **Validate inputs**: Always validate and sanitize user inputs 4. **Follow least privilege**: Grant minimal necessary permissions -5. **Keep dependencies updated**: Regularly update dependencies to patch known vulnerabilities +5. **Keep dependencies updated**: Regularly update dependencies to patch known + vulnerabilities ## Security Audits -We regularly review our codebase for security vulnerabilities and welcome security audits from the community. +We regularly review our codebase for security vulnerabilities and welcome +security audits from the community. ### Automated Security Checks @@ -77,8 +87,10 @@ We regularly review our codebase for security vulnerabilities and welcome securi ## Contact -For security-related questions or concerns, please contact the project maintainers at scan@scanash.com +For security-related questions or concerns, please contact the project +maintainers at scan@scanash.com --- -**Note**: This security policy is subject to change. Please check back regularly for updates. +**Note**: This security policy is subject to change. Please check back regularly +for updates. diff --git a/eslint.config.mjs b/eslint.config.mjs index 0bc92ca..3041446 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,31 +1,99 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import { defineConfig } from "eslint/config"; - - -export default defineConfig([ - { - files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], - plugins: { js }, - extends: ["js/recommended"], - ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"] +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default [ + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.next/**', + '**/coverage/**', + '**/*.min.js', + '**/logs/**', + ], }, - { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } }, + + js.configs.recommended, ...tseslint.configs.recommended, + { - files: ["**/*.{ts,tsx}"], - ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"], + files: ['**/*.{js,mjs,cjs,ts,mts,cts,tsx}'], + languageOptions: { + globals: { + ...globals.node, + ...globals.es2022, + }, + ecmaVersion: 2022, + sourceType: 'module', + }, rules: { - "@typescript-eslint/no-unused-vars": [ - "error", + 'no-console': 'off', + 'no-debugger': 'error', + 'no-alert': 'error', + 'no-eval': 'error', + 'no-implied-eval': 'error', + + eqeqeq: ['error', 'always'], + curly: ['warn', 'multi-line'], + 'no-else-return': 'warn', + 'no-empty-function': 'warn', + 'no-return-assign': 'error', + 'no-throw-literal': 'error', + + 'no-unreachable': 'error', + 'no-unused-expressions': 'error', + 'no-useless-return': 'warn', + + '@typescript-eslint/no-unused-vars': [ + 'error', { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, ], - "@typescript-eslint/no-explicit-any": "warn" - } - } -]); + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-inferrable-types': 'warn', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/consistent-type-imports': 'off', + + 'no-unused-vars': 'off', + 'no-undef': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'error', + + 'no-process-exit': 'off', + 'no-new-require': 'error', + 'no-path-concat': 'error', + 'no-sync': 'warn', + }, + }, + + { + files: ['**/*.config.{js,mjs,ts}', '**/eslint.config.mjs', '**/src/index.ts'], + rules: { + 'no-console': 'off', + 'no-process-exit': 'off', + }, + }, + + { + files: ['**/*.test.{js,ts}', '**/*.spec.{js,ts}', '**/tests/**/*.{js,ts}'], + languageOptions: { + globals: { + ...globals.jest, + ...globals.node, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, +]; diff --git a/src/commands/fun/8ball.ts b/src/commands/fun/8ball.ts index 6cb6ced..f519ca0 100644 --- a/src/commands/fun/8ball.ts +++ b/src/commands/fun/8ball.ts @@ -73,7 +73,7 @@ const eightBallCommand = { 'en-US': 'Your yes/no question for the magic 8-ball', }) .setRequired(true) - .setMaxLength(200) + .setMaxLength(200), ) .setContexts([ InteractionContextType.BotDM, @@ -84,14 +84,14 @@ const eightBallCommand = { execute: async ( client: import('@/services/Client').default, - interaction: import('discord.js').ChatInputCommandInteraction + interaction: import('discord.js').ChatInputCommandInteraction, ) => { try { const cooldownCheck = await checkCooldown( cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -110,11 +110,11 @@ const eightBallCommand = { const question = sanitizeInput(interaction.options.getString('question')); const translatedResponse = await client.getLocaleText( `commands.8ball.responces.${random(responses)}`, - interaction.locale + interaction.locale, ); commandLogger.logFromInteraction( interaction, - `question: "${question?.substring(0, 50)}${question && question.length > 50 ? '...' : ''}"` + `question: "${question?.substring(0, 50)}${question && question.length > 50 ? '...' : ''}"`, ); const [title, questionLabel, answerLabel] = await Promise.all([ await client.getLocaleText('commands.8ball.says', interaction.locale), @@ -126,7 +126,7 @@ const eightBallCommand = { .setAccentColor(0x8b5cf6) .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔮 ${title}`)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(`**${questionLabel}**\n> ${question}\n\n`) + new TextDisplayBuilder().setContent(`**${questionLabel}**\n> ${question}\n\n`), ) .addTextDisplayComponents(new TextDisplayBuilder().setContent(`## ✨ ${answerLabel}`)) .addTextDisplayComponents(new TextDisplayBuilder().setContent(translatedResponse)); diff --git a/src/commands/fun/cat.ts b/src/commands/fun/cat.ts index a70a38f..45ea7e2 100644 --- a/src/commands/fun/cat.ts +++ b/src/commands/fun/cat.ts @@ -64,7 +64,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -96,11 +96,11 @@ export default { ? await client.getLocaleText('reddit.from', interaction.locale, { subreddit: catData.subreddit, }) - : '' - ) + : '', + ), ) .addMediaGalleryComponents( - new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(catData.url)) + new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(catData.url)), ) .addActionRowComponents( new ActionRowBuilder().addComponents( @@ -108,8 +108,8 @@ export default { .setStyle(ButtonStyle.Danger) .setLabel(refreshLabel) .setEmoji({ name: '🐱' }) - .setCustomId('refresh_cat') - ) + .setCustomId('refresh_cat'), + ), ); try { @@ -135,7 +135,7 @@ export default { const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale); const errorContainer = new ContainerBuilder().addTextDisplayComponents( - new TextDisplayBuilder().setContent(errorMsg) + new TextDisplayBuilder().setContent(errorMsg), ); if (!interaction.replied && !interaction.deferred) { diff --git a/src/commands/fun/dog.ts b/src/commands/fun/dog.ts index 4fb625e..ec6edd2 100644 --- a/src/commands/fun/dog.ts +++ b/src/commands/fun/dog.ts @@ -64,7 +64,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -94,11 +94,11 @@ export default { ? await client.getLocaleText('reddit.from', interaction.locale, { subreddit: dogData.subreddit, }) - : '' - ) + : '', + ), ) .addMediaGalleryComponents( - new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(dogData.url)) + new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(dogData.url)), ) .addActionRowComponents( new ActionRowBuilder().addComponents( @@ -106,8 +106,8 @@ export default { .setStyle(ButtonStyle.Secondary) .setLabel(refreshLabel) .setEmoji({ name: '🐶' }) - .setCustomId('refresh_dog') - ) + .setCustomId('refresh_dog'), + ), ); await interaction.editReply({ @@ -128,7 +128,7 @@ export default { const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale); const errorContainer = new ContainerBuilder().addTextDisplayComponents( - new TextDisplayBuilder().setContent(errorMsg) + new TextDisplayBuilder().setContent(errorMsg), ); if (!interaction.replied && !interaction.deferred) { diff --git a/src/commands/fun/joke.ts b/src/commands/fun/joke.ts index 294312f..1fc447c 100644 --- a/src/commands/fun/joke.ts +++ b/src/commands/fun/joke.ts @@ -79,8 +79,8 @@ export default { value: 'programming', name_localizations: { 'es-ES': 'Programación', 'es-419': 'Programación' }, }, - { name: 'Dad', value: 'dad', name_localizations: { 'es-ES': 'Papá', 'es-419': 'Papá' } } - ) + { name: 'Dad', value: 'dad', name_localizations: { 'es-ES': 'Papá', 'es-419': 'Papá' } }, + ), ) .setContexts([ InteractionContextType.BotDM, @@ -95,7 +95,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -114,7 +114,7 @@ export default { interaction.locale, { type: await client.getLocaleText(`commands.joke.type.${joke.type}`, interaction.locale), - } + }, ); const waitingFooter = await client.getLocaleText( @@ -122,7 +122,7 @@ export default { interaction.locale, { seconds: 3, - } + }, ); const embed = new EmbedBuilder() @@ -136,7 +136,7 @@ export default { setTimeout(async () => { try { embed.setDescription( - `${sanitizeInput(joke.setup)}\n\n*${sanitizeInput(joke.punchline)}*` + `${sanitizeInput(joke.setup)}\n\n*${sanitizeInput(joke.punchline)}*`, ); embed.setFooter({ text: 'Ba dum tss! 🥁' }); await interaction.editReply({ embeds: [embed] }); diff --git a/src/commands/fun/trivia.ts b/src/commands/fun/trivia.ts index c7068b9..d15a1e8 100644 --- a/src/commands/fun/trivia.ts +++ b/src/commands/fun/trivia.ts @@ -172,13 +172,13 @@ function decodeHtmlEntities(text: string): string { async function fetchTriviaQuestions( client: import('@/services/Client').default, locale: string, - playerCount: number + playerCount: number, ): Promise { try { const totalQuestions = Math.min(Math.max(playerCount * 2 + 1, 5), 50); const response = await dynamicFetch( - `https://opentdb.com/api.php?amount=${totalQuestions}&type=multiple` + `https://opentdb.com/api.php?amount=${totalQuestions}&type=multiple`, ); const data: TriviaAPIResponse = await response.json(); @@ -256,13 +256,13 @@ function getNextPlayer(session: GameSession): string { async function startTriviaGame( interaction: ChatInputCommandInteraction | ButtonInteraction, session: GameSession, - client: import('@/services/Client').default + client: import('@/services/Client').default, ) { try { session.questions = await fetchTriviaQuestions( client, interaction.locale, - session.players.size + session.players.size, ); session.originalQuestionCount = session.questions.length; session.isActive = true; @@ -274,7 +274,7 @@ async function startTriviaGame( } catch { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.failed_start', - interaction.locale + interaction.locale, ); await interaction.editReply({ content: errorMsg, @@ -286,7 +286,7 @@ async function startTriviaGame( async function askQuestion( interaction: ChatInputCommandInteraction | ButtonInteraction, session: GameSession, - client: import('@/services/Client').default + client: import('@/services/Client').default, ) { if (session.currentQuestionIndex >= session.questions.length) { const sortedScores = Array.from(session.scores.entries()).sort(([, a], [, b]) => b - a); @@ -300,7 +300,7 @@ async function askQuestion( const tiedGameText = await client.getLocaleText( 'commands.trivia.messages.tied_game', - interaction.locale + interaction.locale, ); await interaction.editReply({ content: tiedGameText || '🤝 Tied game! Keep going until someone scores!', @@ -349,7 +349,7 @@ async function askQuestion( '\n' + difficultyText.replace( '{difficulty}', - question.difficulty.charAt(0).toUpperCase() + question.difficulty.slice(1) + question.difficulty.charAt(0).toUpperCase() + question.difficulty.slice(1), ) + '\n\n' + questionText.replace('{question}', question.question); @@ -365,7 +365,7 @@ async function askQuestion( async function endGame( interaction: ChatInputCommandInteraction | ButtonInteraction, session: GameSession, - client: import('@/services/Client').default + client: import('@/services/Client').default, ) { const sortedScores = Array.from(session.scores.entries()).sort(([, a], [, b]) => b - a); @@ -412,7 +412,7 @@ const triviaCommand = { execute: async ( client: import('@/services/Client').default, - interaction: ChatInputCommandInteraction + interaction: ChatInputCommandInteraction, ) => { try { const channelId = interaction.channelId; @@ -420,7 +420,7 @@ const triviaCommand = { if (gameManager.has(channelId)) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.game_exists', - interaction.locale + interaction.locale, ); return interaction.reply({ content: errorMsg, @@ -471,7 +471,7 @@ const triviaCommand = { handleButton: async ( client: import('@/services/Client').default, - interaction: ButtonInteraction + interaction: ButtonInteraction, ) => { try { const channelId = interaction.channelId; @@ -480,7 +480,7 @@ const triviaCommand = { if (!session) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.no_active_game', - interaction.locale + interaction.locale, ); return interaction.reply({ content: errorMsg, @@ -494,7 +494,7 @@ const triviaCommand = { if (!session.queueOpen) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.game_started', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, @@ -505,7 +505,7 @@ const triviaCommand = { if (session.players.has(interaction.user.id)) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.already_joined', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, @@ -535,7 +535,7 @@ const triviaCommand = { if (interaction.user.id !== session.gameCreator) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.only_creator_start', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, @@ -546,7 +546,7 @@ const triviaCommand = { if (session.players.size < 1) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.need_players', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, @@ -556,7 +556,7 @@ const triviaCommand = { const startingMsg = await client.getLocaleText( 'commands.trivia.messages.starting_game', - interaction.locale + interaction.locale, ); await interaction.update({ content: startingMsg, @@ -568,7 +568,7 @@ const triviaCommand = { if (interaction.user.id !== session.gameCreator) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.only_creator_cancel', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, @@ -580,7 +580,7 @@ const triviaCommand = { const cancelMsg = await client.getLocaleText( 'commands.trivia.messages.game_cancelled', - interaction.locale + interaction.locale, ); await interaction.update({ content: cancelMsg, @@ -590,7 +590,7 @@ const triviaCommand = { if (!session.isActive) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.no_active_question', - interaction.locale + interaction.locale, ); return interaction.reply({ content: errorMsg, @@ -601,7 +601,7 @@ const triviaCommand = { if (interaction.user.id !== session.currentPlayer) { const errorMsg = await client.getLocaleText( 'commands.trivia.messages.not_your_turn', - interaction.locale + interaction.locale, ); return interaction.reply({ content: '❌ ' + errorMsg, diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index e44bfcb..bea6977 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -63,27 +63,27 @@ export default { .addMediaGalleryComponents( new MediaGalleryBuilder().addItems( - new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png') - ) + new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png'), + ), ) .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(description || 'Get information about Aethel') + new TextDisplayBuilder().setContent(description || 'Get information about Aethel'), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - `\n## **${linksSocialText || 'Links & Social Media'}**` - ) + `\n## **${linksSocialText || 'Links & Social Media'}**`, + ), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - '[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)' - ) + '[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)', + ), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`) + new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( @@ -92,17 +92,17 @@ export default { '**AI Integration** - Powered by OpenAI and other providers\n' + '**Reminders** - Never forget important tasks\n' + '**Utilities** - Weather, help, and productivity tools\n' + - '**Multi-language** - Supports multiple languages' - ) + '**Multi-language** - Supports multiple languages', + ), ) .addSeparatorComponents( - new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true) + new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - `-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}` - ) + `-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`, + ), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addActionRowComponents( @@ -114,8 +114,8 @@ export default { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(supportServerText || 'Support') - .setURL('https://discord.gg/63stE8pEaK') - ) + .setURL('https://discord.gg/63stE8pEaK'), + ), ); await interaction.reply({ diff --git a/src/commands/utilities/ai.ts b/src/commands/utilities/ai.ts index ed8f461..219a3ec 100644 --- a/src/commands/utilities/ai.ts +++ b/src/commands/utilities/ai.ts @@ -113,7 +113,7 @@ export function processUrls(text: string): string { const after = text[startIdx + url.length]; if (before === '<' && after === '>') return url + (punctuation || ''); return `<${url}>${punctuation || ''}`; - } + }, ); } @@ -138,7 +138,7 @@ function buildSystemPrompt( client?: BotClient, model?: string, username?: string, - interaction?: ChatInputCommandInteraction + interaction?: ChatInputCommandInteraction, ): string { const now = new Date(); const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -160,11 +160,11 @@ function buildSystemPrompt( let supportedCommands = '/help - Show all available commands and their usage'; if (client?.commands) { const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) => - a.localeCompare(b) + a.localeCompare(b), ); supportedCommands = commandEntries .map( - ([name, command]) => `/${name} - ${command.data.description || 'No description available'}` + ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, ) .join('\n'); } @@ -218,7 +218,7 @@ function buildConversation( detail?: 'low' | 'high' | 'auto'; }; }>, - systemPrompt: string + systemPrompt: string, ): ConversationMessage[] { let conversation = existingConversation.filter((msg) => msg.role !== 'system'); conversation.push({ role: 'user', content: prompt }); @@ -231,7 +231,7 @@ function buildConversation( return conversation; } -function splitResponseIntoChunks(response: string, maxLength: number = 2000): string[] { +function splitResponseIntoChunks(response: string, maxLength = 2000): string[] { if (response.length <= maxLength) return [response]; const chunks: string[] = []; @@ -272,12 +272,12 @@ async function setUserApiKey( userId: string, apiKey: string | null, model: string | null, - apiUrl: string | null + apiUrl: string | null, ): Promise { if (apiKey === null) { await pool.query( `UPDATE users SET api_key_encrypted = NULL, custom_model = NULL, custom_api_url = NULL, updated_at = now() WHERE user_id = $1`, - [userId] + [userId], ); logger.info(`Cleared API credentials for user ${userId}`); } else { @@ -291,7 +291,7 @@ async function setUserApiKey( VALUES ($1, $2, $3, $4, now()) ON CONFLICT (user_id) DO UPDATE SET api_key_encrypted = $2, custom_model = $3, custom_api_url = $4, updated_at = now()`, - [userId, encrypted, model?.trim() || null, apiUrl?.trim() || null] + [userId, encrypted, model?.trim() || null, apiUrl?.trim() || null], ); logger.info(`Successfully saved encrypted API credentials for user ${userId}`); } @@ -340,7 +340,7 @@ async function clearCorruptedApiKey(userId: string): Promise { } } -async function incrementAndCheckDailyLimit(userId: string, limit: number = 20): Promise { +async function incrementAndCheckDailyLimit(userId: string, limit = 20): Promise { const today = new Date().toISOString().slice(0, 10); const client = await pool.connect(); try { @@ -351,7 +351,7 @@ async function incrementAndCheckDailyLimit(userId: string, limit: number = 20): const res = await client.query( `INSERT INTO ai_usage (user_id, usage_date, count) VALUES ($1, $2, 1) ON CONFLICT (user_id, usage_date) DO UPDATE SET count = ai_usage.count + 1 RETURNING count`, - [userId, today] + [userId, today], ); await client.query('COMMIT'); return res.rows[0].count <= limit; @@ -366,7 +366,7 @@ async function incrementAndCheckDailyLimit(userId: string, limit: number = 20): async function testApiKey( apiKey: string, model: string, - apiUrl: string + apiUrl: string, ): Promise<{ success: boolean; error?: string }> { try { const client = getOpenAIClient(apiKey, apiUrl); @@ -397,7 +397,7 @@ async function testApiKey( async function makeAIRequest( config: ReturnType, - conversation: ConversationMessage[] + conversation: ConversationMessage[], ): Promise { try { const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); @@ -429,7 +429,7 @@ async function makeAIRequest( async function processAIRequest( client: BotClient, - interaction: ChatInputCommandInteraction + interaction: ChatInputCommandInteraction, ): Promise { try { if (!interaction.deferred && !interaction.replied) { @@ -439,7 +439,7 @@ async function processAIRequest( const prompt = interaction.options.getString('prompt')!; commandLogger.logFromInteraction( interaction, - `prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"` + `prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"`, ); const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); @@ -452,14 +452,14 @@ async function processAIRequest( if (!allowed) { await interaction.editReply( '❌ ' + - (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)) + (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), ); return; } } } else if (!config.finalApiKey) { await interaction.editReply( - '❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)) + '❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), ); return; } @@ -471,7 +471,7 @@ async function processAIRequest( client, config.finalModel, interaction.user.tag, - interaction + interaction, ); const conversation = buildConversation(conversationArray, prompt, systemPrompt); @@ -505,7 +505,7 @@ async function processAIRequest( async function sendAIResponse( interaction: ChatInputCommandInteraction, aiResponse: AIResponse, - client: BotClient + client: BotClient, ): Promise { let fullResponse = ''; @@ -584,13 +584,13 @@ export default { 'es-419': 'Tu mensaje para la IA', 'en-US': 'Your message to the AI', }) - .setRequired(true) + .setRequired(true), ) .addBooleanOption((option) => - option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false) + option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false), ) .addBooleanOption((option) => - option.setName('reset').setDescription('Reset your AI chat history').setRequired(false) + option.setName('reset').setDescription('Reset your AI chat history').setRequired(false), ), async execute(client: BotClient, interaction: ChatInputCommandInteraction) { @@ -647,7 +647,7 @@ export default { .setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale)) .setStyle(TextInputStyle.Short) .setPlaceholder( - await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale) + await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale), ) .setRequired(true); @@ -656,7 +656,7 @@ export default { .setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale)) .setStyle(TextInputStyle.Short) .setPlaceholder( - await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale) + await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale), ) .setRequired(true); @@ -665,14 +665,14 @@ export default { .setLabel(await client.getLocaleText('commands.ai.modal.model', interaction.locale)) .setStyle(TextInputStyle.Short) .setPlaceholder( - await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale) + await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale), ) .setRequired(true); modal.addComponents( new ActionRowBuilder().addComponents(apiKeyInput), new ActionRowBuilder().addComponents(apiUrlInput), - new ActionRowBuilder().addComponents(modelInput) + new ActionRowBuilder().addComponents(modelInput), ); await interaction.showModal(modal); @@ -701,7 +701,7 @@ export default { if (!pendingRequest) { return interaction.editReply( - await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale) + await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale), ); } @@ -715,37 +715,37 @@ export default { parsedUrl = new URL(apiUrl); } catch { await interaction.editReply( - 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).' + 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', ); return; } if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { await interaction.editReply( - 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).' + 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', ); return; } await interaction.editReply( - await client.getLocaleText('commands.ai.testing', interaction.locale) + await client.getLocaleText('commands.ai.testing', interaction.locale), ); const testResult = await testApiKey(apiKey, model, apiUrl); if (!testResult.success) { const errorMessage = await client.getLocaleText( 'commands.ai.testfailed', - interaction.locale + interaction.locale, ); await interaction.editReply( - errorMessage.replace('{error}', testResult.error || 'Unknown error') + errorMessage.replace('{error}', testResult.error || 'Unknown error'), ); return; } await setUserApiKey(userId, apiKey, model, apiUrl); await interaction.editReply( - await client.getLocaleText('commands.ai.testsuccess', interaction.locale) + await client.getLocaleText('commands.ai.testsuccess', interaction.locale), ); if (!originalInteraction.deferred && !originalInteraction.replied) { diff --git a/src/commands/utilities/cobalt.ts b/src/commands/utilities/cobalt.ts index 2b537ef..f59d7d0 100644 --- a/src/commands/utilities/cobalt.ts +++ b/src/commands/utilities/cobalt.ts @@ -59,7 +59,7 @@ export default { 'es-419': 'La URL del video a descargar', 'en-US': 'The URL of the video to download', }) - .setRequired(true) + .setRequired(true), ) .addStringOption((option) => option @@ -84,8 +84,8 @@ export default { { name: '1440p', value: '1440' }, { name: '2160p (4K)', value: '2160' }, { name: '4320p (8K)', value: '4320' }, - { name: 'Max', value: 'max' } - ) + { name: 'Max', value: 'max' }, + ), ) .addBooleanOption((option) => option @@ -100,7 +100,7 @@ export default { 'es-ES': 'Descargar solo audio', 'es-419': 'Descargar solo audio', 'en-US': 'Download audio only', - }) + }), ) .addBooleanOption((option) => option @@ -115,7 +115,7 @@ export default { 'es-ES': 'Silenciar audio', 'es-419': 'Silenciar audio', 'en-US': 'Mute audio', - }) + }), ) .addBooleanOption((option) => option @@ -130,7 +130,7 @@ export default { 'es-ES': 'Descargar como GIF de Twitter', 'es-419': 'Descargar como GIF de Twitter', 'en-US': 'Download as Twitter GIF', - }) + }), ) .addBooleanOption((option) => option @@ -145,7 +145,7 @@ export default { 'es-ES': 'Incluir audio original de TikTok', 'es-419': 'Incluir audio original de TikTok', 'en-US': 'Include TikTok original audio', - }) + }), ) .addStringOption((option) => option @@ -165,8 +165,8 @@ export default { { name: 'MP3', value: 'mp3' }, { name: 'OGG', value: 'ogg' }, { name: 'WAV', value: 'wav' }, - { name: 'Best', value: 'best' } - ) + { name: 'Best', value: 'best' }, + ), ) .setContexts([ InteractionContextType.BotDM, @@ -221,7 +221,7 @@ export default { const buttonLabel = await client.getLocaleText( 'commands.cobalt.button_label', - interaction.locale + interaction.locale, ); const container = new ContainerBuilder() @@ -230,18 +230,18 @@ export default { new MediaGalleryBuilder().addItems( new MediaGalleryItemBuilder() .setDescription(data.filename || 'Downloaded media') - .setURL(downloadUrl) - ) + .setURL(downloadUrl), + ), ) .addSectionComponents( new SectionBuilder() .addTextDisplayComponents((textDisplay) => - textDisplay.setContent(`-# Took: ${processingTime}s`) + textDisplay.setContent(`-# Took: ${processingTime}s`), ) .setButtonAccessory((button) => - button.setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl) - ) + button.setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl), + ), ); await interaction.editReply({ @@ -251,7 +251,7 @@ export default { } else if (data.status === 'error') { const unknownError = await client.getLocaleText( 'commands.cobalt.unknown_error', - interaction.locale + interaction.locale, ); let errorText = unknownError; @@ -266,15 +266,15 @@ export default { interaction.locale, { error: errorText, - } + }, ); const errorContainer = new ContainerBuilder() .setAccentColor(0xff4757) .addSectionComponents( new SectionBuilder().addTextDisplayComponents((textDisplay) => - textDisplay.setContent(`❌ ${errorMessage}`) - ) + textDisplay.setContent(`❌ ${errorMessage}`), + ), ); await interaction.editReply({ @@ -285,7 +285,7 @@ export default { const multipleItemsMessage = await client.getLocaleText( 'commands.cobalt.multiple_items', interaction.locale, - { url } + { url }, ); const pickerContainer = new ContainerBuilder() @@ -293,9 +293,9 @@ export default { .addSectionComponents( new SectionBuilder().addTextDisplayComponents((textDisplay) => textDisplay.setContent( - `⚠️ ${multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.'}` - ) - ) + `⚠️ ${multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.'}`, + ), + ), ); await interaction.editReply({ @@ -305,7 +305,7 @@ export default { } else if (data.status === 'local-processing') { const notSupportedMessage = await client.getLocaleText( 'commands.cobalt.local_processing_not_supported', - interaction.locale + interaction.locale, ); const processingContainer = new ContainerBuilder() @@ -313,9 +313,9 @@ export default { .addSectionComponents( new SectionBuilder().addTextDisplayComponents((textDisplay) => textDisplay.setContent( - `⚠️ ${notSupportedMessage || 'Local processing is not supported. Please try a different URL or option.'}` - ) - ) + `⚠️ ${notSupportedMessage || 'Local processing is not supported. Please try a different URL or option.'}`, + ), + ), ); await interaction.editReply({ @@ -325,15 +325,15 @@ export default { } else { const unknownResponseMessage = await client.getLocaleText( 'commands.cobalt.unknown_response', - interaction.locale + interaction.locale, ); const unknownContainer = new ContainerBuilder() .setAccentColor(0xff4757) .addSectionComponents( new SectionBuilder().addTextDisplayComponents((textDisplay) => - textDisplay.setContent(`❓ ${unknownResponseMessage}`) - ) + textDisplay.setContent(`❓ ${unknownResponseMessage}`), + ), ); await interaction.editReply({ diff --git a/src/commands/utilities/remind.ts b/src/commands/utilities/remind.ts index eac5916..838e3c4 100644 --- a/src/commands/utilities/remind.ts +++ b/src/commands/utilities/remind.ts @@ -91,7 +91,7 @@ export async function loadActiveReminders(client: BotClient) { ...reminder, created_at: reminder.created_at || new Date(reminder.expires_at), }), - timeUntilExpiry + timeUntilExpiry, ); activeReminders.set(reminder.reminder_id, { @@ -100,7 +100,7 @@ export async function loadActiveReminders(client: BotClient) { }); logger.debug( - `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`, ); } @@ -131,7 +131,7 @@ export function scheduleReminder(client: BotClient, reminder: Reminder) { ...reminder, created_at: reminder.created_at || new Date(), }), - timeUntilExpiry + timeUntilExpiry, ); activeReminders.set(reminder.reminder_id, { @@ -140,7 +140,7 @@ export function scheduleReminder(client: BotClient, reminder: Reminder) { }); logger.info( - `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`, ); return true; } catch (error) { @@ -166,7 +166,7 @@ function createReminderHandler(client: BotClient, reminder: Reminder) { const minutes = Math.floor( (new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) / - (60 * 1000) + (60 * 1000), ); const reminderTitle = @@ -174,7 +174,7 @@ function createReminderHandler(client: BotClient, reminder: Reminder) { const reminderDesc = await client.getLocaleText( 'commands.remind.remindyou', reminder.locale, - { message: reminder.message } + { message: reminder.message }, ); const timeElapsedText = @@ -192,7 +192,7 @@ function createReminderHandler(client: BotClient, reminder: Reminder) { name: originalTimeText, value: ``, inline: true, - } + }, ) .setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` }) .setTimestamp(); @@ -201,7 +201,7 @@ function createReminderHandler(client: BotClient, reminder: Reminder) { const originalMessageText = await client.getLocaleText('common.ogmessage', reminder.locale); const jumpToMessageText = await client.getLocaleText( 'common.jumptomessage', - reminder.locale + reminder.locale, ); reminderEmbed.addFields({ @@ -233,7 +233,7 @@ function createReminderHandler(client: BotClient, reminder: Reminder) { { error, reminderId: reminder.reminder_id, - } + }, ); } finally { try { @@ -271,7 +271,7 @@ setInterval( } } }, - 60 * 60 * 1000 + 60 * 60 * 1000, ); function createCommandBuilder() { @@ -298,7 +298,7 @@ function createCommandBuilder() { 'es-ES': 'Cuándo recordarte (ej: 1h, 30m, 5h30m)', 'es-419': 'Cuándo recordarte (ej: 1h, 30m, 5h30m)', }) - .setRequired(true) + .setRequired(true), ) .addStringOption((option) => option @@ -312,7 +312,7 @@ function createCommandBuilder() { 'es-ES': 'Sobre qué quieres que te recuerde', 'es-419': 'Sobre qué quieres que te recuerde', }) - .setRequired(true) + .setRequired(true), ) .setContexts([ InteractionContextType.BotDM, @@ -360,14 +360,14 @@ export default { commandLogger.logFromInteraction( interaction, - `time: ${timeStr}, message: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"` + `time: ${timeStr}, message: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`, ); if (!validateTimeString(timeStr)) { logger.warn(`Invalid time format from ${userTag}: ${timeStr}`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.invalidformat', - interaction.locale + interaction.locale, ); return await interaction.editReply({ content: errorMsg, @@ -379,7 +379,7 @@ export default { logger.warn(`Invalid message from ${userTag} - Length: ${message?.length}`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.providevalidchars', - interaction.locale + interaction.locale, ); return await interaction.editReply({ content: errorMsg, @@ -392,7 +392,7 @@ export default { logger.warn(`Reminder time too short from ${userTag}: ${timeStr}`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.atleastoneminute', - interaction.locale + interaction.locale, ); return await interaction.editReply({ content: errorMsg, @@ -403,7 +403,7 @@ export default { logger.warn(`Reminder time too long from ${userTag}: ${minutes} minutes`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.notlongerthanaday', - interaction.locale + interaction.locale, ); return await interaction.editReply({ content: errorMsg, @@ -436,7 +436,7 @@ export default { ...reminderData, created_at: new Date(), }), - minutes * 60 * 1000 + minutes * 60 * 1000, ); activeReminders.set(reminderId, { @@ -447,13 +447,13 @@ export default { const embed = new EmbedBuilder() .setColor(0xfaa0a0) .setTitle( - '⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)) + '⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)), ) .setDescription( await client.getLocaleText('commands.remind.iwillremindyou', interaction.locale, { message, time: formatTimeString(minutes), - }) + }), ) .addFields( { @@ -468,7 +468,7 @@ export default { (await client.getLocaleText('commands.remind.willtrigger', interaction.locale)), value: ``, inline: true, - } + }, ) .setFooter({ text: await client.getLocaleText('commands.remind.reminderid', interaction.locale, { @@ -487,9 +487,8 @@ export default { userTag, expiresAt: expiresAt.toISOString(), messagePreview: message.length > 50 ? `${message.substring(0, 50)}...` : message, - } + }, ); - return; } catch (error) { if (error instanceof DatabaseError) { logger.error(`Database error saving reminder: ${error.message}`, { @@ -506,7 +505,7 @@ export default { }); const errorMessage = await client.getLocaleText( 'commands.remind.errors.failedtosave', - interaction.locale + interaction.locale, ); throw new Error(errorMessage); } @@ -551,16 +550,16 @@ export default { messageId: message.id, channelId: message.channelId, guildId: message.guildId, - } + }, ); const modalTitle = await client.getLocaleText( 'commands.remind.modal.setreminder', - interaction.locale + interaction.locale, ); const timeLabel = await client.getLocaleText( 'commands.remind.modal.whento', - interaction.locale + interaction.locale, ); const modal = new ModalBuilder().setCustomId(modalId).setTitle(modalTitle); @@ -583,7 +582,7 @@ export default { userId: user.id, userTag: user.tag, messageId: message?.id, - } + }, ); let errorMessage: string; @@ -595,7 +594,7 @@ export default { } else { errorMessage = await client.getLocaleText( 'commands.remind.errors.modalfailed', - interaction.locale + interaction.locale, ); errorTitle = '❌ ' + (await client.getLocaleText('error', interaction.locale)); } @@ -631,7 +630,7 @@ export default { logger.warn(`No message info found for modal ID: ${modalId}`, { userId: user.id }); const errorMsg = await client.getLocaleText( 'commands.remind.errors.expired', - interaction.locale + interaction.locale, ); return await modalInteraction.editReply({ content: errorMsg, @@ -647,7 +646,7 @@ export default { logger.warn(`Invalid time format from ${user.tag} in modal: ${timeStr}`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.invalidformat', - interaction.locale + interaction.locale, ); return await modalInteraction.editReply({ content: errorMsg, @@ -660,7 +659,7 @@ export default { logger.warn(`Invalid time duration from ${user.tag} in modal: ${minutes} minutes`); const errorMsg = await client.getLocaleText( 'commands.remind.errors.notlongerthanaday', - interaction.locale + interaction.locale, ); return await modalInteraction.editReply({ content: errorMsg, @@ -702,16 +701,15 @@ export default { return await modalInteraction.editReply({ content: error.userMessage, }); - } else { - logger.error(`Error saving reminder to database: ${(error as Error).message}`, { error }); - const errorMsg = await client.getLocaleText( - 'commands.remind.errors.failedtosave', - interaction.locale - ); - return await modalInteraction.editReply({ - content: errorMsg, - }); } + logger.error(`Error saving reminder to database: ${(error as Error).message}`, { error }); + const errorMsg = await client.getLocaleText( + 'commands.remind.errors.failedtosave', + interaction.locale, + ); + return await modalInteraction.editReply({ + content: errorMsg, + }); } const timeoutId = setTimeout( @@ -724,7 +722,7 @@ export default { message_url: messageInfo.url, }, }), - minutes * 60 * 1000 + minutes * 60 * 1000, ); activeReminders.set(reminderId, { @@ -734,19 +732,19 @@ export default { const jumpToMessageField = await client.getLocaleText( 'common.jumptomessage', - interaction.locale + interaction.locale, ); const embed = new EmbedBuilder() .setColor(0xfaa0a0) .setTitle( - '⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)) + '⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)), ) // .setTitle(reminderSetTitle) .setDescription( await client.getLocaleText('commands.remind.contextiwillremindyou', interaction.locale, { time: formatTimeString(minutes), - }) + }), ) // .setDescription(reminderSetDesc) .addFields( @@ -758,7 +756,7 @@ export default { name: await client.getLocaleText('commands.remind.willtrigger', interaction.locale), value: ``, inline: true, - } + }, ) .setFooter({ text: await client.getLocaleText('commands.remind.reminderid', interaction.locale, { @@ -775,20 +773,19 @@ export default { channelId: messageInfo.channelId, minutes, }); - return; } catch (error) { logger.error( `Error handling reminder modal for ${user.tag} (${user.id}): ${(error as Error).message}`, { error, modalId, - } + }, ); try { const errorMsg = await client.getLocaleText( 'commands.remind.errors.base', - interaction.locale + interaction.locale, ); await modalInteraction.editReply({ content: errorMsg, @@ -796,7 +793,6 @@ export default { } catch (replyError) { logger.error('Failed to send error response to user:', { error: replyError }); } - return; } }, } as RemindCommandProps; diff --git a/src/commands/utilities/time.ts b/src/commands/utilities/time.ts index 1d97f3b..0a9a2ac 100644 --- a/src/commands/utilities/time.ts +++ b/src/commands/utilities/time.ts @@ -61,7 +61,7 @@ export default { 'es-419': 'La ciudad (ej: Londres, Nueva York)', }) .setRequired(true) - .setAutocomplete(true) + .setAutocomplete(true), ) .setContexts([ InteractionContextType.BotDM, @@ -93,7 +93,7 @@ export default { const flag = iso2ToDiscordFlag(match.iso2); const fieldDay = await client.getLocaleText( 'commands.time.embed.field_day_of_week', - locale + locale, ); const fieldTime = await client.getLocaleText('commands.time.embed.field_time', locale); const fieldDate = await client.getLocaleText('commands.time.embed.field_date', locale); @@ -126,7 +126,7 @@ export default { const description = await client.getLocaleText( 'commands.time.embed.multi_city_description', locale, - { city } + { city }, ); const fields = await Promise.all( shown.map(async (match) => { @@ -137,7 +137,7 @@ export default { const flag = iso2ToDiscordFlag(match.iso2); const fieldDay = await client.getLocaleText( 'commands.time.embed.field_day_of_week', - locale + locale, ); const fieldTime = await client.getLocaleText('commands.time.embed.field_time', locale); const fieldDate = await client.getLocaleText('commands.time.embed.field_date', locale); @@ -146,7 +146,7 @@ export default { value: `${fieldDay}: **${day}**\n${fieldTime}: **${time}**\n${fieldDate}: **${date}**`, inline: false, }; - }) + }), ); const title = await client.getLocaleText('commands.time.embed.title_multi', locale, { city, @@ -186,7 +186,7 @@ export default { async autocomplete(client, interaction: AutocompleteInteraction) { const focused = interaction.options.getFocused(); const filtered = POPULAR_CITIES.filter((city) => - city.toLowerCase().includes(focused.toLowerCase()) + city.toLowerCase().includes(focused.toLowerCase()), ).slice(0, 25); await interaction.respond(filtered.map((city) => ({ name: city, value: city }))); }, diff --git a/src/commands/utilities/todo.ts b/src/commands/utilities/todo.ts index 533ea9e..6551779 100644 --- a/src/commands/utilities/todo.ts +++ b/src/commands/utilities/todo.ts @@ -21,7 +21,7 @@ async function getTodosForUser(userId: string) { try { const { rows } = await pool.query( 'SELECT item FROM todos WHERE user_id = $1 AND done = FALSE ORDER BY created_at ASC', - [userId] + [userId], ); return rows.map((r) => r.item); } catch (error) { @@ -33,7 +33,7 @@ async function getDoneForUser(userId: string) { try { const { rows } = await pool.query( 'SELECT item FROM todos WHERE user_id = $1 AND done = TRUE ORDER BY completed_at ASC', - [userId] + [userId], ); return rows.map((r) => r.item); } catch (error) { @@ -74,8 +74,8 @@ export default { 'es-ES': 'La tarea a agregar', 'es-419': 'La tarea a agregar', }) - .setRequired(true) - ) + .setRequired(true), + ), ) .addSubcommand((sub) => sub @@ -98,8 +98,8 @@ export default { 'es-419': 'Selecciona una tarea para marcar como hecha', }) .setRequired(true) - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), ) .addSubcommand((sub) => sub @@ -112,7 +112,7 @@ export default { .setDescriptionLocalizations({ 'es-ES': 'Ver tu lista de tareas', 'es-419': 'Ver tu lista de tareas', - }) + }), ) .setContexts([ InteractionContextType.BotDM, @@ -154,7 +154,7 @@ export default { try { const { rowCount } = await pool.query( 'UPDATE todos SET done = TRUE, completed_at = NOW() WHERE user_id = $1 AND item = $2 AND done = FALSE', - [userId, item] + [userId, item], ); if (rowCount === 0) { await interaction.reply({ diff --git a/src/commands/utilities/weather.ts b/src/commands/utilities/weather.ts index b97cbf6..8e24b26 100644 --- a/src/commands/utilities/weather.ts +++ b/src/commands/utilities/weather.ts @@ -52,7 +52,7 @@ async function fetchWeatherData(location: string, units = 'metric') { const data = (await response.json()) as WeatherAPIResponse; if (!response.ok || data.cod !== 200) { const error = new Error( - (data as WeatherErrorResponse).message ?? 'Failed to fetch weather data' + (data as WeatherErrorResponse).message ?? 'Failed to fetch weather data', ); throw error; } @@ -89,7 +89,7 @@ export default { 'en-US': 'City name (e.g., London, New York, Tokyo)', }) .setRequired(true) - .setMaxLength(100) + .setMaxLength(100), ) .setContexts([ InteractionContextType.BotDM, @@ -104,7 +104,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -188,7 +188,7 @@ export default { { name: weatherText, value: description, inline: true }, { name: humidityText, value: `${data.main.humidity}%`, inline: true }, { name: windText, value: `${windSpeed} ${windUnit}`, inline: true }, - { name: pressureText, value: `${data.main.pressure} hPa`, inline: true } + { name: pressureText, value: `${data.main.pressure} hPa`, inline: true }, ) .setFooter({ text: footer + ' Open Weather' }) .setTimestamp(); diff --git a/src/commands/utilities/whois.ts b/src/commands/utilities/whois.ts index 3c9bdd0..eb4528e 100644 --- a/src/commands/utilities/whois.ts +++ b/src/commands/utilities/whois.ts @@ -34,7 +34,7 @@ const delay = (ms: number): Promise => new Promise((resolve) => setTimeout async function withRetry( fn: () => Promise, maxRetries = MAX_RETRIES, - baseDelay = 1000 + baseDelay = 1000, ): Promise { let lastError: any; @@ -466,7 +466,7 @@ async function getWhoisData(query: string): Promise { } throw new Error( - 'Could not retrieve WHOIS information. The domain may not exist or the WHOIS server may be temporarily unavailable.' + 'Could not retrieve WHOIS information. The domain may not exist or the WHOIS server may be temporarily unavailable.', ); }; @@ -481,7 +481,7 @@ export default { .setName('whois') .setDescription('Look up WHOIS information for a domain or IP address') .addStringOption((option) => - option.setName('query').setDescription('Domain or IP address to look up').setRequired(true) + option.setName('query').setDescription('Domain or IP address to look up').setRequired(true), ) .setContexts([ InteractionContextType.BotDM, @@ -495,7 +495,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -527,7 +527,7 @@ export default { .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔍 WHOIS Lookup`)) .addTextDisplayComponents(new TextDisplayBuilder().setContent(`## ${sanitizedQuery}`)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(formattedData || 'No WHOIS data available') + new TextDisplayBuilder().setContent(formattedData || 'No WHOIS data available'), ), ]; diff --git a/src/commands/utilities/wiki.ts b/src/commands/utilities/wiki.ts index 688bd42..d484cee 100644 --- a/src/commands/utilities/wiki.ts +++ b/src/commands/utilities/wiki.ts @@ -93,7 +93,7 @@ export default { 'en-US': 'What do you want to search for?', }) .setRequired(true) - .setMaxLength(200) + .setMaxLength(200), ) .setContexts([ InteractionContextType.BotDM, @@ -108,7 +108,7 @@ export default { cooldownManager, interaction.user.id, client, - interaction.locale + interaction.locale, ); if (cooldownCheck.onCooldown) { return interaction.reply(createCooldownResponse(cooldownCheck.message!)); @@ -134,7 +134,7 @@ export default { if (extract.length > MAX_EXTRACT_LENGTH) { const truncatedText = await client.getLocaleText( 'commands.wiki.readmoreonwiki', - interaction.locale + interaction.locale, ); extract = extract.substring(0, MAX_EXTRACT_LENGTH - truncatedText.length - 2) + @@ -144,7 +144,7 @@ export default { const readMore = await client.getLocaleText( 'commands.wiki.readmoreonwiki', - interaction.locale + interaction.locale, ); const title = await client.getLocaleText('commands.wiki.pedia', interaction.locale, { article: article.title, @@ -153,7 +153,7 @@ export default { const embed = new EmbedBuilder() .setTitle(title) .setURL( - `https://${wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}` + `https://${wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`, ) .setDescription(extract) .setColor(0x4285f4) diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 6a5bc5d..68016fc 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -58,12 +58,11 @@ export default class InteractionCreateEvent { content: `You have been banned from using Aethel commands for 7 days due to repeated use of unallowed language. Ban expires: .`, flags: MessageFlags.Ephemeral, }); - } else { - return i.reply({ - content: `Your request was flagged by Aethel for ${category}. You have ${strike_count}/5 strikes. For more information, visit https://aethel.xyz/legal/terms`, - flags: MessageFlags.Ephemeral, - }); } + return i.reply({ + content: `Your request was flagged by Aethel for ${category}. You have ${strike_count}/5 strikes. For more information, visit https://aethel.xyz/legal/terms`, + flags: MessageFlags.Ephemeral, + }); } } } @@ -164,11 +163,13 @@ export default class InteractionCreateEvent { ? await this.client.getLocaleText('reddit.from', i.locale, { subreddit: data.subreddit, }) - : '' - ) + : '', + ), ) .addMediaGalleryComponents( - new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(data.url)) + new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL(data.url), + ), ) .addActionRowComponents( new ActionRowBuilder().addComponents( @@ -176,8 +177,8 @@ export default class InteractionCreateEvent { .setStyle(ButtonStyle.Danger) .setLabel(refreshLabel) .setEmoji({ name: '🐱' }) - .setCustomId('refresh_cat') - ) + .setCustomId('refresh_cat'), + ), ); await i.update({ @@ -187,8 +188,8 @@ export default class InteractionCreateEvent { } else { const container = new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( - await this.client.getLocaleText('commands.cat.error', i.locale) - ) + await this.client.getLocaleText('commands.cat.error', i.locale), + ), ); await i.update({ @@ -200,8 +201,8 @@ export default class InteractionCreateEvent { logger.error('Error refreshing cat image:', error); const container = new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( - await this.client.getLocaleText('commands.cat.error', i.locale) - ) + await this.client.getLocaleText('commands.cat.error', i.locale), + ), ); await i.update({ @@ -224,7 +225,7 @@ export default class InteractionCreateEvent { for (const cmd of this.client.commands.values()) { const ClientApplicationCommandCache = this.client.application?.commands.cache.find( - (command) => command.name == cmd.data.name + (command) => command.name === cmd.data.name, ); const category = cmd.category || 'Uncategorized'; if (!commandCategories.has(category)) { @@ -233,35 +234,35 @@ export default class InteractionCreateEvent { const localizedDescription = await this.client.getLocaleText( `commands.${cmd.data.name}.description`, - i.locale + i.locale, ); commandCategories .get(category)! .push( - ` - ${localizedDescription}` + ` - ${localizedDescription}`, ); } const container = new ContainerBuilder() .setAccentColor(0x5865f2) .addTextDisplayComponents( - new TextDisplayBuilder().setContent('# 📋 **Available Commands**') + new TextDisplayBuilder().setContent('# 📋 **Available Commands**'), ); for (const [category, cmds] of commandCategories.entries()) { const localizedCategory = await this.client.getLocaleText( `categories.${category}`, - i.locale + i.locale, ); container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(`\n## 📂 ${localizedCategory}`) + new TextDisplayBuilder().setContent(`\n## 📂 ${localizedCategory}`), ); container.addTextDisplayComponents( new TextDisplayBuilder().setContent( - cmds.map((line) => line.replace(/\u007F/g, '')).join('\n') - ) + cmds.map((line) => line.replace(/\u007F/g, '')).join('\n'), + ), ); } @@ -273,8 +274,8 @@ export default class InteractionCreateEvent { .setStyle(ButtonStyle.Secondary) .setLabel(backLabel) .setEmoji({ name: '⬅️' }) - .setCustomId(`help_back_${i.user.id}`) - ) + .setCustomId(`help_back_${i.user.id}`), + ), ); await i.update({ @@ -317,29 +318,29 @@ export default class InteractionCreateEvent { .addMediaGalleryComponents( new MediaGalleryBuilder().addItems( - new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png') - ) + new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png'), + ), ) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`) + new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`), ) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(description || 'Get information about Aethel') + new TextDisplayBuilder().setContent(description || 'Get information about Aethel'), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - `\n## **${linksSocialText || 'Links & Social Media'}**` - ) + `\n## **${linksSocialText || 'Links & Social Media'}**`, + ), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - '[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)' - ) + '[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)', + ), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addTextDisplayComponents( - new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`) + new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( @@ -348,17 +349,17 @@ export default class InteractionCreateEvent { '**AI Integration** - Powered by OpenAI and other providers\n' + '**Reminders** - Never forget important tasks\n' + '**Utilities** - Weather, help, and productivity tools\n' + - '**Multi-language** - Supports multiple languages' - ) + '**Multi-language** - Supports multiple languages', + ), ) .addSeparatorComponents( - new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true) + new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true), ) .addTextDisplayComponents( new TextDisplayBuilder().setContent( - `-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}` - ) + `-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`, + ), ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large)) .addActionRowComponents( @@ -370,8 +371,8 @@ export default class InteractionCreateEvent { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(supportServerText || 'Support') - .setURL('https://discord.gg/63stE8pEaK') - ) + .setURL('https://discord.gg/63stE8pEaK'), + ), ); await i.update({ @@ -386,8 +387,8 @@ export default class InteractionCreateEvent { if (!response.ok) { const container = new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( - await this.client.getLocaleText('commands.dog.error', i.locale) - ) + await this.client.getLocaleText('commands.dog.error', i.locale), + ), ); return await i.update({ @@ -428,13 +429,13 @@ export default class InteractionCreateEvent { ? await this.client.getLocaleText('reddit.from', i.locale, { subreddit: data!.subreddit, }) - : '' - ) + : '', + ), ) .addMediaGalleryComponents( new MediaGalleryBuilder().addItems( - new MediaGalleryItemBuilder().setURL(data!.url) - ) + new MediaGalleryItemBuilder().setURL(data!.url), + ), ) .addActionRowComponents( new ActionRowBuilder().addComponents( @@ -442,8 +443,8 @@ export default class InteractionCreateEvent { .setStyle(ButtonStyle.Secondary) .setLabel(refreshLabel) .setEmoji({ name: '🐶' }) - .setCustomId('refresh_dog') - ) + .setCustomId('refresh_dog'), + ), ); await i.update({ @@ -453,8 +454,8 @@ export default class InteractionCreateEvent { } else { const container = new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( - await this.client.getLocaleText('commands.dog.error', i.locale) - ) + await this.client.getLocaleText('commands.dog.error', i.locale), + ), ); await i.update({ @@ -466,8 +467,8 @@ export default class InteractionCreateEvent { logger.error('Error refreshing dog image:', error); const container = new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( - await this.client.getLocaleText('commands.dog.error', i.locale) - ) + await this.client.getLocaleText('commands.dog.error', i.locale), + ), ); await i.update({ @@ -483,7 +484,6 @@ export default class InteractionCreateEvent { components: [], }); } - return; } }; } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 3c91019..e4a0437 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -46,7 +46,7 @@ export default class MessageCreateEvent { if (!isDM && !isMentioned) { logger.debug( - `Ignoring message - not a DM and bot not mentioned (channel type: ${message.channel.type})` + `Ignoring message - not a DM and bot not mentioned (channel type: ${message.channel.type})`, ); return; } @@ -63,7 +63,7 @@ export default class MessageCreateEvent { const hasImageAttachments = message.attachments.some( (att) => att.contentType?.startsWith('image/') || - att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i) + att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i), ); const hasImageUrls = false; @@ -87,14 +87,14 @@ export default class MessageCreateEvent { : userCustomModel || 'moonshotai/kimi-k2'; logger.info( - `Using model: ${selectedModel} for message with images: ${hasImages}${userCustomModel ? ' (user custom model)' : ' (default model)'}` + `Using model: ${selectedModel} for message with images: ${hasImages}${userCustomModel ? ' (user custom model)' : ' (default model)'}`, ); const systemPrompt = buildSystemPrompt( isDM, this.client, selectedModel, - message.author.username + message.author.username, ); let messageContent: @@ -112,7 +112,7 @@ export default class MessageCreateEvent { const imageAttachments = message.attachments.filter( (att) => att.contentType?.startsWith('image/') || - att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i) + att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i), ); const contentArray: Array<{ @@ -164,11 +164,11 @@ export default class MessageCreateEvent { const updatedConversation = buildConversation( filteredConversation, messageContent, - systemPrompt + systemPrompt, ); const { apiKey: userApiKey, apiUrl: userApiUrl } = await getUserCredentials( - message.author.id + message.author.id, ); const config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null); @@ -178,7 +178,7 @@ export default class MessageCreateEvent { const allowed = await incrementAndCheckDailyLimit(message.author.id, 10); if (!allowed) { await message.reply( - "❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command." + "❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.", ); return; } @@ -231,13 +231,13 @@ export default class MessageCreateEvent { const fallbackConversation = buildConversation( cleanedConversation, fallbackContent, - buildSystemPrompt(isDM, this.client, fallbackModel, message.author.username) + buildSystemPrompt(isDM, this.client, fallbackModel, message.author.username), ); const fallbackConfig = getApiConfiguration( userApiKey ?? null, fallbackModel, - userApiUrl ?? null + userApiUrl ?? null, ); aiResponse = await makeAIRequest(fallbackConfig, fallbackConversation); @@ -248,7 +248,7 @@ export default class MessageCreateEvent { if (!aiResponse) { await message.reply( - 'Sorry, I encountered an error processing your message. Please try again later.' + 'Sorry, I encountered an error processing your message. Please try again later.', ); return; } @@ -267,11 +267,11 @@ export default class MessageCreateEvent { } catch (error) { logger.error( `Error processing ${isDM ? 'DM' : 'server message'}:`, - error instanceof Error ? error.message : String(error) + error instanceof Error ? error.message : String(error), ); try { await message.reply( - 'Sorry, I encountered an error processing your message. Please try again later.' + 'Sorry, I encountered an error processing your message. Please try again later.', ); } catch (replyError) { logger.error('Failed to send error message:', replyError); diff --git a/src/handlers/initialzeCommands.ts b/src/handlers/initialzeCommands.ts index fe4581f..3c99809 100644 --- a/src/handlers/initialzeCommands.ts +++ b/src/handlers/initialzeCommands.ts @@ -23,7 +23,7 @@ export default async (c: BotClient) => { const commands: (SlashCommandBuilder | ContextMenuCommandBuilder)[] = []; for (const cat of cmdCat) { const commandFiles = readdirSync(path.join(cmdDir, cat)).filter( - (f) => f.endsWith('.js') || f.endsWith('.ts') + (f) => f.endsWith('.js') || f.endsWith('.ts'), ); for (const file of commandFiles) { const commandPath = path.join(cmdDir, cat, file); diff --git a/src/index.ts b/src/index.ts index e45c721..3de2379 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ app.use( allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization', 'Cache-Control', 'Pragma'], credentials: true, maxAge: 86400, - }) + }), ); app.set('trust proxy', 1); app.use( @@ -47,7 +47,7 @@ app.use( message: { error: 'Too many requests, please try again later.' }, standardHeaders: true, legacyHeaders: false, - }) + }), ); app.use(e.json({ limit: '10mb' })); @@ -65,7 +65,7 @@ app.use((req, res, next) => { } else { res.setHeader( 'Content-Security-Policy', - "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:" + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:", ); } next(); @@ -94,7 +94,7 @@ setInterval( () => { resetOldStrikes().catch(console.error); }, - 60 * 60 * 1000 + 60 * 60 * 1000, ); app.listen(PORT, () => { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 5a94e84..b1fe337 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -32,10 +32,9 @@ export const authenticateToken = (req: Request, res: Response, next: NextFunctio } else if (error instanceof jwt.JsonWebTokenError) { logger.debug('Invalid JWT token used'); return res.status(401).json({ error: 'Invalid token' }); - } else { - logger.error('JWT verification error:', error); - return res.status(500).json({ error: 'Token verification failed' }); } + logger.error('JWT verification error:', error); + return res.status(500).json({ error: 'Token verification failed' }); } }; diff --git a/src/middlewares/verifyApiKey.ts b/src/middlewares/verifyApiKey.ts index e425546..24c5089 100644 --- a/src/middlewares/verifyApiKey.ts +++ b/src/middlewares/verifyApiKey.ts @@ -3,7 +3,7 @@ import { RequestHandler } from 'express'; const authenticateApiKey: RequestHandler = (req, res, next) => { const apiKey = req.headers['x-api-key']; - if (!apiKey || typeof apiKey != 'string') { + if (!apiKey || typeof apiKey !== 'string') { res.status(401).json({ error: 'Unauthorized: Missing API key' }); return; } diff --git a/src/routes/apiKeys.ts b/src/routes/apiKeys.ts index 22da236..14a0d97 100644 --- a/src/routes/apiKeys.ts +++ b/src/routes/apiKeys.ts @@ -119,7 +119,7 @@ router.post( logger.error('Error updating API key:', error); res.status(500).json({ error: 'Internal server error' }); } - } + }, ); router.put( @@ -179,7 +179,7 @@ router.put( logger.error('Error updating API key:', error); res.status(500).json({ error: 'Internal server error' }); } - } + }, ); router.delete('/', async (req, res) => { @@ -293,7 +293,7 @@ router.post( res.status(500).json({ error: 'API key test failed due to server error' }); } - } + }, ); export default router; diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 0b38117..e30bbff 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -80,7 +80,7 @@ router.get('/discord/callback', async (req, res) => { 'INSERT INTO users (user_id, language, created_at) VALUES ($1, $2, NOW())'; await pool.query(insertQuery, [discordUser.id, 'en']); logger.info( - `New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})` + `New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})`, ); } @@ -92,7 +92,7 @@ router.get('/discord/callback', async (req, res) => { avatar: discordUser.avatar, }, JWT_SECRET, - { expiresIn: '7d' } + { expiresIn: '7d' }, ); const redirectUrl = new URL(`${FRONTEND_URL}/login`); diff --git a/src/routes/reminders.ts b/src/routes/reminders.ts index 2bd2708..67fd7b4 100644 --- a/src/routes/reminders.ts +++ b/src/routes/reminders.ts @@ -74,7 +74,7 @@ router.post('/', authenticateToken, async (req: Request, res: Response) => { if (scheduled) { logger.info( - `Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard` + `Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard`, ); } else { logger.warn(`Failed to schedule reminder ${savedReminder.reminder_id} from dashboard`); @@ -145,7 +145,7 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo if (user) { const minutes = Math.floor( (new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) / - (60 * 1000) + (60 * 1000), ); const reminderTitle = @@ -153,7 +153,7 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo const reminderDesc = await client.getLocaleText( 'commands.remind.remindyou', reminder.locale, - { message: reminder.message } + { message: reminder.message }, ); const timeElapsedText = @@ -171,7 +171,7 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo name: originalTimeText, value: ``, inline: true, - } + }, ) .setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` }) .setTimestamp(); @@ -179,11 +179,11 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo if (reminder.metadata?.message_url) { const originalMessageText = await client.getLocaleText( 'common.ogmessage', - reminder.locale + reminder.locale, ); const jumpToMessageText = await client.getLocaleText( 'common.jumptomessage', - reminder.locale + reminder.locale, ); reminderEmbed.addFields({ @@ -196,7 +196,7 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo if (reminder.message.includes('http') && !reminder.metadata?.message_url) { const messageLinkText = await client.getLocaleText( 'common.messagelink', - reminder.locale + reminder.locale, ); reminderEmbed.addFields({ name: messageLinkText, @@ -220,7 +220,7 @@ router.patch('/:id/complete', authenticateToken, async (req: Request, res: Respo { error: notificationError, reminderId: reminder.reminder_id, - } + }, ); } diff --git a/src/routes/todos.ts b/src/routes/todos.ts index 3d0a96e..d6c4f97 100644 --- a/src/routes/todos.ts +++ b/src/routes/todos.ts @@ -63,7 +63,7 @@ router.post( logger.error('Error creating todo:', error); res.status(500).json({ error: 'Internal server error' }); } - } + }, ); router.put( @@ -105,7 +105,7 @@ router.put( logger.error('Error updating todo:', error); res.status(500).json({ error: 'Internal server error' }); } - } + }, ); router.delete('/:id', async (req, res) => { diff --git a/src/utils/commandLogger.ts b/src/utils/commandLogger.ts index 75140c9..ea55c7e 100644 --- a/src/utils/commandLogger.ts +++ b/src/utils/commandLogger.ts @@ -3,24 +3,20 @@ import { CommandInteraction } from 'discord.js'; export interface CommandLogOptions { commandName: string; - userId: string; - username: string; additionalInfo?: string; - guildId?: string; - channelId?: string; + isGuild?: boolean; + isDM?: boolean; } export function logUserAction(options: CommandLogOptions): void { - const { commandName, userId, username, guildId, channelId, additionalInfo } = options; + const { commandName, isGuild, isDM, additionalInfo } = options; - let logMessage = `User ${username} (${userId}) used ${commandName} command`; + let logMessage = `User executed ${commandName} command`; - if (guildId) { - logMessage += ` in guild ${guildId}`; - } - - if (channelId) { - logMessage += ` in channel ${channelId}`; + if (isGuild) { + logMessage += ` in guild`; + } else if (isDM) { + logMessage += ` in DM`; } if (additionalInfo) { @@ -33,14 +29,12 @@ export function logUserAction(options: CommandLogOptions): void { export function logUserActionFromInteraction( interaction: CommandInteraction, commandName: string, - additionalInfo?: string + additionalInfo?: string, ): void { logUserAction({ commandName, - userId: interaction.user.id, - username: interaction.user.tag, - guildId: interaction.guildId || undefined, - channelId: interaction.channelId, + isGuild: !!interaction.guildId, + isDM: !interaction.guildId, additionalInfo, }); } diff --git a/src/utils/cooldown.ts b/src/utils/cooldown.ts index 74fd5b2..5c27c91 100644 --- a/src/utils/cooldown.ts +++ b/src/utils/cooldown.ts @@ -19,7 +19,7 @@ setInterval( } } }, - 5 * 60 * 1000 + 5 * 60 * 1000, ); export function createCooldownManager(commandName: string, cooldownTime: number): CooldownManager { @@ -35,7 +35,7 @@ export async function checkCooldown( manager: CooldownManager, userId: string, client: BotClient, - locale: string + locale: string, ): Promise<{ onCooldown: boolean; timeLeft?: number; message?: string }> { const now = Date.now(); const cooldownEnd = manager.cooldowns.get(userId) || 0; diff --git a/src/utils/dynamicFetch.ts b/src/utils/dynamicFetch.ts index 73699ec..e203253 100644 --- a/src/utils/dynamicFetch.ts +++ b/src/utils/dynamicFetch.ts @@ -11,7 +11,7 @@ class FetchError extends Error { message: string, public readonly status?: number, public readonly statusText?: string, - public readonly url?: string + public readonly url?: string, ) { super(message); this.name = 'FetchError'; @@ -70,7 +70,7 @@ const fetch = async (url: string | URL, init?: RequestInit & FetchOptions): Prom `Request timeout after ${timeout}ms`, undefined, undefined, - url.toString() + url.toString(), ); } @@ -87,7 +87,7 @@ const fetch = async (url: string | URL, init?: RequestInit & FetchOptions): Prom `Client error: ${error.message}`, error.status, 'statusText' in error ? (error.statusText as string) : undefined, - url.toString() + url.toString(), ); } } @@ -117,7 +117,7 @@ const fetch = async (url: string | URL, init?: RequestInit & FetchOptions): Prom `Request failed after ${retries + 1} attempts: ${lastError?.message || 'Unknown error'}`, undefined, undefined, - url.toString() + url.toString(), ); }; diff --git a/src/utils/encrypt.ts b/src/utils/encrypt.ts index 47fe22f..20e9ff1 100644 --- a/src/utils/encrypt.ts +++ b/src/utils/encrypt.ts @@ -10,7 +10,7 @@ const MAX_ENCRYPTED_LENGTH = 10000; class EncryptionError extends Error { constructor( message: string, - public readonly operation: 'encrypt' | 'decrypt' + public readonly operation: 'encrypt' | 'decrypt', ) { super(message); this.name = 'EncryptionError'; @@ -54,7 +54,7 @@ function decrypt(encrypted: string): string { if (!encrypted || typeof encrypted !== 'string') { throw new EncryptionError( 'Invalid input: encrypted data must be a non-empty string', - 'decrypt' + 'decrypt', ); } @@ -86,14 +86,14 @@ function decrypt(encrypted: string): string { if (iv.length !== IV_LENGTH) { throw new EncryptionError( `Invalid IV length: expected ${IV_LENGTH}, got ${iv.length}`, - 'decrypt' + 'decrypt', ); } if (tag.length !== 16) { throw new EncryptionError( `Invalid auth tag length: expected 16, got ${tag.length}`, - 'decrypt' + 'decrypt', ); } @@ -113,11 +113,11 @@ function decrypt(encrypted: string): string { if (error instanceof Error) { if (error.message.includes('Unsupported state or unable to authenticate data')) { logger.warn( - 'Authentication failed during decryption - data may be corrupted or key changed' + 'Authentication failed during decryption - data may be corrupted or key changed', ); throw new EncryptionError( 'Authentication failed - data may be corrupted or encryption key changed', - 'decrypt' + 'decrypt', ); } if (error.message.includes('Invalid key length')) { diff --git a/src/utils/getGitCommitHash.ts b/src/utils/getGitCommitHash.ts index 0202d3d..8bc3b3f 100644 --- a/src/utils/getGitCommitHash.ts +++ b/src/utils/getGitCommitHash.ts @@ -39,6 +39,8 @@ function getGitCommitHash(): string { return cachedCommitHash || getGitCommitHashSync(); } -initializeGitCommitHash().catch(() => {}); +initializeGitCommitHash().catch((error) => { + console.warn('Failed to initialize git commit hash:', error.message); +}); export default getGitCommitHash; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 9982277..6addf78 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -3,7 +3,21 @@ import { LOG_LEVEL, NODE_ENV } from '@/config/index'; import path from 'path'; const sanitizeFormat = winston.format((info) => { - const sensitiveKeys = ['password', 'token', 'api_key', 'secret', 'authorization']; + const sensitiveKeys = [ + 'password', + 'token', + 'api_key', + 'secret', + 'authorization', + 'userid', + 'user_id', + 'username', + 'user_tag', + 'guildid', + 'guild_id', + 'channelid', + 'channel_id', + ]; const sanitize = (obj: unknown): unknown => { if (typeof obj !== 'object' || obj === null) return obj; @@ -12,6 +26,8 @@ const sanitizeFormat = winston.format((info) => { for (const key in sanitized) { if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) { sanitized[key] = '[REDACTED]'; + } else if (typeof sanitized[key] === 'string') { + sanitized[key] = (sanitized[key] as string).replace(/\b\d{17,19}\b/g, '[ID_REDACTED]'); } else if (typeof sanitized[key] === 'object') { sanitized[key] = sanitize(sanitized[key]); } @@ -31,7 +47,7 @@ const logger = winston.createLogger({ winston.format.errors({ stack: true }), sanitizeFormat(), winston.format.splat(), - winston.format.json() + winston.format.json(), ), defaultMeta: { service: 'Aethel', @@ -45,7 +61,7 @@ const logger = winston.createLogger({ winston.format.printf(({ timestamp, level, message, service, ...meta }) => { const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''; return `${timestamp} [${service}] ${level}: ${message} ${metaStr}`; - }) + }), ), }), ], @@ -62,7 +78,7 @@ if (NODE_ENV === 'production') { maxsize: 5242880, maxFiles: 5, format: winston.format.combine(winston.format.timestamp(), winston.format.json()), - }) + }), ); logger.add( @@ -71,20 +87,20 @@ if (NODE_ENV === 'production') { maxsize: 5242880, maxFiles: 5, format: winston.format.combine(winston.format.timestamp(), winston.format.json()), - }) + }), ); } logger.exceptions.handle( new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.simple()), - }) + }), ); logger.rejections.handle( new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.simple()), - }) + }), ); export default logger; diff --git a/src/utils/memoryManager.ts b/src/utils/memoryManager.ts index 7852e4f..308c1b6 100644 --- a/src/utils/memoryManager.ts +++ b/src/utils/memoryManager.ts @@ -47,10 +47,9 @@ class MemoryManager { const result = [...entry]; delete (result as unknown as Record).timestamp; return result as V; - } else { - const { timestamp: _timestamp, ...valueWithoutTimestamp } = entry; - return valueWithoutTimestamp as V; } + const { timestamp: _timestamp, ...valueWithoutTimestamp } = entry; + return valueWithoutTimestamp as V; } delete(key: K): boolean { diff --git a/src/utils/reminderDb.ts b/src/utils/reminderDb.ts index 72c0b5d..51b7c0e 100644 --- a/src/utils/reminderDb.ts +++ b/src/utils/reminderDb.ts @@ -45,13 +45,13 @@ function createDatabaseError(error: unknown, operation: string): DatabaseError { return new DatabaseError( errorMessage, 'Unable to connect to the database. Please try again later.', - true + true, ); case '42703': return new DatabaseError( errorMessage, 'Database schema error. Please contact support.', - true + true, ); case '23505': return new DatabaseError(errorMessage, 'This reminder already exists.', true); @@ -59,7 +59,7 @@ function createDatabaseError(error: unknown, operation: string): DatabaseError { return new DatabaseError( errorMessage, 'Missing required information. Please try again.', - true + true, ); case '23503': return new DatabaseError(errorMessage, 'Invalid reference. Please try again.', true); @@ -67,7 +67,7 @@ function createDatabaseError(error: unknown, operation: string): DatabaseError { return new DatabaseError( errorMessage, 'A database error occurred. Please try again later.', - false + false, ); } } @@ -75,7 +75,7 @@ function createDatabaseError(error: unknown, operation: string): DatabaseError { async function ensureUserRegistered( userId: string, userTag: string, - language: string = 'en' + language = 'en', ): Promise { const query = ` SELECT ensure_user_registered($1, $2, $3) @@ -92,7 +92,7 @@ async function saveReminder(reminderData: ReminderData): Promise { await ensureUserRegistered( reminderData.user_id, reminderData.user_tag, - reminderData.locale || 'en' + reminderData.locale || 'en', ); const query = ` diff --git a/src/utils/userStrikes.ts b/src/utils/userStrikes.ts index 582ef11..d3ebe66 100644 --- a/src/utils/userStrikes.ts +++ b/src/utils/userStrikes.ts @@ -9,7 +9,7 @@ export interface StrikeInfo { export class StrikeError extends Error { constructor( message: string, - public readonly userId?: string + public readonly userId?: string, ) { super(message); this.name = 'StrikeError'; @@ -24,7 +24,7 @@ export async function getUserStrikeInfo(userId: string): Promise { last_strike_at = NOW() RETURNING strike_count, banned_until; `, - [userId] + [userId], ); if (res.rows.length === 0) { @@ -137,7 +137,7 @@ export async function resetOldStrikes(): Promise { `UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE last_strike_at < NOW() - INTERVAL '3 days' - RETURNING user_id` + RETURNING user_id`, ); const resetCount = res.rows.length; @@ -161,7 +161,7 @@ export async function clearUserStrikes(userId: string): Promise { try { const res = await pgClient.query( 'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1', - [userId] + [userId], ); logger.info('Cleared user strikes', { userId }); diff --git a/src/utils/validation.ts b/src/utils/validation.ts index e737c6e..46d1f37 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -9,7 +9,7 @@ interface ValidationResult { function validateCommandOptions( interaction: ChatInputCommandInteraction, - requiredOptions: string[] = [] + requiredOptions: string[] = [], ): ValidationResult { for (const option of requiredOptions) { const value = interaction.options.getString(option); diff --git a/web/src/App.tsx b/web/src/App.tsx index 28c1c8d..565af48 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -22,14 +22,35 @@ function App() { return ( {/* Public routes */} - } /> + } + /> : } + element={ + isAuthenticated ? ( + + ) : ( + + ) + } + /> + } + /> + } + /> + } /> - } /> - } /> - } /> {/* Protected routes */} {isAuthenticated ? ( @@ -38,17 +59,45 @@ function App() { element={ - } /> - } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + /> + + } + /> } /> ) : ( - } /> + + } + /> )} ); diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 4d8224b..e6437f8 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -33,7 +33,10 @@ const Layout = ({ children }: LayoutProps) => { mobileMenuOpen ? 'opacity-100' : 'opacity-0 pointer-events-none' }`} > -
setMobileMenuOpen(false)} /> +
setMobileMenuOpen(false)} + />
- + { return Promise.reject(error); - } + }, ); api.interceptors.response.use( @@ -34,7 +34,7 @@ api.interceptors.response.use( toast.error('An unexpected error occurred.'); } return Promise.reject(error); - } + }, ); export default api; diff --git a/web/src/main.tsx b/web/src/main.tsx index 69dd34d..8e46a6d 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -23,5 +23,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - + , ); diff --git a/web/src/pages/ApiKeysPage.tsx b/web/src/pages/ApiKeysPage.tsx index ae72911..ee26bb5 100644 --- a/web/src/pages/ApiKeysPage.tsx +++ b/web/src/pages/ApiKeysPage.tsx @@ -207,7 +207,10 @@ const ApiKeysPage = () => { {apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key -
+

* You must test the API key diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx index ceeb202..4e05498 100644 --- a/web/src/pages/DashboardPage.tsx +++ b/web/src/pages/DashboardPage.tsx @@ -65,7 +65,7 @@ const DashboardPage = () => { const recentTodos = activeTodosForDisplay.slice(0, 5); const activeRemindersForDisplay = reminders?.filter( - (reminder: Reminder) => !reminder.completed && new Date(reminder.expires_at) >= new Date() + (reminder: Reminder) => !reminder.completed && new Date(reminder.expires_at) >= new Date(), ) || []; const recentReminders = activeRemindersForDisplay.slice(0, 5); @@ -194,7 +194,10 @@ const DashboardPage = () => { {recentTodos.length > 0 ? (

{recentTodos.slice(0, 5).map((todo: Todo) => ( -
+
{ const daysOverdue = isOverdue ? Math.ceil( (Date.now() - new Date(reminder.expires_at).getTime()) / - (1000 * 60 * 60 * 24) + (1000 * 60 * 60 * 24), ) : 0; return ( -
+
-
- + Privacy Policy - + Terms of Service
@@ -203,7 +214,11 @@ export default function Home() { rel="noopener noreferrer" className="inline-block hover:opacity-80 transition-opacity" > - Royale Hosting + Royale Hosting Royale Hosting { onClick={handleDiscordLogin} className="w-full flex items-center justify-center px-8 py-3 border border-transparent rounded-full shadow-sm text-white bg-[#5865F2] hover:bg-[#4752c4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-all transform hover:scale-105 font-bold shadow-lg hover:shadow-xl" > - + Continue with Discord diff --git a/web/src/pages/PrivacyPage.tsx b/web/src/pages/PrivacyPage.tsx index 1121fe7..385d042 100644 --- a/web/src/pages/PrivacyPage.tsx +++ b/web/src/pages/PrivacyPage.tsx @@ -2,7 +2,10 @@ import LegalLayout from '../components/LegalLayout'; export default function PrivacyPolicy() { return ( - +

diff --git a/web/src/pages/RemindersPage.tsx b/web/src/pages/RemindersPage.tsx index 15645a9..b56e648 100644 --- a/web/src/pages/RemindersPage.tsx +++ b/web/src/pages/RemindersPage.tsx @@ -150,7 +150,10 @@ const RemindersPage: React.FC = () => {

Create New Reminder

-
+