Skip to content

Commit 4a257f0

Browse files
authored
Merge pull request #156 from PretendoNetwork/work/grpc-2
backend: Add "posts by user" endpoints for user pages
2 parents fb400d9 + baf26a0 commit 4a257f0

12 files changed

Lines changed: 128 additions & 51 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* !!! HEY
2+
* This type lives in apps/miiverse-api/src/services/internal/contract/page.ts
3+
* Modify it there and copy-paste here! */
4+
5+
export type PageDto<T> = {
6+
items: T[];
7+
};

apps/juxtaposition-ui/src/api/post.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { apiFetchUser } from '@/fetch';
2+
import type { PageDto } from '@/api/page';
23
import type { UserTokens } from '@/fetch';
34

45
/* !!! HEY
@@ -63,3 +64,22 @@ export async function getPostById(tokens: UserTokens, post_id: string): Promise<
6364
const post = await apiFetchUser<PostDto>(tokens, `/api/v1/posts/${post_id}`);
6465
return post;
6566
}
67+
68+
export async function getPostsByPoster(tokens: UserTokens, poster_pid: number, offset: number): Promise<PageDto<PostDto> | null> {
69+
const params = new URLSearchParams({
70+
posted_by: poster_pid.toString(),
71+
offset: offset.toString()
72+
});
73+
const posts = await apiFetchUser<PageDto<PostDto>>(tokens, `/api/v1/posts?${params}`);
74+
return posts;
75+
}
76+
77+
export async function getPostsByEmpathy(tokens: UserTokens, empathy_by: number, offset: number): Promise<PageDto<PostDto> | null> {
78+
const params = new URLSearchParams({
79+
empathy_by: empathy_by.toString(),
80+
offset: offset.toString(),
81+
include_replies: 'true'
82+
});
83+
const posts = await apiFetchUser<PageDto<PostDto>>(tokens, `/api/v1/posts?${params}`);
84+
return posts;
85+
}

apps/juxtaposition-ui/src/database.js

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,6 @@ async function getPostByID(postID) {
102102
});
103103
}
104104

105-
async function getPostsByUserID(userID) {
106-
verifyConnected();
107-
return POST.find({
108-
pid: userID,
109-
parent: null,
110-
removed: false
111-
});
112-
}
113-
114105
async function getPostReplies(postID, number) {
115106
verifyConnected();
116107
return POST.find({
@@ -141,16 +132,6 @@ async function getUserPostRepliesAfterTimestamp(post, numberOfPosts) {
141132
}).limit(numberOfPosts);
142133
}
143134

144-
async function getNumberUserPostsByID(userID, number, includeRemoved = false) {
145-
verifyConnected();
146-
return POST.find({
147-
pid: userID,
148-
parent: null,
149-
message_to_pid: null,
150-
...(includeRemoved ? {} : { removed: false })
151-
}).sort({ created_at: -1 }).limit(number);
152-
}
153-
154135
async function getTotalPostsByUserID(userID) {
155136
verifyConnected();
156137
return POST.find({
@@ -254,16 +235,6 @@ async function getUserPostsAfterTimestamp(post, numberOfPosts) {
254235
}).limit(numberOfPosts);
255236
}
256237

257-
async function getUserPostsOffset(pid, limit, offset) {
258-
verifyConnected();
259-
return POST.find({
260-
pid: pid,
261-
parent: null,
262-
message_to_pid: null,
263-
removed: false
264-
}).skip(offset).limit(limit).sort({ created_at: -1 });
265-
}
266-
267238
async function getCommunityPostsAfterTimestamp(post, numberOfPosts) {
268239
verifyConnected();
269240
return POST.find({
@@ -529,17 +500,14 @@ module.exports = {
529500
getNumberVerifiedCommunityPostsByID,
530501
getNewPostsByCommunity,
531502
getPostsByCommunityKey,
532-
getPostsByUserID,
533503
getPostReplies,
534504
getUserPostRepliesAfterTimestamp,
535-
getNumberUserPostsByID,
536505
getTotalPostsByUserID,
537506
getPostByID,
538507
getDuplicatePosts,
539508
getEndpoints,
540509
getEndPoint,
541510
getUserPostsAfterTimestamp,
542-
getUserPostsOffset,
543511
getCommunityPostsAfterTimestamp,
544512
getNewsFeed,
545513
getNewsFeedAfterTimestamp,

apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const { COMMUNITY } = require('@/models/communities');
1010
const util = require('@/util');
1111
const storage = multer.memoryStorage();
1212
const upload = multer({ storage: storage });
13-
const { config } = require('@/config');
1413
const { logger } = require('@/logger');
14+
const { getPostsByPoster } = require('@/api/post');
1515
const router = express.Router();
1616

1717
router.get('/posts', async function (req, res) {
@@ -87,7 +87,7 @@ router.get('/accounts/:pid', async function (req, res) {
8787
return res.redirect('/404');
8888
}
8989
const userSettings = await database.getUserSettings(req.params.pid);
90-
const posts = await database.getNumberUserPostsByID(req.params.pid, config.postLimit);
90+
const posts = (await getPostsByPoster(req.tokens, req.params.pid, 0)).items;
9191
const communityMap = await util.getCommunityHash();
9292
const userMap = util.getUserHash();
9393
const reasonMap = util.getReasonMap();

apps/juxtaposition-ui/src/services/juxt-web/routes/console/userpage.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
const express = require('express');
22
const multer = require('multer');
33
const moment = require('moment');
4+
const { getPostsByPoster, getPostsByEmpathy } = require('@/api/post');
45
const database = require('@/database');
56
const util = require('@/util');
67
const { getUserFriendPIDs } = util;
78
const { POST } = require('@/models/post');
89
const { SETTINGS } = require('@/models/settings');
9-
const redis = require('@/redisCache');
10-
const { config } = require('@/config');
1110
const { logger } = require('@/logger');
1211
const router = express.Router();
1312
const upload = multer({ dest: 'uploads/' });
@@ -148,11 +147,7 @@ async function userPage(req, res, userID) {
148147
}
149148

150149
const userSettings = await database.getUserSettings(userID);
151-
let posts = JSON.parse(await redis.getValue(`${userID}-user_page_posts`));
152-
if (!posts) {
153-
posts = await database.getNumberUserPostsByID(userID, config.postLimit, res.locals.moderator);
154-
await redis.setValue(`${userID}_user_page_posts`, JSON.stringify(posts), 60 * 60 * 1);
155-
}
150+
const posts = (await getPostsByPoster(req.tokens, userID, 0))?.items;
156151

157152
const numPosts = await database.getTotalPostsByUserID(userID);
158153
const communityMap = await util.getCommunityHash();
@@ -215,7 +210,7 @@ async function userRelations(req, res, userID) {
215210
let selection;
216211

217212
if (req.params.type === 'yeahs') {
218-
const posts = await POST.find({ yeahs: userID, removed: false }).sort({ created_at: -1 }).limit(config.postLimit);
213+
const posts = (await getPostsByEmpathy(req.tokens, userID, 0))?.items;
219214
const communityMap = await util.getCommunityHash();
220215
const bundle = {
221216
posts,
@@ -304,7 +299,7 @@ async function morePosts(req, res, userID) {
304299
if (!offset) {
305300
offset = 0;
306301
}
307-
const posts = await database.getUserPostsOffset(userID, config.postLimit, offset);
302+
const posts = (await getPostsByPoster(req.tokens, userID, offset))?.items;
308303

309304
const bundle = {
310305
posts,
@@ -334,7 +329,7 @@ async function moreYeahPosts(req, res, userID) {
334329
if (!offset) {
335330
offset = 0;
336331
}
337-
const posts = await POST.find({ yeahs: userID, removed: false }).sort({ created_at: -1 }).skip(offset).limit(config.postLimit);
332+
const posts = (await getPostsByEmpathy(req.tokens, userID, offset))?.items;
338333

339334
const bundle = {
340335
posts: posts,

apps/miiverse-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"supertest": "^7.1.1",
4141
"tga": "^1.0.3",
4242
"xmlbuilder": "^15.1.1",
43-
"zod": "^3.21.4"
43+
"zod": "^3.25.7"
4444
},
4545
"devDependencies": {
4646
"@pretendonetwork/eslint-config": "^0.0.11",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* !!! HEY
2+
* This type has a copy in apps/juxtaposition-ui/src/api/page.ts
3+
* Make sure to copy over any modifications! */
4+
5+
export type PageDto<T> = {
6+
items: T[];
7+
};
8+
9+
export function mapPages<T>(items: T[]): PageDto<T> {
10+
return {
11+
items
12+
};
13+
}

apps/miiverse-api/src/services/internal/contract/post.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { IPost } from '@/types/mongoose/post';
22

33
/* !!! HEY
4-
* This type has a copy in apps/juxtaposition-ui/src/models/api/post.ts
4+
* This type has a copy in apps/juxtaposition-ui/src/api/post.ts
55
* Make sure to copy over any modifications! */
66

77
/* This type is the contract for the frontend. If we make changes to the db, this shape should be kept. */
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as z from 'zod/v4';
2+
3+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Zod schema is inferred here
4+
export function pageSchema(limit = 50) {
5+
return z.object({
6+
limit: z.coerce.number().min(1).max(limit).default(10),
7+
offset: z.coerce.number().min(0).default(0)
8+
});
9+
}
10+
11+
export type PageControls = z.infer<ReturnType<typeof pageSchema>>;
Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,53 @@
11
import express from 'express';
2-
import { getPostByID } from '@/database';
2+
import * as z from 'zod/v4';
3+
import { Post } from '@/models/post';
34
import { errors } from '@/services/internal/errors';
4-
import { handle } from '@/services/internal/utils';
5+
import { deleteOptional, filterRemovedPosts, handle } from '@/services/internal/utils';
56
import { guards } from '@/services/internal/middleware/guards';
67
import { mapPost } from '@/services/internal/contract/post';
8+
import { mapPages } from '@/services/internal/contract/page';
9+
import { pageSchema } from '@/services/internal/pagination';
710

811
export const postsRouter = express.Router();
912

13+
// Get posts by topic tag, poster, or empathy
14+
postsRouter.get('/posts', guards.user, handle(async ({ req, res }) => {
15+
// the idea is that any combination of these can be left undefined
16+
const query = z.object({
17+
topic_tag: z.string().optional(),
18+
posted_by: z.coerce.number().optional(),
19+
empathy_by: z.coerce.number().optional(),
20+
include_replies: z.stringbool().default(false)
21+
}).and(pageSchema()).parse(req.query);
22+
23+
const posts = await Post.find(deleteOptional({
24+
pid: query.posted_by,
25+
topic_tag: query.topic_tag,
26+
yeahs: query.empathy_by,
27+
28+
message_to_pid: null, // messages aren't really posts
29+
...query.include_replies ? {} : { parent: null },
30+
...filterRemovedPosts(res.locals.account)
31+
})).sort({ created_at: -1 }).skip(query.offset).limit(query.limit);
32+
33+
// PageDto<PostDto>
34+
return mapPages(posts.map(mapPost));
35+
}));
36+
37+
// Get post by id
1038
postsRouter.get('/posts/:post_id', guards.guest, handle(async ({ req, res }) => {
11-
const post = await getPostByID(req.params.post_id);
12-
if (!post || (post.removed && res.locals.account?.moderator !== true)) {
39+
const params = z.object({
40+
post_id: z.string().length(21)
41+
}).parse(req.params);
42+
43+
const post = await Post.findOne({
44+
id: params.post_id,
45+
...filterRemovedPosts(res.locals.account)
46+
});
47+
if (!post) {
1348
throw new errors.notFound('Post not found');
1449
}
1550

51+
// PostDto
1652
return mapPost(post);
1753
}));

0 commit comments

Comments
 (0)