var express = require('express'); var router = express.Router(); const DB = require("./../mongoDB.js"); const Post = require("./../def/post.js"); const Notifications = require("./../notifications.js"); DB.getDB.then((DB) => { const getProfileId = (req) => { return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id); }; const postBelongToProfile = (post, profileid) => { if (!post) return false; return post.profileid == profileid; }; const generateNonOrganicPosts = async (req, profileid) => { let posts = []; // News Posts const newsPosts = await DB.getNews(); posts.push(...newsPosts); // Popular Profiles const popularProfiles = await DB.getPopularProfiles(5); content = "Active users to follow:\n"; popularProfiles.forEach((p) => { content += "@p:" + p._id + "\n"; }); let popularProfilesPost = new Post({ profileid: 1, content, nonOrganicType: "PopularUsers" }); posts.push(popularProfilesPost); // Popular Groups const popularGroups = await DB.getPopularGroups(5); content = "Popular groups to follow:\n"; popularGroups.forEach((p) => { content += "@p:" + p._id + "\n"; }); let popularGroupsPost = new Post({ profileid: 1, content, nonOrganicType: "PopularGroups" }); posts.push(popularGroupsPost); const yourFollowsInterests = await DB.getFriendsFriends(profileid); content = "People your follow has these interests:\n"; yourFollowsInterests.forEach((p) => { content += "@p:" + p + "\n"; }); let yourFollowsInterestsPost = new Post({ profileid: 1, content, nonOrganicType: "PopularUsers" }); posts.push(yourFollowsInterestsPost); return posts; }; const mergePosts = (organic, nonOrganic) => { if (!organic || organic.length < 5) return nonOrganic; //organic.push(...nonOrganic); let mergedPosts = []; while (organic.length + nonOrganic.length) { if (organic.length == 0) { mergedPosts.push(nonOrganic.shift()); continue; } if (nonOrganic.length == 0) { mergedPosts.push(organic.shift()); continue; } if (nonOrganic[0].nonOrganicType == 'News') { mergedPosts.push(nonOrganic.shift()); continue; } if (Math.random() < 0.2) { mergedPosts.push(nonOrganic.shift()); } else { mergedPosts.push(organic.shift()); } } return mergedPosts; }; /** * @swagger * tags: * name: Posts * description: Post management */ /** * @swagger * components: * schemas: * Post: * type: object * properties: * profileid: * type: string * description: The ID of the profile that created the post. * content: * type: string * description: The content of the post. * createdAt: * type: string * format: date-time * description: The timestamp when the post was created. * reactions: * type: object * description: An object containing reactions to the post, keyed by profile ID. * comments: * type: array * items: * type: object * description: An array of comments on the post. * bookmarks: * type: array * items: * type: string * description: An array of profile IDs that bookmarked the post. * nonOrganicType: * type: string * description: Type of non-organic content (e.g., "News", "PopularUsers", "PopularGroups"). * contentHistory: * type: array * items: * type: string * description: A history of content edits. * toProfile: * type: string * description: The ID of the profile this post is directed to (e.g., a group). * lastUpdated: * type: string * format: date-time * description: The timestamp when the post was last updated. * tags: * type: array * items: * type: string * description: An array of tags associated with the post. * chatSenderId: * type: string * description: The ID of the chat sender, if applicable. * * /post/organic: * get: * summary: Get the organic feed for the current user * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/organic", async (req, res) => { const profileid = getProfileId(req); let organicPosts = await DB.getFeed(profileid); //Add non-organic posts const nonOrganicPosts = await generateNonOrganicPosts(req, profileid); const posts = mergePosts(organicPosts, nonOrganicPosts); return res.json(posts); }); /** * @swagger * /post: * get: * summary: Get the feed with promotional content * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/", async (req, res) => { const profileid = getProfileId(req); //Add non-organic posts const nonOrganicPosts = await generateNonOrganicPosts(req, profileid); let promotionalPosts = await DB.getPromotionalPosts(profileid); const posts = mergePosts(promotionalPosts, nonOrganicPosts); return res.json(posts); }); /** * @swagger * /post/tag/{tag}: * get: * summary: Get posts with a specific tag * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: tag * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Post' * 400: * description: Tag is required */ router.get("/tag/:tag", async (req, res) => { const profileid = getProfileId(req); const tag = req.query.tag || req.params.tag; if(!tag) { console.log("Tag query empty: ", tag); return res.json({ status: "Tag is required", }); } console.log("Tag query: ", tag); let posts = await DB.getPostsByTag('#' + tag, profileid); return res.json(posts); }); /** * @swagger * /post/usr/{id}: * get: * summary: Get posts from a specific user * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/usr/:id", async (req, res) => { const profileId = req.params.id; const viewerProdileId = getProfileId(req); const profile = await DB.getProfileCache(profileId); const posts = await DB.getPosts(profileId, viewerProdileId); if (profile.isCourse) { //check for subscription //const viewerProdile = await DB.getProfileCache(viewerProdileId); //if (!viewerProdile.subscription || viewerProdile.subscription < (new Date()-1)) { // return res.json([posts[posts.length - 1]]); //} } return res.json(posts); }); /** * @swagger * /post/usr/{id}/images: * get: * summary: Get all image posts from a user * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/usr/:id/images", async (req, res) => { const profileid = req.params.id; const viewerProfileId = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, viewerProfileId); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/usr/{id}/embedded: * get: * summary: Get all embedded posts from a user * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/usr/:id/embedded", async (req, res) => { const profileid = req.params.id; const viewerProfileId = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, viewerProfileId, "@iframe:"); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/usr/{id}/media: * get: * summary: Get all media posts from a user * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/usr/:id/media", async (req, res) => { const profileid = req.params.id; const viewerProfileId = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, viewerProfileId, "@youtube:|@vimeo:|@hls:"); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/video/{id}: * get: * summary: Get video details by ID * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK */ router.get("/video/:id", async (req, res) => { videoId = req.params.id; return res.json([]); }); /** * @swagger * /post: * post: * summary: Create a new post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * responses: * 200: * description: OK * 403: * description: Not authorized to post to this group */ router.post("/", async (req, res) => { let post = { profileid: getProfileId(req), ...req.body } const isGroupPrivate = await DB.isGroupPrivate(post.toProfile); const isGroupNewsOnly = await DB.isGroupNewsOnly(post.toProfile); if (post.toProfile && isGroupPrivate) { let requestProfile = getProfileId(req) + ""; let group = await DB.getProfileCache(post.toProfile); if (!group.subscribed[requestProfile] && group._id != requestProfile) { return res.json({ status: "You are not part of this private group", }); } } if (post.toProfile && isGroupNewsOnly) { return res.json({ status: "This is a news only group, only the admin can post", }); } post.toProfile = post.toProfile ? DB.ObjectID(post.toProfile) : undefined; let postObj = new Post(post); let dbr = await DB.newPost(postObj); post = postObj.toObj(); post._id = dbr.insertedId; if ((post.toProfile && post.toProfile != post.profileid) || post.nonOrganicType == 'News') { await Notifications.youGotANewPost(post); } await Notifications.yourGroupMakeANewPost(post); if(!isGroupPrivate){ await Notifications.yourSubscriptionProfileHasNewPost(post); } return res.json({ status: "ok", ...post }) }); /** * @swagger * /post/react: * post: * summary: React to a post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * responses: * 200: * description: OK */ router.post("/react", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; let reaction = { type: "like", createdAt: new Date() }; r = await DB.newReaction(postid, profileid, reaction); Notifications.youGotANewReaction(postid, profileid, 'like'); return res.json({ status: "ok" }); }); /** * @swagger * /post/unreact: * post: * summary: Remove a reaction from a post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * responses: * 200: * description: OK */ router.post("/unreact", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; r = await DB.removeReaction(postid, profileid); return res.json({ status: "ok" }) }); /** * @swagger * /post/bookmark: * post: * summary: Bookmark a post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * responses: * 200: * description: OK */ router.post("/bookmark", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; r = await DB.bookmarkPost(postid, profileid); return res.json({ status: "ok" }); }) /** * @swagger * /post/unbookmark: * post: * summary: Remove a bookmark from a post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * responses: * 200: * description: OK */ router.post("/unbookmark", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; r = await DB.unbookmarkPost(postid, profileid); return res.json({ status: "ok" }) }); /** * @swagger * /post/comment: * post: * summary: Add a comment to a post * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * content: * type: string * responses: * 200: * description: OK */ router.post("/comment/", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; let content = req.body.content; let comment = { profileid: profileid, content: content, createdAt: new Date(), lastUpdated: new Date(), reactions: {} } Notifications.youGotANewPostComment(postid, profileid, content) r = await DB.newComment(postid, comment); return res.json({ status: "ok", ...comment }) }); /** * @swagger * /post/comment/react: * post: * summary: React to a comment * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * commentDate: * type: string * format: date-time * responses: * 200: * description: OK */ router.post("/comment/react", async (req, res) => { let userid = getProfileId(req); let postid = req.body.postid; let commentDate = new Date(req.body.commentDate); let reaction = { type: "like", createdAt: new Date() }; r = await DB.newCommentReaction(postid, commentDate, userid, reaction); return res.json({ status: "ok" }) }); /** * @swagger * /post/comment/unreact: * post: * summary: Remove a reaction from a comment * tags: [Posts] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * postid: * type: string * commentDate: * type: string * format: date-time * responses: * 200: * description: OK */ router.post("/comment/unreact", async (req, res) => { let profileid = getProfileId(req); let postid = req.body.postid; let commentDate = new Date(req.body.commentDate); r = await DB.removeCommentReaction(postid, commentDate, profileid); return res.json({ status: "ok" }) }); /** * @swagger * /post/images: * get: * summary: Get all image posts for the current user * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/images", async (req, res) => { const profileid = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, profileid); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/embedded: * get: * summary: Get all embedded posts for the current user * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/embedded", async (req, res) => { const profileid = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@iframe:"); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/media: * get: * summary: Get all media posts for the current user * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * posts: * type: array * items: * $ref: '#/components/schemas/Post' */ router.get("/media", async (req, res) => { const profileid = getProfileId(req); const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@youtube:|@vimeo:|@hls:"); return res.json({ status: "ok", posts }); }); /** * @swagger * /post/course/recent: * get: * summary: Get recently watched media from courses * tags: [Posts] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * recentMedia: * type: object * additionalProperties: * type: array * items: * type: object * properties: * watchRecord: * type: object * _id: * type: string * profileid: * type: string * content: * type: string * createdAt: * type: string * format: date-time * reactions: * type: object * comments: * type: array * items: * type: object * bookmarks: * type: array * items: * type: string * nonOrganicType: * type: string * contentHistory: * type: array * items: * type: string * toProfile: * type: string * lastUpdated: * type: string * format: date-time * tags: * type: array * items: * type: string * chatSenderId: * type: string * mediaProfileMap: * type: object * additionalProperties: * type: object */ router.get("/course/recent", async (req, res) => { const profileid = getProfileId(req); const profile = await DB.getProfileCache(profileid); const mediaPostsId = [] const recentMedia = {}; const mediaProfileP = [] let postPromises = []; // Get watch information from profile data Object.keys(profile.data).forEach(key => { // So far we only use watch info using postid keys if(DB.ObjectID.isValid(key)){ mediaPostsId.push(profile.data[key]); postPromises.push(DB.getPostCached(profile.data[key].postId)); if(!recentMedia[profile.data[key].profileId]){ recentMedia[profile.data[key].profileId] = []; mediaProfileP.push(DB.getProfileCache(profile.data[key].profileId)); } } }); recentMediaArray = mediaPostsId.sort((a,b)=>{ return b.ts - a.ts; }); const postObjs = await Promise.all(postPromises); const postObjsMap = postObjs.reduce((map, item)=>{ map[item._id] = item; return map }, {}); recentMediaArray.forEach(record => { const postAndRecord = { watchRecord: record, ... postObjsMap[record.postId] } recentMedia[record.profileId].push(postAndRecord); }); // Transform profiles array to dict let mediaProfile = await Promise.all(mediaProfileP); let mediaProfileMap = mediaProfile.reduce((dict, item) => { dict[item._id] = item; return dict; }, {}); return res.json({ status: "ok", recentMedia, mediaProfileMap, }); }); /** * @swagger * /post/{id}: * get: * summary: Get a specific post by ID * tags: [Posts] * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * $ref: '#/components/schemas/Post' */ router.get("/:id", async (req, res) => { const postId = req.params.id; const post = await DB.getPost(postId); return res.json({ status: "ok", ...post }); }); router.delete("/:id", async (req, res) => { const profileid = getProfileId(req) + ''; const postId = req.params.id; const post = await DB.getPost(postId); if (!postBelongToProfile(post, profileid)) return res.json({ status: "This post is not yours." }); await DB.removePost(postId); return res.json({ status: "ok" }); }); router.post("/:id", async (req, res) => { const profileid = getProfileId(req) + ''; const postId = req.params.id; const post = await DB.getPost(postId); const newContent = req.body.content; if (!postBelongToProfile(post, profileid)) return res.json({ status: "This post is not yours." }); await DB.updatePost(postId, newContent, post.content); return res.json({ status: "ok" }); }); }); module.exports = router