Skip to content

Commit

Permalink
Add pin post routes
Browse files Browse the repository at this point in the history
  • Loading branch information
SupertigerDev committed Dec 12, 2024
1 parent 370cf87 commit 1844da2
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 4 deletions.
14 changes: 14 additions & 0 deletions prisma/migrations/20241212130729_add_pinned_post/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "PinnedPost" (
"postId" TEXT NOT NULL,
"pinnedById" TEXT NOT NULL,
"pinnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "PinnedPost_pkey" PRIMARY KEY ("postId")
);

-- AddForeignKey
ALTER TABLE "PinnedPost" ADD CONSTRAINT "PinnedPost_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PinnedPost" ADD CONSTRAINT "PinnedPost_pinnedById_fkey" FOREIGN KEY ("pinnedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
16 changes: 16 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ model User {
scheduledForContentDeletion ScheduleAccountContentDelete?
pinnedPosts PinnedPost[]
}

model UserConnection {
Expand Down Expand Up @@ -801,13 +804,26 @@ model Post {
poll PostPoll?
pinned PinnedPost?
reminders Reminder[]
@@index([createdById, createdAt])
}

model PinnedPost {
postId String @id
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
pinnedById String
pinnedBy User @relation(fields: [pinnedById], references: [id])
pinnedAt DateTime @default(now())
}

model PostPoll {
id String @id
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ async function scheduleDeleteAccountContent() {
prisma.postLike.deleteMany({ where: { postId: { in: postIds } } }),
prisma.postPoll.deleteMany({ where: { postId: { in: postIds } } }),
prisma.attachment.deleteMany({ where: { postId: { in: postIds } } }),
prisma.announcementPost.deleteMany({ where: { postId: { in: postIds } } }),
prisma.pinnedPost.deleteMany({ where: { postId: { in: postIds } } }),
]);
}
if (messages.length || posts.length || likedPosts.length) {
Expand Down
5 changes: 5 additions & 0 deletions src/routes/posts/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { postsGetLikes } from './postGetLikes';
import { postsDiscover } from './postsDiscover';
import { postPollVote } from './postPollVote';
import { postsGetAnnouncement } from './postsGetAnnouncement';
import { postPin } from './postPin';
import { postPinDelete } from './postPinDelete';

const PostsRouter = Router();

Expand All @@ -39,4 +41,7 @@ postUnlike(PostsRouter);
postsGetComments(PostsRouter);
postsGetLikes(PostsRouter);

postPin(PostsRouter);
postPinDelete(PostsRouter);

export { PostsRouter };
34 changes: 34 additions & 0 deletions src/routes/posts/postPin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Request, Response, Router } from 'express';
import { param } from 'express-validator';
import { customExpressValidatorResult, generateError } from '../../common/errorHandler';
import { authenticate } from '../../middleware/authenticate';
import { pinPost } from '../../services/Post';

export function postPin(Router: Router) {
Router.post('/posts/:postId/pin', authenticate(), param('postId').isString().withMessage('postId must be a string!').isLength({ min: 1, max: 100 }).withMessage('postId length must be between 1 and 100 characters.'), route);
}

interface Param {
postId: string;
}

async function route(req: Request, res: Response) {
const params = req.params as unknown as Param;

const validateError = customExpressValidatorResult(req);

if (validateError) {
return res.status(400).json(validateError);
}

const [status, error] = await pinPost(params.postId, req.userCache.id).catch((e) => {
console.error(e);
return [false, generateError('Something went wrong. Try again later.')];
});

if (error) {
return res.status(400).json(error);
}

res.json({ success: status });
}
34 changes: 34 additions & 0 deletions src/routes/posts/postPinDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Request, Response, Router } from 'express';
import { param } from 'express-validator';
import { customExpressValidatorResult, generateError } from '../../common/errorHandler';
import { authenticate } from '../../middleware/authenticate';
import { unpinPost } from '../../services/Post';

export function postPinDelete(Router: Router) {
Router.delete('/posts/:postId/pin', authenticate(), param('postId').isString().withMessage('postId must be a string!').isLength({ min: 1, max: 100 }).withMessage('postId length must be between 1 and 100 characters.'), route);
}

interface Param {
postId: string;
}

async function route(req: Request, res: Response) {
const params = req.params as unknown as Param;

const validateError = customExpressValidatorResult(req);

if (validateError) {
return res.status(400).json(validateError);
}

const [status, error] = await unpinPost(params.postId, req.userCache.id).catch((e) => {
console.error(e);
return [false, generateError('Something went wrong. Try again later.')];
});

if (error) {
return res.status(400).json(error);
}

res.json({ success: status });
}
1 change: 1 addition & 0 deletions src/routes/posts/postsGet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function route(req: Request, res: Response) {
limit,
afterId: query.afterId,
beforeId: query.beforeId,
hidePins: true,
});

res.json(posts);
Expand Down
4 changes: 3 additions & 1 deletion src/routes/users/userDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ async function route(req: Request, res: Response) {
const requesterId = req.userCache?.id;
const recipientId = req.params.userId || requesterId;

const [details, error] = await getUserDetails(requesterId || '123', recipientId, req.userIP);
const includePinnedPosts = req.query.includePinnedPosts === 'true';

const [details, error] = await getUserDetails(requesterId || '123', recipientId, req.userIP, includePinnedPosts);
if (error) {
return res.status(400).json(error);
}
Expand Down
94 changes: 93 additions & 1 deletion src/services/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,17 @@ interface FetchPostsOpts {
skip?: number;
cursor?: Prisma.PostWhereUniqueInput;
requesterIpAddress: string;
hidePins?: boolean;
}

export async function fetchPosts(opts: FetchPostsOpts) {
const where = {
...opts.where,
...(opts.hidePins
? {
OR: [{ pinned: { isNot: null }, commentToId: { not: null } }, { pinned: null }],
}
: {}),
...(opts.afterId ? { id: { lt: opts.afterId } } : {}),
...(opts.beforeId ? { id: { gt: opts.beforeId } } : {}),

Expand Down Expand Up @@ -201,7 +207,7 @@ export async function fetchPosts(opts: FetchPostsOpts) {
}
: undefined),
deleted: null,
};
} as Prisma.PostWhereUniqueInput;

const posts = await prisma.post.findMany({
where,
Expand Down Expand Up @@ -468,6 +474,7 @@ export async function deletePost(postId: string, userId: string): Promise<Custom
prisma.postPoll.deleteMany({ where: { postId } }),
prisma.attachment.deleteMany({ where: { postId } }),
prisma.announcementPost.deleteMany({ where: { postId } }),
prisma.pinnedPost.deleteMany({ where: { postId } }),
]);

return [true, null];
Expand Down Expand Up @@ -708,3 +715,88 @@ export async function removeAnnouncementPost(postId: string) {
},
});
}

export async function pinPost(postId: string, requesterId: string) {
const post = await prisma.post.findUnique({
where: { id: postId, createdById: requesterId, deleted: null },
});
if (!post) {
return [null, generateError('Post not found.')] as const;
}

const isPinned = await prisma.pinnedPost.findUnique({
where: { postId, pinnedById: requesterId },
});

if (isPinned) {
return [null, generateError('Post is already pinned.')] as const;
}

const pinCount = await prisma.pinnedPost.count({
where: { pinnedById: requesterId },
});

if (pinCount >= 4) {
return [null, generateError('You can only pin up to 4 posts.')] as const;
}

await prisma.pinnedPost.create({
data: {
pinnedById: requesterId,
postId,
},
});

return [true, null] as const;
}

export async function unpinPost(postId: string, requesterId: string) {
const isPinned = await prisma.pinnedPost.findUnique({
where: { postId, pinnedById: requesterId },
});
if (!isPinned) return [null, generateError('Post is not pinned.')] as const;
await prisma.pinnedPost.delete({
where: {
postId,
pinnedById: requesterId,
},
});
return [true, null] as const;
}

interface fetchPinnedPostsOpts {
userId: string;
requesterUserId: string;
bypassBlocked?: boolean;
requesterIpAddress: string;
}

export async function fetchPinnedPosts(opts: fetchPinnedPostsOpts) {
const posts = await prisma.post.findMany({
orderBy: { pinned: { pinnedAt: 'desc' } },
where: {
deleted: null,
createdById: opts.userId,
pinned: {
pinnedById: opts.requesterUserId,
},
...(!opts.bypassBlocked
? {
createdBy: {
friends: {
none: {
status: FriendStatus.BLOCKED,
recipientId: opts.requesterUserId,
},
},
},
}
: undefined),
},
include: constructPostInclude(opts.requesterUserId),
});

updateViews(posts, opts.requesterIpAddress);

return posts;
}
13 changes: 11 additions & 2 deletions src/services/User/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FriendStatus } from '../../types/Friend';
import { dateToDateTime, excludeFields, prisma, publicUserExcludeFields } from '../../common/database';
import { generateId } from '../../common/flakeId';

import { createPostNotification, fetchLatestPost, PostNotificationType } from '../Post';
import { createPostNotification, fetchLatestPost, fetchPinnedPost, fetchPinnedPosts, PostNotificationType } from '../Post';

import { leaveVoiceChannel } from '../Voice';
import { MessageInclude } from '../Message';
Expand Down Expand Up @@ -260,7 +260,7 @@ export const updateUserPresence = async (userId: string, presence: PresencePaylo
return ['Presence updated.', null];
};

export const getUserDetails = async (requesterId: string, recipientId: string, requesterIpAddress: string) => {
export const getUserDetails = async (requesterId: string, recipientId: string, requesterIpAddress: string, includePinnedPosts = false) => {
const user = await prisma.user.findFirst({
where: { id: recipientId },
select: {
Expand Down Expand Up @@ -349,6 +349,14 @@ export const getUserDetails = async (requesterId: string, recipientId: string, r
bypassBlocked: isAdmin,
requesterIpAddress,
});
const pinnedPosts = includePinnedPosts
? await fetchPinnedPosts({
userId: recipientId,
requesterUserId: requesterId,
bypassBlocked: isAdmin,
requesterIpAddress,
})
: [];

const isBlocked = await isUserBlocked(requesterId, recipientId);

Expand Down Expand Up @@ -381,6 +389,7 @@ export const getUserDetails = async (requesterId: string, recipientId: string, r
latestPost,
profile: user.profile,
...(!isSuspensionExpired ? { suspensionExpiresAt: suspension?.expireAt } : {}),
...(includePinnedPosts ? { pinnedPosts } : {}),
},
null,
];
Expand Down

0 comments on commit 1844da2

Please sign in to comment.