Files
modstagram-back/index.js
2026-07-08 21:01:30 +03:30

493 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const app = express();
const path = require('path');
const http = require('http').createServer(app);
const cors = require('cors');
const moment = require('moment-jalaali');
const MessageModel = require('./models/MessageModel');
const fs = require('fs-extra');
const cron = require('node-cron');
const UserModel = require('./models/UserModel');
const blockCheck = require('./middlewares/blockCheck');
const rateLimit = require('express-rate-limit');
const { default: axios } = require('axios');
const allowedOrigins = [
'http://localhost:3000',
'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت
'http://localhost:3002',
'http://localhost:3003',
'http://localhost',
'https://modstagram.com',
'http://modstagram.com',
'http://193.151.143.243:3004',
'https://www.modstagram.com',
'https://api.modstagram.com',
'https://panel.modstagram.com',
'https://www.panel.modstagram.com',
'https://panel.modstagram.ir',
'https://www.panel.modstagram.ir',
'ionic://localhost',
'capacitor://localhost',
// IPهای محلی (برای تست در شبکه داخلی)/
/^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/,
/^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/,
];
app.use(cors({
origin: (origin, callback) => {
// در محیط توسعه، همه originها مجاز باشن
if (process.env.NODE_ENV !== 'production') {
return callback(null, true);
}
// اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده
if (!origin) {
return callback(null, true);
}
// چک لیست یا RegExp
const isAllowed = allowedOrigins.some(item => {
if (typeof item === 'string') return item === origin;
if (item instanceof RegExp) return item.test(origin);
return false;
});
if (isAllowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'],
credentials: true,
optionsSuccessStatus: 200 // برای مرورگرهای قدیمی
}));
// Socket.IO CORS
const io = require('socket.io')(http, {
cors: {
origin: (origin, callback) => {
if (process.env.NODE_ENV !== 'production') {
return callback(null, true);
}
if (!origin) {
return callback(null, true);
}
const isAllowed = allowedOrigins.some(item => {
if (typeof item === 'string') return item === origin;
if (item instanceof RegExp) return item.test(origin);
return false;
});
if (isAllowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true
}
});
// افزایش limit برای Base64 (1000mb کافیه)
app.use(express.json({ limit: '1000mb' }));
app.use(express.urlencoded({ limit: '1000mb', extended: true }));
// مسیرهای مشخص قبل از /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');
// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل
app.use((req, res, next) => {
if (req.path === '/api/v1/posts/create' && req.method === 'POST') {
console.log('DEBUG: Skipping body parsers for /api/v1/posts/create');
return next();
}
express.json()(req, res, next);
});
app.use((req, res, next) => {
if (req.path === '/api/v1/posts/create' && req.method === 'POST') {
return next();
}
express.urlencoded({ extended: true })(req, res, next);
});
require('./middlewares')(app);
const limiter = rateLimit({
windowMs: 3 * 60 * 1000,
max: 3000,
standardHeaders: true,
legacyHeaders: false,
message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.',
handler: (req, res) => {
const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000);
res.set('Retry-After', retryAfter);
res.status(429).json({
error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.`
});
}
});
app.use(limiter);
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 = activeMessageFilter({
$or: [
{ senderId: sid, receiverId: rid },
{ senderId: rid, receiverId: sid }
]
})
const totalMessagesCount = await MessageModel.countDocuments(filter)
const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1
const skip = (pageNumber - 1) * limitPerPage
const messages = await MessageModel.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitPerPage)
await MessageModel.updateMany(
{ _id: { $in: messages.map((m) => m._id) }, receiverId: sid },
{ $set: { readStatus: 1 } }
)
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)
res.status(500).json({ error: 'Server error' })
}
})
app.post('/api/v1/chat', [blockCheck], async (req, res) => {
try {
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,
expiresAt: buildExpiresAt(destructSec)
}
const newMessage = new MessageModel(payload)
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.get('/api/v1/chat/read', async (req, res) => {
try {
const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود
// پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است
const messages = await MessageModel.find(
{ senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند
)
// به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده"
await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } })
res.status(200).json({ message: 'Message read status updated successfully' })
} catch (error) {
console.error('Error updating message read status:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.post("/api/v1/notification/send-sms", async (req, res) => {
const { receiverId, message } = req.body;
const user = await UserModel.findById(receiverId);
if (!user || !user.mobile) return res.status(404).json({ error: "User not found" });
console.log("3");
const mobile = user.mobile
const user_name = user.user_name
const data = JSON.stringify({
mobile,
templateId: '876533',
parameters: [
{ name: 'USER', value: user_name }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt'
},
data
}
axios(config)
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error)
})
res.status(200).json({ success: true });
});
app.post('/api/v1/chat/file', [blockCheck], async (req, res) => {
try {
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) {
const uploadDir = path.join(__dirname, '../storage/messages')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const uniqueFileName = `${Date.now()}-${file.name}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath)
fileUrl = `/messages/${uniqueFileName}`
}
const newMessage = new MessageModel({
senderId,
receiverId,
content: content || '',
file: fileUrl,
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)
io.on('connection', (socket) => {
socket.on('joinUser', ({ userId }) => {
if (userId) socket.join(`user:${userId}`)
})
socket.on('joinChat', ({ userId, receiverId }) => {
if (userId && receiverId) {
socket.join(`chat:${userId}:${receiverId}`)
socket.join(`chat:${receiverId}:${userId}`)
}
})
socket.on('typing', ({ senderId, receiverId }) => {
io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId })
})
socket.on('stopTyping', ({ senderId, receiverId }) => {
io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId })
})
socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => {
try {
if (messageId) {
await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 })
io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', {
messageId,
status: 'seen'
})
}
} catch (e) {
console.error('messageSeen error', e)
}
})
})
// 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({}, {
daily_free_request: 1,
last_free_request_date: new Date(),
monthly_free_offer: 1,
last_free_offer_date: new Date()
})
console.log('Monthly free requests reset for all users.')
} catch (error) {
console.error('Error resetting daily free requests:', error)
}
}, {
scheduled: true,
timezone: 'Asia/Tehran'
})
module.exports = (port) => {
http.listen(port, () => {
console.log(`HTTP server is running on port ${port}`)
})
}