Add Docker support to simplify deployment

This commit is contained in:
Puechberty Arthur
2026-04-11 00:10:18 +02:00
parent 5747a60745
commit aa8f8048fe
6 changed files with 184 additions and 35 deletions
+19
View File
@@ -0,0 +1,19 @@
.git
.github
.vscode
node_modules
tmp
logs
.env
.env.keys
command-leaderboard.json
command-last-run.json
blacklist.json
jeopardy.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+29 -23
View File
@@ -1,45 +1,51 @@
# Discord-related info
XIAO_TOKEN=
XIAO_TOKEN=your_discord_bot_token
# Separate OWNERS with a ,
OWNERS=
OWNERS=123456789012345678
LOVER_USER_ID=
XIAO_PREFIX=
INVITE=
XIAO_PREFIX=!
INVITE=https://discord.gg/your-server
REPORT_CHANNEL_ID=
JOIN_LEAVE_CHANNEL_ID=
# Redis info
REDIS_HOST=
REDIS_PASS=
# For docker-compose, keep REDIS_HOST=redis
REDIS_HOST=redis
REDIS_PASS=change_me_redis_password
# Runtime
TZ=UTC
# Folder used to persist bot state files in Docker
XIAO_STATE_DIR=/data
# Emoji IDs
SUCCESS_EMOJI_ID=
FAILURE_EMOJI_ID=
GOLD_FISH_EMOJI_ID=
GOLD_FISH_EMOJI_NAME=
GOLD_FISH_EMOJI_NAME=Gold Fish
MOCKING_EMOJI_ID=
MOCKING_EMOJI_NAME=
SILVER_FISH_EMOJI_ID=
SILVER_FISH_EMOJI_NAME=
SILVER_FISH_EMOJI_NAME=Silver Fish
PORTAL_EMOJI_ID=
PORTAL_EMOJI_NAME=
PORTAL_EMOJI_NAME=PORTAL
LOADING_EMOJI_ID=
MEGA_EVOLVE_EMOJI_ID=
MEGA_EVOLVE_EMOJI_NAME=
MEGA_EVOLVE_EMOJI_NAME=MEGA
NAME_RATER_EMOJI_ID=
NAME_RATER_EMOJI_NAME=
# API Keys, IDs, and Secrets
ANILIST_USERNAME=
BITLY_KEY=
CLEVERBOT_KEY=
GITHUB_ACCESS_TOKEN=
GOV_KEY=
IDIOT_URL=
REMOVEBG_KEY=
SAUCENAO_KEY=
THECATAPI_KEY=
THEDOGAPI_KEY=
WEBSTER_KEY=
XIAO_GITHUB_REPO_NAME=
XIAO_GITHUB_REPO_USERNAME=
ANILIST_USERNAME=AniList
BITLY_KEY=your_bitly_key
CLEVERBOT_KEY=your_cleverbot_key
GITHUB_ACCESS_TOKEN=your_github_token
GOV_KEY=your_nasa_api_key
IDIOT_URL=https://en.wikipedia.org/wiki/Idiot
REMOVEBG_KEY=your_removebg_key
SAUCENAO_KEY=your_saucenao_key
THECATAPI_KEY=your_thecatapi_key
THEDOGAPI_KEY=your_thedogapi_key
WEBSTER_KEY=your_webster_key
XIAO_GITHUB_REPO_NAME=xiao
XIAO_GITHUB_REPO_USERNAME=xiaobotdev
+32
View File
@@ -0,0 +1,32 @@
FROM node:24-bookworm
WORKDIR /app
# System dependencies used by native modules and image/audio commands.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
ffmpeg \
git \
graphicsmagick \
imagemagick \
libgif-dev \
libjpeg62-turbo-dev \
liblqr-1-0 \
libpango1.0-dev \
librsvg2-dev \
make \
g++ \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm install --omit=dev && npm cache clean --force
COPY . .
# Refresh parse-domain data when available.
RUN npx --yes parse-domain-update || true
ENV NODE_ENV=production
CMD ["node", "Xiao.js"]
+38
View File
@@ -0,0 +1,38 @@
services:
redis:
image: redis:7-alpine
container_name: xiao-redis
restart: unless-stopped
command:
- redis-server
- --appendonly
- "yes"
- --requirepass
- ${REDIS_PASS:-change_me_redis_password}
volumes:
- redis-data:/data
xiao:
build:
context: .
dockerfile: Dockerfile
container_name: xiao-bot
restart: unless-stopped
depends_on:
- redis
env_file:
- .env
environment:
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PASS: ${REDIS_PASS:-change_me_redis_password}
TZ: ${TZ:-UTC}
XIAO_STATE_DIR: ${XIAO_STATE_DIR:-/data}
volumes:
- xiao-tmp:/app/tmp
- xiao-state:/data
- ./.env:/app/.env:ro
volumes:
redis-data:
xiao-tmp:
xiao-state:
+36 -6
View File
@@ -7,6 +7,21 @@ const Registry = require('./Registry');
const Dispatcher = require('./Dispatcher');
require('./Extensions');
const STATE_DIR = process.env.XIAO_STATE_DIR
? path.resolve(process.env.XIAO_STATE_DIR)
: path.join(__dirname, '..');
function resolveStatePath(fileName) {
return path.join(STATE_DIR, fileName);
}
function writeJsonFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const buf = Buffer.from(`${JSON.stringify(data, null, '\t')}\n`);
fs.writeFileSync(filePath, buf, { encoding: 'utf8' });
return data;
}
module.exports = class CommandClient extends Client {
constructor(options) {
super(options);
@@ -157,7 +172,11 @@ module.exports = class CommandClient extends Client {
}
importBlacklist() {
const read = fs.readFileSync(path.join(__dirname, '..', 'blacklist.json'), { encoding: 'utf8' });
const filePath = resolveStatePath('blacklist.json');
if (!fs.existsSync(filePath)) {
return writeJsonFile(filePath, { guild: [], user: [] });
}
const read = fs.readFileSync(filePath, { encoding: 'utf8' });
const file = JSON.parse(read);
if (typeof file !== 'object' || Array.isArray(file)) return null;
if (!file.guild || !file.user) return null;
@@ -191,12 +210,17 @@ module.exports = class CommandClient extends Client {
}
text += '\n ]\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'blacklist.json'), buf, { encoding: 'utf8' });
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(resolveStatePath('blacklist.json'), buf, { encoding: 'utf8' });
return buf;
}
importCommandLeaderboard(add = false) {
const read = fs.readFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), {
const filePath = resolveStatePath('command-leaderboard.json');
if (!fs.existsSync(filePath)) {
return writeJsonFile(filePath, {});
}
const read = fs.readFileSync(filePath, {
encoding: 'utf8'
});
const file = JSON.parse(read);
@@ -221,14 +245,19 @@ module.exports = class CommandClient extends Client {
text = text.slice(0, -1);
text += '\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), buf, {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(resolveStatePath('command-leaderboard.json'), buf, {
encoding: 'utf8'
});
return buf;
}
importLastRun() {
const read = fs.readFileSync(path.join(__dirname, '..', 'command-last-run.json'), {
const filePath = resolveStatePath('command-last-run.json');
if (!fs.existsSync(filePath)) {
return writeJsonFile(filePath, {});
}
const read = fs.readFileSync(filePath, {
encoding: 'utf8'
});
const file = JSON.parse(read);
@@ -254,7 +283,8 @@ module.exports = class CommandClient extends Client {
text = text.slice(0, -1);
text += '\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'command-last-run.json'), buf, {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(resolveStatePath('command-last-run.json'), buf, {
encoding: 'utf8'
});
return buf;
+30 -6
View File
@@ -8,6 +8,14 @@ const path = require('path');
const { checkFileExists } = require('../util/Util');
const rounds = ['jeopardy_round', 'double_jeopardy_round', 'final_jeopardy_round'];
const STATE_DIR = process.env.XIAO_STATE_DIR
? path.resolve(process.env.XIAO_STATE_DIR)
: path.join(__dirname, '..');
function resolveStatePath(fileName) {
return path.join(STATE_DIR, fileName);
}
module.exports = class JeopardyScrape {
constructor(client) {
Object.defineProperty(this, 'client', { value: client });
@@ -70,17 +78,17 @@ module.exports = class JeopardyScrape {
}
async importData() {
const read = await fs.promises.readFile(path.join(__dirname, '..', 'jeopardy.json'), { encoding: 'utf8' });
const read = await fs.promises.readFile(resolveStatePath('jeopardy.json'), { encoding: 'utf8' });
const { seasons, gameIDs } = JSON.parse(read);
this.gameIDs = gameIDs;
this.seasons = seasons;
this.gameIDs = Array.isArray(gameIDs) ? gameIDs : [];
this.seasons = Array.isArray(seasons) ? seasons : [];
this.clues = await this.importClues();
this.imported = true;
return this;
}
importClues() {
const pipeline = fs.createReadStream(path.join(__dirname, '..', 'jeopardy.json'), { encoding: 'utf8' })
const pipeline = fs.createReadStream(resolveStatePath('jeopardy.json'), { encoding: 'utf8' })
.pipe(parser())
.pipe(pick({ filter: 'clues' }))
.pipe(streamArray());
@@ -96,19 +104,35 @@ module.exports = class JeopardyScrape {
gameIDs: this.gameIDs,
seasons: this.seasons
}));
fs.writeFileSync(path.join(__dirname, '..', 'jeopardy.json'), buf, { encoding: 'utf8' });
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(resolveStatePath('jeopardy.json'), buf, { encoding: 'utf8' });
return buf;
}
async checkForUpdates() {
if (!this.imported) {
const fileExists = await checkFileExists(path.join(__dirname, '..', 'jeopardy.json'));
const fileExists = await checkFileExists(resolveStatePath('jeopardy.json'));
if (fileExists) {
this.client.logger.info('[JEOPARDY] Importing from file...');
await this.importData();
this.client.logger.info('[JEOPARDY] Import complete!');
} else {
this.client.logger.warn('[JEOPARDY] No jeopardy.json found. Skipping update for now.');
this.clues = [];
this.gameIDs = [];
this.seasons = [];
this.imported = true;
this.exportData();
return 0;
}
}
if (!Array.isArray(this.clues)) this.clues = [];
if (!Array.isArray(this.gameIDs)) this.gameIDs = [];
if (!Array.isArray(this.seasons)) this.seasons = [];
if (!this.seasons.length) {
this.client.logger.warn('[JEOPARDY] No seasons loaded. Skipping update.');
return 0;
}
const cluesBefore = this.clues.length;
const latestSeason = this.seasons[this.seasons.length - 1];
const seasons = await this.fetchSeasons();