1079 lines
52 KiB
JavaScript
Executable File
1079 lines
52 KiB
JavaScript
Executable File
const { Client, SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionsBitField } = require('discord.js');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const logger = require('./modules/colorfulLogger'); // 사용자가 제공한 로거 모듈
|
|
|
|
// Constants
|
|
const defaultDirectory = path.join('/home', 'yeongaori', 'DiscordGaori', '조교가오리');
|
|
const dataDir = path.join(defaultDirectory, 'data'); // 데이터 디렉토리 경로
|
|
const testDataDirectory = path.join(dataDir, 'testData.txt'); // 원본 경로 유지 (현재 코드에선 직접 사용 안함)
|
|
const ipdaeDataDirectory = path.join(dataDir, 'ipdaeData.json');
|
|
const celebrationConfigPath = path.join(dataDir, 'celebrationConfig.json');
|
|
const notificationHistoryPath = path.join(dataDir, 'notificationHistory.json'); // 알림 이력 파일 경로
|
|
const commandPrefix = "!";
|
|
|
|
// ... 기존 require 및 상수, 전역 변수 선언 이후 ...
|
|
|
|
// 안전한 종료를 위한 함수
|
|
async function saveNotificationHistoryBeforeExit() {
|
|
logger.info('Attempting to save notification history before exit...');
|
|
try {
|
|
// 현재 메모리의 notificationHistory 객체를 파일에 덮어쓰기
|
|
await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8');
|
|
logger.info('notificationHistory.json saved successfully on exit.');
|
|
} catch (e) {
|
|
logger.error(`Error saving notificationHistory.json on exit: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// 프로세스 종료 신호 감지 (Ctrl+C 등)
|
|
process.on('SIGINT', async () => {
|
|
logger.info('SIGINT signal received. Shutting down gracefully...');
|
|
await client.destroy(); // Discord 클라이언트 연결 종료
|
|
await saveNotificationHistoryBeforeExit(); // 알림 이력 저장
|
|
process.exit(0); // 정상 종료
|
|
});
|
|
|
|
// 프로세스 종료 신호 감지 (kill 명령어 등)
|
|
process.on('SIGTERM', async () => {
|
|
logger.info('SIGTERM signal received. Shutting down gracefully...');
|
|
await client.destroy(); // Discord 클라이언트 연결 종료
|
|
await saveNotificationHistoryBeforeExit(); // 알림 이력 저장
|
|
process.exit(0); // 정상 종료
|
|
});
|
|
|
|
// 처리되지 않은 예외 발생 시 (최후의 수단)
|
|
process.on('uncaughtException', async (error, origin) => {
|
|
logger.error('!!!!!!!!!! UNCAUGHT EXCEPTION !!!!!!!!!!!');
|
|
logger.error(`Error: ${error.message}`);
|
|
logger.error(`Origin: ${origin}`);
|
|
logger.error(error.stack);
|
|
// 이 시점에서는 프로그램 상태가 불안정할 수 있으므로, 저장은 최선책일 뿐 보장되지 않음
|
|
// await saveNotificationHistoryBeforeExit();
|
|
process.exit(1); // 오류와 함께 종료
|
|
});
|
|
|
|
// 처리되지 않은 프로미스 거부 발생 시
|
|
process.on('unhandledRejection', async (reason, promise) => {
|
|
logger.error('!!!!!!!!!! UNHANDLED REJECTION !!!!!!!!!!!');
|
|
logger.error(`Promise: ${promise}`);
|
|
logger.error(`Reason: ${reason instanceof Error ? reason.message : reason}`);
|
|
if (reason instanceof Error) {
|
|
logger.error(reason.stack);
|
|
}
|
|
// await saveNotificationHistoryBeforeExit();
|
|
// process.exit(1); // 오류와 함께 종료 (선택 사항)
|
|
});
|
|
|
|
// ... (기존의 IIFE 데이터 로딩 로직 및 나머지 코드 시작) ...
|
|
|
|
// Client setup
|
|
const client = new Client({ intents: ['Guilds', 'GuildMessages', 'MessageContent'] });
|
|
let ipdaeDatas = []; // 사용자 입대 정보 { id, date, type }
|
|
let celebrationConfig = {}; // 축하 메시지 설정
|
|
let notificationHistory = {}; // 사용자별 알림 받은 기념일 목록 { userId: [milestone1, milestone2], ... }
|
|
|
|
// 데이터 디렉토리 존재 확인 및 생성 (봇 시작 시 실행)
|
|
(async () => {
|
|
try {
|
|
await fs.mkdir(dataDir, { recursive: true });
|
|
logger.info(`Data directory ensured: ${dataDir}`);
|
|
} catch (e) {
|
|
logger.error(`Failed to create data directory ${dataDir}: ${e.message}`);
|
|
}
|
|
})();
|
|
|
|
// 입대 데이터, 알림 이력, 축하 설정 데이터 로드 및 마이그레이션 (봇 시작 시 실행)
|
|
(async () => {
|
|
let ipdaeDataNeedsSaveAfterMigration = false;
|
|
let notificationHistoryNeedsSaveAfterMigration = false;
|
|
|
|
// 1. ipdaeData.json 로드
|
|
try {
|
|
const data = await fs.readFile(ipdaeDataDirectory, 'utf8');
|
|
ipdaeDatas = JSON.parse(data);
|
|
ipdaeDatas.sort((a, b) => a.id.localeCompare(b.id)); // ID 기준으로 정렬
|
|
logger.info('ipdaeData.json loaded and parsed.');
|
|
} catch (e) {
|
|
logger.warn(`Failed to load ipdaeData.json: ${e.message}. Initializing as empty array.`);
|
|
ipdaeDatas = [];
|
|
if (e.code === 'ENOENT') { // 파일이 없을 경우
|
|
try {
|
|
await fs.writeFile(ipdaeDataDirectory, JSON.stringify([], null, 4), 'utf8');
|
|
logger.info('Created empty ipdaeData.json.');
|
|
} catch (writeError) {
|
|
logger.error(`Failed to create empty ipdaeData.json: ${writeError.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. notificationHistory.json 로드
|
|
try {
|
|
const historyData = await fs.readFile(notificationHistoryPath, 'utf8');
|
|
notificationHistory = JSON.parse(historyData);
|
|
logger.info('notificationHistory.json loaded and parsed.');
|
|
} catch (e) {
|
|
logger.warn(`notificationHistory.json not found or invalid: ${e.message}. Initializing as empty object.`);
|
|
notificationHistory = {};
|
|
if (e.code === 'ENOENT') { // 파일이 없을 경우
|
|
try {
|
|
await fs.writeFile(notificationHistoryPath, JSON.stringify({}, null, 4), 'utf8');
|
|
logger.info('Created empty notificationHistory.json.');
|
|
} catch (writeError) {
|
|
logger.error(`Failed to create empty notificationHistory.json: ${writeError.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. 마이그레이션: ipdaeData.json의 notifiedMilestones -> notificationHistory.json
|
|
// 이 로직은 ipdaeData.json에 notifiedMilestones 필드가 더 이상 없을 때까지 실행될 수 있음
|
|
for (let i = 0; i < ipdaeDatas.length; i++) {
|
|
const user = ipdaeDatas[i];
|
|
if (user.hasOwnProperty('notifiedMilestones')) { // 이전 버전 필드 존재 여부 확인
|
|
if (Array.isArray(user.notifiedMilestones) && user.notifiedMilestones.length > 0) {
|
|
// notificationHistory에 해당 유저의 데이터가 없거나 비어있을 경우에만 마이그레이션
|
|
if (!notificationHistory[user.id] || Object.keys(notificationHistory[user.id]).length === 0) {
|
|
notificationHistory[user.id] = [...user.notifiedMilestones]; // 배열 복사
|
|
notificationHistoryNeedsSaveAfterMigration = true;
|
|
logger.info(`Migrated notifiedMilestones for user ${user.id} to notificationHistory.`);
|
|
} else {
|
|
logger.info(`User ${user.id} already has data in notificationHistory.json. Old notifiedMilestones in ipdaeData.json will be removed without merging.`);
|
|
}
|
|
}
|
|
delete user.notifiedMilestones; // ipdaeData 객체에서 필드 삭제
|
|
ipdaeDataNeedsSaveAfterMigration = true;
|
|
}
|
|
}
|
|
|
|
|
|
if (ipdaeDataNeedsSaveAfterMigration) {
|
|
try {
|
|
await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeDatas, null, 4), 'utf8');
|
|
logger.info('ipdaeData.json updated after migrating/removing notifiedMilestones field.');
|
|
} catch (e) {
|
|
logger.error(`Failed to save ipdaeData.json during migration: ${e.message}`);
|
|
}
|
|
}
|
|
if (notificationHistoryNeedsSaveAfterMigration) {
|
|
try {
|
|
await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8');
|
|
logger.info('notificationHistory.json saved after migration.');
|
|
} catch (e) {
|
|
logger.error(`Failed to save notificationHistory.json during migration: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// 4. celebrationConfig.json 로드
|
|
try {
|
|
const configData = await fs.readFile(celebrationConfigPath, 'utf8');
|
|
celebrationConfig = JSON.parse(configData);
|
|
// 이전 버전 호환: 멘션 관련 필드가 없는 경우 기본값 설정
|
|
if (celebrationConfig.milestones && Array.isArray(celebrationConfig.milestones)) {
|
|
celebrationConfig.milestones.forEach(m => {
|
|
if (typeof m.mentionUser === 'undefined') m.mentionUser = true;
|
|
if (typeof m.mentionEveryone === 'undefined') m.mentionEveryone = false;
|
|
});
|
|
} else { // milestones 필드가 없거나 배열이 아니면 기본 설정으로 초기화
|
|
throw new Error("Milestones data is missing or invalid in celebrationConfig.json");
|
|
}
|
|
logger.info('celebrationConfig.json loaded and parsed.');
|
|
} catch (e) {
|
|
logger.warn(`celebrationConfig.json error: ${e.message}. Creating/Using default config.`);
|
|
celebrationConfig = {
|
|
channelTopicKeyword: "전역축하알림",
|
|
milestones: [
|
|
{ days: 100, message: "🎉 {userName}님, 전역까지 D-100일! 영광의 그날까지 파이팅입니다! 🎉", mentionUser: true, mentionEveryone: false },
|
|
{ days: 50, message: "🥳 {userName}님, 전역까지 D-50일! 이제 절반의 고지를 넘었습니다! 🥳", mentionUser: true, mentionEveryone: false },
|
|
{ days: 30, message: "🗓️ {userName}님, 전역까지 D-30일! 한 달 뒤면 자유의 몸! 🗓️", mentionUser: true, mentionEveryone: false },
|
|
{ days: 7, message: "✨ {userName}님, 전역까지 D-7일! 일주일만 버티면 됩니다! ✨", mentionUser: true, mentionEveryone: false },
|
|
{ days: 1, message: "🚀 {userName}님, 드디어 내일 전역입니다! 사회로 나갈 준비 되셨나요? 🚀", mentionUser: true, mentionEveryone: false },
|
|
{ days: 0, message: "🫡 {userName}님, 🎉축 전역🎉 대한민국의 자랑스러운 아들! 그동안 정말 고생 많으셨습니다! 🫡", mentionUser: true, mentionEveryone: true }
|
|
]
|
|
};
|
|
try {
|
|
await fs.writeFile(celebrationConfigPath, JSON.stringify(celebrationConfig, null, 4), 'utf8');
|
|
logger.info(`Default celebrationConfig.json created/used at ${celebrationConfigPath}`);
|
|
} catch (saveError) {
|
|
logger.error(`Failed to save default celebrationConfig.json: ${saveError.message}`);
|
|
}
|
|
}
|
|
})();
|
|
|
|
client.once('ready', async readyClient => {
|
|
logger.info(`Logged in as ${readyClient.user.tag}`);
|
|
await registerSlashCommands();
|
|
logger.info('Performing initial celebration check on startup...');
|
|
await checkAndSendCelebrationMessages();
|
|
scheduleDailyCelebrationCheck();
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
async function resetSlashCommands() {
|
|
try {
|
|
await client.application.commands.set([]);
|
|
logger.info('Successfully reset all slash commands.');
|
|
} catch (error) {
|
|
logger.error(`Failed to reset slash commands: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async function registerSlashCommands() {
|
|
const commands = [
|
|
new SlashCommandBuilder()
|
|
.setName('전역일')
|
|
.setDescription('전역일과 관련된 정보들을 확인합니다')
|
|
.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)),
|
|
new SlashCommandBuilder()
|
|
.setName('입대일')
|
|
.setDescription('입대일을 설정합니다')
|
|
.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: '사회복무요원' }
|
|
))
|
|
];
|
|
try {
|
|
await client.application.commands.set(commands);
|
|
logger.info('Successfully registered slash commands.');
|
|
} catch (error) {
|
|
logger.error(`Failed to register slash commands: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
client.on('interactionCreate', async interaction => {
|
|
if (!interaction.isCommand()) return;
|
|
await onSlashCommandCreate(interaction);
|
|
});
|
|
|
|
client.on('interactionCreate', async interaction => {
|
|
if (!interaction.isButton()) return;
|
|
await onMessageComponentCreate(interaction);
|
|
});
|
|
|
|
client.on('messageCreate', async message => {
|
|
if (message.author.bot) return;
|
|
await onMessageCreate(message);
|
|
});
|
|
|
|
const loadingEmbed = new EmbedBuilder()
|
|
.setTitle('데이터 로드 중...')
|
|
.setDescription('데이터를 불러오는 중입니다. 잠시만 기다려 주세요.')
|
|
.setColor('#FDFD96');
|
|
|
|
async function onSlashCommandCreate(interaction) {
|
|
let currentCommand = 'onSlashCommandCreateEvent';
|
|
const { commandName, user, options, guild } = interaction;
|
|
const userId = user.id;
|
|
const userName = getNameById(userId, guild);
|
|
|
|
try {
|
|
if (commandName === '입대일') {
|
|
currentCommand = 'startDateSlashCommand';
|
|
const slashStartDateArg = options.getString('입대일');
|
|
const slashStartTypeArg = options.getString('복무형태');
|
|
|
|
logger.info(`Processing /입대일 for ${userName} (${userId}) with date: ${slashStartDateArg}, type: ${slashStartTypeArg}`);
|
|
|
|
await interaction.deferReply();
|
|
const resultEmbed = await getStartDateEmbed(userId, slashStartDateArg, slashStartTypeArg, '/입대일', undefined);
|
|
await interaction.editReply({ embeds: [resultEmbed] });
|
|
}
|
|
if (commandName === '전역일') {
|
|
currentCommand = 'endDateSlashCommand';
|
|
let decimalArg = options.getInteger('소수점') || 4;
|
|
let targetUserOption = options.getUser('유저');
|
|
let targetUserId = targetUserOption ? targetUserOption.id : userId;
|
|
const fullInfoArg = options.getBoolean('상세내용') || false;
|
|
|
|
const targetUserName = getNameById(targetUserId, guild);
|
|
|
|
logger.info(`Processing /전역일 for ${targetUserName} (${targetUserId}), requested by ${userName} (${userId})`);
|
|
|
|
await interaction.deferReply();
|
|
const resultEmbed = getEndDateEmbed(targetUserId, targetUserName, decimalArg, fullInfoArg);
|
|
await interaction.editReply({ embeds: [resultEmbed] });
|
|
}
|
|
} catch (e) {
|
|
logger.error(`Error in ${currentCommand} for ${userName} (${userId}): ${e.message} \nStack: ${e.stack}`);
|
|
const embed = new EmbedBuilder()
|
|
.setTitle(`${currentCommand} 실행 중 문제가 발생했습니다`)
|
|
.addFields({ name: '에러 메시지', value: e.message })
|
|
.setColor('#F44336');
|
|
if (interaction.replied || interaction.deferred) {
|
|
await interaction.editReply({ embeds: [embed], ephemeral: true });
|
|
} else {
|
|
try {
|
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
} catch (replyError) {
|
|
logger.error(`Failed to send error reply for ${currentCommand}: ${replyError.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function onMessageCreate(event) {
|
|
let currentCommand = 'onMessageCreateEvent';
|
|
const { channel, content, author, guild } = event;
|
|
const userId = author.id;
|
|
const userName = getNameById(userId, guild);
|
|
|
|
async function send(data) {
|
|
if (typeof data === 'string') {
|
|
return channel.send(data);
|
|
}
|
|
return channel.send({ embeds: [data] });
|
|
}
|
|
|
|
try {
|
|
if (!content.startsWith(commandPrefix)) return;
|
|
const message = content.substring(1);
|
|
const args = message.split(' ');
|
|
const commandNameFromMessage = args[0].toLowerCase();
|
|
|
|
const startDateCommands = ['입대일', 'ㅇㄷㅇ', '소집일', 'ㅅㅈㅇ'];
|
|
if (startDateCommands.includes(commandNameFromMessage)) {
|
|
currentCommand = commandPrefix + commandNameFromMessage;
|
|
logger.info(`Processing ${currentCommand} from ${userName} (${userId}) with args: ${args.slice(1).join(' ')}`);
|
|
const argStartDate = args[1];
|
|
const argStartType = args[2];
|
|
const customUserIdForAdmin = args[3] && userId === '602447697047191562' ? args[3] : undefined;
|
|
const resultEmbed = await getStartDateEmbed(userId, argStartDate, argStartType, currentCommand, customUserIdForAdmin);
|
|
await send(resultEmbed);
|
|
return;
|
|
}
|
|
|
|
const endDateCommands = ['전역일', 'ㅈㅇㅇ', '소집해제일', 'ㅅㅈㅎㅈㅇ', '소해일', 'ㅅㅎㅇ'];
|
|
if (endDateCommands.includes(commandNameFromMessage)) {
|
|
currentCommand = commandPrefix + commandNameFromMessage;
|
|
logger.info(`Processing ${currentCommand} from ${userName} (${userId}) with args: ${args.slice(1).join(' ')}`);
|
|
|
|
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(targetUserIdForCmd, guild);
|
|
const resultEmbed = getEndDateEmbed(targetUserIdForCmd, targetUserNameForCmd, decimalArgForCmd, false);
|
|
await send(resultEmbed);
|
|
return;
|
|
}
|
|
|
|
const randomCommands = ['랜덤', 'ㄹㄷ', 'random'];
|
|
if (randomCommands.includes(commandNameFromMessage)) {
|
|
currentCommand = commandPrefix + commandNameFromMessage;
|
|
const randomArgs = args.slice(1);
|
|
if (randomArgs.length === 0) {
|
|
await send(String(generateRandomNumber(1, 100)));
|
|
} else if (randomArgs.length === 1) {
|
|
const max = parseInt(randomArgs[0]);
|
|
if (isNaN(max)) {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('유효한 숫자가 아닙니다.').setColor('#F44336')] });
|
|
} else {
|
|
await send(String(generateRandomNumber(1, max)));
|
|
}
|
|
} else if (randomArgs.length === 2) {
|
|
const min = parseInt(randomArgs[0]);
|
|
const max = parseInt(randomArgs[1]);
|
|
if (isNaN(min) || isNaN(max) || min > max) {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('올바르지 않은 형식입니다. min > max 이거나 숫자가 아닙니다.').setColor('#F44336')] });
|
|
} else {
|
|
await send(String(generateRandomNumber(min, max)));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
const diceCommands = ['주사위', 'ㅈㅅㅇ', '다이스', 'ㄷㅇㅅ', 'dice'];
|
|
if (diceCommands.includes(commandNameFromMessage)) {
|
|
currentCommand = commandPrefix + commandNameFromMessage;
|
|
await send(String(generateRandomNumber(1, 6)));
|
|
return;
|
|
}
|
|
|
|
if (message.startsWith('gicode')) {
|
|
currentCommand = commandPrefix + 'gicode';
|
|
const giArgs = parseArgs(message);
|
|
const title = giArgs[1];
|
|
if (giArgs.length < 3) {
|
|
const embed = new EmbedBuilder()
|
|
.setTitle('인수 오류').setDescription('지정되지 않은 인수가 있습니다')
|
|
.addFields({ name: '명령어 형식', value: `${commandPrefix}gicode 제목 코드1 코드2...` })
|
|
.setColor('#F44336');
|
|
await send(embed); return;
|
|
}
|
|
if (userId === '602447697047191562') {
|
|
const buttons = giArgs.slice(2).map(code =>
|
|
new ButtonBuilder().setLabel(code).setStyle(ButtonStyle.Link).setURL(`https://genshin.hoyoverse.com/gift?code=${code}`)
|
|
).slice(0, 5);
|
|
const row = new ActionRowBuilder().addComponents(buttons);
|
|
await channel.send({ content: `# ${title}`, components: [row] });
|
|
if (event.deletable) await event.delete().catch(e => logger.warn(`Failed to delete gicode trigger message: ${e.message}`));
|
|
} else {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('이 명령어를 실행할 권한이 없습니다.').setColor('#F44336')] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.startsWith('hsrcode')) {
|
|
currentCommand = commandPrefix + 'hsrcode';
|
|
const hsrArgs = parseArgs(message);
|
|
const title = hsrArgs[1];
|
|
if (hsrArgs.length < 3) {
|
|
const embed = new EmbedBuilder()
|
|
.setTitle('인수 오류').setDescription('지정되지 않은 인수가 있습니다')
|
|
.addFields({ name: '명령어 형식', value: `${commandPrefix}hsrcode 제목 코드1 코드2...` })
|
|
.setColor('#F44336');
|
|
await send(embed); return;
|
|
}
|
|
if (userId === '602447697047191562') {
|
|
const buttons = hsrArgs.slice(2).map(code =>
|
|
new ButtonBuilder().setLabel(code).setStyle(ButtonStyle.Link).setURL(`https://hsr.hoyoverse.com/gift?code=${code}`)
|
|
).slice(0, 5);
|
|
const row = new ActionRowBuilder().addComponents(buttons);
|
|
await channel.send({ content: `# ${title}`, components: [row] });
|
|
if (event.deletable) await event.delete().catch(e => logger.warn(`Failed to delete hsrcode trigger message: ${e.message}`));
|
|
} else {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('이 명령어를 실행할 권한이 없습니다.').setColor('#F44336')] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.startsWith('eval')) {
|
|
currentCommand = commandPrefix + 'eval';
|
|
if (userId === '602447697047191562') {
|
|
const jsCode = content.slice(content.indexOf('eval') + 'eval'.length).trim();
|
|
if (![' ', '\n'].includes(message.charAt(4)) && message.charAt(4) !== undefined ) {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('잘못된 인수입니다. eval 명령어 다음에는 공백이 필요합니다.').setColor('#F44336')] });
|
|
return;
|
|
}
|
|
if (!jsCode) {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('실행할 코드가 없습니다.').setColor('#F44336')] });
|
|
return;
|
|
}
|
|
try {
|
|
const result = await (async () => {
|
|
// eslint-disable-next-line no-eval
|
|
return await eval(jsCode);
|
|
})();
|
|
await send(`Input:\n\`\`\`javascript\n${jsCode}\n\`\`\`\nOutput:\n\`\`\`\n${String(result)}\n\`\`\``);
|
|
} catch (e) {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('Eval 실행 중 문제가 발생했습니다').addFields({ name: '에러 메시지', value: e.message }).setColor('#F44336')] });
|
|
}
|
|
} else {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('이 명령어를 실행할 권한이 없습니다.').setColor('#F44336')] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (message.startsWith('test')) {
|
|
currentCommand = commandPrefix + 'test';
|
|
if (userId === '602447697047191562') {
|
|
await send('Test command executed.');
|
|
const testArg = args[1] || 'default_test_value';
|
|
const row = new ActionRowBuilder().addComponents(
|
|
new ButtonBuilder().setCustomId(`success_${testArg}`).setLabel('Success!').setStyle(ButtonStyle.Success),
|
|
new ButtonBuilder().setCustomId(`danger_${testArg}`).setLabel('Danger...').setStyle(ButtonStyle.Danger),
|
|
new ButtonBuilder().setCustomId(`secondary_${testArg}`).setLabel('Secondary Option').setStyle(ButtonStyle.Secondary),
|
|
new ButtonBuilder().setLabel('Test Link').setStyle(ButtonStyle.Link).setURL('https://www.google.com/')
|
|
);
|
|
try {
|
|
const imagePath = path.join(defaultDirectory, '1.jpg');
|
|
await fs.access(imagePath);
|
|
await channel.send({
|
|
content: `Text for MessageBuilder: ${testArg}`,
|
|
files: [imagePath],
|
|
components: [row]
|
|
});
|
|
} catch (fileError) {
|
|
logger.warn(`Test command: File 1.jpg not found or unreadable. Error: ${fileError.message}`);
|
|
await channel.send({
|
|
content: `Text for MessageBuilder (file 1.jpg not found): ${testArg}`,
|
|
components: [row]
|
|
});
|
|
}
|
|
await send('Test command finished executing.');
|
|
} else {
|
|
await send({ embeds: [new EmbedBuilder().setTitle('이 명령어를 실행할 권한이 없습니다.').setColor('#F44336')] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
} catch (e) {
|
|
logger.error(`Error in ${currentCommand} for ${userName} (${userId}): ${e.message} \nStack: ${e.stack}`);
|
|
await send({ embeds: [new EmbedBuilder().setTitle(`${currentCommand} 실행 중 문제가 발생했습니다`).addFields({ name: '에러 메시지', value: e.message }).setColor('#F44336')] });
|
|
}
|
|
}
|
|
|
|
async function onMessageComponentCreate(interaction) {
|
|
const { customId, user, guild } = interaction;
|
|
const userName = getNameById(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');
|
|
|
|
try {
|
|
await interaction.reply({ embeds: [resultEmbed], ephemeral: true });
|
|
} catch (e) {
|
|
logger.error(`Failed to reply to button interaction ${customId}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
async function getStartDateEmbed(callerUserId, dateArg, typeArg, commandString, customTargetUserId) {
|
|
let isSaved = false;
|
|
let notificationHistoryModified = false; // 이 함수 내에서 notificationHistory 변경 여부
|
|
dateArg = String(dateArg || '').trim();
|
|
typeArg = String(typeArg || '').trim();
|
|
let startDateEmbedTitle = '';
|
|
let targetUserId = callerUserId;
|
|
|
|
if (customTargetUserId && callerUserId === '602447697047191562') {
|
|
targetUserId = String(customTargetUserId.replace(/\D/g, ''));
|
|
const targetUserName = getNameById(targetUserId);
|
|
startDateEmbedTitle = `${targetUserName}(${targetUserId}) 님의 입대일이 저장되었습니다`;
|
|
} else {
|
|
startDateEmbedTitle = '입대일이 저장되었습니다';
|
|
}
|
|
|
|
if (!(/^\d{4}-\d{2}-\d{2}$/.test(dateArg) && ['육군', '해군', '공군', '해병대', '사회복무요원'].includes(typeArg))) {
|
|
return new EmbedBuilder()
|
|
.setTitle('명령어 형식 오류')
|
|
.setDescription(`입력된 날짜: \`${dateArg}\`, 복무형태: \`${typeArg}\``)
|
|
.addFields({ name: '올바른 형식', value: `${commandString} YYYY-MM-DD 육군/해군/공군/해병대/사회복무요원` })
|
|
.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');
|
|
}
|
|
|
|
const index = binarySearch(ipdaeDatas, targetUserId);
|
|
if (index !== -1) { // 기존 사용자 정보 수정
|
|
isSaved = true;
|
|
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;
|
|
startDateEmbedTitle = startDateEmbedTitle.replace('저장', '수정');
|
|
|
|
// 입대일 또는 복무형태가 변경된 경우, 해당 사용자의 알림 이력 초기화
|
|
if (oldDate !== dateArg || oldType !== typeArg) {
|
|
logger.info(`Enlistment data changed for ${targetUserId}. Resetting their notification history.`);
|
|
notificationHistory[targetUserId] = [];
|
|
notificationHistoryModified = true;
|
|
}
|
|
} else { // 신규 사용자 정보 추가
|
|
const newData = { id: targetUserId, date: dateArg, type: typeArg };
|
|
binaryInsert(ipdaeDatas, newData);
|
|
// 신규 사용자의 알림 이력도 초기화 (또는 빈 배열 확인)
|
|
if (!notificationHistory[targetUserId]) {
|
|
notificationHistory[targetUserId] = [];
|
|
notificationHistoryModified = true;
|
|
} else if (notificationHistory[targetUserId].length > 0) { // 혹시 모를 기존 이력 초기화
|
|
notificationHistory[targetUserId] = [];
|
|
notificationHistoryModified = true;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// 1. ipdaeData.json 저장
|
|
await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeDatas, null, 4), 'utf8');
|
|
logger.info(`ipdaeData.json successfully updated for user ${targetUserId}. Action: ${isSaved ? 'Updated' : 'Added'}.`);
|
|
|
|
// 2. notificationHistory.json 저장 (변경되었을 경우)
|
|
if (notificationHistoryModified) {
|
|
await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8');
|
|
logger.info(`notificationHistory.json saved due to changes for user ${targetUserId}.`);
|
|
}
|
|
|
|
const finalDataIndex = binarySearch(ipdaeDatas, targetUserId);
|
|
const finalData = ipdaeDatas[finalDataIndex];
|
|
return new EmbedBuilder()
|
|
.setTitle(startDateEmbedTitle)
|
|
.addFields(
|
|
{ name: '입대일', value: dateFormatter(finalData.date) },
|
|
{ name: '복무 형태', value: finalData.type }
|
|
)
|
|
.setColor('#3BB143');
|
|
} catch (e) {
|
|
logger.error(`Failed to save data for user ${targetUserId}: ${e.message}`);
|
|
return new EmbedBuilder()
|
|
.setTitle('데이터 저장/수정 중 오류가 발생했습니다.')
|
|
.addFields({ name: '에러 메시지', value: e.message })
|
|
.setColor('#F44336');
|
|
}
|
|
}
|
|
|
|
// getEndDateEmbed 함수 (최종 수정된 계급 로직 포함)
|
|
function getEndDateEmbed(targetUserId, targetUserName, decimal, usedFullInfo) {
|
|
let months = 0;
|
|
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 startText = '입대일';
|
|
let endText = '전역일';
|
|
|
|
switch (ipdaeData.type) {
|
|
case '육군': months = 18; break;
|
|
case '해군': months = 20; break;
|
|
case '공군': months = 21; break;
|
|
case '해병대': months = 18; break;
|
|
case '사회복무요원':
|
|
months = 21;
|
|
startText = '소집일';
|
|
endText = '소집해제일';
|
|
break;
|
|
default:
|
|
logger.error(`Unknown service type "${ipdaeData.type}" for user ${targetUserId}`);
|
|
return new EmbedBuilder().setTitle("오류").setDescription("알 수 없는 복무 형태입니다.").setColor("#F44336");
|
|
}
|
|
|
|
const startDate = ipdaeData.date;
|
|
const endDate = addMonthsToDate(startDate, months);
|
|
|
|
const now = new Date();
|
|
|
|
const currentProgress = parseFloat(calculateProgress(startDate, endDate, now));
|
|
const totalDays = calculateDateDifference(startDate, endDate) + 1;
|
|
|
|
const todayFormatted = formatDate(now);
|
|
let daysServed = calculateDateDifference(startDate, todayFormatted) + 1;
|
|
if (new Date(todayFormatted) < new Date(startDate)) {
|
|
daysServed = 0;
|
|
}
|
|
daysServed = Math.min(daysServed, totalDays);
|
|
|
|
const remainingDays = totalDays - daysServed;
|
|
const trueRemainingDays = Math.max(0, remainingDays);
|
|
|
|
const [sYearStr, sMonthStr, sDayStr] = startDate.split('-');
|
|
const currentYearForRank = now.getFullYear();
|
|
const currentMonthForRank = now.getMonth() + 1;
|
|
|
|
let pastMonths = calculateMonthDifference(sYearStr, sMonthStr, String(currentYearForRank), String(currentMonthForRank));
|
|
|
|
if (sDayStr === '01') {
|
|
pastMonths += 1;
|
|
}
|
|
|
|
let militaryRank;
|
|
|
|
if (pastMonths <= 2) {
|
|
militaryRank = `이병 ${pastMonths + 1}호봉`;
|
|
} else if (pastMonths <= 8) {
|
|
militaryRank = `일병 ${pastMonths - 2}호봉`;
|
|
} else if (pastMonths <= 14) {
|
|
militaryRank = `상병 ${pastMonths - 8}호봉`;
|
|
} else if (trueRemainingDays <= 0) {
|
|
militaryRank = '민간인';
|
|
} else {
|
|
militaryRank = `병장 ${pastMonths - 14}호봉`;
|
|
}
|
|
|
|
const progressBarText = createProgressBar(currentProgress, 10, typeof decimal === 'number' ? decimal : parseInt(decimal) || 4);
|
|
|
|
if (!usedFullInfo) {
|
|
return new EmbedBuilder()
|
|
.setTitle(`${userNameForEmbed}의 ${endText}`)
|
|
.addFields(
|
|
{ name: '현재 진행도', value: `${progressBarText}\n${militaryRank}${addSpace(Math.max(0, 14 - militaryRank.length))}D-${trueRemainingDays}` },
|
|
{ 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(trueRemainingDays) },
|
|
{ name: '현재 계급', value: militaryRank }
|
|
)
|
|
.setColor('#007FFF');
|
|
}
|
|
}
|
|
|
|
async function checkAndSendCelebrationMessages() {
|
|
logger.info('Starting celebration check...');
|
|
if (!celebrationConfig || !celebrationConfig.milestones || !celebrationConfig.channelTopicKeyword) {
|
|
logger.warn('Celebration config is missing or incomplete. Skipping celebration check.');
|
|
return;
|
|
}
|
|
|
|
let notificationHistoryModifiedThisRun = false; // 이번 실행에서 notificationHistory 변경 여부
|
|
const today = new Date();
|
|
|
|
for (const userData of ipdaeDatas) {
|
|
if (!userData.id || !userData.date || !userData.type) {
|
|
logger.warn(`Skipping user data due to missing id, date, or type: ${JSON.stringify(userData)}`);
|
|
continue;
|
|
}
|
|
|
|
let months = 0;
|
|
switch (userData.type) {
|
|
case '육군': months = 18; break;
|
|
case '해군': months = 20; break;
|
|
case '공군': months = 21; break;
|
|
case '해병대': months = 18; break;
|
|
case '사회복무요원': months = 21; break;
|
|
default:
|
|
logger.warn(`Unknown service type "${userData.type}" for user ${userData.id}. Skipping.`);
|
|
continue;
|
|
}
|
|
|
|
const endDateString = addMonthsToDate(userData.date, months);
|
|
const remainingDays = calculateDaysFromNow(endDateString, today);
|
|
const userHistory = notificationHistory[userData.id] || []; // 없으면 빈 배열
|
|
|
|
for (const milestone of celebrationConfig.milestones) {
|
|
const currentMilestoneMentionUser = typeof milestone.mentionUser === 'boolean' ? milestone.mentionUser : true;
|
|
const currentMilestoneMentionEveryone = typeof milestone.mentionEveryone === 'boolean' ? milestone.mentionEveryone : false;
|
|
|
|
if (remainingDays === milestone.days) {
|
|
if (!userHistory.includes(milestone.days)) { // 이력 확인
|
|
let userToNotify;
|
|
try {
|
|
userToNotify = await client.users.fetch(userData.id);
|
|
} catch (fetchError) {
|
|
logger.warn(`Could not fetch user ${userData.id} for celebration: ${fetchError.message}. Skipping.`);
|
|
continue;
|
|
}
|
|
if (!userToNotify) continue;
|
|
|
|
for (const guild of client.guilds.cache.values()) {
|
|
let member;
|
|
try {
|
|
member = guild.members.cache.get(userData.id) || await guild.members.fetch(userData.id).catch(() => null);
|
|
} catch { member = null; }
|
|
|
|
if (member) {
|
|
const displayName = member.displayName || userToNotify.username;
|
|
const targetChannels = guild.channels.cache.filter(ch =>
|
|
ch.isTextBased() &&
|
|
ch.topic &&
|
|
ch.topic.includes(celebrationConfig.channelTopicKeyword) &&
|
|
guild.members.me &&
|
|
guild.members.me.permissionsIn(ch).has(PermissionsBitField.Flags.SendMessages)
|
|
);
|
|
|
|
targetChannels.forEach(async channel => {
|
|
try {
|
|
let messagePrefix = "";
|
|
let allowEveryoneMentionForSend = false;
|
|
let allowUserMentionForSend = false;
|
|
|
|
if (currentMilestoneMentionEveryone) {
|
|
if (guild.members.me.permissionsIn(channel).has(PermissionsBitField.Flags.MentionEveryone)) {
|
|
messagePrefix += "@everyone ";
|
|
allowEveryoneMentionForSend = true;
|
|
} else {
|
|
logger.warn(`Bot lacks MentionEveryone permission in #${channel.name} in ${guild.name}. Skipping @everyone mention for ${displayName}.`);
|
|
}
|
|
}
|
|
|
|
if (currentMilestoneMentionUser) {
|
|
messagePrefix += `<@${userData.id}> `;
|
|
allowUserMentionForSend = true;
|
|
}
|
|
|
|
const baseMessageText = milestone.message.replace(/{userName}/g, displayName);
|
|
const finalMessageToSend = messagePrefix + baseMessageText;
|
|
|
|
await channel.send({
|
|
content: finalMessageToSend,
|
|
allowedMentions: {
|
|
parse: allowEveryoneMentionForSend ? ['everyone'] : [],
|
|
users: allowUserMentionForSend ? [userData.id] : [],
|
|
roles: []
|
|
}
|
|
});
|
|
|
|
logger.info(`Sent ${milestone.days}-day celebration for ${displayName} (${userData.id}) to #${channel.name} in ${guild.name}. Mentions: User=${allowUserMentionForSend}, Everyone=${allowEveryoneMentionForSend}.`);
|
|
|
|
// 알림 이력 기록
|
|
if (!notificationHistory[userData.id]) { // 혹시 모를 경우 대비하여 배열 초기화
|
|
notificationHistory[userData.id] = [];
|
|
}
|
|
if (!notificationHistory[userData.id].includes(milestone.days)) {
|
|
notificationHistory[userData.id].push(milestone.days);
|
|
notificationHistoryModifiedThisRun = true;
|
|
}
|
|
} catch (sendError) {
|
|
logger.error(`Failed to send celebration message to #${channel.name} in ${guild.name} for ${displayName}: ${sendError.message}`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notificationHistoryModifiedThisRun) {
|
|
try {
|
|
await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8');
|
|
logger.info('notificationHistory.json updated with new celebration notification statuses.');
|
|
} catch (e) {
|
|
logger.error(`Failed to save notificationHistory.json after celebration checks: ${e.message}`);
|
|
}
|
|
}
|
|
logger.info('Celebration check finished.');
|
|
}
|
|
|
|
function scheduleDailyCelebrationCheck() {
|
|
const now = new Date();
|
|
const tomorrow = new Date(now);
|
|
tomorrow.setDate(now.getDate() + 1);
|
|
tomorrow.setHours(0, 0, 5, 0); // 다음 날 00:00:05 KST (서버 시간 무관, UTC 기준 다음날 00:00:05에 실행될 수 있도록 시간 계산)
|
|
|
|
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(async () => {
|
|
logger.info('Executing scheduled daily celebration check (first run after startup schedule)...');
|
|
await checkAndSendCelebrationMessages();
|
|
setInterval(async () => {
|
|
logger.info('Executing scheduled daily celebration check (interval)...');
|
|
await checkAndSendCelebrationMessages();
|
|
}, 24 * 60 * 60 * 1000); // 24시간
|
|
}, timeToFirstCheck);
|
|
}
|
|
|
|
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);
|
|
const diffTime = inputDate.getTime() - today.getTime();
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
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);
|
|
const diffTime = Math.abs(d2.getTime() - d1.getTime());
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
function calculateProgress(startDateStr, endDateStr, now = new Date()) {
|
|
const start = new Date(startDateStr).getTime();
|
|
const end = new Date(endDateStr).getTime();
|
|
const currentTime = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
|
|
if (start >= end) return '100.0000000';
|
|
if (currentTime <= start) return '0.0000000';
|
|
if (currentTime >= end) return '100.0000000';
|
|
|
|
const totalDuration = end - start;
|
|
const elapsedDuration = currentTime - start;
|
|
|
|
let progress = (elapsedDuration / totalDuration) * 100;
|
|
progress = Math.max(0, Math.min(100, progress));
|
|
return progress.toFixed(7);
|
|
}
|
|
|
|
function createProgressBar(progress, totalLength, toFixedValue) {
|
|
progress = Math.max(0, Math.min(100, parseFloat(progress)));
|
|
toFixedValue = parseInt(toFixedValue);
|
|
if (isNaN(toFixedValue) || toFixedValue < 0) toFixedValue = 4;
|
|
|
|
const filledLength = Math.round((progress / 100) * totalLength);
|
|
const unfilledLength = totalLength - filledLength;
|
|
const filledBar = '█'.repeat(filledLength);
|
|
const unfilledBar = '░'.repeat(unfilledLength);
|
|
return `[${filledBar}${unfilledBar}] ${progress.toFixed(toFixedValue)}%`;
|
|
}
|
|
|
|
function getNameById(id, guild = null) {
|
|
try {
|
|
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; // 서버별 별명 반환
|
|
// 캐시에 없으면 fetch 시도 (비동기 처리는 이 함수 구조상 어려움, 필요시 async/await로 변경)
|
|
// guild.members.fetch(id).then(m => m.displayName).catch(() => user.username); // 비동기 호출 필요시
|
|
}
|
|
|
|
// 특정 서버 컨텍스트 없거나, 해당 서버에 멤버 정보 없으면
|
|
// 봇이 속한 아무 서버에서라도 별명 찾아보기 (첫번째 찾은 별명 사용)
|
|
for (const g of client.guilds.cache.values()){
|
|
const memberInAnyGuild = g.members.cache.get(id);
|
|
if(memberInAnyGuild) return memberInAnyGuild.displayName;
|
|
}
|
|
return user.username; // 최후의 수단: 기본 사용자 이름
|
|
} catch (e) {
|
|
logger.warn(`Error in getNameById for ID ${id}: ${e.message}`);
|
|
return `Unknown User (${id})`; // 오류 시 기본값
|
|
}
|
|
}
|
|
|
|
function calculateMonthDifference(startDateYear, startDateMonth, endDateYear, endDateMonth) {
|
|
startDateYear = Number(startDateYear);
|
|
startDateMonth = Number(startDateMonth);
|
|
endDateYear = Number(endDateYear);
|
|
endDateMonth = Number(endDateMonth);
|
|
|
|
const yearDifference = endDateYear - startDateYear;
|
|
const monthDifference = endDateMonth - startDateMonth;
|
|
|
|
return (yearDifference * 12) + monthDifference;
|
|
}
|
|
|
|
function beautifyNumber(number) {
|
|
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
|
|
function addSpace(num) {
|
|
if (typeof num !== 'number' || num < 0) return '';
|
|
return ' '.repeat(num);
|
|
}
|
|
|
|
function generateRandomNumber(min, max) {
|
|
min = Math.ceil(min);
|
|
max = Math.floor(max);
|
|
if (min > max) [min, max] = [max, min];
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
// 원본 parseArgs 함수 (사용자 제공)
|
|
function parseArgs(cmdStr) {
|
|
const re_next_arg = /^\s*((?:(?:"(?:\\.|[^"])*")|(?:'[^']*')|\\.|\S)+)\s*(.*)$/;
|
|
let next_arg = ['', '', cmdStr];
|
|
const args = [];
|
|
while ((next_arg = re_next_arg.exec(next_arg[2]))) {
|
|
let quoted_arg = next_arg[1];
|
|
let unquoted_arg = '';
|
|
while (quoted_arg.length > 0) {
|
|
if (/^"/.test(quoted_arg)) {
|
|
const quoted_part = /^"((?:\\.|[^"])*)"(.*)$/.exec(quoted_arg);
|
|
unquoted_arg += quoted_part[1].replace(/\\(.)/g, '$1');
|
|
quoted_arg = quoted_part[2];
|
|
} else if (/^'/.test(quoted_arg)) {
|
|
const quoted_part = /^'([^']*)'(.*)$/.exec(quoted_arg);
|
|
unquoted_arg += quoted_part[1];
|
|
quoted_arg = quoted_part[2];
|
|
} else if (/^\\/.test(quoted_arg)) {
|
|
unquoted_arg += quoted_arg[1];
|
|
quoted_arg = quoted_arg.substring(2);
|
|
} else {
|
|
unquoted_arg += quoted_arg[0];
|
|
quoted_arg = quoted_arg.substring(1);
|
|
}
|
|
}
|
|
args.push(unquoted_arg);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
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}`;
|
|
}
|
|
|
|
logger.info('Attempting to log in with token...');
|
|
|
|
//메인봇 토큰
|
|
client.login('MTI4MjY1Mzg3NDk3NjcyMjk0NA.Gdp9Oc.1af98FXAiV911qFL5a5hHI0Mmf95qmT7ES9FhQ');
|
|
|
|
//테스트봇 토큰
|
|
//client.login('MTMzNTE1MzgwNjg1NzQ2OTk2Mw.GZPzlI.YzJsJZgPD1Ac-Ddnkw-c9C1SI1QL2xz2IlZ7Z4');
|