171 lines
7.0 KiB
JavaScript
171 lines
7.0 KiB
JavaScript
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
|
|
|
|
if (doc.replyToId) {
|
|
const parent = await MessageModel.findById(doc.replyToId).lean()
|
|
if (parent) {
|
|
const parentUser =
|
|
String(parent.senderId) === String(doc.senderId)
|
|
? await UserModel.findById(doc.senderId).select('first_name last_name user_name').lean()
|
|
: await UserModel.findById(parent.senderId).select('first_name last_name user_name').lean()
|
|
replyTo = {
|
|
_id: parent._id,
|
|
content: opaquePreview(parent.content, 200),
|
|
senderName: parentUser
|
|
? `${parentUser.first_name || ''} ${parentUser.last_name || ''}`.trim() || parentUser.user_name
|
|
: 'کاربر'
|
|
}
|
|
}
|
|
}
|
|
|
|
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),
|
|
viewOnceOpenedAt: doc.viewOnceOpenedAt
|
|
? new Date(doc.viewOnceOpenedAt).toISOString()
|
|
: undefined,
|
|
replyTo,
|
|
forwardedFrom: doc.forwardedFrom || undefined,
|
|
reactions: (doc.reactions || []).map((r) => ({
|
|
userId: normalizeId(r.userId),
|
|
emoji: r.emoji
|
|
}))
|
|
})
|
|
}
|
|
|
|
function emitChatEvents(io, message, senderId, receiverId, pushOverride) {
|
|
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, pushOverride).catch(() => {})
|
|
}
|
|
|
|
// پیامهای متنیِ معمولی E2EE هستن (رمزنگاری سرتاسر) — سرور محتوا رو نمیبینه و نباید توی
|
|
// پوش هم نشونش بده، فقط اسم فرستنده + یه متن ثابت. اما اشتراکگذاریِ پست/محصول (که خودِ
|
|
// سرور با دادهٔ واقعیِ دیتابیس میسازه، نه متنِ رمزنگاریشدهٔ کاربر) میتونه با pushOverride
|
|
// نوع/متنِ دقیقتری بفرسته (نگاه کن به فراخوانیها توی engagementController.js/shopProductController.js)
|
|
async function sendChatPushNotification(senderId, receiverId, pushOverride = {}) {
|
|
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: pushOverride.description || 'یک پیام جدید برای شما ارسال کرد',
|
|
type: pushOverride.type || 'chat_message',
|
|
data: { project_post_id: senderId, link: `/settings/chats/${sender.user_name || ''}/${senderId}` },
|
|
image: buildStorageUrl(sender.profile_image) || undefined
|
|
})
|
|
}
|
|
|
|
// چتِ فروشگاه (خریدار <-> مالکِ فروشگاه) قبلاً هیچ پوشی نمیفرستاد — کاملاً جدا از
|
|
// sendChatPushNotification بالا چون فرستنده میتونه «فروشگاه» باشه، نه یه کاربرِ ساده،
|
|
// پس تایتل/عکسِ نوتیف باید از روی نقشِ فرستنده (shop یا buyer) انتخاب بشه
|
|
async function sendShopChatPushNotification({ shop, senderId, senderRole, receiverId, buyerId }) {
|
|
let title
|
|
let image
|
|
if (senderRole === 'shop') {
|
|
title = shop?.name || 'فروشگاه'
|
|
image = buildStorageUrl(shop?.logo) || undefined
|
|
} else {
|
|
const sender = await UserModel.findById(senderId).select('first_name last_name user_name profile_image').lean()
|
|
if (!sender) return
|
|
title = `${sender.first_name || ''} ${sender.last_name || ''}`.trim() || sender.user_name || 'کاربر'
|
|
image = buildStorageUrl(sender.profile_image) || undefined
|
|
}
|
|
await sendPushToUser(receiverId, {
|
|
title,
|
|
description: 'یک پیام جدید برای شما ارسال کرد',
|
|
type: 'shop_chat_message',
|
|
// buyer_id همیشه لازمه تا مالکِ فروشگاه (که میتونه چند نخِ گفتگو با خریدارهای مختلف
|
|
// داشته باشه) بدونه دقیقاً کدوم نخ رو باز کنه؛ برای خودِ خریدار هم همون id خودشه
|
|
data: { shop_id: String(shop?._id || ''), sender_role: senderRole, buyer_id: String(buyerId || '') },
|
|
image
|
|
})
|
|
}
|
|
|
|
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 = String(content).split('\n')[0]
|
|
const j = JSON.parse(line)
|
|
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: rest
|
|
}
|
|
}
|
|
} catch (_) {
|
|
/* not json */
|
|
}
|
|
return { body: typeof content === 'string' ? content : String(content), forwardedFrom: null }
|
|
}
|
|
|
|
module.exports = {
|
|
formatTimeOnly,
|
|
enrichMessage,
|
|
emitChatEvents,
|
|
parseForwardedContent,
|
|
sendShopChatPushNotification
|
|
}
|