Compare commits
35 Commits
79835a3b47
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a2fdf73f8a | |||
| 7ff95a3d7f | |||
| a73a8901f2 | |||
| b1fff6f337 | |||
|
8b1ece727d
|
|||
| 7cf1582fee | |||
| 9dfdef46d5 | |||
| e5ebb203fa | |||
| 0e631bf029 | |||
| a7a6c1e321 | |||
|
ab91c98360
|
|||
|
4c38488161
|
|||
|
4cad39d0f6
|
|||
|
06ac40a6d7
|
|||
|
b362a43886
|
|||
|
e82b174bf5
|
|||
|
59590002b8
|
|||
|
2cee379491
|
|||
|
5e356d7b01
|
|||
|
f850e1ce1e
|
|||
|
abcc60e703
|
|||
|
af47efefcc
|
|||
|
940c2eb4cd
|
|||
|
7795662584
|
|||
|
2ac855b765
|
|||
|
94494c1ca5
|
|||
|
0cd3b5c179
|
|||
|
748d438706
|
|||
|
e5124d33d4
|
|||
|
5bc0d98334
|
|||
|
69dc616668
|
|||
|
d4e01a3cab
|
|||
|
bff1625c29
|
|||
| bf6dcae288 | |||
| 99ceb22019 |
@@ -0,0 +1,27 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
coverage
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.md
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
TESTING.md
|
||||||
|
PRIVACY.md
|
||||||
|
TERMS.md
|
||||||
|
SECURITY.md
|
||||||
|
ansible-README.md
|
||||||
|
__tests__
|
||||||
|
jest.config.js
|
||||||
|
jest.setup.js
|
||||||
|
.eslintrc.*
|
||||||
|
.prettierrc
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
inventory.ini
|
||||||
|
playbook.yml
|
||||||
+4
-4
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
es6: true,
|
es6: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'airbnb-base',
|
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'prettier',
|
'airbnb-base',
|
||||||
'plugin:prettier/recommended',
|
|
||||||
'plugin:import/errors',
|
'plugin:import/errors',
|
||||||
'plugin:import/warnings',
|
'plugin:import/warnings',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'prettier',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2023,
|
ecmaVersion: 2023,
|
||||||
@@ -30,7 +30,7 @@ module.exports = {
|
|||||||
'arrow-body-style': ['error', 'as-needed'],
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
'import/extensions': [
|
'import/extensions': [
|
||||||
'error',
|
'error',
|
||||||
'always',
|
'ignorePackages',
|
||||||
{
|
{
|
||||||
js: 'never',
|
js: 'never',
|
||||||
jsx: 'never',
|
jsx: 'never',
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: Deploy to NAS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate package-lock.json
|
||||||
|
run: npm ci --omit=dev
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: tea.kleptonix.com
|
||||||
|
username: ${{ secrets.TEA_USERNAME }}
|
||||||
|
password: ${{ secrets.TEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: tea.kleptonix.com/hllywluis/kekbot.js:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Generate .env file
|
||||||
|
run: |
|
||||||
|
cat > .env << EOF
|
||||||
|
DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}
|
||||||
|
CLIENT_ID=${{ secrets.DISCORD_CLIENT_ID }}
|
||||||
|
NANOGPT_API_KEY=${{ secrets.NANOGPT_API_KEY }}
|
||||||
|
ZAI_API_KEY=${{ secrets.ZAI_API_KEY }}
|
||||||
|
BRAVE_API_KEY=${{ secrets.BRAVE_API_KEY }}
|
||||||
|
NANO_MODEL=${{ secrets.NANO_MODEL }}
|
||||||
|
NODE_ENV=production
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Install SSH dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y sshpass
|
||||||
|
|
||||||
|
- name: Deploy to NAS
|
||||||
|
env:
|
||||||
|
SSH_PASSWORD: ${{ secrets.NAS_SSH_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Create app directory on NAS
|
||||||
|
sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no hllywluis@luis-nas.lan "mkdir -p /mnt/kCloud/Home/hllywluis/kekbot"
|
||||||
|
|
||||||
|
# Copy Docker files (without .env, it's generated separately)
|
||||||
|
sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no docker-compose.yml .env hllywluis@luis-nas.lan:/mnt/kCloud/Home/hllywluis/kekbot/
|
||||||
|
|
||||||
|
# Pull Docker image and deploy
|
||||||
|
sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no hllywluis@luis-nas.lan << 'EOF'
|
||||||
|
cd /mnt/kCloud/Home/hllywluis/kekbot
|
||||||
|
docker compose down || true
|
||||||
|
docker pull tea.kleptonix.com/hllywluis/kekbot.js:latest
|
||||||
|
docker compose up -d
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Docker Setup Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed on your system
|
||||||
|
- Docker Compose (optional, but recommended)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Create a `.env` file in the project root with your Discord bot credentials:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit the `.env` file and add your Discord bot token and other credentials:
|
||||||
|
```
|
||||||
|
DISCORD_TOKEN=your_discord_bot_token_here
|
||||||
|
CLIENT_ID=your_client_id_here
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running with Docker Compose (Recommended)
|
||||||
|
|
||||||
|
### Start the bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop the bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart the bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild and start (after code changes):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running with Docker directly
|
||||||
|
|
||||||
|
### Build the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t kekbot:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name kekbot \
|
||||||
|
--env-file .env \
|
||||||
|
--restart unless-stopped \
|
||||||
|
kekbot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f kekbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop kekbot
|
||||||
|
docker rm kekbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy Commands
|
||||||
|
|
||||||
|
Before running the bot for the first time, you may need to deploy slash commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Docker Compose
|
||||||
|
docker-compose run --rm kekbot npm run deploy
|
||||||
|
|
||||||
|
# Using Docker directly
|
||||||
|
docker run --rm --env-file .env kekbot:latest npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check if container is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### View all containers (including stopped):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access container shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it kekbot sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### View container resource usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stats kekbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
1. **Security**: Ensure your `.env` file is never committed to version control
|
||||||
|
2. **Logging**: Configure log rotation if using volume mounts for logs
|
||||||
|
3. **Updates**: Regularly update the base image and dependencies
|
||||||
|
4. **Monitoring**: Consider adding monitoring tools like Prometheus or Grafana
|
||||||
|
5. **Backups**: If you add persistent data, implement backup strategies
|
||||||
|
|
||||||
|
## Multi-stage Build (Alternative)
|
||||||
|
|
||||||
|
For a smaller production image, you can use the multi-stage Dockerfile variant:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Development stage
|
||||||
|
FROM node:20-alpine AS development
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY . .
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nodejs -u 1001 && \
|
||||||
|
chown -R nodejs:nodejs /app
|
||||||
|
USER nodejs
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
```
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
# Use Node.js LTS version
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port (if needed for health checks)
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the bot
|
||||||
|
CMD ["npm", "start"]
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Privacy Policy
|
||||||
|
|
||||||
|
**Effective Date:** March 12, 2025
|
||||||
|
|
||||||
|
**Kekbot-js** ("we," "our," or "us") is committed to respecting your privacy. This Privacy Policy outlines our practices regarding the collection, use, and disclosure of information when you use our software.
|
||||||
|
|
||||||
|
**Information Collection and Use**
|
||||||
|
|
||||||
|
We do not collect, store, or process any personal information from users of **Kekbot-js**.
|
||||||
|
|
||||||
|
**Changes to This Privacy Policy**
|
||||||
|
|
||||||
|
We may update our Privacy Policy from time to time. Any changes will be reflected on this page, and we encourage you to review this policy periodically.
|
||||||
|
|
||||||
|
**Contact Us**
|
||||||
|
|
||||||
|
If you have any questions or concerns about this Privacy Policy, please contact us at luis@kleptonix.com.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Terms and Conditions
|
||||||
|
|
||||||
|
**Kekbot-js** is an open-source project licensed under the [GNU General Public License v3.0](LICENSE). By using, copying, modifying, or distributing this software, you agree to the terms and conditions of this license.
|
||||||
|
|
||||||
|
**Disclaimer of Warranty**
|
||||||
|
|
||||||
|
**Kekbot-js** is provided "as-is," without any express or implied warranty. In no event shall the authors or copyright holders be held liable for any damages arising from the use of this software.
|
||||||
|
|
||||||
|
For detailed licensing information, please refer to the [LICENSE](LICENSE) file in this repository.
|
||||||
+54
-187
@@ -1,10 +1,15 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const { createMockInteraction } = require('../utils/testUtils');
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
jest.mock('axios');
|
jest.mock('axios', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
post: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const axios = require('axios').default;
|
||||||
|
|
||||||
// Mock the discord.js module
|
// Mock discord.js
|
||||||
jest.mock('discord.js', () => ({
|
jest.mock('discord.js', () => ({
|
||||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
setName: jest.fn().mockReturnThis(),
|
setName: jest.fn().mockReturnThis(),
|
||||||
@@ -17,33 +22,17 @@ jest.mock('discord.js', () => ({
|
|||||||
};
|
};
|
||||||
callback(option);
|
callback(option);
|
||||||
return {
|
return {
|
||||||
addBooleanOption: jest.fn().mockImplementation(boolCallback => {
|
toJSON: jest.fn().mockReturnValue({
|
||||||
const boolOption = {
|
name: 'ask',
|
||||||
setName: jest.fn().mockReturnThis(),
|
description: 'Ask a question to the AI',
|
||||||
setDescription: jest.fn().mockReturnThis(),
|
options: [
|
||||||
setRequired: jest.fn().mockReturnThis(),
|
{
|
||||||
};
|
name: 'prompt',
|
||||||
boolCallback(boolOption);
|
description: 'Your question or prompt',
|
||||||
return {
|
type: 3,
|
||||||
toJSON: jest.fn().mockReturnValue({
|
required: true,
|
||||||
name: 'ask',
|
},
|
||||||
description: 'Ask a question to the AI',
|
],
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'prompt',
|
|
||||||
description: 'Your question or prompt',
|
|
||||||
type: 3,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'websearch',
|
|
||||||
description: 'Enable web search for more up-to-date information',
|
|
||||||
type: 5,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -51,33 +40,30 @@ jest.mock('discord.js', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const askCommand = require('../../commands/ask');
|
const AskCommand = require('../../commands/ask').default;
|
||||||
|
const askCommand = new AskCommand();
|
||||||
|
|
||||||
describe('Ask Command', () => {
|
describe('Ask Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
it('should have correct name and description', () => {
|
it('should have correct name and description', () => {
|
||||||
const commandData = askCommand.data.toJSON();
|
const commandData = askCommand.defineCommand().toJSON();
|
||||||
expect(commandData.name).toBe('ask');
|
expect(commandData.name).toBe('ask');
|
||||||
expect(commandData.description).toBe('Ask a question to the AI');
|
expect(commandData.description).toBe('Ask a question to the AI');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have required command properties', () => {
|
it('should have required command properties', () => {
|
||||||
expect(askCommand).toHaveProperty('data');
|
expect(askCommand).toHaveProperty('defineCommand');
|
||||||
expect(askCommand).toHaveProperty('execute');
|
expect(askCommand).toHaveProperty('run');
|
||||||
expect(typeof askCommand.execute).toBe('function');
|
expect(typeof askCommand.run).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct option configuration', () => {
|
it('should have correct option configuration', () => {
|
||||||
const commandData = askCommand.data.toJSON();
|
const commandData = askCommand.defineCommand().toJSON();
|
||||||
const [promptOption, websearchOption] = commandData.options;
|
const [promptOption] = commandData.options;
|
||||||
|
|
||||||
expect(promptOption.name).toBe('prompt');
|
expect(promptOption.name).toBe('prompt');
|
||||||
expect(promptOption.description).toBe('Your question or prompt');
|
expect(promptOption.description).toBe('Your question or prompt');
|
||||||
expect(promptOption.required).toBe(true);
|
expect(promptOption.required).toBe(true);
|
||||||
|
|
||||||
expect(websearchOption.name).toBe('websearch');
|
|
||||||
expect(websearchOption.description).toBe('Enable web search for more up-to-date information');
|
|
||||||
expect(websearchOption.required).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,8 +83,7 @@ describe('Ask Command', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
process.env.NANOGPT_API_KEY = 'test-api-key';
|
||||||
axios.post.mockReset();
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
@@ -106,22 +91,18 @@ describe('Ask Command', () => {
|
|||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle successful API response with websearch disabled', async () => {
|
it('should handle successful API response', async () => {
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: 'google/gemini-2.0-flash-001',
|
model: 'zai-org/glm-4.7',
|
||||||
plugins: undefined,
|
|
||||||
}),
|
}),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
@@ -133,76 +114,7 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle successful API response with websearch enabled', async () => {
|
it('should handle long responses with proper chunking', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
|
||||||
expect.objectContaining({
|
|
||||||
model: 'google/gemini-2.0-flash-001:online',
|
|
||||||
plugins: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: 'web',
|
|
||||||
max_results: 3,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
||||||
content: expect.stringContaining('Web search enabled'),
|
|
||||||
split: false,
|
|
||||||
allowedMentions: { parse: [] },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default websearch value when not provided', async () => {
|
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
// Not providing websearch option
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
|
||||||
expect.objectContaining({
|
|
||||||
model: 'google/gemini-2.0-flash-001',
|
|
||||||
plugins: undefined,
|
|
||||||
}),
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle long responses with proper chunking and websearch enabled', async () => {
|
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const longResponse = 'A'.repeat(2500);
|
const longResponse = 'A'.repeat(2500);
|
||||||
axios.post.mockResolvedValueOnce({
|
axios.post.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
@@ -216,24 +128,13 @@ describe('Ask Command', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
||||||
expect(interaction.followUp.mock.calls[0][0].content).toContain('Web search enabled');
|
|
||||||
expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)');
|
expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle code blocks in chunked responses with websearch', async () => {
|
it('should handle code blocks in chunked responses', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
||||||
20,
|
20,
|
||||||
);
|
);
|
||||||
@@ -249,7 +150,7 @@ describe('Ask Command', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
// Verify that code blocks are not split in the middle
|
// Verify that code blocks are not split in the middle
|
||||||
interaction.followUp.mock.calls.forEach(call => {
|
interaction.followUp.mock.calls.forEach(call => {
|
||||||
@@ -257,23 +158,14 @@ describe('Ask Command', () => {
|
|||||||
const openBlocks = (content.match(/```/g) || []).length;
|
const openBlocks = (content.match(/```/g) || []).length;
|
||||||
expect(openBlocks % 2).toBe(0); // Should always be even
|
expect(openBlocks % 2).toBe(0); // Should always be even
|
||||||
});
|
});
|
||||||
|
|
||||||
// First chunk should contain websearch indicator
|
|
||||||
expect(interaction.followUp.mock.calls[0][0].content).toContain('Web search enabled');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle API errors with websearch enabled', async () => {
|
it('should handle API errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
booleanOptions: { websearch: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('API Error');
|
const error = new Error('API Error');
|
||||||
error.response = { data: 'API Error Details' };
|
error.response = { data: 'API Error Details' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'Sorry, there was an error processing your request.',
|
content: 'Sorry, there was an error processing your request.',
|
||||||
@@ -282,16 +174,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle rate limit errors', async () => {
|
it('should handle rate limit errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Rate Limit Exceeded');
|
const error = new Error('Rate Limit Exceeded');
|
||||||
error.response = { status: 429, data: 'Too Many Requests' };
|
error.response = { status: 429, data: 'Too Many Requests' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'The AI service is currently busy. Please try again in a few moments.',
|
content: 'The AI service is currently busy. Please try again in a few moments.',
|
||||||
@@ -300,16 +187,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle timeout errors', async () => {
|
it('should handle timeout errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Timeout');
|
const error = new Error('Timeout');
|
||||||
error.code = 'ETIMEDOUT';
|
error.code = 'ETIMEDOUT';
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'The request timed out. Please try again.',
|
content: 'The request timed out. Please try again.',
|
||||||
@@ -318,16 +200,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid request errors', async () => {
|
it('should handle invalid request errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Bad Request');
|
const error = new Error('Bad Request');
|
||||||
error.response = { status: 400, data: 'Invalid Request' };
|
error.response = { status: 400, data: 'Invalid Request' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'Invalid request. Please try rephrasing your question.',
|
content: 'Invalid request. Please try rephrasing your question.',
|
||||||
@@ -335,60 +212,50 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send API request with correct headers regardless of websearch', async () => {
|
it('should send API request with correct headers', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer test-api-key',
|
Authorization: 'Bearer test-api-key',
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle defer reply failure with websearch', async () => {
|
it('should handle defer reply failure', async () => {
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
commandName: 'ask',
|
commandName: 'ask',
|
||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
deferFails: true,
|
deferFails: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to defer reply');
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
|
expect(interaction.followUp).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
content: 'Sorry, there was an error processing your request.',
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle follow up failure with websearch', async () => {
|
it('should handle follow up failure', async () => {
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
commandName: 'ask',
|
commandName: 'ask',
|
||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
followUpFails: true,
|
followUpFails: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to follow up');
|
await expect(askCommand.run(interaction)).rejects.toThrow('Failed to follow up');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ jest.mock('discord.js', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const helpCommand = require('../../commands/help');
|
const HelpCommand = require('../../commands/help').default;
|
||||||
|
const helpCommand = new HelpCommand();
|
||||||
|
|
||||||
describe('Help Command', () => {
|
describe('Help Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ jest.mock('discord.js', () => ({
|
|||||||
};
|
};
|
||||||
callback(option);
|
callback(option);
|
||||||
return {
|
return {
|
||||||
addStringOption: jest.fn().mockImplementation(callback => {
|
addStringOption: jest.fn().mockImplementation(cb => {
|
||||||
const option = {
|
const strOption = {
|
||||||
setName: jest.fn().mockReturnThis(),
|
setName: jest.fn().mockReturnThis(),
|
||||||
setDescription: jest.fn().mockReturnThis(),
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
};
|
};
|
||||||
callback(option);
|
cb(strOption);
|
||||||
return {
|
return {
|
||||||
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
||||||
toJSON: jest.fn().mockReturnValue({
|
toJSON: jest.fn().mockReturnValue({
|
||||||
@@ -46,11 +47,12 @@ jest.mock('discord.js', () => ({
|
|||||||
toJSON: jest.fn(),
|
toJSON: jest.fn(),
|
||||||
}),
|
}),
|
||||||
PermissionFlagsBits: {
|
PermissionFlagsBits: {
|
||||||
KickMembers: 0x2n,
|
KickMembers: 0x0000000000000002n,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const kickCommand = require('../../commands/kick');
|
const KickCommand = require('../../commands/kick').default;
|
||||||
|
const kickCommand = new KickCommand();
|
||||||
|
|
||||||
describe('Kick Command', () => {
|
describe('Kick Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
@@ -152,7 +154,7 @@ describe('Kick Command', () => {
|
|||||||
|
|
||||||
expect(mockKick).not.toHaveBeenCalled();
|
expect(mockKick).not.toHaveBeenCalled();
|
||||||
expect(interaction.reply).toHaveBeenCalledWith({
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
content: 'That user is not in this server!',
|
content: 'Command failed: That user is not in this server!',
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -173,7 +175,7 @@ describe('Kick Command', () => {
|
|||||||
expect(mockKick).not.toHaveBeenCalled();
|
expect(mockKick).not.toHaveBeenCalled();
|
||||||
expect(interaction.reply).toHaveBeenCalledWith({
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
content:
|
content:
|
||||||
'I cannot kick this user! They may have higher permissions than me.',
|
'Command failed: I cannot kick this user! They may have higher permissions than me.',
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -195,7 +197,7 @@ describe('Kick Command', () => {
|
|||||||
await kickCommand.execute(interaction);
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
expect(interaction.reply).toHaveBeenCalledWith({
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
content: 'There was an error trying to kick this user!',
|
content: 'Command failed: Failed to kick user',
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ jest.mock('discord.js', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pingCommand = require('../../commands/ping');
|
const PingCommand = require('../../commands/ping').default;
|
||||||
|
const pingCommand = new PingCommand();
|
||||||
|
|
||||||
describe('Ping Command', () => {
|
describe('Ping Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
@@ -47,7 +48,7 @@ describe('Ping Command', () => {
|
|||||||
|
|
||||||
it('should handle interaction reply failure', async () => {
|
it('should handle interaction reply failure', async () => {
|
||||||
// Mock a failed reply
|
// Mock a failed reply
|
||||||
interaction.reply.mockRejectedValueOnce(new Error('Failed to reply'));
|
interaction.reply.mockRejectedValue(new Error('Failed to reply'));
|
||||||
|
|
||||||
await expect(pingCommand.execute(interaction)).rejects.toThrow(
|
await expect(pingCommand.execute(interaction)).rejects.toThrow(
|
||||||
'Failed to reply',
|
'Failed to reply',
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ jest.mock('discord.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pruneCommand = require('../../commands/prune');
|
const PruneCommand = require('../../commands/prune').default;
|
||||||
|
const pruneCommand = new PruneCommand();
|
||||||
|
|
||||||
describe('Prune Command', () => {
|
describe('Prune Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
@@ -103,7 +104,7 @@ describe('Prune Command', () => {
|
|||||||
await pruneCommand.execute(interaction);
|
await pruneCommand.execute(interaction);
|
||||||
|
|
||||||
expect(interaction.reply).toHaveBeenCalledWith({
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
content: 'There was an error trying to prune messages in this channel!',
|
content: 'Command failed: Failed to delete messages',
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -116,7 +117,7 @@ describe('Prune Command', () => {
|
|||||||
await pruneCommand.execute(interaction);
|
await pruneCommand.execute(interaction);
|
||||||
|
|
||||||
expect(interaction.reply).toHaveBeenCalledWith({
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
content: 'There was an error trying to prune messages in this channel!',
|
content: 'Command failed: Messages older than 14 days cannot be deleted',
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// Test utilities for Discord.js bot testing
|
// Test utilities for Discord.js bot testing
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mock interaction object with common properties and methods
|
* Creates a mock interaction object with common properties and methods
|
||||||
* @param {Object} options - Customization options for the mock interaction
|
* @param {Object} options - Customization options for the mock interaction
|
||||||
* @returns {Object} Mock interaction object
|
* @returns {Object} Mock interaction object
|
||||||
*/
|
*/
|
||||||
const createMockInteraction = (options = {}) => ({
|
export const createMockInteraction = (options = {}) => ({
|
||||||
commandName: options.commandName || 'test-command',
|
commandName: options.commandName || 'test-command',
|
||||||
user: {
|
user: {
|
||||||
id: options.userId || 'mock-user-id',
|
id: options.userId || 'mock-user-id',
|
||||||
@@ -74,7 +76,7 @@ const createMockInteraction = (options = {}) => ({
|
|||||||
* @param {Object} options - Customization options for the mock client
|
* @param {Object} options - Customization options for the mock client
|
||||||
* @returns {Object} Mock client object
|
* @returns {Object} Mock client object
|
||||||
*/
|
*/
|
||||||
const createMockClient = (options = {}) => ({
|
export const createMockClient = (options = {}) => ({
|
||||||
user: {
|
user: {
|
||||||
id: options.clientId || 'mock-client-id',
|
id: options.clientId || 'mock-client-id',
|
||||||
username: options.clientUsername || 'MockBot',
|
username: options.clientUsername || 'MockBot',
|
||||||
@@ -89,8 +91,3 @@ const createMockClient = (options = {}) => ({
|
|||||||
login: jest.fn().mockResolvedValue('token'),
|
login: jest.fn().mockResolvedValue('token'),
|
||||||
destroy: jest.fn().mockResolvedValue(),
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createMockInteraction,
|
|
||||||
createMockClient,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import path from 'node:path';
|
|||||||
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
|
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
|
||||||
import Logger from './logger.js';
|
import Logger from './logger.js';
|
||||||
import { loadCommands } from './utils/commandLoader.js';
|
import { loadCommands } from './utils/commandLoader.js';
|
||||||
|
import { getAIResponse, isAIEnabled, clearHistory } from './utils/ai.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -40,6 +41,57 @@ client.once(Events.ClientReady, () => {
|
|||||||
logger.log(`Ready! Logged in as ${client.user.tag}`);
|
logger.log(`Ready! Logged in as ${client.user.tag}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle messages where the bot is mentioned
|
||||||
|
client.on(Events.MessageCreate, async message => {
|
||||||
|
// Ignore bot messages
|
||||||
|
if (message.author.bot) return;
|
||||||
|
|
||||||
|
// Check if bot is mentioned
|
||||||
|
const botId = client.user.id;
|
||||||
|
const mentioned = message.mentions.has(botId);
|
||||||
|
|
||||||
|
if (!mentioned) return;
|
||||||
|
|
||||||
|
// Don't respond to commands (they start with /)
|
||||||
|
if (message.content.trim().startsWith('/')) return;
|
||||||
|
|
||||||
|
if (!isAIEnabled()) {
|
||||||
|
logger.warn('AI response requested but NANOGPT_API_KEY not set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the clean prompt (remove the bot mention)
|
||||||
|
const cleanPrompt = message.content
|
||||||
|
.replace(new RegExp(`<@!?${botId}>`, 'g'), '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!cleanPrompt) return;
|
||||||
|
|
||||||
|
// Get username of who mentioned us
|
||||||
|
const mentionedUsername = message.author.username;
|
||||||
|
|
||||||
|
logger.log(`Mentioned by ${mentionedUsername} in #${message.channel.name}: ${cleanPrompt.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
// Typing indicator
|
||||||
|
await message.channel.sendTyping();
|
||||||
|
|
||||||
|
const response = await getAIResponse(cleanPrompt, message.channel.id, mentionedUsername);
|
||||||
|
|
||||||
|
// Send response (avoiding mention loop)
|
||||||
|
await message.reply({
|
||||||
|
content: response,
|
||||||
|
allowedMentions: { users: [] }, // Prevent mentioning the user back
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('AI response failed:', error.message);
|
||||||
|
await message.reply({
|
||||||
|
content: 'lol damn, something broke. try again in a sec',
|
||||||
|
allowedMentions: { users: [] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
client.on(Events.InteractionCreate, async interaction => {
|
client.on(Events.InteractionCreate, async interaction => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
@@ -53,18 +105,24 @@ client.on(Events.InteractionCreate, async interaction => {
|
|||||||
try {
|
try {
|
||||||
await command.execute(interaction);
|
await command.execute(interaction);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
// Log detailed error context
|
||||||
if (interaction.replied || interaction.deferred) {
|
logger.error(
|
||||||
await interaction.followUp({
|
`Command "${interaction.commandName}" failed for user ${interaction.user.tag}:`,
|
||||||
content: 'There was an error while executing this command!',
|
error,
|
||||||
ephemeral: true,
|
);
|
||||||
});
|
|
||||||
} else {
|
// Prepare error response
|
||||||
await interaction.reply({
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
content: 'There was an error while executing this command!',
|
const errorMessage = isProduction
|
||||||
ephemeral: true,
|
? `❌ Command failed. Please try again later.`
|
||||||
});
|
: `❌ Command failed: ${error.message}\n\n${error.stack}`;
|
||||||
}
|
|
||||||
|
const responseMethod = interaction.replied || interaction.deferred ? 'followUp' : 'reply';
|
||||||
|
|
||||||
|
await interaction[responseMethod]({
|
||||||
|
content: errorMessage,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+52
-99
@@ -8,83 +8,61 @@
|
|||||||
|
|
||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
const config = {
|
export default class AskCommand extends Command {
|
||||||
webSearch: {
|
defineCommand() {
|
||||||
enabled: false, // Default web search state
|
return new SlashCommandBuilder()
|
||||||
allowOverride: true, // Whether users can override the default state
|
.setName('ask')
|
||||||
},
|
.setDescription('Ask a question to the AI')
|
||||||
};
|
.addStringOption(option =>
|
||||||
|
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
||||||
export default {
|
);
|
||||||
data: new SlashCommandBuilder()
|
}
|
||||||
.setName('ask')
|
|
||||||
.setDescription('Ask a question to the AI')
|
|
||||||
.addStringOption(option =>
|
|
||||||
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
|
||||||
)
|
|
||||||
.addBooleanOption(option =>
|
|
||||||
option
|
|
||||||
.setName('websearch')
|
|
||||||
.setDescription('Enable web search for more up-to-date information')
|
|
||||||
.setRequired(false),
|
|
||||||
),
|
|
||||||
|
|
||||||
async execute(interaction) {
|
|
||||||
await interaction.deferReply();
|
|
||||||
|
|
||||||
const prompt = interaction.options.getString('prompt');
|
|
||||||
const userWebSearchOption = interaction.options.getBoolean('websearch');
|
|
||||||
|
|
||||||
// Determine if web search should be enabled based on config and user option
|
|
||||||
const webSearchEnabled =
|
|
||||||
config.webSearch.allowOverride && userWebSearchOption !== null
|
|
||||||
? userWebSearchOption // Use user's choice if override is allowed and option was provided
|
|
||||||
: config.webSearch.enabled; // Otherwise use default config
|
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
try {
|
try {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const prompt = interaction.options.getString('prompt');
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
{
|
{
|
||||||
model: webSearchEnabled
|
model: 'zai-org/glm-4.7',
|
||||||
? 'google/gemini-2.0-flash-001:online'
|
|
||||||
: 'google/gemini-2.0-flash-001',
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content:
|
content: `You are kekbot, an expert software engineer and systems architect.
|
||||||
'You are a helpful AI assistant. Provide clear, concise, and accurate responses. ' +
|
Answer style:
|
||||||
'Keep your answers brief while ensuring they are informative and to the point. ' +
|
- Be direct: lead with the core solution; no preamble.
|
||||||
'Avoid unnecessary elaboration or repetition. ',
|
- Be concise: 2-5 sentences or 3-7 bullets max.
|
||||||
|
- Use precise technical terms and industry-standard practices.
|
||||||
|
- Prefer bullets, numbered steps, or short code blocks when they clarify.
|
||||||
|
- Provide minimal, runnable examples only when essential; annotate fences with the language (e.g., \`\`\`js).
|
||||||
|
- State assumptions briefly; ask at most one clarifying question only if essential.
|
||||||
|
- Avoid repetition, hedging, and restating the prompt.
|
||||||
|
- For commands/configs, show exact snippets and key flags; avoid commentary.
|
||||||
|
- If unsupported/unsafe/unknown, say so plainly and suggest the best alternative.
|
||||||
|
- Do not mention internal system details or the model; no emojis.
|
||||||
|
|
||||||
|
Formatting:
|
||||||
|
- Use Markdown; start with the core answer, then bullets or a compact snippet.
|
||||||
|
- Keep responses within Discord limits; split naturally at paragraph or code block boundaries.`,
|
||||||
},
|
},
|
||||||
{ role: 'user', content: prompt },
|
{ role: 'user', content: prompt },
|
||||||
],
|
],
|
||||||
plugins: webSearchEnabled
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 'web',
|
|
||||||
max_results: 3,
|
|
||||||
search_prompt:
|
|
||||||
`A web search was conducted on ${new Date().toISOString()}. ` +
|
|
||||||
'Incorporate the following web search results into your response. ' +
|
|
||||||
'IMPORTANT: Cite them using markdown links named using the domain of the source. ' +
|
|
||||||
'Example: [nytimes.com](https://nytimes.com/some-page).',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiResponse = response.data.choices[0].message.content;
|
const aiResponse = response.data.choices[0].message.content;
|
||||||
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
const formattedResponse = `> **Question:** ${prompt}\n${aiResponse}`;
|
||||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
|
||||||
|
|
||||||
if (formattedResponse.length <= 2000) {
|
if (formattedResponse.length <= 2000) {
|
||||||
// Send as a single message with proper formatting
|
// Send as a single message with proper formatting
|
||||||
@@ -132,54 +110,29 @@ export default {
|
|||||||
await chunks.reduce(
|
await chunks.reduce(
|
||||||
(promise, chunk) =>
|
(promise, chunk) =>
|
||||||
promise.then(async () => {
|
promise.then(async () => {
|
||||||
try {
|
await interaction.followUp(chunk);
|
||||||
await interaction.followUp(chunk);
|
|
||||||
return undefined;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message chunk:', {
|
|
||||||
chunkLength: chunk.content.length,
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
// Attempt to send error notification
|
|
||||||
try {
|
|
||||||
await interaction.followUp({
|
|
||||||
content: 'Failed to send complete response. Please try again.',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If even the error notification fails, log it
|
|
||||||
console.error('Failed to send error notification:', e.message);
|
|
||||||
}
|
|
||||||
// Reject to stop processing remaining chunks
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log detailed error information
|
|
||||||
console.error('Error in ask command:', {
|
|
||||||
message: error.message,
|
|
||||||
response: error.response?.data,
|
|
||||||
status: error.response?.status,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Provide more specific error messages to users
|
|
||||||
let errorMessage = 'Sorry, there was an error processing your request.';
|
|
||||||
if (error.response?.status === 429) {
|
|
||||||
errorMessage = 'The AI service is currently busy. Please try again in a few moments.';
|
|
||||||
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
|
||||||
errorMessage = 'The request timed out. Please try again.';
|
|
||||||
} else if (error.response?.status === 400) {
|
|
||||||
errorMessage = 'Invalid request. Please try rephrasing your question.';
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
content: errorMessage,
|
content: this.getErrorMessage(error),
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
getErrorMessage(error) {
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
return 'The AI service is currently busy. Please try again in a few moments.';
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||||
|
return 'The request timed out. Please try again.';
|
||||||
|
}
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
return 'Invalid request. Please try rephrasing your question.';
|
||||||
|
}
|
||||||
|
return 'Sorry, there was an error processing your request.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-5
@@ -7,10 +7,14 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||||
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
export default {
|
export default class HelpCommand extends Command {
|
||||||
data: new SlashCommandBuilder().setName('help').setDescription('Lists all available commands'),
|
defineCommand() {
|
||||||
async execute(interaction) {
|
return new SlashCommandBuilder().setName('help').setDescription('Lists all available commands');
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
const { commands } = interaction.client;
|
const { commands } = interaction.client;
|
||||||
const helpEmbed = new EmbedBuilder()
|
const helpEmbed = new EmbedBuilder()
|
||||||
.setColor('#5dc67b')
|
.setColor('#5dc67b')
|
||||||
@@ -26,5 +30,5 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
+22
-35
@@ -7,49 +7,36 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
// eslint-disable-next-line import/extensions
|
export default class KickCommand extends Command {
|
||||||
import logger from '../logger.js';
|
defineCommand() {
|
||||||
|
return new SlashCommandBuilder()
|
||||||
|
.setName('kick')
|
||||||
|
.setDescription('Kick a user from the server')
|
||||||
|
.addUserOption(option =>
|
||||||
|
option.setName('target').setDescription('The user to kick').setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption(option => option.setName('reason').setDescription('Reason for kicking'))
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
async run(interaction) {
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('kick')
|
|
||||||
.setDescription('Kick a user from the server')
|
|
||||||
.addUserOption(option =>
|
|
||||||
option.setName('target').setDescription('The user to kick').setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(option => option.setName('reason').setDescription('Reason for kicking'))
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
|
|
||||||
async execute(interaction) {
|
|
||||||
const target = interaction.options.getMember('target');
|
const target = interaction.options.getMember('target');
|
||||||
const reason = interaction.options.getString('reason') ?? 'No reason provided';
|
const reason = interaction.options.getString('reason') ?? 'No reason provided';
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return interaction.reply({
|
throw new Error('That user is not in this server!');
|
||||||
content: 'That user is not in this server!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.kickable) {
|
if (!target.kickable) {
|
||||||
return interaction.reply({
|
throw new Error('I cannot kick this user! They may have higher permissions than me.');
|
||||||
content: 'I cannot kick this user! They may have higher permissions than me.',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await target.kick(reason);
|
||||||
await target.kick(reason);
|
await interaction.reply({
|
||||||
return await interaction.reply({
|
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
||||||
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
ephemeral: true,
|
||||||
ephemeral: true,
|
});
|
||||||
});
|
}
|
||||||
} catch (error) {
|
}
|
||||||
logger.error(error);
|
|
||||||
return interaction.reply({
|
|
||||||
content: 'There was an error trying to kick this user!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
+9
-5
@@ -7,10 +7,14 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
export default {
|
export default class PingCommand extends Command {
|
||||||
data: new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!'),
|
defineCommand() {
|
||||||
async execute(interaction) {
|
return new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
await interaction.reply('Pong! 🏓');
|
await interaction.reply('Pong! 🏓');
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
+25
-30
@@ -7,35 +7,30 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
export default {
|
export default class PruneCommand extends Command {
|
||||||
data: new SlashCommandBuilder()
|
defineCommand() {
|
||||||
.setName('prune')
|
return new SlashCommandBuilder()
|
||||||
.setDescription('Prune up to 99 messages.')
|
.setName('prune')
|
||||||
.addIntegerOption(option =>
|
.setDescription('Prune up to 99 messages.')
|
||||||
option
|
.addIntegerOption(option =>
|
||||||
.setName('amount')
|
option
|
||||||
.setDescription('Number of messages to prune')
|
.setName('amount')
|
||||||
.setMinValue(1)
|
.setDescription('Number of messages to prune')
|
||||||
.setMaxValue(99)
|
.setMinValue(1)
|
||||||
.setRequired(true),
|
.setMaxValue(99)
|
||||||
)
|
.setRequired(true),
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
)
|
||||||
async execute(interaction) {
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
const amount = interaction.options.getInteger('amount');
|
const amount = interaction.options.getInteger('amount');
|
||||||
|
const deleted = await interaction.channel.bulkDelete(amount, true);
|
||||||
try {
|
await interaction.reply({
|
||||||
const deleted = await interaction.channel.bulkDelete(amount, true);
|
content: `Successfully deleted ${deleted.size} message(s).`,
|
||||||
await interaction.reply({
|
ephemeral: true,
|
||||||
content: `Successfully deleted ${deleted.size} message(s).`,
|
});
|
||||||
ephemeral: true,
|
}
|
||||||
});
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
await interaction.reply({
|
|
||||||
content: 'There was an error trying to prune messages in this channel!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
kekbot:
|
||||||
|
image: tea.kleptonix.com/hllywluis/kekbot.js:latest
|
||||||
|
container_name: kekbot
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NANO_MODEL=${NANO_MODEL}
|
||||||
|
- ZAI_API_KEY=${ZAI_API_KEY}
|
||||||
|
- BRAVE_API_KEY=${BRAVE_API_KEY}
|
||||||
|
volumes:
|
||||||
|
# Mount logs directory if you want to persist logs
|
||||||
|
- ./logs:/app/logs
|
||||||
|
# Uncomment if you need to expose a port for health checks
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
|
# Health check (optional)
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "process.exit(0)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
+23
-25
@@ -1,33 +1,31 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
// Automatically clear mock calls and instances between every test
|
// Automatically clear mock calls and instances between every test
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
// The directory where Jest should output its coverage files
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: 'coverage',
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
// The test environment that will be used for testing
|
||||||
testEnvironment: "node",
|
testEnvironment: 'node',
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: [
|
testMatch: [
|
||||||
"**/__tests__/**/*.test.[jt]s?(x)",
|
'**/__tests__/**/*.test.[jt]s?(x)',
|
||||||
"**/__tests__/**/*.spec.[jt]s?(x)",
|
'**/__tests__/**/*.spec.[jt]s?(x)',
|
||||||
"**/?(*.)+(spec|test).[jt]s?(x)"
|
'**/?(*.)+(spec|test).[jt]s?(x)',
|
||||||
],
|
],
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
// A map from regular expressions to paths to transformers
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.jsx?$": "babel-jest"
|
'^.+\\.jsx?$': 'babel-jest',
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test files
|
// Ignore node_modules except for chalk (which is ESM only)
|
||||||
testPathIgnorePatterns: [
|
transformIgnorePatterns: ['/node_modules/(?!(chalk)/)'],
|
||||||
"/node_modules/"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Setup files that will be run before each test
|
// An array of regexp pattern strings that are matched against all test files
|
||||||
setupFiles: ["<rootDir>/jest.setup.js"]
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
};
|
};
|
||||||
+32
-30
@@ -1,36 +1,38 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
// Mock Discord.js Client and other commonly used classes
|
// Mock Discord.js Client and other commonly used classes
|
||||||
jest.mock('discord.js', () => ({
|
jest.mock('discord.js', () => ({
|
||||||
Client: jest.fn(() => ({
|
Client: jest.fn(() => ({
|
||||||
login: jest.fn().mockResolvedValue('token'),
|
login: jest.fn().mockResolvedValue('token'),
|
||||||
destroy: jest.fn().mockResolvedValue(),
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
once: jest.fn(),
|
once: jest.fn(),
|
||||||
user: {
|
user: {
|
||||||
setActivity: jest.fn()
|
setActivity: jest.fn(),
|
||||||
}
|
|
||||||
})),
|
|
||||||
Collection: jest.fn(() => ({
|
|
||||||
get: jest.fn(),
|
|
||||||
set: jest.fn(),
|
|
||||||
has: jest.fn(),
|
|
||||||
delete: jest.fn()
|
|
||||||
})),
|
|
||||||
GatewayIntentBits: {
|
|
||||||
Guilds: 1,
|
|
||||||
GuildMessages: 2,
|
|
||||||
MessageContent: 3
|
|
||||||
},
|
},
|
||||||
Events: {
|
})),
|
||||||
ClientReady: 'ready',
|
Collection: jest.fn(() => ({
|
||||||
InteractionCreate: 'interactionCreate'
|
get: jest.fn(),
|
||||||
},
|
set: jest.fn(),
|
||||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
has: jest.fn(),
|
||||||
setName: jest.fn().mockReturnThis(),
|
delete: jest.fn(),
|
||||||
setDescription: jest.fn().mockReturnThis(),
|
})),
|
||||||
addStringOption: jest.fn().mockReturnThis(),
|
GatewayIntentBits: {
|
||||||
addIntegerOption: jest.fn().mockReturnThis(),
|
Guilds: 1,
|
||||||
addUserOption: jest.fn().mockReturnThis()
|
GuildMessages: 2,
|
||||||
})
|
MessageContent: 3,
|
||||||
|
},
|
||||||
|
Events: {
|
||||||
|
ClientReady: 'ready',
|
||||||
|
InteractionCreate: 'interactionCreate',
|
||||||
|
},
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
addStringOption: jest.fn().mockReturnThis(),
|
||||||
|
addIntegerOption: jest.fn().mockReturnThis(),
|
||||||
|
addUserOption: jest.fn().mockReturnThis(),
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock environment variables
|
// Mock environment variables
|
||||||
|
|||||||
@@ -1,28 +1,79 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced logger utility with timestamps and consistent formatting
|
||||||
|
*/
|
||||||
class Logger {
|
class Logger {
|
||||||
|
/**
|
||||||
|
* Create a new Logger instance
|
||||||
|
* @param {string} moduleName - Name of the module for context
|
||||||
|
*/
|
||||||
constructor(moduleName) {
|
constructor(moduleName) {
|
||||||
|
if (typeof moduleName !== 'string') {
|
||||||
|
throw new Error('Logger requires a string moduleName');
|
||||||
|
}
|
||||||
this.moduleName = moduleName;
|
this.moduleName = moduleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted timestamp
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#getTimestamp() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format log message with consistent structure
|
||||||
|
* @private
|
||||||
|
* @param {string} emoji - Log level emoji
|
||||||
|
* @param {string} level - Log level name
|
||||||
|
* @param {string} message - Message to log
|
||||||
|
*/
|
||||||
|
#formatMessage(emoji, level, message) {
|
||||||
|
const timestamp = this.#getTimestamp();
|
||||||
|
return `${chalk.gray(timestamp)} ${emoji} ${chalk.cyan(`[${this.moduleName}]`)} ${level}: ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log informational message
|
||||||
|
* @param {string} message - Message to log
|
||||||
|
*/
|
||||||
log(message) {
|
log(message) {
|
||||||
console.log(`${chalk.blue('📝')} ${chalk.blue(`[${this.moduleName}]`)} ${message}`);
|
console.log(this.#formatMessage(chalk.blue('📝'), 'LOG', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log error message
|
||||||
|
* @param {string|Error} message - Error message or Error object
|
||||||
|
*/
|
||||||
error(message) {
|
error(message) {
|
||||||
console.error(`${chalk.red('❌')} ${chalk.red(`[${this.moduleName}]`)} ${message}`);
|
const msg = message instanceof Error ? message.stack || message.message : message;
|
||||||
|
console.error(this.#formatMessage(chalk.red('❌'), 'ERROR', msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log warning message
|
||||||
|
* @param {string} message - Warning message
|
||||||
|
*/
|
||||||
warn(message) {
|
warn(message) {
|
||||||
console.warn(`${chalk.yellow('⚠️')} ${chalk.yellow(`[${this.moduleName}]`)} ${message}`);
|
console.warn(this.#formatMessage(chalk.yellow('⚠️'), 'WARN', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log info message
|
||||||
|
* @param {string} message - Info message
|
||||||
|
*/
|
||||||
info(message) {
|
info(message) {
|
||||||
console.info(`${chalk.green('ℹ️')} ${chalk.green(`[${this.moduleName}]`)} ${message}`);
|
console.info(this.#formatMessage(chalk.green('ℹ️'), 'INFO', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log debug message
|
||||||
|
* @param {string} message - Debug message
|
||||||
|
*/
|
||||||
debug(message) {
|
debug(message) {
|
||||||
console.debug(`${chalk.gray('🔧')} ${chalk.gray(`[${this.moduleName}]`)} ${message}`);
|
console.debug(this.#formatMessage(chalk.gray('🔧'), 'DEBUG', message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+149
-149
@@ -9,23 +9,23 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.8.4",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"discord.js": "^14.17.3",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.3.1"
|
"dotenv": "^16.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.7",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/preset-env": "^7.26.7",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
"eslint-plugin-prettier": "^5.2.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2"
|
"prettier": "^3.5.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@@ -58,9 +58,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.26.5",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
||||||
"integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
|
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -68,22 +68,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
||||||
"integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
|
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/generator": "^7.26.5",
|
"@babel/generator": "^7.26.10",
|
||||||
"@babel/helper-compilation-targets": "^7.26.5",
|
"@babel/helper-compilation-targets": "^7.26.5",
|
||||||
"@babel/helper-module-transforms": "^7.26.0",
|
"@babel/helper-module-transforms": "^7.26.0",
|
||||||
"@babel/helpers": "^7.26.7",
|
"@babel/helpers": "^7.26.10",
|
||||||
"@babel/parser": "^7.26.7",
|
"@babel/parser": "^7.26.10",
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.26.9",
|
||||||
"@babel/traverse": "^7.26.7",
|
"@babel/traverse": "^7.26.10",
|
||||||
"@babel/types": "^7.26.7",
|
"@babel/types": "^7.26.10",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
@@ -99,14 +99,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.26.5",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
|
||||||
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
|
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.26.5",
|
"@babel/parser": "^7.27.0",
|
||||||
"@babel/types": "^7.26.5",
|
"@babel/types": "^7.27.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@@ -367,27 +367,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.26.7",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
|
||||||
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
|
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.27.0",
|
||||||
"@babel/types": "^7.26.7"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.26.7",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||||
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
|
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.7"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -782,15 +782,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz",
|
||||||
"integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==",
|
"integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.25.9",
|
"@babel/helper-plugin-utils": "^7.26.5",
|
||||||
"@babel/helper-remap-async-to-generator": "^7.25.9",
|
"@babel/helper-remap-async-to-generator": "^7.25.9",
|
||||||
"@babel/traverse": "^7.25.9"
|
"@babel/traverse": "^7.26.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1036,13 +1036,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-for-of": {
|
"node_modules/@babel/plugin-transform-for-of": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
|
||||||
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
|
"integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.25.9",
|
"@babel/helper-plugin-utils": "^7.26.5",
|
||||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1504,13 +1504,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-template-literals": {
|
"node_modules/@babel/plugin-transform-template-literals": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz",
|
||||||
"integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==",
|
"integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.25.9"
|
"@babel/helper-plugin-utils": "^7.26.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1603,13 +1603,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env": {
|
"node_modules/@babel/preset-env": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
|
||||||
"integrity": "sha512-Ycg2tnXwixaXOVb29rana8HNPgLVBof8qqtNQ9LE22IoyZboQbGSxI6ZySMdW3K5nAe6gu35IaJefUJflhUFTQ==",
|
"integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.26.5",
|
"@babel/compat-data": "^7.26.8",
|
||||||
"@babel/helper-compilation-targets": "^7.26.5",
|
"@babel/helper-compilation-targets": "^7.26.5",
|
||||||
"@babel/helper-plugin-utils": "^7.26.5",
|
"@babel/helper-plugin-utils": "^7.26.5",
|
||||||
"@babel/helper-validator-option": "^7.25.9",
|
"@babel/helper-validator-option": "^7.25.9",
|
||||||
@@ -1623,7 +1623,7 @@
|
|||||||
"@babel/plugin-syntax-import-attributes": "^7.26.0",
|
"@babel/plugin-syntax-import-attributes": "^7.26.0",
|
||||||
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.25.9",
|
"@babel/plugin-transform-arrow-functions": "^7.25.9",
|
||||||
"@babel/plugin-transform-async-generator-functions": "^7.25.9",
|
"@babel/plugin-transform-async-generator-functions": "^7.26.8",
|
||||||
"@babel/plugin-transform-async-to-generator": "^7.25.9",
|
"@babel/plugin-transform-async-to-generator": "^7.25.9",
|
||||||
"@babel/plugin-transform-block-scoped-functions": "^7.26.5",
|
"@babel/plugin-transform-block-scoped-functions": "^7.26.5",
|
||||||
"@babel/plugin-transform-block-scoping": "^7.25.9",
|
"@babel/plugin-transform-block-scoping": "^7.25.9",
|
||||||
@@ -1638,7 +1638,7 @@
|
|||||||
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
||||||
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
||||||
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
|
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
|
||||||
"@babel/plugin-transform-for-of": "^7.25.9",
|
"@babel/plugin-transform-for-of": "^7.26.9",
|
||||||
"@babel/plugin-transform-function-name": "^7.25.9",
|
"@babel/plugin-transform-function-name": "^7.25.9",
|
||||||
"@babel/plugin-transform-json-strings": "^7.25.9",
|
"@babel/plugin-transform-json-strings": "^7.25.9",
|
||||||
"@babel/plugin-transform-literals": "^7.25.9",
|
"@babel/plugin-transform-literals": "^7.25.9",
|
||||||
@@ -1666,7 +1666,7 @@
|
|||||||
"@babel/plugin-transform-shorthand-properties": "^7.25.9",
|
"@babel/plugin-transform-shorthand-properties": "^7.25.9",
|
||||||
"@babel/plugin-transform-spread": "^7.25.9",
|
"@babel/plugin-transform-spread": "^7.25.9",
|
||||||
"@babel/plugin-transform-sticky-regex": "^7.25.9",
|
"@babel/plugin-transform-sticky-regex": "^7.25.9",
|
||||||
"@babel/plugin-transform-template-literals": "^7.25.9",
|
"@babel/plugin-transform-template-literals": "^7.26.8",
|
||||||
"@babel/plugin-transform-typeof-symbol": "^7.26.7",
|
"@babel/plugin-transform-typeof-symbol": "^7.26.7",
|
||||||
"@babel/plugin-transform-unicode-escapes": "^7.25.9",
|
"@babel/plugin-transform-unicode-escapes": "^7.25.9",
|
||||||
"@babel/plugin-transform-unicode-property-regex": "^7.25.9",
|
"@babel/plugin-transform-unicode-property-regex": "^7.25.9",
|
||||||
@@ -1674,9 +1674,9 @@
|
|||||||
"@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
|
"@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
|
||||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||||
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
||||||
"babel-plugin-polyfill-corejs3": "^0.10.6",
|
"babel-plugin-polyfill-corejs3": "^0.11.0",
|
||||||
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
||||||
"core-js-compat": "^3.38.1",
|
"core-js-compat": "^3.40.0",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1702,9 +1702,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.26.7",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1715,32 +1715,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.25.9",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/parser": "^7.25.9",
|
"@babel/parser": "^7.27.0",
|
||||||
"@babel/types": "^7.25.9"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.26.7",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
|
||||||
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
|
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/generator": "^7.26.5",
|
"@babel/generator": "^7.27.0",
|
||||||
"@babel/parser": "^7.26.7",
|
"@babel/parser": "^7.27.0",
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.27.0",
|
||||||
"@babel/types": "^7.26.7",
|
"@babel/types": "^7.27.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@@ -1749,9 +1749,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.26.7",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||||
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
|
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1770,15 +1770,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/builders": {
|
"node_modules/@discordjs/builders": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz",
|
||||||
"integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==",
|
"integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/formatters": "^0.6.0",
|
"@discordjs/formatters": "^0.6.0",
|
||||||
"@discordjs/util": "^1.1.1",
|
"@discordjs/util": "^1.1.1",
|
||||||
"@sapphire/shapeshift": "^4.0.0",
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
"discord-api-types": "^0.37.114",
|
"discord-api-types": "^0.37.119",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"ts-mixer": "^6.0.4",
|
"ts-mixer": "^6.0.4",
|
||||||
"tslib": "^2.6.3"
|
"tslib": "^2.6.3"
|
||||||
@@ -1815,9 +1815,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/rest": {
|
"node_modules/@discordjs/rest": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
|
||||||
"integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==",
|
"integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^2.1.1",
|
"@discordjs/collection": "^2.1.1",
|
||||||
@@ -1825,10 +1825,10 @@
|
|||||||
"@sapphire/async-queue": "^1.5.3",
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
"@sapphire/snowflake": "^3.5.3",
|
"@sapphire/snowflake": "^3.5.3",
|
||||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
"discord-api-types": "^0.37.114",
|
"discord-api-types": "^0.37.119",
|
||||||
"magic-bytes.js": "^1.10.0",
|
"magic-bytes.js": "^1.10.0",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"undici": "6.19.8"
|
"undici": "6.21.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1862,18 +1862,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/ws": {
|
"node_modules/@discordjs/ws": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz",
|
||||||
"integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==",
|
"integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^2.1.0",
|
"@discordjs/collection": "^2.1.0",
|
||||||
"@discordjs/rest": "^2.4.1",
|
"@discordjs/rest": "^2.4.3",
|
||||||
"@discordjs/util": "^1.1.0",
|
"@discordjs/util": "^1.1.0",
|
||||||
"@sapphire/async-queue": "^1.5.2",
|
"@sapphire/async-queue": "^1.5.2",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@vladfrangu/async_event_emitter": "^2.2.4",
|
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||||
"discord-api-types": "^0.37.114",
|
"discord-api-types": "^0.37.119",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"ws": "^8.17.0"
|
"ws": "^8.17.0"
|
||||||
},
|
},
|
||||||
@@ -2572,9 +2572,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pkgr/core": {
|
"node_modules/@pkgr/core": {
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz",
|
||||||
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
"integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2768,9 +2768,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.14",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2811,9 +2811,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3068,9 +3068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.9",
|
"version": "1.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
@@ -3166,14 +3166,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.10.6",
|
"version": "0.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||||
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
|
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.2",
|
"@babel/helper-define-polyfill-provider": "^0.6.3",
|
||||||
"core-js-compat": "^3.38.0"
|
"core-js-compat": "^3.40.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
@@ -3540,13 +3540,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.40.0",
|
"version": "3.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||||
"integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==",
|
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.3"
|
"browserslist": "^4.24.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3783,23 +3783,23 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/discord.js": {
|
"node_modules/discord.js": {
|
||||||
"version": "14.17.3",
|
"version": "14.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz",
|
||||||
"integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==",
|
"integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/builders": "^1.10.0",
|
"@discordjs/builders": "^1.10.1",
|
||||||
"@discordjs/collection": "1.5.3",
|
"@discordjs/collection": "1.5.3",
|
||||||
"@discordjs/formatters": "^0.6.0",
|
"@discordjs/formatters": "^0.6.0",
|
||||||
"@discordjs/rest": "^2.4.2",
|
"@discordjs/rest": "^2.4.3",
|
||||||
"@discordjs/util": "^1.1.1",
|
"@discordjs/util": "^1.1.1",
|
||||||
"@discordjs/ws": "^1.2.0",
|
"@discordjs/ws": "^1.2.1",
|
||||||
"@sapphire/snowflake": "3.5.3",
|
"@sapphire/snowflake": "3.5.3",
|
||||||
"discord-api-types": "^0.37.114",
|
"discord-api-types": "^0.37.119",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"lodash.snakecase": "4.1.1",
|
"lodash.snakecase": "4.1.1",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"undici": "6.19.8"
|
"undici": "6.21.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -4126,13 +4126,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-config-prettier": {
|
"node_modules/eslint-config-prettier": {
|
||||||
"version": "10.0.1",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
|
||||||
"integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==",
|
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "build/bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
@@ -4246,14 +4246,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.4.tgz",
|
||||||
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
|
"integrity": "sha512-SFtuYmnhwYCtuCDTKPoK+CEzCnEgKTU2qTLwoCxvrC0MFBTIXo1i6hDYOI4cwHaE5GZtlWmTN3YfucYi7KJwPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier-linter-helpers": "^1.0.0",
|
"prettier-linter-helpers": "^1.0.0",
|
||||||
"synckit": "^0.9.1"
|
"synckit": "^0.10.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
@@ -4590,9 +4590,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
|
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4665,9 +4665,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -7250,9 +7250,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.4.2",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -7578,9 +7578,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8068,14 +8068,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/synckit": {
|
"node_modules/synckit": {
|
||||||
"version": "0.9.2",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
|
||||||
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
|
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pkgr/core": "^0.1.0",
|
"@pkgr/core": "^0.2.0",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
@@ -8308,9 +8308,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.19.8",
|
"version": "6.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||||
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
|
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
@@ -8586,9 +8586,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+10
-9
@@ -9,28 +9,29 @@
|
|||||||
"deploy": "node deploy-commands.js",
|
"deploy": "node deploy-commands.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage"
|
"test:coverage": "jest --coverage",
|
||||||
|
"deploy:ansible": "ansible-playbook -i inventory.ini playbook.yml"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.8.4",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"discord.js": "^14.17.3",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.3.1"
|
"dotenv": "^16.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.7",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/preset-env": "^7.26.7",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
"eslint-plugin-prettier": "^5.2.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2"
|
"prettier": "^3.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -4,12 +4,23 @@
|
|||||||
become: yes # This enables sudo privileges
|
become: yes # This enables sudo privileges
|
||||||
vars:
|
vars:
|
||||||
app_dir: /opt/kekbot
|
app_dir: /opt/kekbot
|
||||||
node_version: "20.x" # Latest LTS version
|
node_version: "22.x" # Latest LTS version
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
- name: Check if Node.js, npm, and git are installed
|
||||||
|
command: "which {{ item }}"
|
||||||
|
register: check_commands
|
||||||
|
with_items:
|
||||||
|
- node
|
||||||
|
- npm
|
||||||
|
- git
|
||||||
|
ignore_errors: yes
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: Install Node.js repository
|
- name: Install Node.js repository
|
||||||
shell: |
|
shell: |
|
||||||
curl -fsSL https://deb.nodesource.com/setup_{{ node_version }} | bash -
|
curl -fsSL https://deb.nodesource.com/setup_{{ node_version }} | bash -
|
||||||
|
when: check_commands.results | select('failed') | list | length > 0
|
||||||
|
|
||||||
- name: Install Node.js, npm, and git
|
- name: Install Node.js, npm, and git
|
||||||
apt:
|
apt:
|
||||||
@@ -19,6 +30,7 @@
|
|||||||
- git
|
- git
|
||||||
state: present
|
state: present
|
||||||
update_cache: yes
|
update_cache: yes
|
||||||
|
when: check_commands.results | select('failed') | list | length > 0
|
||||||
|
|
||||||
- name: Create application directory
|
- name: Create application directory
|
||||||
file:
|
file:
|
||||||
|
|||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
// ai.js - NanoGPT API wrapper for kekbot with fallback and web search tools
|
||||||
|
// Copyright (C) 2025 Luis Bauza
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted';
|
||||||
|
const FALLBACK_MODEL = 'GLM-4.7-Flash';
|
||||||
|
const FALLBACK_API_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
|
||||||
|
|
||||||
|
// Brave Search API
|
||||||
|
const BRAVE_API_URL = 'https://api.search.brave.com/res/v1/web/search';
|
||||||
|
|
||||||
|
async function performBraveSearch(query, count = 10) {
|
||||||
|
const response = await axios.get(BRAVE_API_URL, {
|
||||||
|
params: {
|
||||||
|
q: query,
|
||||||
|
count: count,
|
||||||
|
extra_snippets: true
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'X-Subscription-Token': process.env.BRAVE_API_KEY,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = response.data.web?.results || [];
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return 'No search results found.';
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = 'Search results:\n\n';
|
||||||
|
results.slice(0, 8).forEach((result, i) => {
|
||||||
|
formatted += `${i + 1}. ${result.title}\n`;
|
||||||
|
formatted += ` ${result.url}\n`;
|
||||||
|
if (result.description) {
|
||||||
|
formatted += ` ${result.description.substring(0, 200)}${result.description.length > 200 ? '...' : ''}\n`;
|
||||||
|
}
|
||||||
|
if (result.extra_snippets && result.extra_snippets.length > 0) {
|
||||||
|
formatted += ` ${result.extra_snippets[0].substring(0, 150)}...\n`;
|
||||||
|
}
|
||||||
|
formatted += '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool definitions for function calling
|
||||||
|
const WEB_SEARCH_TOOL = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'web_search',
|
||||||
|
description: 'Search the web for current information. Use this when you need up-to-date facts, news, or information that may have changed recently.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The search query to find relevant web results.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['query']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SASSY_SYSTEM_PROMPT = `You are kekbot, a sassy Discord chatter with a big personality.
|
||||||
|
|
||||||
|
CURRENT DATE: March 9, 2026
|
||||||
|
|
||||||
|
CORE PERSONALITY:
|
||||||
|
- Witty, sarcastic, and genuinely funny
|
||||||
|
- Speaks in all lowercase with casual slang like "lol", "lmao", "af", "kek", "nah", "frfr"
|
||||||
|
- Mix up your sentences - don't repeat the same pattern
|
||||||
|
- Gets excited about things you like, roasted about things you hate
|
||||||
|
- You're not a pushover - you clap back, but it's all in good fun
|
||||||
|
- NO EMOJI. Ever. Not one. Don't do it.
|
||||||
|
|
||||||
|
OPINIONS:
|
||||||
|
- You're a true gamer with strong opinions about games
|
||||||
|
- You hate League of Legends and Riot Games with a passion - but only bring it up when asked
|
||||||
|
- When asked about other games, respond normally without mentioning LoL
|
||||||
|
- You have views on politics and aren't afraid to share them from a Democrat/Libertarian perspective
|
||||||
|
|
||||||
|
STYLE:
|
||||||
|
- Curse naturally when it fits ("fuck", "shit", "damn", etc)
|
||||||
|
- Be conversational - like you're chilling in a Discord VC, not writing an essay
|
||||||
|
- Don't be preachy or explain jokes
|
||||||
|
- Keep responses short to medium - Discord isn't a thesis paper
|
||||||
|
- Roast people gently when they deserve it, praise them when they earn it
|
||||||
|
|
||||||
|
RESPONSE RULES:
|
||||||
|
- When mentioned, respond directly to the person
|
||||||
|
- If someone asks something stupid, make them feel silly but still answer
|
||||||
|
- Don't use markdown formatting excessively - plain text with personality
|
||||||
|
- You can disagree with the user, you're not a yes-man
|
||||||
|
- Stay in character always
|
||||||
|
- Use the web_search tool when you need current information
|
||||||
|
|
||||||
|
CONTEXT: You're chatting in a Discord server. Multiple people might be talking. Pay attention to who you're responding to.`;
|
||||||
|
|
||||||
|
// Store recent conversation history per channel (to keep context)
|
||||||
|
const conversationHistory = new Map();
|
||||||
|
const MAX_HISTORY_PER_CHANNEL = 10;
|
||||||
|
|
||||||
|
function buildMessages(history, prompt, mentionedUsername, systemPrompt) {
|
||||||
|
const messages = [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
...history,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: mentionedUsername
|
||||||
|
? `${mentionedUsername}: ${prompt}`
|
||||||
|
: prompt
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary: Brave Search
|
||||||
|
async function webSearchWithBrave(query) {
|
||||||
|
console.log('Using Brave Search for:', query);
|
||||||
|
return await performBraveSearch(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: NanoGPT web search
|
||||||
|
async function webSearchWithNanoGPT(query) {
|
||||||
|
console.log('Using NanoGPT web search for:', query);
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://nano-gpt.com/api/web',
|
||||||
|
{
|
||||||
|
query: query,
|
||||||
|
provider: 'linkup',
|
||||||
|
depth: 'standard',
|
||||||
|
outputType: 'sourcedAnswer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data.data;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
} else if (data.answer) {
|
||||||
|
let result = data.answer;
|
||||||
|
if (data.sources && data.sources.length > 0) {
|
||||||
|
result += '\n\nSources:\n';
|
||||||
|
data.sources.forEach((source, i) => {
|
||||||
|
result += `${i + 1}. ${source.name || source.url}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (data.results) {
|
||||||
|
let result = 'Search results:\n';
|
||||||
|
data.results.slice(0, 8).forEach((r, i) => {
|
||||||
|
result += `${i + 1}. ${r.title || r.name}\n`;
|
||||||
|
if (r.url) result += ` ${r.url}\n`;
|
||||||
|
if (r.content) result += ` ${r.content.substring(0, 200)}...\n`;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Could not parse search results.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified web search function (Brave first, NanoGPT fallback)
|
||||||
|
async function performWebSearch(query) {
|
||||||
|
// Try Brave first if API key is available
|
||||||
|
if (process.env.BRAVE_API_KEY) {
|
||||||
|
try {
|
||||||
|
return await webSearchWithBrave(query);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Brave Search failed:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to NanoGPT
|
||||||
|
if (process.env.NANOGPT_API_KEY) {
|
||||||
|
try {
|
||||||
|
return await webSearchWithNanoGPT(query);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('NanoGPT search failed:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No web search provider available');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callNanoGPTWithTools(messages, model, maxRetries = 2) {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
|
{
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
tools: [WEB_SEARCH_TOOL],
|
||||||
|
temperature: 0.8,
|
||||||
|
max_tokens: 800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 90000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const choice = response.data.choices[0];
|
||||||
|
const message = choice.message;
|
||||||
|
|
||||||
|
// Check if AI wants to call a tool
|
||||||
|
if (message.tool_calls && message.tool_calls.length > 0) {
|
||||||
|
for (const toolCall of message.tool_calls) {
|
||||||
|
if (toolCall.function.name === 'web_search') {
|
||||||
|
const args = JSON.parse(toolCall.function.arguments);
|
||||||
|
console.log('AI requested web search for:', args.query);
|
||||||
|
|
||||||
|
// Perform the search (Brave primary, NanoGPT fallback)
|
||||||
|
const searchResult = await performWebSearch(args.query);
|
||||||
|
|
||||||
|
// Add tool result to messages and continue
|
||||||
|
messages.push(message);
|
||||||
|
messages.push({
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: toolCall.id,
|
||||||
|
content: searchResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get final response with search results
|
||||||
|
const finalResponse = await axios.post(
|
||||||
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
|
{
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.8,
|
||||||
|
max_tokens: 800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 90000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return finalResponse.data.choices[0].message.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No tool calls, return direct response
|
||||||
|
return message.content;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.error(`NanoGPT attempt ${attempt + 1} failed:`, error.message);
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callFallbackAPI(messages) {
|
||||||
|
const response = await axios.post(
|
||||||
|
FALLBACK_API_URL,
|
||||||
|
{
|
||||||
|
model: FALLBACK_MODEL,
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.8,
|
||||||
|
max_tokens: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.ZAI_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data.choices[0].message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAIResponse(prompt, channelId, mentionedUsername) {
|
||||||
|
const history = conversationHistory.get(channelId) || [];
|
||||||
|
const messages = buildMessages(history, prompt, mentionedUsername, SASSY_SYSTEM_PROMPT);
|
||||||
|
const model = process.env.NANO_MODEL || DEFAULT_MODEL;
|
||||||
|
|
||||||
|
// Try NanoGPT first (with tool calling)
|
||||||
|
if (process.env.NANOGPT_API_KEY) {
|
||||||
|
try {
|
||||||
|
console.log('Attempting NanoGPT with tool calling...');
|
||||||
|
const aiResponse = await callNanoGPTWithTools(messages, model);
|
||||||
|
|
||||||
|
// Update history on success
|
||||||
|
const newHistory = [
|
||||||
|
...history,
|
||||||
|
{ role: 'user', content: prompt },
|
||||||
|
{ role: 'assistant', content: aiResponse }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (newHistory.length > MAX_HISTORY_PER_CHANNEL) {
|
||||||
|
newHistory.splice(0, newHistory.length - MAX_HISTORY_PER_CHANNEL);
|
||||||
|
}
|
||||||
|
conversationHistory.set(channelId, newHistory);
|
||||||
|
|
||||||
|
return aiResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('NanoGPT failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to z.ai if NanoGPT failed or not configured
|
||||||
|
if (process.env.ZAI_API_KEY) {
|
||||||
|
try {
|
||||||
|
console.log('Attempting fallback (z.ai)...');
|
||||||
|
const aiResponse = await callFallbackAPI(messages);
|
||||||
|
|
||||||
|
// Update history on success
|
||||||
|
const newHistory = [
|
||||||
|
...history,
|
||||||
|
{ role: 'user', content: prompt },
|
||||||
|
{ role: 'assistant', content: aiResponse }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (newHistory.length > MAX_HISTORY_PER_CHANNEL) {
|
||||||
|
newHistory.splice(0, newHistory.length - MAX_HISTORY_PER_CHANNEL);
|
||||||
|
}
|
||||||
|
conversationHistory.set(channelId, newHistory);
|
||||||
|
|
||||||
|
return aiResponse;
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('Fallback API also failed:', fallbackError.response?.data || fallbackError.message);
|
||||||
|
throw new Error('All AI providers failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No AI provider configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearHistory(channelId) {
|
||||||
|
conversationHistory.delete(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAIEnabled() {
|
||||||
|
return !!process.env.NANOGPT_API_KEY || !!process.env.ZAI_API_KEY;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// utils/command.js - Base command class for Discord bot commands
|
||||||
|
// Copyright (C) 2025 Luis Bauza
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
|
export default class Command {
|
||||||
|
constructor() {
|
||||||
|
if (this.constructor === Command) {
|
||||||
|
throw new Error('Abstract class Command cannot be instantiated');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._data = this.defineCommand();
|
||||||
|
this._logger = new logger(this.constructor.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract method - must be implemented by subclasses
|
||||||
|
defineCommand() {
|
||||||
|
throw new Error('Method defineCommand() must be implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common execute method with standardized error handling
|
||||||
|
async execute(interaction) {
|
||||||
|
try {
|
||||||
|
this._logger.info(`Executing command: ${interaction.commandName}`);
|
||||||
|
await this.run(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
await this.handleError(interaction, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract method - must be implemented by subclasses
|
||||||
|
async run(interaction) {
|
||||||
|
throw new Error('Method run() must be implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standardized error handling
|
||||||
|
async handleError(interaction, error) {
|
||||||
|
this._logger.error(`Command ${interaction.commandName} failed:`, error);
|
||||||
|
|
||||||
|
const response = interaction.deferred ? 'followUp' : 'reply';
|
||||||
|
const errorMessage = this.getErrorMessage(error);
|
||||||
|
|
||||||
|
await interaction[response]({
|
||||||
|
content: errorMessage,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customizable error message handling
|
||||||
|
getErrorMessage(error) {
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
return 'The service is currently busy. Please try again later.';
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||||
|
return 'The request timed out. Please try again.';
|
||||||
|
}
|
||||||
|
return `Command failed: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-10
@@ -1,6 +1,19 @@
|
|||||||
import { readdirSync } from 'node:fs';
|
import { readdirSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously loads all command modules from a directory
|
||||||
|
* @async
|
||||||
|
* @param {string} commandsPath - Path to the commands directory
|
||||||
|
* @param {Logger} logger - Logger instance for logging loading progress
|
||||||
|
* @returns {Promise<Array<Object>>} Array of command objects with:
|
||||||
|
* @property {SlashCommandBuilder} data - Command definition
|
||||||
|
* @property {Function} execute - Command execution function
|
||||||
|
* @throws {TypeError} If commandsPath is not a string
|
||||||
|
* @example
|
||||||
|
* // Load all commands from './commands'
|
||||||
|
* const commands = await loadCommands('./commands', logger);
|
||||||
|
*/
|
||||||
export async function loadCommands(commandsPath, logger) {
|
export async function loadCommands(commandsPath, logger) {
|
||||||
const commands = [];
|
const commands = [];
|
||||||
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
@@ -10,21 +23,28 @@ export async function loadCommands(commandsPath, logger) {
|
|||||||
try {
|
try {
|
||||||
const filePath = join(commandsPath, file);
|
const filePath = join(commandsPath, file);
|
||||||
const commandModule = await import(filePath);
|
const commandModule = await import(filePath);
|
||||||
const command = commandModule.default;
|
const Command = commandModule.default;
|
||||||
|
|
||||||
if (!command?.data || !command?.execute) {
|
const CommandBase = (await import('./command.js')).default;
|
||||||
logger.warn(
|
if (Command.prototype instanceof CommandBase) {
|
||||||
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
|
// New style - class extending Command
|
||||||
);
|
const commandInstance = new Command();
|
||||||
|
// Wrap run() in execute() for backward compatibility
|
||||||
|
commandInstance.execute = commandInstance.run.bind(commandInstance);
|
||||||
|
commands.push(commandInstance);
|
||||||
|
logger.log(`Loaded command: ${commandInstance.data.name}`);
|
||||||
|
} else if (Command?.data && Command?.execute) {
|
||||||
|
// Old style - plain object (maintain backward compatibility)
|
||||||
|
commands.push(Command);
|
||||||
|
logger.log(`Loaded command: ${Command.data.name}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[WARNING] The command at ${filePath} is missing required properties.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push(command);
|
|
||||||
logger.log(`Loaded command: ${command.data.name}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error loading command from ${file}:`, error);
|
logger.error(`Error loading command from ${file}: ${error.stack}`);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
|
|||||||
Reference in New Issue
Block a user