diff --git a/controllers/application/stories/storyController.js b/controllers/application/stories/storyController.js index 2185a75..e8495fe 100644 --- a/controllers/application/stories/storyController.js +++ b/controllers/application/stories/storyController.js @@ -21,6 +21,29 @@ function storyMediaUrl (storagePath) { return normalized.replace('/root/modstagram-back/storage', '/storage') } +function clamp01 (value, fallback) { + const num = Number(value) + if (Number.isNaN(num)) return fallback + return Math.min(1, Math.max(0, num)) +} + +function sanitizeOverlays (raw) { + if (!Array.isArray(raw)) return [] + return raw + .filter((o) => o && typeof o.text === 'string' && o.text.trim().length) + .slice(0, 20) + .map((o) => ({ + text: String(o.text).slice(0, 300), + x: clamp01(o.x, 0.5), + y: clamp01(o.y, 0.5), + font_size: clamp01(o.font_size, 0.06) || 0.06, + color: typeof o.color === 'string' ? o.color.slice(0, 32) : '#ffffff', + background: typeof o.background === 'string' ? o.background.slice(0, 32) : '', + rotation: Number.isFinite(Number(o.rotation)) ? Number(o.rotation) : 0, + align: ['left', 'center', 'right'].includes(o.align) ? o.align : 'center' + })) +} + async function resolveViewerId (req) { const token = req.header('Authorization')?.split(' ')[1] if (!token) return null @@ -35,7 +58,7 @@ async function resolveViewerId (req) { const createStoryBase64 = async (req, res) => { try { const userId = req.user._id - const { file } = req.body + const { file, overlays } = req.body if (!file?.data || !file?.type) { return res.status(400).json({ error: true, message: 'فایل استوری الزامی است' }) @@ -52,6 +75,7 @@ const createStoryBase64 = async (req, res) => { user_id: userId, media_path: filePath, media_type: mediaType, + overlays: sanitizeOverlays(overlays), expires_at: new Date(Date.now() + STORY_TTL_MS), viewers: [] }) @@ -96,6 +120,7 @@ const getStoriesFeed = async (req, res, next) => { _id: story._id, media_path: storyMediaUrl(story.media_path), media_type: story.media_type, + overlays: story.overlays || [], createdAt: story.createdAt, expires_at: story.expires_at, viewed: viewerId @@ -174,6 +199,30 @@ const markStoryViewed = async (req, res) => { } } +const updateStory = async (req, res) => { + try { + const userId = req.user._id + const { storyId } = req.params + const { overlays } = req.body + + const story = await StoryModel.findOne({ _id: storyId, user_id: userId }) + if (!story || story.expires_at <= new Date()) { + return res.status(404).json({ message: 'استوری یافت نشد' }) + } + + story.overlays = sanitizeOverlays(overlays) + await story.save() + + return res.status(200).json({ + message: 'استوری ویرایش شد', + overlays: story.overlays + }) + } catch (err) { + console.error('updateStory:', err) + return res.status(500).json({ message: 'خطا در ویرایش استوری' }) + } +} + const deleteStory = async (req, res) => { try { const userId = req.user._id @@ -193,5 +242,6 @@ module.exports = { createStoryBase64, getStoriesFeed, markStoryViewed, + updateStory, deleteStory } diff --git a/controllers/application/users/getUserController.js b/controllers/application/users/getUserController.js index 771c83c..753a7ed 100644 --- a/controllers/application/users/getUserController.js +++ b/controllers/application/users/getUserController.js @@ -970,10 +970,13 @@ const getPostsWeb = async (req, res, next) => { lng, feedMode, seedPostId, + sort, } = req.query; const reelsMode = feedMode === "reels" || type === "video"; const gridMode = feedMode === "grid"; + // فید اصلی صفحه پست‌ها: فقط جدیدترین‌ها (بدون شخصی‌سازی) + const latestMode = sort === "latest" && !reelsMode; const postFilter = { status: { $ne: null } }; if (type === 'video' || type === 'image') { @@ -1090,6 +1093,24 @@ const getPostsWeb = async (req, res, next) => { explorePosts = await mapPostsWithUsers(explorePosts, usersMap, decodedToken); + // فید اصلی: فقط جدیدترین‌ها بر اساس تاریخ ساخت + if (latestMode) { + explorePosts.sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) + ); + const totalItems = explorePosts.length; + const startIndex = (parseInt(page, 10) - 1) * parseInt(limit, 10); + const paginatedPosts = explorePosts.slice( + startIndex, + startIndex + parseInt(limit, 10) + ); + return res.status(200).json({ + posts: paginatedPosts, + totalPages: Math.ceil(totalItems / limit) || 1, + totalItems, + }); + } + const { posts: paginatedPosts, totalItems } = await paginatePersonalizedExplore({ posts: explorePosts, usersById, diff --git a/index.js b/index.js index bf9b23a..b6bd829 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +require('dotenv').config(); const express = require('express'); const app = express(); const path = require('path'); @@ -486,8 +487,7 @@ cron.schedule('0 0 1 * *', async () => { scheduled: true, timezone: 'Asia/Tehran' }) -module.exports = (port) => { - http.listen(port, () => { - console.log(`HTTP server is running on port ${port}`) - }) -} +const port = process.env.APP_PORT || 3002; +http.listen(port, () => { + console.log(`HTTP server is running on port ${port}`); +}); diff --git a/models/StoryModel.js b/models/StoryModel.js index e7b1294..e8448c0 100644 --- a/models/StoryModel.js +++ b/models/StoryModel.js @@ -1,6 +1,18 @@ const mongoose = require('mongoose') const timestamp = require('mongoose-timestamp') +// متن‌های روی استوری با مختصات نسبی (۰ تا ۱) تا در هر ابعادی درست رندر شوند +const overlaySchema = new mongoose.Schema({ + text: { type: String, default: '' }, + x: { type: Number, default: 0.5 }, + y: { type: Number, default: 0.5 }, + font_size: { type: Number, default: 0.06 }, + color: { type: String, default: '#ffffff' }, + background: { type: String, default: '' }, + rotation: { type: Number, default: 0 }, + align: { type: String, default: 'center' } +}, { _id: false }) + const storySchema = new mongoose.Schema({ user_id: { type: mongoose.Schema.Types.ObjectId, @@ -17,6 +29,10 @@ const storySchema = new mongoose.Schema({ enum: ['image', 'video'], required: true }, + overlays: { + type: [overlaySchema], + default: [] + }, expires_at: { type: Date, required: true, diff --git a/routes/application/stories/index.js b/routes/application/stories/index.js index 701a745..37d7163 100644 --- a/routes/application/stories/index.js +++ b/routes/application/stories/index.js @@ -6,6 +6,7 @@ const { createStoryBase64, getStoriesFeed, markStoryViewed, + updateStory, deleteStory } = require('../../../controllers/application/stories/storyController') @@ -14,6 +15,7 @@ const router = express.Router() router.get('/feed', getStoriesFeed) router.post('/create-base64', auth, blockCheck, isHalfRegister, createStoryBase64) router.post('/view', auth, blockCheck, markStoryViewed) +router.patch('/:storyId', auth, blockCheck, updateStory) router.delete('/:storyId', auth, blockCheck, deleteStory) module.exports = router diff --git a/server.js b/server.js index 6a5b686..a9a6000 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,13 @@ require('dotenv').config() console.log('APP_PORT:', process.env.APP_PORT) -const bootApplication = require('./index') +// index.js خودش روی پورت listen می‌کند (اجرای مستقیم روی سرور: node index.js). +// اینجا فقط require می‌کنیم تا سرور بالا بیاید. اگر نسخه‌ای از index.js تابع +// export کند، آن را هم صدا می‌زنیم تا با هر دو حالت سازگار باشد. +const app = require('./index') console.log('Index loaded, booting...') -const port = process.env.APP_PORT || 3002 -bootApplication(port) -console.log(`✅ Server should be running on port ${port}`) +if (typeof app === 'function') { + const port = process.env.APP_PORT || 3002 + app(port) +}