full
This commit is contained in:
@@ -1,11 +1,28 @@
|
||||
const moment = require('moment-jalaali')
|
||||
const MessageModel = require('../models/MessageModel')
|
||||
const UserModel = require('../models/UserModel')
|
||||
const { opaquePreview, isE2eeEnvelope } = require('../utils/e2ee')
|
||||
const { sendPushToUser } = require('../utils/pushNotification')
|
||||
const { buildStorageUrl } = require('../utils/storageUrl')
|
||||
|
||||
function formatTimeOnly(date) {
|
||||
return moment(date).locale('fa').format('HH:mm:ss')
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
if (value == null) return "";
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
function normalizeMessagePayload(message) {
|
||||
return {
|
||||
...message,
|
||||
_id: message._id != null ? String(message._id) : message._id,
|
||||
senderId: normalizeId(message.senderId),
|
||||
receiverId: normalizeId(message.receiverId),
|
||||
};
|
||||
}
|
||||
|
||||
async function enrichMessage(message) {
|
||||
const doc = message._doc ? { ...message._doc } : { ...message }
|
||||
let replyTo = null
|
||||
@@ -19,7 +36,7 @@ async function enrichMessage(message) {
|
||||
: await UserModel.findById(parent.senderId).select('first_name last_name user_name').lean()
|
||||
replyTo = {
|
||||
_id: parent._id,
|
||||
content: (parent.content || 'پیام').slice(0, 200),
|
||||
content: opaquePreview(parent.content, 200),
|
||||
senderName: parentUser
|
||||
? `${parentUser.first_name || ''} ${parentUser.last_name || ''}`.trim() || parentUser.user_name
|
||||
: 'کاربر'
|
||||
@@ -27,11 +44,15 @@ async function enrichMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
return normalizeMessagePayload({
|
||||
...doc,
|
||||
createdAt: formatTimeOnly(doc.createdAt),
|
||||
createdAtIso: doc.createdAt ? new Date(doc.createdAt).toISOString() : undefined,
|
||||
expiresAt: doc.expiresAt ? new Date(doc.expiresAt).toISOString() : undefined,
|
||||
selfDestructSeconds: doc.selfDestructSeconds || undefined,
|
||||
selfDestructStartedAt: doc.selfDestructStartedAt
|
||||
? new Date(doc.selfDestructStartedAt).toISOString()
|
||||
: undefined,
|
||||
viewOnce: Boolean(doc.viewOnce),
|
||||
viewOnceLocked: Boolean(doc.viewOnceLocked),
|
||||
viewOnceExpired: Boolean(doc.viewOnceExpired),
|
||||
@@ -39,37 +60,78 @@ async function enrichMessage(message) {
|
||||
? new Date(doc.viewOnceOpenedAt).toISOString()
|
||||
: undefined,
|
||||
replyTo,
|
||||
forwardedFrom: doc.forwardedFrom || undefined
|
||||
}
|
||||
forwardedFrom: doc.forwardedFrom || undefined,
|
||||
reactions: (doc.reactions || []).map((r) => ({
|
||||
userId: normalizeId(r.userId),
|
||||
emoji: r.emoji
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function emitChatEvents(io, message, senderId, receiverId) {
|
||||
const payload = message
|
||||
const roomA = `chat:${senderId}:${receiverId}`
|
||||
const roomB = `chat:${receiverId}:${senderId}`
|
||||
io.to(roomA).to(roomB).emit('newMessage', payload)
|
||||
io.to(`user:${senderId}`).to(`user:${receiverId}`).emit('chatListUpdate', {
|
||||
senderId,
|
||||
receiverId,
|
||||
const sid = normalizeId(senderId)
|
||||
const rid = normalizeId(receiverId)
|
||||
const payload = normalizeMessagePayload(
|
||||
typeof message === "object" && message !== null ? { ...message } : { content: message }
|
||||
)
|
||||
const roomA = `chat:${sid}:${rid}`
|
||||
const roomB = `chat:${rid}:${sid}`
|
||||
const userSender = `user:${sid}`
|
||||
const userReceiver = `user:${rid}`
|
||||
|
||||
io.to(roomA).to(roomB).to(userSender).to(userReceiver).emit('newMessage', payload)
|
||||
io.to(userSender).to(userReceiver).emit('chatListUpdate', {
|
||||
senderId: sid,
|
||||
receiverId: rid,
|
||||
message: payload
|
||||
})
|
||||
|
||||
sendChatPushNotification(sid, rid).catch(() => {})
|
||||
}
|
||||
|
||||
// پیامها E2EE هستن (رمزنگاری سرتاسر) — سرور محتوا رو نمیبینه و نباید توی پوش هم نشونش بده،
|
||||
// فقط اسم فرستنده + یه متن ثابت. عین همین تابع برای همهی مسیرهای emitChatEvents (چت مستقیم،
|
||||
// پاسخ استوری، اشتراکگذاری پست/محصول توی چت) صدا زده میشه
|
||||
async function sendChatPushNotification(senderId, receiverId) {
|
||||
const sender = await UserModel.findById(senderId).select('first_name last_name user_name profile_image').lean()
|
||||
if (!sender) return
|
||||
const name = `${sender.first_name || ''} ${sender.last_name || ''}`.trim() || sender.user_name || 'کاربر'
|
||||
await sendPushToUser(receiverId, {
|
||||
title: name,
|
||||
description: 'یک پیام جدید برای شما ارسال کرد',
|
||||
type: 'chat_message',
|
||||
data: { project_post_id: senderId },
|
||||
image: buildStorageUrl(sender.profile_image) || undefined
|
||||
})
|
||||
}
|
||||
|
||||
function parseForwardedContent(content) {
|
||||
if (!content) return { body: '', forwardedFrom: null }
|
||||
|
||||
// پاکت E2EE را دست نزن — JSON.parse + regex قبلی ciphertext را خراب میکرد
|
||||
if (typeof content === 'string' && isE2eeEnvelope(content)) {
|
||||
return { body: content, forwardedFrom: null }
|
||||
}
|
||||
if (content && typeof content === 'object' && content.e2ee === true) {
|
||||
return { body: JSON.stringify(content), forwardedFrom: null }
|
||||
}
|
||||
|
||||
try {
|
||||
const line = content.split('\n')[0]
|
||||
const line = String(content).split('\n')[0]
|
||||
const j = JSON.parse(line)
|
||||
if (j.forwardedFrom) {
|
||||
if (j.forwardedFrom && !j.e2ee) {
|
||||
const rest = String(content).includes('\n')
|
||||
? String(content).slice(String(content).indexOf('\n') + 1).trim()
|
||||
: ''
|
||||
return {
|
||||
forwardedFrom: j.forwardedFrom,
|
||||
body: content.replace(/^\{.*\}\n?/, '').trim()
|
||||
body: rest
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
/* not json */
|
||||
}
|
||||
return { body: content, forwardedFrom: null }
|
||||
return { body: typeof content === 'string' ? content : String(content), forwardedFrom: null }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
2
helpers/chatRoomHelpers.js
Normal file
2
helpers/chatRoomHelpers.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/** @deprecated use controllers/application/chatRooms/chatRoomHelpers */
|
||||
module.exports = require('../controllers/application/chatRooms/chatRoomHelpers')
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -22,15 +23,47 @@ function activeMessageFilter(baseFilter) {
|
||||
baseFilter,
|
||||
{
|
||||
$or: [
|
||||
{ expiresAt: { $exists: false } },
|
||||
{ expiresAt: null },
|
||||
{ expiresAt: { $gt: new Date() } }
|
||||
{
|
||||
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
|
||||
@@ -118,12 +151,86 @@ function scheduleMessageExpiry(io, message) {
|
||||
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
|
||||
deleteMessagesAndNotify,
|
||||
startSelfDestructOnView,
|
||||
markSelfDestructViewed,
|
||||
startPendingSelfDestructForViewer
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user