init
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 6s

This commit is contained in:
2025-05-25 22:29:44 +07:00
commit 5eb5aa6eec
27 changed files with 1998 additions and 0 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,24 @@
name: Gitea Auto Deploy
run-name: ${{ gitea.actor }} pushed code 🚀
on: [push]
jobs:
Deploy-Container:
runs-on: ubuntu-latest
steps:
- name: Check out latest code
uses: actions/checkout@v4
- name: Stop and remove old containers
run: |
docker compose down || true
- name: Remove unused Docker resources
run: |
docker system prune -a --volumes -f
- name: Build and restart containers
run: |
docker compose up -d

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
node_modules
.history

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json package-lock*.json ./
RUN npm ci
FROM base AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json ./package.json
COPY package.json package-lock.json tsconfig.json .env ./
COPY src ./src
CMD ["npm", "run", "start"]

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
firefly-bot:
build:
context: .
dockerfile: Dockerfile
container_name: firefly-bot
networks:
- bot-network
networks:
bot-network:

1390
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "firefly-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"build": "tsc"
},
"_moduleAliases": {
"@": "src"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.9.0",
"discord.js": "^14.19.3",
"dotenv": "^16.5.0",
"module-alias": "^2.2.3"
},
"devDependencies": {
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
}

2
src/commands/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./prefix";
export * from "./slash";

View File

@@ -0,0 +1,10 @@
import { Client, Message } from "discord.js";
import { fetchApk } from "@/services/fetchApk";
export const name = "apk";
export const description = "Fetch apk";
export async function execute(client: Client, message: Message, args: string[]) {
const version = args[0];
await fetchApk(version, message);
}

View File

@@ -0,0 +1,24 @@
import { Client, Collection } from "discord.js";
import path from "path";
import fs from "fs";
import type { prefixCommand } from "@/types/discord.js";
export function ActiveAllPrefixCommands(client: Client) {
const commandFolder = path.join(__dirname);
client.prefix = new Collection<string, prefixCommand>();
for (const folder of fs.readdirSync(commandFolder)) {
const folderPath = path.join(commandFolder, folder);
if (!fs.statSync(folderPath).isDirectory()) continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of commandFiles) {
const command = require(path.join(folderPath, file));
if (!command.name || typeof command.execute !== "function") {
console.log(`Prefix command at ${path.join(folderPath, file)} is missing a name or execute function`);
continue;
}
// console.log(`Prefix command ${command.name} loaded`);
client.prefix.set(command.name, command);
}
}
}

View File

@@ -0,0 +1,10 @@
import { Message, Client } from "discord.js";
export const name = "ping";
export const description = "Kiểm tra thời gian phản hồi của bot";
export async function execute(client: Client, message: Message, args: string[]) {
const sent = await message.reply('Pong!');
const latency = sent.createdTimestamp - message.createdTimestamp;
await sent.edit(`Pong! \`${latency}ms\``);
}

View File

@@ -0,0 +1,13 @@
import { Client, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import { fetchApk } from "@/services/fetchApk";
export const data = new SlashCommandBuilder()
.setName("apk")
.setDescription("Fetch apk")
.addStringOption((option) => option
.setName("version")
.setDescription("Version of apk")
.setRequired(true));
export async function execute(client, interaction) {
const version = interaction.options.getString("version");
await fetchApk(version, interaction);
}

View File

@@ -0,0 +1,17 @@
import { Client, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import { fetchApk } from "@/services/fetchApk";
export const data = new SlashCommandBuilder()
.setName("apk")
.setDescription("Fetch apk")
.addStringOption((option) =>
option
.setName("version")
.setDescription("Version of apk")
.setRequired(true)
)
export async function execute(client: Client, interaction: ChatInputCommandInteraction) {
const version = interaction.options.getString("version");
await fetchApk(version, interaction);
}

View File

@@ -0,0 +1,47 @@
import { Client, Collection, REST, Routes } from "discord.js";
import path from "path";
import fs from "fs";
import { config } from "@/config";
export async function ActiveAllSlashCommands(client) {
client.slash = new Collection();
const commandFolder = path.join(__dirname);
for (const folder of fs.readdirSync(commandFolder)) {
const folderPath = path.join(commandFolder, folder);
if (!fs.statSync(folderPath).isDirectory())
continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of commandFiles) {
const command = require(path.join(folderPath, file));
if (!command.data?.name || typeof command.execute !== "function") {
console.log(`Slash command at ${path.join(folderPath, file)} is missing a name or execute function`);
continue;
}
client.slash.set(command.data.name, command);
}
}
}
const rest = new REST({ version: '10' }).setToken(config.DISCORD_TOKEN);
export async function registerCommandsAll() {
const commands = [];
const commandFolder = path.join(__dirname);
for (const folder of fs.readdirSync(commandFolder)) {
const folderPath = path.join(commandFolder, folder);
if (!fs.statSync(folderPath).isDirectory())
continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of commandFiles) {
const command = require(path.join(folderPath, file));
if (!command.data?.name || typeof command.execute !== "function")
continue;
commands.push(command.data.toJSON());
}
}
try {
console.log('Started refreshing application (/) commands.');
const data = await rest.put(Routes.applicationCommands(config.DISCORD_CLIENT_ID), { body: commands });
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
}
catch (error) {
console.error(error);
}
}

View File

@@ -0,0 +1,53 @@
import type { slashCommand } from "@/types/discord.js";
import { Client, Collection, REST, Routes, type RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord.js";
import path from "path";
import fs from "fs";
import { config } from "@/config";
export async function ActiveAllSlashCommands(client: Client) {
client.slash = new Collection<string, slashCommand>();
const commandFolder = path.join(__dirname);
for (const folder of fs.readdirSync(commandFolder)) {
const folderPath = path.join(commandFolder, folder);
if (!fs.statSync(folderPath).isDirectory()) continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of commandFiles) {
const command = require(path.join(folderPath, file));
if (!command.data?.name || typeof command.execute !== "function") {
console.log(`Slash command at ${path.join(folderPath, file)} is missing a name or execute function`);
continue;
}
// console.log(`Slash command ${command.data.name} loaded`);
client.slash.set(command.data.name, command);
}
}
}
const rest = new REST({ version: '10' }).setToken(config.DISCORD_TOKEN);
export async function registerCommandsAll() {
const commands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = [];
const commandFolder = path.join(__dirname);
for (const folder of fs.readdirSync(commandFolder)) {
const folderPath = path.join(commandFolder, folder);
if (!fs.statSync(folderPath).isDirectory()) continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of commandFiles) {
const command = require(path.join(folderPath, file));
if (!command.data?.name || typeof command.execute !== "function") continue;
commands.push(command.data.toJSON());
}
}
try {
console.log('Started refreshing application (/) commands.');
const data = await rest.put(Routes.applicationCommands(config.DISCORD_CLIENT_ID),
{ body: commands }) as RESTPostAPIChatInputApplicationCommandsJSONBody[];
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
}

View File

@@ -0,0 +1,7 @@
import { ChatInputCommandInteraction, Client, SlashCommandBuilder } from "discord.js";
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with pong!");
export async function execute(client, interaction) {
await interaction.reply("Pong!");
}

View File

@@ -0,0 +1,10 @@
import { ChatInputCommandInteraction, Client, SlashCommandBuilder } from "discord.js";
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with pong!")
export async function execute(client: Client, interaction: ChatInputCommandInteraction) {
await interaction.reply("Pong!");
}

18
src/config.ts Normal file
View File

@@ -0,0 +1,18 @@
import dotenv from "dotenv";
dotenv.config();
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_ID, DISCORD_PREFIX } = process.env;
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DISCORD_GUILD_ID || !DISCORD_PREFIX) {
throw new Error("Missing environment variables");
}
export const config = {
DISCORD_TOKEN,
DISCORD_CLIENT_ID,
DISCORD_GUILD_ID,
DISCORD_PREFIX,
};

26
src/events/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Client } from "discord.js";
import path from "path";
import fs from "fs";
export default function ActiveAllEvents(client: Client) {
const eventFolder = path.join(__dirname);
for (const folder of fs.readdirSync(eventFolder)) {
const folderPath = path.join(eventFolder, folder);
if (!fs.statSync(folderPath).isDirectory()) continue;
const eventFiles = fs.readdirSync(folderPath).filter(file => file.endsWith(".ts"));
for (const file of eventFiles) {
const event = require(path.join(folderPath, file));
if (!event.name || typeof event.execute !== "function") {
console.log(`Event at ${path.join(folderPath, file)} is missing a name or execute function`);
continue;
}
// console.log(`Event ${event.name} loaded`);
if (event.once) {
client.once(event.name, (...args) => event.execute(client, ...args));
} else {
client.on(event.name, (...args) => event.execute(client, ...args));
}
}
}
}

View File

@@ -0,0 +1,31 @@
import { ChatInputCommandInteraction, Client, Events, MessageFlags } from "discord.js";
export const name = Events.InteractionCreate;
export const once = false;
export async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand()) return;
const command = client.slash.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(client, interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: "An error occurred while executing this command.",
flags: MessageFlags.Ephemeral
});
} else {
await interaction.reply({
content: "An error occurred while executing this command.",
flags: MessageFlags.Ephemeral
});
}
}
}

View File

@@ -0,0 +1,30 @@
import { config } from "@/config";
import { Client, Events, Message } from "discord.js";
export const name = Events.MessageCreate;
export const once = false;
export async function execute(client: Client, message: Message) {
if (message.author.bot) return;
const prefix = config.DISCORD_PREFIX;
if (!message.content.startsWith(prefix)) return;
const args = message.content.slice(prefix.length).trim().split(/ +/);
const commandName = args.shift()?.toLowerCase();
if (!commandName) return;
const command = client.prefix.get(commandName);
if (command) {
try {
await command.execute(client, message, args);
} catch (error) {
console.error(error);
await message.reply('Đã xảy ra lỗi khi thực hiện lệnh này!');
}
}
}

33
src/events/ready/ready.js Normal file
View File

@@ -0,0 +1,33 @@
import { ActivityType, Client, Events, PresenceUpdateStatus } from "discord.js";
export const name = Events.ClientReady;
export const once = true;
export async function execute(client) {
if (!client.user || !client.application)
return;
console.log(`🚩Logged in as ${client.user.tag}!`);
const activityTypeMap = {
'PLAYING': ActivityType.Playing,
'WATCHING': ActivityType.Watching,
'LISTENING': ActivityType.Listening,
'STREAMING': ActivityType.Streaming,
'COMPETING': ActivityType.Competing
};
const statusMap = {
'online': PresenceUpdateStatus.Online,
'idle': PresenceUpdateStatus.Idle,
'dnd': PresenceUpdateStatus.DoNotDisturb,
'invisible': PresenceUpdateStatus.Invisible
};
const statusType = process.env.BOT_STATUS || 'online';
const activityType = process.env.ACTIVITY_TYPE || 'PLAYING';
const activityName = process.env.ACTIVITY_NAME || 'Shoudo sakusen jikkou!';
client.user.setPresence({
status: statusMap[statusType],
activities: [{
name: activityName,
type: activityTypeMap[activityType]
}]
});
console.log(`🗿Bot status set to: ${statusType}`);
console.log(`👨🎤Activity set to: ${activityType} ${activityName}`);
}

41
src/events/ready/ready.ts Normal file
View File

@@ -0,0 +1,41 @@
import { ActivityType, Client, Events, PresenceUpdateStatus } from "discord.js";
// import { registerCommandsAll } from "@/commands";
export const name = Events.ClientReady;
export const once = true;
export async function execute(client: Client) {
if (!client.user || !client.application) return;
console.log(`🚩Logged in as ${client.user.tag}!`);
// await registerCommandsAll()
const activityTypeMap = {
'PLAYING': ActivityType.Playing,
'WATCHING': ActivityType.Watching,
'LISTENING': ActivityType.Listening,
'STREAMING': ActivityType.Streaming,
'COMPETING': ActivityType.Competing
} as const;
const statusMap = {
'online': PresenceUpdateStatus.Online,
'idle': PresenceUpdateStatus.Idle,
'dnd': PresenceUpdateStatus.DoNotDisturb,
'invisible': PresenceUpdateStatus.Invisible
} as const;
const statusType = (process.env.BOT_STATUS as keyof typeof statusMap) || 'online';
const activityType = (process.env.ACTIVITY_TYPE as keyof typeof activityTypeMap) || 'PLAYING';
const activityName = process.env.ACTIVITY_NAME || 'Shoudo sakusen jikkou!';
client.user.setPresence({
status: statusMap[statusType],
activities: [{
name: activityName,
type: activityTypeMap[activityType]
}]
});
console.log(`🗿Bot status set to: ${statusType}`);
console.log(`👨🎤Activity set to: ${activityType} ${activityName}`)
}

31
src/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import 'module-alias/register'
import { Client, GatewayIntentBits, Partials } from "discord.js";
import { config } from "./config";
import ActiveAllEvents from "./events";
import { ActiveAllPrefixCommands, ActiveAllSlashCommands } from "./commands";
console.log("🔥 Starting bot...")
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildPresences,
],
partials: [
Partials.Channel,
Partials.Message,
Partials.User,
Partials.GuildMember
]
});
ActiveAllPrefixCommands(client);
ActiveAllSlashCommands(client);
ActiveAllEvents(client);
(async () => {
await client.login(config.DISCORD_TOKEN);
})();

67
src/services/fetchApk.ts Normal file
View File

@@ -0,0 +1,67 @@
import axios from "axios";
import { ChatInputCommandInteraction, EmbedBuilder, Message, MessageFlags } from "discord.js";
export async function fetchApk(version: string | undefined | null, req: ChatInputCommandInteraction | Message) {
if (!version) {
await req.reply({
content: '❌ Please provide a version.',
ephemeral: true,
});
return;
}
const t = Math.floor(Date.now() / 1000);
const url = 'https://globaldp-beta-cn01.bhsr.com/query_dispatch';
const params = {
version: `CNBETAAndroid${version.replace(/\s+/g, "")}`,
t: t.toString(),
language_type: '3',
platform_type: '3',
channel_id: '1',
sub_channel_id: '1',
is_new_format: '1',
};
let sent;
if (req instanceof Message) {
sent = await req.reply({
content: '🔄 Fetching APK...',
flags: MessageFlags.SuppressNotifications
});
}
try {
const response = await axios.get(url, { params });
const decoded = Buffer.from(response.data, 'base64').toString('utf-8');
const match = decoded.match(/https?:\/\/[^\s\\"]+/);
if (match) {
const embed = new EmbedBuilder()
.setTitle("📦 APK Download Link")
.setDescription(`[Click here to download](${match[0]})`)
.addFields({ name: "Version", value: version })
.setColor("Random")
.setTimestamp();
await req.reply({ embeds: [embed] });
} else {
const embed = new EmbedBuilder()
.setTitle("❌ No URL Found")
.setDescription("No valid APK URL was found in the response.")
.setColor("Red");
await req.reply({ embeds: [embed] });
}
} catch (error: any) {
console.error("❌ Error:", error.message);
const embed = new EmbedBuilder()
.setTitle("🚨 Error")
.setDescription("An error occurred while fetching the APK.")
.setColor("Red");
await req.reply({ embeds: [embed] });
}
if (sent) {
await sent.delete();
}
}

19
src/types/discord.js.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import { Collection, Client as BaseClient, SlashCommandBuilder, CommandInteraction } from "discord.js";
export interface slashCommand {
data: SlashCommandBuilder;
execute: (client: BaseClient, interaction: CommandInteraction) => Promise<void>;
}
export interface prefixCommand {
name: string;
description: string;
execute: (client: Client, message: Message, args: string[]) => Promise<void>;
}
declare module "discord.js" {
interface Client {
slash: Collection<string, slashCommand>;
prefix: Collection<string, prefixCommand>;
}
}

35
tsconfig.json Normal file
View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"noImplicitAny": true,
"sourceMap": false,
"removeComments": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"verbatimModuleSyntax": false,
"baseUrl": "src",
"module": "CommonJS",
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// "noEmit": true,
"outDir": "dist",
"lib": ["es2022"],
"paths": {
"@/*": ["*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}