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) { 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 }) } // چتِ فروشگاه (خریدار <-> مالکِ فروشگاه) قبلاً هیچ پوشی نمی‌فرستاد — کاملاً جدا از // 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 }