diff --git a/src/controllers/PostsController.ts b/src/controllers/PostsController.ts new file mode 100644 index 0000000..6297ae9 --- /dev/null +++ b/src/controllers/PostsController.ts @@ -0,0 +1,68 @@ +import { RequestHandler } from "../entities/RequestHandler"; +import { UserHelper } from "../helpers/UserHelper"; +import { PostsHelper } from "../helpers/PostsHelper"; +import { Post, PostVisibilityLevel } from "../entities/Post"; + +/** + * Posts controller + * + * @author Pierre HUBERT + */ + +const VISIBILITY_LEVELS_API = {}; +VISIBILITY_LEVELS_API[PostVisibilityLevel.VISIBILITY_PUBLIC] = "public"; +VISIBILITY_LEVELS_API[PostVisibilityLevel.VISIBILITY_FRIENDS] = "friends"; +VISIBILITY_LEVELS_API[PostVisibilityLevel.VISIBILITY_USER] = "private"; +VISIBILITY_LEVELS_API[PostVisibilityLevel.VISIBILITY_GROUP_MEMBERS] = "members"; + +export class PostsController { + + /** + * Get the posts of a user + * + * @param h Request handler + */ + public static async GetListUser(h: RequestHandler) { + const userID = await h.postUserId("userID"); + const startFrom = h.postInt("startFrom", 0); + + if(!await UserHelper.CanSeeUserPage(h.optionnalUserID, userID)) + h.error(401, "You are not allowed to access this user posts !"); + + const posts = await PostsHelper.GetUserPosts(h.optionnalUserID, userID, startFrom); + + let list = []; + for (const p of posts) { + list.push(await this.PostToAPI(p)); + } + + h.send(list); + } + + + /** + * Turn a post object into an API entry + * + * @param p The post + */ + public static async PostToAPI(p: Post) : Promise { + let data : any = { + ID: p.id, + useriD: p.userID, + user_page_id: p.userPageID, + group_id: p.groupID, + post_time: p.timeCreate, + content: p.hasContent ? p.content : null, + visibility_level: VISIBILITY_LEVELS_API[p.visibilityLevel], + kind: p.kind, + + // File specific + file_size: !p.hasFile ? null : p.file.size, + file_type: !p.hasFile ? null : p.file.type, + file_path: !p.hasFile ? null : p.file.path, + file_url: !p.hasFile ? null : p.file.url + }; + + return data; + } +} \ No newline at end of file diff --git a/src/controllers/Routes.ts b/src/controllers/Routes.ts index acfbee9..2c19b32 100644 --- a/src/controllers/Routes.ts +++ b/src/controllers/Routes.ts @@ -11,6 +11,7 @@ import { WebAppControllers } from "./WebAppController"; import { CallsController } from "./CallsController"; import { FriendsController } from "./FriendsController"; import { MoviesController } from "./MoviesController"; +import { PostsController } from "./PostsController"; /** * Controllers routes @@ -182,6 +183,11 @@ export const Routes : Route[] = [ + // Posts controller + {path: "/posts/get_user", cb: (h) => PostsController.GetListUser(h), needLogin: false}, + + + // Notifications controller {path: "/notifications/count_unread", cb: (h) => NotificationsController.CountUnread(h)}, diff --git a/src/entities/Post.ts b/src/entities/Post.ts new file mode 100644 index 0000000..2489d2c --- /dev/null +++ b/src/entities/Post.ts @@ -0,0 +1,151 @@ +import { pathUserData } from "../utils/UserDataUtils"; + +/** + * Post entity + * + * @author Pierre HUBERT + */ + +export enum PostVisibilityLevel { + //Posts that can be seen by anyone + VISIBILITY_PUBLIC = 1, + + //Posts that can be seen by the friends of the user + VISIBILITY_FRIENDS = 2, + + //Posts that can be seen by the user only + VISIBILITY_USER = 3, + + //Posts that can be seen by the members of a group (same as friends) + VISIBILITY_GROUP_MEMBERS = 50, +} + + +export enum PostAccessLevel { + //When a user can't access to a post + NO_ACCESS = 0, + + //When a user can see a post and perform basic actions such as liking + BASIC_ACCESS = 1, + + //When a user has intermediate access to the post (delete post) + INTERMEDIATE_ACCESS = 2, + + //When a user has a full access to the post + FULL_ACCESS = 3, +} + +export enum PostKind { + POST_KIND_TEXT = "text", + POST_KIND_IMAGE = "image", + POST_KIND_WEBLINK = "weblink", + POST_KIND_PDF = "pdf", + POST_KIND_MOVIE = "movie", + POST_KIND_COUNTDOWN = "countdown", + POST_KIND_SURVEY = "survey", + POST_KIND_YOUTUBE = "youtube", +} + +export enum PostPageKind { + PAGE_KIND_USER = "user", + PAGE_KIND_GROUP = "group", +} + + +export interface PostFileBuilder { + path: string, + size: number, + type: string, +} + +export class PostFile implements PostFileBuilder { + path: string; + size: number; + type: string; + + public constructor(info: PostFileBuilder) { + for (const key in info) { + if (info.hasOwnProperty(key)) + this[key] = info[key]; + } + } + + get url() : string { + return pathUserData(this.path) + } +} + +export interface PostLinkBuilder { + url: string, + title: string, + description: string, + image: string +} + +export class PostLink implements PostLinkBuilder { + url: string; + title: string; + description: string; + image: string; + + public constructor(info: PostLinkBuilder) { + for (const key in info) { + if (info.hasOwnProperty(key)) + this[key] = info[key]; + } + } +} + + +export interface PostBuilder { + id: number, + userID: number, + timeCreate: number, + kindPage: PostPageKind, + pageID: number, + content: string, + visibilityLevel: PostVisibilityLevel, + kind: PostKind, + file ?: PostFile, + movieID ?: number, + timeEnd ?: number, + link ?: PostLink, +} + +export class Post implements PostBuilder { + id: number; + userID: number; + timeCreate: number; + kindPage: PostPageKind; + pageID: number; + content: string; + visibilityLevel: PostVisibilityLevel; + kind: PostKind; + file?: PostFile; + movieID?: number; + timeEnd?: number; + link?: PostLink; + + public constructor(info: PostBuilder) { + for (const key in info) { + if (info.hasOwnProperty(key)) + this[key] = info[key]; + } + } + + get userPageID() : number { + return this.kindPage == PostPageKind.PAGE_KIND_USER ? this.pageID : 0 + } + + get groupID() : number { + return this.kindPage == PostPageKind.PAGE_KIND_GROUP ? this.pageID : 0 + } + + get hasContent() : boolean { + return this.content != null && this.content && this.content.length > 0; + } + + get hasFile() : boolean { + return this.file != null && this.file != undefined; + } +} \ No newline at end of file diff --git a/src/helpers/PostsHelper.ts b/src/helpers/PostsHelper.ts new file mode 100644 index 0000000..148e2ca --- /dev/null +++ b/src/helpers/PostsHelper.ts @@ -0,0 +1,162 @@ +import { PostKind, PostVisibilityLevel, Post, PostPageKind, PostFile, PostLink } from "../entities/Post"; +import { FriendsHelper } from "./FriendsHelper"; +import { DatabaseHelper } from "./DatabaseHelper"; + +/** + * Posts helper + * + * @author Pierre HUBERT + */ + +/** + * Table name + */ +const TABLE_NAME = "texte"; + +/** + * Database mapping + */ +const PostDBTypes : Record = { + "texte": PostKind.POST_KIND_IMAGE, + "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 { + + /** + * 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)); + } + + /** + * 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 + }), + }); + } +} \ No newline at end of file