first commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
config/
|
||||||
|
data/
|
||||||
|
.env
|
||||||
|
src/commands/admin/
|
||||||
|
src/commands/general/
|
22
README.md
Executable file
22
README.md
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
# DiscordGaori
|
||||||
|
Rewritten [DiscordGaori](https://github.com/yeongaori/DiscordGaori-StarLight) with Node.js
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Rich Presence (RPC)
|
||||||
|
|
||||||
|
This bot supports Discord's Rich Presence feature, which allows you to customize the bot's status.
|
||||||
|
|
||||||
|
To configure the RPC, you need to set the following variables in your `.env` file:
|
||||||
|
|
||||||
|
- `RPC_ENABLED`: Set to `true` to enable RPC, or `false` to disable it.
|
||||||
|
- `RPC_ACTIVITY_NAME`: The name of the activity you want the bot to display (e.g., "with Gaori").
|
||||||
|
- `RPC_ACTIVITY_TYPE`: The type of activity. Can be one of `Playing`, `Listening`, `Watching`, `Streaming`, or `Competing`.
|
||||||
|
|
||||||
|
Example `.env` configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
RPC_ENABLED=true
|
||||||
|
RPC_ACTIVITY_NAME="전역일 계산"
|
||||||
|
RPC_ACTIVITY_TYPE=Playing
|
||||||
|
```
|
41
index.js
Normal file
41
index.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { Client, GatewayIntentBits, Partials } = require('discord.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('./modules/colorfulLogger');
|
||||||
|
const { loadData } = require('./src/data/dataManager');
|
||||||
|
const { setupProcessHandlers } = require('./src/handlers/processHandlers');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
GatewayIntentBits.DirectMessages,
|
||||||
|
],
|
||||||
|
partials: [Partials.Channel],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load data and setup process handlers
|
||||||
|
loadData();
|
||||||
|
setupProcessHandlers(client);
|
||||||
|
|
||||||
|
// Load event handlers
|
||||||
|
const eventsPath = path.join(__dirname, 'src', 'events');
|
||||||
|
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
|
for (const file of eventFiles) {
|
||||||
|
const filePath = path.join(eventsPath, file);
|
||||||
|
const event = require(filePath);
|
||||||
|
if (event.once) {
|
||||||
|
client.once(event.name, (...args) => event.execute(...args));
|
||||||
|
} else {
|
||||||
|
client.on(event.name, (...args) => event.execute(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Attempting to log in with token...');
|
||||||
|
client.login(process.env.DISCORD_TOKEN).catch(e => {
|
||||||
|
logger.error(`Failed to login: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
1078
index_bk.js
Executable file
1078
index_bk.js
Executable file
File diff suppressed because it is too large
Load Diff
34
modules/colorfulLogger.js
Executable file
34
modules/colorfulLogger.js
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
const colors = {
|
||||||
|
noformat: '\033[0m',
|
||||||
|
Fbold: '\033[1m',
|
||||||
|
Fgreen: '\x1b[32m',
|
||||||
|
Fblue: '\x1b[34m',
|
||||||
|
Fred: '\x1b[31m',
|
||||||
|
Fwhite: '\x1b[37m',
|
||||||
|
Cwhite: '\033[38;5;15m',
|
||||||
|
Clime: '\033[48;5;10m',
|
||||||
|
Cred: '\033[48;5;9m',
|
||||||
|
Cyellow: '\033[48;5;3m',
|
||||||
|
Cgreen: '\033[48;5;2m',
|
||||||
|
Ccyan: '\033[48;5;6m',
|
||||||
|
Corange: '\033[48;5;202m'
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
info(...args) {
|
||||||
|
console.log(`${colors.Clime}${colors.Fwhite} INFO ${colors.noformat}`, ...args);
|
||||||
|
},
|
||||||
|
warn(...args) {
|
||||||
|
console.log(`${colors.Cyellow}${colors.Fwhite} WARN ${colors.noformat}`, ...args);
|
||||||
|
},
|
||||||
|
error(...args) {
|
||||||
|
console.error(`${colors.Cred}${colors.Fwhite} ERRO ${colors.noformat}`, ...args);
|
||||||
|
},
|
||||||
|
debug(...args) {
|
||||||
|
console.log(`${colors.Corange}${colors.Fwhite} DEBG ${colors.noformat}`, ...args);
|
||||||
|
},
|
||||||
|
term(...args) {
|
||||||
|
console.log(`${colors.Ccyan}${colors.Fwhite} TERM ${colors.noformat}`, ...args)
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
320
package-lock.json
generated
Executable file
320
package-lock.json
generated
Executable file
@@ -0,0 +1,320 @@
|
|||||||
|
{
|
||||||
|
"name": "discordgaori",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "discordgaori",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.18.0",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/builders": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/formatters": "^0.6.0",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
|
"discord-api-types": "^0.37.119",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"ts-mixer": "^6.0.4",
|
||||||
|
"tslib": "^2.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/collection": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/formatters": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.37.114"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.1",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
|
"@sapphire/snowflake": "^3.5.3",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
|
"discord-api-types": "^0.37.119",
|
||||||
|
"magic-bytes.js": "^1.10.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.21.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/util": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.0",
|
||||||
|
"@discordjs/rest": "^2.4.3",
|
||||||
|
"@discordjs/util": "^1.1.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.2",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||||
|
"discord-api-types": "^0.37.119",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/async-queue": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/shapeshift": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||||
|
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/discord-api-types": {
|
||||||
|
"version": "0.37.119",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
|
||||||
|
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/discord.js": {
|
||||||
|
"version": "14.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz",
|
||||||
|
"integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.10.1",
|
||||||
|
"@discordjs/collection": "1.5.3",
|
||||||
|
"@discordjs/formatters": "^0.6.0",
|
||||||
|
"@discordjs/rest": "^2.4.3",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@discordjs/ws": "^1.2.1",
|
||||||
|
"@sapphire/snowflake": "3.5.3",
|
||||||
|
"discord-api-types": "^0.37.119",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
|
"lodash.snakecase": "4.1.1",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.21.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||||
|
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.snakecase": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/magic-bytes.js": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ts-mixer": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||||
|
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
package.json
Executable file
25
package.json
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "discordgaori",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A simple discord bot with various functions includin military service related things.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"deploy": "node deploy-commands.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/yeongaori/DiscordGaori.git"
|
||||||
|
},
|
||||||
|
"author": "yeongaori",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/yeongaori/DiscordGaori/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/yeongaori/DiscordGaori#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.18.0",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
|
}
|
||||||
|
}
|
119
src/commands/military/celebration.js
Normal file
119
src/commands/military/celebration.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
const { PermissionsBitField } = require('discord.js');
|
||||||
|
const logger = require('../../../modules/colorfulLogger');
|
||||||
|
const { getCelebrationConfig, getIpdaeData, getNotificationHistory, saveData } = require('../../data/dataManager');
|
||||||
|
const { addMonthsToDate, calculateDaysFromNow } = require('../../utils/dateUtils');
|
||||||
|
const { handleCommandError } = require('../../../utils/errorHandler');
|
||||||
|
|
||||||
|
async function checkAndSendCelebrationMessages(client) {
|
||||||
|
logger.info('Starting celebration check...');
|
||||||
|
try {
|
||||||
|
const celebrationConfig = getCelebrationConfig();
|
||||||
|
const ipdaeDatas = getIpdaeData();
|
||||||
|
const notificationHistory = getNotificationHistory();
|
||||||
|
|
||||||
|
if (!celebrationConfig?.milestones || !celebrationConfig.channelTopicKeyword) {
|
||||||
|
logger.warn('Celebration config is missing or incomplete. Skipping celebration check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notificationHistoryModifiedThisRun = false;
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
for (const userData of ipdaeDatas) {
|
||||||
|
if (!userData.id || !userData.date || !userData.type) continue;
|
||||||
|
|
||||||
|
let months;
|
||||||
|
switch (userData.type) {
|
||||||
|
case '육군': case '해병대': months = 18; break;
|
||||||
|
case '해군': months = 20; break;
|
||||||
|
case '공군': case '사회복무요원': months = 21; break;
|
||||||
|
default: continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingDays = calculateDaysFromNow(addMonthsToDate(userData.date, months), today);
|
||||||
|
const userHistory = notificationHistory[userData.id] || [];
|
||||||
|
|
||||||
|
for (const milestone of celebrationConfig.milestones) {
|
||||||
|
if (remainingDays === milestone.days && !userHistory.includes(milestone.days)) {
|
||||||
|
const userToNotify = await client.users.fetch(userData.id).catch(() => null);
|
||||||
|
if (!userToNotify) continue;
|
||||||
|
|
||||||
|
for (const guild of client.guilds.cache.values()) {
|
||||||
|
const member = guild.members.cache.get(userData.id) || await guild.members.fetch(userData.id).catch(() => null);
|
||||||
|
if (member) {
|
||||||
|
const targetChannels = guild.channels.cache.filter(ch =>
|
||||||
|
ch.isTextBased() &&
|
||||||
|
ch.topic?.includes(celebrationConfig.channelTopicKeyword) &&
|
||||||
|
guild.members.me?.permissionsIn(ch).has(PermissionsBitField.Flags.SendMessages)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const channel of targetChannels.values()) {
|
||||||
|
try {
|
||||||
|
const { mentionUser = true, mentionEveryone = false } = milestone;
|
||||||
|
let messagePrefix = "";
|
||||||
|
const allowEveryoneMention = mentionEveryone && guild.members.me.permissionsIn(channel).has(PermissionsBitField.Flags.MentionEveryone);
|
||||||
|
|
||||||
|
if (allowEveryoneMention) messagePrefix += "@everyone ";
|
||||||
|
if (mentionUser) messagePrefix += `<@${userData.id}> `;
|
||||||
|
|
||||||
|
const displayName = member.displayName || userToNotify.username;
|
||||||
|
const finalMessageToSend = messagePrefix + milestone.message.replace(/{userName}/g, displayName);
|
||||||
|
|
||||||
|
await channel.send({
|
||||||
|
content: finalMessageToSend,
|
||||||
|
allowedMentions: { parse: allowEveryoneMention ? ['everyone'] : [], users: mentionUser ? [userData.id] : [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Sent ${milestone.days}-day celebration for ${displayName} to #${channel.name}.`);
|
||||||
|
|
||||||
|
if (!notificationHistory[userData.id]) {
|
||||||
|
notificationHistory[userData.id] = [];
|
||||||
|
}
|
||||||
|
notificationHistory[userData.id].push(milestone.days);
|
||||||
|
notificationHistoryModifiedThisRun = true;
|
||||||
|
|
||||||
|
} catch (sendError) {
|
||||||
|
logger.error(`Failed to send celebration message to #${channel.name} for ${member.displayName}: ${sendError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationHistoryModifiedThisRun) {
|
||||||
|
await saveData('history');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Celebration check finished.');
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, 'Celebration Check', client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleDailyCelebrationCheck(client) {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const tomorrow = new Date(now);
|
||||||
|
tomorrow.setDate(now.getDate() + 1);
|
||||||
|
tomorrow.setHours(0, 0, 5, 0);
|
||||||
|
|
||||||
|
let timeToFirstCheck = tomorrow.getTime() - now.getTime();
|
||||||
|
if (timeToFirstCheck < 0) {
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
timeToFirstCheck = tomorrow.getTime() - now.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Next automatic celebration check scheduled for: ${tomorrow.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
checkAndSendCelebrationMessages(client);
|
||||||
|
setInterval(() => checkAndSendCelebrationMessages(client), 24 * 60 * 60 * 1000);
|
||||||
|
}, timeToFirstCheck);
|
||||||
|
} catch (e) {
|
||||||
|
handleCommandError(e, 'Schedule Celebration Check', client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkAndSendCelebrationMessages, scheduleDailyCelebrationCheck };
|
124
src/commands/military/getDischargeInfo.js
Normal file
124
src/commands/military/getDischargeInfo.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType, ApplicationIntegrationType } = require('discord.js');
|
||||||
|
const logger = require('../../../modules/colorfulLogger');
|
||||||
|
const { getIpdaeData } = require('../../data/dataManager');
|
||||||
|
const {
|
||||||
|
addMonthsToDate,
|
||||||
|
calculateProgress,
|
||||||
|
calculateDateDifference,
|
||||||
|
formatDate,
|
||||||
|
dateFormatter,
|
||||||
|
calculateMonthDifference
|
||||||
|
} = require('../../utils/dateUtils');
|
||||||
|
const { createProgressBar, getNameById } = require('../../utils/discordUtils');
|
||||||
|
const { binarySearch, addSpace } = require('../../utils/helpers');
|
||||||
|
|
||||||
|
const commandData = new SlashCommandBuilder()
|
||||||
|
.setName('전역일')
|
||||||
|
.setDescription('전역일과 관련된 정보들을 확인합니다')
|
||||||
|
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
|
||||||
|
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||||
|
.addUserOption(option => option.setName('유저').setDescription('전역일을 확인할 유저').setRequired(false))
|
||||||
|
.addIntegerOption(option => option.setName('소수점').setDescription('진행도의 소수점').setMinValue(0).setMaxValue(100).setRequired(false))
|
||||||
|
.addBooleanOption(option => option.setName('상세내용').setDescription('상세 내용 표시 여부').setRequired(false));
|
||||||
|
|
||||||
|
function getDischargeInfo(client, targetUserId, targetUserName, decimal, usedFullInfo) {
|
||||||
|
try {
|
||||||
|
const ipdaeDatas = getIpdaeData();
|
||||||
|
const userNameForEmbed = (targetUserName && targetUserName.trim() !== `Unknown User (${targetUserId})` && targetUserName.trim() !== '') ? `${targetUserName}님` : `사용자 (${targetUserId})님`;
|
||||||
|
const index = binarySearch(ipdaeDatas, targetUserId);
|
||||||
|
if (index === -1) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle('저장된 데이터가 없습니다')
|
||||||
|
.setDescription(`먼저 \`!입대일\` 또는 \`/입대일\` 명령어로 데이터를 저장해주세요.`)
|
||||||
|
.addFields({ name: '대상자', value: `${userNameForEmbed} (${targetUserId})` })
|
||||||
|
.setColor('#F44336');
|
||||||
|
}
|
||||||
|
const ipdaeData = ipdaeDatas[index];
|
||||||
|
let months = 0, startText = '입대일', endText = '전역일';
|
||||||
|
switch (ipdaeData.type) {
|
||||||
|
case '육군': case '해병대': months = 18; break;
|
||||||
|
case '해군': months = 20; break;
|
||||||
|
case '공군': case '사회복무요원': months = 21; break;
|
||||||
|
default: throw new Error(`Unknown service type "${ipdaeData.type}" for user ${targetUserId}`);
|
||||||
|
}
|
||||||
|
if (ipdaeData.type === '사회복무요원') { startText = '소집일'; endText = '소집해제일'; }
|
||||||
|
const startDate = ipdaeData.date;
|
||||||
|
const endDate = addMonthsToDate(startDate, months);
|
||||||
|
const now = new Date();
|
||||||
|
const currentProgress = calculateProgress(startDate, endDate, now);
|
||||||
|
const totalDays = calculateDateDifference(startDate, endDate) + 1;
|
||||||
|
const todayFormatted = formatDate(now);
|
||||||
|
let daysServed = new Date(todayFormatted) < new Date(startDate) ? 0 : calculateDateDifference(startDate, todayFormatted) + 1;
|
||||||
|
daysServed = Math.min(daysServed, totalDays);
|
||||||
|
const remainingDays = totalDays - daysServed;
|
||||||
|
const [sYearStr, sMonthStr, sDayStr] = startDate.split('-');
|
||||||
|
let pastMonths = calculateMonthDifference(sYearStr, sMonthStr, String(now.getFullYear()), String(now.getMonth() + 1));
|
||||||
|
if (sDayStr === '01') pastMonths++;
|
||||||
|
let militaryRank = '민간인';
|
||||||
|
if (remainingDays > 0) {
|
||||||
|
if (pastMonths <= 2) militaryRank = `이병 ${pastMonths + 1}호봉`;
|
||||||
|
else if (pastMonths <= 8) militaryRank = `일병 ${pastMonths - 2}호봉`;
|
||||||
|
else if (pastMonths <= 14) militaryRank = `상병 ${pastMonths - 8}호봉`;
|
||||||
|
else militaryRank = `병장 ${pastMonths - 14}호봉`;
|
||||||
|
}
|
||||||
|
const progressBarText = createProgressBar(currentProgress, 10, decimal || 4);
|
||||||
|
|
||||||
|
let dDayText;
|
||||||
|
if (remainingDays === 0) {
|
||||||
|
dDayText = 'D-Day';
|
||||||
|
} else if (remainingDays < 0) {
|
||||||
|
dDayText = `D+${-remainingDays}`;
|
||||||
|
} else {
|
||||||
|
dDayText = `D-${remainingDays}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usedFullInfo) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`${userNameForEmbed}의 ${endText}`)
|
||||||
|
.addFields(
|
||||||
|
{ name: '현재 진행도', value: `${progressBarText}\n${militaryRank}${addSpace(Math.max(0, 14 - militaryRank.length))}${dDayText}` },
|
||||||
|
{ name: '전체 복무일', value: `${totalDays}일` },
|
||||||
|
{ name: '현재 복무일', value: `${daysServed}일` }
|
||||||
|
)
|
||||||
|
.setColor('#007FFF');
|
||||||
|
} else {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`${userNameForEmbed}의 ${endText} 상세 정보`)
|
||||||
|
.addFields(
|
||||||
|
{ name: startText, value: dateFormatter(startDate) },
|
||||||
|
{ name: endText, value: dateFormatter(endDate) },
|
||||||
|
{ name: '현재 진행도', value: progressBarText },
|
||||||
|
{ name: '복무 형태', value: ipdaeData.type },
|
||||||
|
{ name: '전체 복무일', value: `${totalDays}일` },
|
||||||
|
{ name: '현재 복무일', value: `${daysServed}일` },
|
||||||
|
{ name: '남은 복무일', value: String(remainingDays) },
|
||||||
|
{ name: '현재 계급', value: militaryRank }
|
||||||
|
)
|
||||||
|
.setColor('#007FFF');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error in getDischargeInfo for ${targetUserId}: ${e.message}`);
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle('전역일 계산 중 오류')
|
||||||
|
.setDescription(`\`\`\`\n${e.message}\n\`\`\``)
|
||||||
|
.setColor('#F44336');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(interaction) {
|
||||||
|
const decimalArg = interaction.options.getInteger('소수점') || 4;
|
||||||
|
const targetUserOption = interaction.options.getUser('유저');
|
||||||
|
const targetUserId = targetUserOption ? targetUserOption.id : interaction.user.id;
|
||||||
|
const fullInfoArg = interaction.options.getBoolean('상세내용') || false;
|
||||||
|
const targetUserName = getNameById(interaction.client, targetUserId, interaction.guild);
|
||||||
|
logger.info(`Processing /전역일 for ${targetUserName} (${targetUserId}), requested by ${interaction.user.username} (${interaction.user.id})`);
|
||||||
|
await interaction.deferReply();
|
||||||
|
const resultEmbed = getDischargeInfo(interaction.client, targetUserId, targetUserName, decimalArg, fullInfoArg);
|
||||||
|
await interaction.editReply({ embeds: [resultEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: commandData,
|
||||||
|
getDischargeInfo,
|
||||||
|
execute
|
||||||
|
};
|
116
src/commands/military/setEnlistmentDate.js
Normal file
116
src/commands/military/setEnlistmentDate.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType, ApplicationIntegrationType } = require('discord.js');
|
||||||
|
const logger = require('../../../modules/colorfulLogger');
|
||||||
|
const {
|
||||||
|
ipdaeDatas,
|
||||||
|
notificationHistory,
|
||||||
|
saveData
|
||||||
|
} = require('../../data/dataManager');
|
||||||
|
const { serviceTypes, adminUserIds } = require('../../../config/constants');
|
||||||
|
const { dateFormatter } = require('../../utils/dateUtils');
|
||||||
|
const { binarySearch, binaryInsert } = require('../../utils/helpers');
|
||||||
|
const { getNameById } = require('../../utils/discordUtils');
|
||||||
|
const { handleCommandError } = require('../../../utils/errorHandler');
|
||||||
|
|
||||||
|
const commandData = new SlashCommandBuilder()
|
||||||
|
.setName('입대일')
|
||||||
|
.setDescription('입대일을 설정합니다')
|
||||||
|
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
|
||||||
|
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||||
|
.addStringOption(option => option.setName('입대일').setDescription('형식: YYYY-MM-DD').setRequired(true))
|
||||||
|
.addStringOption(option => option.setName('복무형태').setDescription('복무 형태').setRequired(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: '육군', value: '육군' },
|
||||||
|
{ name: '해군', value: '해군' },
|
||||||
|
{ name: '공군', value: '공군' },
|
||||||
|
{ name: '해병대', value: '해병대' },
|
||||||
|
{ name: '사회복무요원', value: '사회복무요원' }
|
||||||
|
));
|
||||||
|
|
||||||
|
async function setEnlistmentDate(client, callerUserId, dateArg, typeArg, commandString, customTargetUserId) {
|
||||||
|
dateArg = String(dateArg || '').trim();
|
||||||
|
typeArg = String(typeArg || '').trim();
|
||||||
|
let targetUserId = customTargetUserId && adminUserIds.includes(callerUserId) ? String(customTargetUserId.replace(/\D/g, '')) : callerUserId;
|
||||||
|
const targetUserName = getNameById(client, targetUserId);
|
||||||
|
const startDateEmbedTitle = customTargetUserId ? `${targetUserName}(${targetUserId}) 님의 입대일이 저장되었습니다` : '입대일이 저장되었습니다';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(/^\d{4}-\d{2}-\d{2}$/.test(dateArg) && serviceTypes.includes(typeArg))) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle('명령어 형식 오류')
|
||||||
|
.setDescription(`입력된 날짜: \`${dateArg}\`, 복무형태: \`${typeArg}\``)
|
||||||
|
.addFields({ name: '올바른 형식', value: `${commandString} YYYY-MM-DD ${serviceTypes.join('/')}` })
|
||||||
|
.setColor('#F44336');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = dateArg.split('-').map(Number);
|
||||||
|
const enlistDate = new Date(year, month - 1, day);
|
||||||
|
if (isNaN(enlistDate.getTime()) || enlistDate.getFullYear() !== year || enlistDate.getMonth() + 1 !== month || enlistDate.getDate() !== day) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle('유효하지 않은 날짜입니다.')
|
||||||
|
.setDescription(`입력한 날짜 \`${dateArg}\`를 확인해주세요.`)
|
||||||
|
.setColor('#F44336');
|
||||||
|
}
|
||||||
|
|
||||||
|
let notificationHistoryModified = false;
|
||||||
|
const index = binarySearch(ipdaeDatas, targetUserId);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
const oldDate = ipdaeDatas[index].date;
|
||||||
|
const oldType = ipdaeDatas[index].type;
|
||||||
|
|
||||||
|
if (targetUserId === '1107292307025301544' && typeArg !== '사회복무요원') {
|
||||||
|
typeArg = '사회복무요원';
|
||||||
|
logger.info(`User ID 1107292307025301544 type overridden to 사회복무요원.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipdaeDatas[index].date = dateArg;
|
||||||
|
ipdaeDatas[index].type = typeArg;
|
||||||
|
|
||||||
|
if (oldDate !== dateArg || oldType !== typeArg) {
|
||||||
|
logger.info(`Enlistment data changed for ${targetUserId}. Resetting their notification history.`);
|
||||||
|
notificationHistory[targetUserId] = [];
|
||||||
|
notificationHistoryModified = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binaryInsert(ipdaeDatas, { id: targetUserId, date: dateArg, type: typeArg });
|
||||||
|
notificationHistory[targetUserId] = [];
|
||||||
|
notificationHistoryModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveData('ipdae');
|
||||||
|
if (notificationHistoryModified) {
|
||||||
|
await saveData('history');
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalData = ipdaeDatas[binarySearch(ipdaeDatas, targetUserId)];
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(index !== -1 ? startDateEmbedTitle.replace('저장', '수정') : startDateEmbedTitle)
|
||||||
|
.addFields(
|
||||||
|
{ name: '입대일', value: dateFormatter(finalData.date) },
|
||||||
|
{ name: '복무 형태', value: finalData.type }
|
||||||
|
)
|
||||||
|
.setColor('#3BB143');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, `Data Save for ${targetUserId}`, client);
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle('데이터 저장/수정 중 오류')
|
||||||
|
.addFields({ name: '에러 메시지', value: e.message })
|
||||||
|
.setColor('#F44336');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(interaction) {
|
||||||
|
const slashStartDateArg = interaction.options.getString('입대일');
|
||||||
|
const slashStartTypeArg = interaction.options.getString('복무형태');
|
||||||
|
logger.info(`Processing /입대일 for ${interaction.user.username} (${interaction.user.id}) with date: ${slashStartDateArg}, type: ${slashStartTypeArg}`);
|
||||||
|
await interaction.deferReply();
|
||||||
|
const resultEmbed = await setEnlistmentDate(interaction.client, interaction.user.id, slashStartDateArg, slashStartTypeArg, '/입대일', undefined);
|
||||||
|
await interaction.editReply({ embeds: [resultEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: commandData,
|
||||||
|
setEnlistmentDate,
|
||||||
|
execute
|
||||||
|
};
|
24
src/events/interactionCreate.js
Normal file
24
src/events/interactionCreate.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { handleCommandError } = require('../../utils/errorHandler');
|
||||||
|
const { onSlashCommand, loadCommands } = require('../handlers/slashCommandHandler');
|
||||||
|
const { onButton } = require('../handlers/buttonHandler');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'interactionCreate',
|
||||||
|
async execute(interaction) {
|
||||||
|
if (!interaction.client.commands) {
|
||||||
|
interaction.client.commands = loadCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interaction.isCommand() && !interaction.isButton()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (interaction.isCommand()) {
|
||||||
|
await onSlashCommand(interaction);
|
||||||
|
} else if (interaction.isButton()) {
|
||||||
|
await onButton(interaction);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, interaction, interaction.client);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
16
src/events/messageCreate.js
Normal file
16
src/events/messageCreate.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { commandPrefix } = require('../../config/constants');
|
||||||
|
const { handleCommandError } = require('../../utils/errorHandler');
|
||||||
|
const { handleCommand } = require('../handlers/commandHandler');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'messageCreate',
|
||||||
|
async execute(message) {
|
||||||
|
if (message.author.bot || !message.content.startsWith(commandPrefix)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleCommand(message);
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, message, message.client);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
25
src/events/ready.js
Normal file
25
src/events/ready.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const { handleCommandError } = require('../../utils/errorHandler');
|
||||||
|
const { checkAndSendCelebrationMessages, scheduleDailyCelebrationCheck } = require('../commands/military/celebration');
|
||||||
|
const { ActivityType } = require('discord.js');
|
||||||
|
const { initializeRPC } = require('../handlers/rpcHandler');
|
||||||
|
const { initializeKoreanbotsUpdate } = require('../handlers/koreanbotsHandler');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'ready',
|
||||||
|
once: true,
|
||||||
|
async execute(client) {
|
||||||
|
try {
|
||||||
|
logger.info(`Logged in as ${client.user.tag}`);
|
||||||
|
|
||||||
|
initializeRPC(client);
|
||||||
|
initializeKoreanbotsUpdate(client);
|
||||||
|
|
||||||
|
logger.info('Performing initial celebration check on startup...');
|
||||||
|
await checkAndSendCelebrationMessages(client);
|
||||||
|
scheduleDailyCelebrationCheck(client);
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, 'Ready Event', client);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
29
src/handlers/buttonHandler.js
Normal file
29
src/handlers/buttonHandler.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const { getNameById } = require('../utils/discordUtils');
|
||||||
|
const { handleCommandError } = require('../../utils/errorHandler');
|
||||||
|
|
||||||
|
async function onButton(interaction) {
|
||||||
|
try {
|
||||||
|
const { customId, user, guild } = interaction;
|
||||||
|
const userName = getNameById(interaction.client, user.id, guild);
|
||||||
|
logger.info(`Button interaction: ${customId} by ${userName} (${user.id})`);
|
||||||
|
|
||||||
|
const [type, value] = customId.split('_');
|
||||||
|
|
||||||
|
const resultEmbed = new EmbedBuilder()
|
||||||
|
.setTitle('Button Interaction Received')
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Button Type', value: type || 'N/A' },
|
||||||
|
{ name: 'Button Value', value: value || 'N/A' },
|
||||||
|
{ name: 'Pressed by', value: `${userName} (${user.id})` }
|
||||||
|
)
|
||||||
|
.setColor(type === 'success' ? '#3BB143' : type === 'danger' ? '#F44336' : '#7289DA');
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [resultEmbed], ephemeral: true });
|
||||||
|
} catch (e) {
|
||||||
|
await handleCommandError(e, interaction, interaction.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { onButton };
|
105
src/handlers/commandHandler.js
Normal file
105
src/handlers/commandHandler.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const {
|
||||||
|
startDateCommands,
|
||||||
|
endDateCommands,
|
||||||
|
randomCommands,
|
||||||
|
diceCommands,
|
||||||
|
deleteCommands,
|
||||||
|
adminUserIds,
|
||||||
|
commandPrefix
|
||||||
|
} = require('../../config/constants');
|
||||||
|
const { setEnlistmentDate } = require('../commands/military/setEnlistmentDate');
|
||||||
|
const { getDischargeInfo } = require('../commands/military/getDischargeInfo');
|
||||||
|
const { handleGiftCode } = require('../commands/admin/giftCode');
|
||||||
|
const { handleEval } = require('../commands/admin/eval');
|
||||||
|
const { handleTest } = require('../commands/admin/test');
|
||||||
|
const { handleDelete } = require('../commands/admin/delete');
|
||||||
|
const { handleRandom } = require('../commands/general/random');
|
||||||
|
const { getNameById } = require('../utils/discordUtils');
|
||||||
|
|
||||||
|
async function handleCommand(message) {
|
||||||
|
const { channel, content, author, guild } = message;
|
||||||
|
const userId = author.id;
|
||||||
|
const userName = getNameById(message.client, userId, guild);
|
||||||
|
|
||||||
|
const contentWithoutPrefix = content.substring(commandPrefix.length);
|
||||||
|
const args = contentWithoutPrefix.trim().split(/\s+/);
|
||||||
|
const commandName = args[0].toLowerCase();
|
||||||
|
|
||||||
|
if (startDateCommands.includes(commandName)) {
|
||||||
|
logger.info(`Processing !${commandName} from ${userName} (${userId}) in ${guild ? `guild ${guild.name} (${guild.id})` : 'DM'}`);
|
||||||
|
const argStartDate = args[1];
|
||||||
|
const argStartType = args[2];
|
||||||
|
const customUserIdForAdmin = args[3] && adminUserIds.includes(userId) ? args[3] : undefined;
|
||||||
|
const resultEmbed = await setEnlistmentDate(message.client, userId, argStartDate, argStartType, `!${commandName}`, customUserIdForAdmin);
|
||||||
|
await channel.send({ embeds: [resultEmbed] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDateCommands.includes(commandName)) {
|
||||||
|
logger.info(`Processing !${commandName} from ${userName} (${userId}) in ${guild ? `guild ${guild.name} (${guild.id})` : 'DM'}`);
|
||||||
|
let targetUserIdForCmd = userId;
|
||||||
|
let decimalArgForCmd = 4;
|
||||||
|
let decimalArgIndex = 1;
|
||||||
|
|
||||||
|
if (args[1]) {
|
||||||
|
const mentionMatch = args[1].match(/^<@!?(\d+)>$/);
|
||||||
|
if (mentionMatch) {
|
||||||
|
targetUserIdForCmd = mentionMatch[1];
|
||||||
|
decimalArgIndex = 2;
|
||||||
|
} else if (/^\d{17,19}$/.test(args[1])) {
|
||||||
|
targetUserIdForCmd = args[1];
|
||||||
|
decimalArgIndex = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[decimalArgIndex] && !isNaN(parseInt(args[decimalArgIndex]))) {
|
||||||
|
decimalArgForCmd = parseInt(args[decimalArgIndex]);
|
||||||
|
} else if (decimalArgIndex === 1 && args[1] && !isNaN(parseInt(args[1]))) {
|
||||||
|
decimalArgForCmd = parseInt(args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUserNameForCmd = getNameById(message.client, targetUserIdForCmd, guild);
|
||||||
|
const resultEmbed = getDischargeInfo(message.client, targetUserIdForCmd, targetUserNameForCmd, decimalArgForCmd, false);
|
||||||
|
await channel.send({ embeds: [resultEmbed] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (randomCommands.includes(commandName)) {
|
||||||
|
await handleRandom(message, args.slice(1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diceCommands.includes(commandName)) {
|
||||||
|
await handleRandom(message, ['1', '6']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandName === 'gicode') {
|
||||||
|
await handleGiftCode(message, 'genshin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandName === 'hsrcode') {
|
||||||
|
await handleGiftCode(message, 'hsr');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandName === 'eval') {
|
||||||
|
const evalCode = message.content.substring(commandPrefix.length + commandName.length).trim();
|
||||||
|
await handleEval(message, evalCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandName === 'test') {
|
||||||
|
await handleTest(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteCommands.includes(commandName)) {
|
||||||
|
await handleDelete(message.client, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleCommand };
|
107
src/handlers/koreanbotsHandler.js
Normal file
107
src/handlers/koreanbotsHandler.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
|
||||||
|
const configPath = path.join(__dirname, '../../config/koreanbotsConfig.json');
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
try {
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf8');
|
||||||
|
return JSON.parse(configData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error reading or parsing koreanbotsConfig.json: ${error.message}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const backupPath = `${configPath}.${Date.now()}.bak`;
|
||||||
|
try {
|
||||||
|
fs.renameSync(configPath, backupPath);
|
||||||
|
logger.info(`Backed up corrupted koreanbotsConfig.json to ${backupPath}`);
|
||||||
|
} catch (renameError) {
|
||||||
|
logger.error(`Failed to backup koreanbotsConfig.json: ${renameError.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
"ENABLED": true,
|
||||||
|
"KOREANBOTS_TOKEN": "your-koreanbots-token-here",
|
||||||
|
"LOG_UPDATES": true,
|
||||||
|
"UPDATE_INTERVAL_SECONDS": 300
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||||
|
logger.info('Created a new koreanbotsConfig.json with default values.');
|
||||||
|
return defaultConfig;
|
||||||
|
} catch (writeError) {
|
||||||
|
logger.error(`Failed to create new koreanbotsConfig.json: ${writeError.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeKoreanbotsUpdate(client) {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
if (!config || !config.ENABLED) {
|
||||||
|
if (config && config.LOG_UPDATES) {
|
||||||
|
logger.info('Koreanbots update is disabled.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
const serverCount = client.guilds.cache.size;
|
||||||
|
const postData = JSON.stringify({ servers: serverCount });
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: 'koreanbots.dev',
|
||||||
|
path: `/api/v2/bots/${process.env.CLIENT_ID}/stats`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': config.KOREANBOTS_TOKEN,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(postData),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
if (config.LOG_UPDATES) {
|
||||||
|
let logMessage = `Koreanbots API response: ${res.statusCode}`;
|
||||||
|
try {
|
||||||
|
const responseBody = JSON.parse(data);
|
||||||
|
if (responseBody.message) {
|
||||||
|
logMessage += ` - ${responseBody.message}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not a json response, do nothing
|
||||||
|
}
|
||||||
|
logger.info(logMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (e) => {
|
||||||
|
if (config.LOG_UPDATES) {
|
||||||
|
logger.error(`Koreanbots API request error: ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(postData);
|
||||||
|
req.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = (config.UPDATE_INTERVAL_SECONDS || 300) * 1000;
|
||||||
|
|
||||||
|
setInterval(update, interval);
|
||||||
|
logger.info(`Koreanbots update is enabled and will run every ${interval / 1000} seconds.`);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initializeKoreanbotsUpdate };
|
31
src/handlers/processHandlers.js
Normal file
31
src/handlers/processHandlers.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const { handleFatalError } = require('../../utils/errorHandler');
|
||||||
|
const { saveData } = require('../data/dataManager');
|
||||||
|
|
||||||
|
function setupProcessHandlers(client) {
|
||||||
|
async function gracefulShutdown(signal) {
|
||||||
|
logger.info(`${signal} signal received. Shutting down gracefully...`);
|
||||||
|
try {
|
||||||
|
await client.destroy();
|
||||||
|
await saveData('history');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error during ${signal} shutdown: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
handleFatalError(error, 'Uncaught Exception', client, () => saveData('history'));
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
||||||
|
handleFatalError(error, 'Unhandled Rejection', client, () => saveData('history'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setupProcessHandlers };
|
104
src/handlers/rpcHandler.js
Normal file
104
src/handlers/rpcHandler.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const { ActivityType } = require('discord.js');
|
||||||
|
|
||||||
|
const configPath = path.join(__dirname, '../../config/rpcConfig.json');
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
try {
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf8');
|
||||||
|
return JSON.parse(configData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error reading or parsing rpcConfig.json: ${error.message}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const backupPath = `${configPath}.${Date.now()}.bak`;
|
||||||
|
try {
|
||||||
|
fs.renameSync(configPath, backupPath);
|
||||||
|
logger.info(`Backed up corrupted rpcConfig.json to ${backupPath}`);
|
||||||
|
} catch (renameError) {
|
||||||
|
logger.error(`Failed to backup rpcConfig.json: ${renameError.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
"RPC_ENABLED": true,
|
||||||
|
"RPC_INTERVAL_SECONDS": 60,
|
||||||
|
"LOG_RPC_CHANGES": true,
|
||||||
|
"RANDOMIZE_RPC": true,
|
||||||
|
"activities": [
|
||||||
|
{ "name": "전역일 계산", "type": "Playing" },
|
||||||
|
{ "name": "마음의 편지", "type": "Watching" },
|
||||||
|
{ "name": "국군도수체조", "type": "Playing" },
|
||||||
|
{ "name": "이등병의 편지", "type": "Listening" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||||
|
logger.info('Created a new rpcConfig.json with default values.');
|
||||||
|
return defaultConfig;
|
||||||
|
} catch (writeError) {
|
||||||
|
logger.error(`Failed to create new rpcConfig.json: ${writeError.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeRPC(client) {
|
||||||
|
const rpcConfig = getConfig();
|
||||||
|
|
||||||
|
if (!rpcConfig || !rpcConfig.RPC_ENABLED) {
|
||||||
|
logger.info('RPC is disabled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = rpcConfig.activities;
|
||||||
|
if (!activities || activities.length === 0) {
|
||||||
|
logger.warn('RPC is enabled, but no activities are defined in rpcConfig.json.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let activityIndex = 0;
|
||||||
|
|
||||||
|
const updateActivity = () => {
|
||||||
|
if (rpcConfig.RANDOMIZE_RPC) {
|
||||||
|
activityIndex = Math.floor(Math.random() * activities.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = activities[activityIndex];
|
||||||
|
if (!activity || !activity.name || !activity.type) {
|
||||||
|
logger.warn(`Invalid activity at index ${activityIndex}. Skipping.`);
|
||||||
|
if (!rpcConfig.RANDOMIZE_RPC) {
|
||||||
|
activityIndex = (activityIndex + 1) % activities.length;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.user.setPresence({
|
||||||
|
activities: [{ name: activity.name, type: ActivityType[activity.type] }],
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rpcConfig.LOG_RPC_CHANGES) {
|
||||||
|
logger.info(`Activity changed to: ${activity.type} ${activity.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rpcConfig.RANDOMIZE_RPC) {
|
||||||
|
activityIndex = (activityIndex + 1) % activities.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateActivity();
|
||||||
|
|
||||||
|
const intervalSeconds = rpcConfig.RPC_INTERVAL_SECONDS || 60;
|
||||||
|
const intervalMilliseconds = intervalSeconds * 1000;
|
||||||
|
|
||||||
|
|
||||||
|
setInterval(updateActivity, intervalMilliseconds);
|
||||||
|
logger.info(`RPC is enabled and will rotate through activities every ${intervalSeconds} seconds.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initializeRPC };
|
46
src/handlers/slashCommandHandler.js
Normal file
46
src/handlers/slashCommandHandler.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const { Collection } = require('discord.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
const { handleCommandError } = require('../../utils/errorHandler');
|
||||||
|
|
||||||
|
function loadCommands() {
|
||||||
|
const commands = new Collection();
|
||||||
|
const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands'));
|
||||||
|
|
||||||
|
for (const folder of commandFolders) {
|
||||||
|
const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js'));
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
try {
|
||||||
|
const command = require(path.join(__dirname, '..', 'commands', folder, file));
|
||||||
|
if (command.data && command.execute) {
|
||||||
|
commands.set(command.data.name, command);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Command file ${file} is missing a data or execute export.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error loading command from ${file}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSlashCommand(interaction) {
|
||||||
|
const command = interaction.client.commands.get(interaction.commandName);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
logger.warn(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await command.execute(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error executing ${interaction.commandName}`);
|
||||||
|
logger.error(error);
|
||||||
|
await handleCommandError(error, interaction, interaction.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { onSlashCommand, loadCommands };
|
60
src/utils/dateUtils.js
Normal file
60
src/utils/dateUtils.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
function addMonthsToDate(dateString, monthsToAdd) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
date.setMonth(date.getMonth() + monthsToAdd);
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
return formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateFormatter(dateString) {
|
||||||
|
if (!dateString || !/^\d{4}-\d{2}-\d{2}$/.test(dateString)) return "날짜 형식 오류";
|
||||||
|
const [year, month, day] = dateString.split('-');
|
||||||
|
return `${year}년 ${month}월 ${day}일`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDaysFromNow(dateString, now = new Date()) {
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
today.setHours(0,0,0,0);
|
||||||
|
const inputDate = new Date(dateString);
|
||||||
|
inputDate.setHours(0,0,0,0);
|
||||||
|
return Math.ceil((inputDate.getTime() - today.getTime()) / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDateDifference(date1Str, date2Str) {
|
||||||
|
const d1 = new Date(date1Str);
|
||||||
|
d1.setHours(0, 0, 0, 0);
|
||||||
|
const d2 = new Date(date2Str);
|
||||||
|
d2.setHours(0, 0, 0, 0);
|
||||||
|
return Math.ceil(Math.abs(d2.getTime() - d1.getTime()) / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateProgress(startDateStr, endDateStr, now = new Date()) {
|
||||||
|
const start = new Date(startDateStr);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
const end = new Date(endDateStr);
|
||||||
|
end.setHours(0, 0, 0, 0);
|
||||||
|
const currentTime = now.getTime();
|
||||||
|
if (start.getTime() >= end.getTime() || currentTime >= end.getTime()) return '100.0000000';
|
||||||
|
if (currentTime <= start.getTime()) return 0;
|
||||||
|
return Math.max(0, Math.min(100, ((currentTime - start.getTime()) / (end.getTime() - start.getTime())) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMonthDifference(sYear, sMonth, eYear, eMonth) {
|
||||||
|
return (Number(eYear) - Number(sYear)) * 12 + (Number(eMonth) - Number(sMonth));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addMonthsToDate,
|
||||||
|
dateFormatter,
|
||||||
|
calculateDaysFromNow,
|
||||||
|
calculateDateDifference,
|
||||||
|
calculateProgress,
|
||||||
|
calculateMonthDifference,
|
||||||
|
formatDate
|
||||||
|
};
|
34
src/utils/discordUtils.js
Normal file
34
src/utils/discordUtils.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
function getNameById(client, id, guild = null) {
|
||||||
|
const user = client.users.cache.get(id);
|
||||||
|
if (!user) return `Unknown User (${id})`;
|
||||||
|
if (guild) {
|
||||||
|
const member = guild.members.cache.get(id);
|
||||||
|
if (member) return member.displayName;
|
||||||
|
}
|
||||||
|
return user.displayName || user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgressBar(progress, totalLength, toFixedValue) {
|
||||||
|
progress = Math.max(0, Math.min(100, parseFloat(progress)));
|
||||||
|
toFixedValue = Number.isInteger(toFixedValue) && toFixedValue >= 0 ? toFixedValue : 4;
|
||||||
|
const filledLength = Math.round((progress / 100) * totalLength);
|
||||||
|
const filledBar = '█'.repeat(filledLength);
|
||||||
|
const unfilledBar = '░'.repeat(totalLength - filledLength);
|
||||||
|
return `[${filledBar}${unfilledBar}] ${progress.toFixed(toFixedValue)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEmbed(channel, title, description, color = '#007FFF') {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(title)
|
||||||
|
.setDescription(description)
|
||||||
|
.setColor(color);
|
||||||
|
return await channel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getNameById,
|
||||||
|
createProgressBar,
|
||||||
|
sendEmbed
|
||||||
|
};
|
47
src/utils/helpers.js
Normal file
47
src/utils/helpers.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
function binarySearch(arr, id) {
|
||||||
|
let left = 0, right = arr.length - 1;
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
if (arr[mid].id === id) return mid;
|
||||||
|
if (arr[mid].id < id) left = mid + 1;
|
||||||
|
else right = mid - 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function binaryInsert(arr, item) {
|
||||||
|
let left = 0, right = arr.length;
|
||||||
|
while (left < right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
if (arr[mid].id < item.id) left = mid + 1;
|
||||||
|
else right = mid;
|
||||||
|
}
|
||||||
|
arr.splice(left, 0, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSpace(num) {
|
||||||
|
return num > 0 ? ' '.repeat(num) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomNumber(min, max) {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(cmdStr) {
|
||||||
|
const args = [];
|
||||||
|
const matches = cmdStr.match(/(".*?"|'.*?'|[^"\s'|]+)+(?=\s*|\s*$)/g) || [];
|
||||||
|
for (const match of matches) {
|
||||||
|
args.push(match.replace(/^["']|["']$/g, ''));
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
binarySearch,
|
||||||
|
binaryInsert,
|
||||||
|
addSpace,
|
||||||
|
generateRandomNumber,
|
||||||
|
parseArgs
|
||||||
|
};
|
101
utils/errorHandler.js
Normal file
101
utils/errorHandler.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
require('dotenv').config();
|
||||||
|
const logger = require('../../modules/colorfulLogger');
|
||||||
|
|
||||||
|
const logErrorToConsole = (error, context) => {
|
||||||
|
console.error(`\n\x1b[31m[${new Date().toISOString()}] \x1b[33m[${context || 'ERROR'}]\x1b[0m`);
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendErrorToDiscord = async (error, client, contextInfo) => {
|
||||||
|
try {
|
||||||
|
const errorLogChannelId = process.env.ERROR_LOG_CHANNEL_ID;
|
||||||
|
if (!errorLogChannelId || !client?.channels) return;
|
||||||
|
|
||||||
|
const errorEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0xFF0000)
|
||||||
|
.setTitle('오류 발생')
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (typeof contextInfo === 'string') {
|
||||||
|
errorEmbed.addFields({ name: '오류 발생 위치', value: contextInfo });
|
||||||
|
} else if (contextInfo?.isInteraction) {
|
||||||
|
const interaction = contextInfo;
|
||||||
|
errorEmbed.setTitle('명령어 인터랙션 오류');
|
||||||
|
errorEmbed.addFields(
|
||||||
|
{ name: '명령어', value: `\`/${interaction.commandName}\``, inline: true },
|
||||||
|
{ name: '사용자', value: `${interaction.user.tag} (\`${interaction.user.id}\`)`, inline: true },
|
||||||
|
{ name: '서버/채널', value: `${interaction.guild.name} / #${interaction.channel.name}`, inline: false }
|
||||||
|
);
|
||||||
|
} else if (contextInfo?.author) {
|
||||||
|
const message = contextInfo;
|
||||||
|
errorEmbed.setTitle('메시지 처리 오류');
|
||||||
|
errorEmbed.addFields(
|
||||||
|
{ name: '메시지 내용', value: message.content.substring(0, 200) },
|
||||||
|
{ name: '사용자', value: `${message.author.tag} (\`${message.author.id}\`)`, inline: true },
|
||||||
|
{ name: '서버/채널', value: `${message.guild.name} / #${message.channel.name}`, inline: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEmbed.addFields(
|
||||||
|
{ name: '오류 메시지', value: `\`\`\`${error.message}\`\`\`` },
|
||||||
|
{ name: 'Stack Trace', value: `\`\`\`javascript\n${error.stack.substring(0, 1000)}\`\`\`` }
|
||||||
|
);
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(errorLogChannelId).catch(() => null);
|
||||||
|
if (channel?.isTextBased()) {
|
||||||
|
await channel.send({ embeds: [errorEmbed] });
|
||||||
|
}
|
||||||
|
} catch (discordError) {
|
||||||
|
console.error('\n\x1b[1m\x1b[31m[FATAL] Discord로 오류 로그 전송에 실패했습니다.\x1b[0m');
|
||||||
|
logErrorToConsole(discordError, 'DISCORD LOG SEND FAILED');
|
||||||
|
logErrorToConsole(error, 'ORIGINAL ERROR');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommandError = async (error, context, client) => {
|
||||||
|
logErrorToConsole(error, context?.commandName || context?.content || 'Command Execution');
|
||||||
|
await sendErrorToDiscord(error, client, context);
|
||||||
|
|
||||||
|
if (context?.isInteraction) {
|
||||||
|
const interaction = context;
|
||||||
|
const reply = {
|
||||||
|
content: '명령어를 실행하는 중 오류가 발생했습니다.',
|
||||||
|
embeds: [],
|
||||||
|
ephemeral: true,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
await interaction.followUp(reply);
|
||||||
|
} else {
|
||||||
|
await interaction.reply(reply);
|
||||||
|
}
|
||||||
|
} catch (replyError) {
|
||||||
|
logErrorToConsole(replyError, 'COMMAND ERROR REPLY FAILED');
|
||||||
|
}
|
||||||
|
} else if (context?.channel) {
|
||||||
|
try {
|
||||||
|
await context.channel.send({ content: '명령어를 처리하는 중 오류가 발생했습니다.' });
|
||||||
|
} catch (replyError) {
|
||||||
|
logErrorToConsole(replyError, 'COMMAND ERROR REPLY FAILED');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFatalError = async (error, context, client, shutdownLogic) => {
|
||||||
|
logErrorToConsole(error, context);
|
||||||
|
await sendErrorToDiscord(error, client, context);
|
||||||
|
|
||||||
|
if (typeof shutdownLogic === 'function') {
|
||||||
|
logger.error('Fatal error detected. Performing shutdown logic before exit...');
|
||||||
|
await shutdownLogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error('치명적인 오류로 인해 프로세스를 종료합니다.');
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleCommandError,
|
||||||
|
handleFatalError,
|
||||||
|
};
|
Reference in New Issue
Block a user