Skip to content

Commit

Permalink
Edit Post schema; Fix thread fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
Vrezerino committed Sep 12, 2024
1 parent 475ef8c commit bcef78f
Show file tree
Hide file tree
Showing 10 changed files with 8,719 additions and 51 deletions.
71 changes: 48 additions & 23 deletions app/api/posts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ import { NextRequest, NextResponse } from 'next/server';
const banlist = process.env.BANLIST;

export const POST = async (req: NextRequest) => {

// This function is called after an insertion of a new post
const fetchPostWithPostNum = async (postId: any, retries = 5, delay = 100) => {
for (let i = 0; i < retries; i++) {
const insertedPost = await (await db())
.collection('posts')
.findOne({ _id: postId }, { projection: { postNum: 1 } });

if (insertedPost && insertedPost.postNum !== undefined) {
return insertedPost.postNum;
}

// Wait before the next attempt
await new Promise(resolve => setTimeout(resolve, delay));
}

console.error('PostNum not found after multiple attempts.');
}

// Make new post and insert into database
try {
const ip = req.headers.get('x-real-ip');
if (ip && banlist && findExactInString(ip, banlist)) {
Expand All @@ -39,36 +59,26 @@ export const POST = async (req: NextRequest) => {
const regex = new RegExp('\\b' + sanitizedBoardName + '\\b');
if (!regex.test(boards)) throw { message: 'Unknown board', status: 400 };

/**
* FormData can not have arrays as is; the array of replied-to post
* numbers was stringified first on the client-side
*/
const replyTo = JSON.parse(formData.get('replyTo') as string);
const content = sanitizeString(formData.get('content') as string);
const OP = (formData.get('OP') as unknown) === 'true';
const threadNum = formData.get('thread') as string;

const newPost: NewPostType = {
// Content will be stripped of multiple linebreaks and spaces
content: sanitizeString(formData.get('content') as string),
title: sanitizeString(formData.get('title') as string),
replyTo,
/**
* OPs haven't replied to any other posts and they are programmatically not able to
* do so directly.
*/
OP: replyTo?.length > 0 ? false : true,
content,
// Generate title from truncated post content, only if you're OP
title: OP ? content.length > 25
? content.substring(0, 21) + '...'
: content : '',
OP,
thread: parseInt(threadNum),
board: sanitizedBoardName,
replies: [],
date: new Date(),
IP: req.headers.get('x-real-ip') || 'none'
}


if (file?.size > 0) newPost.image = file;

if (!newPost.title) {
newPost.title = newPost.content.length > 25
? newPost.content.substring(0, 21) + '...'
: newPost.content;
}

const { error } = newPostSchema.validate(newPost);
if (error) throw { message: error.message, status: 400 };

Expand Down Expand Up @@ -106,10 +116,25 @@ export const POST = async (req: NextRequest) => {
newPost.imageUrl = file?.size > 0 ? `${AWS_URL}/img/posts/${filename}` : '';
delete newPost.image;

// Finally, save post to database
// Save post to database
const result = await (await db()).collection('posts').insertOne(newPost);

if (result.acknowledged && result.insertedId !== null) {
// Get the postNum of the post we just inserted
const newPostNum = await fetchPostWithPostNum(result.insertedId);

// Add the postNum to all recipient's reply array
const recipients: number[] = JSON.parse(formData.get('replyTo') as string);

if (recipients?.length > 0) {
await (await db()).collection('posts').updateMany(
{ 'postNum': { $in: recipients } },
{
$push: { replies: newPostNum }
}
);
}

if (result.acknowledged && result.insertedId) {
return NextResponse.json('Created', { status: 201 });
}
} catch (e) {
Expand Down
31 changes: 20 additions & 11 deletions app/dashboard/[board]/[postNum]/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,33 @@ export const getPost = async (board: string, pn: string) => {
const post = await (await db()).collection('posts').findOne({ board, postNum });

// Recursive function to fetch replies for a given post number
const fetchReplies = async (postNum : number) => {
const fetchReplies = async (postNum: number) => {
// Find the post with the given postNum and get its replies
const postWithReplies = await (await db())
.collection('posts')
.findOne({ postNum: postNum }, { projection: { replies: 1 } });

// Exclude IP field
const projection: FindOptions = { projection: { IP: 0 } };
const data = await (await db())
.collection('posts')
.find({ replyTo: { $in: [postNum] } }, projection)
.toArray();
if (postWithReplies && postWithReplies.replies && postWithReplies.replies.length > 0) {
// Fetch all posts whose postNum is in the replies array
const repliesArray = await (await db())
.collection('posts')
.find({ postNum: { $in: postWithReplies.replies } }, projection) // Use the projection to limit fields
.toArray();

// Iterate through replies and fetch their own replies recursively
for (const reply of data) {
reply.replies = await fetchReplies(reply.postNum);
}

return data;
// Iterate through replies and fetch their own replies recursively
for (const reply of repliesArray) {
reply.replies = await fetchReplies(reply.postNum);
}

return repliesArray;
}
}

const replies = await fetchReplies(postNum);

// Only add the replies member to object if the replies array isn't empty
return JSON.parse(JSON.stringify({ ...post, ...(replies?.length > 0 && { replies }) }));
return JSON.parse(JSON.stringify({ ...post, ...(replies && { replies }) }));
};
20 changes: 10 additions & 10 deletions app/dashboard/[board]/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,26 @@ export const getBumpedPosts = async (board: string) => {
{
$lookup: {
from: 'posts',
localField: 'replyTo',
foreignField: 'postNum',
as: 'parentPosts'
localField: 'replies', // Array of postNums
foreignField: 'postNum', // Find posts by postNum
as: 'repliedPosts' // Output the matched posts in repliedPosts array
}
},
// Add a field "lastReplyDate" that is the date of the last reply, or null if no replies
{
$addFields: {
sortBy: {
lastReplyDate: {
$cond: {
if: { $eq: ['$OP', true] }, // Check if the post is OP
then: '$date', // If OP, sort by the post date
else: { $max: '$parentPosts.date' } // Otherwise, sort by the max date in parentPosts
if: { $gt: [{ $size: '$repliedPosts' }, 0] }, // Check if there are any replies
then: { $max: '$repliedPosts.date' }, // Get the maximum date from replies
else: '$date' // If no replies, use the post's own date
}
}
}
},
// Sort by lastReplyDate in descending order
{
$sort: {
'sortBy': -1 // Sort by the calculated sort date
}
$sort: { lastReplyDate: -1 }
},
{
$project: {
Expand Down
4 changes: 3 additions & 1 deletion app/lib/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface PostType {
title: string | null;
thread: number | null;
content: string;
postNum: number;
date: Date;
Expand All @@ -14,12 +15,13 @@ export interface PostType {

export type NewPostType = {
title: string;
thread: number | null;
content: string;
date: Date;
OP: boolean;
IP: string;
board: string;
replyTo: Array<number>
replies: Array<number>
imageUrl?: string
image?: File
};
Expand Down
5 changes: 3 additions & 2 deletions app/lib/newPostSchema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Joi from 'joi';

export const newPostSchema = Joi.object({
title: Joi.string().max(25),
title: Joi.string().allow(null, '').max(25),
content: Joi.string().min(1).max(1500).trim().required(),
thread: Joi.number().allow(null),
image: Joi.any(),
board: Joi.string().required(),
replyTo: Joi.array(),
OP: Joi.boolean(),
date: Joi.date(),
replies: Joi.array(),
IP: Joi.string()
});
3 changes: 2 additions & 1 deletion app/ui/dashboard/latestPosts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const LatestPosts = ({ posts }: { posts: PostType[] }) => {
{post?.imageUrl && <Image src={post?.imageUrl} alt={post.title || ''} width={32} height={32} />}
</div>
<div className='flex-1 min-w-0 ms-1'>
<Link href={`dashboard/${post?.board}/${(post?.replyTo && post?.replyTo[0]) ?? post?.postNum}`}>
{/* Link to threadnum and #postnum if post is not an OP, otherwise just postnum */}
<Link href={`dashboard/${post?.board}${post?.thread ? '/' + post.thread + '#' : '' + '/'}${post?.postNum}`}>
<p className='text-sm font-medium text-gray-900 truncate dark:text-white'>
{post?.title}
</p>
Expand Down
2 changes: 1 addition & 1 deletion app/ui/dashboard/post/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const PostContent = ({
}
}
return (
<div key={`post-${post.postNum}`} id={post.postNum.toString()} className='post dark:post-darkmode flex bg-white border border-neutral-200 rounded-sm shadow sm:flex-row md:max-w-xl dark:border-neutral-800 dark:bg-neutral-900'>
<div key={`post-${post.postNum}`} id={post.postNum?.toString()} className='post dark:post-darkmode flex bg-white border border-neutral-200 rounded-sm shadow sm:flex-row md:max-w-xl dark:border-neutral-800 dark:bg-neutral-900'>
{post.imageUrl && (
<div key={`imgContainer-${post.postNum}`} className='min-w-40 relative'>
<Link key={post.postNum} href={post.imageUrl} target='_blank'>
Expand Down
8 changes: 8 additions & 0 deletions app/ui/dashboard/post/postForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ const PostFormBig = ({
// Arrays must be stringified in FormData objects — parse it on server
formData.set('replyTo', JSON.stringify(recipients));

// If postForm gets existing OP as prop, the new post will be a reply. Otherwise it is itself OP
if (op) {
formData.set('OP', 'false');
formData.set('thread', op.postNum.toString());
} else {
formData.set('OP', 'true');
}

// Get the name of the board you're posting on and set it to formData
formData.set('board', pathname.split('/')[2]);

Expand Down
Loading

0 comments on commit bcef78f

Please sign in to comment.