import { RequestHandler } from "../entities/RequestHandler"; import { GroupsHelper, PATH_GROUPS_LOGOS } from "../helpers/GroupsHelper"; import { GroupsAccessLevel, GroupInfo, GroupVisibilityLevel, GroupPostsCreationLevel, GroupRegistrationLevel } from "../entities/Group"; import { GroupMembershipLevels, GroupMember } from "../entities/GroupMember"; import { time } from "../utils/DateUtils"; import { LikesHelper, LikesType } from "../helpers/LikesHelper"; import { GroupSettings } from "../entities/GroupSettings"; import { removeHTMLNodes, checkURL } from "../utils/StringUtils"; import { findKey } from "../utils/ArrayUtils"; import { checkVirtualDirectoryAvailability, VirtualDirType } from "../utils/VirtualDirsUtils"; /** * Groups API controller * * @author Pierre HUBERT */ /** * API groups registration levels */ const GROUPS_REGISTRATION_LEVELS = {}; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.OPEN_REGISTRATION] = "open"; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.MODERATED_REGISTRATION] = "moderated"; GROUPS_REGISTRATION_LEVELS[GroupRegistrationLevel.CLOSED_REGISTRATION] = "closed"; /** * API groups membership levels */ const GROUPS_MEMBERSHIP_LEVELS = {}; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.ADMINISTRATOR] = "administrator"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.MODERATOR] = "moderator"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.MEMBER] = "member"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.INVITED] = "invited"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.PENDING] = "pending"; GROUPS_MEMBERSHIP_LEVELS[GroupMembershipLevels.VISITOR] = "visitor"; /** * API groups visibility levels */ const GROUPS_VISIBILITY_LEVELS = {}; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.OPEN_GROUP] = "open"; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.PRIVATE_GROUP] = "private"; GROUPS_VISIBILITY_LEVELS[GroupVisibilityLevel.SECRETE_GROUP] = "secrete"; /** * API posts creation levels */ const GROUPS_POSTS_LEVELS = []; GROUPS_POSTS_LEVELS[GroupPostsCreationLevel.POSTS_LEVEL_MODERATORS] = "moderators"; GROUPS_POSTS_LEVELS[GroupPostsCreationLevel.POSTS_LEVEL_ALL_MEMBERS] = "members"; export class GroupsController { /** * Create a new group * * @param h Request handler */ public static async Create(h: RequestHandler) { const name = h.postString("name", 3); const groupID = await GroupsHelper.Create({ name: name, userID: h.getUserId(), timeCreate: time() }); h.send({ success: "The group has been successfully created!", id: groupID }); } /** * Get the list of groups of the user * * @param h Request handler */ public static async GetListUser(h: RequestHandler) { h.send(await GroupsHelper.GetListUser(h.getUserId())); } /** * Get information about a single group * * @param h Request handler */ public static async GetInfoSingle(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.LIMITED_ACCESS); const groupInfo = await GroupsHelper.GetInfo(groupID); h.send(await this.GroupInfoToAPI(groupInfo, h)); } /** * Get information about multiple users * * @param h Request handler */ public static async GetInfoMultiple(h: RequestHandler) { const ids = h.postNumbersList("list"); const result = {}; for (const id of ids) { // Check group existence & user authorization if(!await GroupsHelper.Exists(id) || await GroupsHelper.GetAccessLevel(id, h.getUserId()) < GroupsAccessLevel.LIMITED_ACCESS) h.error(404, "Group " + id + " not found"); const group = await GroupsHelper.GetInfo(id); result[id] = await this.GroupInfoToAPI(group, h); } h.send(result); } /** * Get advanced information about a group * * @param h Request handler */ public static async GetAdvancedInfo(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.VIEW_ACCESS); const group = await GroupsHelper.GetInfo(groupID); h.send(await this.GroupInfoToAPI(group, h, true)); } /** * Get group settings * * @param h Request handler */ public static async GetSettings(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.ADMIN_ACCESS); // For now, this method is the same as the get advanced info methods, // but this might change in the future... const group = await GroupsHelper.GetInfo(groupID); h.send(await this.GroupInfoToAPI(group, h, true)); } /** * Set (update) group settings * * @param h Request handler */ public static async SetSettings(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.ADMIN_ACCESS); // Check group visibility const visibilityKey = findKey(GROUPS_VISIBILITY_LEVELS, h.postString("visibility", 3)); if(visibilityKey == null) h.error(400, "Group visibility level not recognized!"); // Check group registration level const registrationKey = findKey(GROUPS_REGISTRATION_LEVELS, h.postString("registration_level", 3)); if(registrationKey == null) h.error(400, "Group registration level not recognized!"); // Check post creation level const postLevelKey = findKey(GROUPS_POSTS_LEVELS, h.postString("posts_level", 3)); if(postLevelKey == null) h.error(400, "Group post creation level not recognized!"); // Check URL const url = h.postString("url", 0); if(url.length > 0 && ! checkURL(url)) h.error(401, "Invalid group URL!"); // Check virtual directory let virtualDirectory = ""; if(h.hasPostString("virtual_directory", 1)) { virtualDirectory = h.postVirtualDirectory("virtual_directory"); // Check out whether virtual directory is available or not if(!await checkVirtualDirectoryAvailability(virtualDirectory, groupID, VirtualDirType.GROUP)) h.error(401, "Requested virtual directory is not available!"); } const settings = new GroupSettings({ // Basic information id: groupID, name: removeHTMLNodes(h.postString("name", 3)), visiblity: Number(visibilityKey), registrationLevel: Number(registrationKey), postsCreationLevel: Number(postLevelKey), // Useless info membersCount: -1, timeCreate: -1, // Optionnal description: removeHTMLNodes(h.postString("description", 0)), // Optionnal url: url, // Optionnal virtualDirectory: virtualDirectory, }); await GroupsHelper.SetSettings(settings); h.success("Group settings have been successfully updated!"); } /** * Check the availability of a virtual directory for a given group * * @param h Request handler */ public static async CheckVirtualDirectory(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.ADMIN_ACCESS); const virtualDirectory = h.postVirtualDirectory("directory"); if(!await checkVirtualDirectoryAvailability(virtualDirectory, groupID, VirtualDirType.GROUP)) h.error(401, "The requested virtual directory seems not to be available!"); h.success("Requested virtual directory seems to be available!"); } /** * Upload a new group logo * * @param h Request handler */ public static async UploadLogo(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.ADMIN_ACCESS); if(!h.hasFile("logo")) h.error(400, "An error occured while receiving logo !"); // Delete current logo (if any) await GroupsHelper.DeleteLogo(groupID); // Save the new group logo const targetFilePath = await h.savePostImage("logo", PATH_GROUPS_LOGOS, 500, 500); // Update the settings of the group const settings = await GroupsHelper.GetInfo(groupID); settings.logo = targetFilePath; await GroupsHelper.SetSettings(settings); h.send({ success: "Group logo has been successfully updated!", url: settings.logoURL }); } /** * Delete the current logo of a group * * @param h Request handler */ public static async DeleteLogo(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.ADMIN_ACCESS); await GroupsHelper.DeleteLogo(groupID); h.send({ success: "Group logo has been successfully deleted!", url: (await GroupsHelper.GetInfo(groupID)).logoURL }) } /** * Get the entire list of members of the group * * @param h Request handler */ public static async GetMembers(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.MODERATOR_ACCESS); const members = await GroupsHelper.GetListMembers(groupID); // Parse the list of members h.send(members.map((m) => this.GroupMemberToAPI(m))); } /** * Invite a user to join the network * * @param h Request handler */ public static async InviteUser(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("group_id", GroupsAccessLevel.MODERATOR_ACCESS); const userID = await h.postUserId("userID"); if(!await GroupsHelper.GetMembershipLevel(groupID, userID)) h.error(401, "The user is not a visitor of the group!"); await GroupsHelper.SendInvitation(groupID, userID); // TODO : Create a notification h.success("The user has been successfully invited to join the group!"); } /** * Respond to a user invitation * * @param h Request handler */ public static async RespondInvitation(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.LIMITED_ACCESS); const accept = h.postBool("accept"); // Check if the user really received an invitation to join the group if(!await GroupsHelper.ReceivedInvitation(groupID, h.getUserId())) h.error(404, "Invitation not found!"); // Respond to the invitation await GroupsHelper.RespondInvitation(groupID, h.getUserId(), accept); // TODO : Create a notification h.success("Response to the invitation was successfully saved!"); } /** * Send a request to join a server * * @param h Request handler */ public static async SendRequest(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.LIMITED_ACCESS); // Check the user is really a visitor of the group if(await GroupsHelper.GetMembershipLevel(groupID, h.getUserId()) != GroupMembershipLevels.VISITOR) h.error(401, "You are not currently a visitor of the group!"); // Check the user is allowed to send a request to join the group const group = await GroupsHelper.GetInfo(groupID); if(group.registrationLevel == GroupRegistrationLevel.CLOSED_REGISTRATION) h.error(401, "You are not authorized to send a registration request for this group!"); // Create & insert membership const member = new GroupMember({ id: -1, userID: h.getUserId(), timeCreate: time(), groupID: groupID, level: group.registrationLevel == GroupRegistrationLevel.MODERATED_REGISTRATION ? GroupMembershipLevels.PENDING : GroupMembershipLevels.MEMBER, following: true, }); await GroupsHelper.InsertMember(member); if(group.registrationLevel == GroupRegistrationLevel.MODERATED_REGISTRATION) { //TODO : Send a notification } h.success("The membership has been successfully saved!"); } /** * Cancel a membership request * * @param h Request handler */ public static async CancelRequest(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.LIMITED_ACCESS); if(await GroupsHelper.GetMembershipLevel(groupID, h.getUserId()) != GroupMembershipLevels.PENDING) h.error(401, "You did not send a membership request to this group!"); // Delete membership of the user await GroupsHelper.DeleteMember(groupID, h.getUserId()); // TODO : delete any potential notificaton h.success("The request has been successfully cancelled!"); } /** * Delete a member from a group (as a moderator or an admin) * * @param h Request handler */ public static async DeleteMember(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.MODERATOR_ACCESS); // Get the membership of the user making the request const currUserMembership = await GroupsHelper.GetMembershipInfo(groupID, h.getUserId()); // Get the ID of the member to delete const userID = await h.postUserId("userID"); const membership = await GroupsHelper.GetMembershipInfo(groupID, userID); if(membership == null) h.error(404, "Membership not found!"); // If the user is an admin, he must not be the last admin of the group if(userID == h.getUserId() && currUserMembership.level == GroupMembershipLevels.ADMINISTRATOR && await GroupsHelper.CountMembersAtLevel(groupID, GroupMembershipLevels.ADMINISTRATOR) == 1) h.error(401, "You are the last administrator of this group!"); // Only administrator can delete members that are more than member (moderators & administrators) if(membership.level < GroupMembershipLevels.MEMBER && currUserMembership.level != GroupMembershipLevels.ADMINISTRATOR) h.error(401, "Only an administrator can delete this membership!"); // Delete the membership await GroupsHelper.DeleteMember(groupID, userID); // TODO : delete any group membership notifications h.success("Membership of the user has been successfully deleted!"); } /** * Update a user membership * * @param h Request handler */ public static async UpdateMembership(h: RequestHandler) { // Get information about the target const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.ADMIN_ACCESS); const userID = await h.postUserId("userID"); if(userID == h.getUserId()) h.error(400, "You can not update your own membership!"); // Get current user membership const level = await GroupsHelper.GetMembershipLevel(groupID, userID); // Check if user is at least a member of the group if(level > GroupMembershipLevels.MEMBER) h.error(401, "This user is not a member of the group!"); // Get the new membership of the user const membershipKey = findKey(GROUPS_MEMBERSHIP_LEVELS, h.postString("level", 3)); if(membershipKey == null) h.error(400, "New user membership level not recognized!"); const newLevel = Number(membershipKey); if(newLevel > GroupMembershipLevels.MEMBER) h.error(401, "You can not assign this visibility level to a group member!"); await GroupsHelper.UpdateMembershipLevel(groupID, userID, newLevel); h.success("User membership has been successfully updated!"); } /** * Respond to a membership request * * @param h Request handler */ public static async RespondRequest(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.MODERATOR_ACCESS); const userID = await h.postUserId("userID"); const accept = h.postBool("accept"); if(await GroupsHelper.GetMembershipLevel(groupID, userID) != GroupMembershipLevels.PENDING) h.error(401, "This user has not requested a membership for this group!"); // Respond to the request await GroupsHelper.RespondRequest(groupID, userID, accept); // TODO : create a notification h.success("The response to the request has been successfully saved!"); } /** * Get information about a single membership * * @param h Request handler */ public static async GetMembership(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.MODERATOR_ACCESS); const userID = await h.postUserId("userID"); const membership = await GroupsHelper.GetMembershipInfo(groupID, userID); if(membership == null) h.error(404, "Specified user does not have any membership in this group!"); h.send(this.GroupMemberToAPI(membership)); } /** * Cancel a group membership invitation * * @param h Request handler */ public static async CancelInvitation(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.MODERATOR_ACCESS); const userID = await h.postUserId("userID"); if(await GroupsHelper.GetMembershipLevel(groupID, userID) != GroupMembershipLevels.INVITED) h.error(401, "This user has not been invited to join this group!"); // Cancel group invitation await GroupsHelper.DeleteMember(groupID, userID); // TODO : delete related notifications h.success("Membership invitation has been cancelled!"); } /** * Delete the membership of a user to a group * * @param h Request handler */ public static async RemoveMembership(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("id", GroupsAccessLevel.LIMITED_ACCESS); const level = await GroupsHelper.GetMembershipLevel(groupID, h.getUserId()); if(level == GroupMembershipLevels.ADMINISTRATOR && await GroupsHelper.CountMembersAtLevel(groupID, GroupMembershipLevels.ADMINISTRATOR) == 1) h.error(401, "You are the last administrator of the group!"); // Delete mebmership await GroupsHelper.DeleteMember(groupID, h.getUserId()); // TODO : delete group membership notifications h.success("Your membership has been successfully deleted!"); } /** * Update the following status of a user * * @param h Request handler */ public static async SetFollowing(h: RequestHandler) { const groupID = await h.postGroupIDWithAccess("groupID", GroupsAccessLevel.MEMBER_ACCESS); const following = h.postBool("follow"); await GroupsHelper.SetFollowing(groupID, h.getUserId(), following); h.success("Follow status has been successfully updated!"); } /** * Turn a GroupInfo object into a valid API object * * @param info Information about the group * @param h Request handler * @param advanced Specify whether advanced information should be returned * in the request or not * @returns Generated object */ private static async GroupInfoToAPI(info: GroupInfo, h: RequestHandler, advanced: boolean = false) : Promise { const membership = await GroupsHelper.GetMembershipInfo(info.id, h.optionnalUserID) const data = { id: info.id, name: info.name, icon_url: info.logoURL, number_members: info.membersCount, visibility: GROUPS_VISIBILITY_LEVELS[info.visiblity], registration_level: GROUPS_REGISTRATION_LEVELS[info.registrationLevel], posts_level: GROUPS_POSTS_LEVELS[info.postsCreationLevel], virtual_directory: info.virtualDirectory ? info.virtualDirectory : "null", membership: GROUPS_MEMBERSHIP_LEVELS[membership ? membership.level : GroupMembershipLevels.VISITOR], following: membership ? membership.following : false } if(advanced) { data["time_create"] = info.timeCreate; data["description"] = info.hasDescription ? info.description : "null"; data["url"] = info.url ? info.hasURL : "null"; data["number_likes"] = await LikesHelper.Count(info.id, LikesType.GROUP); data["is_liking"] = h.signedIn ? await LikesHelper.IsLiking(h.getUserId(), info.id, LikesType.GROUP) : false; } return data; } /** * Convert a {GroupMember} object into an API entry * * @param m Group Member to transform */ private static GroupMemberToAPI(m: GroupMember) : Object { return { user_id: m.userID, group_id: m.groupID, time_create: m.timeCreate, level: GROUPS_MEMBERSHIP_LEVELS[m.level] }; } }