This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

321
utils/reelsTabFeed.js Normal file
View File

@@ -0,0 +1,321 @@
const UserFollowModel = require('../models/UserFollowModel')
const NEAR_ME_RADIUS_METERS = 5000
const PostBookmarkModel = require('../models/PostBookmarkModel')
const PostModel = require('../models/PostModel')
const UserModel = require('../models/UserModel')
const LikeModel = require('../models/LikeModel')
const ExploreInteractionModel = require('../models/ExploreInteractionModel')
const { getBlockerUserIds } = require('./blockVisibility')
const { filterPostsByDistance } = require('./nearbyPosts')
const { postStatusFilter } = require('./moderationStatus')
const { buildUnifiedInterestScore } = require('./exploreAlgorithm')
async function getSuggestedFollowUsers(userId, limit = 15) {
const viewer = await UserModel.findById(userId)
.select('expertise blocked_users')
.lean()
const following = await UserFollowModel.find({ followerId: userId })
.select('followingId')
.lean()
const excludeIds = new Set(
following.map((f) => String(f.followingId)).concat(String(userId))
)
const blockerIds = new Set(
(await getBlockerUserIds(UserModel, userId)).map(String)
)
const [interactions, likedPosts, interestWeights] = await Promise.all([
ExploreInteractionModel.find({ viewerId: userId })
.sort({ createdAt: -1 })
.limit(40)
.select('targetId authorId action')
.lean(),
LikeModel.find({ userId })
.sort({ createdAt: -1 })
.limit(30)
.select('postId')
.lean(),
// شخصی‌سازی متقابل: علایقی که کاربر توی فروشگاه/آموزش/بیلبورد نشون داده هم اینجا اثر بذاره
buildUnifiedInterestScore(userId),
])
const interactedAuthorIds = new Set()
interactions.forEach((row) => {
if (row.authorId) interactedAuthorIds.add(String(row.authorId))
})
const likedPostIds = likedPosts.map((l) => l.postId).filter(Boolean)
if (likedPostIds.length) {
const likedPostsData = await PostModel.find({ _id: { $in: likedPostIds } })
.select('user_id')
.lean()
likedPostsData.forEach((p) => interactedAuthorIds.add(String(p.user_id)))
}
let preferredExpertises = new Set()
if (viewer?.expertise) preferredExpertises.add(viewer.expertise)
if (interactedAuthorIds.size) {
const authors = await UserModel.find({
_id: { $in: [...interactedAuthorIds] },
})
.select('expertise')
.lean()
authors.forEach((a) => {
if (a.expertise) preferredExpertises.add(a.expertise)
})
}
const candidates = await UserModel.find({
_id: { $nin: [...excludeIds] },
user_name: { $nin: [null, ''] },
})
.select(
'_id user_name first_name last_name expertise profile_image user_level'
)
.limit(120)
.lean()
const filtered = candidates.filter(
(u) => !blockerIds.has(String(u._id)) && !excludeIds.has(String(u._id))
)
if (!filtered.length) return []
const candidateIds = filtered.map((u) => u._id)
const recentPosts = await PostModel.find({
user_id: { $in: candidateIds },
status: postStatusFilter(),
is_archived: { $ne: true },
})
.sort({ createdAt: -1 })
.lean()
const postByUser = {}
for (const p of recentPosts) {
const uid = String(p.user_id)
if (!postByUser[uid]) postByUser[uid] = p
}
const scored = filtered
.filter((u) => postByUser[String(u._id)])
.map((u) => {
let score = Math.random() * 2
if (preferredExpertises.has(u.expertise)) score += 8
if (interactedAuthorIds.has(String(u._id))) score += 12
if (u.expertise && interestWeights[u.expertise]) {
score += Math.min(20, interestWeights[u.expertise])
}
const post = postByUser[String(u._id)]
score += (post.likes?.length || 0) * 0.15
return { user: u, score, previewPost: postByUser[String(u._id)] }
})
.sort((a, b) => b.score - a.score)
.slice(0, limit)
return scored.map(({ user, previewPost }) => ({
_id: user._id,
user_name: user.user_name,
first_name: user.first_name,
last_name: user.last_name,
expertise: user.expertise,
user_level: user.user_level,
profile_image: user.profile_image,
previewPost: {
_id: previewPost._id,
type: previewPost.type,
files: previewPost.files,
caption: previewPost.caption,
},
is_following: false,
}))
}
async function getFollowingReelsFeed({
userId,
page,
limit,
postFilter,
mapPostsWithUsers,
}) {
const follows = await UserFollowModel.find({ followerId: userId })
.select('followingId')
.lean()
const followingIds = follows.map((f) => f.followingId)
if (!followingIds.length) {
const suggestedUsers = await getSuggestedFollowUsers(userId)
return {
posts: [],
totalPages: 0,
totalItems: 0,
feedMeta: { emptyFollowing: true, suggestedUsers },
}
}
const suggestedUsers = await getSuggestedFollowUsers(userId)
let posts = await PostModel.find({
...postFilter,
status: postStatusFilter(),
user_id: { $in: followingIds },
})
.sort({ createdAt: -1 })
.lean()
const userIds = [...new Set(posts.map((p) => String(p.user_id)))]
const users = await UserModel.find({ _id: { $in: userIds } })
.select(
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified user_score rate blocked_users'
)
.lean()
posts = await mapPostsWithUsers(posts, users, { id: userId })
const totalItems = posts.length
const pageNum = parseInt(page, 10) || 1
const limitNum = parseInt(limit, 10) || 10
const startIndex = (pageNum - 1) * limitNum
const paginatedPosts = posts.slice(startIndex, startIndex + limitNum)
return {
posts: paginatedPosts,
totalPages: Math.ceil(totalItems / limitNum) || 1,
totalItems,
feedMeta: { emptyFollowing: false, suggestedUsers },
}
}
async function getSavedReelsFeed({
userId,
page,
limit,
postFilter,
mapPostsWithUsers,
}) {
const pageNum = Math.max(1, parseInt(page, 10) || 1)
const limitNum = Math.min(30, Math.max(1, parseInt(limit, 10) || 10))
const skip = (pageNum - 1) * limitNum
const totalItems = await PostBookmarkModel.countDocuments({ userId })
const bookmarks = await PostBookmarkModel.find({ userId })
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitNum)
.lean()
const postIds = bookmarks.map((b) => b.postId)
if (!postIds.length) {
return {
posts: [],
totalPages: Math.ceil(totalItems / limitNum) || 0,
totalItems,
feedMeta: { emptySaved: true },
}
}
let posts = await PostModel.find({
_id: { $in: postIds },
status: postStatusFilter(),
...postFilter,
}).lean()
const orderMap = new Map(postIds.map((id, idx) => [String(id), idx]))
posts.sort(
(a, b) =>
(orderMap.get(String(a._id)) ?? 0) - (orderMap.get(String(b._id)) ?? 0)
)
const userIds = [...new Set(posts.map((p) => p.user_id))]
const users = await UserModel.find({ _id: { $in: userIds } })
.select(
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified user_score rate blocked_users'
)
.lean()
posts = await mapPostsWithUsers(posts, users, { id: userId })
return {
posts,
totalPages: Math.ceil(totalItems / limitNum) || 1,
totalItems,
feedMeta: { emptySaved: totalItems === 0 },
}
}
async function getNearMeReelsFeed({
lat,
lng,
page,
limit,
postFilter,
mapPostsWithUsers,
decodedToken,
}) {
const parsedLat = parseFloat(lat)
const parsedLng = parseFloat(lng)
if (!Number.isFinite(parsedLat) || !Number.isFinite(parsedLng)) {
return {
posts: [],
totalPages: 0,
totalItems: 0,
feedMeta: { needsLocation: true },
}
}
let posts = await PostModel.find({
...postFilter,
status: postStatusFilter(),
lat: { $nin: [null, ''] },
lng: { $nin: [null, ''] },
})
.sort({ createdAt: -1 })
.lean()
posts = filterPostsByDistance(
posts,
parsedLat,
parsedLng,
NEAR_ME_RADIUS_METERS
)
if (!posts.length) {
return {
posts: [],
totalPages: 0,
totalItems: 0,
feedMeta: { emptyNearMe: true },
}
}
const userIds = [...new Set(posts.map((p) => p.user_id))]
const users = await UserModel.find({ _id: { $in: userIds } })
.select(
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified user_score rate lat lng blocked_users'
)
.lean()
posts = await mapPostsWithUsers(posts, users, decodedToken)
const totalItems = posts.length
const pageNum = parseInt(page, 10) || 1
const limitNum = parseInt(limit, 10) || 10
const startIndex = (pageNum - 1) * limitNum
const paginatedPosts = posts.slice(startIndex, startIndex + limitNum)
return {
posts: paginatedPosts,
totalPages: Math.ceil(totalItems / limitNum) || 1,
totalItems,
feedMeta: { emptyNearMe: totalItems === 0 },
}
}
module.exports = {
getSuggestedFollowUsers,
getFollowingReelsFeed,
getSavedReelsFeed,
getNearMeReelsFeed,
}