Skip to content

Commit a996456

Browse files
committed
pushed
1 parent 249c7a6 commit a996456

File tree

5 files changed

+339
-25
lines changed

5 files changed

+339
-25
lines changed

pages/[subdomain]/profile/[name]/[[...tab]]/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ export default function ProfilePage({
10041004
<p className='mb-4'>
10051005
Their user generated content is hidden and they cannot view your content.
10061006
<br />
1007-
If you believe this person is violating our terms of service, or for more help, visit our <a href='https://discord.gg/j6RxRdqq4A' target='_blank' rel='noopener noreferrer'>Discord server</a>.
1007+
If you believe this person is violating our terms of service, or for more help, visit our <a className='text-blue-500 underline' href='https://discord.gg/j6RxRdqq4A' target='_blank' rel='noopener noreferrer'>Discord server</a>.
10081008
</p>
10091009
<button
10101010
className='px-3 py-1 rounded-md bg-green-500 hover:bg-green-600'

pages/api/latest-reviews/index.ts

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cleanReview from '@root/lib/cleanReview';
44
import { LEVEL_DEFAULT_PROJECTION, USER_DEFAULT_PROJECTION } from '@root/models/constants/projections';
55
import { PipelineStage } from 'mongoose';
66
import type { NextApiRequest, NextApiResponse } from 'next';
7+
import GraphType from '../../../constants/graphType';
78
import apiWrapper from '../../../helpers/apiWrapper';
89
import { getEnrichLevelsPipelineSteps, getEnrichUserConfigPipelineStage } from '../../../helpers/enrich';
910
import { logger } from '../../../helpers/logger';
@@ -12,7 +13,7 @@ import dbConnect from '../../../lib/dbConnect';
1213
import { getUserFromToken } from '../../../lib/withAuth';
1314
import Review from '../../../models/db/review';
1415
import User from '../../../models/db/user';
15-
import { LevelModel, ReviewModel, UserModel } from '../../../models/mongoose';
16+
import { GraphModel, LevelModel, ReviewModel, UserModel } from '../../../models/mongoose';
1617

1718
export default apiWrapper({ GET: {} }, async (req: NextApiRequest, res: NextApiResponse) => {
1819
const token = req.cookies?.token;
@@ -34,7 +35,8 @@ export async function getLatestReviews(gameId: GameId, reqUser: User | null = nu
3435
const lookupPipelineUser: PipelineStage[] = getEnrichLevelsPipelineSteps(reqUser);
3536

3637
try {
37-
const reviews = await ReviewModel.aggregate([
38+
// Create the basic aggregation pipeline
39+
const pipeline: PipelineStage[] = [
3840
{
3941
$match: {
4042
isDeleted: { $ne: true },
@@ -88,10 +90,89 @@ export async function getLatestReviews(gameId: GameId, reqUser: User | null = nu
8890
}
8991
},
9092
...getEnrichUserConfigPipelineStage(gameId, { excludeCalcs: true, localField: 'userId._id', lookupAs: 'userId.config' }),
91-
{
92-
$limit: 7,
93-
},
94-
]);
93+
];
94+
95+
// If the requesting user exists, add stages to filter out reviews from blocked users
96+
if (reqUser) {
97+
// Filter out reviews where the reviewer is blocked by the requesting user
98+
pipeline.push(
99+
// Lookup to check if the reviewer is blocked
100+
{
101+
$lookup: {
102+
from: GraphModel.collection.name,
103+
let: { reviewerId: '$userId._id' },
104+
pipeline: [
105+
{
106+
$match: {
107+
$expr: {
108+
$and: [
109+
{ $eq: ['$source', reqUser._id] },
110+
{ $eq: ['$target', '$$reviewerId'] },
111+
{ $eq: ['$type', GraphType.BLOCK] },
112+
{ $eq: ['$sourceModel', 'User'] },
113+
{ $eq: ['$targetModel', 'User'] }
114+
]
115+
}
116+
}
117+
}
118+
],
119+
as: 'reviewerBlockStatus'
120+
}
121+
},
122+
// Only include reviews where the reviewer is not blocked
123+
{
124+
$match: {
125+
'reviewerBlockStatus': { $size: 0 }
126+
}
127+
},
128+
129+
// Filter out reviews where the level author is blocked by the requesting user
130+
{
131+
$lookup: {
132+
from: GraphModel.collection.name,
133+
let: { levelAuthorId: '$levelId.userId' },
134+
pipeline: [
135+
{
136+
$match: {
137+
$expr: {
138+
$and: [
139+
{ $eq: ['$source', reqUser._id] },
140+
{ $eq: ['$target', '$$levelAuthorId'] },
141+
{ $eq: ['$type', GraphType.BLOCK] },
142+
{ $eq: ['$sourceModel', 'User'] },
143+
{ $eq: ['$targetModel', 'User'] }
144+
]
145+
}
146+
}
147+
}
148+
],
149+
as: 'levelAuthorBlockStatus'
150+
}
151+
},
152+
// Only include reviews where the level author is not blocked
153+
{
154+
$match: {
155+
'levelAuthorBlockStatus': { $size: 0 }
156+
}
157+
},
158+
159+
// Remove the block status fields from the output
160+
{
161+
$project: {
162+
reviewerBlockStatus: 0,
163+
levelAuthorBlockStatus: 0
164+
}
165+
}
166+
);
167+
}
168+
169+
// Add the limit as the final stage
170+
pipeline.push({
171+
$limit: 7,
172+
});
173+
174+
// Execute the aggregation pipeline
175+
const reviews = await ReviewModel.aggregate(pipeline);
95176

96177
return reviews.map(review => {
97178
cleanReview(review.levelId.complete, reqUser, review);

pages/api/search/index.ts

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { LEVEL_SEARCH_DEFAULT_PROJECTION, USER_DEFAULT_PROJECTION } from '@root/
88
import { Aggregate, FilterQuery, PipelineStage, Types } from 'mongoose';
99
import type { NextApiRequest, NextApiResponse } from 'next';
1010
import { getDifficultyRangeFromName } from '../../../components/formatted/formattedDifficulty';
11+
import GraphType from '../../../constants/graphType';
1112
import TimeRange from '../../../constants/timeRange';
1213
import apiWrapper from '../../../helpers/apiWrapper';
14+
import { getContentFilterForBlockedUsers } from '../../../helpers/contentFilterHelpers';
1315
import { getEnrichLevelsPipelineSteps, getEnrichUserConfigPipelineStage } from '../../../helpers/enrich';
1416
import { logger } from '../../../helpers/logger';
1517
import cleanUser from '../../../lib/cleanUser';
1618
import dbConnect from '../../../lib/dbConnect';
1719
import { getUserFromToken } from '../../../lib/withAuth';
1820
import { EnrichedLevel } from '../../../models/db/level';
1921
import User from '../../../models/db/user';
20-
import { LevelModel, StatModel, UserModel } from '../../../models/mongoose';
22+
import { GraphModel, LevelModel, StatModel, UserModel } from '../../../models/mongoose';
2123
import { BlockFilterMask, SearchQuery } from '../../[subdomain]/search';
2224

2325
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -410,6 +412,84 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
410412
...getEnrichUserConfigPipelineStage(gameId, { excludeCalcs: true, localField: 'userId._id', lookupAs: 'userId.config' }),
411413
] as PipelineStage.Lookup[];
412414

415+
// Add a stage to filter out levels from blocked users
416+
const blockFilterStage: PipelineStage[] = reqUser ? [
417+
// Lookup to check if the level author is blocked by the user
418+
{
419+
$lookup: {
420+
from: GraphModel.collection.name,
421+
let: { authorId: '$userId' },
422+
pipeline: [
423+
{
424+
$match: {
425+
$expr: {
426+
$and: [
427+
{ $eq: ['$source', reqUser._id] },
428+
{ $eq: ['$target', '$$authorId'] },
429+
{ $eq: ['$type', GraphType.BLOCK] },
430+
{ $eq: ['$sourceModel', 'User'] },
431+
{ $eq: ['$targetModel', 'User'] }
432+
]
433+
}
434+
}
435+
}
436+
],
437+
as: 'blockStatus'
438+
}
439+
},
440+
// Only include levels where the author is not blocked
441+
{
442+
$match: {
443+
'blockStatus': { $size: 0 }
444+
}
445+
},
446+
// Remove the blockStatus field
447+
{
448+
$project: {
449+
blockStatus: 0
450+
}
451+
}
452+
] : [];
453+
454+
// Add a separate filter stage for after userId is unwound
455+
const blockFilterAfterUnwindStage: PipelineStage[] = reqUser ? [
456+
// Lookup to check if the level author is blocked by the user
457+
{
458+
$lookup: {
459+
from: GraphModel.collection.name,
460+
let: { authorId: '$userId._id' },
461+
pipeline: [
462+
{
463+
$match: {
464+
$expr: {
465+
$and: [
466+
{ $eq: ['$source', reqUser._id] },
467+
{ $eq: ['$target', '$$authorId'] },
468+
{ $eq: ['$type', GraphType.BLOCK] },
469+
{ $eq: ['$sourceModel', 'User'] },
470+
{ $eq: ['$targetModel', 'User'] }
471+
]
472+
}
473+
}
474+
}
475+
],
476+
as: 'blockStatus'
477+
}
478+
},
479+
// Only include levels where the author is not blocked
480+
{
481+
$match: {
482+
'blockStatus': { $size: 0 }
483+
}
484+
},
485+
// Remove the blockStatus field
486+
{
487+
$project: {
488+
blockStatus: 0
489+
}
490+
}
491+
] : [];
492+
413493
try {
414494
// eslint-disable-next-line @typescript-eslint/no-explicit-any
415495
let agg: Aggregate<any[]> | undefined = undefined;
@@ -421,6 +501,8 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
421501
// Prepare the facet pipeline with potential author limiting
422502
const facetPipeline = [
423503
...(lookupUserBeforeSort ? lookupUserStage : []),
504+
// Apply block filter after looking up users but before further processing
505+
...(lookupUserBeforeSort ? blockFilterAfterUnwindStage : []),
424506
// NB: projection is typically supposed to be the last stage of the pipeline, but we need it here because of potential sorting by calc_playattempts_unique_users_count
425507
{ $project: { ...projection } },
426508
{
@@ -465,6 +547,8 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
465547
{ $skip: skip },
466548
{ $limit: limit },
467549
...(lookupUserBeforeSort ? [] : lookupUserStage),
550+
// If user lookup happens after pagination, apply block filter here
551+
...(lookupUserBeforeSort ? [] : blockFilterAfterUnwindStage),
468552
...getEnrichLevelsPipelineSteps(new Types.ObjectId(userId) as unknown as User) as PipelineStage.Lookup[]
469553
);
470554

@@ -476,13 +560,14 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
476560
metadata: [
477561
// NB: need this stage here because it alters the count
478562
...statLookupAndMatchStage,
563+
...(reqUser ? blockFilterStage as any[] : []), // Add block filter for accurate count
479564
{ $count: 'totalRows' },
480565
]
481566
}),
482567
data: facetPipeline,
483568
},
484569
},
485-
]);
570+
] as any);
486571
} else {
487572
// eslint-disable-next-line @typescript-eslint/no-explicit-any
488573
let statMatchQuery: FilterQuery<any> = {};
@@ -493,6 +578,26 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
493578
statMatchQuery = { complete: true };
494579
}
495580

581+
// Create the byStat aggregation with block filtering
582+
const byStatPipeline: PipelineStage[] = [
583+
...(lookupUserBeforeSort ? lookupUserStage : []),
584+
// Apply block filter after looking up users
585+
...(lookupUserBeforeSort ? blockFilterAfterUnwindStage : []),
586+
// NB: projection is typically supposed to be the last stage of the pipeline, but we need it here because of potential sorting by calc_playattempts_unique_users_count
587+
// TODO: instead can have an optional $addFields here, then do the projection after
588+
{ $project: { ...projection, userMovesTs: 1 } },
589+
{ $sort: sortObj.reduce((acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), {}) },
590+
{ $skip: skip },
591+
{ $limit: limit },
592+
...(lookupUserBeforeSort ? [] : lookupUserStage),
593+
// If user lookup happens after pagination, apply block filter here
594+
...(lookupUserBeforeSort ? [] : blockFilterAfterUnwindStage),
595+
// note this last getEnrichLevelsPipeline is "technically a bit wasteful" if they select Hide Solved or In Progress
596+
// Because technically the above statLookupAndMatchStage will have this data already...
597+
// But since the results are limited by limit, this is constant time and not a big deal to do the lookup again...
598+
...getEnrichLevelsPipelineSteps(new Types.ObjectId(userId) as unknown as User) as PipelineStage.Lookup[],
599+
];
600+
496601
agg = StatModel.aggregate([
497602
{
498603
$match: {
@@ -538,26 +643,14 @@ export async function doQuery(gameId: GameId, query: SearchQuery, reqUser?: User
538643
'$facet': {
539644
...(query.disableCount === 'true' ? {} : {
540645
metadata: [
646+
...(reqUser ? blockFilterStage as any[] : []), // Add block filter for accurate count
541647
{ $count: 'totalRows' },
542648
]
543649
}),
544-
data: [
545-
...(lookupUserBeforeSort ? lookupUserStage : []),
546-
// NB: projection is typically supposed to be the last stage of the pipeline, but we need it here because of potential sorting by calc_playattempts_unique_users_count
547-
// TODO: instead can have an optional $addFields here, then do the projection after
548-
{ $project: { ...projection, userMovesTs: 1 } },
549-
{ $sort: sortObj.reduce((acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), {}) },
550-
{ $skip: skip },
551-
{ $limit: limit },
552-
...(lookupUserBeforeSort ? [] : lookupUserStage),
553-
// note this last getEnrichLevelsPipeline is "technically a bit wasteful" if they select Hide Solved or In Progress
554-
// Because technically the above statLookupAndMatchStage will have this data already...
555-
// But since the results are limited by limit, this is constant time and not a big deal to do the lookup again...
556-
...getEnrichLevelsPipelineSteps(new Types.ObjectId(userId) as unknown as User) as PipelineStage.Lookup[],
557-
],
650+
data: byStatPipeline,
558651
},
559652
},
560-
]);
653+
] as any);
561654
}
562655

563656
const res = (await agg)[0];

0 commit comments

Comments
 (0)