pull/249/merge
X 6 months ago committed by GitHub
commit 0281ae058f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,8 +1,12 @@
import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass';
import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoDBConfiguration } from 'ass';
import fs from 'fs-extra';
import { path } from '@tycrek/joint';
import { log } from './log.js';
import { prepareTemplate } from './templates/parser.js';
import { TemplateError } from './templates/error.js';
import { DEFAULT_EMBED, validateEmbed } from './embed.js';
const FILEPATH = path.join('.ass-data/userconfig.json');
@ -58,9 +62,6 @@ const Checkers: UserConfigTypeChecker = {
password: basicStringChecker,
database: basicStringChecker,
port: (val) => numChecker(val) && val >= 1 && val <= 65535
},
postgres: {
port: (val) => numChecker(val) && val >= 1 && val <= 65535
}
},
@ -109,18 +110,13 @@ export class UserConfig {
// * Optional database config(s)
if (config.database != null) {
// these both have the same schema so we can just check both
if (config.database.kind == 'mysql' || config.database.kind == 'postgres') {
if (config.database.kind == 'mysql' || config.database.kind == 'postgres' || config.database.kind == 'mongodb') {
if (config.database.options != undefined) {
if (!Checkers.sql.mySql.host(config.database.options.host)) throw new Error('Invalid database host');
if (!Checkers.sql.mySql.user(config.database.options.user)) throw new Error('Invalid databse user');
if (!Checkers.sql.mySql.password(config.database.options.password)) throw new Error('Invalid database password');
if (!Checkers.sql.mySql.database(config.database.options.database)) throw new Error('Invalid database');
if (!Checkers.sql.mySql.port(config.database.options.port)) throw new Error('Invalid database port');
if (config.database.kind == 'postgres') {
if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration).port)) {
throw new Error("Invalid database port");
}
}
} else throw new Error('Database options missing');
}
}
@ -132,6 +128,30 @@ export class UserConfig {
if (!Checkers.rateLimit.endpoint(config.rateLimit.api)) throw new Error('Invalid API rate limit configuration');
}
// * the embed
if (config.embed != null) {
try {
for (let part of ['title', 'description', 'sitename'] as ('title' | 'description' | 'sitename')[]) {
if (config.embed[part] != null) {
if (typeof config.embed[part] == 'string') {
config.embed[part] = prepareTemplate(config.embed[part] as string, {
allowIncludeFile: true
});
} else throw new Error(`Template string for embed ${part} is not a string`);
} else config.embed[part] = DEFAULT_EMBED[part];
}
validateEmbed(config.embed);
} catch (err) {
if (err instanceof TemplateError) {
// tlog messes up the formatting
console.error(err.format());
throw new Error('Template error');
} else throw err;
}
} else config.embed = DEFAULT_EMBED;
// All is fine, carry on!
return config;
}

@ -15,6 +15,7 @@ import { DBManager } from './sql/database.js';
import { JSONDatabase } from './sql/json.js';
import { MySQLDatabase } from './sql/mysql.js';
import { PostgreSQLDatabase } from './sql/postgres.js';
import { MongoDBDatabase } from './sql/mongodb.js';
import { buildFrontendRouter } from './routers/_frontend.js';
/**
@ -128,6 +129,9 @@ async function main() {
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
case 'mongodb':
await DBManager.use(new MongoDBDatabase());
break;
}
} catch (err) { throw new Error(`Failed to configure SQL`); }
} else { // default to json database

@ -15,6 +15,7 @@ type DataSector = 'files' | 'users';
const DBNAMES = {
'mysql': 'MySQL',
'postgres': 'PostgreSQL',
'mongodb': 'MongoDB',
'json': 'JSON'
};

@ -0,0 +1,32 @@
import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from 'ass'
import { TemplateExecutor } from './templates/executor.js';
let executor = TemplateExecutor.createExecutor();
export const DEFAULT_EMBED: EmbedTemplate = {
sitename: 'ass',
title: '',
description: ''
};
// ensures a template is valid
export const validateEmbed = (template: EmbedTemplate) => {
// lets hope this works
let context = executor.createContext(null!, null!);
executor.validateTemplate(template.title, context);
executor.validateTemplate(template.description, context);
executor.validateTemplate(template.sitename, context);
}
// cooks up the embed
export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => {
let context = executor.createContext(user, file);
return {
title: executor.executeTemplate(template.title, context),
description: executor.executeTemplate(template.description, context),
sitename: executor.executeTemplate(template.sitename, context)
};
};

@ -12,6 +12,7 @@ import { DBManager } from '../sql/database.js';
import { JSONDatabase } from '../sql/json.js';
import { MySQLDatabase } from '../sql/mysql.js';
import { PostgreSQLDatabase } from '../sql/postgres.js';
import { MongoDBDatabase } from '../sql/mongodb.js';
const router = Router({ caseSensitive: true });
@ -41,13 +42,16 @@ router.post('/setup', BodyParserJson(), async (req, res) => {
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
case 'mongodb':
await DBManager.use(new MongoDBDatabase());
break;
}
}
// set rate limits
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);;
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);
log.success('Setup', 'completed');
@ -62,10 +66,11 @@ router.post('/setup', BodyParserJson(), async (req, res) => {
router.post('/login', rateLimiterMiddleware('login', UserConfig.config?.rateLimit?.login), BodyParserJson(), (req, res) => {
const { username, password } = req.body;
// something tells me we shouldnt be using getall here
data.getAll('users')
.then((users) => {
if (!users) throw new Error('Missing users data');
else return Object.entries(users as AssUser[])
else return Object.entries(users as AssUser[])
.filter(([_uid, user]: [string, AssUser]) => user.username === username)[0][1]; // [0] is the first item in the filter results, [1] is AssUser
})
.then((user) => Promise.all([bcrypt.compare(password, user.password), user]))

@ -1,4 +1,4 @@
import { BusBoyFile, AssFile } from 'ass';
import { BusBoyFile, AssFile, AssUser } from 'ass';
import fs from 'fs-extra';
import bb from 'express-busboy';
@ -13,6 +13,8 @@ import { random } from '../generators.js';
import { UserConfig } from '../UserConfig.js';
import { getFileS3, uploadFileS3 } from '../s3.js';
import { rateLimiterMiddleware } from '../ratelimit.js';
import { DBManager } from '../sql/database.js';
import { DEFAULT_EMBED, prepareEmbed } from '../embed.js';
const router = Router({ caseSensitive: true });
@ -30,7 +32,7 @@ bb.extend(router, {
router.get('/', (req, res) => UserConfig.ready ? res.render('index', { version: App.pkgVersion }) : res.redirect('/setup'));
// Upload flow
router.post('/', rateLimiterMiddleware("upload", UserConfig.config?.rateLimit?.upload), async (req, res) => {
router.post('/', rateLimiterMiddleware('upload', UserConfig.config?.rateLimit?.upload), async (req, res) => {
// Check user config
if (!UserConfig.ready) return res.status(500).type('text').send('Configuration missing!');
@ -96,7 +98,47 @@ router.post('/', rateLimiterMiddleware("upload", UserConfig.config?.rateLimit?.u
}
});
router.get('/:fakeId', (req, res) => res.redirect(`/direct/${req.params.fakeId}`));
router.get('/:fakeId', async (req, res) => {
if (!UserConfig.ready) res.redirect('/setup');
// Get the ID
const fakeId = req.params.fakeId;
// Get the file metadata
let _data;
try { _data = await DBManager.get('assfiles', fakeId); }
catch (err) {
log.error('Failed to get', fakeId);
console.error(err);
return res.status(500).send();
}
if (!_data) return res.status(404).send();
else {
let meta = _data as AssFile;
let user = await DBManager.get('assusers', meta.uploader) as AssUser | undefined;
res.render("viewer", {
url: `/direct/${fakeId}`,
uploader: user?.username ?? 'unknown',
size: meta.size,
time: meta.timestamp,
embed: prepareEmbed({
title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title,
description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description,
sitename: UserConfig.config.embed?.sitename ?? DEFAULT_EMBED.sitename
}, user ?? {
admin: false,
files: [],
id: "",
meta: {},
password: "",
tokens: [],
username: "unknown"
}, meta)
});
}
});
router.get('/direct/:fakeId', async (req, res) => {
if (!UserConfig.ready) res.redirect('/setup');

@ -35,7 +35,7 @@ export class DBManager {
public static configure(): Promise<void> {
if (this._db && this._dbReady) {
return this._db.configure();
} else throw new Error("No database active");
} else throw new Error('No database active');
}
/**
@ -44,7 +44,7 @@ export class DBManager {
public static put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void> {
if (this._db && this._dbReady) {
return this._db.put(table, key, data);
} else throw new Error("No database active");
} else throw new Error('No database active');
}
/**
@ -53,7 +53,7 @@ export class DBManager {
public static get(table: DatabaseTable, key: NID): Promise<DatabaseValue> {
if (this._db && this._dbReady) {
return this._db.get(table, key);
} else throw new Error("No database active");
} else throw new Error('No database active');
}
/**
@ -62,6 +62,6 @@ export class DBManager {
public static getAll(table: DatabaseTable): Promise<DatabaseValue[]> {
if (this._db && this._dbReady) {
return this._db.getAll(table);
} else throw new Error("No database active");
} else throw new Error('No database active');
}
}

@ -0,0 +1,244 @@
import { AssFile, AssUser, MongoDBConfiguration, NID, UploadToken, Database, DatabaseTable, DatabaseValue } from 'ass';
import mongoose, { Model, Mongoose, Schema } from 'mongoose';
import { UserConfig } from '../UserConfig.js';
import { log } from '../log.js';
interface TableVersion {
name: string;
version: number;
}
const VERSIONS_SCHEMA = new Schema<TableVersion>({
name: String,
version: Number
});
interface MongoSchema<T> {
id: NID,
data: T
}
const FILE_SCHEMA = new Schema<MongoSchema<AssFile>>({
id: String,
data: {
fakeid: String,
fileKey: String,
filename: String,
mimetype: String,
save: {
local: String,
s3: Boolean // this will break if it gets the url object
// but im so fucking tired of this, were just
// going to keep it like this until it becomes
// a problem
},
sha256: String,
size: Number,
timestamp: String,
uploader: String
}
});
const TOKEN_SCHEMA = new Schema<MongoSchema<UploadToken>>({
id: String,
data: {
id: String,
token: String,
hint: String
}
});
const USER_SCHEMA = new Schema<MongoSchema<AssUser>>({
id: String,
data: {
id: String,
username: String,
password: String,
admin: Boolean,
tokens: [ String ],
files: [ String ],
meta: {
type: String,
get: (v: string) => JSON.parse(v),
set: (v: string) => JSON.stringify(v)
}
}
});
/**
* database adapter for mongodb
*/
export class MongoDBDatabase implements Database {
private _client: Mongoose;
// mongoose models
private _versionModel: Model<TableVersion>;
private _fileModel: Model<MongoSchema<AssFile>>;
private _tokenModel: Model<MongoSchema<UploadToken>>;
private _userModel: Model<MongoSchema<AssUser>>;
private _validateConfig(): string | undefined {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MongoDB configuration missing';
if (UserConfig.config.database.kind != 'mongodb') return 'Database not set to MongoDB, but MongoDB is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MongoDB configuration missing';
let config = UserConfig.config.database.options;
// check the postgres config
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(config.host) ? 'Missing MongoDB Host'
: !checker(config.user) ? 'Missing MongoDB User'
: !checker(config.password) ? 'Missing MongoDB Password'
: !checker(config.database) ? 'Missing MongoDB Database'
// ! Blame VS Code for this weird indentation
: undefined;
return issue;
}
open(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// validate config
let configError = this._validateConfig();
if (configError != null) throw new Error(configError);
let options = UserConfig.config.database!.options! as MongoDBConfiguration;
// connect
log.info('MongoDB', `connecting to ${options.host}:${options.port}`);
this._client = await mongoose.connect(`mongodb://${options.user}:${options.password}@${options.host}:${options.port}/${options.database}`);
log.success('MongoDB', 'ok');
resolve();
} catch (err) {
log.error('MongoDB', 'failed to connect');
console.error(err);
reject(err);
}
});
}
close(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// gracefully disconnect
await this._client.disconnect();
resolve();
} catch (err) {
log.error('MongoDB', 'failed to disconnect');
console.error(err);
reject(err);
}
});
}
configure(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
this._versionModel = this._client.model('assversions', VERSIONS_SCHEMA);
this._fileModel = this._client.model('assfiles', FILE_SCHEMA);
this._tokenModel = this._client.model('asstokens', TOKEN_SCHEMA);
this._userModel = this._client.model('assusers', USER_SCHEMA);
// theres only one version right now so we dont need to worry about anything, just adding the version thingies if they arent there
let versions = await this._versionModel.find().exec()
.then(res => res.reduce((obj, doc) => obj.set(doc.name, doc.version), new Map<string, number>()));
for (let [table, version] of [['assfiles', 1], ['asstokens', 1], ['assusers', 1]] as [string, number][]) {
if (!versions.has(table)) {
// set the version
new this._versionModel({
name: table,
version: version
}).save();
versions.set(table, version);
}
}
resolve();
} catch (err) {
log.error('MongoDB', 'failed to configure');
console.error(err);
reject(err);
}
});
}
put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};
await new models[table]({
id: key,
data: data
}).save();
resolve();
} catch (err) {
reject(err);
}
});
}
get(table: DatabaseTable, key: string): Promise<DatabaseValue> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};
// @ts-ignore
// typescript cant infer this but it is 100% correct
// no need to worry :>
let result = await models[table].find({
id: key
}).exec();
if (result.length == 0) {
throw new Error(`Key '${key}' not found in '${table}'`);
}
resolve(result.length ? result[0].data : void 0);
} catch (err) {
reject(err);
}
});
}
// TODO: Unsure if this works.
getAll(table: DatabaseTable): Promise<DatabaseValue[]> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};
// more ts-ignore!
// @ts-ignore
let result = await models[table].find({}).exec() // @ts-ignore
.then(res => res.reduce((obj, doc) => (obj.push(doc.data)), []));
resolve(result);
} catch (err) {
reject(err);
}
});
}
};

@ -31,7 +31,7 @@ export class MySQLDatabase implements Database {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MySQL configuration missing';
if (UserConfig.config.database.kind != "mysql") return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (UserConfig.config.database.kind != 'mysql') return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MySQL configuration missing';
let mySqlConf = UserConfig.config.database.options;

@ -17,7 +17,7 @@ export class PostgreSQLDatabase implements Database {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'PostgreSQL configuration missing';
if (UserConfig.config.database.kind != "postgres") return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong';
if (UserConfig.config.database.kind != 'postgres') return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'PostgreSQL configuration missing';
let config = UserConfig.config.database.options;
@ -33,7 +33,6 @@ export class PostgreSQLDatabase implements Database {
: undefined;
return issue;
}
public open(): Promise<void> {
@ -172,26 +171,30 @@ export class PostgreSQLDatabase implements Database {
let result = await this._client.query(queries[table], [key]);
resolve(result.rowCount ? result.rows[0].data : void 0);
if (result.rowCount == 0) {
throw new Error(`Key '${key}' not found in '${table}'`);
}
resolve(result.rows[0].data);
} catch (err) {
reject(err);
}
});
}
// todo: verify this works
// XXX: This is broken
public getAll(table: DatabaseTable): Promise<DatabaseValue[]> {
return new Promise(async (resolve, reject) => {
try {
const queries = {
assfiles: 'SELECT json_object_agg(id, data) AS stuff FROM assfiles;',
assusers: 'SELECT json_object_agg(id, data) AS stuff FROM assusers;',
asstokens: 'SELECT json_object_agg(id, data) AS stuff FROM asstokens;'
assfiles: 'SELECT json_agg(data) as stuff FROM assfiles;',
assusers: 'SELECT json_agg(data) as stuff FROM assusers;',
asstokens: 'SELECT json_agg(data) as stuff FROM asstokens;'
};
let result = await this._client.query(queries[table]);
resolve(result.rowCount ? result.rows[0].stuff : void 0);
resolve(result.rowCount ? result.rows[0].stuff : []);
} catch (err) {
reject(err);
}

@ -0,0 +1,10 @@
import { TemplateCommandOp, TemplateCommandSchema } from 'ass';
import { TemplateContext } from './executor.js';
export type TemplateCommand<N extends string, S extends TemplateCommandSchema> = {
readonly name: N;
readonly schema: S;
exec: (op: TemplateCommandOp<N, S>, ctx: TemplateContext) => string;
};

@ -0,0 +1,81 @@
import { TemplateSourceRange } from 'ass';
export class TemplateError extends Error {
range?: TemplateSourceRange;
constructor(msg: string, range?: TemplateSourceRange) {
super(msg);
this.range = range;
}
/**
* Formats the error.
*/
public format(): string {
let format = '';
if (this.range) {
let fcol = 0;
let fline = 1;
let pstart = 0;
for (let i = 0; i < this.range.from; i++) {
fcol++;
if (this.range.file.code[i] == '\n') {
fline++;
fcol = 0;
pstart = i + 1;
}
}
let tcol = fcol;
let tline = fline;
let pend = pstart;
for (let i = this.range.from; i < this.range.to; i++) {
tcol++;
if (this.range.file.code[i] == '\n') {
tline++;
tcol = 0;
pend = i + 1;
}
}
if ((pend = this.range.file.code.indexOf('\n', pend)) == -1) {
pend = this.range.file.code.length - 1;
}
if (fline == tline) {
format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend + 1)}\n`;
format += `${fline.toString().padStart(5, ' ')} | ${' '.repeat(fcol)}^${'~'.repeat(Math.max(tcol - fcol, 0))}\n`;
} else {
let lines = this.range.file.code.substring(pstart, pend + 1).split('\n');
format += ` | /${'~'.repeat(lines[0].length)}v\n`;
for (let i = fline; i < fline + 5 && i <= tline; i++) {
format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`;
}
if (fline + 5 < tline) {
format += ` | | ...\n`;
for (let i = tline - 4; i <= tline; i++) {
format += `${i.toString().padStart(5, ' ')} | | ${lines[i - fline]}\n`;
}
}
format += ` | \\${'~'.repeat(tcol + 1)}^\n`;
}
}
format += `${this.name}: ${this.message}`;
return format;
}
}
// template syntax error with token range, token range converted to source position
// outside of prepareTemplate
export class TemplateSyntaxError extends TemplateError {};

@ -0,0 +1,122 @@
import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass';
import { TemplateCommand } from './command.js';
import { TemplateError } from './error.js';
export class TemplateContext {
public readonly owner: TemplateExecutor;
public uploader: AssUser;
public file: AssFile;
constructor(owner: TemplateExecutor, uploader: AssUser, file: AssFile) {
this.owner = owner;
this.uploader = uploader;
this.file = file;
}
}
export class TemplateExecutor {
private commands: { [index: string]: TemplateCommand<any, any> } = {};
// register a template command globally
public registerCommand<N extends string, S extends TemplateCommandSchema>(name: N, attrs: S, cmd: (op: TemplateCommandOp<N, S>, ctx: TemplateContext) => string) {
if (this.commands[name] == null) {
this.commands[name] = {
name: name,
schema: attrs,
exec: cmd
};
} else throw new Error(`Template command "${name}" already exists`);
}
public createContext(uploader: AssUser, file: AssFile) {
return new TemplateContext(this, uploader, file);
}
// expects template to be valid and does not preform runtime checks.
// run validateTemplate first
public executeTemplate(op: TemplateOp, ctx: TemplateContext): string {
switch (typeof op) {
case 'string': return op;
case 'object': return this.commands[op.op].exec(op, ctx);
}
}
public validateTemplate(op: TemplateOp, ctx: TemplateContext): void {
if (typeof op == 'string') return;
if (this.commands[op.op] != null) {
let cmd = this.commands[op.op].schema as TemplateCommandSchema;
if (cmd.named) {
for (let name in cmd.named) {
let arg = cmd.named[name];
// @ts-ignore
if (arg.required && op.named[name] == null) {
throw new TemplateError(`Required template argument "${name}" is missing.`, op.srcRange);
}
}
}
for (let arg of op.args) this.validateTemplate(arg, ctx);
for (let name in op.named) {
if (!cmd.named || cmd.named[name] == null) {
let arg = (op.named as {[index:string]:TemplateOp})[name] as TemplateOp | undefined;
// @ts-ignore
throw new TemplateError(`Unknown template argument "${name}".`, {
file: op.srcRange.file,
from: (typeof arg == 'object' && arg.srcRange.from - 1 - name.length) || op.srcRange.from,
to: (typeof arg == 'object' && arg.srcRange.to) || op.srcRange.to
});
}
// @ts-ignore
this.validateTemplate(op.named[name]!, ctx);
}
} else throw new TemplateError(`Template command "${op.op}" does not exist.`, op.srcRange);
}
// creates an executor with the default commands.
public static createExecutor(): TemplateExecutor {
let ex = new TemplateExecutor();
// joins two strings
ex.registerCommand('concat', {}, (op, ctx) => {
return op.args.reduce((a: string, b): string => a + ctx.owner.executeTemplate(b, ctx), "");
});
// converts a number to a file size
ex.registerCommand('formatbytes', {
named: { unit: {} }
}, (op, ctx) => {
let value = ctx.owner.executeTemplate(op.args[0], ctx);
let exponent = (op.named.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[ctx.owner.executeTemplate(op.named.unit, ctx)])
|| Math.max(Math.min(Math.floor(Math.log10(Number(value))/3), 4), 0);
return `${(Number(value) / 1000 ** exponent).toFixed(2)}${['b', 'kb', 'mb', 'gb', 'tb'][exponent]}`;
});
// gets the size of the active file
ex.registerCommand('filesize', {}, (op, ctx) => {
return ctx.file.size.toString();
});
// gets the uploader of the active file
ex.registerCommand('uploader', {}, (op, ctx) => {
return ctx.uploader.username;
});
// selects a random argument
ex.registerCommand('random', {}, (op, ctx) => {
if (op.args.length > 0) {
return ctx.owner.executeTemplate(op.args[Math.round(Math.random() * (op.args.length - 1))], ctx);
} else throw new TemplateError('Random without arguments');
});
return ex;
}
}

@ -0,0 +1,305 @@
import { TemplateOp, TemplateSource } from 'ass';
import fs from 'fs';
import { TemplateSyntaxError } from './error.js';
enum TokenType {
T_OPEN, T_CLOSE,
PIPE, EQUALS,
TEXT,
};
type TemplateToken = {
type : TokenType;
data?: string;
from: number;
to: number;
};
// tree used by findReplacement to select the best amp-substitution
type TemplateAmpNode = { [index: string]: TemplateAmpNode | string | undefined; }
const TEMPLATE_AMP_SUBSTITUTIONS: TemplateAmpNode = {
e: { q: { $: '=' } },
p: { i: { p: { e: { $: '|' } } } },
t: {
c: { l: { o: { s: { e: { $: '}}' } } } } },
o: { p: { e: { n: { $: '{{' } } } }
}
};
function getTemplateTokens(src: string): TemplateToken[] {
let tokens: TemplateToken[] = [];
let buf : string = '';
let pos : number = 0;
// digs through TEMPLATE_AMP_SUBSTITUTIONS to find
// longest possible string to replace
function findReplacement() {
let raw = "";
let bestpos: number | null = null;
let best: string | null = null;
let node = TEMPLATE_AMP_SUBSTITUTIONS;
while (true) {
if (pos >= src.length) break;
if (!/[a-z]/.test(src[pos])) break;
if (node[src[pos]] != null) { // enter the thing
node = node[src[pos]] as TemplateAmpNode;
} else {
break;
}
if (node.$ != null) {
best = node.$ as string;
bestpos = pos;
}
raw += src[pos++];
}
if (best != null) {
pos = bestpos! + 1;
return best;
}
return `&${raw}`;
}
for (; pos < src.length; pos++) {
let lp = pos;
if (pos + 1 < src.length && src[pos] == '{' && src[pos + 1] == '{') {
tokens.push({
type: TokenType.T_OPEN,
from: pos,
to: pos + 1
});
pos++;
} else if (pos + 1 < src.length && src[pos] == '}' && src[pos + 1] == '}') {
tokens.push({
type: TokenType.T_CLOSE,
from: pos,
to: pos + 1
});
pos++;
} else if (src[pos] == '|') {
tokens.push({
type: TokenType.PIPE,
from: pos,
to: pos
});
} else if (src[pos] == '=') {
tokens.push({
type: TokenType.EQUALS,
from: pos,
to: pos
});
} else if (src[pos] == '&') {
pos++;
buf += findReplacement();
pos--; continue;
} else if (src[pos] == '\n') {
pos++;
for (; pos < src.length && (src[pos] == ' ' || src[pos] == '\t'); pos++);
pos--; continue;
} else {
buf += src[pos];
continue;
}
if (buf.length) {
tokens.splice(-1, 0, {
type: TokenType.TEXT,
data: buf,
from: lp - buf.length,
to: lp - 1
});
buf = '';
}
}
if (buf.length) tokens.push({
type: TokenType.TEXT,
data: buf,
from: src.length - buf.length,
to: src.length
});
return tokens;
}
export type PrepareTemplateOptions = {
allowIncludeFile?: boolean;
};
export function prepareTemplate(src: string, config?: PrepareTemplateOptions): TemplateOp {
let options = {
includeFiles: config?.allowIncludeFile ?? false
};
type ParserStackEntry = {
pos: number
};
let tokens = getTemplateTokens(src);
let pos = 0;
let stack: ParserStackEntry[] = [];
function stackPush() {
stack.push({ pos: pos });
}
function stackDrop() {
stack.pop();
}
let file: TemplateSource = { code: src };
// parse the text part of stuff. like uh
// you know uh like uh this part
// V---V V V-V V
// Hello {Concat|W|ORL|D}
function parseConcat(root: boolean = false): TemplateOp {
let joined: TemplateOp[] = [];
let start = pos;
stackPush();
out: while (pos < tokens.length) {
switch (tokens[pos].type) {
case TokenType.TEXT:
joined.push(tokens[pos++].data!);
continue out;
case TokenType.EQUALS:
if (root == true) throw new TemplateSyntaxError('Unexpected equals', { file: file, from: tokens[pos].from, to: tokens[pos].to });
case TokenType.PIPE:
if (root == true) throw new TemplateSyntaxError('Unexpected pipe', { file: file, from: tokens[pos].from, to: tokens[pos].to });
case TokenType.T_CLOSE:
if (root == true) throw new TemplateSyntaxError('Unexpected closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to });
break out;
case TokenType.T_OPEN:
joined.push(parseTemplate());
}
}
stackDrop();
return joined.length == 1 ? joined[0] : {
op: "concat",
named: {},
args: joined,
srcRange: {
file: file,
from: tokens[start]?.from ?? 0,
to: tokens[pos - 1]?.to ?? src.length - 1
}
};
}
// parse templates
function parseTemplate(): TemplateOp {
let name: string;
let args: TemplateOp[] = [];
let nargs: {[index: string]: TemplateOp} = {};
let start = pos;
stackPush();
if (pos < tokens.length && tokens[pos].type == TokenType.T_OPEN) {
pos++;
} else throw new Error('Catastrophic failure');
if (pos < tokens.length && tokens[pos].type == TokenType.TEXT) {
name = tokens[pos++].data!;
} else if (pos < tokens.length) {
if (tokens[pos].type == TokenType.T_CLOSE) {
throw new TemplateSyntaxError('Template name missing', { file: file, from: tokens[pos - 1].from, to: tokens[pos].to });
} else throw new TemplateSyntaxError('Expected template name', { file: file, from: tokens[pos].from, to: tokens[pos].to });
} else throw new TemplateSyntaxError('Unexpected end of file');
if (pos < tokens.length && tokens[pos].type == TokenType.PIPE) {
pos++;
out: while (pos < tokens.length) {
let argStart = pos;
let arg = parseConcat();
// this is some really nasty control flow im so sorry
if (pos < tokens.length) {
switch (tokens[pos].type) {
case TokenType.EQUALS: // named arguments
if (typeof arg != 'string') {
throw new TemplateSyntaxError('Argument name must be a plain string', { file: file, from: tokens[argStart].from, to: tokens[pos - 1].to });
}
pos++;
if (pos < tokens.length) {
let arg2 = parseConcat();
nargs[arg] = arg2;
if (pos < tokens.length) {
switch (tokens[pos].type) {
case TokenType.T_CLOSE: break out;
case TokenType.PIPE: pos++;
}
} else throw new TemplateSyntaxError('syntax error');
} else throw new TemplateSyntaxError('syntax error');
break;
case TokenType.T_CLOSE:
args.push(arg);
break out;
case TokenType.PIPE:
args.push(arg);
pos++;
}
}
}
} else if (pos < tokens.length && tokens[pos].type != TokenType.T_CLOSE) {
throw new TemplateSyntaxError('Expected arguments or closing tag', { file: file, from: tokens[pos].from, to: tokens[pos].to });
}
if (pos < tokens.length && tokens[pos].type == TokenType.T_CLOSE) {
pos++;
} else throw new TemplateSyntaxError('Template closing tag missing');
stackDrop();
// include is executed early
if (name.toLowerCase() == 'include') {
if (nargs['file'] != null) {
// security check!
if (!options.includeFiles) {
throw new TemplateSyntaxError('You are not allowed to include files', { file: file, from: tokens[start].from, to: tokens[pos - 1].to });
}
if (typeof nargs['file'] == 'string') {
if (fs.existsSync(nargs['file'])) {
let template = fs.readFileSync(nargs['file'], { encoding: 'utf-8' });
let tl = prepareTemplate(template, config);
return tl;
} else throw new TemplateSyntaxError('File does not exist', { file: file, from: tokens[start].from, to: tokens[pos - 1].to});
} else throw new TemplateSyntaxError('Include directive can not contain templates', { file: file, from: tokens[start].from, to: tokens[pos - 1].to});
} else throw new TemplateSyntaxError(`Bad include directive`, { file: file, from: tokens[start].from, to: tokens[pos - 1].to});
}
return {
op: name.toLocaleLowerCase(),
named: nargs,
args: args,
srcRange: {
file: file,
from: tokens[start]?.from ?? 0,
to: tokens[pos - 1]?.to ?? src.length - 1
}
};
}
let result = parseConcat(true);
return result;
}

105
common/types.d.ts vendored

@ -30,6 +30,25 @@ declare module 'ass' {
database?: DatabaseConfiguration;
rateLimit?: RateLimitConfiguration;
// to whoever has to make the config screen
// for this, im very verys sorry
embed?: EmbedTemplate;
}
/**
* Embed config
*/
interface EmbedConfiguration {
/**
* Title in embed
*/
title?: string,
/**
* Description(s) in embed
*/
description?: string[] | string,
}
interface S3Configuration {
@ -91,8 +110,8 @@ declare module 'ass' {
}
interface DatabaseConfiguration {
kind: 'mysql' | 'postgres' | 'json';
options?: MySQLConfiguration | PostgresConfiguration;
kind: 'mysql' | 'postgres' | 'json' | 'mongodb';
options?: MySQLConfiguration | PostgresConfiguration | MongoDBConfiguration;
}
interface MySQLConfiguration {
@ -111,6 +130,14 @@ declare module 'ass' {
database: string;
}
interface MongoDBConfiguration {
host: string;
port: number;
user: string;
password: string;
database: string;
}
/**
* rate limiter configuration
* @since 0.15.0
@ -171,9 +198,6 @@ declare module 'ass' {
password: (val: any) => boolean;
database: (val: any) => boolean;
}
postgres: {
port: (val: any) => boolean;
}
}
rateLimit: {
endpoint: (val: any) => boolean;
@ -288,6 +312,77 @@ declare module 'ass' {
cliKey: string;
meta: { [key: string]: any };
}
/**
* Template operation
*/
type TemplateOp = TemplateCommandOp<any, TemplateCommandSchema> | string;
/**
* Please don't waste your time trying to make this look
* nice, it's not possible.
*/
type TemplateCommandOp<N extends string, T extends TemplateCommandSchema> = {
op: N;
args: TemplateOp[];
named: {
+readonly [name in keyof T['named']]: (
TemplateOp | (T['named'] extends object
? T['named'][name] extends { required?: boolean }
? T['named'][name]['required'] extends true
? TemplateOp
: undefined
: undefined
: undefined)
)
};
srcRange: TemplateSourceRange;
};
/**
* Basically a declaration
*/
type TemplateCommandSchema = {
named?: {
[index: string]: {
required?: boolean
}
};
};
/**
* Template source code
*/
type TemplateSource = {
code: string;
};
/**
* Range in template source code
*/
type TemplateSourceRange = {
file: TemplateSource;
from: number;
to: number;
};
/**
* This is so beyond cursed
*/
interface EmbedTemplate {
title: TemplateOp;
description: TemplateOp;
sitename: TemplateOp;
}
/**
*
*/
interface PreparedEmbed {
title: string;
description: string;
sitename: string;
}
}
//#region Dummy modules

@ -57,6 +57,13 @@ document.addEventListener('DOMContentLoaded', () => {
pgsqlPassword: document.querySelector('#pgsql-password') as SlInput,
pgsqlDatabase: document.querySelector('#pgsql-database') as SlInput,
mongoDBTab: document.querySelector('#mongodb-tab') as SlTab,
mongoDBHost: document.querySelector('#mongodb-host') as SlInput,
mongoDBPort: document.querySelector('#mongodb-port') as SlInput,
mongoDBUser: document.querySelector('#mongodb-user') as SlInput,
mongoDBPassword: document.querySelector('#mongodb-password') as SlInput,
mongoDBDatabase: document.querySelector('#mongodb-database') as SlInput,
userUsername: document.querySelector('#user-username') as SlInput,
userPassword: document.querySelector('#user-password') as SlInput,
@ -130,6 +137,19 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
}
} else if (Elements.mongoDBTab.active) {
if (Elements.mongoDBHost.value != null && Elements.mongoDBHost.value != '') {
config.database = {
kind: 'mongodb',
options: {
host: Elements.mongoDBHost.value,
port: parseInt(Elements.mongoDBPort.value),
user: Elements.mongoDBUser.value,
password: Elements.mongoDBPassword.value,
database: Elements.mongoDBDatabase.value
}
};
}
}
// append rate limit config, if specified

@ -53,6 +53,7 @@
"fs-extra": "^11.2.0",
"luxon": "^3.4.4",
"memorystore": "^1.6.7",
"mongoose": "^8.0.0",
"mysql2": "^3.6.5",
"node-vibrant": "^3.1.6",
"pg": "^8.11.3",

@ -71,6 +71,9 @@ dependencies:
memorystore:
specifier: ^1.6.7
version: 1.6.7
mongoose:
specifier: ^8.0.0
version: 8.0.0
mysql2:
specifier: ^3.6.5
version: 3.6.5
@ -1862,6 +1865,12 @@ packages:
- supports-color
dev: false
/@mongodb-js/saslprep@1.1.1:
resolution: {integrity: sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==}
dependencies:
sparse-bitfield: 3.0.3
dev: false
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -2898,6 +2907,17 @@ packages:
- vue
dev: true
/@types/webidl-conversions@7.0.2:
resolution: {integrity: sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==}
dev: false
/@types/whatwg-url@8.2.2:
resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==}
dependencies:
'@types/node': 20.8.9
'@types/webidl-conversions': 7.0.2
dev: false
/@xoi/gps-metadata-remover@1.1.2(@babel/core@7.23.5):
resolution: {integrity: sha512-QeGcEvlesS+cXwfao14kdLI2zHJk3vppKSEbpbiNP1abx45P8HWqGEWhgF71bKlnCSW8a7b4RNDNa4mj1aHPMA==}
dependencies:
@ -3195,6 +3215,11 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: false
/bson@6.2.0:
resolution: {integrity: sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==}
engines: {node: '>=16.20.1'}
dev: false
/buffer-equal@0.0.1:
resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==}
engines: {node: '>=0.4.0'}
@ -4557,6 +4582,11 @@ packages:
promise: 7.3.1
dev: false
/kareem@2.5.1:
resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==}
engines: {node: '>=12.0.0'}
dev: false
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@ -4707,6 +4737,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
dev: false
/memorystore@1.6.7:
resolution: {integrity: sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==}
engines: {node: '>=0.10'}
@ -4875,11 +4909,86 @@ packages:
hasBin: true
dev: false
/mongodb-connection-string-url@2.6.0:
resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==}
dependencies:
'@types/whatwg-url': 8.2.2
whatwg-url: 11.0.0
dev: false
/mongodb@6.2.0:
resolution: {integrity: sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.188.0
'@mongodb-js/zstd': ^1.1.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.2.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
dependencies:
'@mongodb-js/saslprep': 1.1.1
bson: 6.2.0
mongodb-connection-string-url: 2.6.0
dev: false
/mongoose@8.0.0:
resolution: {integrity: sha512-PzwkLgm1Jhj0NQdgGfnFsu0QP9V1sBFgbavEgh/IPAUzKAagzvEhuaBuAQOQGjczVWnpIU9tBqyd02cOTgsPlA==}
engines: {node: '>=16.20.1'}
dependencies:
bson: 6.2.0
kareem: 2.5.1
mongodb: 6.2.0
mpath: 0.9.0
mquery: 5.0.0
ms: 2.1.3
sift: 16.0.1
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- gcp-metadata
- kerberos
- mongodb-client-encryption
- snappy
- socks
- supports-color
dev: false
/mpath@0.9.0:
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
engines: {node: '>=4.0.0'}
dev: false
/mquery@5.0.0:
resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==}
engines: {node: '>=14.0.0'}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: false
/mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'}
dev: true
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
@ -5805,6 +5914,11 @@ packages:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
dev: false
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
dev: false
/qr-creator@1.0.0:
resolution: {integrity: sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==}
dev: false
@ -6124,6 +6238,10 @@ packages:
object-inspect: 1.13.1
dev: false
/sift@16.0.1:
resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==}
dev: false
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: false
@ -6171,6 +6289,12 @@ packages:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
dev: true
/sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
dependencies:
memory-pager: 1.5.0
dev: false
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
@ -6453,10 +6577,13 @@ packages:
ieee754: 1.2.1
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
/tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
dependencies:
punycode: 2.3.1
dev: false
/trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
dev: true
@ -6772,6 +6899,19 @@ packages:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
dev: false
/whatwg-url@11.0.0:
resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
engines: {node: '>=12'}
dependencies:
tr46: 3.0.0
webidl-conversions: 7.0.0
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:

@ -24,6 +24,15 @@
.setup-panel>sl-input {
@apply mb-4;
}
.res-image {
@apply max-h-[75vh];
}
/* THANKS TAILWIND */
sl-divider {
border: solid var(--width) var(--color);
}
}
@layer utilities {

@ -67,6 +67,20 @@ block content
h3.setup-text-item-title Database
sl-input#pgsql-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa')
//- * MongoDB
sl-tab#mongodb-tab(slot='nav' panel='mongodb') MongoDB
sl-tab-panel(name='mongodb')
h3.setup-text-item-title Host
sl-input#mongodb-host(type='text' placeholder='mongo.example.com' clearable): sl-icon(slot='prefix' name='fas-server' library='fa')
h3.setup-text-item-title Port
sl-input#mongodb-port(type='number' placeholder='27017' min='1' max='65535' no-spin-buttons clearable): sl-icon(slot='prefix' name='fas-number' library='fa')
h3.setup-text-item-title User
sl-input#mongodb-user(type='text' placeholder='mongo' clearable): sl-icon(slot='prefix' name='fas-user' library='fa')
h3.setup-text-item-title Password
sl-input#mongodb-password(type='password' placeholder='super-secure' clearable): sl-icon(slot='prefix' name='fas-lock' library='fa')
h3.setup-text-item-title Database
sl-input#mongodb-database(type='text' placeholder='assdb' clearable): sl-icon(slot='prefix' name='fas-database' library='fa')
//- * S3
h2.setup-text-section-header.mt-4 S3 #[span.setup-text-optional optional]
.setup-panel

@ -0,0 +1,60 @@
doctype html
html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns')
head
//- this stuff
meta(charset='UTF-8')
meta(name='viewport', content='width=device-witdh, initial-scale=1.0')
meta(name='theme-color' content='black')
link(rel='stylesheet' href='/.css')
//- title
title ass 🍑
//- embed data
meta(property='og:title', content=embed.title)
meta(property='og:description', content=embed.description)
meta(property='og:site_name', content=embed.sitename)
meta(property='og:type', content='image')
meta(property='og:image', content=url)
meta(property='og:url', content='.')
meta(property='twitter:card', content='summary_large_image')
//- mixins
include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug
include ../node_modules/shoelace-pug-loader/loader.pug
//- shoelace
+slTheme('dark')
+slAuto
body.w-screen.h-screen.flex-col
div.w-full.h-full.flex.justify-center.items-center.text-center
main.flex.flex-col
- let borderStyle = { 'border-color': 'var(--sl-color-neutral-200)' };
header.border-t.border-x.flex.h-8.bg-stone-900(style=borderStyle)
- let dividerStyle = { '--spacing': '0px', 'margin-left': '8px' };
//- uploader
span
sl-icon.p-2.align-middle(name='person-fill', label='uploader')
| #{uploader}
sl-divider(vertical, style=dividerStyle)
//- file size
span
sl-icon.p-2.align-middle(name='database-fill', label='size')
sl-format-bytes(value=size)
sl-divider(vertical, style=dividerStyle)
//- upload date
span
//- calendar is a funny word
sl-icon.p-2.align-middle(name='calendar-fill', label='upload date')
sl-format-date(value=time)
sl-divider(vertical, style=dividerStyle)
//- spacer
div.flex-grow
//- download button
sl-divider(vertical, style=dividerStyle)
span.float-right
sl-icon-button(name='download', href=url, download, label='download')
img.res-image.border-b.border-x(src=url, style=borderStyle)
Loading…
Cancel
Save