// Default configuration const config = { host: '0.0.0.0', port: 40115, domain: 'upload.example.com', maxUploadSize: 50, useSsl: true, isProxied: true, resourceIdSize: 12, gfyIdSize: 2, resourceIdType: 'random', spaceReplace: '_', mediaStrict: false, viewDirect: false, useIdInViewer: false, idInViewerExtension: false, dataEngine: '@tycrek/papito', frontendName: 'ass-x', savePerDay: false, adminWebhookEnabled: false, s3enabled: false, }; // Default admin webhook const adminWebhookConfig = { adminWebhookUrl: '', adminWebhookUsername: '', adminWebhookAvatar: '', } // Default S3 config const s3config = { s3endpoint: 'sfo3.digitaloceanspaces.com', s3bucket: 'bucket-name', s3usePathStyle: false, s3accessKey: 'accessKey', s3secretKey: 'secretKey', }; // Redacted configs from previous versions const oldConfig = { // Note for people manually editing config.json __WARNING__: "The following configs are no longer used and are here for backwards compatibility. For optimal use, DO NOT edit them.", // Removed in 0.8.4 diskFilePath: 'uploads/', saveWithDate: true, // Some systems don't like dirs with massive amounts of files saveAsOriginal: false, // Prone to conflicts, which ass doesn't handle useSia: false, // Sia has been shut down in 2022, uploads fail as of 2022-12-26 }; function getConfirmSchema(description) { return { properties: { confirm: { description, type: 'string', pattern: /^[y|n]/gim, message: 'Must respond with either \'y\' or \'n\'', required: true, before: (value) => value.toLowerCase().startsWith('y') } } }; } // If directly called on the command line, run setup script function doSetup() { const path = (...paths) => require('path').join(process.cwd(), ...paths); const { TLog } = require('@tycrek/log'); const fs = require('fs-extra'); const prompt = require('prompt'); const chalk = require('chalk'); const token = require('./generators/token'); const log = new TLog('debug').setTimestamp({ enabled: false }); // Override default configs with existing configs to allow migrating configs // Now that's a lot of configs! try { const existingConfig = require('../config.json'); Object.entries(existingConfig).forEach(([key, value]) => { Object.prototype.hasOwnProperty.call(config, key) && (config[key] = value); // skipcq: JS-0093 Object.prototype.hasOwnProperty.call(adminWebhookConfig, key) && (adminWebhookConfig[key] = value); // skipcq: JS-0093 Object.prototype.hasOwnProperty.call(s3config, key) && (s3config[key] = value); // skipcq: JS-0093 Object.prototype.hasOwnProperty.call(oldConfig, key) && (oldConfig[key] = value); // skipcq: JS-0093 }); } catch (ex) { if (ex.code !== 'MODULE_NOT_FOUND' && !ex.toString().includes('Unexpected end')) log.error(`${ex}`); } // Disabled the annoying "prompt: " prefix and removes colours prompt.message = ''; prompt.colors = false; prompt.start(); // Schema for setup prompts const setupSchema = { properties: { host: { description: 'Local IP to bind to', type: 'string', default: config.host, required: false }, port: { description: 'Port number to listen on', type: 'integer', default: config.port, required: false }, domain: { description: `Domain name to send to ShareX clients (example: ${config.domain})`, type: 'string', required: false, message: 'You must input a valid domain name or IP to continue' }, maxUploadSize: { description: `Maximum size for uploaded files, in megabytes`, type: 'integer', default: config.maxUploadSize, require: false }, isProxied: { description: 'Will you be running through a reverse proxy', type: 'boolean', default: config.isProxied, required: false }, useSsl: { description: 'Use HTTPS (must be configured with reverse proxy)', type: 'boolean', default: config.useSsl, required: false }, resourceIdSize: { description: 'URL length (length of ID\'s for your files, recommended: 6-15. Higher = more uploads, but longer URLs)', type: 'integer', default: config.resourceIdSize, required: false }, resourceIdType: { description: 'URL type (can be one of: zws, random, gfycat, original, timestamp)', type: 'string', default: config.resourceIdType, require: false, pattern: /(original|zws|random|gfycat|timestamp)/gi, // skipcq: JS-0113 message: 'Must be one of: zws, random, gfycat, original, timestamp' }, spaceReplace: { description: 'Character to replace spaces in filenames with (must be a hyphen -, underscore _, or use ! to remove spaces)', type: 'string', default: config.spaceReplace, required: false, pattern: /^[-_!]$/gim, message: 'Must be a - , _ , or !' }, gfyIdSize: { description: 'Adjective count for "gfycat" URL type', type: 'integer', default: config.gfyIdSize, required: false }, mediaStrict: { description: 'Only allow uploads of media files (images, videos, audio)', type: 'boolean', default: config.mediaStrict, required: false }, viewDirect: { description: 'View uploads in browser as direct resource, rather than a viewing page', type: 'boolean', default: config.viewDirect, required: false }, useIdInViewer: { description: 'Use the ID in the web viewer instead of the filename', type: 'boolean', default: config.useIdInViewer, required: false }, idInViewerExtension: { description: '(Only applies if "useIdInViewer" is true) Include the file extension in the ID in the web viewer', type: 'boolean', default: config.idInViewerExtension, required: false }, dataEngine: { description: 'Data engine to use (must match an npm package name. If unsure, leave blank)', type: 'string', default: config.dataEngine, required: false }, frontendName: { description: 'Name of your frontend (leave blank if not using frontends)', type: 'string', default: config.frontendName, required: false }, savePerDay: { description: 'Save uploads in folders by day (YYYY-MM-DD) instead of by month (YYYY-MM)', type: 'boolean', default: config.savePerDay, required: false }, adminWebhookEnabled: { description: 'Enable admin Discord webhook (This will let you audit ALL uploads, from ALL users)', type: 'boolean', default: config.adminWebhookEnabled, required: false }, s3enabled: { description: 'Enable uploading to S3 storage endpoints', type: 'boolean', default: config.s3enabled, required: false } } }; const adminWebhookSchema = { properties: { adminWebhookUrl: { description: 'Discord webhook URL for admin notifications', type: 'string', default: adminWebhookConfig.adminWebhookUrl, required: true }, adminWebhookUsername: { description: 'Username to send the webhook as', type: 'string', default: adminWebhookConfig.adminWebhookUsername, required: false }, adminWebhookAvatar: { description: 'Avatar to use for the webhook icon', type: 'string', default: adminWebhookConfig.adminWebhookAvatar, required: false } } }; const s3schema = { properties: { s3endpoint: { description: 'S3 Endpoint URL to upload objects to', type: 'string', default: s3config.s3endpoint, required: false }, s3bucket: { description: 'S3 Bucket name to upload objects to', type: 'string', default: s3config.s3bucket, required: false }, s3usePathStyle: { description: 'S3 path endpoint, otherwise uses subdomain endpoint', type: 'boolean', default: s3config.s3usePathStyle, required: false }, s3accessKey: { description: 'Access key for the specified S3 API', type: 'string', default: s3config.s3accessKey, required: false }, s3secretKey: { description: 'Secret key for the specified S3 API', type: 'string', default: s3config.s3secretKey, required: false }, } }; // Schema for confirm prompt. User must enter 'y' or 'n' (case-insensitive) const confirmSchema = getConfirmSchema('\nIs the above information correct? (y/n)'); log.blank().blank().blank().blank() .info('<<< ass setup >>>').blank(); let results = {}; prompt.get(setupSchema) .then((r) => results = r) // skipcq: JS-0086 // Check if using admin webhook .then(() => results.adminWebhookEnabled ? prompt.get(adminWebhookSchema) : adminWebhookConfig) // skipcq: JS-0229 .then((r) => Object.entries(r).forEach(([k, v]) => results[k] = v)) // skipcq: JS-0086 // Check if using S3 .then(() => results.s3enabled ? prompt.get(s3schema) : s3config) // skipcq: JS-0229 .then((r) => Object.entries(r).forEach(([k, v]) => results[k] = v)) // skipcq: JS-0086 // Verify information is correct .then(() => log .blank() .info('Please verify your information', '\n'.concat(Object.entries(results).map(([setting, value]) => `${' '}${chalk.dim.gray('-->')} ${chalk.bold.white(`${setting}:`)} ${chalk.white(value)}`).join('\n'))) .blank()) // Apply old configs .then(() => Object.entries(oldConfig).forEach(([setting, value]) => (typeof results[setting] === 'undefined') && (results[setting] = value))) // Confirm .then(() => prompt.get(confirmSchema)) .then(({ confirm }) => (confirm ? fs.writeJson(path('config.json'), results, { spaces: 4 }) : log.error('Setup aborted').callback(() => process.exit(1)))) // Other setup tasks .then(() => { // Make sure auth.json exists and generate the first key if (!fs.existsSync(path('auth.json')) || fs.readFileSync(path('auth.json')).length < 8) { let users = {}; users[token()] = { username: 'ass', count: 0 }; fs.writeJsonSync(path('auth.json'), { users }, { spaces: 4 }); log.debug('File created', 'auth.json') .success('!! Important', `Save this token in a secure spot: ${Object.keys(users)[0]}`) .blank(); } let existingData = {} try { existingData = fs.readJsonSync(path('data.json')); } catch (ex) { log.warn('data.json', 'File empty, fixing') } // All 3 as a Promise.all return Promise.all([ fs.ensureDir(path('share')), fs.ensureDir(path(results.diskFilePath, 'thumbnails')), fs.writeJson(path('data.json'), existingData, { spaces: 4 }) ]); }) // Complete & exit .then(() => log.blank().success('Setup complete').callback(() => process.exit(0))) .catch((err) => log.blank().callback(() => console.error(err)).callback(() => process.exit(1))); } module.exports = { doSetup, config }; // If called on the command line, run the setup. // Using this makes sure setup is not run when imported by another file if (require.main === module) { doSetup(); } /*{ description: 'Enter your password', // Prompt displayed to the user. If not supplied name will be used. type: 'string', // Specify the type of input to expect. pattern: /^\w+$/, // Regular expression that input must be valid against. message: 'Password must be letters', // Warning message to display if validation fails. hidden: true, // If true, characters entered will either not be output to console or will be outputed using the `replace` string. replace: '*', // If `hidden` is set it will replace each hidden character with the specified string. default: 'lamepassword', // Default value to use if no value is entered. required: true, // If true, value entered must be non-empty. before: function (value) { return 'v' + value; } // Runs before node-prompt callbacks. It modifies user's input }*/