Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master

pull/5/head
Trivernis 5 years ago committed by Gitea
commit 1f9f8f1fa3

@ -51,7 +51,6 @@ angularIndex = "index.html"
# The path to the public files
publicPath = "./public"
# Configuration for the api
[api]
@ -64,6 +63,9 @@ maxQueryComplexity = 5000
# sets the image format for the site. Values: png or webp
imageFormat = "png"
# the max file size for uploading in bytes
maxFileSize = 10_485_760
# Configuration for the api rate limit
[api.rateLimit]

@ -32,6 +32,7 @@
"@types/express-graphql": "^0.8.0",
"@types/express-session": "^1.15.14",
"@types/express-socket.io-session": "^1.3.2",
"@types/fluent-ffmpeg": "^2.1.13",
"@types/fs-extra": "^8.0.0",
"@types/graphql-query-complexity": "^0.2.1",
"@types/http-status": "^0.2.30",
@ -45,6 +46,7 @@
"@types/sharp": "^0.23.1",
"@types/socket.io": "^2.1.2",
"@types/socket.io-redis": "^1.0.25",
"@types/stream-buffers": "^3.0.3",
"@types/uuid": "^3.4.6",
"@types/validator": "^10.11.3",
"chai": "^4.2.0",
@ -59,7 +61,9 @@
"typescript": "^3.7.2"
},
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.0.20",
"@types/body-parser": "^1.17.1",
"@types/graphql-upload": "^8.0.3",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"config": "^3.2.4",
@ -72,10 +76,12 @@
"express-limiter": "^1.6.1",
"express-session": "^1.16.2",
"express-socket.io-session": "^1.3.5",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^8.1.0",
"graphql": "^14.4.2",
"graphql-import": "^0.7.1",
"graphql-query-complexity": "^0.4.1",
"graphql-upload": "^9.0.0",
"http-status": "^1.3.2",
"js-yaml": "^3.13.1",
"legit": "^1.0.7",
@ -91,6 +97,8 @@
"socket.io": "^2.2.0",
"socket.io-redis": "^5.2.0",
"sqlite3": "^4.1.0",
"stream-buffers": "^3.0.2",
"stream-to-array": "^2.3.0",
"toml": "^3.0.0",
"uuid": "^3.3.3",
"winston": "^3.2.1",

@ -4,13 +4,9 @@ import * as cookieParser from "cookie-parser";
import * as cors from "cors";
import * as express from "express";
import {Request, Response} from "express";
import * as graphqlHTTP from "express-graphql";
import * as session from "express-session";
import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra";
import {buildSchema, GraphQLError} from "graphql";
import {importSchema} from "graphql-import";
import queryComplexity, {directiveEstimator, simpleEstimator} from "graphql-query-complexity";
import * as http from "http";
import {IncomingMessage} from "http";
import * as httpStatus from "http-status";
@ -20,9 +16,9 @@ import {RedisClient} from "redis";
import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis";
import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals";
import {GraphqlRoute} from "./routes/GraphqlRoute";
import HomeRoute from "./routes/HomeRoute";
import {UploadRoute} from "./routes/UploadRoute";
@ -160,8 +156,10 @@ class App {
const uploadRoute = new UploadRoute(this.publicPath);
const homeRoute = new HomeRoute();
const graphqlRoute = new GraphqlRoute();
await uploadRoute.init();
await homeRoute.init(this.io);
await graphqlRoute.init();
this.app.use("/home", homeRoute.router);
this.limiter({
@ -190,42 +188,8 @@ class App {
path: "/graphql",
total: config.get("api.rateLimit.graphql.total"),
});
this.app.use("/graphql", graphqlRoute.router);
// @ts-ignore
this.app.use("/graphql", graphqlHTTP(async (request: any, response: any, {variables}) => {
response.setHeader("X-Max-Query-Complexity", config.get("api.maxQueryComplexity"));
return {
// @ts-ignore all
context: {session: request.session},
formatError: (err: GraphQLError | any) => {
if (err.statusCode) {
response.status(err.statusCode);
} else {
response.status(400);
}
logger.debug(err.message);
logger.silly(err.stack);
return err.graphqlError ?? err;
},
graphiql: config.get("api.graphiql"),
rootValue: resolver(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
validationRules: [
queryComplexity({
estimators: [
directiveEstimator(),
simpleEstimator(),
],
maximumComplexity: config.get("api.maxQueryComplexity"),
onComplete: (complexity: number) => {
logger.debug(`QueryComplexity: ${complexity}`);
response.setHeader("X-Query-Complexity", complexity);
},
variables,
}),
],
};
}));
// allow access to cluster information
this.app.use("/cluster-info", (req: Request, res: Response) => {
res.json({

@ -13,7 +13,7 @@ if (cluster.isMaster) {
cluster.on("exit", (worker, code) => {
console.error(`[CLUSTER-M] Worker ${worker.id} died! (code: ${code})`);
console.log("[CLUSTER-M] Starting new worker");
cluster.fork();
setTimeout(cluster.fork, 1000);
});
cluster.on("online", (worker) => {
worker.process.stdout.on("data", (data) => {

@ -0,0 +1,145 @@
import * as config from "config";
import * as crypto from "crypto";
import * as ffmpeg from "fluent-ffmpeg";
import * as fsx from "fs-extra";
import * as path from "path";
import * as sharp from "sharp";
import {Readable} from "stream";
import {ReadableStreamBuffer} from "stream-buffers";
import globals from "./globals";
const toArray = require("stream-to-array");
const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path;
const dataDirName = "data";
interface IUploadConfirmation {
/**
* Indicates the error that might have occured during the upload
*/
error?: string;
/**
* The file that has been uploaded
*/
fileName?: string;
/**
* If the upload was successful
*/
success: boolean;
}
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
/**
* A helper class for uploading and managing files
*/
export class UploadManager {
/**
* Returns the hash of the current time to be used as a filename.
*/
private static getCrypticFileName() {
const hash = crypto.createHash("md5");
hash.update(Number(Date.now()).toString());
return hash.digest("hex");
}
private readonly dataDir: string;
constructor() {
this.dataDir = path.join(globals.getPublicDir(), dataDirName);
ffmpeg.setFfmpegPath(ffmpegPath);
}
/**
* Deletes an image for a provided web path.
* @param webPath
*/
public async deleteWebFile(webPath: string) {
const realPath = path.join(dataDirName, path.basename(webPath));
if (await fsx.pathExists(realPath)) {
await fsx.unlink(realPath);
} else {
globals.logger.warn(`Could not delete web image ${realPath}: Not found!`);
}
}
/**
* Converts a file to the webp format and stores it with a uuid filename.
* The web path for the image is returned.
* @param data
* @param width
* @param height
* @param fit
*/
public async processAndStoreImage(data: Buffer, width = 512, height = 512,
fit: ImageFit = "cover"): Promise<string> {
const fileBasename = UploadManager.getCrypticFileName() + "." + config.get("api.imageFormat");
await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename);
let image = sharp(data)
.resize(width, height, {
background: "#00000000",
fit,
})
.normalise();
if (config.get<string>("api.imageFormat") === "webp") {
image = image.webp({
reductionEffort: 6,
smartSubsample: true,
});
} else {
image = image.png({
adaptiveFiltering: true,
colors: 128,
});
}
await image.toFile(filePath);
return `/${dataDirName}/${fileBasename}`;
}
/**
* Converts a video into a smaller format and .mp4 and returns the web path
* @param data
* @param width
*/
public async processAndStoreVideo(data: Buffer, width: number = 720): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const fileBasename = UploadManager.getCrypticFileName() + ".webm";
await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename);
const tempFile = filePath + ".tmp";
await fsx.writeFile(tempFile, data);
const video = ffmpeg(tempFile);
video
.size(`${width}x?`)
.toFormat("webm")
.on("end", async () => {
await fsx.unlink(tempFile);
resolve(`/${dataDirName}/${fileBasename}`);
})
.on("error", async (err) => {
await fsx.unlink(tempFile);
reject(err);
})
.save(filePath);
} catch (err) {
reject(err);
}
});
}
/**
* Convers a readable to a buffer
* @param stream
*/
public async streamToBuffer(stream: Readable) {
const parts = await toArray(stream);
const buffers = parts
.map((part: any) => Buffer.isBuffer(part) ? part : Buffer.from(part));
return Buffer.concat(buffers);
}
}

@ -0,0 +1,14 @@
import * as httpStatus from "http-status";
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a invalid file type is uploaded
*/
export class InvalidFileError extends BaseError {
public readonly statusCode = httpStatus.NOT_ACCEPTABLE;
constructor(mimetype: string) {
super(`The mimetype '${mimetype}' is not allowed.`);
}
}

@ -0,0 +1,10 @@
import {BaseError} from "./BaseError";
/**
* An error that is thrown when a file failed to upload
*/
export class UploadFailedError extends BaseError {
constructor() {
super("Failed to upload the file");
}
}

@ -1,5 +1,6 @@
import * as config from "config";
import {EventEmitter} from "events";
import * as path from "path";
import * as winston from "winston";
require("winston-daily-rotate-file");
@ -38,6 +39,18 @@ namespace globals {
}),
],
});
/**
* Returns the absolute public path
*/
export function getPublicDir(): string {
let publicPath = config.get<string>("frontend.publicPath");
if (!path.isAbsolute(publicPath)) {
publicPath = path.normalize(path.join(__dirname, "../", publicPath));
}
return publicPath;
}
export const internalEmitter: EventEmitter = new EventEmitter();
}

@ -33,6 +33,12 @@ export class Post extends Model<Post> {
@Column({allowNull: true})
public activityId: number;
/**
* An url pointing to any media that belongs to the post
*/
@Column({allowNull: true, type: sqz.STRING(512)})
public mediaUrl: string;
/**
* The author of the post
*/
@ -100,6 +106,22 @@ export class Post extends Model<Post> {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
}
/**
* Returns the media description object of the post
*/
public get media() {
const url = this.getDataValue("mediaUrl");
if (url) {
const type = url.endsWith(".webm") ? "VIDEO" : "IMAGE";
return {
type,
url,
};
} else {
return null;
}
}
/**
* Toggles the vote of the user.
* @param userId

@ -1,5 +1,7 @@
export namespace is {
const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g;
const videoRegex = /video\/.*/g;
const imageRegex = /image\/.*/g;
/**
* Tests if a string is a valid email.
@ -8,4 +10,20 @@ export namespace is {
export function email(testString: string) {
return emailRegex.test(testString);
}
/**
* Returns if the mimetype is a video
* @param mimetype
*/
export function video(mimetype: string) {
return videoRegex.test(mimetype);
}
/**
* Returns if the mimetype is an image
* @param mimetype
*/
export function image(mimetype: string) {
return imageRegex.test(mimetype);
}
}

@ -0,0 +1,81 @@
import * as config from "config";
import {Router} from "express";
import * as graphqlHTTP from "express-graphql";
import {buildSchema, GraphQLError} from "graphql";
import {importSchema} from "graphql-import";
import queryComplexity, {directiveEstimator, simpleEstimator} from "graphql-query-complexity";
import {graphqlUploadExpress} from "graphql-upload";
import * as path from "path";
import globals from "../lib/globals";
import Route from "../lib/Route";
import {resolver} from "./graphql/resolvers";
const logger = globals.logger;
/**
* A class for the /grpahql route
*/
export class GraphqlRoute extends Route {
/**
* Constructor, creates new router.
*/
constructor() {
super();
this.router = Router();
}
/**
* Initializes the route
* @param params
*/
public async init(...params: any): Promise<any> {
this.router.use(graphqlUploadExpress({
maxFileSize: config.get<number>("api.maxFileSize"),
maxFiles: 10,
}));
// @ts-ignore
this.router.use(graphqlHTTP(async (request: any, response: any, {variables}) => {
response.setHeader("X-Max-Query-Complexity", config.get("api.maxQueryComplexity"));
return {
// @ts-ignore all
context: {session: request.session},
formatError: (err: GraphQLError | any) => {
if (err.statusCode) {
response.status(err.statusCode);
} else {
response.status(400);
}
logger.debug(err.message);
logger.silly(err.stack);
return err.graphqlError ?? err;
},
graphiql: config.get<boolean>("api.graphiql"),
rootValue: resolver(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
validationRules: [
queryComplexity({
estimators: [
directiveEstimator(),
simpleEstimator(),
],
maximumComplexity: config.get<number>("api.maxQueryComplexity"),
onComplete: (complexity: number) => {
logger.debug(`QueryComplexity: ${complexity}`);
response.setHeader("X-Query-Complexity", complexity);
},
variables,
}),
],
};
}));
}
/**
* Destroys the route
* @param params
*/
public async destroy(...params: any): Promise<any> {
return undefined;
}
}

@ -2,15 +2,17 @@ import * as bodyParser from "body-parser";
import * as config from "config";
import * as crypto from "crypto";
import {Router} from "express";
import * as fileUpload from "express-fileupload";
import {UploadedFile} from "express-fileupload";
import * as fileUpload from "express-fileupload";
import * as ffmpeg from "fluent-ffmpeg";
import * as fsx from "fs-extra";
import * as status from "http-status";
import * as path from "path";
import * as sharp from "sharp";
import globals from "../lib/globals";
import {Group, User} from "../lib/models";
import {Group, Post, User} from "../lib/models";
import {is} from "../lib/regex";
import Route from "../lib/Route";
import {UploadManager} from "../lib/UploadManager";
const dataDirName = "data";
@ -51,11 +53,13 @@ export class UploadRoute extends Route {
* The directory where the uploaded data will be saved in
*/
public readonly dataDir: string;
private uploadManager: UploadManager;
constructor(private publicPath: string) {
super();
this.router = Router();
this.dataDir = path.join(this.publicPath, dataDirName);
this.uploadManager = new UploadManager();
}
/**
@ -63,7 +67,9 @@ export class UploadRoute extends Route {
*/
public async init() {
await fsx.ensureDir(this.dataDir);
this.router.use(fileUpload());
this.router.use(fileUpload({
limits: config.get<number>("api.maxFileSize"),
}));
this.router.use(bodyParser());
// Uploads a file to the data directory and returns the filename
this.router.use(async (req, res) => {
@ -73,6 +79,8 @@ export class UploadRoute extends Route {
uploadConfirmation = await this.uploadProfilePicture(req);
} else if (req.files.groupPicture) {
uploadConfirmation = await this.uploadGroupPicture(req);
} else if (req.files.postMedia) {
uploadConfirmation = await this.uploadPostMedia(req);
} else {
res.status(status.BAD_REQUEST);
uploadConfirmation = {
@ -112,9 +120,9 @@ export class UploadRoute extends Route {
try {
const user = await User.findByPk(request.session.userId);
if (user) {
fileName = await this.processAndStoreImage(profilePic.data);
fileName = await this.uploadManager.processAndStoreImage(profilePic.data);
if (user.profilePicture) {
await this.deleteWebImage(user.profilePicture);
await this.uploadManager.deleteWebFile(user.profilePicture);
}
user.profilePicture = fileName;
await user.save();
@ -156,9 +164,9 @@ export class UploadRoute extends Route {
}
const isAdmin = await group.$has("rAdmins", user);
if (isAdmin) {
fileName = await this.processAndStoreImage(groupPicture.data);
fileName = await this.uploadManager.processAndStoreImage(groupPicture.data);
if (group.picture) {
await this.deleteWebImage(group.picture);
await this.uploadManager.deleteWebFile(group.picture);
}
group.picture = fileName;
await group.save();
@ -182,48 +190,47 @@ export class UploadRoute extends Route {
}
/**
* Converts a file to the webp format and stores it with a uuid filename.
* The web path for the image is returned.
* @param data
* @param width
* @param height
* @param fit
* Uploads a media file for a post
* @param request
*/
private async processAndStoreImage(data: Buffer, width = 512, height = 512,
fit: ImageFit = "cover"): Promise<string> {
const fileBasename = UploadRoute.getFileName() + "." + config.get("api.imageFormat");
await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename);
let image = await sharp(data)
.resize(width, height, {
fit,
})
.normalise();
if (config.get("api.imageFormat") === "webp") {
image = await image.webp({
reductionEffort: 6,
smartSubsample: true,
});
private async uploadPostMedia(request: any) {
let error: string;
let fileName: string;
let success = false;
const postId = request.body.postId;
const postMedia = request.files.postMedia as UploadedFile;
if (postId) {
try {
const post = await Post.findByPk(postId);
if (post.authorId === request.session.userId) {
if (is.image(postMedia.mimetype)) {
fileName = await this.uploadManager.processAndStoreImage(postMedia.data, 1080, 720, "contain");
} else if (is.video(postMedia.mimetype)) {
fileName = await this.uploadManager.processAndStoreVideo(postMedia.data, 1080);
} else {
image = await image.png({
adaptiveFiltering: true,
colors: 128,
});
error = "Wrong type of file provided";
}
await image.toFile(filePath);
return `/${dataDirName}/${fileBasename}`;
if (fileName) {
post.mediaUrl = fileName;
await post.save();
success = true;
}
/**
* Deletes an image for a provided web path.
* @param webPath
*/
private async deleteWebImage(webPath: string) {
const realPath = path.join(this.dataDir, path.basename(webPath));
if (await fsx.pathExists(realPath)) {
await fsx.unlink(realPath);
} else {
globals.logger.warn(`Could not delete web image ${realPath}: Not found!`);
error = "You are not the author of the post";
}
} catch (err) {
error = err.message;
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
} else {
error = "No post Id provided";
}
return {
error,
fileName,
success,
};
}
}

@ -1,4 +1,4 @@
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
import {NotLoggedInGqlError} from "../../lib/errors/graphqlErrors";
/**
* Base resolver class to provide common methods to all resolver classes

@ -1,17 +1,21 @@
import {GraphQLError} from "graphql";
import {FileUpload} from "graphql-upload";
import * as yaml from "js-yaml";
import isEmail from "validator/lib/isEmail";
import dataaccess from "../lib/dataAccess";
import {BlacklistedError} from "../lib/errors/BlacklistedError";
import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError";
import {InvalidEmailError} from "../lib/errors/InvalidEmailError";
import {NotAGroupAdminError} from "../lib/errors/NotAGroupAdminError";
import {NotAnAdminError} from "../lib/errors/NotAnAdminError";
import {NotTheGroupCreatorError} from "../lib/errors/NotTheGroupCreatorError";
import {PostNotFoundError} from "../lib/errors/PostNotFoundError";
import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents";
import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../lib/models";
import dataaccess from "../../lib/dataAccess";
import {BlacklistedError} from "../../lib/errors/BlacklistedError";
import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError";
import {InvalidEmailError} from "../../lib/errors/InvalidEmailError";
import {InvalidFileError} from "../../lib/errors/InvalidFileError";
import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError";
import {NotAnAdminError} from "../../lib/errors/NotAnAdminError";
import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError";
import {PostNotFoundError} from "../../lib/errors/PostNotFoundError";
import globals from "../../lib/globals";
import {InternalEvents} from "../../lib/InternalEvents";
import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models";
import {is} from "../../lib/regex";
import {UploadManager} from "../../lib/UploadManager";
import {BaseResolver} from "./BaseResolver";
const legit = require("legit");
@ -21,6 +25,17 @@ const legit = require("legit");
*/
export class MutationResolver extends BaseResolver {
/**
* An instance of the upload manager to handle uploads
*/
protected uploadManager: UploadManager;
constructor() {
super();
this.uploadManager = new UploadManager();
}
/**
* Accepts the usage of cookies and stores the session
* @param args
@ -130,8 +145,8 @@ export class MutationResolver extends BaseResolver {
* @param activityId
* @param request
*/
public async createPost({content, activityId}: { content: string, activityId?: number }, request: any):
Promise<Post> {
public async createPost({content, activityId}: { content: string, activityId?: number},
request: any): Promise<Post> {
this.ensureLoggedIn(request);
if (content.length > 2048) {
throw new GraphQLError("Content too long.");
@ -156,6 +171,14 @@ export class MutationResolver extends BaseResolver {
});
const isAdmin = (await User.findOne({where: {id: request.session.userId}})).isAdmin;
if (post.rAuthor.id === request.session.userId || isAdmin) {
if (post.mediaUrl) {
try {
await this.uploadManager.deleteWebFile(post.mediaUrl);
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
}
return await dataaccess.deletePost(post.id);
} else {
throw new GraphQLError("User is not author of the post.");

@ -1,12 +1,12 @@
import {GraphQLError} from "graphql";
import {Op} from "sequelize";
import dataaccess from "../lib/dataAccess";
import {ChatNotFoundError} from "../lib/errors/ChatNotFoundError";
import {PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import {GroupNotFoundError} from "../lib/errors/GroupNotFoundError";
import {RequestNotFoundError} from "../lib/errors/RequestNotFoundError";
import {UserNotFoundError} from "../lib/errors/UserNotFoundError";
import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Request, User} from "../lib/models";
import dataaccess from "../../lib/dataAccess";
import {ChatNotFoundError} from "../../lib/errors/ChatNotFoundError";
import {PostNotFoundGqlError} from "../../lib/errors/graphqlErrors";
import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError";
import {RequestNotFoundError} from "../../lib/errors/RequestNotFoundError";
import {UserNotFoundError} from "../../lib/errors/UserNotFoundError";
import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models";
import {BlacklistedResult} from "./BlacklistedResult";
import {MutationResolver} from "./MutationResolver";
import {SearchResult} from "./SearchResult";

@ -1,4 +1,4 @@
import {Event, Group, Post, User} from "../lib/models";
import {Event, Group, Post, User} from "../../lib/models";
/**
* A class to wrap search results returned by the search resolver

@ -7,6 +7,8 @@ directive @complexity(
multipliers: [String!]
) on FIELD_DEFINITION
scalar Upload
type Query {
"returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID, handle: String): User
@ -338,6 +340,9 @@ type Post {
"the activity that belongs to the post"
activity: Activity
"the uploaded file or video for the post"
media: Media
}
"represents a request of any type"
@ -506,8 +511,21 @@ type BlacklistedResult {
phrases: [String!]!
}
"a type of uploaded media"
type Media {
"the url pointing to the media in the data folder"
url: String!
"the type of media that is uploaded"
type: MediaType
}
"represents the type of media"
enum MediaType {
VIDEO
IMAGE
}
"represents the type of vote performed on a post"
enum VoteType {

@ -18,6 +18,61 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
"@ffmpeg-installer/darwin-x64@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz#48e1706c690e628148482bfb64acb67472089aaa"
integrity sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==
"@ffmpeg-installer/ffmpeg@^1.0.20":
version "1.0.20"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.0.20.tgz#d3c9c2bbcd76149468fb0886c2b3fe9e4795490b"
integrity sha512-wbgd//6OdwbFXYgV68ZyKrIcozEQpUKlvV66XHaqO2h3sFbX0jYLzx62Q0v8UcFWN21LoxT98NU2P+K0OWsKNA==
optionalDependencies:
"@ffmpeg-installer/darwin-x64" "4.1.0"
"@ffmpeg-installer/linux-arm" "4.1.3"
"@ffmpeg-installer/linux-arm64" "4.1.4"
"@ffmpeg-installer/linux-ia32" "4.1.0"
"@ffmpeg-installer/linux-x64" "4.1.0"
"@ffmpeg-installer/win32-ia32" "4.1.0"
"@ffmpeg-installer/win32-x64" "4.1.0"
"@ffmpeg-installer/linux-arm64@4.1.4":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz#7219f3f901bb67f7926cb060b56b6974a6cad29f"
integrity sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==
"@ffmpeg-installer/linux-arm@4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz#c554f105ed5f10475ec25d7bec94926ce18db4c1"
integrity sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==
"@ffmpeg-installer/linux-ia32@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz#adad70b0d0d9d8d813983d6e683c5a338a75e442"
integrity sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==
"@ffmpeg-installer/linux-x64@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz#b4a5d89c4e12e6d9306dbcdc573df716ec1c4323"
integrity sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==
"@ffmpeg-installer/win32-ia32@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz#6eac4fb691b64c02e7a116c1e2d167f3e9b40638"
integrity sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==
"@ffmpeg-installer/win32-x64@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz#17e8699b5798d4c60e36e2d6326a8ebe5e95a2c5"
integrity sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==
"@types/accepts@*":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
dependencies:
"@types/node" "*"
"@types/babel-types@*", "@types/babel-types@^7.0.0":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
@ -90,6 +145,16 @@
dependencies:
"@types/express" "*"
"@types/cookies@*":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b"
integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw==
dependencies:
"@types/connect" "*"
"@types/express" "*"
"@types/keygrip" "*"
"@types/node" "*"
"@types/cors@^2.8.6":
version "2.8.6"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02"
@ -145,6 +210,20 @@
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/fluent-ffmpeg@^2.1.13":
version "2.1.13"
resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.13.tgz#bfffbcf298b0980924e9ba9aa471aba234626afb"
integrity sha512-hg87ZQb9WVcNGQHNhrYwWJM0ARNYbQbLGh1c6CfPl55/I+BH5UTpFJAr5aZWYGbl8BFVY82oF5iG4I+Ra3btiQ==
dependencies:
"@types/node" "*"
"@types/fs-capacitor@*":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e"
integrity sha512-FKVPOCFbhCvZxpVAMhdBdTfVfXUpsh15wFHgqOKxh9N9vzWZVuWCSijZ5T4U34XYNnuj2oduh6xcs1i+LPI+BQ==
dependencies:
"@types/node" "*"
"@types/fs-extra@^8.0.0":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.0.1.tgz#a2378d6e7e8afea1564e44aafa2e207dadf77686"
@ -159,6 +238,21 @@
dependencies:
graphql-query-complexity "*"
"@types/graphql-upload@^8.0.3":
version "8.0.3"
resolved "https://registry.yarnpkg.com/@types/graphql-upload/-/graphql-upload-8.0.3.tgz#b371edb5f305a2a1f7b7843a890a2a7adc55c3ec"
integrity sha512-hmLg9pCU/GmxBscg8GCr1vmSoEmbItNNxdD5YH2TJkXm//8atjwuprB+xJBK714JG1dkxbbhp5RHX+Pz1KsCMA==
dependencies:
"@types/express" "*"
"@types/fs-capacitor" "*"
"@types/koa" "*"
graphql "^14.5.3"
"@types/http-assert@*":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==
"@types/http-status@^0.2.30":
version "0.2.30"
resolved "https://registry.yarnpkg.com/@types/http-status/-/http-status-0.2.30.tgz#b43a1e1673b6ed9b5a28e8647862b51b6473634d"
@ -169,6 +263,30 @@
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656"
integrity sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==
"@types/keygrip@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
"@types/koa-compose@*":
version "3.2.5"
resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
dependencies:
"@types/koa" "*"
"@types/koa@*":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.0.tgz#394a3e9ec94f796003a6c8374b4dbc2778746f20"
integrity sha512-Hgx/1/rVlJvqYBrdeCsS7PDiR2qbxlMt1RnmNWD4Uxi5FF9nwkYqIldo7urjc+dfNpk+2NRGcnAYd4L5xEhCcQ==
dependencies:
"@types/accepts" "*"
"@types/cookies" "*"
"@types/http-assert" "*"
"@types/keygrip" "*"
"@types/koa-compose" "*"
"@types/node" "*"
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
@ -219,6 +337,14 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/readable-stream@^2.3.5":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.5.tgz#99c215f9c78563ebdfeff400246a724fb36bae4a"
integrity sha512-Mq2eLkGYamlcolW603FY2ROBvcl90jPF+3jLkjpBV6qS+2aVeJqlgRG0TVAa1oWbmPdb5yOWlOPVvQle76nUNw==
dependencies:
"@types/node" "*"
safe-buffer "*"
"@types/redis@^2.8.14":
version "2.8.14"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.14.tgz#2ed46d0f923f7ccd63fbe73a46a1241e606cf716"
@ -265,6 +391,13 @@
dependencies:
"@types/node" "*"
"@types/stream-buffers@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b"
integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ==
dependencies:
"@types/node" "*"
"@types/uuid@^3.4.6":
version "3.4.6"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016"
@ -401,7 +534,7 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768=
any-promise@^1.3.0:
any-promise@^1.1.0, any-promise@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
@ -601,6 +734,11 @@ async-settle@^1.0.0:
dependencies:
async-done "^1.2.2"
async@>=0.2.9:
version "3.1.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.1.0.tgz#42b3b12ae1b74927b5217d8c0016baaf62463772"
integrity sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ==
async@^2.6.1:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
@ -2011,6 +2149,14 @@ flat@^4.1.0:
dependencies:
is-buffer "~2.0.3"
fluent-ffmpeg@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74"
integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=
dependencies:
async ">=0.2.9"
which "^1.1.1"
flush-write-stream@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@ -2062,6 +2208,14 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
fs-capacitor@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-4.0.1.tgz#eb65700e641ce0323411694c1d8c1609273642ff"
integrity sha512-e0qFoKQMFe52F54dMvZLD+I1M/Gs6xB2gnZVQB5FYT/8ioP6qTb3U/tzp55O0IuPOMvSM8j4ta0bVafIFjJzxQ==
dependencies:
"@types/readable-stream" "^2.3.5"
readable-stream "^3.4.0"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@ -2292,6 +2446,16 @@ graphql-query-complexity@*, graphql-query-complexity@^0.4.1:
dependencies:
lodash.get "^4.4.2"
graphql-upload@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-9.0.0.tgz#14c5250895f5d04ea83dcfcb3a16bd0e1c4cee95"
integrity sha512-YR2o9GoDa5On3q3lYLkLo3gHfa8crCHvMY1QbT7Zqja6BUqiihqaGjbWbvSPko/gbDSmZE+zLcX46Ef+/SmRyA==
dependencies:
busboy "^0.3.1"
fs-capacitor "^4.0.1"
http-errors "^1.7.3"
object-path "^0.11.4"
graphql@^14.4.2, graphql@^14.5.3:
version "14.5.8"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c"
@ -3573,6 +3737,11 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object-path@^0.11.4:
version "0.11.4"
resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949"
integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=
object-visit@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@ -4223,6 +4392,15 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.4.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606"
integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@ -4428,16 +4606,16 @@ rimraf@^2.6.1:
dependencies:
glob "^7.1.3"
safe-buffer@*, safe-buffer@5.2.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@5.2.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@ -4832,6 +5010,11 @@ static-extend@^0.1.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
stream-buffers@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521"
integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==
stream-exhaust@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d"
@ -4842,6 +5025,13 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
stream-to-array@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=
dependencies:
any-promise "^1.1.0"
streamsearch@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@ -5476,7 +5666,7 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@1.3.1, which@^1.2.14:
which@1.3.1, which@^1.1.1, which@^1.2.14:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==

Loading…
Cancel
Save