diff --git a/.dockerignore b/.dockerignore index ff08669..3428c09 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,5 +7,6 @@ docker-compose.yml .gitignore .idea -data/main.sqlite -data/backups \ No newline at end of file +data/backups +logs +docs diff --git a/README-dev.md b/README-dev.md index b3b7d34..6467df1 100644 --- a/README-dev.md +++ b/README-dev.md @@ -1,6 +1,7 @@ * [Running Locally](#running-locally) * [Option 1: Install and run with Docker (recommended)](#option-1-install-and-run-with-docker-recommended) + * [Other useful docker commands](#other-useful-docker-commands) * [Option 2: Manually install and run](#option-2-manually-install-and-run) * [Data storage and access](#data-storage-and-access) * [How to create and edit commands](#how-to-create-and-edit-commands) @@ -21,10 +22,10 @@ Clone the repository: git clone https://github.com/ArrowM/Queue-Bot ``` -Create or reuse a Discord bot application and invite it to your server. +Create a Discord bot application and invite it to your server. See [Discord.js guide](https://discordjs.guide/preparations/setting-up-a-bot-application.html). -Update the `.env` file with your bot's TOKEN and CLIENT_ID. +Update the `.env` file with your bot's `TOKEN` and `CLIENT_ID`. ### Option 1: Install and run with Docker (recommended) @@ -33,23 +34,27 @@ Update the `.env` file with your bot's TOKEN and CLIENT_ID. You may need to grant yourself docker perms (replacing `` with your actual username, `pi` in my case: ```bash +chmod +x launch-docker.sh sudo usermod -aG docker sudo reboot ``` -Setup (**run each time you update the project**): +Run `./launch-docker.sh` or `launch-docker.bat` to: +- dump logs to `./logs` and close the previous container (if applicable) +- build the image & container, then start the bot in a detached state +- attach to the container (which can safely be exited with the `Ctrl+p Ctrl+q` key sequence. Using `CTRL-c` while attached will stop the container) -```bash -docker compose build -```` +*Note you may need to respond to a migration prompt if you have updated your project*. + +#### Other useful docker commands -Start the bot in a detached container: +Attach to the bot container (`Ctrl+p Ctrl+q` to detach): ```bash -docker compose up -d +docker attach queue-bot ``` -View the logs: +View live logs: ```bash docker logs -f queue-bot @@ -63,6 +68,8 @@ docker compose down ### Option 2: Manually install and run +This method is not recommended, because it requires installing node and lacks the logging, auto-restart, and rebuild speed of Docker. + [Install Node.js](https://nodejs.org/en/download/package-manager). Run the setup script (**run each time you update the project**): diff --git a/docker-compose.yml b/docker-compose.yml index a669f7b..d760395 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,9 @@ services: - ./.env:/app/.env # Bind mount for .env file container_name: queue-bot restart: always - command: ["npm", "run", "docker:start"] logging: options: max-size: "10m" max-file: "5" + stdin_open: true # Allow stdin to be open + tty: true # Allocate a pseudo-TTY diff --git a/launch-docker.bat b/launch-docker.bat new file mode 100644 index 0000000..b2073cf --- /dev/null +++ b/launch-docker.bat @@ -0,0 +1,36 @@ +@echo off +rem Set the container name +set CONTAINER_NAME=queue-bot + +rem Check if the container is running +docker ps -q -f name=%CONTAINER_NAME% > nul 2>&1 +if %errorlevel% equ 0 ( + rem Create a dated log file name + set LOG_FILE=logs\%CONTAINER_NAME%_%date:~10,4%-%date:~4,2%-%date:~7,2%_%time:~0,2%-%time:~3,2%-%time:~6,2%.log + + rem Save the logs to the file + mkdir logs 2>nul + docker logs %CONTAINER_NAME% > %LOG_FILE% + + rem Check if the logs were saved successfully + if %errorlevel% equ 0 ( + echo Logs saved to %LOG_FILE% + ) else ( + echo Failed to save logs + exit /b 1 + ) +) else ( + echo Container %CONTAINER_NAME% is not running. Skipping log saving. +) + +git pull + +docker-compose down + +npx drizzle-kit push + +docker-compose up -d --build + +docker image prune -f + +docker logs -f queue-bot diff --git a/launch-docker.sh b/launch-docker.sh new file mode 100644 index 0000000..759ad41 --- /dev/null +++ b/launch-docker.sh @@ -0,0 +1,32 @@ +# Set the container name +CONTAINER_NAME="queue-bot" + +# Check if the container is running +if [ "$(docker ps -q -f name=${CONTAINER_NAME})" ]; then + # Create a dated log file name + LOG_FILE="logs/${CONTAINER_NAME}_$(date +'%Y-%m-%d_%H-%M-%S').log" + + # Save the logs to the file + mkdir -p logs + docker logs -t $CONTAINER_NAME >& $LOG_FILE + + # Check if the logs were saved successfully + if [ $? -eq 0 ]; then + echo "Logs saved to $LOG_FILE" + else + echo "Failed to save logs" + exit 1 + fi +else + echo "Container ${CONTAINER_NAME} is not running. Skipping log saving." +fi + +git pull + +docker compose down + +docker compose up -d --build + +docker image prune -f + +docker attach queue-bot \ No newline at end of file diff --git a/package.json b/package.json index 16ea47b..b6d116c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "setup": "npm ci && drizzle-kit push", "start": "node --loader @swc-node/register/esm --no-warnings --enable-source-maps --env-file .env src/index.ts", - "docker:start": "drizzle-kit push && npm run start", + "docker:start": "drizzle-kit push && npm start", "lint": "eslint src --fix" }, "type": "module", diff --git a/src/db/legacy-migration/migrate.ts b/src/db/legacy-migration/migrate.ts index 4ae7c8c..58fc01a 100644 --- a/src/db/legacy-migration/migrate.ts +++ b/src/db/legacy-migration/migrate.ts @@ -99,6 +99,7 @@ export async function loadExportData() { await Promise.all(files.map(file => new Promise((resolve, reject) => { const data: any[] = []; fs.createReadStream(`${LEGACY_EXPORT_DIR}/${file}`) + // @ts-ignore .pipe(csv()) .on("data", (row) => { data.push(row); diff --git a/src/db/schema.ts b/src/db/schema.ts index 5adf142..86bc02f 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -16,8 +16,8 @@ import { export const GUILD_TABLE = sqliteTable("guild", ({ guildId: text("guild_id").$type().primaryKey(), - logChannelId: text("log_channel_id").$type(), - logScope: text("log_scope").$type(), + logChannelId: text("log_channel_id").$type(), + logScope: text("log_scope").$type(), joinTime: integer("joinTime").$type().notNull().$defaultFn(() => BigInt(Date.now())), lastUpdateTime: integer("last_updated_time").$type().notNull().$defaultFn(() => BigInt(Date.now())), @@ -72,11 +72,11 @@ export const QUEUE_TABLE = sqliteTable("queue", ({ pullMessage: text("pull_message"), rejoinCooldownPeriod: integer("rejoin_cooldown_period").$type().notNull().default(0 as any), rejoinGracePeriod: integer("rejoin_grace_period").$type().notNull().default(0 as any), - roleInQueueId: text("role_in_queue_id").$type(), - roleOnPullId: text("role_on_pull_id").$type(), + roleInQueueId: text("role_in_queue_id").$type(), + roleOnPullId: text("role_on_pull_id").$type(), size: integer("size").$type(), timestampType: text("time_display_type").$type().default(TimestampType.Off), - voiceDestinationChannelId: text("voice_destination_channel_id").$type(), + voiceDestinationChannelId: text("voice_destination_channel_id").$type(), voiceOnlyToggle: integer("voice_only_toggle", { mode: "boolean" }).notNull().default(false), }), (table) => ({ @@ -131,7 +131,7 @@ export const DISPLAY_TABLE = sqliteTable("display", ({ guildId: text("guild_id").notNull().references(() => GUILD_TABLE.guildId, { onDelete: "cascade" }), queueId: integer("queue_id").$type().notNull().references(() => QUEUE_TABLE.id, { onDelete: "cascade" }), displayChannelId: text("display_channel_id").notNull(), - lastMessageId: text("last_message_id").$type(), + lastMessageId: text("last_message_id").$type(), }), (table) => ({ unq: unique().on(table.queueId, table.displayChannelId), @@ -185,10 +185,10 @@ export const SCHEDULE_TABLE = sqliteTable("schedule", ({ guildId: text("guild_id").notNull().references(() => GUILD_TABLE.guildId, { onDelete: "cascade" }), queueId: integer("queue_id").$type().notNull().references(() => QUEUE_TABLE.id, { onDelete: "cascade" }), - command: text("command").notNull().$type(), + command: text("command").notNull().$type(), cron: text("cron").notNull(), timezone: text("timezone"), - messageChannelId: text("message_channel_id").$type(), + messageChannelId: text("message_channel_id").$type(), reason: text("reason"), }), (table) => ({