Initial commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'airbnb-base',
|
||||||
|
'eslint:recommended',
|
||||||
|
'prettier',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:import/errors',
|
||||||
|
'plugin:import/warnings',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2023,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
impliedStrict: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': 'warn',
|
||||||
|
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'no-param-reassign': ['error', { props: false }],
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
'import/extensions': [
|
||||||
|
'error',
|
||||||
|
'always',
|
||||||
|
{
|
||||||
|
js: 'never',
|
||||||
|
jsx: 'never',
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Defer to Prettier for all formatting concerns
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.test.js', '**/*.spec.js'],
|
||||||
|
env: {
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
inventory.ini
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
response_*/
|
||||||
|
|
||||||
|
# Test Coverage
|
||||||
|
coverage/
|
||||||
|
.coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
coverage.json
|
||||||
|
clover.xml
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"proseWrap": "preserve"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Kekbot.js
|
||||||
|
|
||||||
|
A simple Discord bot with essential moderation and utility commands.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
Copyright (C) 2025 Luis Bauza
|
||||||
|
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details see the LICENSE file.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions described in the license.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Modern Discord.js v14 implementation
|
||||||
|
- Slash commands for better user experience
|
||||||
|
- Essential moderation commands (kick, prune)
|
||||||
|
- Help command for easy reference
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `/help` - Lists all available commands
|
||||||
|
- `/ping` - Check if the bot is responsive
|
||||||
|
- `/kick` - Kick a user from the server (requires Kick Members permission)
|
||||||
|
- `/prune` - Delete up to 99 messages (requires Manage Messages permission)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Create a `.env` file with your bot token and client ID:
|
||||||
|
|
||||||
|
```
|
||||||
|
DISCORD_TOKEN=your_token_here
|
||||||
|
CLIENT_ID=your_client_id_here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Deploy slash commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 16.9.0 or higher
|
||||||
|
- Discord.js v14
|
||||||
|
- A Discord bot token and application ID
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
# Security Policy and Best Practices
|
||||||
|
|
||||||
|
## Credential Management
|
||||||
|
|
||||||
|
### Current Security Measures
|
||||||
|
|
||||||
|
- Environment variables for sensitive credentials (Discord token, API keys)
|
||||||
|
- Git pre-commit hooks to prevent credential exposure
|
||||||
|
- .gitignore rules for sensitive files
|
||||||
|
- Test environment using mock credentials
|
||||||
|
|
||||||
|
### Cleaning Git History of Credentials
|
||||||
|
|
||||||
|
If you need to remove sensitive data from git history, follow these steps carefully:
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
1. Install git-filter-repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew install git-filter-repo
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
pip3 install git-filter-repo
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
pip install git-filter-repo
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a backup of your repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r your-repo your-repo-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cleaning Process
|
||||||
|
|
||||||
|
1. **Prepare Your Repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Change to your repository directory
|
||||||
|
cd your-repo
|
||||||
|
|
||||||
|
# Create a fresh clone (recommended for cleaning)
|
||||||
|
git clone --mirror git@github.com:username/your-repo.git repo-mirror
|
||||||
|
cd repo-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a Pattern File**
|
||||||
|
Create a file named `patterns.txt` containing patterns of credentials to remove:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Discord tokens (common patterns)
|
||||||
|
['"](N[a-zA-Z0-9]{23}\.[a-zA-Z0-9]{6}\.[a-zA-Z0-9_-]{27})['"]
|
||||||
|
# API keys
|
||||||
|
['"](sk-[a-zA-Z0-9]{48}|pk-[a-zA-Z0-9]{48})['"]
|
||||||
|
# Generic tokens/keys
|
||||||
|
['"]([a-zA-Z0-9_-]{32,64})['"]
|
||||||
|
# Add more patterns as needed
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Clean the Repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove files containing credentials
|
||||||
|
git filter-repo --invert-paths --paths-from-file patterns.txt
|
||||||
|
|
||||||
|
# Clean all refs
|
||||||
|
git reflog expire --expire=now --all
|
||||||
|
git gc --prune=now --aggressive
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify the Cleaning**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for any remaining credentials
|
||||||
|
git log -p | grep -i -f patterns.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Update Remote Repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Force push all branches
|
||||||
|
git push origin --force --all
|
||||||
|
|
||||||
|
# Force push all tags
|
||||||
|
git push origin --force --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Update All Local Clones**
|
||||||
|
For each local clone of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In each local clone
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/main # or your default branch
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Important Notes
|
||||||
|
|
||||||
|
- This process is irreversible and rewrites git history
|
||||||
|
- All collaborators must re-clone the repository after cleaning
|
||||||
|
- Immediately revoke and rotate any exposed credentials
|
||||||
|
- Old credentials may still exist in:
|
||||||
|
- GitHub Actions logs
|
||||||
|
- Issue comments
|
||||||
|
- Pull request descriptions
|
||||||
|
- Cached views on GitHub
|
||||||
|
- Other users' local clones
|
||||||
|
|
||||||
|
### Setting Up Environment Variables
|
||||||
|
|
||||||
|
1. Create a `.env` file in the project root (this file is automatically ignored by git)
|
||||||
|
2. Add your credentials using this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
DISCORD_TOKEN=your_discord_token_here
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Never commit the `.env` file to version control
|
||||||
|
|
||||||
|
### Development Guidelines
|
||||||
|
|
||||||
|
#### DO
|
||||||
|
|
||||||
|
- Use environment variables for all sensitive data
|
||||||
|
- Keep the `.env` file secure and local to your development environment
|
||||||
|
- Use mock/fake data for tests
|
||||||
|
- Review code for credential exposure before committing
|
||||||
|
- Use the pre-commit hook to catch potential credential leaks
|
||||||
|
|
||||||
|
#### DON'T
|
||||||
|
|
||||||
|
- Hardcode credentials in source code
|
||||||
|
- Commit `.env` files to version control
|
||||||
|
- Share credentials through insecure channels
|
||||||
|
- Disable the pre-commit hook
|
||||||
|
- Log sensitive information
|
||||||
|
|
||||||
|
### Secure Credential Storage
|
||||||
|
|
||||||
|
For production environments:
|
||||||
|
|
||||||
|
1. Use a secrets management service or secure environment variable storage
|
||||||
|
2. Rotate credentials regularly
|
||||||
|
3. Use the principle of least privilege
|
||||||
|
4. Monitor for unauthorized access
|
||||||
|
5. Keep backup copies of credentials in a secure location
|
||||||
|
|
||||||
|
### What to Do If Credentials Are Exposed
|
||||||
|
|
||||||
|
If credentials are accidentally committed:
|
||||||
|
|
||||||
|
1. Immediately invalidate and rotate the exposed credentials
|
||||||
|
2. Use tools like `git-filter-repo` to remove the credentials from git history
|
||||||
|
3. Force push the cleaned history
|
||||||
|
4. Notify relevant team members/security personnel
|
||||||
|
5. Review access logs for potential unauthorized use
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
|
||||||
|
The project includes a pre-commit hook that scans for potential credentials. The hook:
|
||||||
|
|
||||||
|
- Checks staged files for common credential patterns
|
||||||
|
- Excludes test files and dependencies
|
||||||
|
- Provides clear error messages if potential credentials are found
|
||||||
|
|
||||||
|
To bypass the hook in exceptional cases (NOT RECOMMENDED):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
For CI/CD pipelines:
|
||||||
|
|
||||||
|
1. Use secure environment variables in CI configuration
|
||||||
|
2. Never display environment variables in build logs
|
||||||
|
3. Rotate CI/CD credentials regularly
|
||||||
|
4. Limit CI/CD service permissions to only what's necessary
|
||||||
|
|
||||||
|
### Security Checks
|
||||||
|
|
||||||
|
Regular security maintenance:
|
||||||
|
|
||||||
|
1. Update dependencies regularly
|
||||||
|
2. Run security audits (`npm audit`)
|
||||||
|
3. Review access logs
|
||||||
|
4. Verify environment variable usage
|
||||||
|
5. Check git history for any exposed credentials
|
||||||
|
|
||||||
|
### Reporting Security Issues
|
||||||
|
|
||||||
|
If you discover a security vulnerability:
|
||||||
|
|
||||||
|
1. Do NOT open a public issue
|
||||||
|
2. Contact the maintainers directly
|
||||||
|
3. Provide detailed information about the vulnerability
|
||||||
|
4. Allow time for the issue to be addressed before public disclosure
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Node.js Security Best Practices](https://nodejs.org/en/security/)
|
||||||
|
- [OWASP Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/)
|
||||||
|
- [Discord Bot Security](https://discord.com/developers/docs/topics/security)
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
# Discord Bot Testing Guide
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
The project uses Jest for testing with the following configuration:
|
||||||
|
|
||||||
|
- Unit tests for commands and utilities
|
||||||
|
- Mocked Discord.js components
|
||||||
|
- 100% code coverage tracking
|
||||||
|
- Automated test running
|
||||||
|
|
||||||
|
### Test File Patterns
|
||||||
|
|
||||||
|
Only files matching these patterns are treated as test files:
|
||||||
|
|
||||||
|
- `**/__tests__/**/*.test.[jt]s?(x)`
|
||||||
|
- `**/__tests__/**/*.spec.[jt]s?(x)`
|
||||||
|
- `**/?(*.)+(spec|test).[jt]s?(x)`
|
||||||
|
|
||||||
|
Helper files (like testUtils.js) should not use these patterns to avoid being treated as test files.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run tests with coverage report
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Test Utilities (`__tests__/utils/testUtils.js`)
|
||||||
|
|
||||||
|
- `createMockInteraction()`: Creates mock Discord.js interactions for testing commands
|
||||||
|
- `createMockClient()`: Creates mock Discord.js client instances
|
||||||
|
|
||||||
|
### Command Tests (`__tests__/commands/`)
|
||||||
|
|
||||||
|
Each command has its own test file that verifies:
|
||||||
|
|
||||||
|
- Command structure (name, description, options)
|
||||||
|
- Command execution
|
||||||
|
- Error handling
|
||||||
|
- Edge cases
|
||||||
|
|
||||||
|
## Writing New Tests
|
||||||
|
|
||||||
|
1. Create test files in the `__tests__` directory following the naming convention `*.test.js`
|
||||||
|
2. Use the provided test utilities to mock Discord.js components
|
||||||
|
3. Structure tests using describe/it blocks for clarity
|
||||||
|
4. Test both success and error cases
|
||||||
|
5. Ensure proper cleanup in beforeEach/afterEach blocks if needed
|
||||||
|
|
||||||
|
### Example Test Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
describe('Command Name', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
// Test command properties
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
// Custom options
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle successful execution', async () => {
|
||||||
|
// Test success case
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
// Test error case
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Mock external dependencies
|
||||||
|
2. Test edge cases and error conditions
|
||||||
|
3. Keep tests focused and isolated
|
||||||
|
4. Use descriptive test names
|
||||||
|
5. Maintain test coverage above 90%
|
||||||
|
6. Clean up resources after tests
|
||||||
|
7. Use appropriate assertions
|
||||||
|
8. Test asynchronous code properly
|
||||||
|
9. Keep utility files separate from test files
|
||||||
|
10. Follow proper naming conventions for test files
|
||||||
|
|
||||||
|
## Coverage Reports
|
||||||
|
|
||||||
|
Coverage reports are generated in the `coverage` directory after running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
The report includes:
|
||||||
|
|
||||||
|
- Statement coverage
|
||||||
|
- Branch coverage
|
||||||
|
- Function coverage
|
||||||
|
- Line coverage
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
// Mock the discord.js module
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
addStringOption: jest.fn().mockImplementation(callback => {
|
||||||
|
const option = {
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
callback(option);
|
||||||
|
return {
|
||||||
|
addBooleanOption: jest.fn().mockImplementation(boolCallback => {
|
||||||
|
const boolOption = {
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
boolCallback(boolOption);
|
||||||
|
return {
|
||||||
|
toJSON: jest.fn().mockReturnValue({
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
toJSON: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const askCommand = require('../../commands/ask');
|
||||||
|
|
||||||
|
describe('Ask Command', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
it('should have correct name and description', () => {
|
||||||
|
const commandData = askCommand.data.toJSON();
|
||||||
|
expect(commandData.name).toBe('ask');
|
||||||
|
expect(commandData.description).toBe('Ask a question to the AI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required command properties', () => {
|
||||||
|
expect(askCommand).toHaveProperty('data');
|
||||||
|
expect(askCommand).toHaveProperty('execute');
|
||||||
|
expect(typeof askCommand.execute).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct option configuration', () => {
|
||||||
|
const commandData = askCommand.data.toJSON();
|
||||||
|
const [promptOption, websearchOption] = commandData.options;
|
||||||
|
|
||||||
|
expect(promptOption.name).toBe('prompt');
|
||||||
|
expect(promptOption.description).toBe('Your question or prompt');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
const mockPrompt = 'What is the meaning of life?';
|
||||||
|
const mockApiResponse = {
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: 'The meaning of life is 42.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
||||||
|
axios.post.mockReset();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle successful API response with websearch disabled', async () => {
|
||||||
|
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-exp:free',
|
||||||
|
plugins: undefined,
|
||||||
|
}),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
|
content: expect.not.stringContaining('Web search enabled'),
|
||||||
|
split: false,
|
||||||
|
allowedMentions: { parse: [] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle successful API response with websearch enabled', 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-exp:free: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-exp:free',
|
||||||
|
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);
|
||||||
|
axios.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: longResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await askCommand.execute(interaction);
|
||||||
|
|
||||||
|
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)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle code blocks in chunked responses with websearch', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseWithCodeBlock =
|
||||||
|
"Here's a code example:\n```python\nprint('hello')\n```".repeat(20);
|
||||||
|
axios.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: responseWithCodeBlock,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await askCommand.execute(interaction);
|
||||||
|
|
||||||
|
// Verify that code blocks are not split in the middle
|
||||||
|
interaction.followUp.mock.calls.forEach(call => {
|
||||||
|
const { content } = call[0];
|
||||||
|
const openBlocks = (content.match(/```/g) || []).length;
|
||||||
|
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 () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error('API Error');
|
||||||
|
error.response = { data: 'API Error Details' };
|
||||||
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
|
await askCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
|
content: 'Sorry, there was an error processing your request.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send API request with correct headers regardless of websearch', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
|
await askCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(Object),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer test-api-key',
|
||||||
|
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle defer reply failure with websearch', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: true,
|
||||||
|
},
|
||||||
|
deferFails: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to defer reply'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle follow up failure with websearch', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ask',
|
||||||
|
stringOptions: {
|
||||||
|
prompt: mockPrompt,
|
||||||
|
},
|
||||||
|
booleanOptions: {
|
||||||
|
websearch: true,
|
||||||
|
},
|
||||||
|
followUpFails: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
|
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to follow up'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
// Mock the discord.js module
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
toJSON: jest.fn().mockReturnValue({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Lists all available commands',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
EmbedBuilder: jest.fn().mockReturnValue({
|
||||||
|
setColor: jest.fn().mockReturnThis(),
|
||||||
|
setTitle: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setTimestamp: jest.fn().mockReturnThis(),
|
||||||
|
addFields: jest.fn().mockReturnThis(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const helpCommand = require('../../commands/help');
|
||||||
|
|
||||||
|
describe('Help Command', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
it('should have correct name and description', () => {
|
||||||
|
const commandData = helpCommand.data.toJSON();
|
||||||
|
expect(commandData.name).toBe('help');
|
||||||
|
expect(commandData.description).toBe('Lists all available commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required command properties', () => {
|
||||||
|
expect(helpCommand).toHaveProperty('data');
|
||||||
|
expect(helpCommand).toHaveProperty('execute');
|
||||||
|
expect(typeof helpCommand.execute).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
const mockCommands = new Map([
|
||||||
|
['ping', { data: { name: 'ping', description: 'Replies with Pong!' } }],
|
||||||
|
[
|
||||||
|
'help',
|
||||||
|
{ data: { name: 'help', description: 'Lists all available commands' } },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'help',
|
||||||
|
});
|
||||||
|
interaction.client.commands = mockCommands;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an embed with all commands', async () => {
|
||||||
|
await helpCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
embeds: expect.arrayContaining([expect.any(Object)]),
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty commands collection', async () => {
|
||||||
|
interaction.client.commands = new Map();
|
||||||
|
await helpCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
embeds: expect.arrayContaining([expect.any(Object)]),
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle interaction reply failure', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'help',
|
||||||
|
replyFails: true,
|
||||||
|
});
|
||||||
|
interaction.client.commands = mockCommands;
|
||||||
|
|
||||||
|
await expect(helpCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to reply',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create embed with correct structure', async () => {
|
||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
await helpCommand.execute(interaction);
|
||||||
|
|
||||||
|
const embedInstance = EmbedBuilder.mock.results[0].value;
|
||||||
|
|
||||||
|
expect(embedInstance.setColor).toHaveBeenCalledWith('#5dc67b');
|
||||||
|
expect(embedInstance.setTitle).toHaveBeenCalledWith('Available Commands');
|
||||||
|
expect(embedInstance.setDescription).toHaveBeenCalledWith(
|
||||||
|
'Here are all my commands:',
|
||||||
|
);
|
||||||
|
expect(embedInstance.setTimestamp).toHaveBeenCalled();
|
||||||
|
expect(embedInstance.addFields).toHaveBeenCalledTimes(mockCommands.size);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
// Mock the discord.js module
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
addUserOption: jest.fn().mockImplementation(callback => {
|
||||||
|
const option = {
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
callback(option);
|
||||||
|
return {
|
||||||
|
addStringOption: jest.fn().mockImplementation(callback => {
|
||||||
|
const option = {
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
callback(option);
|
||||||
|
return {
|
||||||
|
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
||||||
|
toJSON: jest.fn().mockReturnValue({
|
||||||
|
name: 'kick',
|
||||||
|
description: 'Kick a user from the server',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'target',
|
||||||
|
description: 'The user to kick',
|
||||||
|
type: 6,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reason',
|
||||||
|
description: 'Reason for kicking',
|
||||||
|
type: 3,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
toJSON: jest.fn(),
|
||||||
|
}),
|
||||||
|
PermissionFlagsBits: {
|
||||||
|
KickMembers: 0x2n,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const kickCommand = require('../../commands/kick');
|
||||||
|
|
||||||
|
describe('Kick Command', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
it('should have correct name and description', () => {
|
||||||
|
const commandData = kickCommand.data.toJSON();
|
||||||
|
expect(commandData.name).toBe('kick');
|
||||||
|
expect(commandData.description).toBe('Kick a user from the server');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required command properties', () => {
|
||||||
|
expect(kickCommand).toHaveProperty('data');
|
||||||
|
expect(kickCommand).toHaveProperty('execute');
|
||||||
|
expect(typeof kickCommand.execute).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct option configuration', () => {
|
||||||
|
const commandData = kickCommand.data.toJSON();
|
||||||
|
const [targetOption, reasonOption] = commandData.options;
|
||||||
|
|
||||||
|
expect(targetOption.name).toBe('target');
|
||||||
|
expect(targetOption.description).toBe('The user to kick');
|
||||||
|
expect(targetOption.required).toBe(true);
|
||||||
|
|
||||||
|
expect(reasonOption.name).toBe('reason');
|
||||||
|
expect(reasonOption.description).toBe('Reason for kicking');
|
||||||
|
expect(reasonOption.required).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
const mockKick = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockKick.mockReset();
|
||||||
|
|
||||||
|
const mockTarget = {
|
||||||
|
kickable: true,
|
||||||
|
kick: mockKick,
|
||||||
|
user: {
|
||||||
|
tag: 'TestUser#1234',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: mockTarget,
|
||||||
|
},
|
||||||
|
stringOptions: {
|
||||||
|
reason: 'Test reason',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully kick a user with reason', async () => {
|
||||||
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(mockKick).toHaveBeenCalledWith('Test reason');
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: expect.stringContaining('Successfully kicked TestUser#1234'),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default reason when none provided', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: {
|
||||||
|
kickable: true,
|
||||||
|
kick: mockKick,
|
||||||
|
user: { tag: 'TestUser#1234' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stringOptions: {
|
||||||
|
reason: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(mockKick).toHaveBeenCalledWith('No reason provided');
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: expect.stringContaining('No reason provided'),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-existent target', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(mockKick).not.toHaveBeenCalled();
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: 'That user is not in this server!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-kickable target', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: {
|
||||||
|
kickable: false,
|
||||||
|
user: { tag: 'Admin#1234' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(mockKick).not.toHaveBeenCalled();
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content:
|
||||||
|
'I cannot kick this user! They may have higher permissions than me.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle kick failure', async () => {
|
||||||
|
const mockTarget = {
|
||||||
|
kickable: true,
|
||||||
|
kick: jest.fn().mockRejectedValueOnce(new Error('Failed to kick user')),
|
||||||
|
user: { tag: 'TestUser#1234' },
|
||||||
|
};
|
||||||
|
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: mockTarget,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await kickCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: 'There was an error trying to kick this user!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle interaction reply failure', async () => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'kick',
|
||||||
|
memberOptions: {
|
||||||
|
target: {
|
||||||
|
kickable: true,
|
||||||
|
kick: mockKick,
|
||||||
|
user: { tag: 'TestUser#1234' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
replyFails: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(kickCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to reply',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
// Mock the discord.js module
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
toJSON: jest.fn().mockReturnValue({
|
||||||
|
name: 'ping',
|
||||||
|
description: 'Replies with Pong!',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pingCommand = require('../../commands/ping');
|
||||||
|
|
||||||
|
describe('Ping Command', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
it('should have correct name and description', () => {
|
||||||
|
const commandData = pingCommand.data.toJSON();
|
||||||
|
expect(commandData.name).toBe('ping');
|
||||||
|
expect(commandData.description).toBe('Replies with Pong!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required command properties', () => {
|
||||||
|
expect(pingCommand).toHaveProperty('data');
|
||||||
|
expect(pingCommand).toHaveProperty('execute');
|
||||||
|
expect(typeof pingCommand.execute).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'ping',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reply with "Pong! 🏓"', async () => {
|
||||||
|
await pingCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledTimes(1);
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith('Pong! 🏓');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle interaction reply failure', async () => {
|
||||||
|
// Mock a failed reply
|
||||||
|
interaction.reply.mockRejectedValueOnce(new Error('Failed to reply'));
|
||||||
|
|
||||||
|
await expect(pingCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to reply',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
const { createMockInteraction } = require('../utils/testUtils');
|
||||||
|
|
||||||
|
// Mock the discord.js module
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
addIntegerOption: jest.fn().mockImplementation(callback => {
|
||||||
|
const option = {
|
||||||
|
setName: jest.fn().mockReturnThis(),
|
||||||
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
|
setMinValue: jest.fn().mockReturnThis(),
|
||||||
|
setMaxValue: jest.fn().mockReturnThis(),
|
||||||
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
callback(option);
|
||||||
|
return {
|
||||||
|
setDefaultMemberPermissions: jest.fn().mockReturnThis(),
|
||||||
|
toJSON: jest.fn().mockReturnValue({
|
||||||
|
name: 'prune',
|
||||||
|
description: 'Prune up to 99 messages.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
description: 'Number of messages to prune',
|
||||||
|
type: 4,
|
||||||
|
required: true,
|
||||||
|
min_value: 1,
|
||||||
|
max_value: 99,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
toJSON: jest.fn(),
|
||||||
|
}),
|
||||||
|
PermissionFlagsBits: {
|
||||||
|
ManageMessages: 0x2000n,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pruneCommand = require('../../commands/prune');
|
||||||
|
|
||||||
|
describe('Prune Command', () => {
|
||||||
|
describe('Command Structure', () => {
|
||||||
|
it('should have correct name and description', () => {
|
||||||
|
const commandData = pruneCommand.data.toJSON();
|
||||||
|
expect(commandData.name).toBe('prune');
|
||||||
|
expect(commandData.description).toBe('Prune up to 99 messages.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have required command properties', () => {
|
||||||
|
expect(pruneCommand).toHaveProperty('data');
|
||||||
|
expect(pruneCommand).toHaveProperty('execute');
|
||||||
|
expect(typeof pruneCommand.execute).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct option configuration', () => {
|
||||||
|
const commandData = pruneCommand.data.toJSON();
|
||||||
|
const option = commandData.options[0];
|
||||||
|
|
||||||
|
expect(option.name).toBe('amount');
|
||||||
|
expect(option.description).toBe('Number of messages to prune');
|
||||||
|
expect(option.required).toBe(true);
|
||||||
|
expect(option.min_value).toBe(1);
|
||||||
|
expect(option.max_value).toBe(99);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command Execution', () => {
|
||||||
|
let interaction;
|
||||||
|
const mockBulkDelete = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockBulkDelete.mockReset();
|
||||||
|
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'prune',
|
||||||
|
integerOptions: {
|
||||||
|
amount: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
interaction.channel.bulkDelete = mockBulkDelete;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully delete messages', async () => {
|
||||||
|
mockBulkDelete.mockResolvedValueOnce({ size: 5 });
|
||||||
|
|
||||||
|
await pruneCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(mockBulkDelete).toHaveBeenCalledWith(5, true);
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: 'Successfully deleted 5 message(s).',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle bulk delete failure', async () => {
|
||||||
|
mockBulkDelete.mockRejectedValueOnce(
|
||||||
|
new Error('Failed to delete messages'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await pruneCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: 'There was an error trying to prune messages in this channel!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle messages older than 14 days', async () => {
|
||||||
|
mockBulkDelete.mockRejectedValueOnce(
|
||||||
|
new Error('Messages older than 14 days cannot be deleted'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await pruneCommand.execute(interaction);
|
||||||
|
|
||||||
|
expect(interaction.reply).toHaveBeenCalledWith({
|
||||||
|
content: 'There was an error trying to prune messages in this channel!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle interaction reply failure', async () => {
|
||||||
|
mockBulkDelete.mockResolvedValueOnce({ size: 5 });
|
||||||
|
interaction = createMockInteraction({
|
||||||
|
commandName: 'prune',
|
||||||
|
integerOptions: {
|
||||||
|
amount: 5,
|
||||||
|
},
|
||||||
|
replyFails: true,
|
||||||
|
});
|
||||||
|
interaction.channel.bulkDelete = mockBulkDelete;
|
||||||
|
|
||||||
|
await expect(pruneCommand.execute(interaction)).rejects.toThrow(
|
||||||
|
'Failed to reply',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Test utilities for Discord.js bot testing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock interaction object with common properties and methods
|
||||||
|
* @param {Object} options - Customization options for the mock interaction
|
||||||
|
* @returns {Object} Mock interaction object
|
||||||
|
*/
|
||||||
|
const createMockInteraction = (options = {}) => ({
|
||||||
|
commandName: options.commandName || 'test-command',
|
||||||
|
user: {
|
||||||
|
id: options.userId || 'mock-user-id',
|
||||||
|
username: options.username || 'MockUser',
|
||||||
|
tag: options.userTag || 'MockUser#0000',
|
||||||
|
},
|
||||||
|
guild: {
|
||||||
|
id: options.guildId || 'mock-guild-id',
|
||||||
|
name: options.guildName || 'Mock Guild',
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
id: options.channelId || 'mock-channel-id',
|
||||||
|
name: options.channelName || 'mock-channel',
|
||||||
|
send: jest.fn().mockResolvedValue({ id: 'mock-message-id' }),
|
||||||
|
bulkDelete: jest.fn().mockResolvedValue({ size: 0 }),
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
commands: new Map(),
|
||||||
|
user: {
|
||||||
|
id: 'mock-client-id',
|
||||||
|
username: 'MockBot',
|
||||||
|
setActivity: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reply: jest.fn().mockImplementation(async _response => {
|
||||||
|
if (options.replyFails) {
|
||||||
|
throw new Error('Failed to reply');
|
||||||
|
}
|
||||||
|
return { id: 'mock-reply-id' };
|
||||||
|
}),
|
||||||
|
deferReply: jest.fn().mockImplementation(async () => {
|
||||||
|
if (options.deferFails) {
|
||||||
|
throw new Error('Failed to defer reply');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
editReply: jest.fn().mockResolvedValue({ id: 'mock-edit-id' }),
|
||||||
|
followUp: jest.fn().mockImplementation(async _response => {
|
||||||
|
if (options.followUpFails) {
|
||||||
|
throw new Error('Failed to follow up');
|
||||||
|
}
|
||||||
|
return { id: 'mock-followup-id' };
|
||||||
|
}),
|
||||||
|
options: {
|
||||||
|
getString: jest.fn(name => options.stringOptions?.[name]),
|
||||||
|
getInteger: jest.fn(name => options.integerOptions?.[name]),
|
||||||
|
getBoolean: jest.fn(name => options.booleanOptions?.[name]),
|
||||||
|
getUser: jest.fn(name => options.userOptions?.[name]),
|
||||||
|
getMember: jest.fn(name => options.memberOptions?.[name]),
|
||||||
|
get: jest.fn(name => options.options?.[name]),
|
||||||
|
},
|
||||||
|
member: {
|
||||||
|
permissions: {
|
||||||
|
has: jest.fn().mockReturnValue(options.hasPermission ?? true),
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
cache: new Map(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isCommand: () => true,
|
||||||
|
isChatInputCommand: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock client object with common properties and methods
|
||||||
|
* @param {Object} options - Customization options for the mock client
|
||||||
|
* @returns {Object} Mock client object
|
||||||
|
*/
|
||||||
|
const createMockClient = (options = {}) => ({
|
||||||
|
user: {
|
||||||
|
id: options.clientId || 'mock-client-id',
|
||||||
|
username: options.clientUsername || 'MockBot',
|
||||||
|
setActivity: jest.fn(),
|
||||||
|
},
|
||||||
|
guilds: {
|
||||||
|
cache: new Map(),
|
||||||
|
},
|
||||||
|
commands: new Map(),
|
||||||
|
on: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
login: jest.fn().mockResolvedValue('token'),
|
||||||
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createMockInteraction,
|
||||||
|
createMockClient,
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const { createMockInteraction, createMockClient } = require('./testUtils');
|
||||||
|
|
||||||
|
describe('Test Utilities', () => {
|
||||||
|
describe('createMockInteraction', () => {
|
||||||
|
it('should create a mock interaction with default values', () => {
|
||||||
|
const interaction = createMockInteraction();
|
||||||
|
|
||||||
|
expect(interaction.commandName).toBe('test-command');
|
||||||
|
expect(interaction.user.id).toBe('mock-user-id');
|
||||||
|
expect(interaction.guild.id).toBe('mock-guild-id');
|
||||||
|
expect(interaction.channel.id).toBe('mock-channel-id');
|
||||||
|
expect(typeof interaction.reply).toBe('function');
|
||||||
|
expect(typeof interaction.deferReply).toBe('function');
|
||||||
|
expect(typeof interaction.editReply).toBe('function');
|
||||||
|
expect(typeof interaction.followUp).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a mock interaction with custom values', () => {
|
||||||
|
const customOptions = {
|
||||||
|
commandName: 'custom-command',
|
||||||
|
userId: 'custom-user-id',
|
||||||
|
username: 'CustomUser',
|
||||||
|
guildId: 'custom-guild-id',
|
||||||
|
channelId: 'custom-channel-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const interaction = createMockInteraction(customOptions);
|
||||||
|
|
||||||
|
expect(interaction.commandName).toBe('custom-command');
|
||||||
|
expect(interaction.user.id).toBe('custom-user-id');
|
||||||
|
expect(interaction.user.username).toBe('CustomUser');
|
||||||
|
expect(interaction.guild.id).toBe('custom-guild-id');
|
||||||
|
expect(interaction.channel.id).toBe('custom-channel-id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createMockClient', () => {
|
||||||
|
it('should create a mock client with default values', () => {
|
||||||
|
const client = createMockClient();
|
||||||
|
|
||||||
|
expect(client.user.id).toBe('mock-client-id');
|
||||||
|
expect(client.user.username).toBe('MockBot');
|
||||||
|
expect(typeof client.login).toBe('function');
|
||||||
|
expect(typeof client.destroy).toBe('function');
|
||||||
|
expect(typeof client.on).toBe('function');
|
||||||
|
expect(typeof client.once).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a mock client with custom values', () => {
|
||||||
|
const customOptions = {
|
||||||
|
clientId: 'custom-client-id',
|
||||||
|
clientUsername: 'CustomBot',
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = createMockClient(customOptions);
|
||||||
|
|
||||||
|
expect(client.user.id).toBe('custom-client-id');
|
||||||
|
expect(client.user.username).toBe('CustomBot');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Ansible Deployment for Kekbot
|
||||||
|
|
||||||
|
This directory contains Ansible configuration for deploying the Discord bot to a server.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Ansible must be installed on your local machine. Install it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On macOS
|
||||||
|
brew install ansible
|
||||||
|
|
||||||
|
# On Ubuntu/Debian
|
||||||
|
sudo apt-get install ansible
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make sure your `.env` file contains all necessary Discord bot tokens and configuration.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `playbook.yml`: Contains all the deployment tasks including:
|
||||||
|
- Installing Node.js and npm
|
||||||
|
- Setting up the application directory
|
||||||
|
- Installing dependencies
|
||||||
|
- Creating and managing a systemd service
|
||||||
|
- Deploying Discord bot commands
|
||||||
|
|
||||||
|
- `inventory.ini`: Contains the server connection details
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
To deploy the bot, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-playbook -i inventory.ini playbook.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Management
|
||||||
|
|
||||||
|
After deployment, you can manage the bot service on the server using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status kekbot
|
||||||
|
|
||||||
|
# Stop the bot
|
||||||
|
sudo systemctl stop kekbot
|
||||||
|
|
||||||
|
# Start the bot
|
||||||
|
sudo systemctl start kekbot
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u kekbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
1. If the deployment fails due to SSH connection issues:
|
||||||
|
- Verify that you can SSH into the server manually
|
||||||
|
- Check that the server details in `inventory.ini` are correct
|
||||||
|
|
||||||
|
2. If the bot fails to start:
|
||||||
|
- Check the logs using `sudo journalctl -u kekbot`
|
||||||
|
- Verify that the `.env` file was properly copied and contains valid tokens
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// bot.js - Discord bot for moderation and utilities
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { Client, Collection, Events, GatewayIntentBits } = require('discord.js');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
client.commands = new Collection();
|
||||||
|
const commandsPath = path.join(__dirname, 'commands');
|
||||||
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
const command = require(filePath);
|
||||||
|
if ('data' in command && 'execute' in command) {
|
||||||
|
client.commands.set(command.data.name, command);
|
||||||
|
} else {
|
||||||
|
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.once(Events.ClientReady, () => {
|
||||||
|
console.log(`Ready! Logged in as ${client.user.tag}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on(Events.InteractionCreate, async interaction => {
|
||||||
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
|
const command = client.commands.get(interaction.commandName);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await command.execute(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
await interaction.followUp({
|
||||||
|
content: 'There was an error while executing this command!',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'There was an error while executing this command!',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(process.env.DISCORD_TOKEN);
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
// ask.js - Discord bot AI question command
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
webSearch: {
|
||||||
|
enabled: false, // Default web search state
|
||||||
|
allowOverride: true, // Whether users can override the default state
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
// Only show the websearch option if overrides are allowed
|
||||||
|
.setDMPermission(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
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://openrouter.ai/api/v1/chat/completions',
|
||||||
|
{
|
||||||
|
model: webSearchEnabled
|
||||||
|
? 'google/gemini-2.0-flash-exp:free:online'
|
||||||
|
: 'google/gemini-2.0-flash-exp:free',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content:
|
||||||
|
'You are a helpful AI assistant. Provide clear, concise, and accurate responses. ' +
|
||||||
|
'Keep your answers brief while ensuring they are informative and to the point. ' +
|
||||||
|
'Avoid unnecessary elaboration or repetition. ',
|
||||||
|
},
|
||||||
|
{ 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: {
|
||||||
|
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||||
|
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const aiResponse = response.data.choices[0].message.content;
|
||||||
|
const webSearchStatus = webSearchEnabled
|
||||||
|
? '\n> *Web search enabled* 🔍\n'
|
||||||
|
: '';
|
||||||
|
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
||||||
|
|
||||||
|
if (formattedResponse.length <= 2000) {
|
||||||
|
// Send as a single message with proper formatting
|
||||||
|
await interaction.followUp({
|
||||||
|
content: formattedResponse,
|
||||||
|
split: false,
|
||||||
|
allowedMentions: { parse: [] },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For longer messages, split while preserving markdown
|
||||||
|
const maxLength = 2000;
|
||||||
|
const chunks = [];
|
||||||
|
let remainingText = formattedResponse;
|
||||||
|
let isFirstChunk = true;
|
||||||
|
|
||||||
|
while (remainingText.length > 0) {
|
||||||
|
let chunk = remainingText.slice(0, maxLength);
|
||||||
|
|
||||||
|
// If we're in the middle of a code block, find a safe split point
|
||||||
|
const lastCodeBlock = chunk.lastIndexOf('```');
|
||||||
|
if (
|
||||||
|
lastCodeBlock !== -1 &&
|
||||||
|
!chunk.slice(lastCodeBlock).includes('\n```')
|
||||||
|
) {
|
||||||
|
// Find the last newline before maxLength
|
||||||
|
const lastNewline = chunk.lastIndexOf('\n');
|
||||||
|
if (lastNewline !== -1) {
|
||||||
|
chunk = chunk.slice(0, lastNewline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subsequent chunks, add continuation indicator
|
||||||
|
if (!isFirstChunk) {
|
||||||
|
chunk = `(continued)\n${chunk}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.push({
|
||||||
|
content: chunk,
|
||||||
|
split: false,
|
||||||
|
allowedMentions: { parse: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingText = remainingText.slice(chunk.length);
|
||||||
|
isFirstChunk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all chunks in sequence
|
||||||
|
await Promise.all(chunks.map(chunk => interaction.followUp(chunk)));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.response?.data || error.message);
|
||||||
|
await interaction.followUp({
|
||||||
|
content: 'Sorry, there was an error processing your request.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// help.js - Discord bot help command
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('help')
|
||||||
|
.setDescription('Lists all available commands'),
|
||||||
|
async execute(interaction) {
|
||||||
|
const { commands } = interaction.client;
|
||||||
|
const helpEmbed = new EmbedBuilder()
|
||||||
|
.setColor('#5dc67b')
|
||||||
|
.setTitle('Available Commands')
|
||||||
|
.setDescription('Here are all my commands:')
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
commands.forEach(command => {
|
||||||
|
helpEmbed.addFields({
|
||||||
|
name: `/${command.data.name}`,
|
||||||
|
value: command.data.description,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// kick.js - Discord bot kick command
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
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 reason =
|
||||||
|
interaction.options.getString('reason') ?? 'No reason provided';
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: 'That user is not in this server!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.kickable) {
|
||||||
|
return interaction.reply({
|
||||||
|
content:
|
||||||
|
'I cannot kick this user! They may have higher permissions than me.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await target.kick(reason);
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'There was an error trying to kick this user!',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// ping.js - Discord bot ping command
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('ping')
|
||||||
|
.setDescription('Replies with Pong!'),
|
||||||
|
async execute(interaction) {
|
||||||
|
await interaction.reply('Pong! 🏓');
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// prune.js - Discord bot message pruning command
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('prune')
|
||||||
|
.setDescription('Prune up to 99 messages.')
|
||||||
|
.addIntegerOption(option =>
|
||||||
|
option
|
||||||
|
.setName('amount')
|
||||||
|
.setDescription('Number of messages to prune')
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(99)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
||||||
|
async execute(interaction) {
|
||||||
|
const amount = interaction.options.getInteger('amount');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleted = await interaction.channel.bulkDelete(amount, true);
|
||||||
|
await interaction.reply({
|
||||||
|
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,43 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
// deploy-commands.js - Discord bot command deployment
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const { REST, Routes } = require('discord.js');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const commands = [];
|
||||||
|
const commandsPath = path.join(__dirname, 'commands');
|
||||||
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
const command = require(filePath);
|
||||||
|
if ('data' in command && 'execute' in command) {
|
||||||
|
commands.push(command.data.toJSON());
|
||||||
|
} else {
|
||||||
|
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log(`Started refreshing ${commands.length} application (/) commands.`);
|
||||||
|
|
||||||
|
const data = await rest.put(
|
||||||
|
Routes.applicationCommands(process.env.CLIENT_ID),
|
||||||
|
{ body: commands },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
collectCoverage: true,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
coverageDirectory: "coverage",
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
testEnvironment: "node",
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
testMatch: [
|
||||||
|
"**/__tests__/**/*.test.[jt]s?(x)",
|
||||||
|
"**/__tests__/**/*.spec.[jt]s?(x)",
|
||||||
|
"**/?(*.)+(spec|test).[jt]s?(x)"
|
||||||
|
],
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
transform: {
|
||||||
|
"^.+\\.jsx?$": "babel-jest"
|
||||||
|
},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test files
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
"/node_modules/"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Setup files that will be run before each test
|
||||||
|
setupFiles: ["<rootDir>/jest.setup.js"]
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Mock Discord.js Client and other commonly used classes
|
||||||
|
jest.mock('discord.js', () => ({
|
||||||
|
Client: jest.fn(() => ({
|
||||||
|
login: jest.fn().mockResolvedValue('token'),
|
||||||
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
|
on: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
user: {
|
||||||
|
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',
|
||||||
|
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
|
||||||
|
process.env.DISCORD_TOKEN = 'mock-token';
|
||||||
|
process.env.CLIENT_ID = 'mock-client-id';
|
||||||
|
process.env.GUILD_ID = 'mock-guild-id';
|
||||||
Generated
+8299
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "kekbot.js",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A simple Discord bot",
|
||||||
|
"main": "bot.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node bot.js",
|
||||||
|
"deploy": "node deploy-commands.js",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"discord.js": "^14.17.3",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.26.7",
|
||||||
|
"@babel/preset-env": "^7.26.7",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Discord tokens (common patterns)
|
||||||
|
['"](N[a-zA-Z0-9]{23}\.[a-zA-Z0-9]{6}\.[a-zA-Z0-9_-]{27})['"]
|
||||||
|
|
||||||
|
# OpenRouter API keys
|
||||||
|
['"](sk-or-[a-zA-Z0-9-]{32,64})['"]
|
||||||
|
|
||||||
|
# Generic API keys and tokens
|
||||||
|
['"]([a-zA-Z0-9_-]{32,64})['"]
|
||||||
|
['"](api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|client[_-]?secret)['"]\s*[:=]\s*['"][^'"]+['"]
|
||||||
|
|
||||||
|
# Environment variables with values
|
||||||
|
^[^#].*=(["'])[^'"]+\1
|
||||||
|
|
||||||
|
# URLs with embedded credentials
|
||||||
|
[a-zA-Z]{3,10}://[^/\s:@]+:[^/\s:@]+@[^/\s]+
|
||||||
|
|
||||||
|
# Private keys and certificates
|
||||||
|
-----BEGIN.*PRIVATE KEY-----[a-zA-Z0-9\s+/=\n]+-----END.*PRIVATE KEY-----
|
||||||
|
-----BEGIN.*CERTIFICATE-----[a-zA-Z0-9\s+/=\n]+-----END.*CERTIFICATE-----
|
||||||
|
|
||||||
|
# Common credential variable names with values
|
||||||
|
(password|secret|token|key|pwd|apikey|api_key|auth)[\s]*[=:]\s*['"][^'"]*['"]
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
- name: Deploy Discord Bot
|
||||||
|
hosts: discord_bot
|
||||||
|
become: yes # This enables sudo privileges
|
||||||
|
vars:
|
||||||
|
app_dir: /opt/kekbot
|
||||||
|
node_version: "20.x" # Latest LTS version
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Install Node.js repository
|
||||||
|
shell: |
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_{{ node_version }} | bash -
|
||||||
|
|
||||||
|
- name: Install Node.js, npm, and git
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- nodejs
|
||||||
|
- npm
|
||||||
|
- git
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Create application directory
|
||||||
|
file:
|
||||||
|
path: "{{ app_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Copy application files
|
||||||
|
copy:
|
||||||
|
src: "{{ item }}"
|
||||||
|
dest: "{{ app_dir }}/"
|
||||||
|
mode: '0644'
|
||||||
|
with_items:
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
- bot.js
|
||||||
|
- deploy-commands.js
|
||||||
|
- .env
|
||||||
|
- .prettierrc
|
||||||
|
|
||||||
|
- name: Copy commands directory
|
||||||
|
copy:
|
||||||
|
src: commands/
|
||||||
|
dest: "{{ app_dir }}/commands/"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
npm:
|
||||||
|
path: "{{ app_dir }}"
|
||||||
|
state: present
|
||||||
|
production: yes
|
||||||
|
|
||||||
|
- name: Deploy commands
|
||||||
|
command: npm run deploy
|
||||||
|
args:
|
||||||
|
chdir: "{{ app_dir }}"
|
||||||
|
|
||||||
|
- name: Create systemd service file
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/kekbot.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Kek Discord Bot
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory={{ app_dir }}
|
||||||
|
ExecStart=/usr/bin/npm start
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=kekbot
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Reload systemd
|
||||||
|
systemd:
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
|
- name: Stop existing bot service if running
|
||||||
|
systemd:
|
||||||
|
name: kekbot
|
||||||
|
state: stopped
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Enable and start bot service
|
||||||
|
systemd:
|
||||||
|
name: kekbot
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
Reference in New Issue
Block a user