full
This commit is contained in:
321
utils/reelsTabFeed.js
Normal file
321
utils/reelsTabFeed.js
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user