Initial commit

This commit is contained in:
payacom
2026-07-08 21:01:30 +03:30
parent 59c7c8e11c
commit 0b01b262fa
38 changed files with 1415 additions and 288 deletions

157
index.js
View File

@@ -98,14 +98,22 @@ const io = require('socket.io')(http, {
app.use(express.json({ limit: '1000mb' }));
app.use(express.urlencoded({ limit: '1000mb', extended: true }));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
// مسیرهای مشخص قبل از /storage عمومی — پروفایل از هر دو محل (قدیم و جدید) سرو می‌شود
app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles')));
app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles')));
app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts')));
app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts')));
app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts')));
app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts')));
app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages')));
app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages')));
app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets')));
app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets')));
app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising')));
app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising')));
app.use('/storage/services', express.static(path.join(__dirname, '../storage/services')));
app.use('/storage/services', express.static(path.join(__dirname, 'storage/services')));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
require('./boot');
@@ -146,19 +154,50 @@ app.set('trust proxy', 'loopback');
require('./routes')(app);
const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers')
const {
parseSelfDestructSeconds,
buildExpiresAt,
activeMessageFilter,
purgeExpiredMessages,
scheduleMessageExpiry,
deleteMessagesAndNotify
} = require('./helpers/expiredMessages')
const {
parseViewOnce,
isViewOnceMediaType,
sanitizeMessageForViewer,
openViewOnceMessage,
completeViewOnceMessage
} = require('./helpers/viewOnceMessages')
const mongoose = require('mongoose')
app.get('/api/v1/chat', async (req, res) => {
try {
const { senderId, receiverId, limit, page } = req.query
if (!senderId || !receiverId) {
return res.status(400).json({ error: 'senderId and receiverId are required' })
}
if (
!mongoose.Types.ObjectId.isValid(String(senderId)) ||
!mongoose.Types.ObjectId.isValid(String(receiverId))
) {
return res.status(400).json({ error: 'Invalid senderId or receiverId' })
}
const sid = new mongoose.Types.ObjectId(String(senderId))
const rid = new mongoose.Types.ObjectId(String(receiverId))
const pageNumber = parseInt(page) || 1
const limitPerPage = parseInt(limit) || 40
const filter = {
const filter = activeMessageFilter({
$or: [
{ senderId, receiverId },
{ senderId: receiverId, receiverId: senderId }
{ senderId: sid, receiverId: rid },
{ senderId: rid, receiverId: sid }
]
}
})
const totalMessagesCount = await MessageModel.countDocuments(filter)
const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1
@@ -170,11 +209,15 @@ app.get('/api/v1/chat', async (req, res) => {
.limit(limitPerPage)
await MessageModel.updateMany(
{ _id: { $in: messages.map((m) => m._id) }, receiverId: senderId },
{ _id: { $in: messages.map((m) => m._id) }, receiverId: sid },
{ $set: { readStatus: 1 } }
)
const enriched = await Promise.all(messages.map((m) => enrichMessage(m)))
const enriched = await Promise.all(
messages.map((m) =>
enrichMessage(sanitizeMessageForViewer(m, sid))
)
)
res.json({ messages: enriched.reverse(), totalPages })
} catch (error) {
console.error('Error fetching messages:', error)
@@ -184,15 +227,17 @@ app.get('/api/v1/chat', async (req, res) => {
app.post('/api/v1/chat', [blockCheck], async (req, res) => {
try {
const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody } = req.body
const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody, selfDestructSeconds } = req.body
const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content)
const destructSec = parseSelfDestructSeconds(selfDestructSeconds)
const payload = {
senderId,
receiverId,
content: body || content,
replyToId: replyToId || undefined,
forwardedFrom: fwdBody || fwdParsed || undefined
forwardedFrom: fwdBody || fwdParsed || undefined,
expiresAt: buildExpiresAt(destructSec)
}
const newMessage = new MessageModel(payload)
@@ -201,6 +246,7 @@ app.post('/api/v1/chat', [blockCheck], async (req, res) => {
res.status(201).json({ data: formatted })
emitChatEvents(io, formatted, senderId, receiverId)
scheduleMessageExpiry(io, newMessage)
} catch (error) {
console.error('Error sending message:', error)
res.status(500).json({ error: 'Server error' })
@@ -263,7 +309,14 @@ app.post("/api/v1/notification/send-sms", async (req, res) => {
app.post('/api/v1/chat/file', [blockCheck], async (req, res) => {
try {
const { senderId, receiverId, content, replyToId, fileType } = req.body
const { senderId, receiverId, content, replyToId, fileType, selfDestructSeconds, viewOnce } = req.body
const destructSec = parseSelfDestructSeconds(selfDestructSeconds)
const viewOnceFlag = parseViewOnce(viewOnce)
const resolvedFileType = fileType || undefined
if (viewOnceFlag && !isViewOnceMediaType(resolvedFileType)) {
return res.status(422).json({ error: 'View once is only for image, video, and voice' })
}
const { file } = req.files
let fileUrl = null
if (file) {
@@ -282,19 +335,94 @@ app.post('/api/v1/chat/file', [blockCheck], async (req, res) => {
receiverId,
content: content || '',
file: fileUrl,
fileType: fileType || undefined,
replyToId: replyToId || undefined
fileType: resolvedFileType,
replyToId: replyToId || undefined,
expiresAt: buildExpiresAt(destructSec),
viewOnce: viewOnceFlag
})
await newMessage.save()
const formatted = await enrichMessage(newMessage)
res.status(201).json({ data: formatted })
emitChatEvents(io, formatted, senderId, receiverId)
scheduleMessageExpiry(io, newMessage)
} catch (error) {
console.error('Error sending message:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.post('/api/v1/chat/view-once/open', [blockCheck], async (req, res) => {
try {
const userId = String(req.user._id)
const { messageId } = req.body
if (!messageId) {
return res.status(422).json({ error: 'messageId is required' })
}
const result = await openViewOnceMessage(messageId, userId)
if (result.error) {
return res.status(result.status || 400).json({ error: result.error })
}
res.json({
file: result.file,
fileType: result.fileType,
messageId: String(result.message._id)
})
} catch (error) {
console.error('view-once open error:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.post('/api/v1/chat/view-once/complete', [blockCheck], async (req, res) => {
try {
const userId = String(req.user._id)
const { messageId } = req.body
if (!messageId) {
return res.status(422).json({ error: 'messageId is required' })
}
const result = await completeViewOnceMessage(io, messageId, userId)
if (result.error) {
return res.status(result.status || 400).json({ error: result.error })
}
res.json({ deletedIds: result.deletedIds })
} catch (error) {
console.error('view-once complete error:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.delete('/api/v1/chat', [blockCheck], async (req, res) => {
try {
const userId = String(req.user._id)
const { messageIds } = req.body
if (!Array.isArray(messageIds) || messageIds.length === 0) {
return res.status(422).json({ message: 'پیامی انتخاب نشده است' })
}
const messages = await MessageModel.find({
_id: { $in: messageIds },
senderId: userId
})
if (!messages.length) {
return res.status(403).json({ message: 'فقط پیام‌های خودتان قابل حذف هستند' })
}
const deletedIds = await deleteMessagesAndNotify(io, messages, { deletedBy: userId })
res.json({ deletedIds, message: 'پیام‌ها حذف شدند' })
} catch (error) {
console.error('Error deleting messages:', error)
res.status(500).json({ error: 'Server error' })
}
})
// Errors
require('./middlewares/exception')(app)
require('./middlewares/404')(app)
@@ -336,6 +464,11 @@ io.on('connection', (socket) => {
// Free Project
require('./services/AdvertisingCron')
require('./services/ProjectCron')
cron.schedule('*/30 * * * * *', () => {
purgeExpiredMessages(io).catch((err) => console.error('expired messages cron:', err))
})
cron.schedule('0 0 1 * *', async () => {
try {
await UserModel.updateMany({}, {