first commit
This commit is contained in:
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
|
||||
};
|
Reference in New Issue
Block a user