322 lines
9.0 KiB
JavaScript
322 lines
9.0 KiB
JavaScript
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,
|
|
}
|