const path = require('path') const fs = require('fs-extra') const MessageModel = require('../models/MessageModel') const ALLOWED_SELF_DESTRUCT_SECONDS = [10, 30, 60, 300, 3600] function parseSelfDestructSeconds(raw) { if (raw == null || raw === '' || raw === '0' || raw === 0) return null const n = parseInt(String(raw), 10) if (!Number.isFinite(n) || !ALLOWED_SELF_DESTRUCT_SECONDS.includes(n)) return null return n } function buildExpiresAt(seconds) { if (!seconds) return undefined return new Date(Date.now() + seconds * 1000) } function activeMessageFilter(baseFilter) { return { $and: [ baseFilter, { $or: [ { expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } } ] } ] } } async function removeMessageFiles(messages) { for (const msg of messages) { if (!msg.file) continue const relative = String(msg.file).replace(/^\//, '') const filePath = path.join(__dirname, '../storage', relative) if (fs.existsSync(filePath)) { await fs.remove(filePath).catch(() => {}) } } } async function deleteMessagesAndNotify(io, messages, options = {}) { if (!messages?.length) return [] await removeMessageFiles(messages) const deletedIds = messages.map((m) => String(m._id)) await MessageModel.deleteMany({ _id: { $in: deletedIds } }) if (io) { const byThread = new Map() messages.forEach((m) => { const s = String(m.senderId) const r = String(m.receiverId) const key = [s, r].sort().join(':') if (!byThread.has(key)) byThread.set(key, { s, r, ids: [] }) byThread.get(key).ids.push(String(m._id)) }) byThread.forEach(({ s, r, ids }) => { io.to(`chat:${s}:${r}`) .to(`chat:${r}:${s}`) .emit('messagesDeleted', { messageIds: ids, deletedBy: options.deletedBy, reason: options.reason || (options.deletedBy ? 'manual' : 'expired') }) io.to(`user:${s}`).to(`user:${r}`).emit('chatListUpdate', { senderId: s, receiverId: r }) }) } return deletedIds } async function purgeExpiredMessages(io) { try { const expired = await MessageModel.find({ expiresAt: { $lte: new Date() } }) if (!expired.length) return [] return deleteMessagesAndNotify(io, expired) } catch (err) { console.error('purgeExpiredMessages error:', err) return [] } } const scheduledTimers = new Map() function scheduleMessageExpiry(io, message) { if (!message?.expiresAt || !message._id) return const id = String(message._id) if (scheduledTimers.has(id)) { clearTimeout(scheduledTimers.get(id)) } const ms = new Date(message.expiresAt).getTime() - Date.now() if (ms <= 0) { MessageModel.findById(id) .then((doc) => doc && deleteMessagesAndNotify(io, [doc])) .catch(() => {}) return } const timer = setTimeout(async () => { scheduledTimers.delete(id) try { const doc = await MessageModel.findById(id) if (doc && doc.expiresAt && new Date(doc.expiresAt) <= new Date()) { await deleteMessagesAndNotify(io, [doc]) } } catch (err) { console.error('scheduleMessageExpiry error:', err) } }, ms) scheduledTimers.set(id, timer) } module.exports = { ALLOWED_SELF_DESTRUCT_SECONDS, parseSelfDestructSeconds, buildExpiresAt, activeMessageFilter, purgeExpiredMessages, scheduleMessageExpiry, deleteMessagesAndNotify }