diff --git a/.gitignore b/.gitignore index c619ade..a01f19f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ config/ -data/ +data/notificationHistory.json +data/ipdaeData.json .env src/commands/admin/ -src/commands/general/ \ No newline at end of file +src/commands/general/ diff --git a/src/data/dataManager.js b/src/data/dataManager.js new file mode 100644 index 0000000..4e0bfdd --- /dev/null +++ b/src/data/dataManager.js @@ -0,0 +1,182 @@ +const { binarySearch, binaryInsert } = require('../utils/helpers'); +const fs = require('fs').promises; +const logger = require('../../modules/colorfulLogger'); +const { + ipdaeDataDirectory, + celebrationConfigPath, + notificationHistoryPath, + dataDir +} = require('../../config/constants'); + +let ipdaeDatas = []; +let celebrationConfig = {}; +let notificationHistory = {}; + +async function loadData() { + try { + await fs.mkdir(dataDir, { recursive: true }); + } catch (e) { + logger.error(`Failed to create data directory ${dataDir}: ${e.message}`); + } + + try { + const data = await fs.readFile(ipdaeDataDirectory, 'utf8'); + ipdaeDatas = JSON.parse(data); + ipdaeDatas.sort((a, b) => a.id.localeCompare(b.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') { + await saveData('ipdae'); + } + } + + 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') { + await saveData('history'); + } + } + + await migrateData(); + + try { + const configData = await fs.readFile(celebrationConfigPath, 'utf8'); + celebrationConfig = JSON.parse(configData); + if (celebrationConfig.milestones && Array.isArray(celebrationConfig.milestones)) { + celebrationConfig.milestones.forEach(m => { + m.mentionUser = m.mentionUser ?? true; + m.mentionEveryone = m.mentionEveryone ?? false; + }); + } else { + 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 } + ] + }; + await saveData('config'); + } +} + +async function migrateData() { + let ipdaeDataNeedsSave = false; + let notificationHistoryNeedsSave = false; + + 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) { + if (!notificationHistory[user.id] || Object.keys(notificationHistory[user.id]).length === 0) { + notificationHistory[user.id] = [...user.notifiedMilestones]; + notificationHistoryNeedsSave = 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 removed.`); + } + } + delete user.notifiedMilestones; + ipdaeDataNeedsSave = true; + } + } + + if (ipdaeDataNeedsSave) { + await saveData('ipdae'); + } + if (notificationHistoryNeedsSave) { + await saveData('history'); + } +} + +async function saveData(type) { + try { + switch (type) { + case 'ipdae': + await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeDatas, null, 4), 'utf8'); + logger.info('ipdaeData.json saved.'); + break; + case 'config': + await fs.writeFile(celebrationConfigPath, JSON.stringify(celebrationConfig, null, 4), 'utf8'); + logger.info('celebrationConfig.json saved.'); + break; + case 'history': + await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8'); + logger.info('notificationHistory.json saved.'); + break; + default: + logger.warn(`Unknown data type for saving: ${type}`); + } + } catch (e) { + logger.error(`Error saving ${type} data: ${e.message}`); + } +} + +function getIpdaeData() { + return ipdaeDatas; +} + +function getCelebrationConfig() { + return celebrationConfig; +} + +function getNotificationHistory() { + return notificationHistory; +} + +async function updateEnlistmentData(userId, date, type) { + let notificationHistoryModified = false; + const index = binarySearch(ipdaeDatas, userId); + + if (index !== -1) { + const oldDate = ipdaeDatas[index].date; + const oldType = ipdaeDatas[index].type; + + ipdaeDatas[index].date = date; + ipdaeDatas[index].type = type; + + if (oldDate !== date || oldType !== type) { + logger.info(`Enlistment data changed for ${userId}. Resetting their notification history.`); + notificationHistory[userId] = []; + notificationHistoryModified = true; + } + } else { + binaryInsert(ipdaeDatas, { id: userId, date: date, type: type }); + notificationHistory[userId] = []; + notificationHistoryModified = true; + } + + await saveData('ipdae'); + if (notificationHistoryModified) { + await saveData('history'); + } + + return ipdaeDatas[binarySearch(ipdaeDatas, userId)]; +} + +module.exports = { + loadData, + saveData, + getIpdaeData, + getCelebrationConfig, + getNotificationHistory, + updateEnlistmentData, + ipdaeDatas, + celebrationConfig, + notificationHistory +}; \ No newline at end of file