pull/249/head
xwashere 6 months ago
parent db32bb1c29
commit 3660b6cc8b
No known key found for this signature in database
GPG Key ID: 042F8BFA1B0EF93B

@ -3,6 +3,9 @@ import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoD
import fs from 'fs-extra';
import { path } from '@tycrek/joint';
import { log } from './log';
import { prepareTemplate } from './templates/parser';
import { TemplateError } from './templates/error';
import { DEFAULT_EMBED, validateEmbed } from './embed';
const FILEPATH = path.join('.ass-data/userconfig.json');
@ -130,6 +133,28 @@ 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);
} 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;
}

@ -1,4 +1,7 @@
import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass"
import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from "ass"
import { TemplateExecutor } from "./templates/executor";
let executor = TemplateExecutor.createExecutor();
export const DEFAULT_EMBED: EmbedTemplate = {
sitename: "ass",
@ -6,52 +9,23 @@ export const DEFAULT_EMBED: EmbedTemplate = {
description: ""
};
class EmbedContext {
public uploader: AssUser;
public file: AssFile;
// ensures a template is valid
export const validateEmbed = (template: EmbedTemplate) => {
// lets hope this works
let context = executor.createContext(null!, null!);
constructor(uploader: AssUser, file: AssFile) {
this.uploader = uploader;
this.file = file;
}
executor.validateTemplate(template.title, context);
executor.validateTemplate(template.description, context);
executor.validateTemplate(template.sitename, context);
}
const executeEmbedOperation = (op: EmbedTemplateOperation, ctx: EmbedContext): string | number => {
if (typeof op == 'string') {
return op;
} else if (typeof op == 'number') {
return op;
} else if (typeof op == 'object') {
switch (op.op) {
case 'random':
if (op.values.length > 0) {
return executeEmbedOperation(op.values[Math.round(Math.random() * (op.values.length - 1))], ctx);
} else throw new Error('Random without child operations');
case 'fileSize':
return ctx.file.size;
case 'uploader':
return ctx.uploader.username;
case 'formatBytes':
// calculate the value
let value = executeEmbedOperation(op.value, ctx);
// calculate the exponent
let exponent = (op.unit != null && { 'b': 0, 'kb': 1, 'mb': 2, 'gb': 3, 'tb': 4 }[executeEmbedOperation(op.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]}`;
case 'concat':
return op.values.reduce<string>((prev, op) => prev.concat(executeEmbedOperation(op, ctx).toString()), "");
}
} else throw new Error("Invalid embed template operation");
};
// cooks up the embed
export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => {
let ctx = new EmbedContext(user, file);
let context = executor.createContext(user, file);
return {
title: executeEmbedOperation(template.title, ctx).toString(),
description: executeEmbedOperation(template.description, ctx).toString(),
sitename: executeEmbedOperation(template.sitename, ctx).toString()
title: executor.executeTemplate(template.title, context),
description: executor.executeTemplate(template.description, context),
sitename: executor.executeTemplate(template.sitename, context)
};
};

@ -0,0 +1,9 @@
import { TemplateCommandOp, TemplateCommandSchema } from 'ass';
import { TemplateContext } from './executor';
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,28 @@
import { TemplateSourceRange } from "ass";
export class TemplateError extends Error {
range?: TemplateSourceRange;
constructor(msg: string, range?: TemplateSourceRange) {
super(msg);
this.range = range;
}
public format(): string {
let format = '';
if (this.range) {
format += this.range.file.code + '\n';
format += ' '.repeat(this.range.from) + '^' + '~'.repeat(Math.max(this.range.to - this.range.from, 0)) + '\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,121 @@
import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass';
import { TemplateCommand } from './command';
import { TemplateError } from './error';
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,270 @@
import { TemplateOp, TemplateSource } from 'ass';
import { TemplateSyntaxError } from './error';
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 {
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 function prepareTemplate(src: string): TemplateOp {
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,
to: tokens[pos - 1].to
}
};
}
// 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();
return {
op: name.toLocaleLowerCase(),
named: nargs,
args: args,
srcRange: {
file: file,
from: tokens[start].from,
to: tokens[pos - 1].to
}
};
}
let result = parseConcat(true);
return result;
}

72
common/types.d.ts vendored

@ -275,60 +275,66 @@ declare module 'ass' {
meta: { [key: string]: any };
}
// Generic embed template operation
type EmbedTemplateOperation =
EmbedTemplateRandomOperation |
EmbedTemplateFileSizeOperation |
EmbedTemplateFormatBytesOperation |
EmbedTemplateUploaderOperation |
EmbedTemplateConcatOperation |
string | number;
/**
* Selects one operation and executes it
* Template operation
*/
type EmbedTemplateRandomOperation = {
op: 'random';
values: EmbedTemplateOperation[];
};
type TemplateOp = TemplateCommandOp<any, TemplateCommandSchema> | string;
/**
* Returns the file size in bytes
* @returns number
* Please don't waste your time trying to make this look
* nice, it's not possible.
*/
type EmbedTemplateFileSizeOperation = { op: 'fileSize' };
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;
};
/**
* Formats the file size in in {value}
* Basically a declaration
*/
type EmbedTemplateFormatBytesOperation = {
op: 'formatBytes';
unit?: EmbedTemplateOperation; // ready for ios!
value: EmbedTemplateOperation;
type TemplateCommandSchema = {
named?: {
[index: string]: {
required?: boolean
}
};
};
/**
* Returns the user who uploaded this file
* Template source code
*/
type EmbedTemplateUploaderOperation = {
op: 'uploader'
type TemplateSource = {
code: string;
};
/**
* Joins strings
* Range in template source code
*/
type EmbedTemplateConcatOperation = {
op: 'concat',
values: EmbedTemplateOperation[]
}
type TemplateSourceRange = {
file: TemplateSource;
from: number;
to: number;
};
/**
* This is so beyond cursed
*/
interface EmbedTemplate {
title: EmbedTemplateOperation;
description: EmbedTemplateOperation;
sitename: EmbedTemplateOperation;
title: TemplateOp;
description: TemplateOp;
sitename: TemplateOp;
}
/**

Loading…
Cancel
Save