Initial push
commit
581c6a074b
|
@ -0,0 +1,33 @@
|
|||
# Discord Bot Token and app id
|
||||
DISCORD_TOKEN=your_discord_bot_token
|
||||
APPLICATION_ID=your_application_id
|
||||
|
||||
# Bot Configuration
|
||||
# Message response chance (0-100)
|
||||
MESSAGE_RESPONSE_CHANCE=10
|
||||
|
||||
# Voice channel settings
|
||||
# Minimum time in seconds between voice channel joins
|
||||
MIN_VOICE_JOIN_INTERVAL=300
|
||||
# Maximum time in seconds between voice channel joins
|
||||
MAX_VOICE_JOIN_INTERVAL=1800
|
||||
# Minimum time in seconds to stay in a voice channel
|
||||
MIN_VOICE_STAY_DURATION=60
|
||||
# Maximum time in seconds to stay in a voice channel
|
||||
MAX_VOICE_STAY_DURATION=300
|
||||
# Minimum time in seconds between sounds in voice channel
|
||||
MIN_VOICE_MEOW_INTERVAL=5
|
||||
# Maximum time in seconds between sounds in voice channel
|
||||
MAX_VOICE_MEOW_INTERVAL=30
|
||||
|
||||
# Status update interval in seconds
|
||||
STATUS_UPDATE_INTERVAL=300
|
||||
|
||||
# Default theme (cat, dog, fox, robot)
|
||||
DEFAULT_THEME=cat
|
||||
|
||||
# Default pronoun for bot actions (their, his, her, etc.)
|
||||
CAT_PRONOUN=their
|
||||
|
||||
# Audio files directory (optional)
|
||||
# AUDIO_DIR=/path/to/audio/files
|
|
@ -0,0 +1,67 @@
|
|||
# Environment variables and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Operating System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Editor directories and files
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Audio files
|
||||
src/audio/*.mp3
|
||||
src/audio/*.wav
|
||||
src/audio/*.ogg
|
||||
!src/audio/README.txt
|
||||
|
||||
# Data directory - guild settings
|
||||
data/*.json
|
||||
|
||||
# Keep data directory itself but ignore its contents
|
||||
!data/.gitkeep
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Docker files
|
||||
.dockerignore
|
||||
docker-compose.override.yml
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Temp files
|
||||
tmp/
|
||||
temp/
|
|
@ -0,0 +1,133 @@
|
|||
# Docker Guide for Cat Bot
|
||||
|
||||
This guide will help you run the Discord cat bot using Docker, which provides an isolated and consistent environment for the bot to run.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/) installed on your system
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed on your system
|
||||
- A valid Discord bot token in your `.env` file
|
||||
|
||||
## Setup
|
||||
|
||||
1. Make sure your `.env` file is properly configured with your Discord bot token and other settings:
|
||||
|
||||
```
|
||||
DISCORD_TOKEN=your_discord_bot_token
|
||||
|
||||
# Message response chance (0-100)
|
||||
MESSAGE_RESPONSE_CHANCE=10
|
||||
|
||||
# Voice channel settings
|
||||
MIN_VOICE_JOIN_INTERVAL=300
|
||||
MAX_VOICE_JOIN_INTERVAL=1800
|
||||
MIN_VOICE_STAY_DURATION=60
|
||||
MAX_VOICE_STAY_DURATION=300
|
||||
MIN_VOICE_MEOW_INTERVAL=5
|
||||
MAX_VOICE_MEOW_INTERVAL=30
|
||||
|
||||
# Cat personalization
|
||||
CAT_PRONOUN=their
|
||||
```
|
||||
|
||||
2. Add your cat sound files to the `src/audio` directory (create it if it doesn't exist):
|
||||
- meow1.mp3
|
||||
- meow2.mp3
|
||||
- meow3.mp3
|
||||
- purr.mp3
|
||||
- hiss.mp3
|
||||
- yowl.mp3
|
||||
|
||||
## Running the Bot
|
||||
|
||||
### Easy Method
|
||||
|
||||
Use the included helper script:
|
||||
|
||||
```bash
|
||||
# Make it executable
|
||||
chmod +x run-docker.sh
|
||||
|
||||
# Run it
|
||||
./run-docker.sh
|
||||
```
|
||||
|
||||
This script will set the correct user permissions, build, and run the container showing logs.
|
||||
|
||||
### Manual Method
|
||||
|
||||
From the project root directory:
|
||||
|
||||
```bash
|
||||
# Export your user ID and group ID
|
||||
export UID=$(id -u)
|
||||
export GID=$(id -g)
|
||||
|
||||
# Start the container
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Stopping the Bot
|
||||
|
||||
To stop the bot:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Updating the Bot
|
||||
|
||||
If you make changes to the bot code:
|
||||
|
||||
1. Stop the bot: `docker-compose down`
|
||||
2. Rebuild the image: `docker-compose build`
|
||||
3. Start the bot again: `docker-compose up -d`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Audio File Issues
|
||||
|
||||
If the bot isn't playing sounds in voice channels:
|
||||
|
||||
1. Check that your audio files exist in the `src/audio` directory
|
||||
2. Verify they're in MP3 format and match the expected filenames
|
||||
3. Look at the container logs to see if the files are being detected:
|
||||
```bash
|
||||
docker-compose logs | grep "audio"
|
||||
```
|
||||
|
||||
### Bot Token Issues
|
||||
|
||||
If the bot can't connect to Discord:
|
||||
|
||||
1. Make sure your `.env` file is properly mounted in the container
|
||||
2. Check that your token is valid and privileged intents are enabled
|
||||
3. Verify logs for any connection errors:
|
||||
```bash
|
||||
docker-compose logs
|
||||
```
|
||||
|
||||
### File Permission Issues
|
||||
|
||||
If you encounter file permission problems:
|
||||
|
||||
1. Use the `run-docker.sh` script which sets proper user IDs
|
||||
2. Or manually set the user ID when running:
|
||||
```bash
|
||||
export UID=$(id -u) GID=$(id -g)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Container Not Starting
|
||||
|
||||
If the container fails to start or continuously restarts:
|
||||
|
||||
1. Check the logs: `docker-compose logs`
|
||||
2. Verify your container has network access to reach Discord's servers
|
||||
3. Make sure you have ffmpeg installed in the container:
|
||||
```bash
|
||||
docker-compose exec cat-bot apk info | grep ffmpeg
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
FROM node:22-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install ffmpeg for audio processing
|
||||
RUN apk add --no-cache ffmpeg
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build TypeScript code
|
||||
RUN npm run build
|
||||
|
||||
# Create audio directory in dist and copy audio files
|
||||
RUN mkdir -p dist/audio
|
||||
RUN cp -r src/audio/* dist/audio/ 2>/dev/null || :
|
||||
|
||||
# Make startup script executable
|
||||
COPY docker-start.sh /usr/src/app/docker-start.sh
|
||||
RUN chmod +x /usr/src/app/docker-start.sh
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV AUDIO_DIR=/usr/src/app/dist/audio
|
||||
|
||||
# Command to run the app
|
||||
CMD ["/usr/src/app/docker-start.sh"]
|
|
@ -0,0 +1,181 @@
|
|||
### Bot Setup in Discord Developer Portal
|
||||
|
||||
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
2. Create a new application or select your existing one
|
||||
3. Go to the "Bot" tab
|
||||
4. Enable the following Privileged Gateway Intents:
|
||||
- SERVER MEMBERS INTENT
|
||||
- MESSAGE CONTENT INTENT
|
||||
5. Save changes
|
||||
6. Create an invite link:
|
||||
- Go to OAuth2 > URL Generator
|
||||
- Under "SCOPES", select "bot" and "applications.commands"
|
||||
- Under "BOT PERMISSIONS", select:
|
||||
- Read Messages/View Channels
|
||||
- Send Messages
|
||||
- Connect
|
||||
- Speak
|
||||
- Change Nickname
|
||||
- Copy the generated URL and use it to invite your bot to servers
|
||||
|
||||
# Multi-Theme Discord Bot
|
||||
|
||||
A Discord bot that simulates various animal behaviors in your server! Choose from different themes like cats, dogs, foxes, or robots.
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple themes to choose from (cat, dog, fox, robot)
|
||||
- Randomly responds to messages with theme-specific vocalizations and actions
|
||||
- Joins voice channels when people are active and plays themed sounds
|
||||
- Fully configurable behavior (response chance, timing, pronoun preferences)
|
||||
- Realistic animal-like unpredictable behavior
|
||||
- Dynamic status messages that change regularly **and adapt based on voice channel presence**
|
||||
- Per-server configuration (each server can have its own theme)
|
||||
- Each theme has unique nicknames that the bot randomly assigns itself
|
||||
- Bot automatically changes its Discord nickname to match the theme
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16.9.0 or higher
|
||||
- npm or yarn
|
||||
- A Discord bot token (create one at the [Discord Developer Portal](https://discord.com/developers/applications))
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone this repository
|
||||
2. Install dependencies:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
or
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
3. Copy the example environment file:
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
4. Edit the `.env` file and add your Discord bot token and configure other settings
|
||||
5. Add cat sound files to the `src/audio` directory (MP3 format)
|
||||
6. Build the project:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
or
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
7. Start the bot:
|
||||
```
|
||||
npm start
|
||||
```
|
||||
or
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Required Bot Permissions
|
||||
|
||||
When adding your bot to a Discord server, make sure it has the following permissions:
|
||||
- Read Messages/View Channels
|
||||
- Send Messages
|
||||
- Connect to Voice Channels
|
||||
- Speak in Voice Channels
|
||||
- Change Nickname (for theme-based nicknames)
|
||||
|
||||
### Adding Audio Files
|
||||
|
||||
The bot expects audio files for each theme to be placed in the `src/audio` directory. The naming convention is:
|
||||
`theme_sound.mp3` (e.g., cat_meow1.mp3, dog_bark1.mp3)
|
||||
|
||||
Recommended files for each theme:
|
||||
|
||||
**Cat Theme**:
|
||||
- cat_meow1.mp3
|
||||
- cat_meow2.mp3
|
||||
- cat_meow3.mp3
|
||||
- cat_purr.mp3
|
||||
- cat_hiss.mp3
|
||||
- cat_yowl.mp3
|
||||
|
||||
**Dog Theme**:
|
||||
- dog_bark1.mp3
|
||||
- dog_bark2.mp3
|
||||
- dog_whine.mp3
|
||||
- dog_pant.mp3
|
||||
- dog_growl.mp3
|
||||
- dog_howl.mp3
|
||||
|
||||
**Fox Theme**:
|
||||
- fox_bark.mp3
|
||||
- fox_scream.mp3
|
||||
- fox_yip.mp3
|
||||
- fox_howl.mp3
|
||||
- fox_chirp.mp3
|
||||
|
||||
**Robot Theme**:
|
||||
- robot_beep.mp3
|
||||
- robot_whir.mp3
|
||||
- robot_startup.mp3
|
||||
- robot_shutdown.mp3
|
||||
- robot_error.mp3
|
||||
- robot_process.mp3
|
||||
|
||||
## Configuration
|
||||
|
||||
All bot settings can be adjusted in the `.env` file:
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| DISCORD_TOKEN | Your Discord bot token | (required) |
|
||||
| APPLICATION_ID | Your application ID for slash commands | (required) |
|
||||
| MESSAGE_RESPONSE_CHANCE | Percentage chance to respond to a message (0-100) | 10 |
|
||||
| MIN_VOICE_JOIN_INTERVAL | Minimum time in seconds between voice channel joins | 300 |
|
||||
| MAX_VOICE_JOIN_INTERVAL | Maximum time in seconds between voice channel joins | 1800 |
|
||||
| MIN_VOICE_STAY_DURATION | Minimum time in seconds to stay in a voice channel | 60 |
|
||||
| MAX_VOICE_STAY_DURATION | Maximum time in seconds to stay in a voice channel | 300 |
|
||||
| MIN_VOICE_MEOW_INTERVAL | Minimum time in seconds between sounds in voice channel | 5 |
|
||||
| MAX_VOICE_MEOW_INTERVAL | Maximum time in seconds between sounds in voice channel | 30 |
|
||||
| STATUS_UPDATE_INTERVAL | How often (in seconds) the bot changes its status | 300 |
|
||||
| DEFAULT_THEME | Default theme for new servers (cat, dog, fox, robot) | cat |
|
||||
| CAT_PRONOUN | Default pronoun to use for actions (their, his, her, etc.) | their |
|
||||
|
||||
## Slash Commands
|
||||
|
||||
The bot provides slash commands for server administrators to customize its behavior:
|
||||
|
||||
- `/theme set <theme>` - Change the bot's theme (cat, dog, fox, robot)
|
||||
- `/theme info` - View information about the current theme
|
||||
- `/settings response <chance>` - Set how often the bot responds (0-100%)
|
||||
- `/settings pronoun <pronoun>` - Set which pronoun the bot uses (their, his, her, etc.)
|
||||
- `/settings view` - View current settings
|
||||
|
||||
## Theme Nicknames
|
||||
|
||||
Each time you change the theme, the bot randomly selects a nickname from the theme's list:
|
||||
|
||||
**Cat Theme**: Whiskers, Mittens, Shadow, Luna, Oliver, Simba, Bella, Mr. Purrington, Fluffy, Captain Fuzzyboots
|
||||
|
||||
**Dog Theme**: Buddy, Max, Bailey, Cooper, Daisy, Sir Barksalot, Fido, Rover, Scout, Captain Goodboy
|
||||
|
||||
**Fox Theme**: Sly, Rusty, Firefox, Swift, Amber, Vixen, Todd, Reynard, Professor Pounce
|
||||
|
||||
**Robot Theme**: B33P-B00P, CyberTron, Metal Friend, Unit-7, RoboCompanion, Circuit, T1000, BinaryBuddy, Mechanoid, SynthFriend
|
||||
|
||||
## Voice-Aware Status Messages
|
||||
|
||||
The bot changes its status messages depending on whether it's in a voice channel:
|
||||
|
||||
### Cat Theme Example:
|
||||
- **Normal:** 🐱 hunting mice, 🐱 plotting world domination, 🐱 napping in a sunbeam
|
||||
- **In Voice Channel:** 🐱 purring loudly, 🐱 meowing for attention, 🐱 batting at microphones
|
||||
|
||||
### Dog Theme Example:
|
||||
- **Normal:** 🐶 chasing squirrels, 🐶 fetching balls, 🐶 guarding the house
|
||||
- **In Voice Channel:** 🐶 barking excitedly, 🐶 panting into the mic, 🐶 howling along
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,26 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
cat-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: discord-cat-bot
|
||||
restart: unless-stopped
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
volumes:
|
||||
# Mount the audio directory so you can add/change sound files without rebuilding
|
||||
- ./src/audio:/usr/src/app/dist/audio
|
||||
# Mount the data directory for persistent settings
|
||||
- ./data:/usr/src/app/data
|
||||
# Mount the .env file for configuration
|
||||
- ./.env:/usr/src/app/.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- AUDIO_DIR=/usr/src/app/dist/audio
|
||||
# Simple healthcheck to ensure the container is running properly
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "console.log('Health check')"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Verify environment
|
||||
echo "Starting Bot..."
|
||||
echo "Node version: $(node -v)"
|
||||
echo "Working directory: $(pwd)"
|
||||
|
||||
# Ensure data directory exists
|
||||
echo "Checking data directory for guild settings"
|
||||
mkdir -p /usr/src/app/data
|
||||
|
||||
# Check for audio files
|
||||
echo "Checking audio directory: ${AUDIO_DIR}"
|
||||
mkdir -p ${AUDIO_DIR}
|
||||
|
||||
if [ -z "$(ls -A ${AUDIO_DIR} 2>/dev/null)" ]; then
|
||||
echo "Warning: No audio files found in ${AUDIO_DIR}"
|
||||
echo "The bot can still run, but won't play sounds in voice channels."
|
||||
else
|
||||
echo "Found audio files:"
|
||||
ls -la ${AUDIO_DIR}
|
||||
fi
|
||||
|
||||
# Check for .env file
|
||||
if [ ! -f .env ]; then
|
||||
echo "Warning: No .env file found"
|
||||
fi
|
||||
|
||||
# Start the bot
|
||||
echo "Starting bot..."
|
||||
node dist/index.js
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Make scripts executable
|
||||
chmod +x run-docker.sh
|
||||
chmod +x docker-start.sh
|
||||
|
||||
echo "Scripts are now executable!"
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "cat-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "A Discord bot that behaves like a cat",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"debug": "ts-node src/debug.ts",
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"keywords": [
|
||||
"discord",
|
||||
"bot",
|
||||
"cat",
|
||||
"typescript"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.10.1",
|
||||
"@discordjs/opus": "^0.10.0",
|
||||
"@discordjs/voice": "^0.16.1",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"libsodium-wrappers": "^0.7.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Export current user ID and group ID to be used by docker-compose
|
||||
export UID=$(id -u)
|
||||
export GID=$(id -g)
|
||||
|
||||
# Make sure audio directory exists
|
||||
mkdir -p ./src/audio
|
||||
|
||||
# Stop any existing container
|
||||
docker-compose down
|
||||
|
||||
# Build and start the container
|
||||
docker-compose up -d
|
||||
|
||||
# Show logs
|
||||
docker-compose logs -f
|
|
@ -0,0 +1,14 @@
|
|||
Add your cat sound MP3 files in this directory.
|
||||
|
||||
Required files (matching the names in src/utils/cat-behaviors.ts):
|
||||
- meow1.mp3
|
||||
- meow2.mp3
|
||||
- meow3.mp3
|
||||
- purr.mp3
|
||||
- hiss.mp3
|
||||
- yowl.mp3
|
||||
|
||||
You can find free cat sound effects on sites like:
|
||||
- Freesound.org
|
||||
- SoundBible.com
|
||||
- ZapSplat.com
|
|
@ -0,0 +1,14 @@
|
|||
import { Collection } from 'discord.js';
|
||||
import * as themeCommand from './theme';
|
||||
import * as settingsCommand from './settings';
|
||||
|
||||
// Collection of commands
|
||||
export const commands = [
|
||||
themeCommand,
|
||||
settingsCommand
|
||||
];
|
||||
|
||||
// Map for quick command lookup
|
||||
export const commandsMap = new Collection(
|
||||
commands.map(command => [command.data.name, command])
|
||||
);
|
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
CommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
PermissionFlagsBits,
|
||||
EmbedBuilder,
|
||||
ChatInputCommandInteraction
|
||||
} from 'discord.js';
|
||||
import { guildSettings } from '../utils/guild-settings';
|
||||
import { getTheme } from '../utils/themes';
|
||||
|
||||
// Create the command builder
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('settings')
|
||||
.setDescription('Configure bot settings')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('response')
|
||||
.setDescription('Set how often the bot responds to messages')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('chance')
|
||||
.setDescription('Chance of responding (0-100%)')
|
||||
.setRequired(true)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(100)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('pronoun')
|
||||
.setDescription('Set which pronoun the bot uses')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('pronoun')
|
||||
.setDescription('The pronoun to use (their/his/her/etc)')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('view')
|
||||
.setDescription('View current settings')
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
|
||||
// Command execution
|
||||
export async function execute(interaction: CommandInteraction): Promise<void> {
|
||||
if (!interaction.guildId) {
|
||||
await interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = (interaction as ChatInputCommandInteraction).options.getSubcommand();
|
||||
|
||||
if (subcommand === 'response') {
|
||||
const chance = interaction.options.get('chance')?.value as number;
|
||||
guildSettings.setResponseChance(interaction.guildId, chance);
|
||||
|
||||
await interaction.reply({
|
||||
content: `Response chance set to ${chance}%`,
|
||||
ephemeral: false
|
||||
});
|
||||
} else if (subcommand === 'pronoun') {
|
||||
const pronoun = interaction.options.get('pronoun')?.value as string;
|
||||
guildSettings.setPronoun(interaction.guildId, pronoun);
|
||||
|
||||
await interaction.reply({
|
||||
content: `Pronoun set to "${pronoun}"`,
|
||||
ephemeral: false
|
||||
});
|
||||
} else if (subcommand === 'view') {
|
||||
const settings = guildSettings.getSettings(interaction.guildId);
|
||||
const theme = getTheme(settings.themeId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${theme.emojiIcon} Bot Settings`)
|
||||
.setColor('#3498db')
|
||||
.addFields(
|
||||
{ name: 'Theme', value: theme.name, inline: true },
|
||||
{ name: 'Response Chance', value: `${settings.responseChance}%`, inline: true },
|
||||
{ name: 'Pronoun', value: settings.pronoun, inline: true }
|
||||
)
|
||||
.setFooter({ text: 'Use /theme and /settings commands to change these settings' });
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
ephemeral: false
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
CommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
PermissionFlagsBits,
|
||||
EmbedBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ActionRowBuilder,
|
||||
Guild,
|
||||
CommandInteractionOptionResolver,
|
||||
CacheType
|
||||
} from 'discord.js';
|
||||
import { guildSettings } from '../utils/guild-settings';
|
||||
import { getAvailableThemeIds, getTheme, getRandomNickname } from '../utils/themes';
|
||||
|
||||
// Create the command builder
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('theme')
|
||||
.setDescription('Change or view the bot\'s theme')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('set')
|
||||
.setDescription('Set the bot\'s theme')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('theme')
|
||||
.setDescription('The theme to set')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
...getAvailableThemeIds().map(id => ({
|
||||
name: getTheme(id).name,
|
||||
value: id
|
||||
}))
|
||||
)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('info')
|
||||
.setDescription('Get information about the current theme')
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
|
||||
|
||||
// Command execution
|
||||
export async function execute(interaction: CommandInteraction): Promise<void> {
|
||||
if (!interaction.guildId) {
|
||||
await interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = (interaction.options as CommandInteractionOptionResolver<CacheType>).getSubcommand();
|
||||
|
||||
if (subcommand === 'set') {
|
||||
const themeId = interaction.options.get('theme')?.value as string;
|
||||
|
||||
if (guildSettings.setTheme(interaction.guildId, themeId)) {
|
||||
const theme = getTheme(themeId);
|
||||
|
||||
// Get a random nickname for this theme
|
||||
const nickname = getRandomNickname(themeId);
|
||||
|
||||
// Try to update the bot's nickname
|
||||
try {
|
||||
const guild = interaction.guild as Guild;
|
||||
const me = guild.members.cache.get(guild.client.user.id);
|
||||
if (me && me.manageable) {
|
||||
await me.setNickname(`${nickname} ${theme.emojiIcon}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to update nickname in guild ${interaction.guildId}:`, error);
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
content: `${theme.emojiIcon} Theme changed to **${theme.name}**! I'll now act like a ${theme.name.toLowerCase()} named **${nickname}**.`,
|
||||
ephemeral: false
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'Failed to set theme. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
} else if (subcommand === 'info') {
|
||||
const settings = guildSettings.getSettings(interaction.guildId);
|
||||
const theme = getTheme(settings.themeId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${theme.emojiIcon} ${theme.name} Theme`)
|
||||
.setDescription(`The bot is currently using the **${theme.name}** theme in this server.`)
|
||||
.setColor('#3498db')
|
||||
.addFields(
|
||||
{ name: 'Response Chance', value: `${settings.responseChance}%`, inline: true },
|
||||
{ name: 'Pronoun', value: settings.pronoun, inline: true },
|
||||
{ name: 'Example Vocalizations', value: theme.vocalizations.slice(0, 3).join('\n'), inline: false },
|
||||
{ name: 'Example Actions', value: theme.actions.slice(0, 3).map(a => a.replace('PRONOUN', settings.pronoun)).join('\n'), inline: false }
|
||||
);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
...getAvailableThemeIds()
|
||||
.filter(id => id !== settings.themeId)
|
||||
.slice(0, 5) // Maximum 5 buttons per row
|
||||
.map(id => {
|
||||
const buttonTheme = getTheme(id);
|
||||
return new ButtonBuilder()
|
||||
.setCustomId(`theme_${id}`)
|
||||
.setLabel(`${buttonTheme.emojiIcon} ${buttonTheme.name}`)
|
||||
.setStyle(ButtonStyle.Secondary);
|
||||
})
|
||||
);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
ephemeral: false
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { DEFAULT_THEME_ID } from '../utils/themes';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
interface CatBotConfig {
|
||||
token: string;
|
||||
messageResponseChance: number;
|
||||
voiceChannelConfig: {
|
||||
minJoinInterval: number;
|
||||
maxJoinInterval: number;
|
||||
minStayDuration: number;
|
||||
maxStayDuration: number;
|
||||
minMeowInterval: number;
|
||||
maxMeowInterval: number;
|
||||
};
|
||||
catPersonalization: {
|
||||
pronoun: string;
|
||||
};
|
||||
audioFilesDir: string;
|
||||
statusUpdateInterval: number;
|
||||
defaultTheme: string;
|
||||
}
|
||||
|
||||
export const config: CatBotConfig = {
|
||||
token: process.env.DISCORD_TOKEN || '',
|
||||
messageResponseChance: Number(process.env.MESSAGE_RESPONSE_CHANCE || 10),
|
||||
voiceChannelConfig: {
|
||||
minJoinInterval: Number(process.env.MIN_VOICE_JOIN_INTERVAL || 300),
|
||||
maxJoinInterval: Number(process.env.MAX_VOICE_JOIN_INTERVAL || 1800),
|
||||
minStayDuration: Number(process.env.MIN_VOICE_STAY_DURATION || 60),
|
||||
maxStayDuration: Number(process.env.MAX_VOICE_STAY_DURATION || 300),
|
||||
minMeowInterval: Number(process.env.MIN_VOICE_MEOW_INTERVAL || 5),
|
||||
maxMeowInterval: Number(process.env.MAX_VOICE_MEOW_INTERVAL || 30),
|
||||
},
|
||||
catPersonalization: {
|
||||
pronoun: process.env.CAT_PRONOUN || 'their',
|
||||
},
|
||||
audioFilesDir: process.env.AUDIO_FILES_DIR || path.join(__dirname, '..', 'audio'),
|
||||
statusUpdateInterval: Number(process.env.STATUS_UPDATE_INTERVAL || 300), // 5 minutes
|
||||
defaultTheme: process.env.DEFAULT_THEME || DEFAULT_THEME_ID,
|
||||
};
|
||||
|
||||
// Validate required configuration
|
||||
if (!config.token) {
|
||||
throw new Error('Missing Discord bot token in environment variables');
|
||||
}
|
||||
|
||||
// Audio files directory - using environment variable or default path
|
||||
const AUDIO_DIR = process.env.AUDIO_DIR || path.join(__dirname, '..', 'audio');
|
||||
export const AUDIO_FILES_DIR = AUDIO_DIR;
|
|
@ -0,0 +1,75 @@
|
|||
import { Client, Events, GatewayIntentBits, ActivityType } from 'discord.js';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Create a simplified client with all intents
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildPresences, // Add this too
|
||||
]
|
||||
});
|
||||
|
||||
// Log detailed info when ready
|
||||
client.once(Events.ClientReady, (readyClient) => {
|
||||
console.log('===========================================');
|
||||
console.log(`Logged in as: ${readyClient.user.tag}`);
|
||||
console.log(`Bot ID: ${readyClient.user.id}`);
|
||||
console.log('===========================================');
|
||||
|
||||
// Set a visible activity status
|
||||
readyClient.user.setActivity('Joining servers...', { type: ActivityType.Playing });
|
||||
|
||||
// Log guild membership
|
||||
console.log(`Currently in ${client.guilds.cache.size} guilds:`);
|
||||
|
||||
if (client.guilds.cache.size === 0) {
|
||||
console.log('Not in any guilds! Please check your invite link.');
|
||||
|
||||
// Generate a proper invite link
|
||||
const inviteURL = `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user.id}&permissions=3214336&scope=bot%20applications.commands`;
|
||||
console.log('\nUse this invite link:');
|
||||
console.log(inviteURL);
|
||||
} else {
|
||||
client.guilds.cache.forEach(guild => {
|
||||
console.log(`- ${guild.name} (ID: ${guild.id}) - Members: ${guild.memberCount}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add handler for guild joins to confirm when it works
|
||||
client.on(Events.GuildCreate, (guild) => {
|
||||
console.log(`✅ JOINED GUILD: ${guild.name} (${guild.id})`);
|
||||
console.log(`Now in ${client.guilds.cache.size} guilds`);
|
||||
});
|
||||
|
||||
// Log any errors
|
||||
client.on(Events.Error, (error) => {
|
||||
console.error('Discord client error:', error);
|
||||
});
|
||||
|
||||
// Login with token
|
||||
console.log('Attempting to log in...');
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
if (!token) {
|
||||
console.error('ERROR: No Discord token found in .env file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Show first few characters of token for debugging (never show full token)
|
||||
console.log(`Using token starting with: ${token.substring(0, 5)}...`);
|
||||
|
||||
client.login(token)
|
||||
.then(() => console.log('Login successful!'))
|
||||
.catch(error => {
|
||||
console.error('Failed to log in:', error.message);
|
||||
console.error('Please check if your token is valid');
|
||||
process.exit(1);
|
||||
});
|
|
@ -0,0 +1,379 @@
|
|||
import {
|
||||
Client,
|
||||
Events,
|
||||
GatewayIntentBits,
|
||||
Message,
|
||||
VoiceChannel,
|
||||
ChannelType,
|
||||
Interaction,
|
||||
Guild,
|
||||
GuildMember,
|
||||
ButtonInteraction,
|
||||
VoiceState
|
||||
} from 'discord.js';
|
||||
import { config } from './config/config';
|
||||
import { getRandomInt, randomChance } from './utils/random';
|
||||
import { getRandomBehavior, getAudioFilePrefix } from './utils/bot-behaviors';
|
||||
import { getRandomNickname, getTheme } from './utils/themes';
|
||||
import { CatAudioPlayer } from './utils/audio-player';
|
||||
import { guildSettings } from './utils/guild-settings';
|
||||
import { deployCommands } from './utils/command-deployer';
|
||||
import { commandsMap } from './commands';
|
||||
import { StatusManager } from './utils/status-manager';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { AUDIO_FILES_DIR } from './config/config';
|
||||
|
||||
// Create a new client instance with required intents
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
]
|
||||
});
|
||||
|
||||
// Initialize audio player
|
||||
const audioPlayer = new CatAudioPlayer();
|
||||
|
||||
// Create status manager
|
||||
const statusManager = new StatusManager(client);
|
||||
|
||||
// Map to track voice channel join timers
|
||||
const voiceJoinTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
// Deploy commands when starting
|
||||
(async () => {
|
||||
try {
|
||||
await deployCommands();
|
||||
} catch (error) {
|
||||
console.error('Failed to deploy commands:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Handle new guild joins
|
||||
client.on(Events.GuildCreate, async (guild: Guild) => {
|
||||
console.log(`Joined new guild: ${guild.name} (${guild.id})`);
|
||||
try {
|
||||
await guild.members.fetch();
|
||||
console.log(`Fetched ${guild.members.cache.size} members for new guild`);
|
||||
|
||||
// Create default settings for this guild
|
||||
const settings = guildSettings.getSettings(guild.id);
|
||||
console.log(`Created settings for guild: ${guild.name} - Theme: ${settings.themeId}`);
|
||||
|
||||
// Set a themed nickname
|
||||
const theme = getTheme(settings.themeId);
|
||||
const nickname = getRandomNickname(settings.themeId);
|
||||
try {
|
||||
const me = guild.members.cache.get(guild.client.user!.id);
|
||||
if (me && me.manageable) {
|
||||
await me.setNickname(`${nickname} ${theme.emojiIcon}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to set nickname in new guild ${guild.id}:`, error);
|
||||
}
|
||||
|
||||
// Update status to reflect new guild count
|
||||
statusManager.startStatusRotation();
|
||||
|
||||
// Notify the server of the bot's presence
|
||||
const defaultChannel = guild.channels.cache.find(channel =>
|
||||
channel.type === ChannelType.GuildText &&
|
||||
(channel.name.includes('general') || channel.name.includes('bot') || channel.name.includes('welcome'))
|
||||
);
|
||||
|
||||
if (defaultChannel?.isTextBased()) {
|
||||
defaultChannel.send(
|
||||
`${getRandomBehavior(guild.id)}\n\nHi everyone! I'm **${nickname}**, a bot that behaves like a ${theme.name.toLowerCase()}! ` +
|
||||
`${theme.emojiIcon} Use \`/theme\` to change what kind of animal I am, and \`/settings\` to configure my behavior!`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error setting up new guild:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bot being removed from a guild
|
||||
client.on(Events.GuildDelete, (guild: Guild) => {
|
||||
console.log(`Removed from guild: ${guild.name} (${guild.id})`);
|
||||
// Clear any timers for this guild
|
||||
if (voiceJoinTimers.has(guild.id)) {
|
||||
clearTimeout(voiceJoinTimers.get(guild.id));
|
||||
voiceJoinTimers.delete(guild.id);
|
||||
}
|
||||
|
||||
// Update status to reflect new guild count
|
||||
statusManager.startStatusRotation();
|
||||
});
|
||||
|
||||
// Handle new members joining
|
||||
client.on(Events.GuildMemberAdd, (member: GuildMember) => {
|
||||
console.log(`New member joined: ${member.user.username} in ${member.guild.name}`);
|
||||
});
|
||||
|
||||
// Handle voice state updates
|
||||
client.on(Events.VoiceStateUpdate, (oldState: VoiceState, newState: VoiceState) => {
|
||||
// Only interested in the bot's voice state
|
||||
if (oldState.member?.user.id !== client.user?.id && newState.member?.user.id !== client.user?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the bot joined or left a voice channel
|
||||
const wasInVoice = !!oldState.channel;
|
||||
const isInVoice = !!newState.channel;
|
||||
|
||||
// If voice state changed, update the status
|
||||
if (wasInVoice !== isInVoice) {
|
||||
console.log(`Bot ${isInVoice ? 'joined' : 'left'} a voice channel, updating status...`);
|
||||
statusManager.startStatusRotation(); // This will force an immediate update
|
||||
}
|
||||
});
|
||||
|
||||
// When the client is ready, run this code (only once)
|
||||
client.once(Events.ClientReady, async (readyClient: Client) => {
|
||||
console.log(`Ready! Logged in as ${readyClient.user?.tag}`);
|
||||
|
||||
// Check if audio directory exists, create it if not
|
||||
if (!fs.existsSync(AUDIO_FILES_DIR)) {
|
||||
fs.mkdirSync(AUDIO_FILES_DIR, { recursive: true });
|
||||
console.log('Created audio directory. Please add cat sound files to it.');
|
||||
}
|
||||
|
||||
// Start status rotation
|
||||
statusManager.startStatusRotation();
|
||||
|
||||
// Fetch members for all guilds
|
||||
console.log(`Bot is in ${client.guilds.cache.size} guilds`);
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
try {
|
||||
console.log(`Fetching members for guild: ${guild.name} (${guild.id})`);
|
||||
await guild.members.fetch();
|
||||
console.log(`Successfully fetched ${guild.members.cache.size} members`);
|
||||
|
||||
// Ensure guild has settings
|
||||
const settings = guildSettings.getSettings(guild.id);
|
||||
|
||||
// Set the bot's nickname based on theme
|
||||
const theme = getTheme(settings.themeId);
|
||||
const nickname = getRandomNickname(settings.themeId);
|
||||
try {
|
||||
const me = guild.members.cache.get(client.user!.id);
|
||||
if (me && me.manageable) {
|
||||
await me.setNickname(`${nickname} ${theme.emojiIcon}`);
|
||||
console.log(`Set nickname to "${nickname} ${theme.emojiIcon}" in guild ${guild.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to set nickname in guild ${guild.name}:`, error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching members for guild ${guild.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start monitoring voice channels
|
||||
startVoiceChannelMonitoring();
|
||||
|
||||
// List info about joined guilds
|
||||
console.log("Bot Information:");
|
||||
client.guilds.cache.forEach((guild: Guild) => {
|
||||
const settings = guildSettings.getSettings(guild.id);
|
||||
console.log(`Guild: ${guild.name} - Theme: ${settings.themeId} - Response: ${settings.responseChance}%`);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle slash commands
|
||||
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
||||
// Handle slash commands
|
||||
if (interaction.isCommand()) {
|
||||
const command = commandsMap.get(interaction.commandName);
|
||||
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (error) {
|
||||
console.error(`Error executing command ${interaction.commandName}:`, error);
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: 'There was an error executing this command!', ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: 'There was an error executing this command!', ephemeral: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle button interactions
|
||||
if (interaction.isButton()) {
|
||||
const buttonInteraction = interaction as ButtonInteraction;
|
||||
const customId = buttonInteraction.customId;
|
||||
|
||||
// Handle theme change buttons
|
||||
if (customId.startsWith('theme_')) {
|
||||
const themeId = customId.split('_')[1];
|
||||
|
||||
if (interaction.guildId) {
|
||||
if (guildSettings.setTheme(interaction.guildId, themeId)) {
|
||||
const theme = getTheme(themeId);
|
||||
const nickname = getRandomNickname(themeId);
|
||||
|
||||
// Try to update the bot's nickname
|
||||
try {
|
||||
const guild = interaction.guild;
|
||||
if (guild) {
|
||||
const me = guild.members.cache.get(guild.client.user!.id);
|
||||
if (me && me.manageable) {
|
||||
await me.setNickname(`${nickname} ${theme.emojiIcon}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to update nickname in guild ${interaction.guildId}:`, error);
|
||||
}
|
||||
|
||||
await buttonInteraction.update({
|
||||
content: `Theme updated to **${theme.name}**! I'm now **${nickname}** ${theme.emojiIcon}`,
|
||||
components: []
|
||||
});
|
||||
} else {
|
||||
await buttonInteraction.update({ content: 'Failed to update theme', components: [] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
// Ignore bot messages
|
||||
if (message.author.bot || !message.guildId) return;
|
||||
|
||||
// Get response chance for this guild
|
||||
const responseChance = guildSettings.getSettings(message.guildId).responseChance;
|
||||
|
||||
// Random chance to respond based on guild settings
|
||||
if (randomChance(responseChance)) {
|
||||
try {
|
||||
// Get a random behavior for this guild's theme
|
||||
const response = getRandomBehavior(message.guildId);
|
||||
|
||||
// Send the response
|
||||
if (message.channel.isTextBased()) {
|
||||
await message.reply(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error responding to message:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Start monitoring voice channels for activity
|
||||
*/
|
||||
function startVoiceChannelMonitoring(): void {
|
||||
// Check voice channels regularly
|
||||
setInterval(() => {
|
||||
client.guilds.cache.forEach((guild: Guild) => {
|
||||
// Find voice channels with members
|
||||
const activeVoiceChannels = guild.channels.cache
|
||||
.filter((channel) =>
|
||||
channel.isVoiceBased() &&
|
||||
'members' in channel &&
|
||||
channel.members.size > 0 &&
|
||||
!channel.members.every((member: GuildMember) => member.user.bot)
|
||||
);
|
||||
|
||||
if (activeVoiceChannels.size > 0) {
|
||||
// Randomly pick a voice channel to potentially join
|
||||
const channelID = guild.id;
|
||||
|
||||
// If we don't have a timer for this guild, create one
|
||||
if (!voiceJoinTimers.has(channelID)) {
|
||||
const randomChannel = activeVoiceChannels.random();
|
||||
if (randomChannel && randomChannel.type === ChannelType.GuildVoice) {
|
||||
scheduleNextVoiceChannelJoin(guild.id, randomChannel as VoiceChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 10000); // Check every 10 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the next voice channel join for a guild
|
||||
*/
|
||||
function scheduleNextVoiceChannelJoin(guildId: string, initialChannel?: VoiceChannel): void {
|
||||
// Clear any existing timer
|
||||
if (voiceJoinTimers.has(guildId)) {
|
||||
const existingTimer = voiceJoinTimers.get(guildId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
}
|
||||
|
||||
// Get random interval for next join
|
||||
const nextJoinDelay = getRandomInt(
|
||||
config.voiceChannelConfig.minJoinInterval,
|
||||
config.voiceChannelConfig.maxJoinInterval
|
||||
) * 1000;
|
||||
|
||||
// Schedule next join
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
|
||||
if (guild) {
|
||||
// Find active voice channels
|
||||
const activeVoiceChannels = guild.channels.cache
|
||||
.filter((channel) =>
|
||||
channel.isVoiceBased() &&
|
||||
'members' in channel &&
|
||||
channel.members.size > 0 &&
|
||||
!channel.members.every((member: GuildMember) => member.user.bot)
|
||||
);
|
||||
|
||||
if (activeVoiceChannels.size > 0) {
|
||||
// Choose a random active voice channel
|
||||
const randomChannel = activeVoiceChannels.random();
|
||||
if (randomChannel && randomChannel.type === ChannelType.GuildVoice) {
|
||||
const chosenChannel = randomChannel as VoiceChannel;
|
||||
|
||||
// Get audio file prefix based on guild theme
|
||||
const audioFilePrefix = getAudioFilePrefix(guildId);
|
||||
|
||||
// Join and play sounds
|
||||
await audioPlayer.joinChannel(
|
||||
chosenChannel,
|
||||
config.voiceChannelConfig.minStayDuration,
|
||||
config.voiceChannelConfig.maxStayDuration,
|
||||
config.voiceChannelConfig.minMeowInterval,
|
||||
config.voiceChannelConfig.maxMeowInterval,
|
||||
audioFilePrefix
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error joining voice channel:', error);
|
||||
}
|
||||
|
||||
// Schedule next join
|
||||
scheduleNextVoiceChannelJoin(guildId);
|
||||
}, nextJoinDelay);
|
||||
|
||||
// Store the timer
|
||||
voiceJoinTimers.set(guildId, timer);
|
||||
|
||||
const guildName = client.guilds.cache.get(guildId)?.name || 'Unknown Guild';
|
||||
const channelName = initialChannel ? initialChannel.name : 'any active channel';
|
||||
console.log(`Scheduled voice channel join for guild ${guildName} (${guildId}) in ${nextJoinDelay / 1000} seconds`);
|
||||
if (initialChannel) {
|
||||
console.log(`Initial channel: ${initialChannel.name}`);
|
||||
} else {
|
||||
console.log(`No initial channel specified, will join any active channel`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log in to Discord with your token
|
||||
client.login(config.token);
|
|
@ -0,0 +1,162 @@
|
|||
import {
|
||||
AudioPlayer,
|
||||
AudioPlayerStatus,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
getVoiceConnection,
|
||||
joinVoiceChannel,
|
||||
VoiceConnectionStatus,
|
||||
NoSubscriberBehavior
|
||||
} from '@discordjs/voice';
|
||||
import { VoiceChannel } from 'discord.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { AUDIO_FILES_DIR } from '../config/config';
|
||||
import { getRandomElement, getRandomInt, sleep } from './random';
|
||||
import { CAT_AUDIO_FILES } from './cat-behaviors';
|
||||
|
||||
export class CatAudioPlayer {
|
||||
private audioPlayer: AudioPlayer;
|
||||
private isPlaying: boolean = false;
|
||||
private shouldStop: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.audioPlayer = createAudioPlayer({
|
||||
behaviors: {
|
||||
noSubscriber: NoSubscriberBehavior.Pause,
|
||||
}
|
||||
});
|
||||
|
||||
this.audioPlayer.on(AudioPlayerStatus.Idle, () => {
|
||||
this.isPlaying = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a voice channel and start playing cat sounds
|
||||
*/
|
||||
public async joinChannel(
|
||||
voiceChannel: VoiceChannel,
|
||||
minStayDuration: number,
|
||||
maxStayDuration: number,
|
||||
minMeowInterval: number,
|
||||
maxMeowInterval: number,
|
||||
audioFilePrefix: string = ''
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Create connection - using Discord.js's built-in adapter
|
||||
// The @ts-ignore is required because of type compatibility issues between Discord.js and @discordjs/voice
|
||||
// This is a common issue and is safe to ignore in this case
|
||||
const connection = joinVoiceChannel({
|
||||
channelId: voiceChannel.id,
|
||||
guildId: voiceChannel.guild.id,
|
||||
// @ts-ignore: Type compatibility issue between Discord.js and @discordjs/voice
|
||||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
||||
try {
|
||||
// Try to reconnect if disconnected unexpectedly
|
||||
await Promise.race([
|
||||
new Promise(resolve => connection.on(VoiceConnectionStatus.Ready, resolve)),
|
||||
new Promise((_, reject) => setTimeout(reject, 5000))
|
||||
]);
|
||||
} catch (error) {
|
||||
// If we can't reconnect within 5 seconds, destroy the connection
|
||||
connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe the connection to the audio player
|
||||
connection.subscribe(this.audioPlayer);
|
||||
|
||||
// Reset stop flag
|
||||
this.shouldStop = false;
|
||||
|
||||
// Determine how long the cat will stay
|
||||
const stayDuration = getRandomInt(minStayDuration, maxStayDuration) * 1000;
|
||||
|
||||
console.log(`Cat joined ${voiceChannel.name} for ${stayDuration / 1000} seconds`);
|
||||
|
||||
// Start playing sounds
|
||||
this.playRandomCatSounds(minMeowInterval, maxMeowInterval, stayDuration, audioFilePrefix);
|
||||
|
||||
// Leave after duration
|
||||
setTimeout(() => {
|
||||
this.shouldStop = true;
|
||||
|
||||
// Wait for any current sound to finish
|
||||
setTimeout(() => {
|
||||
const connection = getVoiceConnection(voiceChannel.guild.id);
|
||||
if (connection) {
|
||||
connection.destroy();
|
||||
console.log(`Cat left ${voiceChannel.name}`);
|
||||
}
|
||||
}, 1000);
|
||||
}, stayDuration);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error joining voice channel:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play random cat sounds at random intervals
|
||||
*/
|
||||
private async playRandomCatSounds(
|
||||
minInterval: number,
|
||||
maxInterval: number,
|
||||
maxDuration: number,
|
||||
audioFilePrefix: string = ''
|
||||
): Promise<void> {
|
||||
let elapsedTime = 0;
|
||||
|
||||
while (elapsedTime < maxDuration && !this.shouldStop) {
|
||||
// Play a random cat sound
|
||||
if (!this.isPlaying) {
|
||||
await this.playRandomSound(audioFilePrefix);
|
||||
}
|
||||
|
||||
// Wait for a random interval
|
||||
const interval = getRandomInt(minInterval, maxInterval) * 1000;
|
||||
await sleep(interval);
|
||||
elapsedTime += interval;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a random cat sound file
|
||||
*/
|
||||
private async playRandomSound(audioFilePrefix: string = ''): Promise<void> {
|
||||
try {
|
||||
// Filter audio files by prefix if provided
|
||||
let availableFiles = CAT_AUDIO_FILES;
|
||||
|
||||
if (audioFilePrefix) {
|
||||
const prefixedFiles = CAT_AUDIO_FILES.filter(file => file.startsWith(audioFilePrefix));
|
||||
if (prefixedFiles.length > 0) {
|
||||
availableFiles = prefixedFiles;
|
||||
}
|
||||
}
|
||||
|
||||
// Get a random audio file
|
||||
const audioFile = getRandomElement(availableFiles);
|
||||
const audioPath = path.join(AUDIO_FILES_DIR, audioFile);
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(audioPath)) {
|
||||
console.warn(`Audio file not found: ${audioPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and play the audio resource
|
||||
const resource = createAudioResource(audioPath);
|
||||
this.isPlaying = true;
|
||||
this.audioPlayer.play(resource);
|
||||
} catch (error) {
|
||||
console.error('Error playing audio:', error);
|
||||
this.isPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { getRandomElement } from './random';
|
||||
import { guildSettings } from './guild-settings';
|
||||
import { getTheme } from './themes';
|
||||
|
||||
/**
|
||||
* Get a random vocalization or action for a guild
|
||||
*/
|
||||
export function getRandomBehavior(guildId: string): string {
|
||||
// Get guild settings
|
||||
const settings = guildSettings.getSettings(guildId);
|
||||
|
||||
// Get theme
|
||||
const theme = getTheme(settings.themeId);
|
||||
|
||||
// 50/50 chance of vocalization vs action
|
||||
if (Math.random() < 0.5) {
|
||||
return getRandomElement(theme.vocalizations);
|
||||
} else {
|
||||
// Replace PRONOUN placeholder with the guild's pronoun setting
|
||||
const action = getRandomElement(theme.actions);
|
||||
return `*${action.replace('PRONOUN', settings.pronoun)}*`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file prefix for a guild based on its theme
|
||||
*/
|
||||
export function getAudioFilePrefix(guildId: string): string {
|
||||
const settings = guildSettings.getSettings(guildId);
|
||||
const theme = getTheme(settings.themeId);
|
||||
return theme.audioFilePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response chance for a guild
|
||||
*/
|
||||
export function getResponseChance(guildId: string): number {
|
||||
const settings = guildSettings.getSettings(guildId);
|
||||
return settings.responseChance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random status message to display
|
||||
*/
|
||||
export function getRandomStatusMessage(totalGuilds: number): string {
|
||||
// Get a random theme for variety
|
||||
const themeIds = Object.keys(getTheme(''));
|
||||
const randomThemeId = getRandomElement(themeIds);
|
||||
const theme = getTheme(randomThemeId);
|
||||
|
||||
// Get a random status message
|
||||
let statusMessage = getRandomElement(theme.statusMessages.normal);
|
||||
|
||||
// Replace GUILD_COUNT placeholder with actual count
|
||||
statusMessage = statusMessage.replace('GUILD_COUNT', totalGuilds.toString());
|
||||
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the emoji icon for a guild's theme
|
||||
*/
|
||||
export function getThemeEmoji(guildId: string): string {
|
||||
const settings = guildSettings.getSettings(guildId);
|
||||
const theme = getTheme(settings.themeId);
|
||||
return theme.emojiIcon;
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import { config } from '../config/config';
|
||||
import { getRandomElement } from './random';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Default audio filenames if no files are found in the directory
|
||||
export const DEFAULT_AUDIO_FILES = [
|
||||
// Cat sounds
|
||||
'cat_meow1.mp3',
|
||||
'cat_meow2.mp3',
|
||||
'cat_meow3.mp3',
|
||||
'cat_purr.mp3',
|
||||
'cat_hiss.mp3',
|
||||
'cat_yowl.mp3',
|
||||
|
||||
// Dog sounds
|
||||
'dog_bark1.mp3',
|
||||
'dog_bark2.mp3',
|
||||
'dog_whine.mp3',
|
||||
'dog_pant.mp3',
|
||||
'dog_growl.mp3',
|
||||
'dog_howl.mp3',
|
||||
|
||||
// Fox sounds
|
||||
'fox_bark.mp3',
|
||||
'fox_scream.mp3',
|
||||
'fox_yip.mp3',
|
||||
'fox_howl.mp3',
|
||||
'fox_chirp.mp3',
|
||||
|
||||
// Robot sounds
|
||||
'robot_beep.mp3',
|
||||
'robot_whir.mp3',
|
||||
'robot_startup.mp3',
|
||||
'robot_shutdown.mp3',
|
||||
'robot_error.mp3',
|
||||
'robot_process.mp3',
|
||||
];
|
||||
|
||||
// Gather audio files from the directory
|
||||
export const CAT_AUDIO_FILES = gatherAudioFiles(config.audioFilesDir);
|
||||
console.log(`Found ${CAT_AUDIO_FILES.length} audio files in ${config.audioFilesDir}`);
|
||||
|
||||
/**
|
||||
* Gather all audio files from a directory
|
||||
*/
|
||||
function gatherAudioFiles(audioPath: string): string[] {
|
||||
try {
|
||||
// Read all files in the audio directory
|
||||
if (fs.existsSync(audioPath)) {
|
||||
const files = fs.readdirSync(audioPath);
|
||||
const audioFiles = files.filter((file) => {
|
||||
const ext = file.split('.').pop()?.toLowerCase();
|
||||
return ext === 'wav' || ext === 'mp3' || ext === 'ogg';
|
||||
});
|
||||
|
||||
if (audioFiles.length > 0) {
|
||||
return audioFiles;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read audio directory ${audioPath}:`, error);
|
||||
}
|
||||
|
||||
// Return default audio files if none found
|
||||
console.warn('No audio files found, using default filenames. Please add audio files to the audio directory.');
|
||||
return DEFAULT_AUDIO_FILES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sample audio files with README
|
||||
* This is useful for helping users set up the audio files
|
||||
*/
|
||||
export function generateSampleAudioInfo(directory: string): void {
|
||||
try {
|
||||
// Create directory if it doesn't exist
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a README file explaining the audio files
|
||||
const readmePath = path.join(directory, 'README.txt');
|
||||
const readmeContent = `Audio Files for the Discord Bot
|
||||
|
||||
This directory should contain audio files for different themes.
|
||||
Naming convention: theme_sound.mp3 (e.g., cat_meow1.mp3, dog_bark1.mp3)
|
||||
|
||||
Recommended files for each theme:
|
||||
|
||||
CAT THEME:
|
||||
- cat_meow1.mp3
|
||||
- cat_meow2.mp3
|
||||
- cat_meow3.mp3
|
||||
- cat_purr.mp3
|
||||
- cat_hiss.mp3
|
||||
- cat_yowl.mp3
|
||||
|
||||
DOG THEME:
|
||||
- dog_bark1.mp3
|
||||
- dog_bark2.mp3
|
||||
- dog_whine.mp3
|
||||
- dog_pant.mp3
|
||||
- dog_growl.mp3
|
||||
- dog_howl.mp3
|
||||
|
||||
FOX THEME:
|
||||
- fox_bark.mp3
|
||||
- fox_scream.mp3
|
||||
- fox_yip.mp3
|
||||
- fox_howl.mp3
|
||||
- fox_chirp.mp3
|
||||
|
||||
ROBOT THEME:
|
||||
- robot_beep.mp3
|
||||
- robot_whir.mp3
|
||||
- robot_startup.mp3
|
||||
- robot_shutdown.mp3
|
||||
- robot_error.mp3
|
||||
- robot_process.mp3
|
||||
|
||||
You can find free animal sound effects on sites like:
|
||||
- Freesound.org
|
||||
- SoundBible.com
|
||||
- ZapSplat.com
|
||||
`;
|
||||
|
||||
fs.writeFileSync(readmePath, readmeContent);
|
||||
console.log(`Created audio README at ${readmePath}`);
|
||||
} catch (error) {
|
||||
console.error('Error creating sample audio info:', error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { REST, Routes } from 'discord.js';
|
||||
import { commands } from '../commands';
|
||||
import { config } from '../config/config';
|
||||
|
||||
/**
|
||||
* Deploy slash commands to Discord
|
||||
*/
|
||||
export async function deployCommands(): Promise<void> {
|
||||
if (!config.token) {
|
||||
throw new Error('Bot token not found in .env file');
|
||||
}
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(config.token);
|
||||
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands...');
|
||||
|
||||
// Extract command data
|
||||
const commandData = commands.map(command => command.data.toJSON());
|
||||
|
||||
// Register commands globally
|
||||
await rest.put(
|
||||
Routes.applicationCommands(process.env.APPLICATION_ID || ''),
|
||||
{ body: commandData }
|
||||
);
|
||||
|
||||
console.log('Successfully reloaded application (/) commands!');
|
||||
} catch (error) {
|
||||
console.error('Error deploying commands:', error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { config } from '../config/config';
|
||||
import { DEFAULT_THEME_ID, getTheme } from './themes';
|
||||
|
||||
// Define the structure of guild settings
|
||||
interface GuildSettings {
|
||||
id: string;
|
||||
themeId: string;
|
||||
pronoun: string;
|
||||
responseChance: number;
|
||||
}
|
||||
|
||||
// Class to manage guild settings
|
||||
export class GuildSettingsManager {
|
||||
private settings: Map<string, GuildSettings> = new Map();
|
||||
private settingsFilePath: string;
|
||||
|
||||
constructor(storagePath: string = path.join(__dirname, '..', '..', 'data')) {
|
||||
// Ensure storage directory exists
|
||||
if (!fs.existsSync(storagePath)) {
|
||||
fs.mkdirSync(storagePath, { recursive: true });
|
||||
}
|
||||
|
||||
this.settingsFilePath = path.join(storagePath, 'guild-settings.json');
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from file
|
||||
*/
|
||||
private loadSettings(): void {
|
||||
try {
|
||||
if (fs.existsSync(this.settingsFilePath)) {
|
||||
const data = fs.readFileSync(this.settingsFilePath, 'utf8');
|
||||
const settingsArray: GuildSettings[] = JSON.parse(data);
|
||||
|
||||
for (const guild of settingsArray) {
|
||||
this.settings.set(guild.id, guild);
|
||||
}
|
||||
|
||||
console.log(`Loaded settings for ${this.settings.size} guilds`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading guild settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings to file
|
||||
*/
|
||||
private saveSettings(): void {
|
||||
try {
|
||||
const settingsArray = Array.from(this.settings.values());
|
||||
fs.writeFileSync(this.settingsFilePath, JSON.stringify(settingsArray, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error saving guild settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings for a guild, creating default settings if none exist
|
||||
*/
|
||||
getSettings(guildId: string): GuildSettings {
|
||||
if (!this.settings.has(guildId)) {
|
||||
const defaultSettings: GuildSettings = {
|
||||
id: guildId,
|
||||
themeId: config.defaultTheme,
|
||||
pronoun: config.catPersonalization.pronoun,
|
||||
responseChance: config.messageResponseChance,
|
||||
};
|
||||
|
||||
this.settings.set(guildId, defaultSettings);
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
return this.settings.get(guildId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme for a guild
|
||||
*/
|
||||
setTheme(guildId: string, themeId: string): boolean {
|
||||
const settings = this.getSettings(guildId);
|
||||
|
||||
// Validate theme ID
|
||||
const theme = getTheme(themeId);
|
||||
if (!theme) return false;
|
||||
|
||||
settings.themeId = themeId;
|
||||
settings.pronoun = theme.defaultPronoun;
|
||||
this.saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the pronoun for a guild
|
||||
*/
|
||||
setPronoun(guildId: string, pronoun: string): void {
|
||||
const settings = this.getSettings(guildId);
|
||||
settings.pronoun = pronoun;
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response chance for a guild
|
||||
*/
|
||||
setResponseChance(guildId: string, chance: number): void {
|
||||
const settings = this.getSettings(guildId);
|
||||
settings.responseChance = Math.max(0, Math.min(100, chance));
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all guilds and their settings
|
||||
*/
|
||||
getAllSettings(): GuildSettings[] {
|
||||
return Array.from(this.settings.values());
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const guildSettings = new GuildSettingsManager();
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Returns a random integer between min (inclusive) and max (inclusive)
|
||||
*/
|
||||
export function getRandomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true with the given probability (0-100)
|
||||
*/
|
||||
export function randomChance(probability: number): boolean {
|
||||
return Math.random() * 100 < probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specified number of milliseconds
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random element from an array
|
||||
*/
|
||||
export function getRandomElement<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import { ActivityType, Client } from 'discord.js';
|
||||
import { config } from '../config/config';
|
||||
import { getRandomElement } from './random';
|
||||
import { getAvailableThemeIds, getTheme } from './themes';
|
||||
|
||||
export class StatusManager {
|
||||
private client: Client;
|
||||
private statusUpdateInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start rotating status messages
|
||||
*/
|
||||
public startStatusRotation(): void {
|
||||
// Clear any existing interval
|
||||
if (this.statusUpdateInterval) {
|
||||
clearInterval(this.statusUpdateInterval);
|
||||
}
|
||||
|
||||
// Update status immediately
|
||||
this.updateStatus();
|
||||
|
||||
// Set interval for future updates
|
||||
this.statusUpdateInterval = setInterval(() => {
|
||||
this.updateStatus();
|
||||
}, config.statusUpdateInterval * 1000);
|
||||
|
||||
console.log(`Status rotation enabled, updating every ${config.statusUpdateInterval} seconds`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop rotating status messages
|
||||
*/
|
||||
public stopStatusRotation(): void {
|
||||
if (this.statusUpdateInterval) {
|
||||
clearInterval(this.statusUpdateInterval);
|
||||
this.statusUpdateInterval = null;
|
||||
console.log('Status rotation disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the bot is currently in any voice channel
|
||||
*/
|
||||
private isInVoiceChannel(): boolean {
|
||||
if (!this.client.guilds) return false;
|
||||
|
||||
// Check if the bot is in a voice channel in any guild
|
||||
for (const guild of this.client.guilds.cache.values()) {
|
||||
const member = guild.members.cache.get(this.client.user?.id || '');
|
||||
if (member?.voice.channel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the bot's status message
|
||||
*/
|
||||
private updateStatus(): void {
|
||||
if (!this.client.user) return;
|
||||
|
||||
// Get guild count
|
||||
const guildCount = this.client.guilds.cache.size;
|
||||
|
||||
// Check if the bot is in a voice channel
|
||||
const inVoiceChannel = this.isInVoiceChannel();
|
||||
|
||||
// Randomly select a theme
|
||||
const themeIds = getAvailableThemeIds();
|
||||
const randomThemeId = getRandomElement(themeIds);
|
||||
const theme = getTheme(randomThemeId);
|
||||
|
||||
// Get a random status message based on voice channel status
|
||||
const statusList = inVoiceChannel ? theme.statusMessages.inVoice : theme.statusMessages.normal;
|
||||
let statusMessage = getRandomElement(statusList);
|
||||
|
||||
// Replace guild count placeholder
|
||||
statusMessage = statusMessage.replace('GUILD_COUNT', guildCount.toString());
|
||||
|
||||
// Add the theme emoji to the status
|
||||
statusMessage = `${theme.emojiIcon} ${statusMessage}`;
|
||||
|
||||
// Choose a random activity type
|
||||
const activityTypes = [
|
||||
ActivityType.Playing,
|
||||
ActivityType.Watching,
|
||||
ActivityType.Listening,
|
||||
ActivityType.Competing
|
||||
];
|
||||
const activityType = getRandomElement(activityTypes);
|
||||
|
||||
// Set the new activity
|
||||
this.client.user.setActivity(statusMessage, { type: activityType });
|
||||
console.log(`Status updated (${inVoiceChannel ? 'in voice' : 'normal'}): ${ActivityType[activityType]} ${statusMessage}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific status
|
||||
*/
|
||||
public setStatus(message: string, type: ActivityType = ActivityType.Playing): void {
|
||||
if (!this.client.user) return;
|
||||
|
||||
this.client.user.setActivity(message, { type });
|
||||
console.log(`Status set: ${ActivityType[type]} ${message}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
/**
|
||||
* Themes for the bot to use on different servers
|
||||
*/
|
||||
|
||||
export interface ThemeConfig {
|
||||
name: string;
|
||||
nicknames: string[];
|
||||
vocalizations: string[];
|
||||
actions: string[];
|
||||
audioFilePrefix: string;
|
||||
statusMessages: {
|
||||
normal: string[];
|
||||
inVoice: string[];
|
||||
};
|
||||
emojiIcon: string;
|
||||
defaultPronoun: string;
|
||||
}
|
||||
|
||||
// Map of all available themes
|
||||
export const availableThemes: Record<string, ThemeConfig> = {
|
||||
cat: {
|
||||
name: 'Cat',
|
||||
nicknames: [
|
||||
'Shadow',
|
||||
'Luna',
|
||||
'Oliver',
|
||||
'Simba',
|
||||
],
|
||||
vocalizations: [
|
||||
'Meow!',
|
||||
'Mrow?',
|
||||
'Purrrrrrrr...',
|
||||
'Mew!',
|
||||
'Mrrrp!',
|
||||
'Hisssss!',
|
||||
'Yowl!',
|
||||
'*scratches furniture*',
|
||||
'Mrrrowww!',
|
||||
'Prrrp?',
|
||||
],
|
||||
actions: [
|
||||
'bumps PRONOUN head against',
|
||||
'rubs against',
|
||||
'walks across',
|
||||
'sits on',
|
||||
'ignores',
|
||||
'stares at',
|
||||
'swats at',
|
||||
'rolls over in front of',
|
||||
'purrs at',
|
||||
'blinks slowly at',
|
||||
'climbs on',
|
||||
'makes biscuits on',
|
||||
'knocks over',
|
||||
'brings a gift to',
|
||||
],
|
||||
audioFilePrefix: 'cat_',
|
||||
statusMessages: {
|
||||
normal: [
|
||||
'hunting mice',
|
||||
'napping in a sunbeam',
|
||||
'plotting world domination',
|
||||
'judging everyone silently',
|
||||
'knocking things off shelves',
|
||||
'demanding treats',
|
||||
'chasing laser pointers',
|
||||
'prowling around GUILD_COUNT servers',
|
||||
'ignoring your calls',
|
||||
'climbing curtains',
|
||||
],
|
||||
inVoice: [
|
||||
'purring loudly',
|
||||
'meowing for attention',
|
||||
'playing with yarn',
|
||||
'scratching at the mic',
|
||||
'hissing at strangers',
|
||||
'listening attentively',
|
||||
'batting at microphones',
|
||||
'curled up sleeping nearby',
|
||||
'stalking prey in voice chat',
|
||||
'knocking things over in voice chat',
|
||||
]
|
||||
},
|
||||
emojiIcon: '🐱',
|
||||
defaultPronoun: 'their',
|
||||
},
|
||||
/**
|
||||
|
||||
dog: {
|
||||
name: 'Dog',
|
||||
nicknames: [
|
||||
'Buddy',
|
||||
'Max',
|
||||
'Bailey',
|
||||
'Cooper',
|
||||
'Daisy',
|
||||
'Sir Barksalot',
|
||||
'Fido',
|
||||
'Rover',
|
||||
'Scout',
|
||||
'Captain Goodboy'
|
||||
],
|
||||
vocalizations: [
|
||||
'Woof!',
|
||||
'Bark!',
|
||||
'Arf arf!',
|
||||
'*pants excitedly*',
|
||||
'Awoooo!',
|
||||
'*sniffs*',
|
||||
'Grrrr...',
|
||||
'Ruff!',
|
||||
'*tail thumping*',
|
||||
'Yip!',
|
||||
],
|
||||
actions: [
|
||||
'licks',
|
||||
'brings a ball to',
|
||||
'zooms past',
|
||||
'wags PRONOUN tail at',
|
||||
'sniffs',
|
||||
'rolls over for',
|
||||
'jumps up on',
|
||||
'follows',
|
||||
'tilts PRONOUN head at',
|
||||
'plays bow to',
|
||||
'protects',
|
||||
'hides PRONOUN toy from',
|
||||
'begs for treats from',
|
||||
'howls with',
|
||||
],
|
||||
audioFilePrefix: 'dog_',
|
||||
statusMessages: {
|
||||
normal: [
|
||||
'chasing squirrels',
|
||||
'fetching balls',
|
||||
'digging holes',
|
||||
'wagging on GUILD_COUNT servers',
|
||||
'begging for treats',
|
||||
'being a good boy/girl',
|
||||
'barking at mailmen',
|
||||
'guarding the house',
|
||||
'stealing socks',
|
||||
'zooming around',
|
||||
],
|
||||
inVoice: [
|
||||
'barking excitedly',
|
||||
'howling along',
|
||||
'panting into the mic',
|
||||
'chewing on headphones',
|
||||
'whining for attention',
|
||||
'tilting head at sounds',
|
||||
'listening for treats',
|
||||
'sniffing the voice channel',
|
||||
'playing fetch in voice chat',
|
||||
'drooling on the microphone',
|
||||
]
|
||||
},
|
||||
emojiIcon: '🐶',
|
||||
defaultPronoun: 'their',
|
||||
},
|
||||
|
||||
fox: {
|
||||
name: 'Fox',
|
||||
nicknames: [
|
||||
'Sly',
|
||||
'Rusty',
|
||||
'Firefox',
|
||||
'Swift',
|
||||
'Amber',
|
||||
'Vixen',
|
||||
'Todd',
|
||||
'Reynard',
|
||||
'Firefox',
|
||||
'Professor Pounce'
|
||||
],
|
||||
vocalizations: [
|
||||
'Yap!',
|
||||
'Ring-ding-ding-ding-dingeringeding!',
|
||||
'Wa-pa-pa-pa-pa-pa-pow!',
|
||||
'Hatee-hatee-hatee-ho!',
|
||||
'Joff-tchoff-tchoffo-tchoffo-tchoff!',
|
||||
'*chirps*',
|
||||
'*screams*',
|
||||
'Gerringding!',
|
||||
'Fraka-kaka-kaka-kaka-kow!',
|
||||
'A-hee-ahee ha-hee!',
|
||||
],
|
||||
actions: [
|
||||
'pounces at',
|
||||
'digs near',
|
||||
'perks PRONOUN ears at',
|
||||
'sneaks around',
|
||||
'hides from',
|
||||
'plays with',
|
||||
'steals from',
|
||||
'yips at',
|
||||
'observes curiously',
|
||||
'trots past',
|
||||
'hunts near',
|
||||
'flicks PRONOUN tail at',
|
||||
'leaps over',
|
||||
'scurries around',
|
||||
],
|
||||
audioFilePrefix: 'fox_',
|
||||
statusMessages: {
|
||||
normal: [
|
||||
'being elusive',
|
||||
'saying what the fox says',
|
||||
'outfoxing everyone',
|
||||
'being fantastic',
|
||||
'raiding chicken coops',
|
||||
'skulking in GUILD_COUNT servers',
|
||||
'causing mischief',
|
||||
'jumping in snow',
|
||||
'hiding treasures',
|
||||
'being sly',
|
||||
],
|
||||
inVoice: [
|
||||
'yipping at voices',
|
||||
'making weird fox noises',
|
||||
'rustling around in bushes',
|
||||
'screaming suddenly',
|
||||
'watching chat quietly',
|
||||
'chittering to self',
|
||||
'pouncing on voice activity',
|
||||
'digging into the voice channel',
|
||||
'stealing voice channel items',
|
||||
'trotting through voice chat',
|
||||
]
|
||||
},
|
||||
emojiIcon: '🦊',
|
||||
defaultPronoun: 'their',
|
||||
},
|
||||
|
||||
robot: {
|
||||
name: 'Robot',
|
||||
nicknames: [
|
||||
'B33P-B00P',
|
||||
'CyberTron',
|
||||
'Metal Friend',
|
||||
'Unit-7',
|
||||
'RoboCompanion',
|
||||
'Circuit',
|
||||
'T1000',
|
||||
'BinaryBuddy',
|
||||
'Mechanoid',
|
||||
'SynthFriend'
|
||||
],
|
||||
vocalizations: [
|
||||
'Beep!',
|
||||
'Boop!',
|
||||
'*whirring noises*',
|
||||
'01001000 01101001!',
|
||||
'*scanning*',
|
||||
'COMPUTING...',
|
||||
'*mechanical clicking*',
|
||||
'SYSTEM ONLINE',
|
||||
'EXECUTING PROTOCOL',
|
||||
'*powers down*',
|
||||
],
|
||||
actions: [
|
||||
'analyzes',
|
||||
'scans',
|
||||
'observes',
|
||||
'processes data from',
|
||||
'follows',
|
||||
'records',
|
||||
'calculates trajectory of',
|
||||
'extends PRONOUN robotic arm to',
|
||||
'powers up near',
|
||||
'boots up beside',
|
||||
'downloads data from',
|
||||
'upgrades',
|
||||
'orbits around',
|
||||
'calibrates sensors for',
|
||||
],
|
||||
audioFilePrefix: 'robot_',
|
||||
statusMessages: {
|
||||
normal: [
|
||||
'computing pi',
|
||||
'executing protocols',
|
||||
'installing updates',
|
||||
'monitoring GUILD_COUNT servers',
|
||||
'analyzing human behavior',
|
||||
'charging batteries',
|
||||
'backing up data',
|
||||
'scanning for threats',
|
||||
'processing inputs',
|
||||
'learning humanity',
|
||||
],
|
||||
inVoice: [
|
||||
'recording conversations',
|
||||
'analyzing audio patterns',
|
||||
'detecting voice signatures',
|
||||
'calibrating audio sensors',
|
||||
'running voice recognition',
|
||||
'optimizing audio codec',
|
||||
'calculating audio spectrum',
|
||||
'diagnosing audio hardware',
|
||||
'decoding speech patterns',
|
||||
'extending audio receptors',
|
||||
]
|
||||
},
|
||||
emojiIcon: '🤖',
|
||||
defaultPronoun: 'its',
|
||||
},
|
||||
|
||||
*/
|
||||
};
|
||||
|
||||
// Default theme
|
||||
export const DEFAULT_THEME_ID = 'cat';
|
||||
|
||||
/**
|
||||
* Returns a theme by ID or the default theme if not found
|
||||
*/
|
||||
export function getTheme(themeId: string): ThemeConfig {
|
||||
return availableThemes[themeId] || availableThemes[DEFAULT_THEME_ID];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all available theme IDs
|
||||
*/
|
||||
export function getAvailableThemeIds(): string[] {
|
||||
return Object.keys(availableThemes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random nickname for a theme
|
||||
*/
|
||||
export function getRandomNickname(themeId: string): string {
|
||||
const theme = getTheme(themeId);
|
||||
return theme.nicknames[Math.floor(Math.random() * theme.nicknames.length)];
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Guild } from 'discord.js';
|
||||
import { DiscordGatewayAdapterCreator } from '@discordjs/voice';
|
||||
|
||||
/**
|
||||
* Creates an adapter for Discord voice connections.
|
||||
* This handles the compatibility between Discord.js and @discordjs/voice
|
||||
*/
|
||||
export function createDiscordJsAdapter(guild: Guild): DiscordGatewayAdapterCreator {
|
||||
// @ts-ignore: The types between discord.js and @discordjs/voice are not fully compatible
|
||||
return guild.voiceAdapterCreator;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
Loading…
Reference in New Issue