mirror of
https://github.com/arthur-pbty/xiao.git
synced 2026-06-03 15:07:42 +02:00
Add Docker support to simplify deployment
This commit is contained in:
@@ -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
@@ -1,45 +1,51 @@
|
|||||||
# Discord-related info
|
# Discord-related info
|
||||||
XIAO_TOKEN=
|
XIAO_TOKEN=your_discord_bot_token
|
||||||
# Separate OWNERS with a ,
|
# Separate OWNERS with a ,
|
||||||
OWNERS=
|
OWNERS=123456789012345678
|
||||||
LOVER_USER_ID=
|
LOVER_USER_ID=
|
||||||
XIAO_PREFIX=
|
XIAO_PREFIX=!
|
||||||
INVITE=
|
INVITE=https://discord.gg/your-server
|
||||||
REPORT_CHANNEL_ID=
|
REPORT_CHANNEL_ID=
|
||||||
JOIN_LEAVE_CHANNEL_ID=
|
JOIN_LEAVE_CHANNEL_ID=
|
||||||
|
|
||||||
# Redis info
|
# Redis info
|
||||||
REDIS_HOST=
|
# For docker-compose, keep REDIS_HOST=redis
|
||||||
REDIS_PASS=
|
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
|
# Emoji IDs
|
||||||
SUCCESS_EMOJI_ID=
|
SUCCESS_EMOJI_ID=
|
||||||
FAILURE_EMOJI_ID=
|
FAILURE_EMOJI_ID=
|
||||||
GOLD_FISH_EMOJI_ID=
|
GOLD_FISH_EMOJI_ID=
|
||||||
GOLD_FISH_EMOJI_NAME=
|
GOLD_FISH_EMOJI_NAME=Gold Fish
|
||||||
MOCKING_EMOJI_ID=
|
MOCKING_EMOJI_ID=
|
||||||
MOCKING_EMOJI_NAME=
|
MOCKING_EMOJI_NAME=
|
||||||
SILVER_FISH_EMOJI_ID=
|
SILVER_FISH_EMOJI_ID=
|
||||||
SILVER_FISH_EMOJI_NAME=
|
SILVER_FISH_EMOJI_NAME=Silver Fish
|
||||||
PORTAL_EMOJI_ID=
|
PORTAL_EMOJI_ID=
|
||||||
PORTAL_EMOJI_NAME=
|
PORTAL_EMOJI_NAME=PORTAL
|
||||||
LOADING_EMOJI_ID=
|
LOADING_EMOJI_ID=
|
||||||
MEGA_EVOLVE_EMOJI_ID=
|
MEGA_EVOLVE_EMOJI_ID=
|
||||||
MEGA_EVOLVE_EMOJI_NAME=
|
MEGA_EVOLVE_EMOJI_NAME=MEGA
|
||||||
NAME_RATER_EMOJI_ID=
|
NAME_RATER_EMOJI_ID=
|
||||||
NAME_RATER_EMOJI_NAME=
|
NAME_RATER_EMOJI_NAME=
|
||||||
|
|
||||||
# API Keys, IDs, and Secrets
|
# API Keys, IDs, and Secrets
|
||||||
ANILIST_USERNAME=
|
ANILIST_USERNAME=AniList
|
||||||
BITLY_KEY=
|
BITLY_KEY=your_bitly_key
|
||||||
CLEVERBOT_KEY=
|
CLEVERBOT_KEY=your_cleverbot_key
|
||||||
GITHUB_ACCESS_TOKEN=
|
GITHUB_ACCESS_TOKEN=your_github_token
|
||||||
GOV_KEY=
|
GOV_KEY=your_nasa_api_key
|
||||||
IDIOT_URL=
|
IDIOT_URL=https://en.wikipedia.org/wiki/Idiot
|
||||||
REMOVEBG_KEY=
|
REMOVEBG_KEY=your_removebg_key
|
||||||
SAUCENAO_KEY=
|
SAUCENAO_KEY=your_saucenao_key
|
||||||
THECATAPI_KEY=
|
THECATAPI_KEY=your_thecatapi_key
|
||||||
THEDOGAPI_KEY=
|
THEDOGAPI_KEY=your_thedogapi_key
|
||||||
WEBSTER_KEY=
|
WEBSTER_KEY=your_webster_key
|
||||||
XIAO_GITHUB_REPO_NAME=
|
XIAO_GITHUB_REPO_NAME=xiao
|
||||||
XIAO_GITHUB_REPO_USERNAME=
|
XIAO_GITHUB_REPO_USERNAME=xiaobotdev
|
||||||
|
|||||||
+32
@@ -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"]
|
||||||
@@ -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
@@ -7,6 +7,21 @@ const Registry = require('./Registry');
|
|||||||
const Dispatcher = require('./Dispatcher');
|
const Dispatcher = require('./Dispatcher');
|
||||||
require('./Extensions');
|
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 {
|
module.exports = class CommandClient extends Client {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -157,7 +172,11 @@ module.exports = class CommandClient extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importBlacklist() {
|
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);
|
const file = JSON.parse(read);
|
||||||
if (typeof file !== 'object' || Array.isArray(file)) return null;
|
if (typeof file !== 'object' || Array.isArray(file)) return null;
|
||||||
if (!file.guild || !file.user) return null;
|
if (!file.guild || !file.user) return null;
|
||||||
@@ -191,12 +210,17 @@ module.exports = class CommandClient extends Client {
|
|||||||
}
|
}
|
||||||
text += '\n ]\n}\n';
|
text += '\n ]\n}\n';
|
||||||
const buf = Buffer.from(text);
|
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;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
importCommandLeaderboard(add = false) {
|
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'
|
encoding: 'utf8'
|
||||||
});
|
});
|
||||||
const file = JSON.parse(read);
|
const file = JSON.parse(read);
|
||||||
@@ -221,14 +245,19 @@ module.exports = class CommandClient extends Client {
|
|||||||
text = text.slice(0, -1);
|
text = text.slice(0, -1);
|
||||||
text += '\n}\n';
|
text += '\n}\n';
|
||||||
const buf = Buffer.from(text);
|
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'
|
encoding: 'utf8'
|
||||||
});
|
});
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
importLastRun() {
|
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'
|
encoding: 'utf8'
|
||||||
});
|
});
|
||||||
const file = JSON.parse(read);
|
const file = JSON.parse(read);
|
||||||
@@ -254,7 +283,8 @@ module.exports = class CommandClient extends Client {
|
|||||||
text = text.slice(0, -1);
|
text = text.slice(0, -1);
|
||||||
text += '\n}\n';
|
text += '\n}\n';
|
||||||
const buf = Buffer.from(text);
|
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'
|
encoding: 'utf8'
|
||||||
});
|
});
|
||||||
return buf;
|
return buf;
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ const path = require('path');
|
|||||||
const { checkFileExists } = require('../util/Util');
|
const { checkFileExists } = require('../util/Util');
|
||||||
const rounds = ['jeopardy_round', 'double_jeopardy_round', 'final_jeopardy_round'];
|
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 {
|
module.exports = class JeopardyScrape {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
Object.defineProperty(this, 'client', { value: client });
|
Object.defineProperty(this, 'client', { value: client });
|
||||||
@@ -70,17 +78,17 @@ module.exports = class JeopardyScrape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async importData() {
|
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);
|
const { seasons, gameIDs } = JSON.parse(read);
|
||||||
this.gameIDs = gameIDs;
|
this.gameIDs = Array.isArray(gameIDs) ? gameIDs : [];
|
||||||
this.seasons = seasons;
|
this.seasons = Array.isArray(seasons) ? seasons : [];
|
||||||
this.clues = await this.importClues();
|
this.clues = await this.importClues();
|
||||||
this.imported = true;
|
this.imported = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
importClues() {
|
importClues() {
|
||||||
const pipeline = fs.createReadStream(path.join(__dirname, '..', 'jeopardy.json'), { encoding: 'utf8' })
|
const pipeline = fs.createReadStream(resolveStatePath('jeopardy.json'), { encoding: 'utf8' })
|
||||||
.pipe(parser())
|
.pipe(parser())
|
||||||
.pipe(pick({ filter: 'clues' }))
|
.pipe(pick({ filter: 'clues' }))
|
||||||
.pipe(streamArray());
|
.pipe(streamArray());
|
||||||
@@ -96,19 +104,35 @@ module.exports = class JeopardyScrape {
|
|||||||
gameIDs: this.gameIDs,
|
gameIDs: this.gameIDs,
|
||||||
seasons: this.seasons
|
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;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkForUpdates() {
|
async checkForUpdates() {
|
||||||
if (!this.imported) {
|
if (!this.imported) {
|
||||||
const fileExists = await checkFileExists(path.join(__dirname, '..', 'jeopardy.json'));
|
const fileExists = await checkFileExists(resolveStatePath('jeopardy.json'));
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
this.client.logger.info('[JEOPARDY] Importing from file...');
|
this.client.logger.info('[JEOPARDY] Importing from file...');
|
||||||
await this.importData();
|
await this.importData();
|
||||||
this.client.logger.info('[JEOPARDY] Import complete!');
|
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 cluesBefore = this.clues.length;
|
||||||
const latestSeason = this.seasons[this.seasons.length - 1];
|
const latestSeason = this.seasons[this.seasons.length - 1];
|
||||||
const seasons = await this.fetchSeasons();
|
const seasons = await this.fetchSeasons();
|
||||||
|
|||||||
Reference in New Issue
Block a user