2019-12-13 16:49:58 +00:00
|
|
|
import { RequestHandler } from "../entities/RequestHandler";
|
2019-12-26 13:14:42 +00:00
|
|
|
import { GroupsHelper, PATH_GROUPS_LOGOS } from "../helpers/GroupsHelper";
|
2019-12-15 16:37:39 +00:00
|
|
|
import { GroupsAccessLevel, GroupInfo, GroupVisibilityLevel, GroupPostsCreationLevel, GroupRegistrationLevel } from "../entities/Group";
|
2019-12-26 13:26:46 +00:00
|
|
|
import { GroupMembershipLevels, GroupMember } from "../entities/GroupMember";
|
2019-12-24 17:47:55 +00:00
|
|
|
import { time } from "../utils/DateUtils";
|
2019-12-25 14:22:43 +00:00
|
|
|
import { LikesHelper, LikesType } from "../helpers/LikesHelper";
|
2019-12-26 12:49:17 +00:00
|
|
|
import { GroupSettings } from "../entities/GroupSettings";
|
|
|
|
import { removeHTMLNodes, checkURL } from "../utils/StringUtils";
|
|
|
|
import { findKey } from "../utils/ArrayUtils";
|
|
|
|
import { checkVirtualDirectoryAvailability, VirtualDirType } from "../utils/VirtualDirsUtils";
|
2019-12-13 16:49:58 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Groups API controller
|
|
|
|
*
|
|
|
|
* @author Pierre HUBERT
|
|
|
|
*/
|
|
|
|
|
2019-12-15 16:37:39 +00:00
|
|
|
/**
|
|
|
|
* API groups registration levels
|
|
|
|
*/
|
2019-12-26 12:49:17 +00:00
|
|
|
const GROUPS_REGISTRATION_LEVELS = {};
|
2019-12-15 16:37:39 +00:00
|
|
|
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
|
|
|
|
*/
|
2019-12-26 12:49:17 +00:00
|
|
|
const GROUPS_MEMBERSHIP_LEVELS = {};
|
2019-12-15 16:37:39 +00:00
|
|
|
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
|
|
|
|
*/
|
2019-12-26 12:49:17 +00:00
|
|
|
const GROUPS_VISIBILITY_LEVELS = {};
|
2019-12-15 16:37:39 +00:00
|
|
|
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";
|
|
|
|
|
|
|
|
|
2019-12-13 16:49:58 +00:00
|
|
|
export class GroupsController {
|
|
|
|
|
2019-12-24 17:47:55 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-13 16:49:58 +00:00
|
|
|
/**
|
|
|
|
* 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()));
|
|
|
|
}
|
|
|
|
|
2019-12-13 17:30:08 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
2019-12-15 16:37:39 +00:00
|
|
|
const groupInfo = await GroupsHelper.GetInfo(groupID);
|
|
|
|
|
2019-12-15 16:54:44 +00:00
|
|
|
h.send(await this.GroupInfoToAPI(groupInfo, h));
|
2019-12-15 16:37:39 +00:00
|
|
|
}
|
|
|
|
|
2019-12-24 17:32:44 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2019-12-24 18:10:45 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
|
2019-12-24 18:15:12 +00:00
|
|
|
h.send(await this.GroupInfoToAPI(group, h, true));
|
2019-12-24 18:10:45 +00:00
|
|
|
}
|
|
|
|
|
2019-12-25 14:43:43 +00:00
|
|
|
/**
|
|
|
|
* 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));
|
|
|
|
}
|
|
|
|
|
2019-12-26 12:49:17 +00:00
|
|
|
/**
|
|
|
|
* 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: <GroupVisibilityLevel>Number(visibilityKey),
|
|
|
|
registrationLevel: <GroupRegistrationLevel>Number(registrationKey),
|
|
|
|
postsCreationLevel: <GroupPostsCreationLevel>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!");
|
|
|
|
}
|
|
|
|
|
2019-12-26 12:56:28 +00:00
|
|
|
/**
|
|
|
|
* 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!");
|
|
|
|
}
|
|
|
|
|
2019-12-26 13:14:42 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-12-26 13:26:46 +00:00
|
|
|
/**
|
|
|
|
* 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)));
|
|
|
|
}
|
|
|
|
|
2019-12-26 16:55:29 +00:00
|
|
|
/**
|
|
|
|
* 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!");
|
|
|
|
}
|
|
|
|
|
2019-12-26 17:16:44 +00:00
|
|
|
/**
|
|
|
|
* 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!");
|
|
|
|
}
|
|
|
|
|
2019-12-27 08:57:28 +00:00
|
|
|
/**
|
|
|
|
* 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!");
|
|
|
|
}
|
|
|
|
|
2019-12-27 09:02:05 +00:00
|
|
|
/**
|
|
|
|
* 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!");
|
|
|
|
}
|
|
|
|
|
2019-12-15 16:37:39 +00:00
|
|
|
/**
|
|
|
|
* Turn a GroupInfo object into a valid API object
|
|
|
|
*
|
|
|
|
* @param info Information about the group
|
2019-12-24 18:15:12 +00:00
|
|
|
* @param h Request handler
|
|
|
|
* @param advanced Specify whether advanced information should be returned
|
|
|
|
* in the request or not
|
2019-12-15 16:37:39 +00:00
|
|
|
* @returns Generated object
|
|
|
|
*/
|
2019-12-24 18:15:12 +00:00
|
|
|
private static async GroupInfoToAPI(info: GroupInfo, h: RequestHandler, advanced: boolean = false) : Promise<any> {
|
2019-12-15 16:54:44 +00:00
|
|
|
|
2019-12-25 14:37:09 +00:00
|
|
|
const membership = await GroupsHelper.GetMembershipInfo(info.id, h.optionnalUserID)
|
2019-12-15 16:54:44 +00:00
|
|
|
|
2019-12-24 18:15:12 +00:00
|
|
|
const data = {
|
2019-12-15 16:37:39 +00:00
|
|
|
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],
|
2019-12-15 16:54:44 +00:00
|
|
|
virtual_directory: info.virtualDirectory ? info.virtualDirectory : "null",
|
|
|
|
|
|
|
|
membership: GROUPS_MEMBERSHIP_LEVELS[membership ? membership.level : GroupMembershipLevels.VISITOR],
|
|
|
|
following: membership ? membership.following : false
|
2019-12-15 16:37:39 +00:00
|
|
|
}
|
2019-12-24 18:15:12 +00:00
|
|
|
|
|
|
|
if(advanced) {
|
|
|
|
data["time_create"] = info.timeCreate;
|
|
|
|
data["description"] = info.hasDescription ? info.description : "null";
|
|
|
|
data["url"] = info.url ? info.hasURL : "null";
|
|
|
|
|
2019-12-25 14:22:43 +00:00
|
|
|
data["number_likes"] = await LikesHelper.Count(info.id, LikesType.GROUP);
|
2019-12-25 14:31:41 +00:00
|
|
|
data["is_liking"] = h.signedIn ? await LikesHelper.IsLiking(h.getUserId(), info.id, LikesType.GROUP) : false;
|
2019-12-24 18:15:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
2019-12-13 17:30:08 +00:00
|
|
|
}
|
2019-12-26 13:26:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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]
|
|
|
|
};
|
|
|
|
}
|
2019-12-13 16:49:58 +00:00
|
|
|
}
|