+29
-2
.prettierrc
+29
-2
.prettierrc
···
3
3
"singleQuote": true,
4
4
"tabWidth": 2,
5
5
"useTabs": false,
6
-
"trailingComma": "es5",
6
+
"trailingComma": "all",
7
7
"bracketSpacing": true,
8
+
"bracketSameLine": false,
8
9
"arrowParens": "always",
9
10
"printWidth": 100,
10
-
"endOfLine": "auto"
11
+
"endOfLine": "lf",
12
+
"quoteProps": "as-needed",
13
+
"singleAttributePerLine": true,
14
+
"embeddedLanguageFormatting": "auto",
15
+
"experimentalTernaries": false,
16
+
"overrides": [
17
+
{
18
+
"files": ["*.md", "*.mdx"],
19
+
"options": {
20
+
"printWidth": 80,
21
+
"proseWrap": "always"
22
+
}
23
+
},
24
+
{
25
+
"files": ["*.json", "*.jsonc"],
26
+
"options": {
27
+
"trailingComma": "none"
28
+
}
29
+
},
30
+
{
31
+
"files": ["*.yml", "*.yaml"],
32
+
"options": {
33
+
"tabWidth": 2,
34
+
"singleQuote": false
35
+
}
36
+
}
37
+
]
11
38
}
+4
-2
CODE_OF_CONDUCT.md
+4
-2
CODE_OF_CONDUCT.md
···
2
2
3
3
## Our Pledge
4
4
5
-
We as members, contributors, and leaders pledge to make participation in our project and our community a harassment-free experience for everyone.
5
+
We as members, contributors, and leaders pledge to make participation in our
6
+
project and our community a harassment-free experience for everyone.
6
7
7
8
## Our Standards
8
9
···
21
22
22
23
## Enforcement
23
24
24
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer at scan@scanash.com.
25
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
26
+
reported by contacting the maintainer at scan@scanash.com.
+20
-17
README.md
+20
-17
README.md
···
4
4
5
5
[](https://nodejs.org/)
6
6
7
-
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.
7
+
A privacy-conscious, production-ready Discord user-installed bot with AI chat,
8
+
reminders, and utility commands. Built with Node.js, Discord.js v14, PostgreSQL,
9
+
and robust security best practices.
8
10
9
11
---
10
12
11
13
## Features
12
14
13
-
- **AI Chat**: `/ai` command with custom API key support (OpenRouter, OpenAI, Grok)
15
+
- **AI Chat**: `/ai` command with custom API key support (OpenRouter, OpenAI,
16
+
Grok)
14
17
- **Reminders**: `/remind` command for scheduling reminders
15
-
- **Utilities**: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, `/whois`
18
+
- **Utilities**: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`,
19
+
`/whois`
16
20
- **Ephemeral Replies** for sensitive commands
17
21
- **Encrypted API Key Storage** (AES-256-GCM)
18
22
- **Rate Limiting & Logging**
···
92
96
93
97
- Use `/ai` for AI chat, with optional custom API key for private usage
94
98
- Use `/remind` to schedule reminders
95
-
- Use utility commands: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`, `/whois`
99
+
- Use utility commands: `/weather`, `/wiki`, `/joke`, `/cat`, `/dog`, `/8ball`,
100
+
`/whois`
96
101
- Sensitive commands use ephemeral replies for privacy
97
102
- Use `/ai use_custom_api:true` to set your own (encrypted) API key
98
103
···
105
110
106
111
## 🌐 Translations & Localization
107
112
108
-
Aethel supports multiple languages! You can help improve or add new translations for the bot.
113
+
Aethel supports multiple languages! You can help improve or add new translations
114
+
for the bot.
109
115
110
116
### Supported Languages
111
117
112
-
- English (en-US)
113
-
<a href="http://translate.aethel.xyz/engage/aethel/en/">
118
+
- English (en-US) <a href="http://translate.aethel.xyz/engage/aethel/en/">
114
119
<img src="http://translate.aethel.xyz/widgets/aethel/en/svg-badge.svg" alt="English translation status" />
115
120
</a>
116
-
- Spanish (es-ES)
117
-
<a href="http://translate.aethel.xyz/engage/aethel/es/">
121
+
- Spanish (es-ES) <a href="http://translate.aethel.xyz/engage/aethel/es/">
118
122
<img src="http://translate.aethel.xyz/widgets/aethel/es/svg-badge.svg" alt="Spanish translation status" />
119
123
</a>
120
124
- Spanish (Latin America) (es-419)
121
125
<a href="http://translate.aethel.xyz/engage/aethel/es_419/">
122
126
<img src="http://translate.aethel.xyz/widgets/aethel/es_419/svg-badge.svg" alt="Spanish (Latin America) translation status" />
123
127
</a>
124
-
- German (de-DE)
125
-
<a href="http://translate.aethel.xyz/engage/aethel/de/">
128
+
- German (de-DE) <a href="http://translate.aethel.xyz/engage/aethel/de/">
126
129
<img src="http://translate.aethel.xyz/widgets/aethel/de/svg-badge.svg" alt="German translation status" />
127
130
</a>
128
-
- French (fr-FR)
129
-
<a href="http://translate.aethel.xyz/engage/aethel/fr/">
131
+
- French (fr-FR) <a href="http://translate.aethel.xyz/engage/aethel/fr/">
130
132
<img src="http://translate.aethel.xyz/widgets/aethel/fr/svg-badge.svg" alt="French translation status" />
131
133
</a>
132
134
- Portuguese (Brazil) (pt-BR)
133
135
<a href="http://translate.aethel.xyz/engage/aethel/pt_BR/">
134
136
<img src="http://translate.aethel.xyz/widgets/aethel/pt_BR/svg-badge.svg" alt="Portuguese (Brazil) translation status" />
135
137
</a>
136
-
- Japanese (ja)
137
-
<a href="http://translate.aethel.xyz/engage/aethel/ja/">
138
+
- Japanese (ja) <a href="http://translate.aethel.xyz/engage/aethel/ja/">
138
139
<img src="http://translate.aethel.xyz/widgets/aethel/ja/svg-badge.svg" alt="Japanese translation status" />
139
140
</a>
140
141
141
142
### Contribute a Translation
142
143
143
-
We use [Weblate](https://translate.aethel.xyz/projects/aethel/) for collaborative translation. Anyone can contribute:
144
+
We use [Weblate](https://translate.aethel.xyz/projects/aethel/) for
145
+
collaborative translation. Anyone can contribute:
144
146
145
-
- Visit the [Aethel Weblate project](https://translate.aethel.xyz/projects/aethel/)
147
+
- Visit the
148
+
[Aethel Weblate project](https://translate.aethel.xyz/projects/aethel/)
146
149
- Sign in or register (free)
147
150
- Pick your language and start translating or reviewing existing translations
148
151
+24
-12
SECURITY.md
+24
-12
SECURITY.md
···
11
11
12
12
## Reporting a Vulnerability
13
13
14
-
We take security vulnerabilities seriously. If you discover a security vulnerability in Aethel, please report it responsibly.
14
+
We take security vulnerabilities seriously. If you discover a security
15
+
vulnerability in Aethel, please report it responsibly.
15
16
16
17
### How to Report
17
18
···
25
26
### What to Expect
26
27
27
28
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
28
-
- **Initial Assessment**: We will provide an initial assessment within 5 business days
29
-
- **Updates**: We will keep you informed of our progress throughout the investigation
29
+
- **Initial Assessment**: We will provide an initial assessment within 5
30
+
business days
31
+
- **Updates**: We will keep you informed of our progress throughout the
32
+
investigation
30
33
- **Resolution**: We aim to resolve critical vulnerabilities within 30 days
31
34
32
35
### Responsible Disclosure
···
35
38
36
39
- We will work with you to understand and resolve the issue
37
40
- We will credit you for the discovery (unless you prefer to remain anonymous)
38
-
- We ask that you do not publicly disclose the vulnerability until we have had a chance to address it
41
+
- We ask that you do not publicly disclose the vulnerability until we have had a
42
+
chance to address it
39
43
40
44
## Security Measures
41
45
42
46
### Current Security Implementations
43
47
44
-
- **SSRF Protection**: API endpoints are restricted to whitelisted hosts to prevent Server-Side Request Forgery attacks
48
+
- **SSRF Protection**: API endpoints are restricted to whitelisted hosts to
49
+
prevent Server-Side Request Forgery attacks
45
50
- **Input Validation**: All user inputs are validated and sanitized
46
51
- **Encryption**: Sensitive data like API keys are encrypted before storage
47
52
- **Authentication**: Secure token-based authentication for API access
···
49
54
50
55
### Allowed API Hosts
51
56
52
-
For security reasons, custom API endpoints are restricted to the following trusted hosts:
57
+
For security reasons, custom API endpoints are restricted to the following
58
+
trusted hosts:
53
59
54
60
- `api.openai.com`
55
61
- `openrouter.ai`
···
59
65
60
66
When contributing to or using Aethel:
61
67
62
-
1. **Never commit secrets**: Do not include API keys, passwords, or other sensitive information in code
63
-
2. **Use environment variables**: Store sensitive configuration in environment variables
68
+
1. **Never commit secrets**: Do not include API keys, passwords, or other
69
+
sensitive information in code
70
+
2. **Use environment variables**: Store sensitive configuration in environment
71
+
variables
64
72
3. **Validate inputs**: Always validate and sanitize user inputs
65
73
4. **Follow least privilege**: Grant minimal necessary permissions
66
-
5. **Keep dependencies updated**: Regularly update dependencies to patch known vulnerabilities
74
+
5. **Keep dependencies updated**: Regularly update dependencies to patch known
75
+
vulnerabilities
67
76
68
77
## Security Audits
69
78
70
-
We regularly review our codebase for security vulnerabilities and welcome security audits from the community.
79
+
We regularly review our codebase for security vulnerabilities and welcome
80
+
security audits from the community.
71
81
72
82
### Automated Security Checks
73
83
···
77
87
78
88
## Contact
79
89
80
-
For security-related questions or concerns, please contact the project maintainers at scan@scanash.com
90
+
For security-related questions or concerns, please contact the project
91
+
maintainers at scan@scanash.com
81
92
82
93
---
83
94
84
-
**Note**: This security policy is subject to change. Please check back regularly for updates.
95
+
**Note**: This security policy is subject to change. Please check back regularly
96
+
for updates.
+92
-24
eslint.config.mjs
+92
-24
eslint.config.mjs
···
1
-
import js from "@eslint/js";
2
-
import globals from "globals";
3
-
import tseslint from "typescript-eslint";
4
-
import { defineConfig } from "eslint/config";
1
+
import js from '@eslint/js';
2
+
import globals from 'globals';
3
+
import tseslint from 'typescript-eslint';
5
4
6
-
7
-
export default defineConfig([
8
-
{
9
-
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
10
-
plugins: { js },
11
-
extends: ["js/recommended"],
12
-
ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"]
5
+
export default [
6
+
{
7
+
ignores: [
8
+
'**/node_modules/**',
9
+
'**/dist/**',
10
+
'**/build/**',
11
+
'**/.next/**',
12
+
'**/coverage/**',
13
+
'**/*.min.js',
14
+
'**/logs/**',
15
+
],
13
16
},
14
-
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } },
17
+
18
+
js.configs.recommended,
15
19
...tseslint.configs.recommended,
20
+
16
21
{
17
-
files: ["**/*.{ts,tsx}"],
18
-
ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"],
22
+
files: ['**/*.{js,mjs,cjs,ts,mts,cts,tsx}'],
23
+
languageOptions: {
24
+
globals: {
25
+
...globals.node,
26
+
...globals.es2022,
27
+
},
28
+
ecmaVersion: 2022,
29
+
sourceType: 'module',
30
+
},
19
31
rules: {
20
-
"@typescript-eslint/no-unused-vars": [
21
-
"error",
32
+
'no-console': 'off',
33
+
'no-debugger': 'error',
34
+
'no-alert': 'error',
35
+
'no-eval': 'error',
36
+
'no-implied-eval': 'error',
37
+
38
+
eqeqeq: ['error', 'always'],
39
+
curly: ['warn', 'multi-line'],
40
+
'no-else-return': 'warn',
41
+
'no-empty-function': 'warn',
42
+
'no-return-assign': 'error',
43
+
'no-throw-literal': 'error',
44
+
45
+
'no-unreachable': 'error',
46
+
'no-unused-expressions': 'error',
47
+
'no-useless-return': 'warn',
48
+
49
+
'@typescript-eslint/no-unused-vars': [
50
+
'error',
22
51
{
23
-
"argsIgnorePattern": "^_",
24
-
"varsIgnorePattern": "^_",
25
-
"caughtErrorsIgnorePattern": "^_"
26
-
}
52
+
argsIgnorePattern: '^_',
53
+
varsIgnorePattern: '^_',
54
+
caughtErrorsIgnorePattern: '^_',
55
+
destructuredArrayIgnorePattern: '^_',
56
+
},
27
57
],
28
-
"@typescript-eslint/no-explicit-any": "warn"
29
-
}
30
-
}
31
-
]);
58
+
'@typescript-eslint/no-explicit-any': 'warn',
59
+
'@typescript-eslint/explicit-function-return-type': 'off',
60
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
61
+
'@typescript-eslint/no-inferrable-types': 'warn',
62
+
'@typescript-eslint/no-non-null-assertion': 'off',
63
+
'@typescript-eslint/prefer-for-of': 'error',
64
+
'@typescript-eslint/consistent-type-imports': 'off',
65
+
66
+
'no-unused-vars': 'off',
67
+
'no-undef': 'off',
68
+
'no-redeclare': 'off',
69
+
'@typescript-eslint/no-redeclare': 'error',
70
+
71
+
'no-process-exit': 'off',
72
+
'no-new-require': 'error',
73
+
'no-path-concat': 'error',
74
+
'no-sync': 'warn',
75
+
},
76
+
},
77
+
78
+
{
79
+
files: ['**/*.config.{js,mjs,ts}', '**/eslint.config.mjs', '**/src/index.ts'],
80
+
rules: {
81
+
'no-console': 'off',
82
+
'no-process-exit': 'off',
83
+
},
84
+
},
85
+
86
+
{
87
+
files: ['**/*.test.{js,ts}', '**/*.spec.{js,ts}', '**/tests/**/*.{js,ts}'],
88
+
languageOptions: {
89
+
globals: {
90
+
...globals.jest,
91
+
...globals.node,
92
+
},
93
+
},
94
+
rules: {
95
+
'@typescript-eslint/no-explicit-any': 'off',
96
+
'@typescript-eslint/explicit-function-return-type': 'off',
97
+
},
98
+
},
99
+
];
+6
-6
src/commands/fun/8ball.ts
+6
-6
src/commands/fun/8ball.ts
···
73
73
'en-US': 'Your yes/no question for the magic 8-ball',
74
74
})
75
75
.setRequired(true)
76
-
.setMaxLength(200)
76
+
.setMaxLength(200),
77
77
)
78
78
.setContexts([
79
79
InteractionContextType.BotDM,
···
84
84
85
85
execute: async (
86
86
client: import('@/services/Client').default,
87
-
interaction: import('discord.js').ChatInputCommandInteraction
87
+
interaction: import('discord.js').ChatInputCommandInteraction,
88
88
) => {
89
89
try {
90
90
const cooldownCheck = await checkCooldown(
91
91
cooldownManager,
92
92
interaction.user.id,
93
93
client,
94
-
interaction.locale
94
+
interaction.locale,
95
95
);
96
96
if (cooldownCheck.onCooldown) {
97
97
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
110
110
const question = sanitizeInput(interaction.options.getString('question'));
111
111
const translatedResponse = await client.getLocaleText(
112
112
`commands.8ball.responces.${random(responses)}`,
113
-
interaction.locale
113
+
interaction.locale,
114
114
);
115
115
commandLogger.logFromInteraction(
116
116
interaction,
117
-
`question: "${question?.substring(0, 50)}${question && question.length > 50 ? '...' : ''}"`
117
+
`question: "${question?.substring(0, 50)}${question && question.length > 50 ? '...' : ''}"`,
118
118
);
119
119
const [title, questionLabel, answerLabel] = await Promise.all([
120
120
await client.getLocaleText('commands.8ball.says', interaction.locale),
···
126
126
.setAccentColor(0x8b5cf6)
127
127
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔮 ${title}`))
128
128
.addTextDisplayComponents(
129
-
new TextDisplayBuilder().setContent(`**${questionLabel}**\n> ${question}\n\n`)
129
+
new TextDisplayBuilder().setContent(`**${questionLabel}**\n> ${question}\n\n`),
130
130
)
131
131
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`## ✨ ${answerLabel}`))
132
132
.addTextDisplayComponents(new TextDisplayBuilder().setContent(translatedResponse));
+7
-7
src/commands/fun/cat.ts
+7
-7
src/commands/fun/cat.ts
···
64
64
cooldownManager,
65
65
interaction.user.id,
66
66
client,
67
-
interaction.locale
67
+
interaction.locale,
68
68
);
69
69
if (cooldownCheck.onCooldown) {
70
70
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
96
96
? await client.getLocaleText('reddit.from', interaction.locale, {
97
97
subreddit: catData.subreddit,
98
98
})
99
-
: ''
100
-
)
99
+
: '',
100
+
),
101
101
)
102
102
.addMediaGalleryComponents(
103
-
new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(catData.url))
103
+
new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(catData.url)),
104
104
)
105
105
.addActionRowComponents(
106
106
new ActionRowBuilder<ButtonBuilder>().addComponents(
···
108
108
.setStyle(ButtonStyle.Danger)
109
109
.setLabel(refreshLabel)
110
110
.setEmoji({ name: '🐱' })
111
-
.setCustomId('refresh_cat')
112
-
)
111
+
.setCustomId('refresh_cat'),
112
+
),
113
113
);
114
114
115
115
try {
···
135
135
const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale);
136
136
137
137
const errorContainer = new ContainerBuilder().addTextDisplayComponents(
138
-
new TextDisplayBuilder().setContent(errorMsg)
138
+
new TextDisplayBuilder().setContent(errorMsg),
139
139
);
140
140
141
141
if (!interaction.replied && !interaction.deferred) {
+7
-7
src/commands/fun/dog.ts
+7
-7
src/commands/fun/dog.ts
···
64
64
cooldownManager,
65
65
interaction.user.id,
66
66
client,
67
-
interaction.locale
67
+
interaction.locale,
68
68
);
69
69
if (cooldownCheck.onCooldown) {
70
70
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
94
94
? await client.getLocaleText('reddit.from', interaction.locale, {
95
95
subreddit: dogData.subreddit,
96
96
})
97
-
: ''
98
-
)
97
+
: '',
98
+
),
99
99
)
100
100
.addMediaGalleryComponents(
101
-
new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(dogData.url))
101
+
new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(dogData.url)),
102
102
)
103
103
.addActionRowComponents(
104
104
new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
···
106
106
.setStyle(ButtonStyle.Secondary)
107
107
.setLabel(refreshLabel)
108
108
.setEmoji({ name: '🐶' })
109
-
.setCustomId('refresh_dog')
110
-
)
109
+
.setCustomId('refresh_dog'),
110
+
),
111
111
);
112
112
113
113
await interaction.editReply({
···
128
128
const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale);
129
129
130
130
const errorContainer = new ContainerBuilder().addTextDisplayComponents(
131
-
new TextDisplayBuilder().setContent(errorMsg)
131
+
new TextDisplayBuilder().setContent(errorMsg),
132
132
);
133
133
134
134
if (!interaction.replied && !interaction.deferred) {
+6
-6
src/commands/fun/joke.ts
+6
-6
src/commands/fun/joke.ts
···
79
79
value: 'programming',
80
80
name_localizations: { 'es-ES': 'Programación', 'es-419': 'Programación' },
81
81
},
82
-
{ name: 'Dad', value: 'dad', name_localizations: { 'es-ES': 'Papá', 'es-419': 'Papá' } }
83
-
)
82
+
{ name: 'Dad', value: 'dad', name_localizations: { 'es-ES': 'Papá', 'es-419': 'Papá' } },
83
+
),
84
84
)
85
85
.setContexts([
86
86
InteractionContextType.BotDM,
···
95
95
cooldownManager,
96
96
interaction.user.id,
97
97
client,
98
-
interaction.locale
98
+
interaction.locale,
99
99
);
100
100
if (cooldownCheck.onCooldown) {
101
101
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
114
114
interaction.locale,
115
115
{
116
116
type: await client.getLocaleText(`commands.joke.type.${joke.type}`, interaction.locale),
117
-
}
117
+
},
118
118
);
119
119
120
120
const waitingFooter = await client.getLocaleText(
···
122
122
interaction.locale,
123
123
{
124
124
seconds: 3,
125
-
}
125
+
},
126
126
);
127
127
128
128
const embed = new EmbedBuilder()
···
136
136
setTimeout(async () => {
137
137
try {
138
138
embed.setDescription(
139
-
`${sanitizeInput(joke.setup)}\n\n*${sanitizeInput(joke.punchline)}*`
139
+
`${sanitizeInput(joke.setup)}\n\n*${sanitizeInput(joke.punchline)}*`,
140
140
);
141
141
embed.setFooter({ text: 'Ba dum tss! 🥁' });
142
142
await interaction.editReply({ embeds: [embed] });
+22
-22
src/commands/fun/trivia.ts
+22
-22
src/commands/fun/trivia.ts
···
172
172
async function fetchTriviaQuestions(
173
173
client: import('@/services/Client').default,
174
174
locale: string,
175
-
playerCount: number
175
+
playerCount: number,
176
176
): Promise<TriviaQuestion[]> {
177
177
try {
178
178
const totalQuestions = Math.min(Math.max(playerCount * 2 + 1, 5), 50);
179
179
180
180
const response = await dynamicFetch(
181
-
`https://opentdb.com/api.php?amount=${totalQuestions}&type=multiple`
181
+
`https://opentdb.com/api.php?amount=${totalQuestions}&type=multiple`,
182
182
);
183
183
const data: TriviaAPIResponse = await response.json();
184
184
···
256
256
async function startTriviaGame(
257
257
interaction: ChatInputCommandInteraction | ButtonInteraction,
258
258
session: GameSession,
259
-
client: import('@/services/Client').default
259
+
client: import('@/services/Client').default,
260
260
) {
261
261
try {
262
262
session.questions = await fetchTriviaQuestions(
263
263
client,
264
264
interaction.locale,
265
-
session.players.size
265
+
session.players.size,
266
266
);
267
267
session.originalQuestionCount = session.questions.length;
268
268
session.isActive = true;
···
274
274
} catch {
275
275
const errorMsg = await client.getLocaleText(
276
276
'commands.trivia.messages.failed_start',
277
-
interaction.locale
277
+
interaction.locale,
278
278
);
279
279
await interaction.editReply({
280
280
content: errorMsg,
···
286
286
async function askQuestion(
287
287
interaction: ChatInputCommandInteraction | ButtonInteraction,
288
288
session: GameSession,
289
-
client: import('@/services/Client').default
289
+
client: import('@/services/Client').default,
290
290
) {
291
291
if (session.currentQuestionIndex >= session.questions.length) {
292
292
const sortedScores = Array.from(session.scores.entries()).sort(([, a], [, b]) => b - a);
···
300
300
301
301
const tiedGameText = await client.getLocaleText(
302
302
'commands.trivia.messages.tied_game',
303
-
interaction.locale
303
+
interaction.locale,
304
304
);
305
305
await interaction.editReply({
306
306
content: tiedGameText || '🤝 Tied game! Keep going until someone scores!',
···
349
349
'\n' +
350
350
difficultyText.replace(
351
351
'{difficulty}',
352
-
question.difficulty.charAt(0).toUpperCase() + question.difficulty.slice(1)
352
+
question.difficulty.charAt(0).toUpperCase() + question.difficulty.slice(1),
353
353
) +
354
354
'\n\n' +
355
355
questionText.replace('{question}', question.question);
···
365
365
async function endGame(
366
366
interaction: ChatInputCommandInteraction | ButtonInteraction,
367
367
session: GameSession,
368
-
client: import('@/services/Client').default
368
+
client: import('@/services/Client').default,
369
369
) {
370
370
const sortedScores = Array.from(session.scores.entries()).sort(([, a], [, b]) => b - a);
371
371
···
412
412
413
413
execute: async (
414
414
client: import('@/services/Client').default,
415
-
interaction: ChatInputCommandInteraction
415
+
interaction: ChatInputCommandInteraction,
416
416
) => {
417
417
try {
418
418
const channelId = interaction.channelId;
···
420
420
if (gameManager.has(channelId)) {
421
421
const errorMsg = await client.getLocaleText(
422
422
'commands.trivia.messages.game_exists',
423
-
interaction.locale
423
+
interaction.locale,
424
424
);
425
425
return interaction.reply({
426
426
content: errorMsg,
···
471
471
472
472
handleButton: async (
473
473
client: import('@/services/Client').default,
474
-
interaction: ButtonInteraction
474
+
interaction: ButtonInteraction,
475
475
) => {
476
476
try {
477
477
const channelId = interaction.channelId;
···
480
480
if (!session) {
481
481
const errorMsg = await client.getLocaleText(
482
482
'commands.trivia.messages.no_active_game',
483
-
interaction.locale
483
+
interaction.locale,
484
484
);
485
485
return interaction.reply({
486
486
content: errorMsg,
···
494
494
if (!session.queueOpen) {
495
495
const errorMsg = await client.getLocaleText(
496
496
'commands.trivia.messages.game_started',
497
-
interaction.locale
497
+
interaction.locale,
498
498
);
499
499
return interaction.reply({
500
500
content: '❌ ' + errorMsg,
···
505
505
if (session.players.has(interaction.user.id)) {
506
506
const errorMsg = await client.getLocaleText(
507
507
'commands.trivia.messages.already_joined',
508
-
interaction.locale
508
+
interaction.locale,
509
509
);
510
510
return interaction.reply({
511
511
content: '❌ ' + errorMsg,
···
535
535
if (interaction.user.id !== session.gameCreator) {
536
536
const errorMsg = await client.getLocaleText(
537
537
'commands.trivia.messages.only_creator_start',
538
-
interaction.locale
538
+
interaction.locale,
539
539
);
540
540
return interaction.reply({
541
541
content: '❌ ' + errorMsg,
···
546
546
if (session.players.size < 1) {
547
547
const errorMsg = await client.getLocaleText(
548
548
'commands.trivia.messages.need_players',
549
-
interaction.locale
549
+
interaction.locale,
550
550
);
551
551
return interaction.reply({
552
552
content: '❌ ' + errorMsg,
···
556
556
557
557
const startingMsg = await client.getLocaleText(
558
558
'commands.trivia.messages.starting_game',
559
-
interaction.locale
559
+
interaction.locale,
560
560
);
561
561
await interaction.update({
562
562
content: startingMsg,
···
568
568
if (interaction.user.id !== session.gameCreator) {
569
569
const errorMsg = await client.getLocaleText(
570
570
'commands.trivia.messages.only_creator_cancel',
571
-
interaction.locale
571
+
interaction.locale,
572
572
);
573
573
return interaction.reply({
574
574
content: '❌ ' + errorMsg,
···
580
580
581
581
const cancelMsg = await client.getLocaleText(
582
582
'commands.trivia.messages.game_cancelled',
583
-
interaction.locale
583
+
interaction.locale,
584
584
);
585
585
await interaction.update({
586
586
content: cancelMsg,
···
590
590
if (!session.isActive) {
591
591
const errorMsg = await client.getLocaleText(
592
592
'commands.trivia.messages.no_active_question',
593
-
interaction.locale
593
+
interaction.locale,
594
594
);
595
595
return interaction.reply({
596
596
content: errorMsg,
···
601
601
if (interaction.user.id !== session.currentPlayer) {
602
602
const errorMsg = await client.getLocaleText(
603
603
'commands.trivia.messages.not_your_turn',
604
-
interaction.locale
604
+
interaction.locale,
605
605
);
606
606
return interaction.reply({
607
607
content: '❌ ' + errorMsg,
+15
-15
src/commands/info/help.ts
+15
-15
src/commands/info/help.ts
···
63
63
64
64
.addMediaGalleryComponents(
65
65
new MediaGalleryBuilder().addItems(
66
-
new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png')
67
-
)
66
+
new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png'),
67
+
),
68
68
)
69
69
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`))
70
70
.addTextDisplayComponents(
71
-
new TextDisplayBuilder().setContent(description || 'Get information about Aethel')
71
+
new TextDisplayBuilder().setContent(description || 'Get information about Aethel'),
72
72
)
73
73
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
74
74
.addTextDisplayComponents(
75
75
new TextDisplayBuilder().setContent(
76
-
`\n## **${linksSocialText || 'Links & Social Media'}**`
77
-
)
76
+
`\n## **${linksSocialText || 'Links & Social Media'}**`,
77
+
),
78
78
)
79
79
.addTextDisplayComponents(
80
80
new TextDisplayBuilder().setContent(
81
-
'[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)'
82
-
)
81
+
'[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)',
82
+
),
83
83
)
84
84
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
85
85
.addTextDisplayComponents(
86
-
new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`)
86
+
new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`),
87
87
)
88
88
.addTextDisplayComponents(
89
89
new TextDisplayBuilder().setContent(
···
92
92
'**AI Integration** - Powered by OpenAI and other providers\n' +
93
93
'**Reminders** - Never forget important tasks\n' +
94
94
'**Utilities** - Weather, help, and productivity tools\n' +
95
-
'**Multi-language** - Supports multiple languages'
96
-
)
95
+
'**Multi-language** - Supports multiple languages',
96
+
),
97
97
)
98
98
99
99
.addSeparatorComponents(
100
-
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true)
100
+
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true),
101
101
)
102
102
.addTextDisplayComponents(
103
103
new TextDisplayBuilder().setContent(
104
-
`-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`
105
-
)
104
+
`-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`,
105
+
),
106
106
)
107
107
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
108
108
.addActionRowComponents(
···
114
114
new ButtonBuilder()
115
115
.setStyle(ButtonStyle.Link)
116
116
.setLabel(supportServerText || 'Support')
117
-
.setURL('https://discord.gg/63stE8pEaK')
118
-
)
117
+
.setURL('https://discord.gg/63stE8pEaK'),
118
+
),
119
119
);
120
120
121
121
await interaction.reply({
+33
-33
src/commands/utilities/ai.ts
+33
-33
src/commands/utilities/ai.ts
···
113
113
const after = text[startIdx + url.length];
114
114
if (before === '<' && after === '>') return url + (punctuation || '');
115
115
return `<${url}>${punctuation || ''}`;
116
-
}
116
+
},
117
117
);
118
118
}
119
119
···
138
138
client?: BotClient,
139
139
model?: string,
140
140
username?: string,
141
-
interaction?: ChatInputCommandInteraction
141
+
interaction?: ChatInputCommandInteraction,
142
142
): string {
143
143
const now = new Date();
144
144
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
···
160
160
let supportedCommands = '/help - Show all available commands and their usage';
161
161
if (client?.commands) {
162
162
const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) =>
163
-
a.localeCompare(b)
163
+
a.localeCompare(b),
164
164
);
165
165
supportedCommands = commandEntries
166
166
.map(
167
-
([name, command]) => `/${name} - ${command.data.description || 'No description available'}`
167
+
([name, command]) => `/${name} - ${command.data.description || 'No description available'}`,
168
168
)
169
169
.join('\n');
170
170
}
···
218
218
detail?: 'low' | 'high' | 'auto';
219
219
};
220
220
}>,
221
-
systemPrompt: string
221
+
systemPrompt: string,
222
222
): ConversationMessage[] {
223
223
let conversation = existingConversation.filter((msg) => msg.role !== 'system');
224
224
conversation.push({ role: 'user', content: prompt });
···
231
231
return conversation;
232
232
}
233
233
234
-
function splitResponseIntoChunks(response: string, maxLength: number = 2000): string[] {
234
+
function splitResponseIntoChunks(response: string, maxLength = 2000): string[] {
235
235
if (response.length <= maxLength) return [response];
236
236
237
237
const chunks: string[] = [];
···
272
272
userId: string,
273
273
apiKey: string | null,
274
274
model: string | null,
275
-
apiUrl: string | null
275
+
apiUrl: string | null,
276
276
): Promise<void> {
277
277
if (apiKey === null) {
278
278
await pool.query(
279
279
`UPDATE users SET api_key_encrypted = NULL, custom_model = NULL, custom_api_url = NULL, updated_at = now() WHERE user_id = $1`,
280
-
[userId]
280
+
[userId],
281
281
);
282
282
logger.info(`Cleared API credentials for user ${userId}`);
283
283
} else {
···
291
291
VALUES ($1, $2, $3, $4, now())
292
292
ON CONFLICT (user_id) DO UPDATE SET
293
293
api_key_encrypted = $2, custom_model = $3, custom_api_url = $4, updated_at = now()`,
294
-
[userId, encrypted, model?.trim() || null, apiUrl?.trim() || null]
294
+
[userId, encrypted, model?.trim() || null, apiUrl?.trim() || null],
295
295
);
296
296
logger.info(`Successfully saved encrypted API credentials for user ${userId}`);
297
297
}
···
340
340
}
341
341
}
342
342
343
-
async function incrementAndCheckDailyLimit(userId: string, limit: number = 20): Promise<boolean> {
343
+
async function incrementAndCheckDailyLimit(userId: string, limit = 20): Promise<boolean> {
344
344
const today = new Date().toISOString().slice(0, 10);
345
345
const client = await pool.connect();
346
346
try {
···
351
351
const res = await client.query(
352
352
`INSERT INTO ai_usage (user_id, usage_date, count) VALUES ($1, $2, 1)
353
353
ON CONFLICT (user_id, usage_date) DO UPDATE SET count = ai_usage.count + 1 RETURNING count`,
354
-
[userId, today]
354
+
[userId, today],
355
355
);
356
356
await client.query('COMMIT');
357
357
return res.rows[0].count <= limit;
···
366
366
async function testApiKey(
367
367
apiKey: string,
368
368
model: string,
369
-
apiUrl: string
369
+
apiUrl: string,
370
370
): Promise<{ success: boolean; error?: string }> {
371
371
try {
372
372
const client = getOpenAIClient(apiKey, apiUrl);
···
397
397
398
398
async function makeAIRequest(
399
399
config: ReturnType<typeof getApiConfiguration>,
400
-
conversation: ConversationMessage[]
400
+
conversation: ConversationMessage[],
401
401
): Promise<AIResponse | null> {
402
402
try {
403
403
const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl);
···
429
429
430
430
async function processAIRequest(
431
431
client: BotClient,
432
-
interaction: ChatInputCommandInteraction
432
+
interaction: ChatInputCommandInteraction,
433
433
): Promise<void> {
434
434
try {
435
435
if (!interaction.deferred && !interaction.replied) {
···
439
439
const prompt = interaction.options.getString('prompt')!;
440
440
commandLogger.logFromInteraction(
441
441
interaction,
442
-
`prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"`
442
+
`prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
443
443
);
444
444
445
445
const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id);
···
452
452
if (!allowed) {
453
453
await interaction.editReply(
454
454
'❌ ' +
455
-
(await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale))
455
+
(await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)),
456
456
);
457
457
return;
458
458
}
459
459
}
460
460
} else if (!config.finalApiKey) {
461
461
await interaction.editReply(
462
-
'❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale))
462
+
'❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)),
463
463
);
464
464
return;
465
465
}
···
471
471
client,
472
472
config.finalModel,
473
473
interaction.user.tag,
474
-
interaction
474
+
interaction,
475
475
);
476
476
const conversation = buildConversation(conversationArray, prompt, systemPrompt);
477
477
···
505
505
async function sendAIResponse(
506
506
interaction: ChatInputCommandInteraction,
507
507
aiResponse: AIResponse,
508
-
client: BotClient
508
+
client: BotClient,
509
509
): Promise<void> {
510
510
let fullResponse = '';
511
511
···
584
584
'es-419': 'Tu mensaje para la IA',
585
585
'en-US': 'Your message to the AI',
586
586
})
587
-
.setRequired(true)
587
+
.setRequired(true),
588
588
)
589
589
.addBooleanOption((option) =>
590
-
option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false)
590
+
option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false),
591
591
)
592
592
.addBooleanOption((option) =>
593
-
option.setName('reset').setDescription('Reset your AI chat history').setRequired(false)
593
+
option.setName('reset').setDescription('Reset your AI chat history').setRequired(false),
594
594
),
595
595
596
596
async execute(client: BotClient, interaction: ChatInputCommandInteraction) {
···
647
647
.setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale))
648
648
.setStyle(TextInputStyle.Short)
649
649
.setPlaceholder(
650
-
await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale)
650
+
await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale),
651
651
)
652
652
.setRequired(true);
653
653
···
656
656
.setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale))
657
657
.setStyle(TextInputStyle.Short)
658
658
.setPlaceholder(
659
-
await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale)
659
+
await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale),
660
660
)
661
661
.setRequired(true);
662
662
···
665
665
.setLabel(await client.getLocaleText('commands.ai.modal.model', interaction.locale))
666
666
.setStyle(TextInputStyle.Short)
667
667
.setPlaceholder(
668
-
await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale)
668
+
await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale),
669
669
)
670
670
.setRequired(true);
671
671
672
672
modal.addComponents(
673
673
new ActionRowBuilder<TextInputBuilder>().addComponents(apiKeyInput),
674
674
new ActionRowBuilder<TextInputBuilder>().addComponents(apiUrlInput),
675
-
new ActionRowBuilder<TextInputBuilder>().addComponents(modelInput)
675
+
new ActionRowBuilder<TextInputBuilder>().addComponents(modelInput),
676
676
);
677
677
678
678
await interaction.showModal(modal);
···
701
701
702
702
if (!pendingRequest) {
703
703
return interaction.editReply(
704
-
await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale)
704
+
await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale),
705
705
);
706
706
}
707
707
···
715
715
parsedUrl = new URL(apiUrl);
716
716
} catch {
717
717
await interaction.editReply(
718
-
'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).'
718
+
'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).',
719
719
);
720
720
return;
721
721
}
722
722
723
723
if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) {
724
724
await interaction.editReply(
725
-
'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).'
725
+
'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).',
726
726
);
727
727
return;
728
728
}
729
729
730
730
await interaction.editReply(
731
-
await client.getLocaleText('commands.ai.testing', interaction.locale)
731
+
await client.getLocaleText('commands.ai.testing', interaction.locale),
732
732
);
733
733
const testResult = await testApiKey(apiKey, model, apiUrl);
734
734
735
735
if (!testResult.success) {
736
736
const errorMessage = await client.getLocaleText(
737
737
'commands.ai.testfailed',
738
-
interaction.locale
738
+
interaction.locale,
739
739
);
740
740
await interaction.editReply(
741
-
errorMessage.replace('{error}', testResult.error || 'Unknown error')
741
+
errorMessage.replace('{error}', testResult.error || 'Unknown error'),
742
742
);
743
743
return;
744
744
}
745
745
746
746
await setUserApiKey(userId, apiKey, model, apiUrl);
747
747
await interaction.editReply(
748
-
await client.getLocaleText('commands.ai.testsuccess', interaction.locale)
748
+
await client.getLocaleText('commands.ai.testsuccess', interaction.locale),
749
749
);
750
750
751
751
if (!originalInteraction.deferred && !originalInteraction.replied) {
+30
-30
src/commands/utilities/cobalt.ts
+30
-30
src/commands/utilities/cobalt.ts
···
59
59
'es-419': 'La URL del video a descargar',
60
60
'en-US': 'The URL of the video to download',
61
61
})
62
-
.setRequired(true)
62
+
.setRequired(true),
63
63
)
64
64
.addStringOption((option) =>
65
65
option
···
84
84
{ name: '1440p', value: '1440' },
85
85
{ name: '2160p (4K)', value: '2160' },
86
86
{ name: '4320p (8K)', value: '4320' },
87
-
{ name: 'Max', value: 'max' }
88
-
)
87
+
{ name: 'Max', value: 'max' },
88
+
),
89
89
)
90
90
.addBooleanOption((option) =>
91
91
option
···
100
100
'es-ES': 'Descargar solo audio',
101
101
'es-419': 'Descargar solo audio',
102
102
'en-US': 'Download audio only',
103
-
})
103
+
}),
104
104
)
105
105
.addBooleanOption((option) =>
106
106
option
···
115
115
'es-ES': 'Silenciar audio',
116
116
'es-419': 'Silenciar audio',
117
117
'en-US': 'Mute audio',
118
-
})
118
+
}),
119
119
)
120
120
.addBooleanOption((option) =>
121
121
option
···
130
130
'es-ES': 'Descargar como GIF de Twitter',
131
131
'es-419': 'Descargar como GIF de Twitter',
132
132
'en-US': 'Download as Twitter GIF',
133
-
})
133
+
}),
134
134
)
135
135
.addBooleanOption((option) =>
136
136
option
···
145
145
'es-ES': 'Incluir audio original de TikTok',
146
146
'es-419': 'Incluir audio original de TikTok',
147
147
'en-US': 'Include TikTok original audio',
148
-
})
148
+
}),
149
149
)
150
150
.addStringOption((option) =>
151
151
option
···
165
165
{ name: 'MP3', value: 'mp3' },
166
166
{ name: 'OGG', value: 'ogg' },
167
167
{ name: 'WAV', value: 'wav' },
168
-
{ name: 'Best', value: 'best' }
169
-
)
168
+
{ name: 'Best', value: 'best' },
169
+
),
170
170
)
171
171
.setContexts([
172
172
InteractionContextType.BotDM,
···
221
221
222
222
const buttonLabel = await client.getLocaleText(
223
223
'commands.cobalt.button_label',
224
-
interaction.locale
224
+
interaction.locale,
225
225
);
226
226
227
227
const container = new ContainerBuilder()
···
230
230
new MediaGalleryBuilder().addItems(
231
231
new MediaGalleryItemBuilder()
232
232
.setDescription(data.filename || 'Downloaded media')
233
-
.setURL(downloadUrl)
234
-
)
233
+
.setURL(downloadUrl),
234
+
),
235
235
)
236
236
237
237
.addSectionComponents(
238
238
new SectionBuilder()
239
239
.addTextDisplayComponents((textDisplay) =>
240
-
textDisplay.setContent(`-# Took: ${processingTime}s`)
240
+
textDisplay.setContent(`-# Took: ${processingTime}s`),
241
241
)
242
242
.setButtonAccessory((button) =>
243
-
button.setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl)
244
-
)
243
+
button.setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl),
244
+
),
245
245
);
246
246
247
247
await interaction.editReply({
···
251
251
} else if (data.status === 'error') {
252
252
const unknownError = await client.getLocaleText(
253
253
'commands.cobalt.unknown_error',
254
-
interaction.locale
254
+
interaction.locale,
255
255
);
256
256
let errorText = unknownError;
257
257
···
266
266
interaction.locale,
267
267
{
268
268
error: errorText,
269
-
}
269
+
},
270
270
);
271
271
272
272
const errorContainer = new ContainerBuilder()
273
273
.setAccentColor(0xff4757)
274
274
.addSectionComponents(
275
275
new SectionBuilder().addTextDisplayComponents((textDisplay) =>
276
-
textDisplay.setContent(`❌ ${errorMessage}`)
277
-
)
276
+
textDisplay.setContent(`❌ ${errorMessage}`),
277
+
),
278
278
);
279
279
280
280
await interaction.editReply({
···
285
285
const multipleItemsMessage = await client.getLocaleText(
286
286
'commands.cobalt.multiple_items',
287
287
interaction.locale,
288
-
{ url }
288
+
{ url },
289
289
);
290
290
291
291
const pickerContainer = new ContainerBuilder()
···
293
293
.addSectionComponents(
294
294
new SectionBuilder().addTextDisplayComponents((textDisplay) =>
295
295
textDisplay.setContent(
296
-
`⚠️ ${multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.'}`
297
-
)
298
-
)
296
+
`⚠️ ${multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.'}`,
297
+
),
298
+
),
299
299
);
300
300
301
301
await interaction.editReply({
···
305
305
} else if (data.status === 'local-processing') {
306
306
const notSupportedMessage = await client.getLocaleText(
307
307
'commands.cobalt.local_processing_not_supported',
308
-
interaction.locale
308
+
interaction.locale,
309
309
);
310
310
311
311
const processingContainer = new ContainerBuilder()
···
313
313
.addSectionComponents(
314
314
new SectionBuilder().addTextDisplayComponents((textDisplay) =>
315
315
textDisplay.setContent(
316
-
`⚠️ ${notSupportedMessage || 'Local processing is not supported. Please try a different URL or option.'}`
317
-
)
318
-
)
316
+
`⚠️ ${notSupportedMessage || 'Local processing is not supported. Please try a different URL or option.'}`,
317
+
),
318
+
),
319
319
);
320
320
321
321
await interaction.editReply({
···
325
325
} else {
326
326
const unknownResponseMessage = await client.getLocaleText(
327
327
'commands.cobalt.unknown_response',
328
-
interaction.locale
328
+
interaction.locale,
329
329
);
330
330
331
331
const unknownContainer = new ContainerBuilder()
332
332
.setAccentColor(0xff4757)
333
333
.addSectionComponents(
334
334
new SectionBuilder().addTextDisplayComponents((textDisplay) =>
335
-
textDisplay.setContent(`❓ ${unknownResponseMessage}`)
336
-
)
335
+
textDisplay.setContent(`❓ ${unknownResponseMessage}`),
336
+
),
337
337
);
338
338
339
339
await interaction.editReply({
+46
-50
src/commands/utilities/remind.ts
+46
-50
src/commands/utilities/remind.ts
···
91
91
...reminder,
92
92
created_at: reminder.created_at || new Date(reminder.expires_at),
93
93
}),
94
-
timeUntilExpiry
94
+
timeUntilExpiry,
95
95
);
96
96
97
97
activeReminders.set(reminder.reminder_id, {
···
100
100
});
101
101
102
102
logger.debug(
103
-
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`
103
+
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`,
104
104
);
105
105
}
106
106
···
131
131
...reminder,
132
132
created_at: reminder.created_at || new Date(),
133
133
}),
134
-
timeUntilExpiry
134
+
timeUntilExpiry,
135
135
);
136
136
137
137
activeReminders.set(reminder.reminder_id, {
···
140
140
});
141
141
142
142
logger.info(
143
-
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`
143
+
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`,
144
144
);
145
145
return true;
146
146
} catch (error) {
···
166
166
167
167
const minutes = Math.floor(
168
168
(new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) /
169
-
(60 * 1000)
169
+
(60 * 1000),
170
170
);
171
171
172
172
const reminderTitle =
···
174
174
const reminderDesc = await client.getLocaleText(
175
175
'commands.remind.remindyou',
176
176
reminder.locale,
177
-
{ message: reminder.message }
177
+
{ message: reminder.message },
178
178
);
179
179
180
180
const timeElapsedText =
···
192
192
name: originalTimeText,
193
193
value: `<t:${Math.floor(new Date(reminder.created_at!).getTime() / 1000)}:f>`,
194
194
inline: true,
195
-
}
195
+
},
196
196
)
197
197
.setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` })
198
198
.setTimestamp();
···
201
201
const originalMessageText = await client.getLocaleText('common.ogmessage', reminder.locale);
202
202
const jumpToMessageText = await client.getLocaleText(
203
203
'common.jumptomessage',
204
-
reminder.locale
204
+
reminder.locale,
205
205
);
206
206
207
207
reminderEmbed.addFields({
···
233
233
{
234
234
error,
235
235
reminderId: reminder.reminder_id,
236
-
}
236
+
},
237
237
);
238
238
} finally {
239
239
try {
···
271
271
}
272
272
}
273
273
},
274
-
60 * 60 * 1000
274
+
60 * 60 * 1000,
275
275
);
276
276
277
277
function createCommandBuilder() {
···
298
298
'es-ES': 'Cuándo recordarte (ej: 1h, 30m, 5h30m)',
299
299
'es-419': 'Cuándo recordarte (ej: 1h, 30m, 5h30m)',
300
300
})
301
-
.setRequired(true)
301
+
.setRequired(true),
302
302
)
303
303
.addStringOption((option) =>
304
304
option
···
312
312
'es-ES': 'Sobre qué quieres que te recuerde',
313
313
'es-419': 'Sobre qué quieres que te recuerde',
314
314
})
315
-
.setRequired(true)
315
+
.setRequired(true),
316
316
)
317
317
.setContexts([
318
318
InteractionContextType.BotDM,
···
360
360
361
361
commandLogger.logFromInteraction(
362
362
interaction,
363
-
`time: ${timeStr}, message: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`
363
+
`time: ${timeStr}, message: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`,
364
364
);
365
365
366
366
if (!validateTimeString(timeStr)) {
367
367
logger.warn(`Invalid time format from ${userTag}: ${timeStr}`);
368
368
const errorMsg = await client.getLocaleText(
369
369
'commands.remind.errors.invalidformat',
370
-
interaction.locale
370
+
interaction.locale,
371
371
);
372
372
return await interaction.editReply({
373
373
content: errorMsg,
···
379
379
logger.warn(`Invalid message from ${userTag} - Length: ${message?.length}`);
380
380
const errorMsg = await client.getLocaleText(
381
381
'commands.remind.errors.providevalidchars',
382
-
interaction.locale
382
+
interaction.locale,
383
383
);
384
384
return await interaction.editReply({
385
385
content: errorMsg,
···
392
392
logger.warn(`Reminder time too short from ${userTag}: ${timeStr}`);
393
393
const errorMsg = await client.getLocaleText(
394
394
'commands.remind.errors.atleastoneminute',
395
-
interaction.locale
395
+
interaction.locale,
396
396
);
397
397
return await interaction.editReply({
398
398
content: errorMsg,
···
403
403
logger.warn(`Reminder time too long from ${userTag}: ${minutes} minutes`);
404
404
const errorMsg = await client.getLocaleText(
405
405
'commands.remind.errors.notlongerthanaday',
406
-
interaction.locale
406
+
interaction.locale,
407
407
);
408
408
return await interaction.editReply({
409
409
content: errorMsg,
···
436
436
...reminderData,
437
437
created_at: new Date(),
438
438
}),
439
-
minutes * 60 * 1000
439
+
minutes * 60 * 1000,
440
440
);
441
441
442
442
activeReminders.set(reminderId, {
···
447
447
const embed = new EmbedBuilder()
448
448
.setColor(0xfaa0a0)
449
449
.setTitle(
450
-
'⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale))
450
+
'⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)),
451
451
)
452
452
.setDescription(
453
453
await client.getLocaleText('commands.remind.iwillremindyou', interaction.locale, {
454
454
message,
455
455
time: formatTimeString(minutes),
456
-
})
456
+
}),
457
457
)
458
458
.addFields(
459
459
{
···
468
468
(await client.getLocaleText('commands.remind.willtrigger', interaction.locale)),
469
469
value: `<t:${Math.floor(expiresAt.getTime() / 1000)}:R>`,
470
470
inline: true,
471
-
}
471
+
},
472
472
)
473
473
.setFooter({
474
474
text: await client.getLocaleText('commands.remind.reminderid', interaction.locale, {
···
487
487
userTag,
488
488
expiresAt: expiresAt.toISOString(),
489
489
messagePreview: message.length > 50 ? `${message.substring(0, 50)}...` : message,
490
-
}
490
+
},
491
491
);
492
-
return;
493
492
} catch (error) {
494
493
if (error instanceof DatabaseError) {
495
494
logger.error(`Database error saving reminder: ${error.message}`, {
···
506
505
});
507
506
const errorMessage = await client.getLocaleText(
508
507
'commands.remind.errors.failedtosave',
509
-
interaction.locale
508
+
interaction.locale,
510
509
);
511
510
throw new Error(errorMessage);
512
511
}
···
551
550
messageId: message.id,
552
551
channelId: message.channelId,
553
552
guildId: message.guildId,
554
-
}
553
+
},
555
554
);
556
555
557
556
const modalTitle = await client.getLocaleText(
558
557
'commands.remind.modal.setreminder',
559
-
interaction.locale
558
+
interaction.locale,
560
559
);
561
560
const timeLabel = await client.getLocaleText(
562
561
'commands.remind.modal.whento',
563
-
interaction.locale
562
+
interaction.locale,
564
563
);
565
564
const modal = new ModalBuilder().setCustomId(modalId).setTitle(modalTitle);
566
565
···
583
582
userId: user.id,
584
583
userTag: user.tag,
585
584
messageId: message?.id,
586
-
}
585
+
},
587
586
);
588
587
589
588
let errorMessage: string;
···
595
594
} else {
596
595
errorMessage = await client.getLocaleText(
597
596
'commands.remind.errors.modalfailed',
598
-
interaction.locale
597
+
interaction.locale,
599
598
);
600
599
errorTitle = '❌ ' + (await client.getLocaleText('error', interaction.locale));
601
600
}
···
631
630
logger.warn(`No message info found for modal ID: ${modalId}`, { userId: user.id });
632
631
const errorMsg = await client.getLocaleText(
633
632
'commands.remind.errors.expired',
634
-
interaction.locale
633
+
interaction.locale,
635
634
);
636
635
return await modalInteraction.editReply({
637
636
content: errorMsg,
···
647
646
logger.warn(`Invalid time format from ${user.tag} in modal: ${timeStr}`);
648
647
const errorMsg = await client.getLocaleText(
649
648
'commands.remind.errors.invalidformat',
650
-
interaction.locale
649
+
interaction.locale,
651
650
);
652
651
return await modalInteraction.editReply({
653
652
content: errorMsg,
···
660
659
logger.warn(`Invalid time duration from ${user.tag} in modal: ${minutes} minutes`);
661
660
const errorMsg = await client.getLocaleText(
662
661
'commands.remind.errors.notlongerthanaday',
663
-
interaction.locale
662
+
interaction.locale,
664
663
);
665
664
return await modalInteraction.editReply({
666
665
content: errorMsg,
···
701
700
logger.error(`Database error saving reminder: ${error.message}`, { error });
702
701
return await modalInteraction.editReply({
703
702
content: error.userMessage,
704
-
});
705
-
} else {
706
-
logger.error(`Error saving reminder to database: ${(error as Error).message}`, { error });
707
-
const errorMsg = await client.getLocaleText(
708
-
'commands.remind.errors.failedtosave',
709
-
interaction.locale
710
-
);
711
-
return await modalInteraction.editReply({
712
-
content: errorMsg,
713
703
});
714
704
}
705
+
logger.error(`Error saving reminder to database: ${(error as Error).message}`, { error });
706
+
const errorMsg = await client.getLocaleText(
707
+
'commands.remind.errors.failedtosave',
708
+
interaction.locale,
709
+
);
710
+
return await modalInteraction.editReply({
711
+
content: errorMsg,
712
+
});
715
713
}
716
714
717
715
const timeoutId = setTimeout(
···
724
722
message_url: messageInfo.url,
725
723
},
726
724
}),
727
-
minutes * 60 * 1000
725
+
minutes * 60 * 1000,
728
726
);
729
727
730
728
activeReminders.set(reminderId, {
···
734
732
735
733
const jumpToMessageField = await client.getLocaleText(
736
734
'common.jumptomessage',
737
-
interaction.locale
735
+
interaction.locale,
738
736
);
739
737
740
738
const embed = new EmbedBuilder()
741
739
.setColor(0xfaa0a0)
742
740
.setTitle(
743
-
'⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale))
741
+
'⏰ ' + (await client.getLocaleText('commands.remind.reminderset', interaction.locale)),
744
742
)
745
743
// .setTitle(reminderSetTitle)
746
744
.setDescription(
747
745
await client.getLocaleText('commands.remind.contextiwillremindyou', interaction.locale, {
748
746
time: formatTimeString(minutes),
749
-
})
747
+
}),
750
748
)
751
749
// .setDescription(reminderSetDesc)
752
750
.addFields(
···
758
756
name: await client.getLocaleText('commands.remind.willtrigger', interaction.locale),
759
757
value: `<t:${Math.floor(expiresAt.getTime() / 1000)}:R>`,
760
758
inline: true,
761
-
}
759
+
},
762
760
)
763
761
.setFooter({
764
762
text: await client.getLocaleText('commands.remind.reminderid', interaction.locale, {
···
775
773
channelId: messageInfo.channelId,
776
774
minutes,
777
775
});
778
-
return;
779
776
} catch (error) {
780
777
logger.error(
781
778
`Error handling reminder modal for ${user.tag} (${user.id}): ${(error as Error).message}`,
782
779
{
783
780
error,
784
781
modalId,
785
-
}
782
+
},
786
783
);
787
784
788
785
try {
789
786
const errorMsg = await client.getLocaleText(
790
787
'commands.remind.errors.base',
791
-
interaction.locale
788
+
interaction.locale,
792
789
);
793
790
await modalInteraction.editReply({
794
791
content: errorMsg,
···
796
793
} catch (replyError) {
797
794
logger.error('Failed to send error response to user:', { error: replyError });
798
795
}
799
-
return;
800
796
}
801
797
},
802
798
} as RemindCommandProps;
+6
-6
src/commands/utilities/time.ts
+6
-6
src/commands/utilities/time.ts
···
61
61
'es-419': 'La ciudad (ej: Londres, Nueva York)',
62
62
})
63
63
.setRequired(true)
64
-
.setAutocomplete(true)
64
+
.setAutocomplete(true),
65
65
)
66
66
.setContexts([
67
67
InteractionContextType.BotDM,
···
93
93
const flag = iso2ToDiscordFlag(match.iso2);
94
94
const fieldDay = await client.getLocaleText(
95
95
'commands.time.embed.field_day_of_week',
96
-
locale
96
+
locale,
97
97
);
98
98
const fieldTime = await client.getLocaleText('commands.time.embed.field_time', locale);
99
99
const fieldDate = await client.getLocaleText('commands.time.embed.field_date', locale);
···
126
126
const description = await client.getLocaleText(
127
127
'commands.time.embed.multi_city_description',
128
128
locale,
129
-
{ city }
129
+
{ city },
130
130
);
131
131
const fields = await Promise.all(
132
132
shown.map(async (match) => {
···
137
137
const flag = iso2ToDiscordFlag(match.iso2);
138
138
const fieldDay = await client.getLocaleText(
139
139
'commands.time.embed.field_day_of_week',
140
-
locale
140
+
locale,
141
141
);
142
142
const fieldTime = await client.getLocaleText('commands.time.embed.field_time', locale);
143
143
const fieldDate = await client.getLocaleText('commands.time.embed.field_date', locale);
···
146
146
value: `${fieldDay}: **${day}**\n${fieldTime}: **${time}**\n${fieldDate}: **${date}**`,
147
147
inline: false,
148
148
};
149
-
})
149
+
}),
150
150
);
151
151
const title = await client.getLocaleText('commands.time.embed.title_multi', locale, {
152
152
city,
···
186
186
async autocomplete(client, interaction: AutocompleteInteraction) {
187
187
const focused = interaction.options.getFocused();
188
188
const filtered = POPULAR_CITIES.filter((city) =>
189
-
city.toLowerCase().includes(focused.toLowerCase())
189
+
city.toLowerCase().includes(focused.toLowerCase()),
190
190
).slice(0, 25);
191
191
await interaction.respond(filtered.map((city) => ({ name: city, value: city })));
192
192
},
+8
-8
src/commands/utilities/todo.ts
+8
-8
src/commands/utilities/todo.ts
···
21
21
try {
22
22
const { rows } = await pool.query(
23
23
'SELECT item FROM todos WHERE user_id = $1 AND done = FALSE ORDER BY created_at ASC',
24
-
[userId]
24
+
[userId],
25
25
);
26
26
return rows.map((r) => r.item);
27
27
} catch (error) {
···
33
33
try {
34
34
const { rows } = await pool.query(
35
35
'SELECT item FROM todos WHERE user_id = $1 AND done = TRUE ORDER BY completed_at ASC',
36
-
[userId]
36
+
[userId],
37
37
);
38
38
return rows.map((r) => r.item);
39
39
} catch (error) {
···
74
74
'es-ES': 'La tarea a agregar',
75
75
'es-419': 'La tarea a agregar',
76
76
})
77
-
.setRequired(true)
78
-
)
77
+
.setRequired(true),
78
+
),
79
79
)
80
80
.addSubcommand((sub) =>
81
81
sub
···
98
98
'es-419': 'Selecciona una tarea para marcar como hecha',
99
99
})
100
100
.setRequired(true)
101
-
.setAutocomplete(true)
102
-
)
101
+
.setAutocomplete(true),
102
+
),
103
103
)
104
104
.addSubcommand((sub) =>
105
105
sub
···
112
112
.setDescriptionLocalizations({
113
113
'es-ES': 'Ver tu lista de tareas',
114
114
'es-419': 'Ver tu lista de tareas',
115
-
})
115
+
}),
116
116
)
117
117
.setContexts([
118
118
InteractionContextType.BotDM,
···
154
154
try {
155
155
const { rowCount } = await pool.query(
156
156
'UPDATE todos SET done = TRUE, completed_at = NOW() WHERE user_id = $1 AND item = $2 AND done = FALSE',
157
-
[userId, item]
157
+
[userId, item],
158
158
);
159
159
if (rowCount === 0) {
160
160
await interaction.reply({
+4
-4
src/commands/utilities/weather.ts
+4
-4
src/commands/utilities/weather.ts
···
52
52
const data = (await response.json()) as WeatherAPIResponse;
53
53
if (!response.ok || data.cod !== 200) {
54
54
const error = new Error(
55
-
(data as WeatherErrorResponse).message ?? 'Failed to fetch weather data'
55
+
(data as WeatherErrorResponse).message ?? 'Failed to fetch weather data',
56
56
);
57
57
throw error;
58
58
}
···
89
89
'en-US': 'City name (e.g., London, New York, Tokyo)',
90
90
})
91
91
.setRequired(true)
92
-
.setMaxLength(100)
92
+
.setMaxLength(100),
93
93
)
94
94
.setContexts([
95
95
InteractionContextType.BotDM,
···
104
104
cooldownManager,
105
105
interaction.user.id,
106
106
client,
107
-
interaction.locale
107
+
interaction.locale,
108
108
);
109
109
if (cooldownCheck.onCooldown) {
110
110
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
188
188
{ name: weatherText, value: description, inline: true },
189
189
{ name: humidityText, value: `${data.main.humidity}%`, inline: true },
190
190
{ name: windText, value: `${windSpeed} ${windUnit}`, inline: true },
191
-
{ name: pressureText, value: `${data.main.pressure} hPa`, inline: true }
191
+
{ name: pressureText, value: `${data.main.pressure} hPa`, inline: true },
192
192
)
193
193
.setFooter({ text: footer + ' Open Weather' })
194
194
.setTimestamp();
+5
-5
src/commands/utilities/whois.ts
+5
-5
src/commands/utilities/whois.ts
···
34
34
async function withRetry<T>(
35
35
fn: () => Promise<T>,
36
36
maxRetries = MAX_RETRIES,
37
-
baseDelay = 1000
37
+
baseDelay = 1000,
38
38
): Promise<T> {
39
39
let lastError: any;
40
40
···
466
466
}
467
467
468
468
throw new Error(
469
-
'Could not retrieve WHOIS information. The domain may not exist or the WHOIS server may be temporarily unavailable.'
469
+
'Could not retrieve WHOIS information. The domain may not exist or the WHOIS server may be temporarily unavailable.',
470
470
);
471
471
};
472
472
···
481
481
.setName('whois')
482
482
.setDescription('Look up WHOIS information for a domain or IP address')
483
483
.addStringOption((option) =>
484
-
option.setName('query').setDescription('Domain or IP address to look up').setRequired(true)
484
+
option.setName('query').setDescription('Domain or IP address to look up').setRequired(true),
485
485
)
486
486
.setContexts([
487
487
InteractionContextType.BotDM,
···
495
495
cooldownManager,
496
496
interaction.user.id,
497
497
client,
498
-
interaction.locale
498
+
interaction.locale,
499
499
);
500
500
if (cooldownCheck.onCooldown) {
501
501
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
527
527
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔍 WHOIS Lookup`))
528
528
.addTextDisplayComponents(new TextDisplayBuilder().setContent(`## ${sanitizedQuery}`))
529
529
.addTextDisplayComponents(
530
-
new TextDisplayBuilder().setContent(formattedData || 'No WHOIS data available')
530
+
new TextDisplayBuilder().setContent(formattedData || 'No WHOIS data available'),
531
531
),
532
532
];
533
533
+5
-5
src/commands/utilities/wiki.ts
+5
-5
src/commands/utilities/wiki.ts
···
93
93
'en-US': 'What do you want to search for?',
94
94
})
95
95
.setRequired(true)
96
-
.setMaxLength(200)
96
+
.setMaxLength(200),
97
97
)
98
98
.setContexts([
99
99
InteractionContextType.BotDM,
···
108
108
cooldownManager,
109
109
interaction.user.id,
110
110
client,
111
-
interaction.locale
111
+
interaction.locale,
112
112
);
113
113
if (cooldownCheck.onCooldown) {
114
114
return interaction.reply(createCooldownResponse(cooldownCheck.message!));
···
134
134
if (extract.length > MAX_EXTRACT_LENGTH) {
135
135
const truncatedText = await client.getLocaleText(
136
136
'commands.wiki.readmoreonwiki',
137
-
interaction.locale
137
+
interaction.locale,
138
138
);
139
139
extract =
140
140
extract.substring(0, MAX_EXTRACT_LENGTH - truncatedText.length - 2) +
···
144
144
145
145
const readMore = await client.getLocaleText(
146
146
'commands.wiki.readmoreonwiki',
147
-
interaction.locale
147
+
interaction.locale,
148
148
);
149
149
const title = await client.getLocaleText('commands.wiki.pedia', interaction.locale, {
150
150
article: article.title,
···
153
153
const embed = new EmbedBuilder()
154
154
.setTitle(title)
155
155
.setURL(
156
-
`https://${wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`
156
+
`https://${wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`,
157
157
)
158
158
.setDescription(extract)
159
159
.setColor(0x4285f4)
+53
-53
src/events/interactionCreate.ts
+53
-53
src/events/interactionCreate.ts
···
58
58
content: `You have been banned from using Aethel commands for 7 days due to repeated use of unallowed language. Ban expires: <t:${Math.floor(new Date(banned_until).getTime() / 1000)}:F>.`,
59
59
flags: MessageFlags.Ephemeral,
60
60
});
61
-
} else {
62
-
return i.reply({
63
-
content: `Your request was flagged by Aethel for ${category}. You have ${strike_count}/5 strikes. For more information, visit https://aethel.xyz/legal/terms`,
64
-
flags: MessageFlags.Ephemeral,
65
-
});
66
61
}
62
+
return i.reply({
63
+
content: `Your request was flagged by Aethel for ${category}. You have ${strike_count}/5 strikes. For more information, visit https://aethel.xyz/legal/terms`,
64
+
flags: MessageFlags.Ephemeral,
65
+
});
67
66
}
68
67
}
69
68
}
···
164
163
? await this.client.getLocaleText('reddit.from', i.locale, {
165
164
subreddit: data.subreddit,
166
165
})
167
-
: ''
168
-
)
166
+
: '',
167
+
),
169
168
)
170
169
.addMediaGalleryComponents(
171
-
new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(data.url))
170
+
new MediaGalleryBuilder().addItems(
171
+
new MediaGalleryItemBuilder().setURL(data.url),
172
+
),
172
173
)
173
174
.addActionRowComponents(
174
175
new ActionRowBuilder<ButtonBuilder>().addComponents(
···
176
177
.setStyle(ButtonStyle.Danger)
177
178
.setLabel(refreshLabel)
178
179
.setEmoji({ name: '🐱' })
179
-
.setCustomId('refresh_cat')
180
-
)
180
+
.setCustomId('refresh_cat'),
181
+
),
181
182
);
182
183
183
184
await i.update({
···
187
188
} else {
188
189
const container = new ContainerBuilder().addTextDisplayComponents(
189
190
new TextDisplayBuilder().setContent(
190
-
await this.client.getLocaleText('commands.cat.error', i.locale)
191
-
)
191
+
await this.client.getLocaleText('commands.cat.error', i.locale),
192
+
),
192
193
);
193
194
194
195
await i.update({
···
200
201
logger.error('Error refreshing cat image:', error);
201
202
const container = new ContainerBuilder().addTextDisplayComponents(
202
203
new TextDisplayBuilder().setContent(
203
-
await this.client.getLocaleText('commands.cat.error', i.locale)
204
-
)
204
+
await this.client.getLocaleText('commands.cat.error', i.locale),
205
+
),
205
206
);
206
207
207
208
await i.update({
···
224
225
225
226
for (const cmd of this.client.commands.values()) {
226
227
const ClientApplicationCommandCache = this.client.application?.commands.cache.find(
227
-
(command) => command.name == cmd.data.name
228
+
(command) => command.name === cmd.data.name,
228
229
);
229
230
const category = cmd.category || 'Uncategorized';
230
231
if (!commandCategories.has(category)) {
···
233
234
234
235
const localizedDescription = await this.client.getLocaleText(
235
236
`commands.${cmd.data.name}.description`,
236
-
i.locale
237
+
i.locale,
237
238
);
238
239
commandCategories
239
240
.get(category)!
240
241
.push(
241
-
`</${ClientApplicationCommandCache?.name}:${ClientApplicationCommandCache?.id}> - ${localizedDescription}`
242
+
`</${ClientApplicationCommandCache?.name}:${ClientApplicationCommandCache?.id}> - ${localizedDescription}`,
242
243
);
243
244
}
244
245
245
246
const container = new ContainerBuilder()
246
247
.setAccentColor(0x5865f2)
247
248
.addTextDisplayComponents(
248
-
new TextDisplayBuilder().setContent('# 📋 **Available Commands**')
249
+
new TextDisplayBuilder().setContent('# 📋 **Available Commands**'),
249
250
);
250
251
251
252
for (const [category, cmds] of commandCategories.entries()) {
252
253
const localizedCategory = await this.client.getLocaleText(
253
254
`categories.${category}`,
254
-
i.locale
255
+
i.locale,
255
256
);
256
257
257
258
container.addTextDisplayComponents(
258
-
new TextDisplayBuilder().setContent(`\n## 📂 ${localizedCategory}`)
259
+
new TextDisplayBuilder().setContent(`\n## 📂 ${localizedCategory}`),
259
260
);
260
261
261
262
container.addTextDisplayComponents(
262
263
new TextDisplayBuilder().setContent(
263
-
cmds.map((line) => line.replace(/\u007F/g, '')).join('\n')
264
-
)
264
+
cmds.map((line) => line.replace(/\u007F/g, '')).join('\n'),
265
+
),
265
266
);
266
267
}
267
268
···
273
274
.setStyle(ButtonStyle.Secondary)
274
275
.setLabel(backLabel)
275
276
.setEmoji({ name: '⬅️' })
276
-
.setCustomId(`help_back_${i.user.id}`)
277
-
)
277
+
.setCustomId(`help_back_${i.user.id}`),
278
+
),
278
279
);
279
280
280
281
await i.update({
···
317
318
318
319
.addMediaGalleryComponents(
319
320
new MediaGalleryBuilder().addItems(
320
-
new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png')
321
-
)
321
+
new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png'),
322
+
),
322
323
)
323
324
.addTextDisplayComponents(
324
-
new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`)
325
+
new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`),
325
326
)
326
327
.addTextDisplayComponents(
327
-
new TextDisplayBuilder().setContent(description || 'Get information about Aethel')
328
+
new TextDisplayBuilder().setContent(description || 'Get information about Aethel'),
328
329
)
329
330
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
330
331
.addTextDisplayComponents(
331
332
new TextDisplayBuilder().setContent(
332
-
`\n## **${linksSocialText || 'Links & Social Media'}**`
333
-
)
333
+
`\n## **${linksSocialText || 'Links & Social Media'}**`,
334
+
),
334
335
)
335
336
.addTextDisplayComponents(
336
337
new TextDisplayBuilder().setContent(
337
-
'[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)'
338
-
)
338
+
'[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)',
339
+
),
339
340
)
340
341
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
341
342
.addTextDisplayComponents(
342
-
new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`)
343
+
new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`),
343
344
)
344
345
.addTextDisplayComponents(
345
346
new TextDisplayBuilder().setContent(
···
348
349
'**AI Integration** - Powered by OpenAI and other providers\n' +
349
350
'**Reminders** - Never forget important tasks\n' +
350
351
'**Utilities** - Weather, help, and productivity tools\n' +
351
-
'**Multi-language** - Supports multiple languages'
352
-
)
352
+
'**Multi-language** - Supports multiple languages',
353
+
),
353
354
)
354
355
355
356
.addSeparatorComponents(
356
-
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true)
357
+
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true),
357
358
)
358
359
.addTextDisplayComponents(
359
360
new TextDisplayBuilder().setContent(
360
-
`-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`
361
-
)
361
+
`-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`,
362
+
),
362
363
)
363
364
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
364
365
.addActionRowComponents(
···
370
371
new ButtonBuilder()
371
372
.setStyle(ButtonStyle.Link)
372
373
.setLabel(supportServerText || 'Support')
373
-
.setURL('https://discord.gg/63stE8pEaK')
374
-
)
374
+
.setURL('https://discord.gg/63stE8pEaK'),
375
+
),
375
376
);
376
377
377
378
await i.update({
···
386
387
if (!response.ok) {
387
388
const container = new ContainerBuilder().addTextDisplayComponents(
388
389
new TextDisplayBuilder().setContent(
389
-
await this.client.getLocaleText('commands.dog.error', i.locale)
390
-
)
390
+
await this.client.getLocaleText('commands.dog.error', i.locale),
391
+
),
391
392
);
392
393
393
394
return await i.update({
···
428
429
? await this.client.getLocaleText('reddit.from', i.locale, {
429
430
subreddit: data!.subreddit,
430
431
})
431
-
: ''
432
-
)
432
+
: '',
433
+
),
433
434
)
434
435
.addMediaGalleryComponents(
435
436
new MediaGalleryBuilder().addItems(
436
-
new MediaGalleryItemBuilder().setURL(data!.url)
437
-
)
437
+
new MediaGalleryItemBuilder().setURL(data!.url),
438
+
),
438
439
)
439
440
.addActionRowComponents(
440
441
new ActionRowBuilder<ButtonBuilder>().addComponents(
···
442
443
.setStyle(ButtonStyle.Secondary)
443
444
.setLabel(refreshLabel)
444
445
.setEmoji({ name: '🐶' })
445
-
.setCustomId('refresh_dog')
446
-
)
446
+
.setCustomId('refresh_dog'),
447
+
),
447
448
);
448
449
449
450
await i.update({
···
453
454
} else {
454
455
const container = new ContainerBuilder().addTextDisplayComponents(
455
456
new TextDisplayBuilder().setContent(
456
-
await this.client.getLocaleText('commands.dog.error', i.locale)
457
-
)
457
+
await this.client.getLocaleText('commands.dog.error', i.locale),
458
+
),
458
459
);
459
460
460
461
await i.update({
···
466
467
logger.error('Error refreshing dog image:', error);
467
468
const container = new ContainerBuilder().addTextDisplayComponents(
468
469
new TextDisplayBuilder().setContent(
469
-
await this.client.getLocaleText('commands.dog.error', i.locale)
470
-
)
470
+
await this.client.getLocaleText('commands.dog.error', i.locale),
471
+
),
471
472
);
472
473
473
474
await i.update({
···
483
484
components: [],
484
485
});
485
486
}
486
-
return;
487
487
}
488
488
};
489
489
}
+13
-13
src/events/messageCreate.ts
+13
-13
src/events/messageCreate.ts
···
46
46
47
47
if (!isDM && !isMentioned) {
48
48
logger.debug(
49
-
`Ignoring message - not a DM and bot not mentioned (channel type: ${message.channel.type})`
49
+
`Ignoring message - not a DM and bot not mentioned (channel type: ${message.channel.type})`,
50
50
);
51
51
return;
52
52
}
···
63
63
const hasImageAttachments = message.attachments.some(
64
64
(att) =>
65
65
att.contentType?.startsWith('image/') ||
66
-
att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i)
66
+
att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i),
67
67
);
68
68
69
69
const hasImageUrls = false;
···
87
87
: userCustomModel || 'moonshotai/kimi-k2';
88
88
89
89
logger.info(
90
-
`Using model: ${selectedModel} for message with images: ${hasImages}${userCustomModel ? ' (user custom model)' : ' (default model)'}`
90
+
`Using model: ${selectedModel} for message with images: ${hasImages}${userCustomModel ? ' (user custom model)' : ' (default model)'}`,
91
91
);
92
92
93
93
const systemPrompt = buildSystemPrompt(
94
94
isDM,
95
95
this.client,
96
96
selectedModel,
97
-
message.author.username
97
+
message.author.username,
98
98
);
99
99
100
100
let messageContent:
···
112
112
const imageAttachments = message.attachments.filter(
113
113
(att) =>
114
114
att.contentType?.startsWith('image/') ||
115
-
att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i)
115
+
att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i),
116
116
);
117
117
118
118
const contentArray: Array<{
···
164
164
const updatedConversation = buildConversation(
165
165
filteredConversation,
166
166
messageContent,
167
-
systemPrompt
167
+
systemPrompt,
168
168
);
169
169
170
170
const { apiKey: userApiKey, apiUrl: userApiUrl } = await getUserCredentials(
171
-
message.author.id
171
+
message.author.id,
172
172
);
173
173
const config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null);
174
174
···
178
178
const allowed = await incrementAndCheckDailyLimit(message.author.id, 10);
179
179
if (!allowed) {
180
180
await message.reply(
181
-
"❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command."
181
+
"❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.",
182
182
);
183
183
return;
184
184
}
···
231
231
const fallbackConversation = buildConversation(
232
232
cleanedConversation,
233
233
fallbackContent,
234
-
buildSystemPrompt(isDM, this.client, fallbackModel, message.author.username)
234
+
buildSystemPrompt(isDM, this.client, fallbackModel, message.author.username),
235
235
);
236
236
237
237
const fallbackConfig = getApiConfiguration(
238
238
userApiKey ?? null,
239
239
fallbackModel,
240
-
userApiUrl ?? null
240
+
userApiUrl ?? null,
241
241
);
242
242
aiResponse = await makeAIRequest(fallbackConfig, fallbackConversation);
243
243
···
248
248
249
249
if (!aiResponse) {
250
250
await message.reply(
251
-
'Sorry, I encountered an error processing your message. Please try again later.'
251
+
'Sorry, I encountered an error processing your message. Please try again later.',
252
252
);
253
253
return;
254
254
}
···
267
267
} catch (error) {
268
268
logger.error(
269
269
`Error processing ${isDM ? 'DM' : 'server message'}:`,
270
-
error instanceof Error ? error.message : String(error)
270
+
error instanceof Error ? error.message : String(error),
271
271
);
272
272
try {
273
273
await message.reply(
274
-
'Sorry, I encountered an error processing your message. Please try again later.'
274
+
'Sorry, I encountered an error processing your message. Please try again later.',
275
275
);
276
276
} catch (replyError) {
277
277
logger.error('Failed to send error message:', replyError);
+1
-1
src/handlers/initialzeCommands.ts
+1
-1
src/handlers/initialzeCommands.ts
···
23
23
const commands: (SlashCommandBuilder | ContextMenuCommandBuilder)[] = [];
24
24
for (const cat of cmdCat) {
25
25
const commandFiles = readdirSync(path.join(cmdDir, cat)).filter(
26
-
(f) => f.endsWith('.js') || f.endsWith('.ts')
26
+
(f) => f.endsWith('.js') || f.endsWith('.ts'),
27
27
);
28
28
for (const file of commandFiles) {
29
29
const commandPath = path.join(cmdDir, cat, file);
+4
-4
src/index.ts
+4
-4
src/index.ts
···
37
37
allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization', 'Cache-Control', 'Pragma'],
38
38
credentials: true,
39
39
maxAge: 86400,
40
-
})
40
+
}),
41
41
);
42
42
app.set('trust proxy', 1);
43
43
app.use(
···
47
47
message: { error: 'Too many requests, please try again later.' },
48
48
standardHeaders: true,
49
49
legacyHeaders: false,
50
-
})
50
+
}),
51
51
);
52
52
53
53
app.use(e.json({ limit: '10mb' }));
···
65
65
} else {
66
66
res.setHeader(
67
67
'Content-Security-Policy',
68
-
"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:"
68
+
"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:",
69
69
);
70
70
}
71
71
next();
···
94
94
() => {
95
95
resetOldStrikes().catch(console.error);
96
96
},
97
-
60 * 60 * 1000
97
+
60 * 60 * 1000,
98
98
);
99
99
100
100
app.listen(PORT, () => {
+2
-3
src/middlewares/auth.ts
+2
-3
src/middlewares/auth.ts
···
32
32
} else if (error instanceof jwt.JsonWebTokenError) {
33
33
logger.debug('Invalid JWT token used');
34
34
return res.status(401).json({ error: 'Invalid token' });
35
-
} else {
36
-
logger.error('JWT verification error:', error);
37
-
return res.status(500).json({ error: 'Token verification failed' });
38
35
}
36
+
logger.error('JWT verification error:', error);
37
+
return res.status(500).json({ error: 'Token verification failed' });
39
38
}
40
39
};
41
40
+1
-1
src/middlewares/verifyApiKey.ts
+1
-1
src/middlewares/verifyApiKey.ts
···
3
3
4
4
const authenticateApiKey: RequestHandler = (req, res, next) => {
5
5
const apiKey = req.headers['x-api-key'];
6
-
if (!apiKey || typeof apiKey != 'string') {
6
+
if (!apiKey || typeof apiKey !== 'string') {
7
7
res.status(401).json({ error: 'Unauthorized: Missing API key' });
8
8
return;
9
9
}
+3
-3
src/routes/apiKeys.ts
+3
-3
src/routes/apiKeys.ts
···
119
119
logger.error('Error updating API key:', error);
120
120
res.status(500).json({ error: 'Internal server error' });
121
121
}
122
-
}
122
+
},
123
123
);
124
124
125
125
router.put(
···
179
179
logger.error('Error updating API key:', error);
180
180
res.status(500).json({ error: 'Internal server error' });
181
181
}
182
-
}
182
+
},
183
183
);
184
184
185
185
router.delete('/', async (req, res) => {
···
293
293
294
294
res.status(500).json({ error: 'API key test failed due to server error' });
295
295
}
296
-
}
296
+
},
297
297
);
298
298
299
299
export default router;
+2
-2
src/routes/auth.ts
+2
-2
src/routes/auth.ts
···
80
80
'INSERT INTO users (user_id, language, created_at) VALUES ($1, $2, NOW())';
81
81
await pool.query(insertQuery, [discordUser.id, 'en']);
82
82
logger.info(
83
-
`New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})`
83
+
`New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})`,
84
84
);
85
85
}
86
86
···
92
92
avatar: discordUser.avatar,
93
93
},
94
94
JWT_SECRET,
95
-
{ expiresIn: '7d' }
95
+
{ expiresIn: '7d' },
96
96
);
97
97
98
98
const redirectUrl = new URL(`${FRONTEND_URL}/login`);
+8
-8
src/routes/reminders.ts
+8
-8
src/routes/reminders.ts
···
74
74
75
75
if (scheduled) {
76
76
logger.info(
77
-
`Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard`
77
+
`Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard`,
78
78
);
79
79
} else {
80
80
logger.warn(`Failed to schedule reminder ${savedReminder.reminder_id} from dashboard`);
···
145
145
if (user) {
146
146
const minutes = Math.floor(
147
147
(new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) /
148
-
(60 * 1000)
148
+
(60 * 1000),
149
149
);
150
150
151
151
const reminderTitle =
···
153
153
const reminderDesc = await client.getLocaleText(
154
154
'commands.remind.remindyou',
155
155
reminder.locale,
156
-
{ message: reminder.message }
156
+
{ message: reminder.message },
157
157
);
158
158
159
159
const timeElapsedText =
···
171
171
name: originalTimeText,
172
172
value: `<t:${Math.floor(new Date(reminder.created_at!).getTime() / 1000)}:f>`,
173
173
inline: true,
174
-
}
174
+
},
175
175
)
176
176
.setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` })
177
177
.setTimestamp();
···
179
179
if (reminder.metadata?.message_url) {
180
180
const originalMessageText = await client.getLocaleText(
181
181
'common.ogmessage',
182
-
reminder.locale
182
+
reminder.locale,
183
183
);
184
184
const jumpToMessageText = await client.getLocaleText(
185
185
'common.jumptomessage',
186
-
reminder.locale
186
+
reminder.locale,
187
187
);
188
188
189
189
reminderEmbed.addFields({
···
196
196
if (reminder.message.includes('http') && !reminder.metadata?.message_url) {
197
197
const messageLinkText = await client.getLocaleText(
198
198
'common.messagelink',
199
-
reminder.locale
199
+
reminder.locale,
200
200
);
201
201
reminderEmbed.addFields({
202
202
name: messageLinkText,
···
220
220
{
221
221
error: notificationError,
222
222
reminderId: reminder.reminder_id,
223
-
}
223
+
},
224
224
);
225
225
}
226
226
+2
-2
src/routes/todos.ts
+2
-2
src/routes/todos.ts
···
63
63
logger.error('Error creating todo:', error);
64
64
res.status(500).json({ error: 'Internal server error' });
65
65
}
66
-
}
66
+
},
67
67
);
68
68
69
69
router.put(
···
105
105
logger.error('Error updating todo:', error);
106
106
res.status(500).json({ error: 'Internal server error' });
107
107
}
108
-
}
108
+
},
109
109
);
110
110
111
111
router.delete('/:id', async (req, res) => {
+11
-17
src/utils/commandLogger.ts
+11
-17
src/utils/commandLogger.ts
···
3
3
4
4
export interface CommandLogOptions {
5
5
commandName: string;
6
-
userId: string;
7
-
username: string;
8
6
additionalInfo?: string;
9
-
guildId?: string;
10
-
channelId?: string;
7
+
isGuild?: boolean;
8
+
isDM?: boolean;
11
9
}
12
10
13
11
export function logUserAction(options: CommandLogOptions): void {
14
-
const { commandName, userId, username, guildId, channelId, additionalInfo } = options;
15
-
16
-
let logMessage = `User ${username} (${userId}) used ${commandName} command`;
12
+
const { commandName, isGuild, isDM, additionalInfo } = options;
17
13
18
-
if (guildId) {
19
-
logMessage += ` in guild ${guildId}`;
20
-
}
14
+
let logMessage = `User executed ${commandName} command`;
21
15
22
-
if (channelId) {
23
-
logMessage += ` in channel ${channelId}`;
16
+
if (isGuild) {
17
+
logMessage += ` in guild`;
18
+
} else if (isDM) {
19
+
logMessage += ` in DM`;
24
20
}
25
21
26
22
if (additionalInfo) {
···
33
29
export function logUserActionFromInteraction(
34
30
interaction: CommandInteraction,
35
31
commandName: string,
36
-
additionalInfo?: string
32
+
additionalInfo?: string,
37
33
): void {
38
34
logUserAction({
39
35
commandName,
40
-
userId: interaction.user.id,
41
-
username: interaction.user.tag,
42
-
guildId: interaction.guildId || undefined,
43
-
channelId: interaction.channelId,
36
+
isGuild: !!interaction.guildId,
37
+
isDM: !interaction.guildId,
44
38
additionalInfo,
45
39
});
46
40
}
+2
-2
src/utils/cooldown.ts
+2
-2
src/utils/cooldown.ts
···
19
19
}
20
20
}
21
21
},
22
-
5 * 60 * 1000
22
+
5 * 60 * 1000,
23
23
);
24
24
25
25
export function createCooldownManager(commandName: string, cooldownTime: number): CooldownManager {
···
35
35
manager: CooldownManager,
36
36
userId: string,
37
37
client: BotClient,
38
-
locale: string
38
+
locale: string,
39
39
): Promise<{ onCooldown: boolean; timeLeft?: number; message?: string }> {
40
40
const now = Date.now();
41
41
const cooldownEnd = manager.cooldowns.get(userId) || 0;
+4
-4
src/utils/dynamicFetch.ts
+4
-4
src/utils/dynamicFetch.ts
···
11
11
message: string,
12
12
public readonly status?: number,
13
13
public readonly statusText?: string,
14
-
public readonly url?: string
14
+
public readonly url?: string,
15
15
) {
16
16
super(message);
17
17
this.name = 'FetchError';
···
70
70
`Request timeout after ${timeout}ms`,
71
71
undefined,
72
72
undefined,
73
-
url.toString()
73
+
url.toString(),
74
74
);
75
75
}
76
76
···
87
87
`Client error: ${error.message}`,
88
88
error.status,
89
89
'statusText' in error ? (error.statusText as string) : undefined,
90
-
url.toString()
90
+
url.toString(),
91
91
);
92
92
}
93
93
}
···
117
117
`Request failed after ${retries + 1} attempts: ${lastError?.message || 'Unknown error'}`,
118
118
undefined,
119
119
undefined,
120
-
url.toString()
120
+
url.toString(),
121
121
);
122
122
};
123
123
+6
-6
src/utils/encrypt.ts
+6
-6
src/utils/encrypt.ts
···
10
10
class EncryptionError extends Error {
11
11
constructor(
12
12
message: string,
13
-
public readonly operation: 'encrypt' | 'decrypt'
13
+
public readonly operation: 'encrypt' | 'decrypt',
14
14
) {
15
15
super(message);
16
16
this.name = 'EncryptionError';
···
54
54
if (!encrypted || typeof encrypted !== 'string') {
55
55
throw new EncryptionError(
56
56
'Invalid input: encrypted data must be a non-empty string',
57
-
'decrypt'
57
+
'decrypt',
58
58
);
59
59
}
60
60
···
86
86
if (iv.length !== IV_LENGTH) {
87
87
throw new EncryptionError(
88
88
`Invalid IV length: expected ${IV_LENGTH}, got ${iv.length}`,
89
-
'decrypt'
89
+
'decrypt',
90
90
);
91
91
}
92
92
93
93
if (tag.length !== 16) {
94
94
throw new EncryptionError(
95
95
`Invalid auth tag length: expected 16, got ${tag.length}`,
96
-
'decrypt'
96
+
'decrypt',
97
97
);
98
98
}
99
99
···
113
113
if (error instanceof Error) {
114
114
if (error.message.includes('Unsupported state or unable to authenticate data')) {
115
115
logger.warn(
116
-
'Authentication failed during decryption - data may be corrupted or key changed'
116
+
'Authentication failed during decryption - data may be corrupted or key changed',
117
117
);
118
118
throw new EncryptionError(
119
119
'Authentication failed - data may be corrupted or encryption key changed',
120
-
'decrypt'
120
+
'decrypt',
121
121
);
122
122
}
123
123
if (error.message.includes('Invalid key length')) {
+3
-1
src/utils/getGitCommitHash.ts
+3
-1
src/utils/getGitCommitHash.ts
···
39
39
return cachedCommitHash || getGitCommitHashSync();
40
40
}
41
41
42
-
initializeGitCommitHash().catch(() => {});
42
+
initializeGitCommitHash().catch((error) => {
43
+
console.warn('Failed to initialize git commit hash:', error.message);
44
+
});
43
45
44
46
export default getGitCommitHash;
+23
-7
src/utils/logger.ts
+23
-7
src/utils/logger.ts
···
3
3
import path from 'path';
4
4
5
5
const sanitizeFormat = winston.format((info) => {
6
-
const sensitiveKeys = ['password', 'token', 'api_key', 'secret', 'authorization'];
6
+
const sensitiveKeys = [
7
+
'password',
8
+
'token',
9
+
'api_key',
10
+
'secret',
11
+
'authorization',
12
+
'userid',
13
+
'user_id',
14
+
'username',
15
+
'user_tag',
16
+
'guildid',
17
+
'guild_id',
18
+
'channelid',
19
+
'channel_id',
20
+
];
7
21
8
22
const sanitize = (obj: unknown): unknown => {
9
23
if (typeof obj !== 'object' || obj === null) return obj;
···
12
26
for (const key in sanitized) {
13
27
if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
14
28
sanitized[key] = '[REDACTED]';
29
+
} else if (typeof sanitized[key] === 'string') {
30
+
sanitized[key] = (sanitized[key] as string).replace(/\b\d{17,19}\b/g, '[ID_REDACTED]');
15
31
} else if (typeof sanitized[key] === 'object') {
16
32
sanitized[key] = sanitize(sanitized[key]);
17
33
}
···
31
47
winston.format.errors({ stack: true }),
32
48
sanitizeFormat(),
33
49
winston.format.splat(),
34
-
winston.format.json()
50
+
winston.format.json(),
35
51
),
36
52
defaultMeta: {
37
53
service: 'Aethel',
···
45
61
winston.format.printf(({ timestamp, level, message, service, ...meta }) => {
46
62
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '';
47
63
return `${timestamp} [${service}] ${level}: ${message} ${metaStr}`;
48
-
})
64
+
}),
49
65
),
50
66
}),
51
67
],
···
62
78
maxsize: 5242880,
63
79
maxFiles: 5,
64
80
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
65
-
})
81
+
}),
66
82
);
67
83
68
84
logger.add(
···
71
87
maxsize: 5242880,
72
88
maxFiles: 5,
73
89
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
74
-
})
90
+
}),
75
91
);
76
92
}
77
93
78
94
logger.exceptions.handle(
79
95
new winston.transports.Console({
80
96
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
81
-
})
97
+
}),
82
98
);
83
99
84
100
logger.rejections.handle(
85
101
new winston.transports.Console({
86
102
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
87
-
})
103
+
}),
88
104
);
89
105
90
106
export default logger;
+2
-3
src/utils/memoryManager.ts
+2
-3
src/utils/memoryManager.ts
···
47
47
const result = [...entry];
48
48
delete (result as unknown as Record<string, unknown>).timestamp;
49
49
return result as V;
50
-
} else {
51
-
const { timestamp: _timestamp, ...valueWithoutTimestamp } = entry;
52
-
return valueWithoutTimestamp as V;
53
50
}
51
+
const { timestamp: _timestamp, ...valueWithoutTimestamp } = entry;
52
+
return valueWithoutTimestamp as V;
54
53
}
55
54
56
55
delete(key: K): boolean {
+6
-6
src/utils/reminderDb.ts
+6
-6
src/utils/reminderDb.ts
···
45
45
return new DatabaseError(
46
46
errorMessage,
47
47
'Unable to connect to the database. Please try again later.',
48
-
true
48
+
true,
49
49
);
50
50
case '42703':
51
51
return new DatabaseError(
52
52
errorMessage,
53
53
'Database schema error. Please contact support.',
54
-
true
54
+
true,
55
55
);
56
56
case '23505':
57
57
return new DatabaseError(errorMessage, 'This reminder already exists.', true);
···
59
59
return new DatabaseError(
60
60
errorMessage,
61
61
'Missing required information. Please try again.',
62
-
true
62
+
true,
63
63
);
64
64
case '23503':
65
65
return new DatabaseError(errorMessage, 'Invalid reference. Please try again.', true);
···
67
67
return new DatabaseError(
68
68
errorMessage,
69
69
'A database error occurred. Please try again later.',
70
-
false
70
+
false,
71
71
);
72
72
}
73
73
}
···
75
75
async function ensureUserRegistered(
76
76
userId: string,
77
77
userTag: string,
78
-
language: string = 'en'
78
+
language = 'en',
79
79
): Promise<void> {
80
80
const query = `
81
81
SELECT ensure_user_registered($1, $2, $3)
···
92
92
await ensureUserRegistered(
93
93
reminderData.user_id,
94
94
reminderData.user_tag,
95
-
reminderData.locale || 'en'
95
+
reminderData.locale || 'en',
96
96
);
97
97
98
98
const query = `
+5
-5
src/utils/userStrikes.ts
+5
-5
src/utils/userStrikes.ts
···
9
9
export class StrikeError extends Error {
10
10
constructor(
11
11
message: string,
12
-
public readonly userId?: string
12
+
public readonly userId?: string,
13
13
) {
14
14
super(message);
15
15
this.name = 'StrikeError';
···
24
24
try {
25
25
const res = await pgClient.query(
26
26
'SELECT strike_count, banned_until FROM user_strikes WHERE user_id = $1',
27
-
[userId]
27
+
[userId],
28
28
);
29
29
30
30
if (res.rows.length === 0) {
···
56
56
last_strike_at = NOW()
57
57
RETURNING strike_count, banned_until;
58
58
`,
59
-
[userId]
59
+
[userId],
60
60
);
61
61
62
62
if (res.rows.length === 0) {
···
137
137
`UPDATE user_strikes
138
138
SET strike_count = 0, banned_until = NULL
139
139
WHERE last_strike_at < NOW() - INTERVAL '3 days'
140
-
RETURNING user_id`
140
+
RETURNING user_id`,
141
141
);
142
142
143
143
const resetCount = res.rows.length;
···
161
161
try {
162
162
const res = await pgClient.query(
163
163
'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1',
164
-
[userId]
164
+
[userId],
165
165
);
166
166
167
167
logger.info('Cleared user strikes', { userId });
+1
-1
src/utils/validation.ts
+1
-1
src/utils/validation.ts
···
9
9
10
10
function validateCommandOptions(
11
11
interaction: ChatInputCommandInteraction,
12
-
requiredOptions: string[] = []
12
+
requiredOptions: string[] = [],
13
13
): ValidationResult {
14
14
for (const option of requiredOptions) {
15
15
const value = interaction.options.getString(option);
+60
-11
web/src/App.tsx
+60
-11
web/src/App.tsx
···
22
22
return (
23
23
<Routes>
24
24
{/* Public routes */}
25
-
<Route path="/" element={<LandingPage />} />
25
+
<Route
26
+
path="/"
27
+
element={<LandingPage />}
28
+
/>
26
29
<Route
27
30
path="/login"
28
-
element={isAuthenticated ? <Navigate to="/dashboard" replace /> : <LoginPage />}
31
+
element={
32
+
isAuthenticated ? (
33
+
<Navigate
34
+
to="/dashboard"
35
+
replace
36
+
/>
37
+
) : (
38
+
<LoginPage />
39
+
)
40
+
}
29
41
/>
30
-
<Route path="/status" element={<StatusPage />} />
31
-
<Route path="/legal/privacy" element={<PrivacyPage />} />
32
-
<Route path="/legal/terms" element={<TermsPage />} />
42
+
<Route
43
+
path="/status"
44
+
element={<StatusPage />}
45
+
/>
46
+
<Route
47
+
path="/legal/privacy"
48
+
element={<PrivacyPage />}
49
+
/>
50
+
<Route
51
+
path="/legal/terms"
52
+
element={<TermsPage />}
53
+
/>
33
54
34
55
{/* Protected routes */}
35
56
{isAuthenticated ? (
···
38
59
element={
39
60
<Layout>
40
61
<Routes>
41
-
<Route path="/dashboard" element={<DashboardPage />} />
42
-
<Route path="/todos" element={<TodosPage />} />
43
-
<Route path="/reminders" element={<RemindersPage />} />
44
-
<Route path="/api-keys" element={<ApiKeysPage />} />
45
-
<Route path="*" element={<Navigate to="/dashboard" replace />} />
62
+
<Route
63
+
path="/dashboard"
64
+
element={<DashboardPage />}
65
+
/>
66
+
<Route
67
+
path="/todos"
68
+
element={<TodosPage />}
69
+
/>
70
+
<Route
71
+
path="/reminders"
72
+
element={<RemindersPage />}
73
+
/>
74
+
<Route
75
+
path="/api-keys"
76
+
element={<ApiKeysPage />}
77
+
/>
78
+
<Route
79
+
path="*"
80
+
element={
81
+
<Navigate
82
+
to="/dashboard"
83
+
replace
84
+
/>
85
+
}
86
+
/>
46
87
</Routes>
47
88
</Layout>
48
89
}
49
90
/>
50
91
) : (
51
-
<Route path="*" element={<Navigate to="/" replace />} />
92
+
<Route
93
+
path="*"
94
+
element={
95
+
<Navigate
96
+
to="/"
97
+
replace
98
+
/>
99
+
}
100
+
/>
52
101
)}
53
102
</Routes>
54
103
);
+4
-1
web/src/components/Layout.tsx
+4
-1
web/src/components/Layout.tsx
···
33
33
mobileMenuOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
34
34
}`}
35
35
>
36
-
<div className="fixed inset-0 bg-black/70" onClick={() => setMobileMenuOpen(false)} />
36
+
<div
37
+
className="fixed inset-0 bg-black/70"
38
+
onClick={() => setMobileMenuOpen(false)}
39
+
/>
37
40
<div
38
41
className={`fixed inset-y-0 left-0 flex w-72 flex-col bg-white/90 dark:bg-gray-800/90 backdrop-blur-md shadow-xl transform transition-transform duration-200 ease-out ${
39
42
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
+6
-1
web/src/components/LegalLayout.tsx
+6
-1
web/src/components/LegalLayout.tsx
···
32
32
to="/"
33
33
className="inline-flex items-center px-6 py-3 bg-white/90 hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-800 text-gray-800 dark:text-gray-100 font-medium rounded-full shadow-md hover:shadow-lg transition-all mb-8"
34
34
>
35
-
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
35
+
<svg
36
+
className="w-4 h-4 mr-2"
37
+
fill="none"
38
+
stroke="currentColor"
39
+
viewBox="0 0 24 24"
40
+
>
36
41
<path
37
42
strokeLinecap="round"
38
43
strokeLinejoin="round"
+2
-2
web/src/lib/api.ts
+2
-2
web/src/lib/api.ts
+1
-1
web/src/main.tsx
+1
-1
web/src/main.tsx
+4
-1
web/src/pages/ApiKeysPage.tsx
+4
-1
web/src/pages/ApiKeysPage.tsx
···
207
207
{apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key
208
208
</h2>
209
209
210
-
<form onSubmit={handleSubmit} className="space-y-4">
210
+
<form
211
+
onSubmit={handleSubmit}
212
+
className="space-y-4"
213
+
>
211
214
<div>
212
215
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
213
216
<span className="text-red-600 dark:text-red-400">*</span> You must test the API key
+10
-4
web/src/pages/DashboardPage.tsx
+10
-4
web/src/pages/DashboardPage.tsx
···
65
65
const recentTodos = activeTodosForDisplay.slice(0, 5);
66
66
const activeRemindersForDisplay =
67
67
reminders?.filter(
68
-
(reminder: Reminder) => !reminder.completed && new Date(reminder.expires_at) >= new Date()
68
+
(reminder: Reminder) => !reminder.completed && new Date(reminder.expires_at) >= new Date(),
69
69
) || [];
70
70
const recentReminders = activeRemindersForDisplay.slice(0, 5);
71
71
···
194
194
{recentTodos.length > 0 ? (
195
195
<div className="space-y-3">
196
196
{recentTodos.slice(0, 5).map((todo: Todo) => (
197
-
<div key={todo.id} className="stats-card">
197
+
<div
198
+
key={todo.id}
199
+
className="stats-card"
200
+
>
198
201
<div className="flex items-center justify-between">
199
202
<div className="flex items-center space-x-3 flex-1">
200
203
<div
···
269
272
const daysOverdue = isOverdue
270
273
? Math.ceil(
271
274
(Date.now() - new Date(reminder.expires_at).getTime()) /
272
-
(1000 * 60 * 60 * 24)
275
+
(1000 * 60 * 60 * 24),
273
276
)
274
277
: 0;
275
278
276
279
return (
277
-
<div key={reminder.reminder_id} className="stats-card">
280
+
<div
281
+
key={reminder.reminder_id}
282
+
className="stats-card"
283
+
>
278
284
<div className="flex items-center justify-between">
279
285
<div className="flex items-center space-x-3 flex-1">
280
286
<div
+19
-4
web/src/pages/LandingPage.tsx
+19
-4
web/src/pages/LandingPage.tsx
···
29
29
className="p-3 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 text-black dark:text-white rounded-full transition-all transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 dark:focus:ring-gray-600 shadow-lg hover:shadow-xl"
30
30
aria-label="View on GitHub"
31
31
>
32
-
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
32
+
<svg
33
+
className="w-6 h-6"
34
+
fill="currentColor"
35
+
viewBox="0 0 24 24"
36
+
aria-hidden="true"
37
+
>
33
38
<path
34
39
fillRule="evenodd"
35
40
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
···
187
192
188
193
<footer className="mt-16 text-center text-gray-500 dark:text-gray-400 pb-8">
189
194
<div className="flex flex-wrap justify-center gap-6 mb-6">
190
-
<Link to="/legal/privacy" className="hover:text-pink-500 transition-colors text-sm">
195
+
<Link
196
+
to="/legal/privacy"
197
+
className="hover:text-pink-500 transition-colors text-sm"
198
+
>
191
199
Privacy Policy
192
200
</Link>
193
-
<Link to="/legal/terms" className="hover:text-pink-500 transition-colors text-sm">
201
+
<Link
202
+
to="/legal/terms"
203
+
className="hover:text-pink-500 transition-colors text-sm"
204
+
>
194
205
Terms of Service
195
206
</Link>
196
207
</div>
···
203
214
rel="noopener noreferrer"
204
215
className="inline-block hover:opacity-80 transition-opacity"
205
216
>
206
-
<img src="/royale_logo.svg" alt="Royale Hosting" className="h-6 mx-auto dark:hidden" />
217
+
<img
218
+
src="/royale_logo.svg"
219
+
alt="Royale Hosting"
220
+
className="h-6 mx-auto dark:hidden"
221
+
/>
207
222
<img
208
223
src="/royale_logo_dark.svg"
209
224
alt="Royale Hosting"
+5
-1
web/src/pages/LoginPage.tsx
+5
-1
web/src/pages/LoginPage.tsx
···
68
68
onClick={handleDiscordLogin}
69
69
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"
70
70
>
71
-
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" fill="currentColor">
71
+
<svg
72
+
className="w-5 h-5 mr-3"
73
+
viewBox="0 0 24 24"
74
+
fill="currentColor"
75
+
>
72
76
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
73
77
</svg>
74
78
Continue with Discord
+4
-1
web/src/pages/PrivacyPage.tsx
+4
-1
web/src/pages/PrivacyPage.tsx
···
2
2
3
3
export default function PrivacyPolicy() {
4
4
return (
5
-
<LegalLayout title="Privacy Policy" lastUpdated="July 26, 2025">
5
+
<LegalLayout
6
+
title="Privacy Policy"
7
+
lastUpdated="July 26, 2025"
8
+
>
6
9
<div className="space-y-8">
7
10
<section>
8
11
<h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4">
+4
-1
web/src/pages/RemindersPage.tsx
+4
-1
web/src/pages/RemindersPage.tsx
···
150
150
<h2 className="text-lg sm:text-xl font-semibold mb-4 sm:mb-6 text-gray-900 dark:text-gray-100">
151
151
Create New Reminder
152
152
</h2>
153
-
<form onSubmit={handleCreateReminder} className="space-y-4 sm:space-y-6">
153
+
<form
154
+
onSubmit={handleCreateReminder}
155
+
className="space-y-4 sm:space-y-6"
156
+
>
154
157
<div>
155
158
<label
156
159
htmlFor="message"
+7
-2
web/src/pages/StatusPage.tsx
+7
-2
web/src/pages/StatusPage.tsx
···
9
9
headers: {
10
10
'X-API-Key': import.meta.env.VITE_STATUS_API_KEY || '',
11
11
},
12
-
}
12
+
},
13
13
);
14
14
15
15
if (!response.ok) {
···
287
287
to="/"
288
288
className="inline-flex items-center px-6 py-3 bg-white/90 hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-800 text-gray-800 dark:text-gray-100 font-medium rounded-full shadow-md hover:shadow-lg transition-all"
289
289
>
290
-
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
290
+
<svg
291
+
className="w-4 h-4 mr-2"
292
+
fill="none"
293
+
stroke="currentColor"
294
+
viewBox="0 0 24 24"
295
+
>
291
296
<path
292
297
strokeLinecap="round"
293
298
strokeLinejoin="round"
+4
-1
web/src/pages/TermsPage.tsx
+4
-1
web/src/pages/TermsPage.tsx
···
2
2
3
3
export default function TermsOfService() {
4
4
return (
5
-
<LegalLayout title="Terms of Service" lastUpdated="June 16, 2025">
5
+
<LegalLayout
6
+
title="Terms of Service"
7
+
lastUpdated="June 16, 2025"
8
+
>
6
9
<div className="space-y-8">
7
10
<section>
8
11
<h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4">
+4
-1
web/src/pages/TodosPage.tsx
+4
-1
web/src/pages/TodosPage.tsx
+2
-2
web/src/stores/authStore.ts
+2
-2
web/src/stores/authStore.ts