Merge branch 'julius-dev' of Software_Engineering_I/greenvironment-server into develop

pull/2/head
Trivernis 5 years ago committed by Gitea
commit e80bab131e

@ -1,371 +1,377 @@
import {AggregateError} from "bluebird"; import {AggregateError} from "bluebird";
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import * as status from "http-status"; import * as status from "http-status";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import * as models from "../lib/models"; import * as models from "../lib/models";
import {is} from "../lib/regex"; import {is} from "../lib/regex";
/** /**
* Returns the resolvers for the graphql api. * Returns the resolvers for the graphql api.
* @param req - the request object * @param req - the request object
* @param res - the response object * @param res - the response object
*/ */
export function resolver(req: any, res: any): any { export function resolver(req: any, res: any): any {
return { return {
async getSelf() { async getSelf() {
if (req.session.userId) { if (req.session.userId) {
return models.User.findByPk(req.session.userId); return models.User.findByPk(req.session.userId);
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
}, },
async getUser({userId, handle}: { userId: number, handle: string }) { async getUser({userId, handle}: { userId: number, handle: string }) {
if (handle) { if (handle) {
return await dataaccess.getUserByHandle(handle); return await dataaccess.getUserByHandle(handle);
} else if (userId) { } else if (userId) {
return models.User.findByPk(userId); return models.User.findByPk(userId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No userId or handle provided."); return new GraphQLError("No userId or handle provided.");
} }
}, },
async getPost({postId}: { postId: number }) { async getPost({postId}: { postId: number }) {
if (postId) { if (postId) {
return await dataaccess.getPost(postId); return await dataaccess.getPost(postId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No postId given."); return new GraphQLError("No postId given.");
} }
}, },
async getChat({chatId}: { chatId: number }) { async getChat({chatId}: { chatId: number }) {
if (chatId) { if (chatId) {
return models.ChatRoom.findByPk(chatId); return models.ChatRoom.findByPk(chatId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId given."); return new GraphQLError("No chatId given.");
} }
}, },
async getGroup({groupId}: {groupId: number}) { async getGroup({groupId}: {groupId: number}) {
if (groupId) { if (groupId) {
return models.Group.findByPk(groupId); return models.Group.findByPk(groupId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No group id given."); return new GraphQLError("No group id given.");
} }
}, },
async getRequest({requestId}: {requestId: number}) { async getRequest({requestId}: {requestId: number}) {
if (requestId) { if (requestId) {
return models.Request.findByPk(requestId); return models.Request.findByPk(requestId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No requestId given."); return new GraphQLError("No requestId given.");
} }
}, },
acceptCookies() { acceptCookies() {
req.session.cookiesAccepted = true; req.session.cookiesAccepted = true;
return true; return true;
}, },
async login({email, passwordHash}: { email: string, passwordHash: string }) { async login({email, passwordHash}: { email: string, passwordHash: string }) {
if (email && passwordHash) { if (email && passwordHash) {
try { try {
const user = await dataaccess.getUserByLogin(email, passwordHash); const user = await dataaccess.getUserByLogin(email, passwordHash);
req.session.userId = user.id; req.session.userId = user.id;
return user; return user;
} catch (err) { } catch (err) {
globals.logger.warn(err.message); globals.logger.warn(err.message);
globals.logger.debug(err.stack); globals.logger.debug(err.stack);
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return err.graphqlError || err.message; return err.graphqlError || err.message;
} }
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No email or password given."); return new GraphQLError("No email or password given.");
} }
}, },
logout() { logout() {
if (req.session.user) { if (req.session.userId) {
delete req.session.user; delete req.session.userId;
return true; req.session.save((err: any) => {
} else { if (err) {
res.status(status.UNAUTHORIZED); globals.logger.error(err.message);
return new NotLoggedInGqlError(); globals.logger.debug(err.stack);
} }
}, });
async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { return true;
if (username && email && passwordHash) { } else {
if (!is.email(email)) { res.status(status.UNAUTHORIZED);
res.status(status.BAD_REQUEST); return new NotLoggedInGqlError();
return new GraphQLError(`'${email}' is not a valid email address!`); }
} },
try { async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) {
const user = await dataaccess.registerUser(username, email, passwordHash); if (username && email && passwordHash) {
req.session.userId = user.id; if (!is.email(email)) {
return user; res.status(status.BAD_REQUEST);
} catch (err) { return new GraphQLError(`'${email}' is not a valid email address!`);
globals.logger.warn(err.message); }
globals.logger.debug(err.stack); try {
res.status(status.BAD_REQUEST); const user = await dataaccess.registerUser(username, email, passwordHash);
return err.graphqlError || err.message; req.session.userId = user.id;
} return user;
} else { } catch (err) {
res.status(status.BAD_REQUEST); globals.logger.warn(err.message);
return new GraphQLError("No username, email or password given."); globals.logger.debug(err.stack);
} res.status(status.BAD_REQUEST);
}, return err.graphqlError || err.message;
async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { }
if (postId && type) { } else {
if (req.session.userId) { res.status(status.BAD_REQUEST);
const post = await models.Post.findByPk(postId); return new GraphQLError("No username, email or password given.");
if (post) { }
return await post.vote(req.session.userId, type); },
} else { async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) {
res.status(status.BAD_REQUEST); if (postId && type) {
return new PostNotFoundGqlError(postId); if (req.session.userId) {
} const post = await models.Post.findByPk(postId);
} else { if (post) {
res.status(status.UNAUTHORIZED); return await post.vote(req.session.userId, type);
return new NotLoggedInGqlError(); } else {
} res.status(status.BAD_REQUEST);
} else { return new PostNotFoundGqlError(postId);
res.status(status.BAD_REQUEST); }
return new GraphQLError("No postId or type given."); } else {
} res.status(status.UNAUTHORIZED);
}, return new NotLoggedInGqlError();
async createPost({content}: { content: string }) { }
if (content) { } else {
if (req.session.userId) { res.status(status.BAD_REQUEST);
const post = await dataaccess.createPost(content, req.session.userId); return new GraphQLError("No postId or type given.");
globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); }
return post; },
} else { async createPost({content}: { content: string }) {
res.status(status.UNAUTHORIZED); if (content) {
return new NotLoggedInGqlError(); if (req.session.userId) {
} const post = await dataaccess.createPost(content, req.session.userId);
} else { globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post);
res.status(status.BAD_REQUEST); return post;
return new GraphQLError("Can't create empty post."); } else {
} res.status(status.UNAUTHORIZED);
}, return new NotLoggedInGqlError();
async deletePost({postId}: { postId: number }) { }
if (postId) { } else {
const post = await models.Post.findByPk(postId, {include: [models.User]}); res.status(status.BAD_REQUEST);
if (post.rAuthor.id === req.session.userId) { return new GraphQLError("Can't create empty post.");
return await dataaccess.deletePost(post.id); }
} else { },
res.status(status.FORBIDDEN); async deletePost({postId}: { postId: number }) {
return new GraphQLError("User is not author of the post."); if (postId) {
} const post = await models.Post.findByPk(postId, {include: [models.User]});
} else { if (post.rAuthor.id === req.session.userId) {
return new GraphQLError("No postId given."); return await dataaccess.deletePost(post.id);
} } else {
}, res.status(status.FORBIDDEN);
async createChat({members}: { members: number[] }) { return new GraphQLError("User is not author of the post.");
if (req.session.userId) { }
const chatMembers = [req.session.userId]; } else {
if (members) { return new GraphQLError("No postId given.");
chatMembers.push(...members); }
} },
return await dataaccess.createChat(...chatMembers); async createChat({members}: { members: number[] }) {
} else { if (req.session.userId) {
res.status(status.UNAUTHORIZED); const chatMembers = [req.session.userId];
return new NotLoggedInGqlError(); if (members) {
} chatMembers.push(...members);
}, }
async sendMessage({chatId, content}: { chatId: number, content: string }) { return await dataaccess.createChat(...chatMembers);
if (!req.session.userId) { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
if (chatId && content) { },
try { async sendMessage({chatId, content}: { chatId: number, content: string }) {
const message = await dataaccess.sendChatMessage(req.session.userId, chatId, content); if (!req.session.userId) {
globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message); res.status(status.UNAUTHORIZED);
return message; return new NotLoggedInGqlError();
} catch (err) { }
globals.logger.warn(err.message); if (chatId && content) {
globals.logger.debug(err.stack); try {
res.status(status.BAD_REQUEST); const message = await dataaccess.sendChatMessage(req.session.userId, chatId, content);
return err.graphqlError || err.message; globals.internalEmitter.emit(InternalEvents.GQLCHATMESSAGE, message);
} return message;
} else { } catch (err) {
res.status(status.BAD_REQUEST); globals.logger.warn(err.message);
return new GraphQLError("No chatId or content given."); globals.logger.debug(err.stack);
} res.status(status.BAD_REQUEST);
}, return err.graphqlError || err.message;
async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }) { }
if (!req.session.userId) { } else {
res.status(status.UNAUTHORIZED); res.status(status.BAD_REQUEST);
return new NotLoggedInGqlError(); return new GraphQLError("No chatId or content given.");
} }
if (receiver && type) { },
return await dataaccess.createRequest(req.session.userId, receiver, type); async sendRequest({receiver, type}: { receiver: number, type: dataaccess.RequestType }) {
} else { if (!req.session.userId) {
res.status(status.BAD_REQUEST); res.status(status.UNAUTHORIZED);
return new GraphQLError("No receiver or type given."); return new NotLoggedInGqlError();
} }
}, if (receiver && type) {
async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { return await dataaccess.createRequest(req.session.userId, receiver, type);
if (!req.session.userId) { } else {
res.status(status.UNAUTHORIZED); res.status(status.BAD_REQUEST);
return new NotLoggedInGqlError(); return new GraphQLError("No receiver or type given.");
} }
if (sender && type) { },
const user = await models.User.findByPk(req.session.userId); async denyRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) {
await user.denyRequest(sender, type); if (!req.session.userId) {
return true; res.status(status.UNAUTHORIZED);
} else { return new NotLoggedInGqlError();
res.status(status.BAD_REQUEST); }
return new GraphQLError("No sender or type given."); if (sender && type) {
} const user = await models.User.findByPk(req.session.userId);
}, await user.denyRequest(sender, type);
async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) { return true;
if (!req.session.userId) { } else {
res.status(status.UNAUTHORIZED); res.status(status.BAD_REQUEST);
return new NotLoggedInGqlError(); return new GraphQLError("No sender or type given.");
} }
if (sender && type) { },
try { async acceptRequest({sender, type}: { sender: number, type: dataaccess.RequestType }) {
const user = await models.User.findByPk(req.session.userId); if (!req.session.userId) {
await user.acceptRequest(sender, type); res.status(status.UNAUTHORIZED);
return true; return new NotLoggedInGqlError();
} catch (err) { }
globals.logger.warn(err.message); if (sender && type) {
globals.logger.debug(err.stack); try {
res.status(status.BAD_REQUEST); const user = await models.User.findByPk(req.session.userId);
return err.graphqlError || err.message; await user.acceptRequest(sender, type);
} return true;
} else { } catch (err) {
res.status(status.BAD_REQUEST); globals.logger.warn(err.message);
return new GraphQLError("No sender or type given."); globals.logger.debug(err.stack);
} res.status(status.BAD_REQUEST);
}, return err.graphqlError || err.message;
async removeFriend({friendId}: {friendId: number}) { }
if (req.session.userId) { } else {
const self = await models.User.findByPk(req.session.userId); res.status(status.BAD_REQUEST);
return await self.removeFriend(friendId); return new GraphQLError("No sender or type given.");
} else { }
res.status(status.UNAUTHORIZED); },
return new NotLoggedInGqlError(); async removeFriend({friendId}: {friendId: number}) {
} if (req.session.userId) {
}, const self = await models.User.findByPk(req.session.userId);
async getPosts({first, offset, sort}: {first: number, offset: number, sort: dataaccess.SortType}) { return await self.removeFriend(friendId);
return await dataaccess.getPosts(first, offset, sort); } else {
}, res.status(status.UNAUTHORIZED);
async createGroup({name, members}: {name: string, members: number[]}) { return new NotLoggedInGqlError();
if (req.session.userId) { }
return await dataaccess.createGroup(name, req.session.userId, members); },
} else { async getPosts({first, offset, sort}: {first: number, offset: number, sort: dataaccess.SortType}) {
return new NotLoggedInGqlError(); return await dataaccess.getPosts(first, offset, sort);
} },
}, async createGroup({name, members}: {name: string, members: number[]}) {
async joinGroup({id}: {id: number}) { if (req.session.userId) {
if (req.session.userId) { return await dataaccess.createGroup(name, req.session.userId, members);
try { } else {
return await dataaccess return new NotLoggedInGqlError();
.changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.ADD); }
} catch (err) { },
res.status(status.BAD_REQUEST); async joinGroup({id}: {id: number}) {
return err.graphqlError; if (req.session.userId) {
} try {
} else { return await dataaccess
res.status(status.UNAUTHORIZED); .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.ADD);
return new NotLoggedInGqlError(); } catch (err) {
} res.status(status.BAD_REQUEST);
}, return err.graphqlError;
async leaveGroup({id}: {id: number}) { }
if (req.session.userId) { } else {
try { res.status(status.UNAUTHORIZED);
return await dataaccess return new NotLoggedInGqlError();
.changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.REMOVE); }
} catch (err) { },
res.status(status.BAD_REQUEST); async leaveGroup({id}: {id: number}) {
return err.graphqlError; if (req.session.userId) {
} try {
} else { return await dataaccess
res.status(status.UNAUTHORIZED); .changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.REMOVE);
return new NotLoggedInGqlError(); } catch (err) {
} res.status(status.BAD_REQUEST);
}, return err.graphqlError;
async addGroupAdmin({groupId, userId}: {groupId: number, userId: number}) { }
if (req.session.userId) { } else {
const group = await models.Group.findByPk(groupId); res.status(status.UNAUTHORIZED);
const self = await models.User.findByPk(req.session.userId); return new NotLoggedInGqlError();
if (group && !(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) { }
res.status(status.FORBIDDEN); },
return new GraphQLError("You are not a group admin!"); async addGroupAdmin({groupId, userId}: {groupId: number, userId: number}) {
} if (req.session.userId) {
try { const group = await models.Group.findByPk(groupId);
return await dataaccess const self = await models.User.findByPk(req.session.userId);
.changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.OP); if (group && !(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) {
} catch (err) { res.status(status.FORBIDDEN);
res.status(status.BAD_REQUEST); return new GraphQLError("You are not a group admin!");
return err.graphqlError; }
} try {
return await dataaccess
} else { .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.OP);
res.status(status.UNAUTHORIZED); } catch (err) {
return new NotLoggedInGqlError(); res.status(status.BAD_REQUEST);
} return err.graphqlError;
}, }
async removeGroupAdmin({groupId, userId}: {groupId: number, userId: number}) {
if (req.session.userId) { } else {
const group = await models.Group.findByPk(groupId); res.status(status.UNAUTHORIZED);
const isCreator = Number(group.creatorId) === Number(req.session.userId); return new NotLoggedInGqlError();
const userIsCreator = Number(group.creatorId) === Number(userId) ; }
if (group && !isCreator && Number(userId) !== Number(req.session.userId)) { },
res.status(status.FORBIDDEN); async removeGroupAdmin({groupId, userId}: {groupId: number, userId: number}) {
return new GraphQLError("You are not the group creator!"); if (req.session.userId) {
} else if (userIsCreator) { const group = await models.Group.findByPk(groupId);
res.status(status.FORBIDDEN); const isCreator = Number(group.creatorId) === Number(req.session.userId);
return new GraphQLError("You are not allowed to remove a creator as an admin."); const userIsCreator = Number(group.creatorId) === Number(userId) ;
} if (group && !isCreator && Number(userId) !== Number(req.session.userId)) {
try { res.status(status.FORBIDDEN);
return await dataaccess return new GraphQLError("You are not the group creator!");
.changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.DEOP); } else if (userIsCreator) {
} catch (err) { res.status(status.FORBIDDEN);
res.status(status.BAD_REQUEST); return new GraphQLError("You are not allowed to remove a creator as an admin.");
return err.graphqlError; }
} try {
} else { return await dataaccess
res.status(status.UNAUTHORIZED); .changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.DEOP);
return new NotLoggedInGqlError(); } catch (err) {
} res.status(status.BAD_REQUEST);
}, return err.graphqlError;
async createEvent({name, dueDate, groupId}: {name: string, dueDate: string, groupId: number}) { }
if (req.session.userId) { } else {
const date = new Date(dueDate); res.status(status.UNAUTHORIZED);
const group = await models.Group.findByPk(groupId); return new NotLoggedInGqlError();
return group.$create<models.Event>("rEvent", {name, dueDate: date}); }
} else { },
res.status(status.UNAUTHORIZED); async createEvent({name, dueDate, groupId}: {name: string, dueDate: string, groupId: number}) {
return new NotLoggedInGqlError(); if (req.session.userId) {
} const date = new Date(dueDate);
}, const group = await models.Group.findByPk(groupId);
async joinEvent({eventId}: {eventId: number}) { return group.$create<models.Event>("rEvent", {name, dueDate: date});
if (req.session.userId) { } else {
const event = await models.Event.findByPk(eventId); res.status(status.UNAUTHORIZED);
const self = await models.User.findByPk(req.session.userId); return new NotLoggedInGqlError();
await event.$add("rParticipants", self); }
return event; },
} else { async joinEvent({eventId}: {eventId: number}) {
res.status(status.UNAUTHORIZED); if (req.session.userId) {
return new NotLoggedInGqlError(); const event = await models.Event.findByPk(eventId);
} const self = await models.User.findByPk(req.session.userId);
}, await event.$add("rParticipants", self);
async leaveEvent({eventId}: {eventId: number}) { return event;
if (req.session.userId) { } else {
const event = await models.Event.findByPk(eventId); res.status(status.UNAUTHORIZED);
const self = await models.User.findByPk(req.session.userId); return new NotLoggedInGqlError();
await event.$remove("rParticipants", self); }
return event; },
} else { async leaveEvent({eventId}: {eventId: number}) {
res.status(status.UNAUTHORIZED); if (req.session.userId) {
return new NotLoggedInGqlError(); const event = await models.Event.findByPk(eventId);
} const self = await models.User.findByPk(req.session.userId);
}, await event.$remove("rParticipants", self);
}; return event;
} } else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
};
}

@ -1,63 +1,64 @@
/** /**
* @author Trivernis * @author Trivernis
* @remarks * @remarks
* *
* Partly taken from {@link https://github.com/Trivernis/whooshy} * Partly taken from {@link https://github.com/Trivernis/whooshy}
*/ */
import {EventEmitter} from "events"; import {EventEmitter} from "events";
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as winston from "winston"; import * as winston from "winston";
require('winston-daily-rotate-file'); require('winston-daily-rotate-file');
const configPath = "config.yaml"; const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml";
// ensure that the config exists by copying the default config. // ensure that the config exists by copying the default config.
if (!(fsx.pathExistsSync(configPath))) { if (!(fsx.pathExistsSync(configPath))) {
fsx.copySync(defaultConfig, configPath); fsx.copySync(defaultConfig, configPath);
} else { } else {
const conf = yaml.safeLoad(fsx.readFileSync(configPath, "utf-8")); const conf = yaml.safeLoad(fsx.readFileSync(configPath, "utf-8"));
const defConf = yaml.safeLoad(fsx.readFileSync(defaultConfig, "utf-8")); const defConf = yaml.safeLoad(fsx.readFileSync(defaultConfig, "utf-8"));
fsx.writeFileSync(configPath, yaml.safeDump(Object.assign(defConf, conf))); fsx.writeFileSync(configPath, yaml.safeDump(Object.assign(defConf, conf)));
} }
/** /**
* Defines global variables to be used. * Defines global variables to be used.
*/ */
namespace globals { namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
// @ts-ignore // @ts-ignore
export const logger = winston.createLogger({ export const logger = winston.createLogger({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.colorize(), winston.format.colorize(),
winston.format.printf(({level, message, timestamp}) => { winston.format.printf(({level, message, timestamp}) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),
level: config.logging.level, level: config.logging.level,
}), }),
// @ts-ignore // @ts-ignore
new (winston.transports.DailyRotateFile)({ new (winston.transports.DailyRotateFile)({
dirname: "logs", dirname: "logs",
filename: "gv-%DATE%.log", filename: "gv-%DATE%.log",
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.printf(({level, message, timestamp}) => { winston.format.printf(({level, message, timestamp}) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),
json: false, json: false,
maxFiles: "7d", level: config.logging.level,
zippedArchive: true, maxFiles: "7d",
}), zippedArchive: true,
], }),
}); ],
export const internalEmitter = new EventEmitter(); });
} export const internalEmitter = new EventEmitter();
}
export default globals;
export default globals;

Loading…
Cancel
Save