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