Compare commits

...

10 Commits

Author SHA1 Message Date
Asaki Yuki
efb717f91f small update 2025-10-26 16:31:22 +07:00
Asaki Yuki
12214df1e9 idk 2025-10-21 12:18:52 +07:00
Asaki Yuki
d419ca91e6 edit something 2025-10-20 23:56:33 +07:00
Asaki Yuki
755a3876a9 bump compose version 2025-10-20 23:10:03 +07:00
Asaki Yuki
b23c4c882e add docker compose 2025-10-20 23:05:57 +07:00
Asaki Yuki
d7624cc25e add .env example file 2025-10-20 22:52:36 +07:00
Asaki Yuki
67121a4349 edit dockerfile 2025-10-20 22:50:56 +07:00
Asaki Yuki
69a74118ae switch to yarn 2025-10-18 23:42:33 +07:00
Asaki Yuki
7920681226 edit something 2025-10-18 23:15:55 +07:00
Asaki Yuki
ffdce31ecb add commands system 2025-10-18 17:41:27 +07:00
19 changed files with 233 additions and 121 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
# This file contains environment variables for the application.
ENVIRONMENT=production # development, production, test
## Discord Bot Configuration
CHANNEL=
CLIENTID=
CLIENTSECRET=
TOKEN=

View File

@@ -1,38 +1,7 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
FROM oven/bun
WORKDIR /usr/firefly/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# [optional] tests & build
ENV NODE_ENV=production
RUN bun test
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .
# run the app
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.ts" ]
RUN bun install
CMD [ "bun", "run", "start" ]

View File

@@ -1,22 +0,0 @@
### Building and running your application
When you're ready, start your application by running:
`docker compose up --build`.
Your application will be available at http://localhost:8080.
### Deploying your application to the cloud
First, build your image, e.g.: `docker build -t myapp .`.
If your cloud uses a different CPU architecture than your development
machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
you'll want to build the image for that platform, e.g.:
`docker build --platform=linux/amd64 -t myapp .`.
Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
docs for more detail on building and pushing.
### References
* [Docker's Node.js guide](https://docs.docker.com/language/nodejs/)

View File

@@ -2,14 +2,17 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "fireflydiscordbot",
"dependencies": {
"@discordjs/voice": "^0.19.0",
"discord.js": "^14.23.2",
"jsonc-parser": "^3.3.1",
"typescript": "^5.9.3",
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/pngjs": "^6.0.5",
"dotenv": "^17.2.3",
},
},
},
@@ -46,8 +49,12 @@
"discord.js": ["discord.js@14.23.2", "", { "dependencies": { "@discordjs/builders": "^1.12.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.29", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-tU2NFr823X3TXEc8KyR/4m296KLxPai4nirN3q9kHCpY4TKj96n9lHZnyLzRNMui8EbL07jg9hgH2PWWfKMGIg=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],

View File

@@ -1,50 +1,11 @@
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
version: "2.40"
services:
server:
build:
context: .
app:
build: .
container_name: firefly-discord-bot
environment:
NODE_ENV: production
ports:
- 8080:8080
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to
# start the database before your application. The `db-data` volume persists the
# database data between container restarts. The `db-password` secret is used
# to set the database password. You must create `db/password.txt` and add
# a password of your choosing to it before running `docker-compose up`.
# depends_on:
# db:
# condition: service_healthy
# db:
# image: postgres
# restart: always
# user: postgres
# secrets:
# - db-password
# volumes:
# - db-data:/var/lib/postgresql/data
# environment:
# - POSTGRES_DB=example
# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
# expose:
# - 5432
# healthcheck:
# test: [ "CMD", "pg_isready" ]
# interval: 10s
# timeout: 5s
# retries: 5
# volumes:
# db-data:
# secrets:
# db-password:
# file: db/password.txt
ENVIRONMENT: ${ENVIRONMENT}
TOKEN: ${TOKEN}
CHANNEL: ${CHANNEL}
CLIENTID: ${CLIENTID}
CLIENTSECRET: ${CLIENTSECRET}

3
config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"startup_messages": ["I'm online!", "Hello world!", "Hi there!"]
}

View File

@@ -1,11 +1,32 @@
{
"name": "fireflydiscordbot",
"version": "1.0.0",
"description": "con bot cua tao",
"keywords": [
"DiscordJS"
],
"repository": {
"type": "git",
"url": "git@100.124.183.21:/srv/git/discordbot.git"
},
"license": "ISC",
"author": "AsakiYuki",
"type": "commonjs",
"main": "src/app.ts",
"scripts": {
"dev": "bun --watch src/app.ts",
"start": "bun src/app.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@discordjs/voice": "^0.19.0",
"discord.js": "^14.23.2",
"jsonc-parser": "^3.3.1",
"typescript": "^5.9.3"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/pngjs": "^6.0.5"
"@types/pngjs": "^6.0.5",
"dotenv": "^17.2.3"
}
}

View File

@@ -1,3 +1,11 @@
import client from "./components/client"
import "./events/preload"
import dotenv from "dotenv"
client
dotenv.config({ quiet: true })
import "./cmd/index"
import path from "path"
import { requireDirectory } from "./handler/requireDirectory"
// Require events directory
requireDirectory(path.join(__dirname, "./events"))

View File

@@ -0,0 +1,30 @@
import { REST, Routes } from "discord.js"
import { Cmd } from "../types/cmd"
import { requireDirectory } from "../handler/requireDirectory"
const TOKEN = process.env.TOKEN
const CLIENTID = process.env.CLIENTID
if (!TOKEN || !CLIENTID) {
console.error("Missing TOKEN or CLIENTID in .env file")
process.exit(1)
}
const excludeFiles = ["index"]
export const execCmds = new Map<string, Cmd>()
requireDirectory(
__dirname,
({ data, exec }: Cmd, filename) => {
if (!data || !exec) throw new Error(`Command ${data.name} is missing data or execute function.`)
if (!data.name) data.setName(filename)
execCmds.set(data.name, { data, exec })
},
excludeFiles
)
const rest = new REST({ version: "10" }).setToken(TOKEN)
rest.put(Routes.applicationCommands(CLIENTID), {
body: Array.from(execCmds.values()).map(({ data }) => data.toJSON()),
})

10
src/cmd/ping.ts Normal file
View File

@@ -0,0 +1,10 @@
import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"
export const data = new SlashCommandBuilder().setName("ping").setDescription("Replies with Pong!")
export const exec = async (interaction: ChatInputCommandInteraction) => {
await interaction.reply({
content: "Pong 🏓!",
flags: MessageFlags.Ephemeral,
})
}

View File

@@ -1,13 +1,13 @@
import { Client, GatewayIntentBits } from "discord.js"
const token: string | undefined = process.env.DISCORD_BOT_TOKEN
const token: string | undefined = process.env.TOKEN
if (!token) {
console.error("Missing DISCORD_BOT_TOKEN environment variable")
console.error("Missing TOKEN in .env file")
process.exit(1)
}
const client = new Client({
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
})
@@ -15,5 +15,3 @@ client.login(token).catch(error => {
console.error("Failed to login to Discord:", error)
process.exit(1)
})
export default client

11
src/components/config.ts Normal file
View File

@@ -0,0 +1,11 @@
import JSONC from "jsonc-parser"
import fs from "fs"
interface CONFIG {
startup_messages: string[]
}
const data = fs.readFileSync("config.json", "utf-8")
const config: CONFIG = JSONC.parse(data)
export default config

35
src/events/clientReady.ts Normal file
View File

@@ -0,0 +1,35 @@
import { ActivityType } from "discord.js"
import { client } from "../components/client"
import config from "../components/config"
import { execCmds } from "../cmd"
client.on("clientReady", async client => {
try {
console.info(`Logged in as ${client.user?.tag}`)
console.info(`${execCmds.size} commands loaded.`)
client.user.setActivity({
name: "Docker",
state: "Develop by Asaki Yuki",
url: "https://asakiyuki.com/",
type: ActivityType.Playing,
})
try {
const channelId = process.env.CLIENTID
if (!channelId) throw new Error("Missing CLIENTID in .env file")
const channel = await client.channels.fetch(channelId)
if (!channel) throw new Error("Channel not found")
const message = config.startup_messages?.[Math.floor(Math.random() * config.startup_messages.length)]
if (!message) throw new Error("Missing startup message")
if (channel.isSendable()) await channel.send(message)
} catch (error) {
console.error(error)
}
} catch (error) {
console.error(error)
}
})

View File

@@ -0,0 +1,6 @@
import { client } from "../components/client"
import { execCmd } from "../handler/executeCommand"
client.on("interactionCreate", async interaction => {
if (interaction.isCommand() && interaction.isChatInputCommand()) execCmd(interaction)
})

12
src/events/preload.ts Normal file
View File

@@ -0,0 +1,12 @@
const time = Date.now()
const log = new Map<string, (...args: any[]) => void>()
const keys = Object.keys(console).filter(key => !key.includes("time"))
keys.forEach(key => {
const func: (...args: any[]) => void = (console as any)[key]
log.set(key, func)
;(console as any)[key] = (...args: any[]) => {
log.get(key)?.(`[${Date.now() - time}ms] [${key.toUpperCase()}]`, ...args)
}
})

View File

@@ -0,0 +1,22 @@
import { ChatInputCommandInteraction, CacheType } from "discord.js"
import { execCmds } from "../cmd"
export async function execCmd(interaction: ChatInputCommandInteraction<CacheType>) {
try {
const commandName = interaction.commandName
const command = execCmds.get(commandName)
console.info(`Executing command '${commandName}' by ${interaction.user.tag} (${interaction.user.id})`)
const options = interaction.options.data
if (options.length > 0) {
console.info(
options.map(option => `\x1b[34m${option.name}\x1b[0m: \x1b[90m${option.value}\x1b[0m`).join(", ")
)
}
if (command) command.exec(interaction)
else console.warn(`Command ${commandName} not found`)
} catch (error) {
console.error(error)
}
}

View File

@@ -0,0 +1,20 @@
import fs from "fs"
export function requireDirectory(
directoryPath: string,
callback?: (fileExport: any, fileName: string) => void,
excludeFileNames?: string[]
) {
const dir = fs.readdirSync(directoryPath).map(file => file.replace(/\.(ts|js)$/, ""))
const files = dir.filter(file => !excludeFileNames?.includes(file))
files.forEach(file => {
try {
if (!callback) return require(`${directoryPath}/${file}`)
const fileExport = require(`${directoryPath}/${file}`)
callback(fileExport, file)
} catch (error) {
console.error(error)
}
})
}

8
src/types/cmd.ts Normal file
View File

@@ -0,0 +1,8 @@
import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"
export type CommandFunction = (interaction: ChatInputCommandInteraction<CacheType>) => void
export interface Cmd {
data: SlashCommandBuilder
exec: CommandFunction
}

View File

@@ -151,6 +151,11 @@ discord.js@^14.23.2:
tslib "^2.6.3"
undici "6.21.3"
dotenv@^17.2.3:
version "17.2.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2"
integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==
fast-deep-equal@3.1.3, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"