Files
EMI-Backend/routes/post.js

1037 lines
30 KiB
JavaScript

var express = require('express');
var router = express.Router();
const DB = require("./../mongoDB.js");
const Post = require("./../def/post.js");
const Notifications = require("./../notifications.js");
const { translateText, normalizeLanguageCode } = require("../utils/chatTranslation.js");
DB.getDB.then((DB) => {
const getProfileId = (req) => {
const rawProfileId = req.cookies.profile_id || req.query.profile_id || req.body.profile_id || req.profileInfo?._id;
if (!rawProfileId) return null;
if (!DB.ObjectID.isValid(rawProfileId)) return null;
return DB.ObjectID(rawProfileId);
};
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) => {
try {
const profileid = getProfileId(req);
if (!profileid) return res.status(400).json([]);
let organicPosts = await DB.getFeed(profileid);
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
const posts = mergePosts(organicPosts || [], nonOrganicPosts || []);
return res.json(posts);
} catch (error) {
console.error("Error loading organic feed", error);
return res.status(500).json([]);
}
});
/**
* @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) => {
try {
const profileid = getProfileId(req);
if (!profileid) return res.status(400).json([]);
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
let promotionalPosts = await DB.getPromotionalPosts(profileid);
const posts = mergePosts(promotionalPosts || [], nonOrganicPosts || []);
return res.json(posts);
} catch (error) {
console.error("Error loading feed", error);
return res.status(500).json([]);
}
});
/**
* @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
})
});
router.post("/translate", async (req, res) => {
let postid = req.body.postid;
let targetLang = normalizeLanguageCode(req.body.targetLang);
// Return ack immediately
res.json({ status: "ok", message: "Translation queued" });
if (!postid || !targetLang) return;
try {
// Get post
const posts = await DB.getPostsByTag('', null); // No good way to get one post by ID directly exposed?
// Let's use dbCols directly if needed or find it. Wait, how do we get a single post?
// I'll assume DB.getPost exists, let me check that later. Actually I will use DB.postCols directly.
const post = await DB.postCols.findOne({ _id: DB.ObjectID(postid) });
if (!post || !post.content) return;
// Strip inline tags and bible tags before translating to reduce token usage and confusion,
// or just translate the raw content and let the AI handle it? The chat translator prompt says:
// "You translate chat messages. Keep meaning, tone, emojis, names, and references. Return only the translated text."
// So it can handle tags.
// To avoid huge translations or mostly-media posts
if (post.content.length > 1000) return;
const translation = await translateText({
text: post.content,
sourceLang: "auto",
targetLang: targetLang
});
if (translation && translation.translatedText) {
await DB.addTranslation(postid, targetLang, translation.translatedText);
}
} catch (error) {
console.error("Error in background post translation", error);
}
});
/**
* @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