Files
modstagram-back/helpers/expiredMessages.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

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
}