@ -1,4 +1,5 @@
import { FileData , AssRequest , AssResponse , ErrWrap , User } from "../definitions" ;
import { ErrWrap , User } from '../types/definitions' ;
import { Config , MagicNumbers } from 'ass-json' ;
import fs from 'fs-extra' ;
import fs from 'fs-extra' ;
import bb from 'express-busboy' ;
import bb from 'express-busboy' ;
@ -6,14 +7,14 @@ import bb from 'express-busboy';
import { DateTime } from 'luxon' ;
import { DateTime } from 'luxon' ;
import { Webhook , MessageBuilder } from 'discord-webhook-node' ;
import { Webhook , MessageBuilder } from 'discord-webhook-node' ;
import { processUploaded } from '../storage' ;
import { processUploaded } from '../storage' ;
const { maxUploadSize , resourceIdSize , gfyIdSize , resourceIdType , spaceReplace } = require ( '../../config.json' ) ;
import { path , log , verify , getTrueHttp , getTrueDomain , generateId , formatBytes } from '../utils' ;
import { path , log , verify , getTrueHttp , getTrueDomain , generateId , formatBytes } from '../utils' ;
const { CODE_UNAUTHORIZED , CODE_PAYLOAD_TOO_LARGE } = require ( '../../MagicNumbers.json' ) ;
import { data } from '../data' ;
import { data } from '../data' ;
import { users } from '../auth' ;
import { users } from '../auth' ;
const { maxUploadSize , resourceIdSize , gfyIdSize , resourceIdType , spaceReplace } : Config = fs . readJsonSync ( path ( 'config.json' ) ) ;
const { CODE_UNAUTHORIZED , CODE_PAYLOAD_TOO_LARGE } : MagicNumbers = fs . readJsonSync ( path ( 'MagicNumbers.json' ) ) ;
const ASS_LOGO = 'https://cdn.discordapp.com/icons/848274994375294986/8d339d4a2f3f54b2295e5e0ff62bd9e6.png?size=1024' ;
const ASS_LOGO = 'https://cdn.discordapp.com/icons/848274994375294986/8d339d4a2f3f54b2295e5e0ff62bd9e6.png?size=1024' ;
import express from 'express' ;
import express , { Request , Response } from 'express' ;
const router = express . Router ( ) ;
const router = express . Router ( ) ;
// Set up express-busboy
// Set up express-busboy
@ -31,7 +32,7 @@ bb.extend(router, {
} ) ) ; * /
} ) ) ; * /
// Block unauthorized requests and attempt token sanitization
// Block unauthorized requests and attempt token sanitization
router . post ( '/' , ( req : Ass Request, res : Ass Response, next : Function ) = > {
router . post ( '/' , ( req : Request, res : Response, next : Function ) = > {
req . headers . authorization = req . headers . authorization || '' ;
req . headers . authorization = req . headers . authorization || '' ;
req . token = req . headers . authorization . replace ( /[^\da-z]/gi , '' ) ; // Strip anything that isn't a digit or ASCII letter
req . token = req . headers . authorization . replace ( /[^\da-z]/gi , '' ) ; // Strip anything that isn't a digit or ASCII letter
! verify ( req , users ) ? log . warn ( 'Upload blocked' , 'Unauthorized' ) . callback ( ( ) = > res . sendStatus ( CODE_UNAUTHORIZED ) ) : next ( ) ; // skipcq: JS-0093
! verify ( req , users ) ? log . warn ( 'Upload blocked' , 'Unauthorized' ) . callback ( ( ) = > res . sendStatus ( CODE_UNAUTHORIZED ) ) : next ( ) ; // skipcq: JS-0093
@ -41,28 +42,28 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
router . post ( '/' , processUploaded ) ;
router . post ( '/' , processUploaded ) ;
// Max upload size error handling
// Max upload size error handling
router . use ( '/' , ( err : ErrWrap , _req : Ass Request, res : Ass Response, next : Function ) = > err . message === 'LIMIT_FILE_SIZE' ? log . warn ( 'Upload blocked' , 'File too large' ) . callback ( ( ) = > res . status ( CODE_PAYLOAD_TOO_LARGE ) . send ( ` Max upload size: ${ maxUploadSize } MB ` ) ) : next ( err ) ) ; // skipcq: JS-0229
router . use ( '/' , ( err : ErrWrap , _req : Request, res : Response, next : Function ) = > err . message === 'LIMIT_FILE_SIZE' ? log . warn ( 'Upload blocked' , 'File too large' ) . callback ( ( ) = > res . status ( CODE_PAYLOAD_TOO_LARGE ) . send ( ` Max upload size: ${ maxUploadSize } MB ` ) ) : next ( err ) ) ; // skipcq: JS-0229
// Process uploaded file
// Process uploaded file
router . post ( '/' , ( req : Ass Request, res : Ass Response, next : Function ) = > {
router . post ( '/' , ( req : Request, res : Response, next : Function ) = > {
// Load overrides
// Load overrides
const trueDomain = getTrueDomain ( req . headers [ 'x-ass-domain' ] ) ;
const trueDomain = getTrueDomain ( req . headers [ 'x-ass-domain' ] ) ;
const generator = req . headers [ 'x-ass-access' ] || resourceIdType ;
const generator = req . headers [ 'x-ass-access' ] ? . toString ( ) || resourceIdType ;
// Save domain with file
// Save domain with file
req . file ! . domain = ` ${ getTrueHttp ( ) } ${ trueDomain } ` ;
req . file . domain = ` ${ getTrueHttp ( ) } ${ trueDomain } ` ;
// Get the uploaded time in milliseconds
// Get the uploaded time in milliseconds
req . file ! . timestamp = DateTime . now ( ) . toMillis ( ) ;
req . file . timestamp = DateTime . now ( ) . toMillis ( ) ;
// Save the timezone offset
// Save the timezone offset
req . file ! . timeoffset = req . headers [ 'x-ass-timeoffset' ] ? . toString ( ) || 'UTC+0' ;
req . file ! . timeoffset = req . headers [ 'x-ass-timeoffset' ] ? . toString ( ) || 'UTC+0' ;
// Keep track of the token that uploaded the resource
// Keep track of the token that uploaded the resource
req . file ! . token = req . token ? ? '' ;
req . file . token = req . token ? ? '' ;
// Attach any embed overrides, if necessary
// Attach any embed overrides, if necessary
req . file ! . opengraph = {
req . file . opengraph = {
title : req.headers [ 'x-ass-og-title' ] ,
title : req.headers [ 'x-ass-og-title' ] ,
description : req.headers [ 'x-ass-og-description' ] ,
description : req.headers [ 'x-ass-og-description' ] ,
author : req.headers [ 'x-ass-og-author' ] ,
author : req.headers [ 'x-ass-og-author' ] ,
@ -73,13 +74,13 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
} ;
} ;
// Fix spaces in originalname
// Fix spaces in originalname
req . file ! . originalname = req . file ! . originalname . replace ( /\s/g , spaceReplace === '!' ? '' : spaceReplace ) ;
req . file ! . originalname = req . file . originalname . replace ( /\s/g , spaceReplace === '!' ? '' : spaceReplace ) ;
// Generate a unique resource ID
// Generate a unique resource ID
let resourceId = '' ;
let resourceId = '' ;
// Function to call to generate a fresh ID. Used for multiple attempts in case an ID is already taken
// Function to call to generate a fresh ID. Used for multiple attempts in case an ID is already taken
const gen = ( ) = > generateId ( generator , resourceIdSize , req . headers [ 'x-ass-gfycat' ] || gfyIdSize , req . file ! . originalname ) ;
const gen = ( ) = > generateId ( generator , resourceIdSize , parseInt ( req . headers [ 'x-ass-gfycat' ] ? . toString ( ) || gfyIdSize .toString ( ) ) , req . file . originalname ) ;
// Keeps track of the number of attempts in case all ID's are taken
// Keeps track of the number of attempts in case all ID's are taken
const attempts = {
const attempts = {
@ -91,7 +92,7 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
function genCheckId ( resolve : Function , reject : Function ) {
function genCheckId ( resolve : Function , reject : Function ) {
const uniqueId = gen ( ) ;
const uniqueId = gen ( ) ;
attempts . count ++ ;
attempts . count ++ ;
data .has ( uniqueId )
data () .has ( uniqueId )
. then ( ( exists : boolean ) = > {
. then ( ( exists : boolean ) = > {
log . debug ( 'ID check' , exists ? 'Taken' : 'Available' ) ;
log . debug ( 'ID check' , exists ? 'Taken' : 'Available' ) ;
return attempts . count - 1 >= attempts . max ? reject ( new Error ( 'No ID\'s remaining' ) ) : exists ? genCheckId ( resolve , reject ) : resolve ( uniqueId ) ;
return attempts . count - 1 >= attempts . max ? reject ( new Error ( 'No ID\'s remaining' ) ) : exists ? genCheckId ( resolve , reject ) : resolve ( uniqueId ) ;
@ -105,16 +106,16 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
resourceId = uniqueId ;
resourceId = uniqueId ;
log . debug ( 'Saving data' , data . name ) ;
log . debug ( 'Saving data' , data . name ) ;
} )
} )
. then ( ( ) = > data .put ( resourceId . split ( '.' ) [ 0 ] , req . file ) )
. then ( ( ) = > data () .put ( resourceId . split ( '.' ) [ 0 ] , req . file ) )
. then ( ( ) = > {
. then ( ( ) = > {
// Log the upload
// Log the upload
const logInfo = ` ${ req . file ! . originalname } ( ${ req . file ! . mimetype } , ${ formatBytes ( req . file ! . size ) } ) ` ;
const logInfo = ` ${ req . file ! . originalname } ( ${ req . file ! . mimetype } , ${ formatBytes ( req . file . size ) } ) ` ;
log . success ( 'File uploaded' , logInfo , ` uploaded by ${ users [ req . token ? ? '' ] ? users [ req . token ? ? '' ] . username : '<token-only>' } ` ) ;
log . success ( 'File uploaded' , logInfo , ` uploaded by ${ users [ req . token ? ? '' ] ? users [ req . token ? ? '' ] . username : '<token-only>' } ` ) ;
// Build the URLs
// Build the URLs
const resourceUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } ` ;
const resourceUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } ` ;
const thumbnailUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } /thumbnail ` ;
const thumbnailUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } /thumbnail ` ;
const deleteUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } /delete/ ${ req . file ! . deleteId } ` ;
const deleteUrl = ` ${ getTrueHttp ( ) } ${ trueDomain } / ${ resourceId } /delete/ ${ req . file . deleteId } ` ;
// Send the response
// Send the response
res . type ( 'json' ) . send ( { resource : resourceUrl , thumbnail : thumbnailUrl , delete : deleteUrl } )
res . type ( 'json' ) . send ( { resource : resourceUrl , thumbnail : thumbnailUrl , delete : deleteUrl } )
@ -134,9 +135,9 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
. setTitle ( logInfo )
. setTitle ( logInfo )
//@ts-ignore
//@ts-ignore
. setURL ( resourceUrl )
. setURL ( resourceUrl )
. setDescription ( ` **Size:** \` ${ formatBytes ( req . file ! . size ) } \` \ n**[Delete]( ${ deleteUrl } )** ` )
. setDescription ( ` **Size:** \` ${ formatBytes ( req . file . size ) } \` \ n**[Delete]( ${ deleteUrl } )** ` )
. setThumbnail ( thumbnailUrl )
. setThumbnail ( thumbnailUrl )
. setColor ( req . file ! . vibrant )
. setColor ( req . file . vibrant )
. setTimestamp ( ) ;
. setTimestamp ( ) ;
// Send the embed to the webhook, then delete the client after to free resources
// Send the embed to the webhook, then delete the client after to free resources
@ -148,7 +149,7 @@ router.post('/', (req: AssRequest, res: AssResponse, next: Function) => {
// Also update the users upload count
// Also update the users upload count
if ( ! users [ req . token ? ? '' ] ) {
if ( ! users [ req . token ? ? '' ] ) {
const generateUsername = ( ) = > generateId ( 'random' , 20 , 0 , req . file ! . size . toString ( ) ) ; // skipcq: JS-0074
const generateUsername = ( ) = > generateId ( 'random' , 20 , 0 , req . file . size . toString ( ) ) ; // skipcq: JS-0074
let username : string = generateUsername ( ) ;
let username : string = generateUsername ( ) ;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// eslint-disable-next-line @typescript-eslint/ban-ts-comment