import { PostKind, PostVisibilityLevel, Post, PostPageKind, PostFile, PostLink, PostAccessLevel } from "../entities/Post"; import { FriendsHelper } from "./FriendsHelper"; import { DatabaseHelper } from "./DatabaseHelper"; import { UserHelper } from "./UserHelper"; import { GroupsHelper } from "./GroupsHelper"; import { GroupMembershipLevels } from "../entities/GroupMember"; import { mysql_date } from "../utils/DateUtils"; import { LikesHelper, LikesType } from "./LikesHelper"; import { CommentsHelper } from "./CommentsHelper"; import { existsSync, unlinkSync } from "fs"; import { SurveyHelper } from "./SurveyHelper"; /** * Posts helper * * @author Pierre HUBERT */ /** * Table name */ const TABLE_NAME = "texte"; /** * Database mapping */ const PostDBTypes : Record = { "texte": PostKind.POST_KIND_TEXT, "image": PostKind.POST_KIND_IMAGE, "webpage_link": PostKind.POST_KIND_WEBLINK, "pdf": PostKind.POST_KIND_PDF, "video": PostKind.POST_KIND_MOVIE, "count_down": PostKind.POST_KIND_COUNTDOWN, "sondage": PostKind.POST_KIND_SURVEY, "youtube": PostKind.POST_KIND_YOUTUBE } /** * Posts helper */ export class PostsHelper { /** * Create a new post * * @param p Information about the post * @returns The ID of the created post * @throws In case of failure */ public static async Create(p: Post) : Promise { // Extract the kind of post let kindDb : string = ""; for (const key in PostDBTypes) { if (PostDBTypes.hasOwnProperty(key) && PostDBTypes[key] == p.kind) kindDb = key; } if(kindDb == "") throw new Error("Unknown post kind: " + kindDb); // Prepare post target let userID: number, friendID: number, groupID: number; // Post on user page if(p.isUserPage) { userID = p.userPageID; friendID = p.userID; groupID = 0; } // Post on group page else { userID = p.userID; friendID = 0; groupID = p.groupID; } // Generate database entry let data = { // Post meta-data & basic info ID_personne: userID, ID_amis: friendID, group_id: groupID, date_envoi: mysql_date(), time_insert: p.timeCreate, niveau_visibilite: p.visibilityLevel, type: kindDb, texte: p.hasContent ? p.content : "", // Generic file infos size: !p.hasFile ? null : p.file.size, file_type: !p.hasFile ? null : p.file.type, path: !p.hasFile ? null : p.file.path, // Movies post idvideo: p.hasMovie ? p.movieID : null, // Countdown timer (TODO : implement) jour_fin: null, mois_fin: null, annee_fin: null, time_end: p.hasTimeEnd ? p.timeEnd : null, // Weblink url_page: !p.hasLink ? null : p.link.url, titre_page: !p.hasLink ? null : p.link.title, description_page: !p.hasLink ? null : p.link.description, image_page: !p.hasLink ? null : p.link.image } // Insert the post const postID = await DatabaseHelper.InsertRow(TABLE_NAME, data); return postID; } /** * Get the posts of a user * * @param userID The ID of the user making the request * @param targetID The ID of the target user * @param startFrom Start point (0 = none) * @param limit Maximum number of messages to fetch */ public static async GetUserPosts(userID: number, targetID: number, startFrom: number = 0, limit: number = 10) : Promise> { if(limit < 1) throw Error("Limit of post query must be greater or equal to one!"); // Determine max user visibility let level : PostVisibilityLevel = PostVisibilityLevel.VISIBILITY_PUBLIC; if(userID == targetID) level = PostVisibilityLevel.VISIBILITY_USER; else if(userID > 0 && await FriendsHelper.AreFriend(userID, targetID)) level = PostVisibilityLevel.VISIBILITY_FRIENDS; // Preprocess conditions /// ============= PERMISSION CONDITIONS ================= // Visibility level let customWhere = "((niveau_visibilite <= ?) "; let customWhereArgs = [level.toString()]; // Add user post (if user signed in) if(userID > 0) { customWhere += " OR (ID_amis = ?) "; customWhereArgs.push(userID.toString()); } customWhere += ")" /// ============= /PERMISSION CONDITIONS ================ // ============== START POINT CONDITION ================= if(startFrom != 0) { customWhere += " AND ID <= ?"; customWhereArgs.push(startFrom.toString()); } // ============== /START POINT CONDITION ================ // Perform the request const entries = await DatabaseHelper.Query({ table: TABLE_NAME, // Base conditions where: { ID_personne: targetID, group_id: 0 }, customWhere: customWhere, customWhereArgs: customWhereArgs, order: "ID DESC", limit: limit, }); return entries.map((r) => this.DBToPost(r)); } /** * Get the posts of a group * * @param userID The ID of the user making the request * @param groupID The ID of the target group * @param startFrom Start point. 0 for none */ public static async GetGroupPosts(userID: number, groupID: number, startFrom: number = 0, limit: number = 10) : Promise> { if(limit < 1) throw new Error("Limit (" + limit +") is invalid!"); // Check membership of the user const membership = await GroupsHelper.GetMembershipLevel(groupID, userID); const canSeeAllPosts = membership <= GroupMembershipLevels.MEMBER; const visibilityLevel = canSeeAllPosts ? PostVisibilityLevel.VISIBILITY_GROUP_MEMBERS : PostVisibilityLevel.VISIBILITY_PUBLIC; // Prepare request // === VISIBILITY RESTRICTION === let customWhere = "(niveau_visibilite <= ?)"; let customWhereArgs = [visibilityLevel.toString()]; // == /VISIBILITY RESTRICTION === // === START POINT === if(startFrom != 0) { customWhere += " AND ID <= ?"; customWhereArgs.push(startFrom.toString()); } // == /START POINT === const results = await DatabaseHelper.Query({ table: TABLE_NAME, where: { group_id: groupID }, customWhere: customWhere, customWhereArgs: customWhereArgs, order: "ID DESC", limit: limit }); return results.map((r) => this.DBToPost(r)); } /** * Get the latest posts of a user * * @param userID The ID of the user making the request * @param startFrom Startpoint for the research * @param limit Limit of the research (default: 10) * @param includeGroups Specify whether groups can be selected too or not */ public static async GetLatest(userID: number, startFrom: number = 10, limit: number = 10, includeGroups: boolean = false) { if(limit < 1) throw new Error("Limit of the query must be greater than 0!"); const visibilityLevel = PostVisibilityLevel.VISIBILITY_FRIENDS; // Get the list of friends of the user const friendsList = await FriendsHelper.GetList(userID, true); // Prepare the request on the database // === Membership condition === let customWhere = "("; // === FRIENDS POSTS === customWhere += "(group_id = 0 AND niveau_visibilite <= ? AND (ID_personne = ?"; let customWhereArgs = [visibilityLevel.toString(), userID.toString()]; friendsList.forEach((f) => { customWhere += " OR ID_personne = ?"; customWhereArgs.push(f.friendID.toString()); }); customWhere += "))" // === FRIENDS POSTS === // === GROUPS POSTS === if(includeGroups) { const groups = await GroupsHelper.GetListUser(userID, true); groups.forEach((groupID) => { customWhere += " OR group_id = ? "; customWhereArgs.push(groupID.toString()); }) } // == /GROUPS POSTS === customWhere += ")"; // === /Membership condition === // === START POINT === if(startFrom > 0) { customWhere += "AND ID <= ?"; customWhereArgs.push(startFrom.toString()); } // == /START POINT === const results = await DatabaseHelper.Query({ table: TABLE_NAME, customWhere: customWhere, customWhereArgs: customWhereArgs, order: "ID DESC", limit: limit }); return results.map((r) => this.DBToPost(r)); } /** * Get information about a single post * * @param postID Target post ID */ public static async GetSingle(postID: number) : Promise { const row = await DatabaseHelper.QueryRow({ table: TABLE_NAME, where: { ID: postID } }); if(row == null) throw new Error("Post " + postID + " not found!"); return this.DBToPost(row); } /** * Get the access level of a user over a post * * This is a convenience function * * @param userID Target user ID * @param postID Target post ID */ public static async GetAccessLevelFromPostID(userID: number, postID: number) : Promise { return await this.GetAccessLevel(userID, await this.GetSingle(postID)); } /** * Get the access level of a user over a post * * @param userID Target user ID * @param post Target post */ public static async GetAccessLevel(userID: number, post: Post) : Promise { // User is the owner of the post if(userID == post.userID) return PostAccessLevel.FULL_ACCESS; // User page if(post.kindPage == PostPageKind.PAGE_KIND_USER) { // Post made on user page if(post.pageID == userID) return PostAccessLevel.INTERMEDIATE_ACCESS; // Check if the post is private if(post.visibilityLevel == PostVisibilityLevel.VISIBILITY_USER) return PostAccessLevel.NO_ACCESS; // In case the post is only for friends else if(post.visibilityLevel == PostVisibilityLevel.VISIBILITY_FRIENDS) { if(userID < 1 /* user not signed in */ || !await FriendsHelper.AreFriend(userID, post.pageID) /* not a friend */) return PostAccessLevel.NO_ACCESS; else return PostAccessLevel.BASIC_ACCESS; } // In case of public post else if(post.visibilityLevel == PostVisibilityLevel.VISIBILITY_PUBLIC) { // Check if the user can see the page if(await UserHelper.CanSeeUserPage(userID, post.userPageID)) return PostAccessLevel.BASIC_ACCESS; // Else no access to the user return PostAccessLevel.NO_ACCESS; } } // Group page else if(post.kindPage == PostPageKind.PAGE_KIND_GROUP) { const accessLevel = await GroupsHelper.GetMembershipLevel(post.groupID, userID); // Moderator & admin = intermediate access if(accessLevel < GroupMembershipLevels.MEMBER) return PostAccessLevel.INTERMEDIATE_ACCESS; // Members can see all the posts of a group if(accessLevel == GroupMembershipLevels.MEMBER) return PostAccessLevel.BASIC_ACCESS; // Now we check if the post is public & the group is open if(post.visibilityLevel != PostVisibilityLevel.VISIBILITY_PUBLIC || !await GroupsHelper.IsOpen(post.groupID)) return PostAccessLevel.NO_ACCESS; // Public post + open group = basic access return PostAccessLevel.BASIC_ACCESS; } throw Error("GetAccessLevel reached an unimplemented status!"); } /** * Check out whether comments are allowed on a post or not * * @param post The post to check */ public static async AllowCommentsOnPost(post: Post) : Promise { return !post.isUserPage || await UserHelper.AllowComments(post.userPageID); } /** * Check out whether a post exists or not * * @param postID The id of the post to check */ public static async Exists(postID: number) : Promise { return await DatabaseHelper.Count({ table: TABLE_NAME, where: { ID: postID } }) > 0; } /** * Set new visibility level to the post * * @param postID Target post ID * @param level Target access level */ public static async SetLevel(postID: number, level: PostVisibilityLevel) { await DatabaseHelper.UpdateRows({ table: TABLE_NAME, where: { ID: postID }, set: { niveau_visibilite: level } }) } /** * Set new content to the post * * @param postID Target post ID * @param content Target content */ public static async SetContent(postID: number, content: string) { await DatabaseHelper.UpdateRows({ table: TABLE_NAME, where: { ID: postID }, set: { texte: content } }) } /** * Delete a post * * @param postID The ID of the post to delete */ public static async Delete(postID: number) { const post = await this.GetSingle(postID); // Delete all the likes associated with the post await LikesHelper.DeleteAll(postID, LikesType.POST); // Delete all the comments associated to the post await CommentsHelper.DeleteAll(postID); // Delete associated file (if any) if(post.kind == PostKind.POST_KIND_IMAGE || post.kind == PostKind.POST_KIND_PDF) { if(existsSync(post.file.sysPath)) unlinkSync(post.file.sysPath); } // Delete associated survey (if any) if(post.kind == PostKind.POST_KIND_SURVEY) { if(await SurveyHelper.Exists(postID)) await SurveyHelper.Delete(postID); } } /** * Turn a database entry into a row object * * @param row The database entry */ private static DBToPost(row: any) : Post { const postPageKind = row.group_id == 0 ? PostPageKind.PAGE_KIND_USER : PostPageKind.PAGE_KIND_GROUP; const postType = PostDBTypes[row.type]; return new Post({ // General info id: row.ID, userID: row.ID_amis == 0 ? row.ID_personne : row.ID_amis, // Kind of target of the page and its ID kindPage: postPageKind, pageID: postPageKind == PostPageKind.PAGE_KIND_USER ? row.ID_personne : row.group_id, // Basic info timeCreate: row.time_insert == null ? (new Date(row.date_envoi).getTime() / 1000) : row.time_insert, content: row.texte, visibilityLevel: row.niveau_visibilite, kind: postType, // Files specific file: row.path == null ? undefined : new PostFile({ size: Number(row.size), type: row.file_type, path: row.path }), // Movies specific movieID: row.idvideo == null ? undefined : row.idvideo, // Coutdown timer specific timeEnd: (row.time_end && row.time_end) > 0 ? row.time_end // Coutdown legacy support : (row.annee_fin && row.annee_fin > 2010 ? new Date(row.annee_fin + "/" + row.mois_fin + "/" + row.jour_fin).getTime() / 1000 : undefined), // Weblink - specific link: row.url_page == null ? undefined : new PostLink({ url: row.url_page, title: row.titre_page, description: row.description_page, image: row.image_page }), }); } }