From a8f1d550785f736c377451980db18b5c25d19add Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 6 Nov 2023 12:56:21 -0500 Subject: [PATCH 01/20] mongoo --- backend/UserConfig.ts | 10 +- backend/app.ts | 4 + backend/data.ts | 1 + backend/routers/api.ts | 4 + backend/sql/mongodb.ts | 238 ++++++++++++++++++++++++++++++++++++++++ backend/sql/postgres.ts | 3 +- common/types.d.ts | 12 +- frontend/setup.mts | 22 +++- package.json | 1 + pnpm-lock.yaml | 144 ++++++++++++++++++++++++ views/setup.pug | 14 +++ 11 files changed, 443 insertions(+), 10 deletions(-) create mode 100644 backend/sql/mongodb.ts diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index 735088c..a6b90ef 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -1,4 +1,4 @@ -import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass'; +import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoDBConfiguration } from 'ass'; import fs from 'fs-extra'; import { path } from '@tycrek/joint'; @@ -108,15 +108,15 @@ 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 (config.database.kind == 'postgres') { - if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration).port)) { - throw new Error("Invalid database port"); + if (config.database.kind == 'postgres' || config.database.kind == 'mongodb') { + if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration | MongoDBConfiguration).port)) { + throw new Error('Invalid database port'); } } } else throw new Error('Database options missing'); diff --git a/backend/app.ts b/backend/app.ts index 85165c8..6f3ac5d 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -16,6 +16,7 @@ import { MySQLDatabase } from './sql/mysql'; import { buildFrontendRouter } from './routers/_frontend'; import { DBManager } from './sql/database'; import { PostgreSQLDatabase } from './sql/postgres'; +import { MongoDBDatabase } from './sql/mongodb'; /** * Top-level metadata exports @@ -127,6 +128,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 diff --git a/backend/data.ts b/backend/data.ts index 72e2f8c..a4c0603 100644 --- a/backend/data.ts +++ b/backend/data.ts @@ -26,6 +26,7 @@ const PATHS = { const DBNAMES = { 'mysql': 'MySQL', 'postgres': 'PostgreSQL', + 'mongodb': 'MongoDB', 'json': 'JSON' }; diff --git a/backend/routers/api.ts b/backend/routers/api.ts index e2aa2bc..43b5023 100644 --- a/backend/routers/api.ts +++ b/backend/routers/api.ts @@ -12,6 +12,7 @@ import { DBManager } from '../sql/database'; import { JSONDatabase } from '../sql/json'; import { MySQLDatabase } from '../sql/mysql'; import { PostgreSQLDatabase } from '../sql/postgres'; +import { MongoDBDatabase } from '../sql/mongodb'; const router = Router({ caseSensitive: true }); @@ -41,6 +42,9 @@ router.post('/setup', BodyParserJson(), async (req, res) => { case 'postgres': await DBManager.use(new PostgreSQLDatabase()); break; + case 'mongodb': + await DBManager.use(new MongoDBDatabase()); + break; } } diff --git a/backend/sql/mongodb.ts b/backend/sql/mongodb.ts new file mode 100644 index 0000000..ca8c258 --- /dev/null +++ b/backend/sql/mongodb.ts @@ -0,0 +1,238 @@ +import { AssFile, AssUser, MongoDBConfiguration, NID, UploadToken } from 'ass'; +import { UserConfig } from '../UserConfig'; +import { Database, DatabaseTable, DatabaseValue } from './database'; +import mongoose, { Model, Mongoose, Schema } from 'mongoose'; +import { log } from '../log'; + +interface TableVersion { + name: string; + version: number; +} + +const VERSIONS_SCHEMA = new Schema({ + name: String, + version: Number +}); + +interface MongoSchema { + id: NID, + data: T +} + +const FILE_SCHEMA = new Schema>({ + 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>({ + id: String, + data: { + id: String, + token: String, + hint: String + } +}); + +const USER_SCHEMA = new Schema>({ + 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; + private _fileModel: Model>; + private _tokenModel: Model>; + private _userModel: Model>; + + 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 { + 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 { + 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 { + 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())); + + 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 { + 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 { + 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(); + + resolve(result.length ? result[0].data : void 0); + } catch (err) { + reject(err); + } + }); + } + + getAll(table: DatabaseTable): Promise<{ [index: string]: 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[doc.id] = doc.data, obj), {})); + + resolve(result); + } catch (err) { + reject(err); + } + }); + } +}; \ No newline at end of file diff --git a/backend/sql/postgres.ts b/backend/sql/postgres.ts index 7b879f7..35d4719 100644 --- a/backend/sql/postgres.ts +++ b/backend/sql/postgres.ts @@ -19,7 +19,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; @@ -35,7 +35,6 @@ export class PostgreSQLDatabase implements Database { : undefined; return issue; - } public open(): Promise { diff --git a/common/types.d.ts b/common/types.d.ts index ddc3cd8..fa3a5a5 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -52,8 +52,8 @@ declare module 'ass' { } interface DatabaseConfiguration { - kind: 'mysql' | 'postgres' | 'json'; - options?: MySQLConfiguration | PostgresConfiguration; + kind: 'mysql' | 'postgres' | 'json' | 'mongodb'; + options?: MySQLConfiguration | PostgresConfiguration | MongoDBConfiguration; } interface MySQLConfiguration { @@ -71,6 +71,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 diff --git a/frontend/setup.mts b/frontend/setup.mts index c4372d8..18357e0 100644 --- a/frontend/setup.mts +++ b/frontend/setup.mts @@ -56,6 +56,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, @@ -127,7 +134,20 @@ document.addEventListener('DOMContentLoaded', () => { database: Elements.pgsqlDatabase.value } } - }; + } + } 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 diff --git a/package.json b/package.json index d4b7dd0..f08f7d9 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "fs-extra": "^11.1.1", "luxon": "^3.4.3", "memorystore": "^1.6.7", + "mongoose": "^8.0.0", "mysql2": "^3.6.2", "node-vibrant": "^3.1.6", "pg": "^8.11.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30d02e7..2fb2b40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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.2 version: 3.6.2 @@ -1205,6 +1208,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'} @@ -1930,6 +1939,17 @@ packages: resolution: {integrity: sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==} dev: false + /@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.2): resolution: {integrity: sha512-QeGcEvlesS+cXwfao14kdLI2zHJk3vppKSEbpbiNP1abx45P8HWqGEWhgF71bKlnCSW8a7b4RNDNa4mj1aHPMA==} dependencies: @@ -2190,6 +2210,11 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.22.1) 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'} @@ -3344,6 +3369,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'} @@ -3459,6 +3489,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'} @@ -3567,6 +3601,81 @@ 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 + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -4466,6 +4575,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 @@ -4716,6 +4830,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 @@ -4743,6 +4861,12 @@ packages: engines: {node: '>=0.10.0'} dev: false + /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'} @@ -5007,6 +5131,13 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 + dev: false + /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: false @@ -5131,6 +5262,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: diff --git a/views/setup.pug b/views/setup.pug index 5426e4e..9758cb8 100644 --- a/views/setup.pug +++ b/views/setup.pug @@ -65,6 +65,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 From 3cfef2ba4c4ddc92d48a05fb07a2dacb6a2ad406 Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 6 Nov 2023 12:57:59 -0500 Subject: [PATCH 02/20] fix style stuff --- backend/routers/index.ts | 2 +- backend/sql/database.ts | 10 +++++----- backend/sql/mysql.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/routers/index.ts b/backend/routers/index.ts index eed8d13..bcf6581 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -30,7 +30,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!'); diff --git a/backend/sql/database.ts b/backend/sql/database.ts index 6624245..800bd53 100644 --- a/backend/sql/database.ts +++ b/backend/sql/database.ts @@ -1,4 +1,4 @@ -import { AssFile, AssUser, NID, UploadToken } from "ass"; +import { AssFile, AssUser, NID, UploadToken } from 'ass'; export type DatabaseValue = AssFile | AssUser | UploadToken; export type DatabaseTable = 'assfiles' | 'assusers' | 'asstokens'; @@ -73,7 +73,7 @@ export class DBManager { public static configure(): Promise { if (this._db && this._dbReady) { return this._db.configure(); - } else throw new Error("No database active"); + } else throw new Error('No database active'); } /** @@ -82,7 +82,7 @@ export class DBManager { public static put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise { 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'); } /** @@ -91,7 +91,7 @@ export class DBManager { public static get(table: DatabaseTable, key: NID): Promise { 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'); } /** @@ -100,6 +100,6 @@ export class DBManager { public static getAll(table: DatabaseTable): Promise<{ [index: string]: 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'); } } \ No newline at end of file diff --git a/backend/sql/mysql.ts b/backend/sql/mysql.ts index 3e90b47..32b9abd 100644 --- a/backend/sql/mysql.ts +++ b/backend/sql/mysql.ts @@ -32,7 +32,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; From ac377ba479dbe6343b1d1e5f3967dcb2a873dddd Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 6 Nov 2023 13:02:33 -0500 Subject: [PATCH 03/20] oops --- backend/routers/api.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/routers/api.ts b/backend/routers/api.ts index 43b5023..6fed8e0 100644 --- a/backend/routers/api.ts +++ b/backend/routers/api.ts @@ -53,11 +53,6 @@ router.post('/setup', BodyParserJson(), async (req, res) => { if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login); if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload); - // 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); - log.success('Setup', 'completed'); return res.json({ success: true }); From 46453b876de3f716a74c28323342eed4935b6234 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 14:40:46 -0500 Subject: [PATCH 04/20] image viewer --- backend/routers/index.ts | 32 ++++++++++++++++++++++-- tailwind.css | 9 +++++++ views/viewer.pug | 53 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 views/viewer.pug diff --git a/backend/routers/index.ts b/backend/routers/index.ts index bcf6581..60948d2 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -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,7 @@ import { random } from '../generators'; import { UserConfig } from '../UserConfig'; import { getFileS3, uploadFileS3 } from '../s3'; import { rateLimiterMiddleware } from '../ratelimit'; +import { DBManager } from '../sql/database'; const router = Router({ caseSensitive: true }); @@ -96,7 +97,34 @@ 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 + }); + } +}); router.get('/direct/:fakeId', async (req, res) => { if (!UserConfig.ready) res.redirect('/setup'); diff --git a/tailwind.css b/tailwind.css index 6e62757..1e87c7e 100644 --- a/tailwind.css +++ b/tailwind.css @@ -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 { diff --git a/views/viewer.pug b/views/viewer.pug new file mode 100644 index 0000000..ef79132 --- /dev/null +++ b/views/viewer.pug @@ -0,0 +1,53 @@ +doctype html +html.dark.sl-theme-dark(lang='en') + 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 + + //- 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) \ No newline at end of file From 81cb4aa7e3b698badcd89b03992d7a40c4ae2ded Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 14:51:18 -0500 Subject: [PATCH 05/20] embed --- views/viewer.pug | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index ef79132..e466b90 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -1,5 +1,5 @@ doctype html -html.dark.sl-theme-dark(lang='en') +html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') head //- this stuff meta(charset='UTF-8') @@ -11,6 +11,11 @@ html.dark.sl-theme-dark(lang='en') title ass 🍑 //- embed data + meta(property='og:title', content=`image uploaded by ${user}`) + meta(property='og:description', content='a one to two sentence description of your objectg') + meta(property='og:type', content='website') + meta(property='og:image', content=url) + meta(property='og:url', content='.') //- mixins include ../node_modules/shoelace-fontawesome-pug/sl-fa-mixin.pug From 7af42c7c89f3c57e7d06d4911b5ea0a4382d4a10 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 15:51:34 -0500 Subject: [PATCH 06/20] embed templates --- backend/embed.ts | 26 +++++++++++++++++++++++ backend/routers/index.ts | 7 +++++- common/types.d.ts | 46 ++++++++++++++++++++++++++++++++++++++++ views/viewer.pug | 4 ++-- 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 backend/embed.ts diff --git a/backend/embed.ts b/backend/embed.ts new file mode 100644 index 0000000..7078e56 --- /dev/null +++ b/backend/embed.ts @@ -0,0 +1,26 @@ +import { EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" + +export const DEFAULT_EMBED: EmbedTemplate = { + title: "ass - The simple self-hosted ShareX server", + description: "ass is a self-hosted ShareX upload server written in Node.js" +} + +const executeEmbedOperation = (op: EmbedTemplateOperation): string => { + if (typeof op == 'string') { + return op; + } else if (typeof op == 'object') { + switch (op.op) { + case 'random': + if (op.options.length > 0) { + return executeEmbedOperation(op.options[Math.round(Math.random() * (op.options.length - 1))]); + } else throw new Error("Random without child operations"); + } + } else throw new Error("Invalid embed template operation"); +}; + +export const prepareEmbed = (template: EmbedTemplate): PreparedEmbed => { + return { + title: executeEmbedOperation(template.title), + description: executeEmbedOperation(template.description) + }; +}; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index 60948d2..39b815f 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -14,6 +14,7 @@ import { UserConfig } from '../UserConfig'; import { getFileS3, uploadFileS3 } from '../s3'; import { rateLimiterMiddleware } from '../ratelimit'; import { DBManager } from '../sql/database'; +import { DEFAULT_EMBED, prepareEmbed } from '../embed'; const router = Router({ caseSensitive: true }); @@ -121,7 +122,11 @@ router.get('/:fakeId', async (req, res) => { url: `/direct/${fakeId}`, uploader: user?.username ?? 'unknown', size: meta.size, - time: meta.timestamp + time: meta.timestamp, + embed: prepareEmbed({ + title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, + description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description + }) }); } }); diff --git a/common/types.d.ts b/common/types.d.ts index fa3a5a5..8881557 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -27,6 +27,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 { @@ -255,6 +274,33 @@ declare module 'ass' { cliKey: string; meta: { [key: string]: any }; } + + // Generic embed template operation + type EmbedTemplateOperation = EmbedTemplateRandomOperation | string; + + /** + * Selects one operation and executes it + */ + type EmbedTemplateRandomOperation = { + op: "random"; + options: EmbedTemplateOperation[]; + }; + + /** + * This is so beyond cursed + */ + interface EmbedTemplate { + title: EmbedTemplateOperation; + description: EmbedTemplateOperation; + } + + /** + * + */ + interface PreparedEmbed { + title: string; + description: string; + } } //#region Dummy modules diff --git a/views/viewer.pug b/views/viewer.pug index e466b90..6abbe4f 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -11,8 +11,8 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') title ass 🍑 //- embed data - meta(property='og:title', content=`image uploaded by ${user}`) - meta(property='og:description', content='a one to two sentence description of your objectg') + meta(property='og:title', content=embed.title) + meta(property='og:description', content=embed.description) meta(property='og:type', content='website') meta(property='og:image', content=url) meta(property='og:url', content='.') From c30f45be20e4e5b350e724d4c54a1b4803917a9b Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:18:37 -0500 Subject: [PATCH 07/20] oops --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index 6abbe4f..e86b891 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='website') + meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From 64993533fb6c8d54a6af06b023bbdd3b666b7aa8 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:29:26 -0500 Subject: [PATCH 08/20] uhh --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index e86b891..214f52c 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='image') + meta(property='og:type', content='object') meta(property='og:image', content=url) meta(property='og:url', content='.') From cb0d3ebde3125b1776b24d6f7d75f751f37775a5 Mon Sep 17 00:00:00 2001 From: xwashere Date: Thu, 9 Nov 2023 16:34:35 -0500 Subject: [PATCH 09/20] uhh --- views/viewer.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index 214f52c..ee535bb 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,9 +13,10 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:type', content='object') + 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 From 83bfd21955cc10b0e5675fee68f76f9394919df2 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 08:54:29 -0500 Subject: [PATCH 10/20] more embed operations --- backend/embed.ts | 45 +++++++++++++++++++++++++++++++++------- backend/routers/index.ts | 10 ++++++++- common/types.d.ts | 42 ++++++++++++++++++++++++++++++++++--- views/viewer.pug | 1 + 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 7078e56..9f5aa5c 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,26 +1,55 @@ -import { EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" +import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" export const DEFAULT_EMBED: EmbedTemplate = { title: "ass - The simple self-hosted ShareX server", description: "ass is a self-hosted ShareX upload server written in Node.js" } -const executeEmbedOperation = (op: EmbedTemplateOperation): string => { +class EmbedContext { + public uploader: AssUser; + public file: AssFile; + + constructor(uploader: AssUser, file: AssFile) { + this.uploader = uploader; + this.file = file; + } +} + +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.options.length > 0) { - return executeEmbedOperation(op.options[Math.round(Math.random() * (op.options.length - 1))]); - } else throw new Error("Random without child operations"); + 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((prev, op) => prev.concat(executeEmbedOperation(op, ctx).toString()), ""); } } else throw new Error("Invalid embed template operation"); }; -export const prepareEmbed = (template: EmbedTemplate): PreparedEmbed => { +export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFile): PreparedEmbed => { + let ctx = new EmbedContext(user, file); + return { - title: executeEmbedOperation(template.title), - description: executeEmbedOperation(template.description) + title: executeEmbedOperation(template.title, ctx).toString(), + description: executeEmbedOperation(template.description, ctx).toString() }; }; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index 39b815f..b5288f0 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -126,7 +126,15 @@ router.get('/:fakeId', async (req, res) => { embed: prepareEmbed({ title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description - }) + }, user ?? { + admin: false, + files: [], + id: "", + meta: {}, + password: "", + tokens: [], + username: "unknown" + }, meta) }); } }); diff --git a/common/types.d.ts b/common/types.d.ts index 8881557..b5c68b7 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -276,16 +276,52 @@ declare module 'ass' { } // Generic embed template operation - type EmbedTemplateOperation = EmbedTemplateRandomOperation | string; + type EmbedTemplateOperation = + EmbedTemplateRandomOperation | + EmbedTemplateFileSizeOperation | + EmbedTemplateFormatBytesOperation | + EmbedTemplateUploaderOperation | + EmbedTemplateConcatOperation | + string | number; /** * Selects one operation and executes it */ type EmbedTemplateRandomOperation = { - op: "random"; - options: EmbedTemplateOperation[]; + op: 'random'; + values: EmbedTemplateOperation[]; }; + /** + * Returns the file size in bytes + * @returns number + */ + type EmbedTemplateFileSizeOperation = { op: 'fileSize' }; + + /** + * Formats the file size in in {value} + */ + type EmbedTemplateFormatBytesOperation = { + op: 'formatBytes'; + unit?: EmbedTemplateOperation; // ready for ios! + value: EmbedTemplateOperation; + }; + + /** + * Returns the user who uploaded this file + */ + type EmbedTemplateUploaderOperation = { + op: 'uploader' + }; + + /** + * Joins strings + */ + type EmbedTemplateConcatOperation = { + op: 'concat', + values: EmbedTemplateOperation[] + } + /** * This is so beyond cursed */ diff --git a/views/viewer.pug b/views/viewer.pug index ee535bb..e517dd4 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,6 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) + meta(property='og:author', content='ass') meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From a1a0975a3f320a5b4a421a84c17e7a6cb89c64f9 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 09:07:24 -0500 Subject: [PATCH 11/20] oops --- views/viewer.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/viewer.pug b/views/viewer.pug index e517dd4..a05ac28 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:author', content='ass') + meta(property='og:site_name', content='ass') meta(property='og:type', content='image') meta(property='og:image', content=url) meta(property='og:url', content='.') From db32bb1c2974192a2999bbe2f53fc1fa8f8ad9f4 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 10 Nov 2023 09:17:29 -0500 Subject: [PATCH 12/20] sitename --- backend/embed.ts | 10 ++++++---- backend/routers/index.ts | 3 ++- common/types.d.ts | 2 ++ views/viewer.pug | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 9f5aa5c..27e70a7 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,9 +1,10 @@ import { AssFile, AssUser, EmbedTemplate, EmbedTemplateOperation, PreparedEmbed } from "ass" export const DEFAULT_EMBED: EmbedTemplate = { - title: "ass - The simple self-hosted ShareX server", - description: "ass is a self-hosted ShareX upload server written in Node.js" -} + sitename: "ass", + title: "", + description: "" +}; class EmbedContext { public uploader: AssUser; @@ -50,6 +51,7 @@ export const prepareEmbed = (template: EmbedTemplate, user: AssUser, file: AssFi return { title: executeEmbedOperation(template.title, ctx).toString(), - description: executeEmbedOperation(template.description, ctx).toString() + description: executeEmbedOperation(template.description, ctx).toString(), + sitename: executeEmbedOperation(template.sitename, ctx).toString() }; }; \ No newline at end of file diff --git a/backend/routers/index.ts b/backend/routers/index.ts index b5288f0..93235ef 100644 --- a/backend/routers/index.ts +++ b/backend/routers/index.ts @@ -125,7 +125,8 @@ router.get('/:fakeId', async (req, res) => { time: meta.timestamp, embed: prepareEmbed({ title: UserConfig.config.embed?.title ?? DEFAULT_EMBED.title, - description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description + description: UserConfig.config.embed?.description ?? DEFAULT_EMBED.description, + sitename: UserConfig.config.embed?.sitename ?? DEFAULT_EMBED.sitename }, user ?? { admin: false, files: [], diff --git a/common/types.d.ts b/common/types.d.ts index b5c68b7..322ae5a 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -328,6 +328,7 @@ declare module 'ass' { interface EmbedTemplate { title: EmbedTemplateOperation; description: EmbedTemplateOperation; + sitename: EmbedTemplateOperation; } /** @@ -336,6 +337,7 @@ declare module 'ass' { interface PreparedEmbed { title: string; description: string; + sitename: string; } } diff --git a/views/viewer.pug b/views/viewer.pug index a05ac28..84bf35d 100644 --- a/views/viewer.pug +++ b/views/viewer.pug @@ -13,7 +13,7 @@ html.dark.sl-theme-dark(lang='en', prefix='og: https://ogp.me/ns') //- embed data meta(property='og:title', content=embed.title) meta(property='og:description', content=embed.description) - meta(property='og:site_name', content='ass') + 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='.') From 3660b6cc8b81989ee93c7329363f91addac1bc8d Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 09:29:49 -0500 Subject: [PATCH 13/20] meow --- backend/UserConfig.ts | 25 ++++ backend/embed.ts | 58 ++------ backend/templates/command.ts | 9 ++ backend/templates/error.ts | 28 ++++ backend/templates/executor.ts | 121 +++++++++++++++ backend/templates/parser.ts | 270 ++++++++++++++++++++++++++++++++++ common/types.d.ts | 72 ++++----- 7 files changed, 508 insertions(+), 75 deletions(-) create mode 100644 backend/templates/command.ts create mode 100644 backend/templates/error.ts create mode 100644 backend/templates/executor.ts create mode 100644 backend/templates/parser.ts diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index a6b90ef..ed74a70 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -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; } diff --git a/backend/embed.ts b/backend/embed.ts index 27e70a7..9828586 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -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((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) }; }; \ No newline at end of file diff --git a/backend/templates/command.ts b/backend/templates/command.ts new file mode 100644 index 0000000..8e44a1b --- /dev/null +++ b/backend/templates/command.ts @@ -0,0 +1,9 @@ +import { TemplateCommandOp, TemplateCommandSchema } from 'ass'; +import { TemplateContext } from './executor'; + +export type TemplateCommand = { + readonly name: N; + readonly schema: S; + + exec: (op: TemplateCommandOp, ctx: TemplateContext) => string; +}; \ No newline at end of file diff --git a/backend/templates/error.ts b/backend/templates/error.ts new file mode 100644 index 0000000..28c7c4b --- /dev/null +++ b/backend/templates/error.ts @@ -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 {}; \ No newline at end of file diff --git a/backend/templates/executor.ts b/backend/templates/executor.ts new file mode 100644 index 0000000..314b12c --- /dev/null +++ b/backend/templates/executor.ts @@ -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 } = {}; + + // register a template command globally + public registerCommand(name: N, attrs: S, cmd: (op: TemplateCommandOp, 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; + } +} \ No newline at end of file diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts new file mode 100644 index 0000000..50a714b --- /dev/null +++ b/backend/templates/parser.ts @@ -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; +} \ No newline at end of file diff --git a/common/types.d.ts b/common/types.d.ts index 322ae5a..1e7fbe2 100644 --- a/common/types.d.ts +++ b/common/types.d.ts @@ -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 | 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 = { + 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; } /** From 8a5684c4ec3c23a894a7be06b5e20a3522c23026 Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:18:15 -0500 Subject: [PATCH 14/20] template inclusion --- backend/templates/error.ts | 52 +++++++++++++++++++++++++++++++++++-- backend/templates/parser.ts | 29 ++++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 28c7c4b..8297780 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -13,8 +13,56 @@ export class TemplateError extends Error { 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'; + 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; + } + } + + pend = Math.max(this.range.file.code.indexOf('\n', pend), pend); + + if (fline == tline) { + format += `${fline.toString().padStart(5, ' ')} | ${this.range.file.code.substring(pstart, pend)}\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).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}`; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 50a714b..e8c3734 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -1,4 +1,7 @@ import { TemplateOp, TemplateSource } from 'ass'; + +import fs from 'fs'; + import { TemplateSyntaxError } from './error'; enum TokenType { @@ -97,6 +100,10 @@ function getTemplateTokens(src: string): TemplateToken[] { 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; @@ -203,7 +210,7 @@ export function prepareTemplate(src: string): TemplateOp { } 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('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) { @@ -253,6 +260,26 @@ export function prepareTemplate(src: string): TemplateOp { stackDrop(); + // include is executed early + if (name.toLowerCase() == 'include') { + if (nargs['file'] != null) { + // TODO: this NEEDS to be restricted before ass 0.15.0 is released + // its extremely insecure and should be restricted to things + // set by operators, users can have their own template inclusion + // thing that doesnt depend on reading files + + if (typeof nargs['file'] == 'string') { + if (fs.existsSync(nargs['file'])) { + let template = fs.readFileSync(nargs['file'], { encoding: 'utf-8' }); + + let tl = prepareTemplate(template); + + 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, From ab94343cb47122f28174d2e5af08609a3ef3919a Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:41:13 -0500 Subject: [PATCH 15/20] fuck --- backend/templates/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index e8c3734..0a94b43 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -186,8 +186,8 @@ export function prepareTemplate(src: string): TemplateOp { args: joined, srcRange: { file: file, - from: tokens[start].from, - to: tokens[pos - 1].to + from: tokens[Math.min(tokens.length - 1, start)].from, + to: tokens[Math.min(tokens.length, pos) - 1].to } }; } From e1a49821e64c98519738100430b7b84784fca6de Mon Sep 17 00:00:00 2001 From: xwashere Date: Mon, 27 Nov 2023 10:43:06 -0500 Subject: [PATCH 16/20] fuck --- backend/templates/parser.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 0a94b43..70d7400 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -186,8 +186,8 @@ export function prepareTemplate(src: string): TemplateOp { args: joined, srcRange: { file: file, - from: tokens[Math.min(tokens.length - 1, start)].from, - to: tokens[Math.min(tokens.length, pos) - 1].to + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 } }; } @@ -286,8 +286,8 @@ export function prepareTemplate(src: string): TemplateOp { args: args, srcRange: { file: file, - from: tokens[start].from, - to: tokens[pos - 1].to + from: tokens[start]?.from ?? 0, + to: tokens[pos - 1]?.to ?? src.length - 1 } }; } From 841556ea58c649f08a34162fef8acb36e950422f Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 1 Dec 2023 08:20:01 -0500 Subject: [PATCH 17/20] restrict include to admin defined templates --- backend/UserConfig.ts | 4 +++- backend/templates/error.ts | 11 ++++++++--- backend/templates/parser.ts | 20 ++++++++++++++------ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/backend/UserConfig.ts b/backend/UserConfig.ts index ed74a70..510c47e 100644 --- a/backend/UserConfig.ts +++ b/backend/UserConfig.ts @@ -139,7 +139,9 @@ export class UserConfig { 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); + 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]; } diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 8297780..4485503 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -9,6 +9,9 @@ export class TemplateError extends Error { this.range = range; } + /** + * Formats the error. + */ public format(): string { let format = ''; @@ -39,13 +42,15 @@ export class TemplateError extends Error { } } - pend = Math.max(this.range.file.code.indexOf('\n', pend), pend); + 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)}\n`; + 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).split('\n'); + let lines = this.range.file.code.substring(pstart, pend + 1).split('\n'); format += ` | /${'~'.repeat(lines[0].length)}v\n`; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index 70d7400..f0ef0f0 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -130,7 +130,15 @@ function getTemplateTokens(src: string): TemplateToken[] { return tokens; } -export function prepareTemplate(src: string): TemplateOp { +export type PrepareTemplateOptions = { + allowIncludeFile?: boolean; +}; + +export function prepareTemplate(src: string, config?: PrepareTemplateOptions): TemplateOp { + let options = { + includeFiles: config?.allowIncludeFile ?? false + }; + type ParserStackEntry = { pos: number }; @@ -263,16 +271,16 @@ export function prepareTemplate(src: string): TemplateOp { // include is executed early if (name.toLowerCase() == 'include') { if (nargs['file'] != null) { - // TODO: this NEEDS to be restricted before ass 0.15.0 is released - // its extremely insecure and should be restricted to things - // set by operators, users can have their own template inclusion - // thing that doesnt depend on reading files + // 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); + 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}); From 839c7a73a7a1859b539771d769a5b87e7819a190 Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 1 Dec 2023 08:31:36 -0500 Subject: [PATCH 18/20] fix this --- backend/sql/mongodb.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/sql/mongodb.ts b/backend/sql/mongodb.ts index ca8c258..7518208 100644 --- a/backend/sql/mongodb.ts +++ b/backend/sql/mongodb.ts @@ -1,8 +1,10 @@ import { AssFile, AssUser, MongoDBConfiguration, NID, UploadToken } from 'ass'; -import { UserConfig } from '../UserConfig'; -import { Database, DatabaseTable, DatabaseValue } from './database'; + import mongoose, { Model, Mongoose, Schema } from 'mongoose'; -import { log } from '../log'; + +import { UserConfig } from '../UserConfig.js'; +import { Database, DatabaseTable, DatabaseValue } from './database.js'; +import { log } from '../log.js'; interface TableVersion { name: string; From 22569a5da72f7c341c8370fbb0c813e2245a870d Mon Sep 17 00:00:00 2001 From: xwashere Date: Fri, 1 Dec 2023 08:39:50 -0500 Subject: [PATCH 19/20] fix a lot of broken stuff: --- backend/embed.ts | 11 ++++++----- backend/templates/command.ts | 3 ++- backend/templates/error.ts | 2 +- backend/templates/executor.ts | 5 +++-- backend/templates/parser.ts | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/embed.ts b/backend/embed.ts index 9828586..ead029f 100644 --- a/backend/embed.ts +++ b/backend/embed.ts @@ -1,12 +1,13 @@ -import { AssFile, AssUser, EmbedTemplate, PreparedEmbed } from "ass" -import { TemplateExecutor } from "./templates/executor"; +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: "" + sitename: 'ass', + title: '', + description: '' }; // ensures a template is valid diff --git a/backend/templates/command.ts b/backend/templates/command.ts index 8e44a1b..eb2f651 100644 --- a/backend/templates/command.ts +++ b/backend/templates/command.ts @@ -1,5 +1,6 @@ import { TemplateCommandOp, TemplateCommandSchema } from 'ass'; -import { TemplateContext } from './executor'; + +import { TemplateContext } from './executor.js'; export type TemplateCommand = { readonly name: N; diff --git a/backend/templates/error.ts b/backend/templates/error.ts index 4485503..9956af6 100644 --- a/backend/templates/error.ts +++ b/backend/templates/error.ts @@ -1,4 +1,4 @@ -import { TemplateSourceRange } from "ass"; +import { TemplateSourceRange } from 'ass'; export class TemplateError extends Error { range?: TemplateSourceRange; diff --git a/backend/templates/executor.ts b/backend/templates/executor.ts index 314b12c..de32160 100644 --- a/backend/templates/executor.ts +++ b/backend/templates/executor.ts @@ -1,6 +1,7 @@ import { AssFile, AssUser, TemplateCommandOp, TemplateCommandSchema, TemplateOp } from 'ass'; -import { TemplateCommand } from './command'; -import { TemplateError } from './error'; + +import { TemplateCommand } from './command.js'; +import { TemplateError } from './error.js'; export class TemplateContext { public readonly owner: TemplateExecutor; diff --git a/backend/templates/parser.ts b/backend/templates/parser.ts index f0ef0f0..cd4d5e4 100644 --- a/backend/templates/parser.ts +++ b/backend/templates/parser.ts @@ -2,7 +2,7 @@ import { TemplateOp, TemplateSource } from 'ass'; import fs from 'fs'; -import { TemplateSyntaxError } from './error'; +import { TemplateSyntaxError } from './error.js'; enum TokenType { T_OPEN, T_CLOSE, From 8cbe4b7325f58f488aa3231eb7a6ea8fa9f0dcb4 Mon Sep 17 00:00:00 2001 From: xwashere Date: Tue, 5 Dec 2023 08:44:19 -0500 Subject: [PATCH 20/20] update postgres db for new db interface --- backend/routers/api.ts | 3 ++- backend/sql/mongodb.ts | 7 ++++++- backend/sql/postgres.ts | 14 +++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/routers/api.ts b/backend/routers/api.ts index 973c777..5f0f1c8 100644 --- a/backend/routers/api.ts +++ b/backend/routers/api.ts @@ -66,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])) diff --git a/backend/sql/mongodb.ts b/backend/sql/mongodb.ts index 64b4450..b0d8eb4 100644 --- a/backend/sql/mongodb.ts +++ b/backend/sql/mongodb.ts @@ -209,6 +209,10 @@ export class MongoDBDatabase implements Database { 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); @@ -216,6 +220,7 @@ export class MongoDBDatabase implements Database { }); } + // TODO: Unsure if this works. getAll(table: DatabaseTable): Promise { return new Promise(async (resolve, reject) => { try { @@ -228,7 +233,7 @@ export class MongoDBDatabase implements Database { // more ts-ignore! // @ts-ignore let result = await models[table].find({}).exec() // @ts-ignore - .then(res => res.reduce((obj, doc) => (obj[doc.id] = doc.data, obj), {})); + .then(res => res.reduce((obj, doc) => (obj.push(doc.data)), [])); resolve(result); } catch (err) { diff --git a/backend/sql/postgres.ts b/backend/sql/postgres.ts index 6d4d5e0..0ea1d54 100644 --- a/backend/sql/postgres.ts +++ b/backend/sql/postgres.ts @@ -171,7 +171,11 @@ 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); } @@ -183,14 +187,14 @@ export class PostgreSQLDatabase implements Database { 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); }