237 lines
6.3 KiB
JavaScript
237 lines
6.3 KiB
JavaScript
const path = require('path')
|
|
const fs = require('fs-extra')
|
|
const MessageModel = require('../models/MessageModel')
|
|
|
|
const ALLOWED_SELF_DESTRUCT_SECONDS = [10, 30, 60, 300, 3600]
|
|
const DELETE_WINDOW_MS = 24 * 60 * 60 * 1000
|
|
|
|
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: [
|
|
{
|
|
selfDestructSeconds: { $exists: true, $ne: null },
|
|
selfDestructStartedAt: { $exists: false }
|
|
},
|
|
{
|
|
selfDestructSeconds: { $exists: true, $ne: null },
|
|
selfDestructStartedAt: { $exists: true, $ne: null },
|
|
$or: [
|
|
{ expiresAt: { $exists: false } },
|
|
{ expiresAt: null },
|
|
{ expiresAt: { $gt: new Date() } }
|
|
]
|
|
},
|
|
{
|
|
$and: [
|
|
{
|
|
$or: [
|
|
{ selfDestructSeconds: { $exists: false } },
|
|
{ selfDestructSeconds: null }
|
|
]
|
|
},
|
|
{
|
|
$or: [
|
|
{ expiresAt: { $exists: false } },
|
|
{ expiresAt: null },
|
|
{ expiresAt: { $gt: new Date() } }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function isWithinDeleteWindow(createdAt) {
|
|
if (!createdAt) return false
|
|
return Date.now() - new Date(createdAt).getTime() <= DELETE_WINDOW_MS
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
async function startSelfDestructOnView(io, message, viewerId) {
|
|
if (!message?.selfDestructSeconds || message.selfDestructStartedAt) {
|
|
return message
|
|
}
|
|
|
|
const viewer = String(viewerId)
|
|
const receiver = String(message.receiverId)
|
|
if (viewer !== receiver) return message
|
|
|
|
const expiresAt = buildExpiresAt(message.selfDestructSeconds)
|
|
const updated = await MessageModel.findByIdAndUpdate(
|
|
message._id,
|
|
{
|
|
$set: {
|
|
selfDestructStartedAt: new Date(),
|
|
expiresAt
|
|
}
|
|
},
|
|
{ new: true }
|
|
)
|
|
|
|
if (updated) {
|
|
scheduleMessageExpiry(io, updated)
|
|
if (io) {
|
|
const sid = String(updated.senderId)
|
|
const rid = String(updated.receiverId)
|
|
io.to(`chat:${sid}:${rid}`)
|
|
.to(`chat:${rid}:${sid}`)
|
|
.emit('messageSelfDestructStarted', {
|
|
messageId: String(updated._id),
|
|
expiresAt: updated.expiresAt.toISOString(),
|
|
selfDestructSeconds: updated.selfDestructSeconds
|
|
})
|
|
}
|
|
}
|
|
|
|
return updated || message
|
|
}
|
|
|
|
async function markSelfDestructViewed(io, messageId, viewerId) {
|
|
const msg = await MessageModel.findById(messageId)
|
|
if (!msg || !msg.selfDestructSeconds || msg.selfDestructStartedAt) {
|
|
return { message: msg }
|
|
}
|
|
|
|
const updated = await startSelfDestructOnView(io, msg, viewerId)
|
|
return {
|
|
message: updated,
|
|
expiresAt: updated?.expiresAt
|
|
? new Date(updated.expiresAt).toISOString()
|
|
: undefined
|
|
}
|
|
}
|
|
|
|
async function startPendingSelfDestructForViewer(io, viewerId, peerId) {
|
|
const viewer = String(viewerId)
|
|
const peer = String(peerId)
|
|
const pending = await MessageModel.find({
|
|
receiverId: viewer,
|
|
senderId: peer,
|
|
selfDestructSeconds: { $exists: true, $ne: null },
|
|
selfDestructStartedAt: { $exists: false }
|
|
})
|
|
|
|
await Promise.all(
|
|
pending.map((msg) => startSelfDestructOnView(io, msg, viewer))
|
|
)
|
|
}
|
|
|
|
module.exports = {
|
|
ALLOWED_SELF_DESTRUCT_SECONDS,
|
|
DELETE_WINDOW_MS,
|
|
parseSelfDestructSeconds,
|
|
buildExpiresAt,
|
|
activeMessageFilter,
|
|
isWithinDeleteWindow,
|
|
purgeExpiredMessages,
|
|
scheduleMessageExpiry,
|
|
deleteMessagesAndNotify,
|
|
startSelfDestructOnView,
|
|
markSelfDestructViewed,
|
|
startPendingSelfDestructForViewer
|
|
}
|