diff --git a/.eslintrc.json b/.eslintrc.json index c5dd0ef..af3e032 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,8 +39,6 @@ "import/prefer-default-export": "off", "no-useless-constructor": "off", "@typescript-eslint/no-useless-constructor": 2, - "import/extensions": "off", - "consistent-return": "off", - "no-continue": "off" + "import/extensions": "off" } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 68c25c4..484ef4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,9 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Serverless directories -.serverless/ - -# macOS files -.DS_Store -*.DS_Store - -# Database files -*.sqlite - -# IDE and text editor configuration files -.vscode/ -.idea/ - -# other files -dist/ +node_modules +yarn.lock src/config.json -src/keys.json +package-lock.json htmlEmail_templates -securesign_genrsa.ts +yarn-error.log +src/keys.json +dist +securesign_genrsa.ts \ No newline at end of file diff --git a/.gitlab-ci.yml.old b/.gitlab-ci.yml similarity index 85% rename from .gitlab-ci.yml.old rename to .gitlab-ci.yml index 0c238be..683d018 100644 --- a/.gitlab-ci.yml.old +++ b/.gitlab-ci.yml @@ -1,19 +1,19 @@ -stages: - - build - - test - -typescript_build: - stage: build - script: - - cp ../config.json ./src/config.json - - yarn install --ignore-engines - - npx tsc -p ./tsconfig.json - -lint: - stage: test - before_script: - - cp ../config.json ./src/config.json - - yarn install --ignore-engines - script: - - yarn run lint-find - +stages: + - build + - test + +typescript_build: + stage: build + script: + - cp ../config.json ./src/config.json + - yarn install --ignore-engines + - tsc -p ./tsconfig.json + +lint: + stage: test + before_script: + - cp ../config.json ./src/config.json + - yarn install --ignore-engines + script: + - yarn run lint-find + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eb10a0b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "eslint.enable": true, + "eslint.validate": [ + { + "language": "typescript", + "autoFix": true + } + ], + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index e416871..af46d3c 100644 --- a/LICENSE +++ b/LICENSE @@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - Cloud Services + Cloud Services Copyright (C) 2019 Library of Code sp-us Engineering Team This program is free software: you can redistribute it and/or modify diff --git a/Makefile b/Makefile deleted file mode 100644 index 6a46355..0000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -# Builds TypeScript & Go - -check_certificate_files := $(wildcard src/go/checkCertificate/*.go) -check_certificate_signatures_files := $(wildcard src/go/checkCertSignatures/*.go) -storage_files := $(wildcard src/go/storage/*.go) -get_user_by_uid_files := $(wildcard src/go/getUserByUid/*.go) - -all: check_certificate check_cert_signatures storage getUserByUid typescript - -check_certificate: - HOME=/root go build -ldflags="-s -w" -o dist/bin/checkCertificate ${check_certificate_files} - @chmod 740 dist/bin/checkCertificate - @file dist/bin/checkCertificate - -check_cert_signatures: - HOME=/root go build -ldflags="-s -w" -o dist/bin/checkCertSignatures ${check_certificate_signatures_files} - @chmod 740 dist/bin/checkCertSignatures - @file dist/bin/checkCertSignatures - -storage: - HOME=/root go build -ldflags="-s -w" -buildmode=pie -o dist/bin/storage ${storage_files} - @chmod 740 dist/bin/storage - @file dist/bin/storage - -getUserByUid: - HOME=/root go build -ldflags="-s -w" -o dist/bin/getUserByUid ${get_user_by_uid_files} - @chmod 740 dist/bin/getUserByUid - @file dist/bin/getUserByUid - -typescript: - tsc -p ./tsconfig.json diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..a243591 --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +echo "Library of Code sp-us | Cloud Services" +echo "TypeScript & Go" +echo "Building TS files" +yarn run build +echo "Building Go files" +go build -o dist/intervals/storage src/intervals/storage.go src/intervals/dirsize.go \ No newline at end of file diff --git a/package.json b/package.json index 2346b08..355e0cc 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,47 @@ { "name": "cloudservices-rewrite", - "version": "2.0", - "description": "The official LOC Cloud Services system, this is a rewrite the original version, using discord.js.", + "version": "1.2.0", + "description": "The official LOC Cloud Services system, this is a rewrite of the original version. ", "main": "dist/Client.js", "scripts": { "lint": "eslint ./ --ext ts --fix", - "build": "make", + "build": "tsc -p ./tsconfig.json", "lint-find": "eslint ./ --ext ts" }, "author": "Library of Code sp-us Engineering Team", "license": "AGPL-3.0-only", "private": false, "dependencies": { - "axios": "^0.21.1", + "@ghaiklor/x509": "^1.0.0", + "axios": "^0.19.0", "body-parser": "^1.19.0", - "cron": "^1.8.2", - "discord.js": "^13.0.0", + "eris": "abalabahaha/eris#dev", + "eris-pagination": "bsian03/eris-pagination", "express": "^4.17.1", - "fs-extra": "^10.0.0", - "hastebin-gen": "^2.0.5", - "helmet": "^4.6.0", - "ioredis": "^4.27.7", - "jsonwebtoken": "^8.5.1", - "moment": "^2.29.1", - "mongoose": "^5.13.5", - "nodemailer": "^6.6.3", - "showdown": "^1.9.1", + "fs-extra": "^8.1.0", + "helmet": "^3.21.2", + "ioredis": "^4.14.1", + "moment": "^2.24.0", + "moment-precise-range-plugin": "^1.3.0", + "mongoose": "^5.7.4", + "nodemailer": "^6.3.1", "signale": "^1.4.0", - "uuid": "^8.3.2" + "uuid": "^3.3.3" }, "devDependencies": { - "@types/cron": "^1.7.3", - "@types/express": "^4.17.13", - "@types/express-serve-static-core": "^4.17.24", - "@types/fs-extra": "^9.0.12", - "@types/ioredis": "^4.26.7", - "@types/jsonwebtoken": "^8.5.4", - "@types/node": "^16.4.13", - "@types/nodemailer": "^6.4.4", - "@types/showdown": "^1.9.4", - "@types/signale": "^1.4.2", - "@types/uuid": "^8.3.1", - "@typescript-eslint/eslint-plugin": "^4.29.0", - "@typescript-eslint/parser": "^4.29.0", - "eslint": "^7.32.0", - "eslint-config-airbnb-base": "^14.2.1", - "eslint-plugin-import": "^2.23.4", - "madge": "^5.0.1", - "typescript": "^4.3.5" + "@types/express": "^4.17.2", + "@types/fs-extra": "^8.0.0", + "@types/helmet": "^0.0.45", + "@types/ioredis": "^4.0.18", + "@types/mongoose": "^5.5.20", + "@types/nodemailer": "^6.2.1", + "@types/signale": "^1.2.1", + "@types/uuid": "^3.4.5", + "@typescript-eslint/eslint-plugin": "^2.4.0", + "@typescript-eslint/parser": "^2.4.0", + "eslint": "^6.5.1", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.18.2", + "typescript": "^3.6.4" } } diff --git a/src/Client.ts b/src/Client.ts new file mode 100644 index 0000000..97cfbb9 --- /dev/null +++ b/src/Client.ts @@ -0,0 +1,131 @@ +import Eris from 'eris'; +import Redis from 'ioredis'; +import mongoose from 'mongoose'; +import signale from 'signale'; +import fs from 'fs-extra'; +import config from './config.json'; +import { Server } from './api'; +import { Account, AccountInterface, Moderation, ModerationInterface, Domain, DomainInterface } from './models'; +import { emojis } from './stores'; +import { Command, Util, Collection } from './class'; +import * as commands from './commands'; + + +export default class Client extends Eris.Client { + public config: { 'token': string; 'cloudflare': string; 'prefix': string; 'emailPass': string; 'mongoURL': string; 'port': number; 'keyPair': { 'publicKey': string; 'privateKey': string; }; }; + + public util: Util; + + public commands: Collection; + + public db: { Account: mongoose.Model; Domain: mongoose.Model; Moderation: mongoose.Model; }; + + public redis: Redis.Redis; + + public stores: { emojis: { success: string, loading: string, error: string }; }; + + public signale: signale.Signale; + + public server: Server; + + public updating: boolean; + + public buildError: boolean + + constructor() { + super(config.token, { getAllUsers: true, restMode: true, defaultImageFormat: 'png' }); + + process.title = 'cloudservices'; + this.config = config; + this.util = new Util(this); + this.commands = new Collection(); + this.db = { Account, Domain, Moderation }; + this.redis = new Redis(); + this.stores = { emojis }; + this.signale = signale; + this.signale.config({ + displayDate: true, + displayTimestamp: true, + displayFilename: true, + }); + this.updating = false; + this.buildError = false; + this.events(); + this.loadFunctions(); + this.init(); + } + + private async events() { + process.on('unhandledRejection', (error) => { + this.signale.error(error); + }); + } + + private async loadFunctions() { + const functions = await fs.readdir('./functions'); + functions.forEach(async (func) => { + if (func === 'index.ts' || func === 'index.js') return; + try { + (require(`./functions/${func}`).default)(this); + } catch (error) { + this.signale.error(`Error occured loading ${func}`); + await this.util.handleError(error); + } + }); + } + + public loadCommand(CommandFile: any) { + // eslint-disable-next-line no-useless-catch + try { + // eslint-disable-next-line + const command: Command = new CommandFile(this); + if (command.subcmds.length) { + command.subcmds.forEach((C) => { + const cmd: Command = new C(this); + command.subcommands.add(cmd.name, cmd); + }); + } + delete command.subcmds; + this.commands.add(command.name, command); + this.signale.complete(`Loaded command ${command.name}`); + } catch (err) { throw err; } + } + + public async init() { + const evtFiles = await fs.readdir('./events/'); + Object.values(commands).forEach((c: Function) => this.loadCommand(c)); + + evtFiles.forEach((file) => { + const eventName = file.split('.')[0]; + if (file === 'index.js') return; + // eslint-disable-next-line + const event = new (require(`./events/${file}`).default)(this); + this.signale.complete(`Loaded event ${eventName}`); + this.on(eventName, (...args) => event.run(...args)); + delete require.cache[require.resolve(`./events/${file}`)]; + }); + + await mongoose.connect(config.mongoURL, { useNewUrlParser: true, useUnifiedTopology: true }); + await this.connect(); + this.on('ready', () => { + this.signale.info(`Connected to Discord as ${this.user.username}#${this.user.discriminator}`); + }); + const intervals = await fs.readdir('./intervals'); + intervals.forEach((interval) => { + // eslint-disable-next-line + if (interval === 'index.js') return; + require(`./intervals/${interval}`).default(this); + this.signale.complete(`Loaded interval ${interval.split('.')[0]}`); + }); + this.server = new Server(this, { port: this.config.port }); + + const corepath = '/var/CloudServices/dist'; + const cmdFiles = await fs.readdir('/var/CloudServices/dist/commands'); + cmdFiles.forEach((f) => delete require.cache[`${corepath}/${f}`]); + delete require.cache[`${corepath}/config.json`]; + delete require.cache[`${corepath}/class/Util`]; + } +} + +// eslint-disable-next-line +new Client(); diff --git a/src/class/Security.ts b/src/api/Security.ts similarity index 50% rename from src/class/Security.ts rename to src/api/Security.ts index a59f9a2..2058619 100644 --- a/src/class/Security.ts +++ b/src/api/Security.ts @@ -1,13 +1,13 @@ /* eslint-disable no-underscore-dangle */ -import jwt from 'jsonwebtoken'; +import crypto from 'crypto'; import { Request } from 'express'; -import { Client } from '.'; +import { Client } from '..'; import { AccountInterface } from '../models'; export default class Security { public client: Client; - protected readonly keyBase: { key: string, iv: string, internal: string }; + private keyBase: { key: string, iv: string }; constructor(client: Client) { this.client = client; @@ -18,7 +18,6 @@ export default class Security { return { key: Buffer.from(this.keyBase.key, 'base64'), iv: Buffer.from(this.keyBase.iv, 'base64'), - internal: Buffer.from(this.keyBase.internal, 'hex'), }; } @@ -27,9 +26,16 @@ export default class Security { * @param _id The Mongoose Document property labeled ._id */ public async createBearer(_id: string): Promise { - const account = await this.client.db.Account.findOne({ _id }); + let account = await this.client.db.Account.findOne({ _id }); if (!account) throw new Error(`Account [${_id}] cannot be found.`); - return jwt.sign({ id: account.id }, this.keys.key, { issuer: 'Library of Code sp-us | CSD' }); + const salt = crypto.randomBytes(50).toString('base64'); + const cipher = crypto.createCipheriv('aes-256-gcm', this.keys.key, this.keys.iv); + await account.updateOne({ salt }); + account = await this.client.db.Account.findOne({ _id }); + let encrypted = cipher.update(JSON.stringify(account), 'utf8', 'base64'); + encrypted += cipher.final('base64'); + await account.updateOne({ authTag: cipher.getAuthTag() }); + return `${salt}:${encrypted}`; } /** @@ -37,14 +43,20 @@ export default class Security { * @param bearer The bearer token provided. */ public async checkBearer(bearer: string): Promise { + const decipher = crypto.createDecipheriv('aes-256-gcm', this.keys.key, this.keys.iv); try { - const res: any = jwt.verify(bearer, this.keys.key, { issuer: 'Library of Code sp-us | CSD' }); - const account = await this.client.db.Account.findOne({ _id: res.id }); - if (!account) return null; - if (account.locked) return null; - if (account.revokedBearers?.includes(bearer)) return null; + const salt = bearer.split(':')[0]; + const saltCheck = await this.client.db.Account.findOne({ salt }); + const encrypted = bearer.split(':')[1]; + let decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decipher.setAuthTag(saltCheck.authTag); + decrypted += decipher.final('utf8'); + const json = JSON.parse(decrypted); + const account = await this.client.db.Account.findOne({ username: json.username }); + if (saltCheck.salt !== account.salt) return null; return account; - } catch { + } catch (error) { + this.client.util.handleError(error); return null; } } @@ -58,7 +70,7 @@ export default class Security { return req.headers.authorization.split(' ')[1]; } if (req.query && req.query.token) { - return req.query.token as string; + return req.query.token; } return '0000000000'; } diff --git a/src/class/Server.ts b/src/api/Server.ts similarity index 72% rename from src/class/Server.ts rename to src/api/Server.ts index 879db4b..db55dcf 100644 --- a/src/class/Server.ts +++ b/src/api/Server.ts @@ -3,8 +3,9 @@ import express from 'express'; import bodyParser from 'body-parser'; import helmet from 'helmet'; import fs from 'fs-extra'; -import { Client, Collection, LocalStorage, Route } from '.'; -import { Security } from '../api'; +import { Client } from '..'; +import { Security } from '.'; +import { Collection, Route } from '../class'; export default class Server { public routes: Collection @@ -15,8 +16,6 @@ export default class Server { public app: express.Express; - public storage: LocalStorage; - public options: { port: number } constructor(client: Client, options?: { port: number }) { @@ -25,24 +24,22 @@ export default class Server { this.client = client; this.security = new Security(this.client); this.app = express(); - this.storage = new LocalStorage('usedra', '/opt/CloudServices/localstorage'); this.connect(); this.loadRoutes(); } private async loadRoutes(): Promise { - const routes = await fs.readdir(`${__dirname}/../api/routes`); + const routes = await fs.readdir(`${__dirname}/routes`); routes.forEach(async (routeFile) => { if (routeFile === 'index.js') return; try { // eslint-disable-next-line new-cap - const route: Route = new (require(`${__dirname}/../api/routes/${routeFile}`).default)(this); + const route: Route = new (require(`${__dirname}/routes/${routeFile}`).default)(this); if (route.conf.deprecated === true) { route.deprecated(); } else if (route.conf.maintenance === true) { route.maintenance(); } else { - route.init(); route.bind(); } this.routes.set(route.conf.path, route); @@ -52,7 +49,6 @@ export default class Server { this.client.util.handleError(error); } }); - this.app.use('/static', express.static('/opt/CloudServices/src/api/static')); } private connect(): void { @@ -60,12 +56,13 @@ export default class Server { this.app.use(helmet({ hsts: false, hidePoweredBy: false, - contentSecurityPolicy: false, - xssFilter: false, - noSniff: false, + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + }, + }, })); this.app.use(bodyParser.json()); - this.app.use(bodyParser.urlencoded({ extended: true })); this.app.listen(this.options.port, () => { this.client.signale.success(`API Server listening on port ${this.options.port}`); }); diff --git a/src/api/index.ts b/src/api/index.ts index 50849b6..4e70cd1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,2 +1,2 @@ -export { default as Server } from '../class/Server'; -export { default as Security } from '../class/Security'; +export { default as Server } from './Server'; +export { default as Security } from './Security'; diff --git a/src/api/routes/Account.ts b/src/api/routes/Account.ts index ff5f259..8a10596 100644 --- a/src/api/routes/Account.ts +++ b/src/api/routes/Account.ts @@ -5,7 +5,7 @@ import { Req } from '../interfaces'; export default class Account extends Route { constructor(server: Server) { - super(server, { path: '/account', deprecated: false, maintenance: false }); + super(server, { path: '/account', deprecated: false }); } public bind() { @@ -27,13 +27,14 @@ export default class Account extends Route { res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc }); } catch (error) { this.handleError(error, res); + this.server.client.util.handleError(error); } }); this.router.get('/moderations/:id?', async (req: Req, res) => { try { const moderations = await this.server.client.db.Moderation.find({ username: req.account.username }); - if (!moderations.length) return res.status(204).json({ code: this.constants.codes.NOT_FOUND, message: null }); + if (!moderations.length) res.sendStatus(204); if (req.params.id) { const filtered = moderations.filter((moderation) => moderation.logID === req.params.id); res.status(200).json({ code: this.constants.codes.SUCCESS, message: { filtered } }); @@ -42,33 +43,17 @@ export default class Account extends Route { } } catch (error) { this.handleError(error, res); + this.server.client.util.handleError(error); } }); - this.router.get('/resources', async (req: Req, res) => { + this.router.get('/storage', async (req: Req, res) => { try { - const [cpuUsage, ramUsage, diskUsage] = await Promise.all([ - this.server.client.util.exec(`top -b -n 1 -u ${req.account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`), - this.server.client.util.exec(`memory ${req.account.username}`), - this.server.client.redis.get(`storage-${req.account.username}`), - ]); - let storage: string; - if (req.query?.cache === 'false') { - const val = await this.server.client.util.exec(`du -sb /home/${req.account.username}`); - // eslint-disable-next-line prefer-destructuring - storage = val.split('\t')[0]; - } else { - storage = diskUsage; - } - res.status(200).json({ code: this.constants.codes.SUCCESS, - message: { - cpu: Number(`${cpuUsage.split('\n')[0] || '0'}`), - ram: (Number(ramUsage) * 1000) / 1024 / 1024, - disk: Number(storage) / 1024 / 1024, - }, - }); + const data = await this.server.client.redis.get(`storage-${req.account.username}`) ? Number(await this.server.client.redis.get(`storage-${req.account.username}`)) : null; + res.status(200).json({ code: this.constants.codes.SUCCESS, message: data }); } catch (error) { this.handleError(error, res); + this.server.client.util.handleError(error); } }); } diff --git a/src/api/routes/Root.ts b/src/api/routes/Root.ts index ddac692..a1e5766 100644 --- a/src/api/routes/Root.ts +++ b/src/api/routes/Root.ts @@ -1,79 +1,41 @@ import os from 'os'; -import jwt from 'jsonwebtoken'; -import { TextChannel, MessageEmbed } from 'discord.js'; import { Server } from '..'; import { Route } from '../../class'; export default class Root extends Route { constructor(server: Server) { - super(server, { path: '/', deprecated: false, maintenance: false }); + super(server, { path: '/', deprecated: false }); } public bind() { - this.router.get('/', async (_req, res) => { + this.router.get('/', async (req, res) => { try { const date = new Date(); date.setSeconds(-process.uptime()); - const accounts = await this.server.client.db.Account.find().lean().exec(); + const accounts = await this.server.client.db.Account.find(); const administrators = accounts.filter((account) => account.root === true).length; - const technicians = accounts.filter((account) => account?.permissions?.director === true).length; - const staff = accounts.filter((account) => account?.permissions?.staff === true).length; const response = { - csd: { - nodeVersion: process.version, - uptime: process.uptime(), - }, + nodeVersion: process.version, + uptime: process.uptime(), server: { - userCount: accounts.length, - administratorCount: administrators, - technicianCount: technicians, - staffCount: staff, - stats: { - uptime: os.uptime(), - loadAverages: os.loadavg(), - hostname: os.hostname(), - ipv4Address: os.networkInterfaces().enp0s3.filter((r) => r.family === 'IPv4')[0].address, - operatingSystem: { - platform: os.platform(), - release: os.release(), - }, - cpu: { - model: os.cpus()[0].model, - clockSpeed: os.cpus()[0].speed / 1000, - coreCount: os.cpus().length, - }, - }, + users: accounts.length, + administrators, + }, + stats: { + uptime: os.uptime(), + loadAverage: os.loadavg(), + cpuModel: os.cpus()[0].model, + cpuClock: os.cpus()[0].speed / 1000, + cpuCores: os.cpus().length, + hostname: os.hostname(), + ipv4: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv4')[0].address, + ipv6: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv6')[0].address, }, }; res.status(200).json({ code: this.constants.codes.SUCCESS, message: response }); } catch (error) { this.handleError(error, res); - } - }); - - // eslint-disable-next-line consistent-return - this.router.get('/verify', async (req, res) => { - if (req.query.t) { - try { - res.setHeader('Access-Control-Allow-Origin', '*'); - const token = jwt.verify(req.query.t.toString(), this.server.client.config.keyPair.privateKey); - const check = await this.server.storage.get(req.query.t.toString()); - if (check) return res.sendStatus(401); - const embed = new MessageEmbed(); - embed.setTitle('Referral Authorization'); - embed.setDescription(req.query.t.toString()); - embed.addField('Referred User', `${token.referredUserAndDiscrim} | ${token.referredUserID}`, true); - embed.addField('Referrer User', token.referrerUsername, true); - embed.addField('Referral Code', token.referralCode, true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - const channel = this.server.client.guilds.cache.get('446067825673633794').channels.cache.get('580950455581147146') as TextChannel; - res.sendStatus(200); - await this.server.storage.set(req.query.t.toString(), true); - return channel.send({ content: `<@${token.staffUserID}>`, embeds: [embed] }); - } catch { - return res.sendStatus(401); - } + this.server.client.util.handleError(error); } }); } diff --git a/src/api/routes/Webhook.ts b/src/api/routes/Webhook.ts deleted file mode 100644 index 276144b..0000000 --- a/src/api/routes/Webhook.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable no-continue */ -import { TextChannel, MessageEmbed, TextBasedChannel } from 'discord.js'; -import { Server } from '..'; -import { Route } from '../../class'; - -export default class Webhook extends Route { - constructor(server: Server) { - super(server, { path: '/wh', deprecated: false, maintenance: false }); - } - - public bind() { - this.router.post('/s1', async (req, res) => { - try { - if (req.headers.authorization !== this.server.security.keys.iv.toString('base64')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.codes.UNAUTHORIZED }); - const embed = new MessageEmbed(); - embed.setTitle('Service Request'); - embed.setDescription(`https://staff.libraryofcode.org/browse/${req.body.key}\n${req.body.url}`); - embed.setColor('#FF00FF'); - embed.addField('Ticket Number', req.body.key, true); - embed.addField('Reporter', req.body.reporter, true); - embed.addField('Status', req.body.status, true); - embed.addField('Summary', req.body.summary); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - const channel = this.server.client.channels.cache.get('780513128240382002') as TextChannel; - channel.send({ content: '<@&741797822940315650>', embeds: [embed] }); - return res.status(200).json({ code: this.constants.codes.SUCCESS, message: this.constants.codes.SUCCESS }); - } catch (err) { - return this.handleError(err, res); - } - }); - - this.router.get('/t3', async (req, res) => { - if (req.query?.auth !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.userID.toString() }); - if (!account) { - return res.sendStatus(404); - } - const tier = await this.server.client.db.Tier.findOne({ id: 3 }); - if (account.tier >= 3) { - return res.sendStatus(200); - } - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: 3, ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: 3 } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> 3`, true); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - await this.server.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier to 3').catch(() => { }); - const channel = this.server.client.channels.cache.get('580950455581147146') as TextChannel; - channel.send({ embeds: [embed] }); - this.server.client.users.cache.get(account.userID).send({ embeds: [embed] }); - return res.sendStatus(200); - }); - - this.router.get('/t3-rm', async (req, res) => { - if (req.query?.auth !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.userID.toString() }); - if (!account) return res.sendStatus(404); - const tier = await this.server.client.db.Tier.findOne({ id: 1 }); - if (account.tier !== 3) return res.sendStatus(200); - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: 1, ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: 1 } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> 1`, true); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - await this.server.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier.').catch(() => { }); - const ch = this.server.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - this.server.client.users.cache.get(account.userID).send({ embeds: [embed] }); - return res.sendStatus(200); - }); - - this.router.get('/t2', async (req, res) => { - if (req.query?.auth !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.userID.toString() }); - if (!account) { - return res.sendStatus(404); - } - const tier = await this.server.client.db.Tier.findOne({ id: 2 }); - if (account.tier >= 2) { - return res.sendStatus(200); - } - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: 2, ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: 2 } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> 2`, true); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - await this.server.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier to 2').catch(() => { }); - const ch = this.server.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - this.server.client.users.cache.get(account.userID).send({ embeds: [embed] }); - return res.sendStatus(200); - }); - - this.router.get('/t2-rm', async (req, res) => { - if (req.query?.auth !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.userID.toString() }); - if (!account) return res.sendStatus(404); - const tier = await this.server.client.db.Tier.findOne({ id: 1 }); - if (account.tier !== 2) return res.sendStatus(200); - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: 1, ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: 1 } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> 1`, true); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - await this.server.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier.').catch(() => { }); - const ch = this.server.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - this.server.client.users.cache.get(account.userID).send({ embeds: [embed] }); - return res.sendStatus(200); - }); - - this.router.get('/set-tier', async (req, res) => { - if (req.query?.auth !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - if (Number(req.query.t.toString()) > 3 || Number(req.query.t.toString()) < 1) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.userID.toString() }); - if (!account) return res.sendStatus(404); - const tier = await this.server.client.db.Tier.findOne({ id: Number(req.query.t.toString()) }); - - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: Number(req.query.t.toString()), ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: Number(req.query.t.toString()) } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> ${Number(req.query.t.toString())}`, true); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL()); - embed.setTimestamp(); - await this.server.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier.').catch(() => { }); - const ch = this.server.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - this.server.client.users.cache.get(account.userID).send({ embeds: [embed] }); - return res.sendStatus(200); - }); - - this.router.get('/info', async (req, res) => { - if (req.query?.authorization !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.id) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.id.toString() }).lean().exec(); - if (!account) return res.status(200).send({ found: false }); - - return res.status(200).send({ - found: true, - emailAddress: account.emailAddress, - tier: account.tier, - supportKey: account.supportKey, - }); - }); - - this.router.get('/score', async (req, res) => { - try { - if (req.query?.authorization !== this.server.security.keys.internal.toString('hex')) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - if (!req.query?.id) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR }); - - const account = await this.server.client.db.Account.findOne({ userID: req.query.id.toString() }).lean().exec(); - const moderations = await this.server.client.db.Moderation.find({ userID: req.query.id.toString() }).lean().exec(); - if (!account && (!moderations || moderations.length <= 0)) return res.status(200).send({ found: false }); - - const response: { - found: boolean, - tier: number, - totalReferrals?: number, - createdAt?: Date, - warns?: Date[], - locks?: Date[], - deletes?: Date[], - } = { - found: true, - tier: account?.tier || 0, - totalReferrals: 0, - createdAt: account?.createdAt || null, - warns: [], - locks: [], - deletes: [], - }; - if (account?.totalReferrals && account?.totalReferrals > 0) response.totalReferrals = account.totalReferrals; - if (moderations.length > 0) { - for (const moderation of moderations) { - if (moderation.reason?.includes('DN/C')) continue; - if (moderation.type === 1) response.warns.push(moderation.date); - if (moderation.type === 2) response.locks.push(moderation.date); - if (moderation.type === 4) response.deletes.push(moderation.date); - } - } - return res.status(200).json(response); - } catch (err) { - return this.handleError(err, res); - } - }); - } -} diff --git a/src/api/static/verify.html b/src/api/static/verify.html deleted file mode 100644 index babca73..0000000 --- a/src/api/static/verify.html +++ /dev/null @@ -1,46 +0,0 @@ - - - Referral Verification - - - - - - - - -
-

Referral Authorization Form

-

This form is for authorizing referral requests that you've provided to other users. If you've received this request from someone you don't recognize, please let us know right away.

-
- -
- -
-
- -
Library of Code sp-us
-
- - - diff --git a/src/class/AccountUtil.ts b/src/class/AccountUtil.ts deleted file mode 100644 index 7b2243c..0000000 --- a/src/class/AccountUtil.ts +++ /dev/null @@ -1,114 +0,0 @@ -import axios from 'axios'; -import moment from 'moment'; -import { randomBytes } from 'crypto'; -import { AccountInterface } from '../models'; -import { Client } from '..'; - -export const LINUX_USERNAME_REGEX = /^[a-z][-a-z0-9]*$/; - -export default class AccountUtil { - public client: Client; - - constructor(client: Client) { - this.client = client; - } - - /** - * This function creates a new user account. - * @param data Data/information on the new user account to create. - * @param data.userID The Discord ID for the user. - * @param data.username The username for the new user, this will also be their username on the machine. - * @param data.emailAddress The user's email address. - * @param moderator The Discord user ID for the Staff member that created the account. - */ - public async createAccount(data: { userID: string, username: string, emailAddress: string }, moderator: string): Promise<{ account: AccountInterface, tempPass: string }> { - const moderatorMember = this.client.guilds.cache.get('446067825673633794').members.cache.get(moderator); - const tempPass = this.client.util.randomPassword(); - const passHash = (await this.client.util.createHash(tempPass)).replace(/[$]/g, '\\$').replace('\n', ''); - const acctName = this.client.users.cache.get(data.userID).username.replace(/[!@#$%^&*(),.?":{}|<>]/g, '-').replace(/\s/g, '-'); - const etcPasswd = `${acctName},${data.userID},,`; - const code = randomBytes(3).toString('hex').toUpperCase(); - - const accountInterface = await this.client.util.createAccount(passHash, etcPasswd, data.username, data.userID, data.emailAddress, moderator, code); - await this.client.util.createModerationLog(data.userID, moderatorMember.user, 0); - const req = await axios.get('https://loc.sh/int/directory'); - const find = req.data.find((mem) => mem.userID === moderator); - - this.client.util.transport.sendMail({ - to: data.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - replyTo: 'cloud-help@libraryofcode.org', - subject: 'Approval for CS Account', - html: ` - - -

Library of Code | Cloud Services

-

Congratulations, your CS Account application has been approved. Welcome! Please see below for some details regarding your account and our services

-

Username: ${data.username}

-

Support Key: ${code} || You may be asked for this support key when contacting Library of Code, please keep the code in a safe area.

-

SSH Login:

ssh ${data.username}@cloud.libraryofcode.org
-

Underwritten by: ${moderatorMember.user.username}, ${find.pn.join(', ')} -

Useful information

-

How to log in:

-
    -
  1. Open your desired terminal application - we recommend using Bash, but you can use your computer's default
  2. -
  3. Type in your SSH Login as above
  4. -
  5. When prompted, enter your password Please note that inputs will be blank, so be careful not to type in your password incorrectly
  6. -
-

If you fail to authenticate yourself too many times, you will be IP banned and will fail to connect. If this is the case, feel free to DM Ramirez with your public IPv4 address. - -

Channels and Links

-
    -
  • Status Page - You can find the status of all our services, including the cloud machine, here.
  • -
  • Wiki - Wiki site, includes information about the Cloud Server.
  • -
  • #cloud-announcements - Announcements regarding the cloud machine will be here. These include planned maintenance, updates to preinstalled services etc.
  • -
  • #cloud-info - Important information you will need to, or should, know to a certain extent.
  • -
  • #cloud-support - A support channel specifically for the cloud machine.
  • -
  • Library of Code Support Desk - Our Support desk, you can contact Staff here.
  • -
-

Want to support us?

-

You can support us by purchasing a Tier 3 subscription! this site for more information.

- Library of Code sp-us | Support Team - - `, - }); - const guild = this.client.guilds.cache.get('446067825673633794'); - const member = guild.members.cache.get(data.userID); - await member.roles.add('546457886440685578'); - const user = this.client.users.cache.get(data.userID); - user.send('<:loc:607695848612167700> **Thank you for creating an account with us!** <:loc:607695848612167700>\n' - + `Please log into your account by running \`ssh ${data.username}@cloud.libraryofcode.org\` in your terminal, then use the password \`${tempPass}\` to log in.\n` - + `You will be asked to change your password, \`(current) UNIX password\` is \`${tempPass}\`, then create a password that is at least 12 characters long, with at least one number, special character, and an uppercase letter\n` - + 'Bear in mind that when you enter your password, it will be blank, so be careful not to type in your password incorrectly.\n\n' - + 'An email containing some useful information has also been sent.\n' - + `Your support key is \`${code}\`. Pin this message, you may need this key to contact Library of Code in the future.`).catch(); - return { account: accountInterface, tempPass }; - } - - public async lock(username: string, moderatorID: string, data?: { reason?: string, time?: number}) { - const account = await this.client.db.Account.findOne({ username }); - if (!account) throw new Error('Account does not exist.'); - if (account.locked) throw new Error('Account is already locked.'); - if (account.username === 'matthew' || account.root) throw new Error('Permission denied.'); - await this.client.util.exec(`lock ${account.username}`); - await account.updateOne({ locked: true }); - - await this.client.util.createModerationLog(account.userID, this.client.users.cache.get(moderatorID), 2, data?.reason, data?.time); - - this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - replyTo: 'cloud-help@libraryofcode.org', - subject: 'Your account has been locked', - html: ` -

Library of Code | Cloud Services

-

Your Cloud Account has been locked until ${data?.time ? moment(data?.time).calendar() : 'indefinitely'} under the EULA.

-

Reason: ${data?.reason ? data.reason : 'none provided'}

-

Technician: ${moderatorID !== this.client.user.id ? (this.client.users.cache.get(moderatorID).username) : 'SYSTEM'}

-

Expiration: ${data?.time ? moment(data?.time).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'N/A'}

- - Library of Code sp-us | Support Team - `, - }); - } -} diff --git a/src/class/CSCLI.ts b/src/class/CSCLI.ts deleted file mode 100644 index c190fdc..0000000 --- a/src/class/CSCLI.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-disable max-classes-per-file */ -/* eslint-disable no-case-declarations */ -/* eslint-disable consistent-return */ -import net from 'net'; -import crypto from 'crypto'; -import { promises as fs } from 'fs'; -import { Client, Collection, Context } from '.'; -import type { Handler } from '.'; - -export default class CSCLI { - public client: Client; - - public servers: { - tcp?: net.Server, - unix?: net.Server, - }; - - public handlers: Collection; - - #hmac: string; - - constructor(client: Client) { - this.client = client; - this.servers = {}; - this.loadKeys(); - this.servers.tcp = net.createServer((socket) => { - socket.on('data', async (data) => { - try { - await this.tcpHandle(socket, data); - } catch (err) { - await this.client.util.handleError(err); - socket.destroy(); - } - }); - }); - this.servers.unix = net.createServer((socket) => { - socket.on('data', async (data) => { - try { - await this.unixHandle(socket, data); - } catch (err) { - await this.client.util.handleError(err); - socket.destroy(); - } - }); - }); - this.init(); - } - - public load(handlerFiles: { [s: string]: typeof Handler; } | ArrayLike) { - this.handlers = new Collection(); - const hdFiles = Object.values(handlerFiles); - for (const Handler1 of hdFiles) { - const handler = new Handler1(); - this.handlers.add(handler.endpoint, handler); - this.client.signale.success(`Successfully loaded endpoint '${handler.endpoint}'.`); - } - } - - public async unixHandle(socket: net.Socket, data: Buffer) { - const args = data.toString().trim().split('$'); - const parsed: { Username: string, Type: string, Message?: string, Data?: any, HMAC: string } = JSON.parse(args[0]); - // FINISH VERIFICATION CHECKS - const handler: Handler = this.handlers.get(parsed.Type); - if (!handler) return socket.destroy(); - - const context = new Context(socket, args[0], this.client); - await handler.handle(context); - if (!context.socket.destroyed) { - socket.destroy(); - } - } - - public async tcpHandle(socket: net.Socket, data: Buffer) { - const args = data.toString().trim().split('$'); - const verification = this.verifyConnection(args[1], args[0]); - if (!verification) { - socket.write('UNAUTHORIZED TO EXECUTE ON THIS SERVER\n'); - return socket.destroy(); - } - const parsed: { Username: string, Type: string, Message?: string, Data?: any, HMAC: string } = JSON.parse(args[0]); - // FINISH VERIFICATION CHECKS - const handler: Handler = this.handlers.get(parsed.Type); - if (!handler) return socket.destroy(); - - const context = new Context(socket, args[0], this.client); - await handler.handle(context); - if (!context.socket.destroyed) { - socket.destroy(); - } - } - - public verifyConnection(key: string, data: any): boolean { - const hmac = crypto.createHmac('sha256', this.#hmac); - hmac.update(data); - const computed = hmac.digest('hex'); - if (computed === key) return true; - return false; - } - - public async loadKeys() { - const key = await fs.readFile('/etc/cscli.conf', { encoding: 'utf8' }); - this.#hmac = key.toString().trim(); - } - - public async init() { - try { - await fs.unlink('/run/csd-comm.sock'); - } catch { - // eslint-disable-next-line no-unused-expressions - null; - } - this.servers.tcp.on('error', (err) => { - this.client.util.handleError(err); - }); - this.servers.unix.on('error', (err) => { - this.client.util.handleError(err); - }); - this.servers.tcp.listen(8124, () => { - this.client.signale.success('[CSD-COMM] Listen - TCP:8124'); - }); - this.servers.unix.listen('/var/run/csd-comm.sock', async () => { - await fs.chmod('/run/csd-comm.sock', 0o770); - await fs.chown('/run/csd-comm.sock', 0, 115); - this.client.signale.success('[CSD-COMM] Listen - UNIX:/run/csd-comm.sock'); - }); - } -} diff --git a/src/class/Client.ts b/src/class/Client.ts deleted file mode 100644 index a273274..0000000 --- a/src/class/Client.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Client as DiscordClient, Intents } from 'discord.js'; -import Redis from 'ioredis'; -import mongoose from 'mongoose'; -import signale from 'signale'; -import fs from 'fs-extra'; -import config from '../config.json'; -import { Account, AccountInterface, Moderation, ModerationInterface, Domain, DomainInterface, Tier, TierInterface } from '../models'; -import { emojis } from '../stores'; -import { Command, CSCLI, Util, Collection, Server, Event } from '.'; - -export default class Client extends DiscordClient { - public config: { 'token': string; 'cloudflare': string; 'prefix': string; 'emailPass': string; 'mongoURL': string; 'port': number; 'keyPair': { 'publicKey': string; 'privateKey': string; }; vendorKey: string; internalKey: string; }; - - public util: Util; - - public commands: Collection; - - public events: Collection; - - public db: { Account: mongoose.Model; Domain: mongoose.Model; Moderation: mongoose.Model; Tier: mongoose.Model; }; - - public redis: Redis.Redis; - - public stores: { emojis: { success: string, loading: string, error: string }; }; - - public functions: Collection; - - public signale: signale.Signale; - - public server: Server; - - public updating: boolean; - - public buildError: boolean - - constructor() { - super({ - shards: 'auto', - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MEMBERS, - Intents.FLAGS.GUILD_BANS, - Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS, - Intents.FLAGS.GUILD_WEBHOOKS, - Intents.FLAGS.GUILD_INVITES, - Intents.FLAGS.GUILD_INTEGRATIONS, - Intents.FLAGS.GUILD_PRESENCES, - Intents.FLAGS.GUILD_MESSAGES, - Intents.FLAGS.GUILD_MESSAGE_REACTIONS, - Intents.FLAGS.DIRECT_MESSAGES, - ], - partials: [ - 'USER', - 'CHANNEL', - 'GUILD_MEMBER', - 'MESSAGE', - 'REACTION', - ], - allowedMentions: { - parse: [ - 'users', - 'roles', - ], - repliedUser: false, - }, - }); - - process.title = 'cloudservices'; - this.config = config; - this.util = new Util(this); - this.commands = new Collection(); - this.events = new Collection(); - this.functions = new Collection(); - this.db = { Account, Domain, Moderation, Tier }; - this.redis = new Redis({ - password: config.redis, - }); - this.stores = { emojis }; - this.signale = signale; - this.signale.config({ - displayDate: true, - displayTimestamp: true, - displayFilename: true, - }); - this.updating = false; - this.buildError = false; - this.errorEvents(); - } - - public async errorEvents() { - process.on('unhandledRejection', (error: Error) => { - this.util.handleError(error); - }); - this.on('error', (error) => { - this.util.handleError(error); - }); - } - - public async loadFunctions() { - const functions = await fs.readdir(`${__dirname}/../functions`); - functions.forEach(async (func) => { - if (func === 'index.ts' || func === 'index.js') return; - try { - const funcRequire: Function = require(`${__dirname}/../functions/${func}`).default; - this.functions.set(func.split('.')[0], funcRequire); - } catch (error) { - this.signale.error(`Error occurred loading ${func}`); - await this.util.handleError(error); - } - }); - } - - public loadCommand(CommandFile: any) { - // eslint-disable-next-line no-useless-catch - try { - const command: Command = new CommandFile(this); - if (command.subcmds.length) { - command.subcmds.forEach((C) => { - const cmd: Command = new C(this); - command.subcommands.add(cmd.name, cmd); - }); - } - delete command.subcmds; - this.commands.add(command.name, command); - this.signale.complete(`Loaded command ${command.name}`); - } catch (err) { throw err; } - } - - public async loadEvents(eventFiles: { [s: string]: typeof Event; } | ArrayLike) { - const evtFiles = Object.entries(eventFiles); - for (const [name, Ev] of evtFiles) { - const event = new Ev(this); - this.events.add(event.event, event); - this.on(event.event, event.run); - this.signale.success(`Successfully loaded event: ${name}`); - delete require.cache[require.resolve(`${__dirname}/../events/${name}`)]; - } - } - - public async loadCommands(commandFiles: { [s: string]: typeof Command; } | ArrayLike) { - const cmdFiles = Object.values(commandFiles); - for (const Cmd of cmdFiles) { - const command = new Cmd(this); - if (command.subcmds.length) { - command.subcmds.forEach((C) => { - const cmd: Command = new C(this); - command.subcommands.add(cmd.name, cmd); - }); - } - delete command.subcmds; - this.commands.add(command.name, command); - this.signale.success(`Successfully loaded command: ${command.name}`); - } - } - - public async init() { - await mongoose.connect(config.mongoURL, { useNewUrlParser: true, useUnifiedTopology: true }); - await this.login(config.token); - this.on('ready', () => { - this.signale.info(`Connected to Discord as ${this.user.username}#${this.user.discriminator}`); - }); - const intervals = await fs.readdir(`${__dirname}/../intervals`); - intervals.forEach((interval) => { - if (interval === 'index.js') return; - require(`${__dirname}/../intervals/${interval}`).default(this); - this.signale.complete(`Loaded interval ${interval.split('.')[0]}`); - }); - this.server = new Server(this, { port: this.config.port }); - - const corepath = '/opt/CloudServices/dist'; - const cmdFiles = await fs.readdir('/opt/CloudServices/dist/commands'); - cmdFiles.forEach((f) => delete require.cache[`${corepath}/${f}`]); - delete require.cache[`${corepath}/config.json`]; - delete require.cache[`${corepath}/class/Util`]; - } -} diff --git a/src/class/Collection.ts b/src/class/Collection.ts index 12b5b02..71e8a07 100644 --- a/src/class/Collection.ts +++ b/src/class/Collection.ts @@ -1,153 +1,155 @@ -/** - * Hold a bunch of something - */ -export default class Collection extends Map { - baseObject: any - - /** - * Creates an instance of Collection - */ - constructor(iterable: any[]|object = null) { - if (iterable && iterable instanceof Array) { - super(iterable); - } else if (iterable && iterable instanceof Object) { - super(Object.entries(iterable)); - } else { - super(); - } - } - - /** - * Map to array - * ```js - * [value, value, value] - * ``` - */ - toArray(): V[] { - return [...this.values()]; - } - - /** - * Map to object - * ```js - * { key: value, key: value, key: value } - * ``` - */ - toObject(): object { - const obj: object = {}; - for (const [key, value] of this.entries()) { - obj[key] = value; - } - return obj; - } - - /** - * Add an object - * - * If baseObject, add only if instance of baseObject - * - * If no baseObject, add - * @param key The key of the object - * @param value The object data - * @param replace Whether to replace an existing object with the same key - * @return The existing or newly created object - */ - add(key: string, value: V, replace: boolean = false): V { - if (this.has(key) && !replace) { - return this.get(key); - } - if (this.baseObject && !(value instanceof this.baseObject)) return null; - - this.set(key, value); - return value; - } - - /** - * Return the first object to make the function evaluate true - * @param func A function that takes an object and returns something - * @return The first matching object, or `null` if no match - */ - find(func: Function): V { - for (const item of this.values()) { - if (func(item)) return item; - } - return null; - } - - /** - * Return an array with the results of applying the given function to each element - * @param callbackfn A function that takes an object and returns something - */ - map(callbackfn: (value?: V, index?: number, array?: V[]) => U): U[] { - const arr = []; - for (const item of this.values()) { - arr.push(callbackfn(item)); - } - return arr; - } - - /** - * Return all the objects that make the function evaluate true - * @param func A function that takes an object and returns true if it matches - */ - filter(func: Function): V[] { - const arr = []; - for (const item of this.values()) { - if (func(item)) { - arr.push(item); - } - } - return arr; - } - - /** - * Test if at least one element passes the test implemented by the provided function. Returns true if yes, or false if not. - * @param func A function that takes an object and returns true if it matches - */ - some(func: Function) { - for (const item of this.values()) { - if (func(item)) { - return true; - } - } - return false; - } - - /** - * Update an object - * @param key The key of the object - * @param value The updated object data - */ - update(key: string, value: V) { - return this.add(key, value, true); - } - - /** - * Remove an object - * @param key The key of the object - * @returns The removed object, or `null` if nothing was removed - */ - remove(key: string): V { - const item = this.get(key); - if (!item) { - return null; - } - this.delete(key); - return item; - } - - /** - * Get a random object from the Collection - * @returns The random object or `null` if empty - */ - random(): V { - if (!this.size) { - return null; - } - return Array.from(this.values())[Math.floor(Math.random() * this.size)]; - } - - toString() { - return `[Collection<${this.baseObject.name}>]`; - } -} +/** + * Hold a bunch of something + */ +export default class Collection extends Map { + baseObject: any + + /** + * Creates an instance of Collection + */ + constructor(iterable: any[]|object = null) { + if (iterable && iterable instanceof Array) { + // @ts-ignore + super(iterable); + } else if (iterable && iterable instanceof Object) { + // @ts-ignore + super(Object.entries(iterable)); + } else { + super(); + } + } + + /** + * Map to array + * ```js + * [value, value, value] + * ``` + */ + toArray(): V[] { + return [...this.values()]; + } + + /** + * Map to object + * ```js + * { key: value, key: value, key: value } + * ``` + */ + toObject(): object { + const obj: object = {}; + for (const [key, value] of this.entries()) { + obj[key] = value; + } + return obj; + } + + /** + * Add an object + * + * If baseObject, add only if instance of baseObject + * + * If no baseObject, add + * @param key The key of the object + * @param value The object data + * @param replace Whether to replace an existing object with the same key + * @return The existing or newly created object + */ + add(key: string, value: V, replace: boolean = false): V { + if (this.has(key) && !replace) { + return this.get(key); + } + if (this.baseObject && !(value instanceof this.baseObject)) return null; + + this.set(key, value); + return value; + } + + /** + * Return the first object to make the function evaluate true + * @param func A function that takes an object and returns something + * @return The first matching object, or `null` if no match + */ + find(func: Function): V { + for (const item of this.values()) { + if (func(item)) return item; + } + return null; + } + + /** + * Return an array with the results of applying the given function to each element + * @param callbackfn A function that takes an object and returns something + */ + map(callbackfn: (value?: V, index?: number, array?: V[]) => U): U[] { + const arr = []; + for (const item of this.values()) { + arr.push(callbackfn(item)); + } + return arr; + } + + /** + * Return all the objects that make the function evaluate true + * @param func A function that takes an object and returns true if it matches + */ + filter(func: Function): V[] { + const arr = []; + for (const item of this.values()) { + if (func(item)) { + arr.push(item); + } + } + return arr; + } + + /** + * Test if at least one element passes the test implemented by the provided function. Returns true if yes, or false if not. + * @param func A function that takes an object and returns true if it matches + */ + some(func: Function) { + for (const item of this.values()) { + if (func(item)) { + return true; + } + } + return false; + } + + /** + * Update an object + * @param key The key of the object + * @param value The updated object data + */ + update(key: string, value: V) { + return this.add(key, value, true); + } + + /** + * Remove an object + * @param key The key of the object + * @returns The removed object, or `null` if nothing was removed + */ + remove(key: string): V { + const item = this.get(key); + if (!item) { + return null; + } + this.delete(key); + return item; + } + + /** + * Get a random object from the Collection + * @returns The random object or `null` if empty + */ + random(): V { + if (!this.size) { + return null; + } + return Array.from(this.values())[Math.floor(Math.random() * this.size)]; + } + + toString() { + return `[Collection<${this.baseObject.name}>]`; + } +} diff --git a/src/class/Command.ts b/src/class/Command.ts index 1185164..e1caa2c 100644 --- a/src/class/Command.ts +++ b/src/class/Command.ts @@ -1,53 +1,42 @@ -import { Message, TextBasedChannels } from 'discord.js'; -import { Client, Collection } from '.'; - -export default class Command { - name: string - - parentName: string - - description?: string - - usage?: string - - enabled: boolean - - aliases?: string[] - - client: Client - - permissions?: { roles?: string[], users?: string[] } - - guildOnly?: boolean - - subcmds?: any[] - - subcommands?: Collection - - public run(message: Message, args: string[]): Promise { return Promise.resolve(); } - - constructor(client: Client) { - this.name = 'None'; - this.description = 'No description given'; - this.usage = 'No usage given'; - this.enabled = true; - this.aliases = []; - this.guildOnly = true; - this.client = client; - this.subcmds = []; - this.subcommands = new Collection(); - this.permissions = {}; - } - - public success(channel: TextBasedChannels, txt: string) { - return channel.send(`***${this.client.stores.emojis.success} ${txt}***`); - } - - public loading(channel: TextBasedChannels, txt: string) { - return channel.send(`***${this.client.stores.emojis.loading} ${txt}***`); - } - - public error(channel: TextBasedChannels, txt: string) { - return channel.send(`***${this.client.stores.emojis.error} ${txt}***`); - } -} +import { Message } from 'eris'; +import { Client } from '..'; +import { Collection } from '.'; + +export default class Command { + name: string + + parentName: string + + description?: string + + usage?: string + + enabled: boolean + + aliases?: string[] + + client: Client + + permissions?: { roles?: string[], users?: string[] } + + guildOnly?: boolean + + subcmds?: any[] + + subcommands?: Collection + + public run(message: Message, args: string[]) {} // eslint-disable-line + + constructor(client: Client) { + this.name = 'None'; + this.description = 'No description given'; + this.usage = 'No usage given'; + this.enabled = true; + this.aliases = []; + this.guildOnly = true; + this.client = client; + this.subcmds = []; + this.subcommands = new Collection(); + this.permissions = {}; + } +} diff --git a/src/class/Context.ts b/src/class/Context.ts deleted file mode 100644 index 100ea45..0000000 --- a/src/class/Context.ts +++ /dev/null @@ -1,39 +0,0 @@ -import net from 'net'; -import { Client } from '.'; - -export default class Context { - public socket: net.Socket; - - public client: Client; - - public data: { - username: string, - endpoint: string, - message?: string, - additionalData?: object, - HMAC: string, - } - - constructor(socket: net.Socket, data: string, client: Client) { - const parsed: { Username: string, Type: string, Message?: string, Data?: object, HMAC: string } = JSON.parse(data); - - this.socket = socket; - this.client = client; - this.data = { - username: parsed.Username, - endpoint: parsed.Type, - message: parsed.Message, - additionalData: parsed.Data, - HMAC: parsed.HMAC, - }; - } - - public send(v: string) { - this.socket.write(`${v.toString()}\n`, (err) => { - if (err) { - this.client.signale.error(`Error occurred while writing: ${err}`); - } - }); - this.socket.destroy(); - } -} diff --git a/src/class/Event.ts b/src/class/Event.ts deleted file mode 100644 index 658f18c..0000000 --- a/src/class/Event.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Client } from '.'; - -export default class Event { - public client: Client - - public event: string; - - constructor(client: Client) { - this.client = client; - this.event = ''; - this.run = this.run.bind(this); - } - - public async run(...args: any[]): Promise { return Promise.resolve(); } -} diff --git a/src/class/Handler.ts b/src/class/Handler.ts deleted file mode 100644 index ad261c3..0000000 --- a/src/class/Handler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Context } from '.'; - -export default class Handler { - public endpoint: string; - - constructor(endpoint?: string) { - this.endpoint = endpoint; - } - - public handle(ctx: Context): Promise { return Promise.resolve(); } -} diff --git a/src/class/LocalStorage.ts b/src/class/LocalStorage.ts deleted file mode 100644 index 5ef9567..0000000 --- a/src/class/LocalStorage.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-disable no-constant-condition */ -import { promises as fs, accessSync, constants, writeFileSync } from 'fs'; -import { promisify } from 'util'; -import { gzip, gzipSync, unzip } from 'zlib'; - -type JSONData = [{key: string, value: any}?]; - -/** - * Persistent local JSON-based storage. - * - auto-locking system to prevent corrupted data - * - uses gzip compression to keep DB storage space utilization low - * @author Matthew - */ -export default class LocalStorage { - protected readonly storagePath: string; - - protected locked: boolean = false; - - constructor(dbName: string, dir = `${__dirname}/../../localstorage`) { - this.storagePath = `${dir}/${dbName}.json.gz`; - this.init(); - } - - private init() { - try { - accessSync(this.storagePath, constants.F_OK); - } catch { - const setup = []; - const data = gzipSync(JSON.stringify(setup)); - writeFileSync(this.storagePath, data); - } - } - - /** - * Compresses data using gzip. - * @param data The data to be compressed. - * ```ts - * await LocalStorage.compress('hello!'); - * ``` - */ - static async compress(data: string): Promise { - const func = promisify(gzip); - const comp = await func(data); - return comp; - } - - /** - * Decompresses data using gzip. - * @param data The data to be decompressed. - * ```ts - * const compressed = await LocalStorage.compress('data'); - * const decompressed = await LocalStorage.decompress(compressed); - * console.log(decompressed); // logs 'data'; - * ``` - */ - static async decompress(data: Buffer): Promise { - const func = promisify(unzip); - const uncomp = await func(data); - return uncomp.toString(); - } - - /** - * Retrieves one data from the store. - * If the store has multiple entries for the same key, this function will only return the first entry. - * ```ts - * await LocalStorage.get('data-key'); - * ``` - * @param key The key for the data entry. - */ - public async get(key: string): Promise { - while (true) { - if (!this.locked) break; - } - this.locked = true; - - const file = await fs.readFile(this.storagePath); - const uncomp = await LocalStorage.decompress(file); - this.locked = false; - const json: JSONData = JSON.parse(uncomp); - const result = json.filter((data) => data.key === key); - if (!result[0]) return null; - return result[0].value; - } - - /** - * Retrieves multiple data keys/values from the store. - * This function will return all of the values matching the key you provided exactly. Use `LocalStorage.get();` if possible. - * ```ts - * await LocalStorage.get('data-key'); - * @param key The key for the data entry. - */ - public async getMany(key: string): Promise<{key: string, value: T}[]> { - while (true) { - if (!this.locked) break; - } - this.locked = true; - - const file = await fs.readFile(this.storagePath); - const uncomp = await LocalStorage.decompress(file); - this.locked = false; - const json: JSONData = JSON.parse(uncomp); - const result = json.filter((data) => data.key === key); - if (result.length < 1) return null; - return result; - } - - /** - * Sets a key/value pair and creates a new data entry. - * @param key The key for the data entry. - * @param value The value for the data entry, can be anything that is valid JSON. - * @param options.override [DEPRECATED] By default, this function will error if the key you're trying to set already exists. Set this option to true to override that setting. - * ```ts - * await LocalStorage.set('data-key', 'test'); - * ``` - */ - public async set(key: string, value: any): Promise { - while (true) { - if (!this.locked) break; - } - this.locked = true; - - const file = await fs.readFile(this.storagePath); - const uncomp = await LocalStorage.decompress(file); - const json: JSONData = JSON.parse(uncomp); - json.push({ key, value }); - const comp = await LocalStorage.compress(JSON.stringify(json)); - await fs.writeFile(this.storagePath, comp); - this.locked = false; - } - - /** - * Deletes the data for the specified key. - * **Warning:** This function will delete ALL matching entries. - * ```ts - * await LocalStorage.del('data-key'); - * ``` - * @param key The key for the data entry. - */ - public async del(key: string): Promise { - while (true) { - if (!this.locked) break; - } - this.locked = true; - - const file = await fs.readFile(this.storagePath); - const uncomp = await LocalStorage.decompress(file); - const json: JSONData = JSON.parse(uncomp); - const filtered = json.filter((data) => data.key !== key); - const comp = await LocalStorage.compress(JSON.stringify(filtered)); - await fs.writeFile(this.storagePath, comp); - this.locked = false; - } -} diff --git a/src/class/PaginationEmbed.ts b/src/class/PaginationEmbed.ts deleted file mode 100644 index b52c52d..0000000 --- a/src/class/PaginationEmbed.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Message, MessageEmbed, EmojiResolvable, User, MessageReaction } from 'discord.js'; - -export default async function PaginationEmbed(message: Message, pages: MessageEmbed[], options?: {leftArrow?: EmojiResolvable, rightArrow?: EmojiResolvable, timeout?: number }) { - // eslint-disable-next-line no-param-reassign - options = { - leftArrow: options?.leftArrow ?? '⬅️', - rightArrow: options?.rightArrow ?? '➡️', - timeout: options?.timeout ?? 120000, - }; - - let pageNumber: number = 0; - - const paginationMessage = await message.channel.send({ content: `Page ${pageNumber + 1} of ${pages.length}`, embeds: [pages[pageNumber]] }); - - await paginationMessage.react(options.leftArrow); - await paginationMessage.react(options.rightArrow); - - const filter = (reaction: MessageReaction, user: User) => { - if ([options.leftArrow, options.rightArrow].includes(reaction.emoji.name) - && !user.bot - && user.id === message.author.id) { - return true; - } - return false; - }; - const reactionCollector = paginationMessage.createReactionCollector({ - filter, - time: options.timeout, - dispose: true, - }); - - reactionCollector.on('collect', (reaction, user) => { - reaction.users.remove(user); - if (reaction.emoji.name === options.leftArrow) { - if (pageNumber > 0) { - pageNumber -= 1; - } else { - pageNumber = pages.length - 1; - } - } else if (reaction.emoji.name === options.rightArrow) { - if (pageNumber + 1 < pages.length) { - pageNumber += 1; - } else { - pageNumber = 0; - } - } - paginationMessage.edit({ content: `Page ${pageNumber + 1} / ${pages.length}`, embeds: [pages[pageNumber]] }); - }); - - reactionCollector.on('end', () => { - if (!paginationMessage.deleted) { - paginationMessage.reactions.removeAll(); - } - }); - return paginationMessage; -} diff --git a/src/class/Report.ts b/src/class/Report.ts deleted file mode 100644 index 2802bb9..0000000 --- a/src/class/Report.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { AxiosError } from 'axios'; -import axios from 'axios'; -import { response } from 'express'; - -export interface SoftReport { - status: 'SUCCESS' | 'UNAUTHORIZED' | 'PERMISSION_DENIED' | 'CLIENT_ERROR' | 'SERVER_ERROR'; - userID?: string; - totalScore?: number; -} - -export interface HardReport extends SoftReport { - activityScore?: number; - roleScore?: number; - moderationScore?: number; - cloudServicesScore?: number; - miscScore?: number; - otherScore?: number; - inquiries?: [ { name: string; date: Date }? ]; -} - -export default class Report { - public static async tier2(userID: string, auth: string) { - try { - const { data } = await axios({ - method: 'get', - url: `https://eds.libraryofcode.org/cs/t2?userID=${userID}&auth=${auth}`, - }); - - return { - status: 'SUCCESS', - decision: data.decision, - }; - } catch (err) { - const error = err; - if (error.response?.status === 404 || error.response.status === 400 || error.response.status === 401) return { status: 'CLIENT_ERROR', decision: 'PRE-DECLINED' }; - return { status: 'SERVER_ERROR', decision: 'PRE-DECLINED' }; - } - } - - public static async getPIN(userID: string, auth: string): Promise<{ status: 'SUCCESS' | 'UNAUTHORIZED' | 'PERMISSION_DENIED' | 'CLIENT_ERROR' | 'SERVER_ERROR'; pin?: number[] }> { - try { - const { data } = await axios({ - method: 'get', - url: `https://loc.sh/int/pin?id=${userID}&auth=${auth}`, - }); - - return { - status: 'SUCCESS', - pin: data.pin, - }; - } catch (err) { - const error = err; - if (error.response?.status === 400) return { status: 'CLIENT_ERROR' }; - if (error.response?.status === 401) return { status: 'UNAUTHORIZED' }; - if (error.response?.status === 403) return { status: 'PERMISSION_DENIED' }; - if ((typeof error.response?.status === 'number') && error.response?.status >= 500) return { status: 'SERVER_ERROR' }; - throw new Error(err); - } - } - - /** - * Requests a Soft Inquiry from Library of Code sp-us Community Relations. - * @author Matthew R - * @param userID The user's ID, they must be in the server. Reports usually aren't generated for new users until the next recalculation. - * @param pin The last 4 digits of the member's PIN number. - * ```ts - * Report.soft('253600545972027394', 1102); - * ``` - */ - public static async soft(userID: string, pin: number, auth: string): Promise { - try { - if (pin < 4) throw new RangeError('PIN cannot be less than 4.'); - const { data } = await axios({ - method: 'post', - url: 'https://comm.libraryofcode.org/report/soft', - headers: { Authorization: auth }, - data: { - userID, - pin, - }, - }); - - return { - status: 'SUCCESS', - userID: data.message.userID, - totalScore: data.message.totalScore, - }; - } catch (err) { - const error = err; - if (error.response?.status === 400) return { status: 'CLIENT_ERROR' }; - if (error.response?.status === 401) return { status: 'UNAUTHORIZED' }; - if (error.response?.status === 403) return { status: 'PERMISSION_DENIED' }; - if ((typeof error.response?.status === 'number') && error.response?.status >= 500) return { status: 'SERVER_ERROR' }; - throw new Error(err); - } - } - - /** - * Requests a Hard Inquiry from Library of Code sp-us Community Relations. - * - Members who elected to be notified for hard pulls will receive a notification if your request is successful. - * - If the member's Community Report is locked, `HardReport.status` will equal `PERMISSION_DENIED`. - * @author Matthew R - * @param userID The user's ID, they must be in the server. Reports usually aren't generated for new users until the next recalculation. - * @param pin The last 4 digits of the member's PIN number. - * @param reason A reason for the hard inquiry. - * ```ts - * Report.hard('253600545972027394', 1102, 'Verification and Eligibility for Personal Account'); - * ``` - */ - public static async hard(userID: string, pin: number, reason: string, auth: string): Promise { - try { - if (pin < 4) throw new RangeError('PIN cannot be less than 4.'); - const { data } = await axios({ - method: 'post', - url: 'https://comm.libraryofcode.org/report/hard', - headers: { Authorization: auth }, - data: { - userID, - pin, - reason, - }, - }); - - return { - status: 'SUCCESS', - userID: data.message.userID, - totalScore: data.message.totalScore, - activityScore: data.message.activityScore, - roleScore: data.message.rolesScore, - moderationScore: data.message.moderationScore, - cloudServicesScore: data.message.cloudServicesScore, - miscScore: data.message.miscScore, - otherScore: data.message.otherScore, - inquiries: data.message.inquiries, - }; - } catch (err) { - const error = err; - if (error.response?.status === 400) return { status: 'CLIENT_ERROR' }; - if (error.response?.status === 401) return { status: 'UNAUTHORIZED' }; - if (error.response?.status === 403) return { status: 'PERMISSION_DENIED' }; - if ((typeof error.response?.status === 'number') && error.response?.status >= 500) return { status: 'SERVER_ERROR' }; - throw new Error(err); - } - } -} diff --git a/src/class/RichEmbed.ts b/src/class/RichEmbed.ts new file mode 100644 index 0000000..49d798d --- /dev/null +++ b/src/class/RichEmbed.ts @@ -0,0 +1,176 @@ +/* eslint-disable no-param-reassign */ + +export default class RichEmbed { + title?: string + + type?: string + + description?: string + + url?: string + + timestamp?: Date + + color?: number + + footer?: { text: string, icon_url?: string, proxy_icon_url?: string} + + image?: { url?: string, proxy_url?: string, height?: number, width?: number } + + thumbnail?: { url?: string, proxy_url?: string, height?: number, width?: number } + + video?: { url?: string, height?: number, width?: number } + + provider?: { name?: string, url?: string} + + author?: { name?: string, url?: string, proxy_icon_url?: string, icon_url?: string} + + fields?: {name: string, value: string, inline?: boolean}[] + + constructor(data: { + title?: string, type?: string, description?: string, url?: string, timestamp?: Date, color?: number, fields?: {name: string, value: string, inline?: boolean}[] + footer?: { text: string, icon_url?: string, proxy_icon_url?: string}, image?: { url?: string, proxy_url?: string, height?: number, width?: number }, + thumbnail?: { url?: string, proxy_url?: string, height?: number, width?: number }, video?: { url?: string, height?: number, width?: number }, + provider?: { name?: string, url?: string}, author?: { name?: string, url?: string, proxy_icon_url?: string, icon_url?: string}, + } = {}) { + /* + let types: { + title?: string, type?: string, description?: string, url?: string, timestamp?: Date, color?: number, fields?: {name: string, value: string, inline?: boolean}[] + footer?: { text: string, icon_url?: string, proxy_icon_url?: string}, image?: { url?: string, proxy_url?: string, height?: number, width?: number }, + thumbnail?: { url?: string, proxy_url?: string, height?: number, width?: number }, video?: { url?: string, height?: number, width?: number }, + provider?: { name?: string, url?: string}, author?: { name?: string, url?: string, proxy_icon_url?: string, icon_url?: string} + }; + */ + this.title = data.title; + this.description = data.description; + this.url = data.url; + this.color = data.color; + this.author = data.author; + this.timestamp = data.timestamp; + this.fields = data.fields || []; + this.thumbnail = data.thumbnail; + this.image = data.image; + this.footer = data.footer; + } + + /** + * Sets the title of this embed. + */ + setTitle(title: string) { + if (typeof title !== 'string') throw new TypeError('RichEmbed titles must be a string.'); + if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); + this.title = title; + return this; + } + + /** + * Sets the description of this embed. + */ + setDescription(description: string) { + if (typeof description !== 'string') throw new TypeError('RichEmbed descriptions must be a string.'); + if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); + this.description = description; + return this; + } + + /** + * Sets the URL of this embed. + */ + setURL(url: string) { + if (typeof url !== 'string') throw new TypeError('RichEmbed URLs must be a string.'); + if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`; + this.url = url; + return this; + } + + /** + * Sets the color of this embed. + */ + setColor(color: string | number) { + if (typeof color === 'string' || typeof color === 'number') { + if (typeof color === 'string') { + const regex = /[^a-f0-9]/gi; + color = color.replace(/#/g, ''); + if (regex.test(color)) throw new RangeError('Hexadecimal colours must not contain characters other than 0-9 and a-f.'); + color = parseInt(color, 16); + } else if (color < 0 || color > 16777215) throw new RangeError('Base 10 colours must not be less than 0 or greater than 16777215.'); + this.color = color; + return this; + } + throw new TypeError('RichEmbed colours must be hexadecimal as string or number.'); + } + + /** + * Sets the author of this embed. + */ + setAuthor(name: string, icon_url?: string, url?: string) { + if (typeof name !== 'string') throw new TypeError('RichEmbed Author names must be a string.'); + if (url && typeof url !== 'string') throw new TypeError('RichEmbed Author URLs must be a string.'); + if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Author icons must be a string.'); + this.author = { name, icon_url, url }; + return this; + } + + /** + * Sets the timestamp of this embed. + */ + setTimestamp(timestamp = new Date()) { + // eslint-disable-next-line no-restricted-globals + if (isNaN(timestamp.getTime())) throw new TypeError('Expecting ISO8601 (Date constructor)'); + this.timestamp = timestamp; + return this; + } + + /** + * Adds a field to the embed (max 25). + */ + addField(name: string, value: string, inline = false) { + if (typeof name !== 'string') throw new TypeError('RichEmbed Field names must be a string.'); + if (typeof value !== 'string') throw new TypeError('RichEmbed Field values must be a string.'); + if (typeof inline !== 'boolean') throw new TypeError('RichEmbed Field inlines must be a boolean.'); + if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); + this.fields.push({ name, value, inline }); + return this; + } + + /** + * Convenience function for `.addField('\u200B', '\u200B', inline)`. + */ + addBlankField(inline = false) { + return this.addField('\u200B', '\u200B', inline); + } + + /** + * Set the thumbnail of this embed. + */ + setThumbnail(url: string) { + if (typeof url !== 'string') throw new TypeError('RichEmbed Thumbnail URLs must be a string.'); + this.thumbnail = { url }; + return this; + } + + /** + * Set the image of this embed. + */ + setImage(url: string) { + if (typeof url !== 'string') throw new TypeError('RichEmbed Image URLs must be a string.'); + if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`; + this.image = { url }; + return this; + } + + /** + * Sets the footer of this embed. + */ + setFooter(text: string, icon_url?: string) { + if (typeof text !== 'string') throw new TypeError('RichEmbed Footers must be a string.'); + if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Footer icon URLs must be a string.'); + if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); + this.footer = { text, icon_url }; + return this; + } +} diff --git a/src/class/Route.ts b/src/class/Route.ts index 5d9d0c1..d15f936 100644 --- a/src/class/Route.ts +++ b/src/class/Route.ts @@ -1,6 +1,6 @@ /* eslint-disable consistent-return */ import { Request, Response, NextFunction, Router as router } from 'express'; -import { Server } from '.'; +import { Server } from '../api'; export default class Route { public server: Server; @@ -9,12 +9,7 @@ export default class Route { public conf: { path: string, deprecated?: boolean, maintenance?: boolean }; - protected constructor(server: Server, conf: { path: string, deprecated?: boolean, maintenance?: boolean }) { - this.conf = { - path: null, - deprecated: false, - maintenance: false, - }; + constructor(server: Server, conf: { path: string, deprecated?: boolean, maintenance?: boolean }) { this.server = server; this.router = router(); this.conf = conf; @@ -34,17 +29,8 @@ export default class Route { }); } - public init(): void { - this.router.all('*', (req, res, next) => { - this.server.client.signale.log(`'${req.method}' request from '${req.ip}' to '${req.hostname}${req.path}'.`); - if (this.conf.maintenance === true) res.status(503).json({ code: this.constants.codes.MAINTENANCE_OR_UNAVAILABLE, message: this.constants.messages.MAINTENANCE_OR_UNAVAILABLE }); - else if (this.conf.deprecated === true) res.status(501).json({ code: this.constants.codes.DEPRECATED, message: this.constants.messages.DEPRECATED }); - else next(); - }); - } - /** - * This function checks for the presence of a Bearer token with Security.extractBearer(), + * This function checks for the presense of a Bearer token with Security.extractBearer(), * then it will attempt to validate it with Security.checkBearer(). * If it can authenticate the request, it'll add a custom property on Request called * `account`, which will hold an the bearer token's account owner. The account is of the @@ -75,76 +61,22 @@ export default class Route { get constants() { return { codes: { - /** - * SUCCESS 100 - * Used if the request was processed successfully. - */ SUCCESS: 100, - /** - * UNAUTHORIZED 101 - * Used if the client calling the request couldn't be correctly authenticated. - */ UNAUTHORIZED: 101, - /** - * PERMISSION DENIED 103 - * Used if the client calling the request doesn't have access to the resource specified. - */ - PERMISSION_DENIED: 103, - /** - * NOT FOUND 104 - * Used if the resource the client requested doesn't exist. - */ + PERMISSION_DENIED: 104, NOT_FOUND: 104, - /** - * ACCOUNT NOT FOUND 1041 - * Used if the account specified by the client couldn't be found. - */ ACCOUNT_NOT_FOUND: 1041, - /** - * CLIENT ERROR 1044 - * Used in cases of user error. Examples are incorrect parameters, incorrect headers, or an invalid request. - */ CLIENT_ERROR: 1044, - /** - * SERVER ERROR 105 - * Used in cases of an internal error that caused the bind() function to throw. - */ SERVER_ERROR: 105, - /** - * DEPRECATED 1051 - * Returned back to the user if the resource requested is deprecated. - */ DEPRECATED: 1051, - /** - * MAINTENANCE OR UNAVAILABLE 1053 - * Used if the resource requested is currently in maintenance, not finished, or temporarily disabled. - */ MAINTENANCE_OR_UNAVAILABLE: 1053, }, messages: { - /** - * The credentials you supplied are invalid. - */ UNAUTHORIZED: ['CREDENTIALS_INVALID', 'The credentials you supplied are invalid.'], - /** - * You do not have valid credentials to access this resource. - */ PERMISSION_DENIED: ['PERMISSION_DENIED', 'You do not have valid credentials to access this resource.'], - /** - * The resource you requested cannot be located. - */ NOT_FOUND: ['NOT_FOUND', 'The resource you requested cannot be located.'], - /** - * An internal error has occurred, Engineers have been notified. - */ SERVER_ERROR: ['INTERNAL_ERROR', 'An internal error has occurred, Engineers have been notified.'], - /** - * The endpoint or resource you\'re trying to access has been deprecated. - */ DEPRECATED: ['ENDPOINT_OR_RESOURCE_DEPRECATED', 'The endpoint or resource you\'re trying to access has been deprecated.'], - /** - * The endpoint or resource you\'re trying to access is either in maintenance or is not available. - */ MAINTENANCE_OR_UNAVAILABLE: ['SERVICE_UNAVAILABLE', 'The endpoint or resource you\'re trying to access is either in maintenance or is not available.'], }, }; diff --git a/src/class/Util.ts b/src/class/Util.ts index 4bf3246..c51c484 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -1,334 +1,249 @@ -/* eslint-disable import/no-unresolved */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-param-reassign */ -import axios from 'axios'; -import { randomBytes } from 'crypto'; -import childProcess from 'child_process'; -import nodemailer from 'nodemailer'; -import { Message, DMChannel, User, MessageEmbed, ColorResolvable, TextChannel } from 'discord.js'; -import { v4 as uuid } from 'uuid'; -import moment from 'moment'; -import fs from 'fs'; -import hastebin from 'hastebin-gen'; -import { getUserByUid } from '../functions'; -import { AccountUtil, Client, Command, PaginationEmbed } from '.'; -import { ModerationInterface, AccountInterface } from '../models'; -import { Certificate } from '../../types/x509'; - -export default class Util { - public client: Client; - - public accounts: AccountUtil; - - public transport: nodemailer.Transporter; - - constructor(client: Client) { - this.client = client; - this.transport = nodemailer.createTransport({ - host: 'staff.libraryofcode.org', - auth: { user: 'support', pass: this.client.config.emailPass }, - }); - this.accounts = new AccountUtil(client); - } - - /** - * Executes a terminal command async. - * @param command The command to execute - * @param options childProcess.ExecOptions - */ - public async exec(command: string, options: childProcess.ExecOptions = {}): Promise { - return new Promise((res, rej) => { - let output = ''; - const writeFunction = (data: string|Buffer|Error) => { - output += `${data}`; - }; - const cmd = childProcess.exec(command, options); - cmd.stdout.on('data', writeFunction); - cmd.stderr.on('data', writeFunction); - cmd.on('error', writeFunction); - cmd.once('close', (code, signal) => { - cmd.stdout.off('data', writeFunction); - cmd.stderr.off('data', writeFunction); - cmd.off('error', writeFunction); - setTimeout(() => {}, 1000); - if (code !== 0) rej(new Error(`Command failed: ${command}\n${output}`)); - res(output); - }); - }); - /* return new Promise((resolve, reject) => { - childProcess.exec(command, options, (err, stdout, stderr) => { - if (stderr) reject(new Error(`Command failed: ${command}\n${stderr}`)); - if (err) reject(err); - resolve(stdout); - }); - }); */ - } - - public sanitize(str: string): string { - let ret = str; - ret = ret.replace('\\', '~'); - ret = ret.replace('.', '~'); - ret = ret.replace(/\W|_/g, '~'); - return ret; - } - - public async sendMessageToUserTerminal(username: string, message: string): Promise { - const ptsArray = await this.getPTS(username); - if (!ptsArray) return false; - for (const pts of ptsArray) { - const msg = `\n==SYSTEM NOTIFICATION | CLOUD SERVICES MANAGEMENT DAEMON==\n${new Date().toLocaleString('en-us')}\n\n${message}\n`; - await this.exec(`echo "${msg}" >> /dev/pts/${pts}`); - } - return true; - } - - private async getPTS(username: string): Promise { - const dir = await fs.promises.readdir('/dev/pts'); - const returnArray: number[] = []; - for (const file of dir) { - const fileInformation = await fs.promises.stat(`/dev/pts/${file}`); - const user = await getUserByUid(this.client, fileInformation.uid); - if (user && user === username) { - returnArray.push(Number(file)); - } - } - if (returnArray.length < 1) return undefined; - return returnArray; - } - - /** - * Resolves a command - * @param query Command input - * @param message Only used to check for errors - */ - public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { - try { - let resolvedCommand: Command; - if (typeof query === 'string') query = query.split(' '); - const commands = this.client.commands.toArray(); - resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); - - if (!resolvedCommand) return Promise.resolve(null); - query.shift(); - while (resolvedCommand.subcommands.size && query.length) { - const subCommands = resolvedCommand.subcommands.toArray(); - const found = subCommands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); - if (!found) break; - resolvedCommand = found; - query.shift(); - } - return Promise.resolve({ cmd: resolvedCommand, args: query }); - } catch (error) { - if (message) this.handleError(error, message); - else this.handleError(error); - return Promise.reject(error); - } - } - - public async handleError(error: Error | any, message?: Message, command?: Command): Promise { - try { - this.client.signale.error(error); - const info = { content: `\`\`\`js\n${error.stack}\n\`\`\``, embed: null }; - if (message) { - const embed = new MessageEmbed(); - embed.setColor('#FF0000'); - embed.setAuthor(`Error caused by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL()); - embed.setTitle('Message content'); - embed.setDescription(message.content); - embed.addField('User', `${message.author.toString()} (\`${message.author.id}\`)`, true); - embed.addField('Channel', `<#${message.channel.id}>`, true); - let guild: string; - if (message.channel instanceof DMChannel) guild = '@me'; - else guild = message.guild.id; - embed.addField('Message link', `[Click here](https://discordapp.com/channels/${guild}/${message.channel.id}/${message.id})`, true); - embed.setTimestamp(new Date(message.createdTimestamp)); - info.embed = embed; - } - const ch = this.client.channels.cache.get('595788220764127272') as TextChannel; - await ch.send(info); - const msg = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g); - if (command) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; }); - if (message) message.channel.send(`***${this.client.stores.emojis.error} An unexpected error has occurred - please contact a member of the Engineering Team.${command ? ' This command has been disabled.' : ''}***`); - } catch (err) { - this.client.signale.error(err); - } - } - - public createPaginationEmbed = PaginationEmbed; - - public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { - let index = 0; - const array: {name: string, value: string, inline?: boolean}[][] = [[]]; - while (fields.length) { - if (array[index].length >= 25) { index += 1; array[index] = []; } - array[index].push(fields[0]); fields.shift(); - } - return array; - } - - public splitString(string: string, length: number): string[] { - if (!string) return []; - if (Array.isArray(string)) string = string.join('\n'); - if (string.length <= length) return [string]; - const arrayString: string[] = []; - let str: string = ''; - let pos: number; - while (string.length > 0) { - pos = string.length > length ? string.lastIndexOf('\n', length) : string.length; - if (pos > length) pos = length; - str = string.substr(0, pos); - string = string.substr(pos); - arrayString.push(str); - } - return arrayString; - } - - public async createHash(password: string): Promise { - const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`); - return hashed; - } - - public isValidEmail(email: string): boolean { - const checkAt = email.indexOf('@'); - if (checkAt < 1) return false; - const checkDomain = email.indexOf('.', checkAt + 2); - if (checkDomain < checkAt) return false; - return true; - } - - public randomPassword(): string { - let tempPass = ''; const passChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; - while (tempPass.length < 5) { tempPass += passChars[Math.floor(Math.random() * passChars.length)]; } - return tempPass; - } - - public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string, code: string): Promise { - await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/bash ${username}`); - await this.exec(`chage -d0 ${username}`); - const tier = await this.client.db.Tier.findOne({ id: 1 }); - - const account = new this.client.db.Account({ - username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, tier: 1, supportKey: code, totalReferrals: 0, referralCode: randomBytes(9).toString('base64').toUpperCase(), ssInit: false, ramLimitNotification: tier.resourceLimits.ram - 50, homepath: `/home/${username}`, - }); - return account.save(); - } - - public async deleteAccount(username: string): Promise { - const account = await this.client.db.Account.findOne({ username }); - if (!account) throw new Error('Account not found'); - this.exec(`lock ${username}`); - const tasks = [ - this.exec(`deluser ${username} --remove-home --backup-to /management/Archives && rm -rf -R ${account.homepath}`), - this.client.db.Account.deleteOne({ username }), - ]; - const guild = this.client.guilds.cache.get('446067825673633794'); - const member = await guild.members.fetch(account.userID); - await member.roles.remove('546457886440685578', 'Cloud Account Deleted'); - // @ts-ignore - await Promise.all(tasks); - } - - public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean|void => {}): Promise { - const msg = await message.channel.send(question); - return new Promise((res, rej) => { - const func = (Msg: Message) => { - if (filter(Msg) === false) return; - const verif = choices ? choices.includes(Msg.content) : Msg.content; - if (verif) { if (shouldDelete) msg.delete().catch(); res(Msg); } - }; - - setTimeout(() => { - if (shouldDelete) msg.delete().catch(); rej(new Error('Did not supply a valid input in time')); - this.client.removeListener('messageCreate', func); - }, timeout); - this.client.on('messageCreate', func); - }); - } - - /** - * @param type `0` - Create - * - * `1` - Warn - * - * `2` - Lock - * - * `3` - Unlock - * - * `4` - Delete - */ - public async createModerationLog(user: string, moderator: User, type: number, reason?: string, duration?: number): Promise { - const moderatorID = moderator.id; - const account = await this.client.db.Account.findOne({ $or: [{ username: user }, { userID: user }] }); - if (!account) return Promise.reject(new Error(`Account ${user} not found`)); - const { username, userID } = account; - const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean }} = { - username, userID, logID: uuid(), moderatorID, type, date: new Date(), - }; - - const now: number = Date.now(); - let date: Date; - let processed = true; - if (reason) logInput.reason = reason; - if (type === 2) { - if (duration) { - date = new Date(now + duration); - processed = false; - } else date = null; - } - - const expiration = { date, processed }; - - logInput.expiration = expiration; - const log = new this.client.db.Moderation(logInput); - await log.save(); - - let embedTitle: string; - let color: ColorResolvable; - let archType: string; - switch (type) { - default: archType = 'Staff'; embedTitle = 'Cloud Account | Generic'; color = '#0892e1'; break; - case 0: archType = 'Technician'; embedTitle = 'Cloud Account | Create'; color = '#00ff00'; break; - case 1: archType = 'Technician'; embedTitle = 'Account Warning | Warn'; color = '#ffff00'; break; - case 2: archType = 'Technician'; embedTitle = 'Account Infraction | Lock'; color = '#ff6600'; break; - case 3: archType = 'Technician'; embedTitle = 'Account Reclaim | Unlock'; color = '#0099ff'; break; - case 4: archType = 'Director'; embedTitle = 'Cloud Account | Delete'; color = '#ff0000'; break; - } - const req = await axios.get('https://loc.sh/int/directory'); - const find = req.data.find((mem) => mem.userID === moderator.id); - const embed = new MessageEmbed() - .setTitle(embedTitle) - .setColor(color) - .addField('User', `${username} | <@${userID}>`, true) - .addField(archType, moderatorID === this.client.user.id ? 'SYSTEM' : `${moderator.username}, ${find.pn.join(', ')} (<@${moderatorID}>)`, true) - .setFooter(this.client.user.username, this.client.user.avatarURL()) - .setTimestamp(); - if (reason) embed.addField('Reason', reason || 'Not specified'); - if (type === 2) embed.addField('Lock Expiration', `${date ? moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'Indefinitely'}`); - const ch = this.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - this.client.users.fetch(userID).then((channel) => channel.send({ embeds: [embed] })).catch(); - - return Promise.resolve(log); - } - - public async getTechnicianFullName(tech: User) { - if (!tech) throw new Error('\'tech\' is undefined.'); - if (tech.id === this.client.user.id) return 'SYSTEM'; - - const req = await axios.get('https://loc.sh/int/directory'); - const find = req.data.find((mem) => mem.userID === tech.id); - return `${tech.username}${find.isManager ? '[k]' : ''}${find.title ? ` (${find.title} / ${find.dept})` : ` (${find.dept})`} <<@${tech.id}>>`; - } - - public parseCertificate(pem: string) { - return axios.post('https://certapi.libraryofcode.org/parse', pem) - .then((response) => response.data); - } - - public upload(text: string, extension = 'txt') { - return hastebin(text, { - url: 'https://snippets.cloud.libraryofcode.org', - extension, - }); - } -} +/* eslint-disable no-param-reassign */ +import { promisify } from 'util'; +import childProcess from 'child_process'; +import nodemailer from 'nodemailer'; +import { Message, PrivateChannel, GroupChannel, Member, User } from 'eris'; +import uuid from 'uuid/v4'; +import moment from 'moment'; +import fs from 'fs'; +import os from 'os'; +import { Client } from '..'; +import { Command, RichEmbed } from '.'; +import { ModerationInterface, AccountInterface } from '../models'; + +export default class Util { + public client: Client; + + public transport: nodemailer.Transporter; + + constructor(client: Client) { + this.client = client; + this.transport = nodemailer.createTransport({ + host: 'staff.libraryofcode.org', + auth: { user: 'support', pass: this.client.config.emailPass }, + }); + } + + public async exec(command: string): Promise { + const ex = promisify(childProcess.exec); + let result: string; + // eslint-disable-next-line no-useless-catch + try { + const res = await ex(command); + result = res.stderr || res.stdout; + } catch (err) { + return Promise.reject(new Error(`Command failed: ${err.cmd}\n${err.stderr || err.stdout}`)); + } + return result; + } + + /** + * Resolves a command + * @param query Command input + * @param message Only used to check for errors + */ + public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { + try { + let resolvedCommand: Command; + if (typeof query === 'string') query = query.split(' '); + const commands = this.client.commands.toArray(); + resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); + + if (!resolvedCommand) return Promise.resolve(null); + query.shift(); + while (resolvedCommand.subcommands.size && query.length) { + const subCommands = resolvedCommand.subcommands.toArray(); + const found = subCommands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); + if (!found) break; + resolvedCommand = found; + query.shift(); + } + return Promise.resolve({ cmd: resolvedCommand, args: query }); + } catch (error) { + if (message) this.handleError(error, message); + else this.handleError(error); + return Promise.reject(error); + } + } + + public async handleError(error: Error, message?: Message, command?: Command): Promise { + try { + this.client.signale.error(error); + const info = { content: `\`\`\`js\n${error.stack}\n\`\`\``, embed: null }; + if (message) { + const embed = new RichEmbed(); + embed.setColor('FF0000'); + embed.setAuthor(`Error caused by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTitle('Message content'); + embed.setDescription(message.content); + embed.addField('User', `${message.author.mention} (\`${message.author.id}\`)`, true); + embed.addField('Channel', message.channel.mention, true); + let guild: string; + if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) guild = '@me'; + else guild = message.channel.guild.id; + embed.addField('Message link', `[Click here](https://discordapp.com/channels/${guild}/${message.channel.id}/${message.id})`, true); + embed.setTimestamp(new Date(message.timestamp)); + info.embed = embed; + } + await this.client.createMessage('595788220764127272', info); + const msg = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g); + if (command) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; }); + if (message) message.channel.createMessage(`***${this.client.stores.emojis.error} An unexpected error has occured - please contact a member of the Engineering Team.${command ? ' This command has been disabled.' : ''}***`); + } catch (err) { + this.client.signale.error(err); + } + } + + public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { + let index = 0; + const array: {name: string, value: string, inline?: boolean}[][] = [[]]; + while (fields.length) { + if (array[index].length >= 25) { index += 1; array[index] = []; } + array[index].push(fields[0]); fields.shift(); + } + return array; + } + + public splitString(string: string, length: number): string[] { + if (!string) return []; + if (Array.isArray(string)) string = string.join('\n'); + if (string.length <= length) return [string]; + const arrayString: string[] = []; + let str: string = ''; + let pos: number; + while (string.length > 0) { + pos = string.length > length ? string.lastIndexOf('\n', length) : string.length; + if (pos > length) pos = length; + str = string.substr(0, pos); + string = string.substr(pos); + arrayString.push(str); + } + return arrayString; + } + + + public async createHash(password: string): Promise { + const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`); + return hashed; + } + + public isValidEmail(email: string): boolean { + const checkAt = email.indexOf('@'); + if (checkAt < 1) return false; + const checkDomain = email.indexOf('.', checkAt + 2); + if (checkDomain < checkAt) return false; + return true; + } + + public randomPassword(): string { + let tempPass = ''; const passChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; + while (tempPass.length < 5) { tempPass += passChars[Math.floor(Math.random() * passChars.length)]; } + return tempPass; + } + + public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string): Promise { + await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/zsh ${username}`); + await this.exec(`chage -d0 ${username}`); + + const account = new this.client.db.Account({ + username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, ssInit: false, homepath: `/home/${username}`, + }); + return account.save(); + } + + public async deleteAccount(username: string): Promise { + const account = await this.client.db.Account.findOne({ username }); + if (!account) throw new Error('Account not found'); + this.exec(`lock ${username}`); + const tasks = [ + this.exec(`deluser ${username} --remove-home --backup-to /management/Archives && rm -rf -R ${account.homepath}`), + this.client.db.Account.deleteOne({ username }), + ]; + this.client.removeGuildMemberRole('446067825673633794', account.userID, '546457886440685578', 'Cloud Account Deleted').catch(); + // @ts-ignore + await Promise.all(tasks); + } + + public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean|void => {}): Promise { + const msg = await message.channel.createMessage(question); + return new Promise((res, rej) => { + setTimeout(() => { if (shouldDelete) msg.delete().catch(); rej(new Error('Did not supply a valid input in time')); }, timeout); + this.client.on('messageCreate', (Msg) => { + if (filter(Msg) === false) return; + const verif = choices ? choices.includes(Msg.content) : Msg.content; + if (verif) { if (shouldDelete) msg.delete().catch(); res(Msg); } + }); + }); + } + + /** + * @param type `0` - Create + * + * `1` - Warn + * + * `2` - Lock + * + * `3` - Unlock + * + * `4` - Delete + */ + public async createModerationLog(user: string, moderator: Member|User, type: number, reason?: string, duration?: number): Promise { + const moderatorID = moderator.id; + const account = await this.client.db.Account.findOne({ $or: [{ username: user }, { userID: user }] }); + if (!account) return Promise.reject(new Error(`Account ${user} not found`)); + const { username, userID } = account; + const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean }} = { + username, userID, logID: uuid(), moderatorID, type, date: new Date(), + }; + + const now: number = Date.now(); + let date: Date; + let processed = true; + if (reason) logInput.reason = reason; + if (type === 2) { + if (duration) { + date = new Date(now + duration); + processed = false; + } else date = null; + } + + const expiration = { date, processed }; + + logInput.expiration = expiration; + const log = new this.client.db.Moderation(logInput); + await log.save(); + + let embedTitle: string; + let color: string; + let archType: string; + switch (type) { + default: archType = 'Staff'; embedTitle = 'Cloud Account | Generic'; color = '0892e1'; break; + case 0: archType = 'Administrator'; embedTitle = 'Cloud Account | Create'; color = '00ff00'; break; + case 1: archType = 'Staff'; embedTitle = 'Account Warning | Warn'; color = 'ffff00'; break; + case 2: archType = 'Moderator'; embedTitle = 'Account Infraction | Lock'; color = 'ff6600'; break; + case 3: archType = 'Moderator'; embedTitle = 'Account Reclaim | Unlock'; color = '0099ff'; break; + case 4: archType = 'Administrator'; embedTitle = 'Cloud Account | Delete'; color = 'ff0000'; break; + } + const embed = new RichEmbed() + .setTitle(embedTitle) + .setColor(color) + .addField('User', `${username} | <@${userID}>`, true) + .addField(archType, moderatorID === this.client.user.id ? 'SYSTEM' : `<@${moderatorID}>`, true) + .setFooter(this.client.user.username, this.client.user.avatarURL) + .setTimestamp(); + if (reason) embed.addField('Reason', reason || 'Not specified'); + if (type === 2) embed.addField('Lock Expiration', `${date ? moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'Indefinitely'}`); + // @ts-ignore + this.client.createMessage('580950455581147146', { embed }); this.client.getDMChannel(userID).then((channel) => channel.createMessage({ embed })).catch(); + + return Promise.resolve(log); + } + + public getAcctHash(userpath: string) { + try { + return fs.readFileSync(`${userpath}/.securesign/auth`).toString(); + } catch (error) { + return null; + } + } +} diff --git a/src/class/index.ts b/src/class/index.ts index ecad702..a85412e 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -1,15 +1,5 @@ -export { default as AccountUtil } from './AccountUtil'; -export { default as Client } from './Client'; -export { default as Collection } from './Collection'; export { default as Command } from './Command'; -export { default as Context } from './Context'; -export { default as CSCLI } from './CSCLI'; -export { default as Event } from './Event'; -export { default as Handler } from './Handler'; -export { default as LocalStorage } from './LocalStorage'; -export { default as Report } from './Report'; -export { default as Route } from './Route'; -export { default as Security } from './Security'; -export { default as Server } from './Server'; +export { default as RichEmbed } from './RichEmbed'; export { default as Util } from './Util'; -export { default as PaginationEmbed } from './PaginationEmbed'; +export { default as Collection } from './Collection'; +export { default as Route } from './Route'; diff --git a/src/commands/addreferral.ts b/src/commands/addreferral.ts deleted file mode 100644 index cf8385e..0000000 --- a/src/commands/addreferral.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Message, TextChannel } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class AddReferral extends Command { - constructor(client: Client) { - super(client); - this.name = 'addreferral'; - this.description = 'Adds a referral value to an account.'; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.enabled = true; - this.aliases = ['af', 'refer']; - this.usage = `${this.client.config.prefix}addreferral `; - } - - public async run(message: Message, args: string[]) { // eslint-disable-line - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { referralCode: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); - if (!account) return this.error(message.channel, 'Cannot find user.'); - - await account.updateOne({ $inc: { totalReferrals: 1 } }); - this.client.users.fetch(account.userID).then((chan) => { - chan.send('__**Referral - Application Approval**__\nAn applicant who used your referral code during the application process has been approved. Your referral count has been increased.'); - }).catch(() => {}); - return this.success(message.channel as TextChannel, `Added referral value to account.\nReferrer: \`${account.username}\` | <@${account.userID}>`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/announce.ts b/src/commands/announce.ts index f272a00..f0b271f 100644 --- a/src/commands/announce.ts +++ b/src/commands/announce.ts @@ -1,5 +1,6 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command } from '../class'; export default class Announce extends Command { constructor(client: Client) { @@ -8,14 +9,14 @@ export default class Announce extends Command { this.description = 'Sends an announcement to all active terminals'; this.usage = `${this.client.config.prefix}announce Hi there! | ${this.client.config.prefix}announce -e EMERGENCY!`; this.aliases = ['ann']; - this.permissions = { roles: ['662163685439045632', ' ', '701454780828221450'] }; + this.permissions = { roles: ['475817826251440128', '525441307037007902'] }; this.enabled = true; } public async run(message: Message, args?: string[]) { try { if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const notification = await this.loading(message.channel, 'Sending announcement, please wait...'); + const notification = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Sending announcement, please wait...***`); if (args[0] === '-e') await this.client.util.exec(`echo "\n\n**************************************************************************\nEMERGENCY SYSTEM BROADCAST MESSAGE | Library of Code sp-us (root enforced)\n--------------------------------------------------------------------------\n\n\n${args.slice(1).join(' ').trim()}\n\n\n\n\n\n\n\n\n\n\n\n\n" | wall -n`); else await this.client.util.exec(`echo "\nSYSTEM BROADCAST MESSAGE | Library of Code sp-us (root enforced)\n\n\n${args.join(' ').trim()}" | wall -n`); message.delete(); diff --git a/src/commands/applyt2.ts b/src/commands/applyt2.ts deleted file mode 100644 index 8a62445..0000000 --- a/src/commands/applyt2.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-useless-escape */ -import { Message, MessageEmbed, TextChannel } from 'discord.js'; -import { Client, Command, Report } from '../class'; - -export default class ApplyT2 extends Command { - constructor(client: Client) { - super(client); - this.name = 'apply-t2'; - this.description = 'Applies for Tier 2.'; - this.enabled = false; - this.guildOnly = true; - this.aliases = ['t2']; - this.usage = `${this.client.config.prefix}apply-t2`; - } - - public async run(message: Message, args: string[]) { // eslint-disable-line - try { - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel, 'I can\'t find a CS Account for you.'); - if (account.tier > 1) return this.error(message.channel, 'You cannot apply for Tier 2 if you already have Tier 2 or higher.'); - - const loading = await this.loading(message.channel, 'Please wait a moment, processing application...'); - - const decision = await Report.tier2(account.userID, this.client.config.internalKey); - if (decision.status === 'SUCCESS') { - await loading.delete(); - message.channel.send(`__**Decision**__\n\n**Status:** ${decision.decision}\n**Processed by:** EDS (A\*01)`); - - if (decision.decision === 'APPROVED') { - const tier = await this.client.db.Tier.findOne({ id: 2 }); - if (account.ramLimitNotification !== -1) { - await account.updateOne({ $set: { tier: 2, ramLimitNotification: tier.resourceLimits.ram - 20 } }); - } else { - await account.updateOne({ $set: { tier: 2 } }); - } - const embed = new MessageEmbed(); - embed.setTitle('Cloud Account | Tier Change'); - embed.setColor('#0099ff'); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - embed.addField('Technician', 'SYSTEM', true); - embed.addField('Old Tier -> New Tier', `${account.tier} -> 2`, true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL()); - embed.setTimestamp(); - await this.client.util.sendMessageToUserTerminal(account.username, 'A technician has changed your tier to 2').catch(() => { }); - const ch = this.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - return this.client.users.fetch(account.userID).then((channel) => channel.send({ embeds: [embed] })).catch(); - } - return null; - } - await loading.delete(); - return message.channel.send(`__**Decision**__\n\n**Status:** ${decision.decision}\n**Processed by:** EDS (A*01)\n\n\n*Pre-Declines will not result in a hard pull, and they may be due to a server issue or insufficient information. You may want to contact a Staff member for further information.*`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/authreferral.ts b/src/commands/authreferral.ts deleted file mode 100644 index dc0eed0..0000000 --- a/src/commands/authreferral.ts +++ /dev/null @@ -1,46 +0,0 @@ -import jwt from 'jsonwebtoken'; -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class AuthReferral extends Command { - constructor(client: Client) { - super(client); - this.name = 'authreferral'; - this.description = 'Requests authorization for a referral.'; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.enabled = true; - this.aliases = ['auth']; - this.usage = `${this.client.config.prefix}authreferral `; - } - - public async run(message: Message, args: string[]) { // eslint-disable-line - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const referrer = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { referralCode: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); - if (!referrer) return this.error(message.channel, 'Cannot find referrer.'); - const referred = await message.guild.members.fetch(args[1]); - if (!referred) return this.error(message.channel, 'Cannot find referred member.'); - - const token = jwt.sign( - { staffUserID: message.author.id, - referralCode: referrer.referralCode, - referrerUserID: referrer.userID, - referrerUsername: referrer.username, - referredUserID: referred.id, - referredUserAndDiscrim: `${referred.user.username}#${referred.user.discriminator}` }, - this.client.config.keyPair.privateKey, { expiresIn: '24 hours', issuer: 'Library of Code sp-us | Cloud Services Daemon' }, - ); - this.client.users.fetch(referrer.userID).then(async (user) => { - await user.send('__**Referral Request Authorization**__\n' - + 'Your referral code has been used in an application recently submitted to us. We need to authorize this request, please visit https://loc.sh/rv and enter the authorization token below. This token expires in 24 hours. If you did not authorize this request, please contact us immediately by DMing Ramirez or opening a ticket at https://loc.sh/cs-help.\n' - + `**Referred User:** ${referred.user.username}#${referred.user.discriminator} | ${referred.user.toString()}`); - await user.send(`\`${token}\``); - }).catch(() => { - this.error(message.channel, 'Could not DM referrer.'); - }); - return this.success(message.channel, `Sent authorization token to ${referrer.username}\n\`${token}\``); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/bearer.ts b/src/commands/bearer.ts index ca924dc..fd0d70a 100644 --- a/src/commands/bearer.ts +++ b/src/commands/bearer.ts @@ -1,32 +1,32 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; -import Bearer_Revoke from './bearer_revoke'; - -export default class Bearer extends Command { - constructor(client: Client) { - super(client); - this.name = 'bearer'; - this.description = 'Creates a bearer token.'; - this.usage = `${this.client.config.prefix}bearer`; - this.subcmds = [Bearer_Revoke]; - this.guildOnly = false; - this.enabled = true; - } - - public async run(message: Message) { - try { - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel, 'Account not found.'); - // eslint-disable-next-line no-underscore-dangle - const bearer = await this.client.server.security.createBearer(account._id); - const dm = await this.client.users.fetch(message.author.id); - const msg = await dm.send(`__**Library of Code sp-us | Cloud Services [API]**__\n*This message will automatically be deleted in 60 seconds, copy the token and save it. You cannot recover it.*\n\n${bearer}`); - this.success(message.channel, 'Bearer token sent to direct messages.'); - return setTimeout(() => { - msg.delete(); - }, 60000); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +/* eslint-disable consistent-return */ +import { Message } from 'eris'; +import { Command } from '../class'; +import { Client } from '..'; + +export default class Bearer extends Command { + constructor(client: Client) { + super(client); + this.name = 'bearer'; + this.description = 'Creates a bearer token.'; + this.usage = `${this.client.config.prefix}bearer`; + this.guildOnly = false; + this.enabled = true; + } + + public async run(message: Message) { + try { + const account = await this.client.db.Account.findOne({ userID: message.author.id }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); + // eslint-disable-next-line no-underscore-dangle + const bearer = await this.client.server.security.createBearer(account._id); + const dm = await this.client.getDMChannel(message.author.id); + const msg = await dm.createMessage(`__**Library of Code sp-us | Cloud Services [API]**__\n*This message will automatically be deleted in 60 seconds, copy the token and save it. You cannot recover it.*\n\n${bearer}`); + message.channel.createMessage(`***${this.client.stores.emojis.success} Bearer token sent to direct messages.***`); + setTimeout(() => { + msg.delete(); + }, 60000); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/bearer_revoke.ts b/src/commands/bearer_revoke.ts deleted file mode 100644 index f04590c..0000000 --- a/src/commands/bearer_revoke.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class Bearer_Revoke extends Command { - constructor(client: Client) { - super(client); - this.name = 'revoke'; - this.description = 'Revokes an API bearer token.'; - this.usage = `${this.client.config.prefix}bearer revoke `; - this.enabled = true; - this.guildOnly = false; - } - - public async run(message: Message, args: string[]) { - try { - message.delete(); - if (!args[0]) return this.client.commands.get('help').run(message, ['bearer', this.name]); - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel, 'You do not have an account.'); - - const bearerVerify = await this.client.server.security.checkBearer(args[0]); - if (!bearerVerify || bearerVerify?.userID !== account.userID) return this.error(message.channel, 'Permission denied.'); - if (account.revokedBearers?.includes(args[0])) return this.error(message.channel, 'This bearer token is already revoked.'); - await account.updateOne({ $addToSet: { revokedBearers: args[0] } }); - return this.success(message.channel, 'Revoked bearer token.'); - } catch (err) { - return this.client.util.handleError(err, message, this); - } - } -} diff --git a/src/commands/cloudflare.ts b/src/commands/cloudflare.ts deleted file mode 100644 index d7223af..0000000 --- a/src/commands/cloudflare.ts +++ /dev/null @@ -1,40 +0,0 @@ -import axios from 'axios'; -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class Cloudflare extends Command { - constructor(client: Client) { - super(client); - this.name = 'cloudflare'; - this.description = 'Remove an entry from Cloudflare DNS records'; - this.permissions = { - roles: ['662163685439045632'], - }; - this.aliases = ['cf']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args.length) return this.client.commands.get('help').run(message, ['cloudflare']); - const msg = await this.loading(message.channel, 'Locating entry...'); - const { data } = await axios({ - method: 'get', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records?name=${args[0]}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - if (!data.result.length) return msg.edit(`${this.client.stores.emojis.error} ***Entry not found.***`); - msg.edit(`${this.client.stores.emojis.success} ***Located entry***\n${this.client.stores.emojis.loading} ***Deleting entry...***`); - const { id }: { id: string } = data.result[0]; - await axios({ - method: 'delete', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records/${id}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - this.client.commands.get('cwg').subcommands.get('create').enabled = true; - return msg.edit(`${this.client.stores.emojis.success} ***Located entry***\n${this.client.stores.emojis.success} ***Deleted entry***`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/createaccount.ts b/src/commands/createaccount.ts index 40c1af7..4546c61 100644 --- a/src/commands/createaccount.ts +++ b/src/commands/createaccount.ts @@ -1,6 +1,7 @@ -import { GuildMember, Message } from 'discord.js'; -import { Client, Command } from '../class'; -import { LINUX_USERNAME_REGEX } from '../class/AccountUtil'; +import { Message, PrivateChannel, GroupChannel } from 'eris'; +import uuid from 'uuid/v4'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; export default class CreateAccount extends Command { constructor(client: Client) { @@ -9,7 +10,7 @@ export default class CreateAccount extends Command { this.description = 'Create an account on the Cloud VM'; this.usage = `${this.client.config.prefix}createaccount [User ID] [Email] [Account name]`; this.aliases = ['createacc', 'cacc', 'caccount', 'create']; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; + this.permissions = { roles: ['475817826251440128', '525441307037007902'] }; this.enabled = true; } @@ -21,25 +22,105 @@ export default class CreateAccount extends Command { public async run(message: Message, args: string[]) { try { + if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) return message; // Stop TS being gay if (!args[2]) return this.client.commands.get('help').run(message, [this.name]); - const member: GuildMember = await message.guild.members.fetch(args[0]); - if (!member) return this.error(message.channel, 'User not found.'); - if (member.user.bot) return this.error(message.channel, 'I cannot create accounts for bots.'); + if (!message.channel.guild.members.has(args[0])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***User not found***`); + if (message.channel.guild.members.get(args[0]).bot) return message.channel.createMessage(`${this.client.stores.emojis.error} ***I cannot create accounts for bots***`); const checkUser = await this.client.db.Account.findOne({ userID: args[0] }); - if (checkUser) return this.error(message.channel, `<@${args[0]}> already has an account.`); + if (checkUser) return message.channel.createMessage(`${this.client.stores.emojis.error} ***<@${args[0]}> already has an account***`); const checkEmail = await this.client.db.Account.findOne({ emailAddress: args[1] }); - if (checkEmail) return this.error(message.channel, 'Account already exists with this email address.'); + if (checkEmail) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this email address***`); const checkAccount = await this.client.db.Account.findOne({ username: args[2] }); - if (checkAccount) return this.error(message.channel, 'Account already exists with this username.'); + if (checkAccount) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this username***`); - if (!this.client.util.isValidEmail(args[1])) return this.error(message.channel, 'Invalid email address supplied.'); - if (!LINUX_USERNAME_REGEX.test(args[2])) return this.error(message.channel, 'Invalid username supplied.'); + if (!this.client.util.isValidEmail(args[1])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid email address supplied***`); + if (!/^[a-z][-a-z0-9]*$/.test(args[2])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid username supplied***`); - const confirmation = await this.loading(message.channel, 'Creating account...'); - const data = await this.client.util.accounts.createAccount({ userID: args[0], username: args[2], emailAddress: args[1] }, message.author.id); - message.delete(); + const confirmation = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Creating account...***`); - return confirmation.edit(`${this.client.stores.emojis.success} ***Account successfully created***\n**Username:** \`${args[2]}\`\n**Temporary Password:** \`${data.tempPass}\``); + const tempPass = this.client.util.randomPassword(); + let passHash = await this.client.util.createHash(tempPass); passHash = passHash.replace(/[$]/g, '\\$').replace('\n', ''); + const acctName = this.client.users.get(args[0]).username.replace(/[!@#$%^&*(),.?":{}|<>]/g, '-').replace(/\s/g, '-'); + const etcPasswd = `${acctName},${args[0]},,`; + + await this.client.util.createAccount(passHash, etcPasswd, args[2], args[0], args[1], message.author.id); + await this.client.util.createModerationLog(args[0], message.member, 0); + /* + const log = await new this.client.db.Moderation({ + username: args[2], userID: args[0], logID: uuid(), moderatorID: message.author.id, reason: 'User requested account creation', type: 0, date: new Date(), + }); + await log.save(); + + const embed = new RichEmbed(); + embed.setTitle('Cloud Account | Create'); + embed.setColor('00ff00'); + embed.addField('User', `${args[2]} | <@${args[0]}>`); + embed.addField('Engineer', `<@${message.author.id}>`, true); + embed.addField('Reason', 'User requested account creation'); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + // @ts-ignore + this.client.createMessage('580950455581147146', { embed }); + */ + + this.client.util.transport.sendMail({ + to: args[1], + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Your account has been created', + html: ` + + +

Library of Code | Cloud Services

+

Your Cloud Account has been created, welcome! Please see below for some details regarding your account and our services

+

Username: ${args[2]}

+

SSH Login:

ssh ${args[2]}@cloud.libraryofcode.org
+

Email address (see below for further information): ${args[2]}@cloud.libraryofcode.org

+

Useful information

+

How to log in:

+
    +
  1. Open your desired terminal application - we recommend using Bash, but you can use your computer's default
  2. +
  3. Type in your SSH Login as above
  4. +
  5. When prompted, enter your password Please note that inputs will be blank, so be careful not to type in your password incorrectly
  6. +
+

If you fail to authenticate yourself too many times, you will be IP banned and will fail to connect. If this is the case, feel free to DM Ramirez with your public IPv4 address. + +

Setting up your cloud email

+

All email applications are different, so here are some information you can use to connect your email

+
    +
  • Server: cloud.libraryofcode.org
  • +
  • Account username/password: Normal login
  • +
  • Account type (incoming): IMAP
  • +
  • Incoming port: 143 (993 if you're using TLS security type)
  • +
  • Incoming Security Type: STARTTLS (TLS if you're using port 993)
  • +
  • Outgoing port: 587 (If that doesn't work, try 25)
  • +
  • Outgoing Security Type: STARTTLS
  • +
+

Channels and Links

+
    +
  • #status - You can find the status of all our services, including the cloud machine, here
  • +
  • #cloud-announcements - Announcements regarding the cloud machine will be here. These include planned maintenance, updates to preinstalled services etc.
  • +
  • #cloud-info - Important information you will need to, or should, know to a certain extent. These include our infractions system and Tier limits
  • +
  • #cloud-support - A support channel specifically for the cloud machine, you can use this to ask how to renew your certificates, for example
  • +
  • Library of Code Support Desk - Our Support desk, you will find some handy info there
  • +
  • Library of Code sp-us | Cloud Wiki - A wiki channel for everything related to the Cloud Services.
  • +
  • SecureSign - our certificates manager
  • +
+

Want to support us?

+

You can support us on Patreon! Head to our Patreon page and feel free to donate as much or as little as you want!
Donating $5 or more will grant you Tier 3, which means we will manage your account at your request, longer certificates, increased Tier limits as well as some roles in the server!

+ Library of Code sp-us | Support Team + + `, + }); + + const dmChannel = await this.client.getDMChannel(args[0]).catch(); + dmChannel.createMessage('<:loc:607695848612167700> **Thank you for creating an account with us!** <:loc:607695848612167700>\n' + + `Please log into your account by running \`ssh ${args[2]}@cloud.libraryofcode.org\` in your terminal, then use the password \`${tempPass}\` to log in.\n` + + `You will be asked to change your password, \`(current) UNIX password\` is \`${tempPass}\`, then create a password that is at least 12 characters long, with at least one number, special character, and an uppercase letter\n` + + 'Bear in mind that when you enter your password, it will be blank, so be careful not to type in your password incorrectly.\n' + + 'You may now return to Modmail, and continue setting up your account from there.\n\n' + + 'An email containing some useful information has also been sent').catch(); + + return confirmation.edit(`${this.client.stores.emojis.success} ***Account successfully created***\n**Username:** \`${args[2]}\`\n**Temporary Password:** \`${tempPass}\``); } catch (error) { return this.client.util.handleError(error, message, this); } diff --git a/src/commands/cwg.ts b/src/commands/cwg.ts index 88159fc..502f51f 100644 --- a/src/commands/cwg.ts +++ b/src/commands/cwg.ts @@ -1,10 +1,9 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; +import { Message } from 'eris'; +import { Command } from '../class'; +import { Client } from '..'; import Create from './cwg_create'; import Data from './cwg_data'; import Delete from './cwg_delete'; -import UpdateCert from './cwg_updatecert'; -import SelvServ from './cwg_selfserv'; export default class CWG extends Command { constructor(client: Client) { @@ -12,8 +11,8 @@ export default class CWG extends Command { this.name = 'cwg'; this.description = 'Manages aspects for the CWG.'; this.usage = `Run ${this.client.config.prefix}${this.name} [subcommand] for usage information`; - // this.permissions = { roles: ['446104438969466890'] }; - this.subcmds = [Create, Data, Delete, UpdateCert, SelvServ]; + this.permissions = { roles: ['446104438969466890'] }; + this.subcmds = [Create, Data, Delete]; this.enabled = true; } diff --git a/src/commands/cwg_create.ts b/src/commands/cwg_create.ts index 72a53b6..ef145c1 100644 --- a/src/commands/cwg_create.ts +++ b/src/commands/cwg_create.ts @@ -1,23 +1,20 @@ -import fs, { writeFile, unlink, symlink } from 'fs-extra'; +import fs from 'fs-extra'; import axios from 'axios'; -import { randomBytes } from 'crypto'; -import { Message, MessageEmbed, TextChannel } from 'discord.js'; +import x509 from '@ghaiklor/x509'; +import { Message } from 'eris'; import { AccountInterface } from '../models'; -import { Client, Command } from '../class'; -import { parseCertificate } from '../functions'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; export default class CWG_Create extends Command { - public urlRegex: RegExp; - constructor(client: Client) { super(client); this.name = 'create'; this.description = 'Bind a domain to the CWG'; - this.usage = `${this.client.config.prefix}cwg create [Cert Chain] [Private Key] || Use snippets raw URL`; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; + this.usage = `${this.client.config.prefix}cwg create [User ID | Username] [Domain] [Port] `; + this.permissions = { roles: ['525441307037007902'] }; this.aliases = ['bind']; this.enabled = true; - this.urlRegex = /^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=]+$/; } public async run(message: Message, args: string[]) { @@ -30,108 +27,74 @@ export default class CWG_Create extends Command { */ try { if (!args[2]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - - if (!this.urlRegex.test(args[1])) return this.error(message.channel, 'Invalid URL supplied.'); - if (Number(args[2]) <= 1024 || Number(args[2]) >= 65535) return this.error(message.channel, 'Port must be greater than 1024 and less than 65535.'); - if (!args[1].endsWith('.cloud.libraryofcode.org') && !args[4]) return this.error(message.channel, 'Certificate Chain and Private Key are required for custom domains.'); - + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Binding domain...***`); const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }] }); - if (!account) return this.error(message.channel, 'Cannot locate account.'); - - if (await this.client.db.Domain.exists({ domain: args[1] })) return this.error(message.channel, 'This domain already exists.'); - + if (!account) return edit.edit(`${this.client.stores.emojis.error} Cannot locate account, please try again.`); + if (args[3] && !args[4]) return edit.edit(`${this.client.stores.emojis.error} x509 Certificate key required`); + let certs: { cert?: string, key?: string }; if (args[4]) certs = { cert: args[3], key: args[4] }; + if (await this.client.db.Domain.exists({ domain: args[1] })) return edit.edit(`***${this.client.stores.emojis.error} This domain already exists.***`); if (await this.client.db.Domain.exists({ port: Number(args[2]) })) { + // await edit.edit(`***${this.client.stores.emojis.error} This port is already binded to a domain. Do you wish to continue? (y/n)***`); let answer: Message; try { - answer = await this.client.util.messageCollector( - message, - `***${this.client.stores.emojis.error} This port is already bound to a domain. Do you wish to continue? (y/n)***`, - 30000, true, ['y', 'n'], (msg) => msg.author.id === message.author.id && msg.channel.id === message.channel.id, - ); + answer = await this.client.util.messageCollector(message, + `***${this.client.stores.emojis.error} This port is already binded to a domain. Do you wish to continue? (y/n)***`, + 30000, true, ['y', 'n'], (msg) => msg.author.id === message.author.id && msg.channel.id === message.channel.id); } catch (error) { - return this.error(message.channel, 'Bind request cancelled.'); + return edit.edit(`***${this.client.stores.emojis.error} Bind request cancelled***`); } - if (answer.content === 'n') return this.error(message.channel, 'Bind request cancelled.'); + if (answer.content === 'n') return edit.edit(`***${this.client.stores.emojis.error} Bind request cancelled***`); } - - const edit = await this.loading(message.channel, 'Binding domain...'); - - let certs: { cert?: string, key?: string } = {}; - if (!args[1].endsWith('.cloud.libraryofcode.org')) { - const urls = args.slice(3, 5); - if (urls.some((l) => !l.includes('snippets.cloud.libraryofcode.org/raw/'))) return this.error(message.channel, 'Invalid snippets URL. Make sure to use https://snippets.cloud.libraryofcode.org/raw/*.'); - - const tasks = urls.map((l) => axios({ method: 'GET', url: l })); - const response = await Promise.all(tasks); - const certAndPrivateKey: string[] = response.map((r) => r.data); - - if (!this.isValidCertificateChain(certAndPrivateKey[0])) return this.error(message.channel, 'The certificate chain provided is invalid.'); - if (!this.isValidPrivateKey(certAndPrivateKey[1])) return this.error(message.channel, 'The private key provided is invalid.'); - - certs = { cert: certAndPrivateKey[0], key: certAndPrivateKey[1] }; - } else { - certs.cert = await fs.readFile('/etc/ssl/private/cloud-libraryofcode-org.chain.crt', { encoding: 'utf8' }); - certs.key = await fs.readFile('/etc/ssl/private/cloud-libraryofcode-org.key', { encoding: 'utf8' }); - } - const domain = await this.createDomain(account, args[1], Number(args[2]), certs); - - const tasks = [message.delete(), this.client.util.exec('systemctl reload nginx')]; + const embed = new RichEmbed(); + embed.setTitle('Domain Creation'); + embed.setColor(3066993); + embed.addField('Account Username', account.username, true); + embed.addField('Account ID', account.id, true); + embed.addField('Engineer', `<@${message.author.id}>`, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + const cert = x509.parseCert(await fs.readFile(domain.x509.cert, { encoding: 'utf8' })); + embed.addField('Certificate Issuer', cert.issuer.organizationName, true); + embed.addField('Certificate Subject', cert.subject.commonName, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(new Date(message.timestamp)); + message.delete(); + await this.client.util.exec('systemctl reload nginx'); + edit.edit(`***${this.client.stores.emojis.success} Successfully binded ${domain.domain} to port ${domain.port} for ${account.userID}.***`); // @ts-ignore - await Promise.all(tasks); - - const embed = new MessageEmbed() - .setTitle('Domain Creation') - .setColor(3066993) - .addField('Account Username', `${account.username} | <@${account.userID}>`, true) - .addField('Technician', await this.client.util.getTechnicianFullName(message.author), true) - .addField('Domain', domain.domain, true) - .addField('Port', String(domain.port), true); - - const certPath = `/opt/CloudServices/temp/${randomBytes(5).toString('hex')}`; - await writeFile(certPath, certs.cert, { encoding: 'utf8' }); - const cert = await parseCertificate(this.client, certPath); - - embed.addField('Certificate Issuer', cert.issuer.organizationName, true) - .addField('Certificate Subject', cert.subject.commonName, true) - .setFooter(this.client.user.username, this.client.user.avatarURL()) - .setTimestamp(new Date(message.createdTimestamp)); - - const completed = [ - edit.edit(`***${this.client.stores.emojis.success} Successfully bound ${domain.domain} to port ${domain.port} for ${account.username}.***`), - (this.client.channels.cache.get('580950455581147146') as TextChannel).send({ embeds: [embed] }), - this.client.users.fetch(account.userID).then((r) => r.send({ embeds: [embed] })), - this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Support Team ', - subject: 'Your domain has been bound', - html: ` -

Library of Code sp-us | Cloud Services

-

Hello, this is an email informing you that a new domain under your account has been bound. - Information is below.

- Domain: ${domain.domain}
- Port: ${domain.port}
- Certificate Issuer: ${cert.issuer.organizationName}
- Certificate Subject: ${cert.subject.commonName}
- Responsible Engineer: ${message.author.username}#${message.author.discriminator}

- - If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.
- - Library of Code sp-us | Support Team - `, - }), - ]; + this.client.createMessage('580950455581147146', { embed }); + // @ts-ignore + this.client.getDMChannel(account.userID).then((r) => r.createMessage({ embed })); + await this.client.util.transport.sendMail({ + to: account.emailAddress, + from: 'Library of Code sp-us | Support Team ', + subject: 'Your domain has been binded', + html: ` +

Library of Code sp-us | Cloud Services

+

Hello, this is an email informing you that a new domain under your account has been binded. + Information is below.

+ Domain: ${domain.domain}
+ Port: ${domain.port}
+ Certificate Issuer: ${cert.issuer.organizationName}
+ Certificate Subject: ${cert.subject.commonName}
+ Responsible Engineer: ${message.author.username}#${message.author.discriminator}

+ If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.
+ + Library of Code sp-us | Support Team + `, + }); if (!domain.domain.includes('cloud.libraryofcode.org')) { - const content = `__**DNS Record Setup**__\nYou recently a bound a custom domain to your Library of Code sp-us Account. You'll have to update your DNS records. We've provided the records below.\n\n\`${domain.domain} IN CNAME cloud.libraryofcode.org AUTO/500\`\nThis basically means you need to make a CNAME record with the key/host of ${domain.domain} and the value/point to cloud.libraryofcode.org. If you have any questions, don't hesitate to ask us.`; - completed.push(this.client.users.fetch(account.userID).then((r) => r.send(content))); + const content = `__**DNS Record Setup**__\nYou recently a binded a custom domain to your Library of Code sp-us Account. You'll have to update your DNS records. We've provided the records below.\n\n\`${domain.domain} IN CNAME cloud.libraryofcode.org AUTO/500\`\nThis basically means you need to make a CNAME record with the key/host of ${domain.domain} and the value/point to cloud.libraryofcode.org. If you have any questions, don't hesitate to ask us.`; + this.client.getDMChannel(account.userID).then((r) => r.createMessage(content)); } - - return Promise.all(completed); + return domain; } catch (err) { - this.client.util.handleError(err, message, this); - const tasks = [fs.unlink(`/etc/nginx/sites-enabled/${args[1]}`), fs.unlink(`/etc/nginx/sites-available/${args[1]}`), this.client.db.Domain.deleteMany({ domain: args[1] })]; - return Promise.allSettled(tasks); + await fs.unlink(`/etc/nginx/sites-available/${args[1]}`); + await fs.unlink(`/etc/nginx/sites-enabled/${args[1]}`); + await this.client.db.Domain.deleteMany({ domain: args[1] }); + return this.client.util.handleError(err, message, this); } } @@ -140,86 +103,45 @@ export default class CWG_Create extends Command { * @param account The account of the user. * @param subdomain The domain to use. `mydomain.cloud.libraryofcode.org` * @param port The port to use, must be between 1024 and 65535. - * @param x509Certificate The contents the certificate and key files. - * @example await CWG.createDomain(account, 'mydomain.cloud.libraryofcode.org', 6781); + * @param x509 The paths to the certificate and key files. Must be already existant. + * @example await CWG.createDomain('mydomain.cloud.libraryofcode.org', 6781); */ - public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string }) { + public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string } = { cert: '/etc/nginx/ssl/cloud-org.chain.crt', key: '/etc/nginx/ssl/cloud-org.key.pem' }) { try { if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); if (!await this.client.db.Account.exists({ userID: account.userID })) throw new Error(`Cannot find account ${account.userID}.`); - let x509: { cert: string, key: string }; - if (x509Certificate) { - x509 = await this.createCertAndPrivateKey(domain, x509Certificate.cert, x509Certificate.key); - } else { - x509 = { - cert: '/etc/ssl/private/cloud-libraryofcode-org.chain.crt', - key: '/etc/ssl/private/cloud-libraryofcode-org.key', - }; - } - let cfg = await fs.readFile('/opt/CloudServices/src/static/nginx.conf', { encoding: 'utf8' }); + await fs.access(x509Certificate.cert, fs.constants.R_OK); + await fs.access(x509Certificate.key, fs.constants.R_OK); + let cfg = await fs.readFile('/var/CloudServices/dist/static/nginx.conf', { encoding: 'utf8' }); cfg = cfg.replace(/\[DOMAIN]/g, domain); cfg = cfg.replace(/\[PORT]/g, String(port)); - cfg = cfg.replace(/\[CERTIFICATE]/g, x509.cert); - cfg = cfg.replace(/\[KEY]/g, x509.key); + cfg = cfg.replace(/\[CERTIFICATE]/g, x509Certificate.cert); + cfg = cfg.replace(/\[KEY]/g, x509Certificate.key); await fs.writeFile(`/etc/nginx/sites-available/${domain}`, cfg, { encoding: 'utf8' }); await fs.symlink(`/etc/nginx/sites-available/${domain}`, `/etc/nginx/sites-enabled/${domain}`); const entry = new this.client.db.Domain({ account, domain, port, - x509, + x509: x509Certificate, enabled: true, }); + if (domain.includes('cloud.libraryofcode.org')) { + const dmn = domain.split('.'); + await axios({ + method: 'post', + url: 'https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records', + headers: { Authorization: `Bearer ${this.client.config.cloudflare}`, 'Content-Type': 'application/json' }, + data: JSON.stringify({ type: 'CNAME', name: `${dmn[0]}.${dmn[1]}`, content: 'cloud.libraryofcode.org', proxied: false }), + }); + } return entry.save(); } catch (error) { - const tasks = [fs.unlink(`/etc/nginx/sites-enabled/${domain}`), fs.unlink(`/etc/nginx/sites-available/${domain}`), this.client.db.Domain.deleteMany({ domain })]; - await Promise.allSettled(tasks); + await fs.unlink(`/etc/nginx/sites-enabled/${domain}`); + await fs.unlink(`/etc/nginx/sites-available/${domain}`); + await this.client.db.Domain.deleteMany({ domain }); throw error; } } - - public async createCertAndPrivateKey(domain: string, certChain: string, privateKey: string) { - // if (!this.isValidCertificateChain(certChain)) throw new Error('Invalid Certificate Chain'); - // if (!this.isValidPrivateKey(privateKey)) throw new Error('Invalid Private Key'); - const path = `/opt/CloudServices/temp/${domain}`; - await Promise.all([writeFile(`${path}.chain.crt`, certChain), writeFile(`${path}.key.pem`, privateKey)]); - if (!this.isMatchingPair(`${path}.chain.crt`, `${path}.key.pem`)) { - await Promise.all([unlink(`${path}.chain.crt`), unlink(`${path}.key.pem`)]); - throw new Error('Certificate and Private Key do not match'); - } - - if (domain.endsWith('.cloud.libraryofcode.org')) { - await Promise.all([symlink('/etc/ssl/private/cloud-libraryofcode-org.chain.crt', `/etc/ssl/certs/cwg/${domain}.chain.crt`), symlink('/etc/ssl/private/cloud-libraryofcode-org.key', `/etc/ssl/private/cwg/${domain}.key.pem`)]); - } else { - await Promise.all([writeFile(`/etc/ssl/certs/cwg/${domain}.chain.crt`, certChain), writeFile(`/etc/ssl/private/cwg/${domain}.key.pem`, privateKey)]); - } - return { cert: `/etc/ssl/certs/cwg/${domain}.chain.crt`, key: `/etc/ssl/private/cwg/${domain}.key.pem` }; - } - - public checkOccurrence(text: string, query: string) { - return (text.match(new RegExp(query, 'g')) || []).length; - } - - public isValidCertificateChain(cert: string) { - if (!cert.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN CERTIFICATE-----')) return false; - if (!cert.replace(/^\s+|\s+$/g, '').endsWith('-----END CERTIFICATE-----')) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----BEGIN CERTIFICATE-----') !== 2) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----END CERTIFICATE-----') !== 2) return false; - return true; - } - - public isValidPrivateKey(key: string) { - if (!key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN RSA PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN ECC PRIVATE KEY-----')) return false; - if (!key.replace(/^\s+|\s+$/g, '').endsWith('-----END PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').endsWith('-----END RSA PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').endsWith('-----END ECC PRIVATE KEY-----')) return false; - if ((this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN RSA PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN ECC PRIVATE KEY-----') !== 1)) return false; - if ((this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END RSA PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END ECC PRIVATE KEY-----') !== 1)) return false; - return true; - } - - public async isMatchingPair(cert: string, privateKey: string) { - const result: string = await this.client.util.exec(`${__dirname}/../bin/checkCertSignatures ${cert} ${privateKey}`); - const { ok }: { ok: boolean } = JSON.parse(result); - return ok; - } } diff --git a/src/commands/cwg_data.ts b/src/commands/cwg_data.ts index 82d00bb..f916223 100644 --- a/src/commands/cwg_data.ts +++ b/src/commands/cwg_data.ts @@ -1,60 +1,49 @@ -import fs from 'fs'; -import moment from 'moment'; -import { Message, MessageEmbed } from 'discord.js'; -import { Client, Command, PaginationEmbed } from '../class'; - -export default class CWG_Data extends Command { - constructor(client: Client) { - super(client); - this.name = 'data'; - this.description = 'Check CWG data'; - this.usage = `${this.client.config.prefix}cwg data `; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const dom = await this.client.db.Domain.find({ $or: [{ domain: args[0] }, { port: Number(args[0]) || 0 }] }); - if (!dom.length) { - if (!Number.isNaN(Number(args[0]))) { - try { - await this.client.util.exec(`fuser ${args[0]}/tcp`); - return this.error(message.channel, 'The port you provided is being used by a system process.'); - } catch (error) { - return this.error(message.channel, 'The domain or port you provided could not be found.'); - } - } - return this.error(message.channel, 'The domain or port you provided could not be found.'); - } - const embeds = await Promise.all(dom.map(async (domain) => { - let pem: string; - try { - pem = fs.readFileSync(domain.x509.cert, { encoding: 'utf8' }); - } catch { - pem = fs.readFileSync('/etc/ssl/private/cloud-libraryofcode-org.chain.crt', { encoding: 'utf8' }); - } - - const cert = await this.client.util.parseCertificate(pem); - const embed = new MessageEmbed(); - embed.setTitle('Domain Information'); - embed.addField('Account Username', domain.account.username, true); - embed.addField('Account ID', domain.account.userID, true); - embed.addField('Domain', domain.domain, true); - embed.addField('Port', String(domain.port), true); - embed.addField('Certificate Issuer', `${cert.issuer.organization[0]} (${cert.issuer.commonName})` || 'N/A', true); - embed.addField('Certificate Subject', cert.subject.commonName || 'N/A', true); - embed.addField('Certificate Expiration Date', cert.notAfter ? moment(cert.notAfter).format('dddd, MMMM Do YYYY, h:mm:ss A') || 'N/A' : 'N/A', true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL()); - embed.setTimestamp(); - return embed; - })); - this.client.signale.log(embeds); - if (embeds.length === 1) return message.channel.send({ embeds: [embeds[0]] }); - return this.client.util.createPaginationEmbed(message, embeds); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import fs from 'fs'; +import moment from 'moment'; +import x509 from '@ghaiklor/x509'; +import { createPaginationEmbed } from 'eris-pagination'; +import { Message } from 'eris'; +import { promisify } from 'util'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; + +export default class CWG_Data extends Command { + constructor(client: Client) { + super(client); + this.name = 'data'; + this.description = 'Check CWG data'; + this.usage = `${this.client.config.prefix}cwg data [Domain | Port]`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); + const dom = await this.client.db.Domain.find({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); + if (!dom.length) return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); + const embeds = dom.map((domain) => { + const cert = fs.readFileSync(domain.x509.cert, { encoding: 'utf8' }); + const embed = new RichEmbed(); + embed.setTitle('Domain Information'); + embed.addField('Account Username', domain.account.username, true); + embed.addField('Account ID', domain.account.userID, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + embed.addField('Certificate Issuer', x509.getIssuer(cert).organizationName, true); + embed.addField('Certificate Subject', x509.getSubject(cert).commonName, true); + embed.addField('Certificate Expiration Date', moment(x509.parseCert(cert).notAfter).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + return embed; + }); + this.client.signale.log(embeds); + // @ts-ignore + if (embeds.length === 1) return message.channel.createMessage({ embed: embeds[0] }); + // @ts-ignore + return createPaginationEmbed(message, this.client, embeds, {}); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/cwg_delete.ts b/src/commands/cwg_delete.ts index 24c721e..cf58e16 100644 --- a/src/commands/cwg_delete.ts +++ b/src/commands/cwg_delete.ts @@ -1,61 +1,63 @@ -import fs from 'fs-extra'; -import axios from 'axios'; -import { Message, MessageEmbed, TextChannel } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class CWG_Delete extends Command { - constructor(client: Client) { - super(client); - this.name = 'delete'; - this.description = 'Unbind a domain from the CWG'; - this.usage = `${this.client.config.prefix}cwg delete [Domain | Port]`; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.aliases = ['unbind']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const domain = await this.client.db.Domain.findOne({ $or: [{ domain: args[0] }, { port: Number(args[0]) || 0 }] }); - if (!domain) return this.error(message.channel, 'The domain or port you provided could not be found.'); - const edit = await this.loading(message.channel, 'Deleting domain...'); - const embed = new MessageEmbed(); - embed.setTitle('Domain Deletion'); - embed.addField('Account Username', `${domain.account.username} | <@${domain.account.userID}>`, true); - embed.addField('Technician', await this.client.util.getTechnicianFullName(message.author), true); - embed.addField('Domain', domain.domain, true); - embed.addField('Port', String(domain.port), true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL()); - embed.setTimestamp(); - if (domain.domain.includes('cloud.libraryofcode.org')) { - const resultID = await axios({ - method: 'get', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records?name=${domain.domain}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - this.client.signale.debug(resultID.data); - if (resultID.data.result[0]) { - const recordID = resultID.data.result[0].id; - await axios({ - method: 'delete', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records/${recordID}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - } - } - try { - await fs.unlink(`/etc/nginx/sites-enabled/${domain.domain}`); - await fs.unlink(`/etc/nginx/sites-available/${domain.domain}`); - } catch (e) { this.client.signale.error(e); } - await this.client.db.Domain.deleteOne({ domain: domain.domain }); - await this.client.util.exec('systemctl reload nginx'); - edit.edit(`***${this.client.stores.emojis.success} Domain ${domain.domain} with port ${domain.port} has been successfully deleted.***`); - const ch = this.client.channels.cache.get('580950455581147146') as TextChannel; - ch.send({ embeds: [embed] }); - return this.client.users.fetch(domain.account.userID).then((u) => u.send({ embeds: [embed] })).catch(() => {}); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import fs from 'fs-extra'; +import axios from 'axios'; +import { Message } from 'eris'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; + +export default class CWG_Delete extends Command { + constructor(client: Client) { + super(client); + this.name = 'delete'; + this.description = 'Unbind a domain to the CWG'; + this.usage = `${this.client.config.prefix}cwg delete [Domain | Port]`; + this.permissions = { roles: ['525441307037007902'] }; + this.aliases = ['unbind']; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); + const domain = await this.client.db.Domain.findOne({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); + if (!domain) return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Deleting domain...***`); + const embed = new RichEmbed(); + embed.setTitle('Domain Deletion'); + embed.addField('Account Username', domain.account.username, true); + embed.addField('Account ID', domain.account.userID, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + if (domain.domain.includes('cloud.libraryofcode.org')) { + const resultID = await axios({ + method: 'get', + url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records?name=${domain.domain}`, + headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, + }); + this.client.signale.debug(resultID.data); + if (resultID.data.result[0]) { + const recordID = resultID.data.result[0].id; + await axios({ + method: 'delete', + url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records/${recordID}`, + headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, + }); + } + } + try { + await fs.unlink(`/etc/nginx/sites-enabled/${domain.domain}`); + await fs.unlink(`/etc/nginx/sites-available/${domain.domain}`); + } catch (e) { this.client.signale.error(e); } + await this.client.db.Domain.deleteOne({ domain: domain.domain }); + await this.client.util.exec('systemctl reload nginx'); + edit.edit(`***${this.client.stores.emojis.success} Domain ${domain.domain} with port ${domain.port} has been successfully deleted.***`); + // @ts-ignore + this.client.createMessage('580950455581147146', { embed }); + // @ts-ignore + return this.client.getDMChannel(domain.account.userID).then((channel) => channel.createMessage({ embed })).catch(() => {}); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/cwg_selfserv.ts b/src/commands/cwg_selfserv.ts deleted file mode 100644 index 8279898..0000000 --- a/src/commands/cwg_selfserv.ts +++ /dev/null @@ -1,227 +0,0 @@ -import fs, { writeFile, unlink, symlink } from 'fs-extra'; -import axios from 'axios'; -import { randomBytes } from 'crypto'; -import { Message, MessageEmbed, TextChannel } from 'discord.js'; -import { AccountInterface } from '../models'; -import { Client, Command } from '../class'; -import { parseCertificate } from '../functions'; - -export default class CWG_SelfService extends Command { - public urlRegex: RegExp; - - constructor(client: Client) { - super(client); - this.name = 'selfserv'; - this.description = 'Creates a subdomain on your account. Do not include the entire subdomain: if you want `mydomain.cloud.libraryofcode.org`, just supply `mydomain` in the command parameters.\nYou must receive an authentication token from the Instant Application Service, see `?apply` for more information.'; - this.usage = `${this.client.config.prefix}cwg selfserv `; - this.aliases = ['ss']; - this.enabled = true; - this.urlRegex = /^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=]+$/; - } - - public async run(message: Message, args: string[]) { - if (!args[0].endsWith('.cloud.libraryofcode.org')) return this.error(message.channel, 'Only subdomains may be created with this command. If you\'d like to use a custom domain please contact a Technician.'); - const reqDomain = `${escape(args[0])}.cloud.libraryofcode.org`; - - try { - if (!args[1]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel, 'Cannot locate account.'); - - if (!this.domainTextValidation(args[0])) return this.error(message.channel, 'The domain value you provided is invalid.'); - - if (await this.client.db.Domain.exists({ domain: reqDomain })) return this.error(message.channel, 'This domain already exists.'); - - if (await this.checkAuthorizationToken(message.author.id, args[1]) !== true) return this.error(message.channel, 'Permission denied.'); - - const edit = await this.loading(message.channel, 'Binding domain...'); - - const certs: { cert?: string, key?: string } = {}; - certs.cert = await fs.readFile('/etc/ssl/private/cloud-libraryofcode-org.chain.crt', { encoding: 'utf8' }); - certs.key = await fs.readFile('/etc/ssl/private/cloud-libraryofcode-org.key', { encoding: 'utf8' }); - - let port: number; - try { - port = await this.getPort(); - } catch { - return this.error(message.channel, 'Unable to acquire port automatically. Please contact a Technician.'); - } - - const domain = await this.createDomain(account, reqDomain, port, certs); - - const tasks = [message.delete(), this.client.util.exec('systemctl reload nginx')]; - // @ts-ignore - await Promise.all(tasks); - - const embed = new MessageEmbed() - .setTitle('Domain Creation') - .setColor(3066993) - .addField('Account Username', `${account.username} | <@${account.userID}>`, true) - .addField('Technician', 'SYSTEM', true) - .addField('Domain', domain.domain, true) - .addField('Port', String(domain.port), true); - - const certPath = `/opt/CloudServices/temp/${randomBytes(5).toString('hex')}`; - await writeFile(certPath, certs.cert, { encoding: 'utf8' }); - const cert = await parseCertificate(this.client, certPath); - - embed.addField('Certificate Issuer', cert.issuer.organizationName, true) - .addField('Certificate Subject', cert.subject.commonName, true) - .setFooter(this.client.user.username, this.client.user.avatarURL()) - .setTimestamp(new Date(message.createdTimestamp)); - - const completed = [ - edit.edit(`***${this.client.stores.emojis.success} Your subdomain has been created, please check your DMs or email address for more information.***`), - (this.client.channels.cache.get('580950455581147146') as TextChannel).send({ embeds: [embed] }), - this.client.users.fetch(account.userID).then((r) => r.send({ embeds: [embed] })), - this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Support Team ', - replyTo: 'Dept of Engineering ', - subject: 'Your domain has been created', - html: ` -

Library of Code sp-us | Cloud Services

-

Hello, this is an email informing you that a new domain under your account has been bound. - Information is below.

- Domain: ${domain.domain}
- Port: ${domain.port}
- Certificate Issuer: ${cert.issuer.organizationName}
- Certificate Subject: ${cert.subject.commonName}
- Responsible Engineer: SYSTEM

- - If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.
- - Library of Code sp-us | Support Team - `, - }), - ]; - - return Promise.all(completed); - } catch (err) { - this.client.util.handleError(err, message, this); - const tasks = [fs.unlink(`/etc/nginx/sites-enabled/${reqDomain}`), fs.unlink(`/etc/nginx/sites-available/${reqDomain}`), this.client.db.Domain.deleteMany({ domain: reqDomain })]; - return Promise.allSettled(tasks); - } - } - - public async getPort() { - const port = Number(await this.client.redis.get('cwgsspc')); - if (await this.client.db.Domain.exists({ port })) throw new Error('Error retreiving port.'); - if (port >= Number(process.env.MAX_CWG_PORT)) throw new Error('Error retrieving port.'); - await this.client.redis.incr('cwgsspc'); - return port; - } - - public domainTextValidation(domain: string) { - if (domain.length >= 25) return false; - return /[A-Za-z0-9](?:[A-Za-z0-9-]{0,19}[A-Za-z0-9])?/.test(domain); - } - - public async checkAuthorizationToken(userID: string, token: string) { - try { - const resp = await axios({ - url: 'https://comm.libraryofcode.org/internal/check-cwg-self-auth', - params: { - userID, - token, - internalKey: this.client.config.internalKey, - }, - }); - if (resp?.status === 204) { - return true; - } - return false; - } catch { - return false; - } - } - - /** - * This function binds a domain to a port on the CWG. - * @param account The account of the user. - * @param subdomain The domain to use. `mydomain.cloud.libraryofcode.org` - * @param port The port to use, must be between 1024 and 65535. - * @param x509Certificate The contents the certificate and key files. - * @example await CWG.createDomain(account, 'mydomain.cloud.libraryofcode.org', 6781); - */ - public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string }) { - try { - if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); - if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); - if (!await this.client.db.Account.exists({ userID: account.userID })) throw new Error(`Cannot find account ${account.userID}.`); - let x509: { cert: string, key: string }; - if (x509Certificate) { - x509 = await this.createCertAndPrivateKey(domain, x509Certificate.cert, x509Certificate.key); - } else { - x509 = { - cert: '/etc/ssl/private/cloud-libraryofcode-org.chain.crt', - key: '/etc/ssl/private/cloud-libraryofcode-org.key', - }; - } - let cfg = await fs.readFile('/opt/CloudServices/src/static/nginx.conf', { encoding: 'utf8' }); - cfg = cfg.replace(/\[DOMAIN]/g, domain); - cfg = cfg.replace(/\[PORT]/g, String(port)); - cfg = cfg.replace(/\[CERTIFICATE]/g, x509.cert); - cfg = cfg.replace(/\[KEY]/g, x509.key); - await fs.writeFile(`/etc/nginx/sites-available/${domain}`, cfg, { encoding: 'utf8' }); - await fs.symlink(`/etc/nginx/sites-available/${domain}`, `/etc/nginx/sites-enabled/${domain}`); - const entry = new this.client.db.Domain({ - account, - domain, - port, - x509, - enabled: true, - }); - return entry.save(); - } catch (error) { - const tasks = [fs.unlink(`/etc/nginx/sites-enabled/${domain}`), fs.unlink(`/etc/nginx/sites-available/${domain}`), this.client.db.Domain.deleteMany({ domain })]; - await Promise.allSettled(tasks); - throw error; - } - } - - public async createCertAndPrivateKey(domain: string, certChain: string, privateKey: string) { - if (!this.isValidCertificateChain(certChain)) throw new Error('Invalid Certificate Chain'); - // if (!this.isValidPrivateKey(privateKey)) throw new Error('Invalid Private Key'); - const path = `/opt/CloudServices/temp/${domain}`; - await Promise.all([writeFile(`${path}.chain.crt`, certChain), writeFile(`${path}.key.pem`, privateKey)]); - if (!this.isMatchingPair(`${path}.chain.crt`, `${path}.key.pem`)) { - await Promise.all([unlink(`${path}.chain.crt`), unlink(`${path}.key.pem`)]); - throw new Error('Certificate and Private Key do not match'); - } - - if (domain.endsWith('.cloud.libraryofcode.org')) { - await Promise.all([symlink('/etc/ssl/private/cloud-libraryofcode-org.chain.crt', `/etc/ssl/certs/cwg/${domain}.chain.crt`), symlink('/etc/ssl/private/cloud-libraryofcode-org.key', `/etc/ssl/private/cwg/${domain}.key.pem`)]); - } else { - await Promise.all([writeFile(`/etc/ssl/certs/cwg/${domain}.chain.crt`, certChain), writeFile(`/etc/ssl/private/cwg/${domain}.key.pem`, privateKey)]); - } - return { cert: `/etc/ssl/certs/cwg/${domain}.chain.crt`, key: `/etc/ssl/private/cwg/${domain}.key.pem` }; - } - - public checkOccurrence(text: string, query: string) { - return (text.match(new RegExp(query, 'g')) || []).length; - } - - public isValidCertificateChain(cert: string) { - if (!cert.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN CERTIFICATE-----')) return false; - if (!cert.replace(/^\s+|\s+$/g, '').endsWith('-----END CERTIFICATE-----')) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----BEGIN CERTIFICATE-----') !== 2) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----END CERTIFICATE-----') !== 2) return false; - return true; - } - - public isValidPrivateKey(key: string) { - if (!key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN RSA PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN ECC PRIVATE KEY-----')) return false; - if (!key.replace(/^\s+|\s+$/g, '').endsWith('-----END PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').endsWith('-----END RSA PRIVATE KEY-----') && !key.replace(/^\s+|\s+$/g, '').endsWith('-----END ECC PRIVATE KEY-----')) return false; - if ((this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN RSA PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN ECC PRIVATE KEY-----') !== 1)) return false; - if ((this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END RSA PRIVATE KEY-----') !== 1) && (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END ECC PRIVATE KEY-----') !== 1)) return false; - return true; - } - - public async isMatchingPair(cert: string, privateKey: string) { - const result: string = await this.client.util.exec(`${__dirname}/../bin/checkCertSignatures ${cert} ${privateKey}`); - const { ok }: { ok: boolean } = JSON.parse(result); - return ok; - } -} diff --git a/src/commands/cwg_updatecert.ts b/src/commands/cwg_updatecert.ts deleted file mode 100644 index 87bd40e..0000000 --- a/src/commands/cwg_updatecert.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { writeFile, unlink } from 'fs-extra'; -import axios from 'axios'; -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class CWG_UpdateCert extends Command { - constructor(client: Client) { - super(client); - this.name = 'updatecert'; - this.description = 'Update a CWG certificate'; - this.usage = `${this.client.config.prefix}cwg updatecert [Domain | Port] [Cert Chain] [Private Key] || Use snippets raw URL`; - this.permissions = { roles: ['662163685439045632'] }; - this.aliases = ['update', 'updatecrt', 'renew', 'renewcert', 'renewcrt']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[2]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const dom = await this.client.db.Domain.findOne({ $or: [{ domain: args[0] }, { port: Number(args[0]) || 0 }] }); - if (!dom) return this.error(message.channel, 'The domain or port you provided could not be found.'); - const { domain, port, x509, account } = dom; - const { cert, key } = x509; - - const urls = args.slice(1, 3); // eslint-disable-line - if (urls.some((l) => !l.includes('snippets.cloud.libraryofcode.org/raw/'))) return this.error(message.channel, 'Invalid snippets URL.'); - const tasks = urls.map((l) => axios({ method: 'GET', url: l })); - const response = await Promise.all(tasks); - const certAndPrivateKey: string[] = response.map((r) => r.data); - - if (!this.isValidCertificateChain(certAndPrivateKey[0])) return this.error(message.channel, 'The certificate chain provided is invalid.'); - if (!this.isValidPrivateKey(certAndPrivateKey[1])) return this.error(message.channel, 'The private key provided is invalid.'); - - const path = `/opt/CloudServices/temp/${account.id}`; - const temp = [writeFile(`${path}.chain.crt`, certAndPrivateKey[0]), writeFile(`${path}.key.pem`, certAndPrivateKey[1])]; - const removeFiles = [unlink(`${path}.chain.crt`), unlink(`${path}.key.pem`)]; - await Promise.all(temp); - if (!this.isMatchingPair(`${path}.chain.crt`, `${path}.key.pem`)) { - await Promise.all(removeFiles); - return this.error(message.channel, 'The certificate and private key provided do not match'); - } - - const writeTasks = [writeFile(cert, certAndPrivateKey[0], { encoding: 'utf8' }), writeFile(key, certAndPrivateKey[1], { encoding: 'utf8' }), ...removeFiles]; - await Promise.all(writeTasks); - - return this.success(message.channel, `Updated certificate for ${domain} on port ${port}`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } - - public checkOccurrence(text: string, query: string) { - return (text.match(new RegExp(query, 'g')) || []).length; - } - - public isValidCertificateChain(cert: string) { - if (!cert.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN CERTIFICATE-----')) return false; - if (!cert.replace(/^\s+|\s+$/g, '').endsWith('-----END CERTIFICATE-----')) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----BEGIN CERTIFICATE-----') !== 2) return false; - if (this.checkOccurrence(cert.replace(/^\s+|\s+$/g, ''), '-----END CERTIFICATE-----') !== 2) return false; - return true; - } - - public isValidPrivateKey(key: string) { - if (!key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN PRIVATE KEY-----')) return false; - if (!key.replace(/^\s+|\s+$/g, '').endsWith('-----END PRIVATE KEY-----')) return false; - if (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN PRIVATE KEY-----') !== 1) return false; - if (this.checkOccurrence(key.replace(/^\s+|\s+$/g, ''), '-----END PRIVATE KEY-----') !== 1) return false; - return true; - } - - public async isMatchingPair(cert: string, privateKey: string) { - const result: string = await this.client.util.exec(`${__dirname}/../bin/checkCertSignatures ${cert} ${privateKey}`); - const { ok }: { ok: boolean } = JSON.parse(result); - return ok; - } -} diff --git a/src/commands/deleteaccount.ts b/src/commands/deleteaccount.ts index e5d6696..32dc9aa 100644 --- a/src/commands/deleteaccount.ts +++ b/src/commands/deleteaccount.ts @@ -1,6 +1,7 @@ -import { Message } from 'discord.js'; -import { v4 as uuid } from 'uuid'; -import { Client, Command } from '../class'; +import { Message, PrivateChannel } from 'eris'; +import uuid from 'uuid/v4'; +import { Command } from '../class'; +import { Client } from '..'; export default class DeleteAccount extends Command { constructor(client: Client) { @@ -9,7 +10,7 @@ export default class DeleteAccount extends Command { this.description = 'Delete an account on the Cloud VM'; this.usage = `${this.client.config.prefix}deleteaccount [User Name | User ID | Email Address] [Reason]`; this.aliases = ['deleteacc', 'dacc', 'daccount', 'delete']; - this.permissions = { roles: ['662163685439045632'] }; + this.permissions = { roles: ['475817826251440128', '525441307037007902'] }; this.guildOnly = true; this.enabled = true; } @@ -18,9 +19,9 @@ export default class DeleteAccount extends Command { try { if (!args[1]) return this.client.commands.get('help').run(message, [this.name]); const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); - if (!account) return message.channel.send(`${this.client.stores.emojis.error} ***Account not found.***`); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found.***`); const { root, username, userID, emailAddress, homepath } = account; - if (root) return message.channel.send(`${this.client.stores.emojis.error} ***Permission denied.***`); + if (root) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Permission denied.***`); const pad = (number: number, amount: number): string => '0'.repeat(amount - number.toString().length) + number; const randomNumber = Math.floor(Math.random() * 9999); @@ -28,36 +29,34 @@ export default class DeleteAccount extends Command { try { await this.client.util.messageCollector(message, `***Please confirm that you are permanently deleting ${username}'s account by entering ${verify}. This action cannot be reversed.***`, - 15000, true, [verify], (msg) => msg.author.id === message.author.id); + 15000, true, [verify], (msg) => !(message.channel instanceof PrivateChannel && msg.author.id === message.author.id)); } catch (error) { if (error.message.includes('Did not supply')) return message; throw error; } - const deleting = await message.channel.send(`${this.client.stores.emojis.loading} ***Deleting account, please wait...***`); + const deleting = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Deleting account, please wait...***`); const reason = args.slice(1).join(' '); const logInput = { username, userID, logID: uuid(), moderatorID: message.author.id, type: 4, date: new Date(), reason: null }; if (reason) logInput.reason = reason; - await this.client.util.createModerationLog(args[0], message.author, 4, reason); + await this.client.util.createModerationLog(args[0], message.member, 4, reason); await this.client.util.deleteAccount(username); - message.delete().catch(() => {}); this.client.util.transport.sendMail({ to: account.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - replyTo: 'cloud-help@libraryofcode.org', + from: 'Library of Code sp-us | Cloud Services ', subject: 'Your account has been deleted', html: `

Library of Code | Cloud Services

-

Your Cloud Account has been deleted by a Director. If your account was deleted due to infractions, this will not be appealable. We're sorry to see you go.

+

Your Cloud Account has been deleted by our Engineers. There is no way to recover your files and this decision cannot be appealed. We're sorry to see you go.

Reason: ${reason}

-

Director: ${message.author.username}

- +

Engineer: ${message.author.username}

+ Library of Code sp-us | Support Team `, }); - return deleting.edit(`${this.client.stores.emojis.success} ***Account ${username} has been deleted by Director ${message.author.username}#${message.author.discriminator}***`); + return deleting.edit(`${this.client.stores.emojis.success} ***Account ${username} has been deleted by Engineer ${message.author.username}#${message.author.discriminator}***`); } catch (error) { return this.client.util.handleError(error, message, this); } diff --git a/src/commands/disk.ts b/src/commands/disk.ts new file mode 100644 index 0000000..b1ac0d6 --- /dev/null +++ b/src/commands/disk.ts @@ -0,0 +1,45 @@ +import { Message } from 'eris'; +import moment from 'moment'; +import { Client } from '..'; +import { RichEmbed, Command } from '../class'; +import { dataConversion } from '../functions'; +// eslint-disable-next-line import/no-unresolved +import 'moment-precise-range-plugin'; + +export default class Disk extends Command { + constructor(client: Client) { + super(client); + this.name = 'disk'; + this.description = 'Checks the used disk space by a user'; + this.usage = `${this.client.config.prefix}disk [Username/User ID/Email]`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = false; + } + + async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); + if (account.root || args[0].includes('./')) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Permission denied***`); + const diskReply = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Fetching total disk size may up to 10 minutes. This message will edit when the disk size has been located.***`); + const start = Date.now(); + const result = await this.client.util.exec(`du -s ${account.homepath}`); + const end = Date.now(); + // @ts-ignore + const totalTime: string = moment.preciseDiff(start, end); + const embed = new RichEmbed(); + embed.setTitle('Disk Usage'); + embed.setColor('ff0000'); + embed.setDescription(result.split(/ +/g)[1]); + embed.addField('Result', dataConversion(Number(result.split(/ +/g)[0])), true); + embed.addField('Time taken', totalTime, true); + embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTimestamp(); + // @ts-ignore + return diskReply.edit({ content: '', embed }); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/emailcode.ts b/src/commands/emailcode.ts deleted file mode 100644 index 3532fbd..0000000 --- a/src/commands/emailcode.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { randomBytes } from 'crypto'; -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class EmailCode extends Command { - constructor(client: Client) { - super(client); - this.name = 'emailcode'; - this.description = 'Sends a code to an email address to use for address verification.'; - this.usage = `${this.client.config.prefix}emailcode `; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.aliases = ['code']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const code = randomBytes(5).toString('hex'); - if (!this.client.util.isValidEmail(args[0])) return this.error(message.channel, 'The email address provided is invalid.'); - this.client.util.transport.sendMail({ - to: args[0], - from: 'Library of Code sp-us | Cloud Services ', - subject: 'Email Verification Code', - html: ` - - -

Library of Code | Cloud Services

-

Please provide the code provided below to the Staff member working with you on account creation.

-

${code}

-

Want to support us?

-

You can support us on Patreon! Head to our Patreon page and feel free to donate as much or as little as you want!
Donating $5 or more will grant you Tier 3, which means we will manage your account at your request, longer certificates, increased Tier limits as well as some roles in the server!

- Library of Code sp-us | Support Team - - `, - }); - return this.success(message.channel, `Code: \`${code}\` | Email Address: ${args[0]}`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/eval.ts b/src/commands/eval.ts index 34a47e7..16d9fc3 100644 --- a/src/commands/eval.ts +++ b/src/commands/eval.ts @@ -1,67 +1,56 @@ -/* eslint-disable no-eval */ -import axios from 'axios'; -import { Message } from 'discord.js'; -import { inspect } from 'util'; -import { Client, Command } from '../class'; - -export default class Eval extends Command { - constructor(client: Client) { - super(client); - this.name = 'eval'; - this.aliases = ['e']; - this.description = 'Evaluate JavaScript code'; - this.enabled = true; - this.permissions = { users: ['253600545972027394', '278620217221971968', '239261547959025665'] }; - this.guildOnly = false; - } - - public async run(message: Message) { - try { - const args = message.content.slice(this.client.config.prefix.length).trim().split(' ').slice(1); - let evalString = args.join(' ').trim(); - let evaled: any; - let depth = 0; - - if (args[0] && args[0].startsWith('-d')) { - depth = Number(args[0].replace('-d', '')); - if (!depth || depth < 0) depth = 0; - args.shift(); - evalString = args.join(' ').trim(); - } - if (args[0] === '-a') { - args.shift(); - evalString = `(async () => { ${args.join(' ').trim()} })()`; - } - - try { - evaled = await eval(evalString); - if (typeof evaled !== 'string') { - evaled = inspect(evaled, { depth }); - } - if (evaled === undefined) { - evaled = 'undefined'; - } - } catch (error) { - evaled = error.stack; - } - - evaled = evaled.replace(new RegExp(this.client.config.token, 'gi'), 'token'); - evaled = evaled.replace(new RegExp(this.client.config.emailPass, 'gi'), 'emailPass'); - evaled = evaled.replace(new RegExp(this.client.config.cloudflare, 'gi'), 'cloudflare'); - - const display = this.client.util.splitString(evaled, 1975); - if (display[5]) { - try { - const { data } = await axios.post('https://snippets.cloud.libraryofcode.org/documents', display.join('')); - return message.channel.send(`${this.client.stores.emojis.success} Your evaluation evaled can be found on https://snippets.cloud.libraryofcode.org/${data.key}`); - } catch (error) { - return message.channel.send(`${this.client.stores.emojis.error} ${error}`); - } - } - - return display.forEach((m) => message.channel.send(`\`\`\`js\n${m}\n\`\`\``)); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +/* eslint-disable no-eval */ +import { Message } from 'eris'; +import { inspect } from 'util'; +import axios from 'axios'; +import { Client } from '..'; +import { Command } from '../class'; + +export default class Eval extends Command { + constructor(client: Client) { + super(client); + this.name = 'eval'; + this.aliases = ['e']; + this.description = 'Evaluate JavaScript code'; + this.enabled = true; + this.permissions = { users: ['253600545972027394', '278620217221971968'] }; + this.guildOnly = false; + } + + public async run(message: Message, args: string[]) { + try { + // const evalMessage = message.content.slice(this.client.config.prefix.length).split(' ').slice(1).join(' '); + let evaled: any; + + try { + evaled = await eval(args.join(' ').trim()); + if (typeof evaled !== 'string') { + evaled = inspect(evaled, { depth: 0 }); + } + if (evaled === undefined) { + evaled = 'undefined'; + } + } catch (error) { + evaled = error.stack; + } + + evaled = evaled.replace(new RegExp(this.client.config.token, 'gi'), 'juul'); + evaled = evaled.replace(new RegExp(this.client.config.emailPass, 'gi'), 'juul'); + evaled = evaled.replace(new RegExp(this.client.config.cloudflare, 'gi'), 'juul'); + + + const display = this.client.util.splitString(evaled, 1975); + if (display[5]) { + try { + const { data } = await axios.post('https://snippets.cloud.libraryofcode.org/documents', display.join('')); + return message.channel.createMessage(`${this.client.stores.emojis.success} Your evaluation evaled can be found on https://snippets.cloud.libraryofcode.org/${data.key}`); + } catch (error) { + return message.channel.createMessage(`${this.client.stores.emojis.error} ${error}`); + } + } + + return display.forEach((m) => message.channel.createMessage(`\`\`\`js\n${m}\n\`\`\``)); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/exec.ts b/src/commands/exec.ts index 8aba188..164efcf 100644 --- a/src/commands/exec.ts +++ b/src/commands/exec.ts @@ -1,6 +1,7 @@ -import { Message } from 'discord.js'; +import { Message } from 'eris'; import axios from 'axios'; -import { Client, Command } from '../class'; +import { Client } from '..'; +import { Command } from '../class'; export default class Exec extends Command { constructor(client: Client) { @@ -9,7 +10,7 @@ export default class Exec extends Command { this.description = 'Executes command'; this.aliases = ['ex']; this.enabled = true; - this.permissions = { users: ['253600545972027394', '278620217221971968', '239261547959025665'] }; + this.permissions = { users: ['253600545972027394', '278620217221971968'] }; this.guildOnly = false; } @@ -17,10 +18,10 @@ export default class Exec extends Command { try { if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const response = await this.loading(message.channel, `\`${args.join(' ')}\``); + const response = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Executing \`${args.join(' ')}\`***`); let result: string; try { - result = await this.client.util.exec(args.join(' '), { cwd: '/opt/CloudServices' }); + result = await this.client.util.exec(args.join(' ')); } catch (error) { result = error.message; } @@ -38,7 +39,7 @@ export default class Exec extends Command { } await response.delete(); - return splitResult.forEach((m) => message.channel.send(`\`\`\`bash\n${m}\n\`\`\``)); + return splitResult.forEach((m) => message.channel.createMessage(`\`\`\`bash\n${m}\n\`\`\``)); } catch (error) { return this.client.util.handleError(error, message, this); } diff --git a/src/commands/getreferral.ts b/src/commands/getreferral.ts deleted file mode 100644 index 2514c8b..0000000 --- a/src/commands/getreferral.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class GetReferral extends Command { - constructor(client: Client) { - super(client); - this.name = 'getreferral'; - this.description = 'Fetches your referral code.'; - this.usage = `${this.client.config.prefix}getreferral`; - this.aliases = ['gf', 'gr']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel, 'You do not have a Cloud Services account.'); - - return this.client.users.fetch(message.author.id).then((u) => { - u.send(`__**CS Account Referral Code**__\n*Referral codes are considered to be somewhat private information. Applicants with referral codes have a greater chance of approval, don't refer someone you don't trust :).*\nYour referral code: \`${account.referralCode}\`\nReferrals Granted: \`${account.totalReferrals ? account.totalReferrals : '0'}\``); - }).catch(() => this.error(message.channel, 'Could not send you a DM.')); - } catch (err) { - return this.client.util.handleError(err, message, this); - } - } -} diff --git a/src/commands/help.ts b/src/commands/help.ts index 2f2ca2b..6ad81aa 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,66 +1,70 @@ -import { Message, MessageEmbed } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class Help extends Command { - constructor(client: Client) { - super(client); - this.name = 'help'; - this.description = 'Display a list of commands'; - this.usage = `${this.client.config.prefix}help | ${this.client.config.prefix}help ping`; - this.aliases = ['commands']; - this.enabled = true; - } - - // eslint-disable-next-line consistent-return - public async run(message: Message, args?: string[]) { - try { - if (!args[0]) { - const cmdList: Command[] = []; - this.client.commands.forEach((c) => cmdList.push(c)); - const commands = this.client.commands.map((c) => { - const aliases = c.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', '); - const perms: string[] = []; - let allowedRoles = c.permissions && c.permissions.roles && c.permissions.roles.map((r) => `<@&${r}>`).join(', '); - if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } - let allowedUsers = c.permissions && c.permissions.users && c.permissions.users.map((u) => `<@!${u}>`).join(', '); - if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } - const displayedPerms = perms.length ? `**Permissions:**\n${perms.join('\n')}` : ''; - return { name: `${this.client.config.prefix}${c.name}`, value: `**Description:** ${c.description}\n**Aliases:** ${aliases}\n**Usage:** ${c.usage}\n${displayedPerms}`, inline: false }; - }); - - const splitCommands = this.client.util.splitFields(commands); - const cmdPages: MessageEmbed[] = []; - splitCommands.forEach((splitCmd) => { - const embed = new MessageEmbed(); - embed.setTimestamp(); - embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL()); - embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL()); - embed.setDescription(`Command list for ${this.client.user.username}`); - splitCmd.forEach((c) => embed.addField(c.name, c.value, c.inline)); - return cmdPages.push(embed); - }); - if (cmdPages.length === 1) return message.channel.send({ embeds: [cmdPages[0]] }); - return this.client.util.createPaginationEmbed(message, cmdPages); - } - const resolved = await this.client.util.resolveCommand(args, message); - if (!resolved) return message.channel.send(`${this.client.stores.emojis.error} **Command not found!**`); - const { cmd } = resolved; - const perms: string[] = []; - let allowedRoles = cmd.permissions && cmd.permissions.roles && cmd.permissions.roles.map((r) => `<@&${r}>`).join(', '); - if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } - let allowedUsers = cmd.permissions && cmd.permissions.users && cmd.permissions.users.map((u) => `<@!${u}>`).join(', '); - if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } - const displayedPerms = perms.length ? `\n**Permissions:**\n${perms.join('\n')}` : ''; - const aliases = cmd.aliases.length ? `\n**Aliases:** ${cmd.aliases.map((alias) => `${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName} ` : ''}${alias}`).join(', ')}` : ''; - const subcommands = cmd.subcommands.size ? `\n**Subcommands:** ${cmd.subcommands.map((s) => `${cmd.name} ${s.name}`).join(', ')}` : ''; - const embed = new MessageEmbed(); - embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL()); - embed.setTitle(`${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName}${cmd.name}` : cmd.name}`); embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL()); - const description = `**Description**: ${cmd.description}\n**Usage:** ${cmd.usage}${aliases}${displayedPerms}${subcommands}`; - embed.setDescription(description); - message.channel.send({ embeds: [embed] }); - } catch (error) { - this.client.util.handleError(error, message, this); - } - } -} +import { Message } from 'eris'; +import { createPaginationEmbed } from 'eris-pagination'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class Help extends Command { + constructor(client: Client) { + super(client); + this.name = 'help'; + this.description = 'Display a list of commands'; + this.usage = `${this.client.config.prefix}help | ${this.client.config.prefix}help ping`; + this.aliases = ['commands']; + this.enabled = true; + } + + // eslint-disable-next-line consistent-return + public async run(message: Message, args?: string[]) { + try { + if (!args[0]) { + const cmdList: Command[] = []; + this.client.commands.forEach((c) => cmdList.push(c)); + const commands = this.client.commands.map((c) => { + const aliases = c.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', '); + const perms: string[] = []; + let allowedRoles = c.permissions && c.permissions.roles && c.permissions.roles.map((r) => `<@&${r}>`).join(', '); + if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } + let allowedUsers = c.permissions && c.permissions.users && c.permissions.users.map((u) => `<@${u}>`).join(', '); + if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } + const displayedPerms = perms.length ? `**Permissions:**\n${perms.join('\n')}` : ''; + return { name: `${this.client.config.prefix}${c.name}`, value: `**Description:** ${c.description}\n**Aliases:** ${aliases}\n**Usage:** ${c.usage}\n${displayedPerms}`, inline: false }; + }); + + const splitCommands = this.client.util.splitFields(commands); + const cmdPages: RichEmbed[] = []; + splitCommands.forEach((splitCmd) => { + const embed = new RichEmbed(); + embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); + embed.setDescription(`Command list for ${this.client.user.username}`); + splitCmd.forEach((c) => embed.addField(c.name, c.value, c.inline)); + return cmdPages.push(embed); + }); + // @ts-ignore + if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); + // @ts-ignore + return createPaginationEmbed(message, this.client, cmdPages); + } + const resolved = await this.client.util.resolveCommand(args, message); + if (!resolved) return message.channel.createMessage(`${this.client.stores.emojis.error} **Command not found!**`); + const { cmd } = resolved; + const perms: string[] = []; + let allowedRoles = cmd.permissions && cmd.permissions.roles && cmd.permissions.roles.map((r) => `<@&${r}>`).join(', '); + if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } + let allowedUsers = cmd.permissions && cmd.permissions.users && cmd.permissions.users.map((u) => `<@${u}>`).join(', '); + if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } + const displayedPerms = perms.length ? `\n**Permissions:**\n${perms.join('\n')}` : ''; + const aliases = cmd.aliases.length ? `\n**Aliases:** ${cmd.aliases.map((alias) => `${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName} ` : ''}${alias}`).join(', ')}` : ''; + const subcommands = cmd.subcommands.size ? `\n**Subcommands:** ${cmd.subcommands.map((s) => `${cmd.name} ${s.name}`).join(', ')}` : ''; + const embed = new RichEmbed(); + embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTitle(`${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName}${cmd.name}` : cmd.name}`); embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); + const description = `**Description**: ${cmd.description}\n**Usage:** ${cmd.usage}${aliases}${displayedPerms}${subcommands}`; + embed.setDescription(description); + // @ts-ignore + message.channel.createMessage({ embed }); + } catch (error) { + this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/index.ts b/src/commands/index.ts index eaa87c7..f8e4bf7 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,34 +1,25 @@ -export { default as addreferral } from './addreferral'; -export { default as announce } from './announce'; -export { default as applyt2 } from './applyt2'; -export { default as authreferral } from './authreferral'; -export { default as bearer } from './bearer'; -export { default as cloudflare } from './cloudflare'; -export { default as createaccount } from './createaccount'; -export { default as cwg } from './cwg'; -export { default as deleteaccount } from './deleteaccount'; -export { default as emailcode } from './emailcode'; -export { default as eval } from './eval'; -export { default as exec } from './exec'; -export { default as getreferral } from './getreferral'; -export { default as help } from './help'; -export { default as info } from './info'; -export { default as limits } from './limits'; -export { default as load } from './load'; -export { default as lock } from './lock'; -export { default as modlogs } from './modlogs'; -export { default as notify } from './notify'; -export { default as ping } from './ping'; -export { default as pull } from './pull'; -export { default as resetpassword } from './resetpassword'; -export { default as restart } from './restart'; -export { default as setlimit } from './setlimit'; -export { default as sysinfo } from './sysinfo'; -export { default as systemd } from './systemd'; -export { default as tier } from './tier'; -export { default as unban } from './unban'; -export { default as unlock } from './unlock'; -export { default as usermod } from './usermod'; -export { default as users } from './users'; -export { default as warn } from './warn'; -export { default as whois } from './whois'; +export { default as announce } from './announce'; +export { default as bearer } from './bearer'; +export { default as createAccount } from './createaccount'; +export { default as cwg } from './cwg'; +export { default as deleteaccount } from './deleteaccount'; +export { default as disk } from './disk'; +export { default as eval } from './eval'; +export { default as exec } from './exec'; +export { default as help } from './help'; +export { default as load } from './load'; +export { default as lock } from './lock'; +export { default as modlogs } from './modlogs'; +export { default as notify } from './notify'; +export { default as parse } from './parse'; +export { default as parseall } from './parseall'; +export { default as ping } from './ping'; +export { default as pull } from './pull'; +export { default as resetpassword } from './resetpassword'; +export { default as restart } from './restart'; +export { default as securesign } from './securesign'; +export { default as sysinfo } from './sysinfo'; +export { default as unban } from './unban'; +export { default as unlock } from './unlock'; +export { default as warn } from './warn'; +export { default as whois } from './whois'; diff --git a/src/commands/info.ts b/src/commands/info.ts deleted file mode 100644 index e3a87cb..0000000 --- a/src/commands/info.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Message, MessageEmbed } from 'discord.js'; -import { totalmem } from 'os'; -import { Client, Command } from '../class'; -import { version as discordjsVersion } from '../../node_modules/discord.js/package.json'; -import { version as expressVersion } from '../../node_modules/express/package.json'; -import { version as mongooseVersion } from '../../node_modules/mongoose/package.json'; -import { version as ioredisVersion } from '../../node_modules/ioredis/package.json'; -import { version as tscVersion } from '../../node_modules/typescript/package.json'; - -export default class Info extends Command { - constructor(client: Client) { - super(client); - this.name = 'info'; - this.description = 'Provides information about CSD.'; - this.usage = `${this.client.config.prefix}info`; - this.enabled = true; - } - - public async run(message: Message) { - try { - const embed = new MessageEmbed(); - embed.setTitle('Information'); - embed.setThumbnail(this.client.user.avatarURL()); - embed.addField('Language(s)', '<:ts:604565354554982401> TypeScript, <:Go:703449475405971466> Go', true); - embed.addField('Runtime', `Node (${process.version})`, true); - embed.addField('Compilers/Transpilers', `TypeScript [tsc] (${tscVersion}) | Go [gc] (${await this.client.util.exec('go version')})`, true); - embed.addField('Process Manager', `SystemD (${(await this.client.util.exec('systemd --version')).split('\n')[0]})`, true); - embed.addField('Discord Library', `Discord.js (${discordjsVersion})`, true); - embed.addField('HTTP Server Library', `Express (${expressVersion})`, true); - embed.addField('Database Library', `MongoDB w/ Mongoose ODM (${mongooseVersion})`, true); - embed.addField('Cache Library', `Redis w/ IORedis (${ioredisVersion})`, true); - embed.addField('Memory Usage', `${Math.round(process.memoryUsage().rss / 1024 / 1024)} MB / ${Math.round(totalmem() / 1024 / 1024 / 1024)} GB`, true); - embed.addField('Repository', 'https://loc.sh/csdgit | Licensed under GNU Affero General Public License V3', true); - return message.channel.send({ embeds: [embed] }); - } catch (err) { - return this.client.util.handleError(err, message, this); - } - } -} diff --git a/src/commands/limits.ts b/src/commands/limits.ts deleted file mode 100644 index ba1c665..0000000 --- a/src/commands/limits.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Message, MessageEmbed } from 'discord.js'; -import { Client, Command } from '../class'; -import { dataConversion } from '../functions'; -import setRamNotification from './limits_setramnotification'; - -export default class Limits extends Command { - constructor(client: Client) { - super(client); - this.name = 'limits'; - this.description = 'Views resource limits for each tier.'; - this.usage = `${this.client.config.prefix}limits`; - this.subcmds = [setRamNotification]; - this.enabled = true; - } - - public async run(message: Message) { - try { - const tiers = await this.client.db.Tier.find(); - const embed = new MessageEmbed(); - embed.setTitle('Resource Limit Information'); - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (account) { - const tier = await this.client.db.Tier.findOne({ id: account.tier }); - let msg: string; - if (account.ramLimitNotification !== -1) { - msg = `You will be notified when you are using ${account.ramLimitNotification} MB+.`; - } else { - msg = 'You will not be notified about impending resource limits for your account.'; - } - embed.setDescription(`Your resource limit is ${dataConversion(tier.resourceLimits?.ram * 1024 * 1024) ?? '0 B'}.\n${msg}`); - } - embed.setFooter(this.client.user.username, this.client.user.avatarURL()); - embed.setTimestamp(); - for (const tier of tiers.sort((a, b) => a.id - b.id)) { - embed.addField(`Tier ${tier.id}`, `**RAM:** ${dataConversion(tier.resourceLimits?.ram * 1024 * 1024) ?? '0 B'}\n**Storage:** ${dataConversion(tier.resourceLimits?.storage * 1024 * 1024) ?? '0 B'}`); - } - return message.channel.send({ embeds: [embed] }); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/limits_setramnotification.ts b/src/commands/limits_setramnotification.ts deleted file mode 100644 index c915eb6..0000000 --- a/src/commands/limits_setramnotification.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Message, TextChannel } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class Limits_SetRAMNotification extends Command { - constructor(client: Client) { - super(client); - this.name = 'set-ram-notification'; - this.description = 'Sets your personal preference for receiving RAM resource limit notifications. Set the limit to "-1" to disable notifications.'; - this.usage = `${this.client.config.prefix}limits set-ram-notification `; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return this.error(message.channel as TextChannel, 'You do not appear to have an account.'); - const tier = await this.client.db.Tier.findOne({ id: account.tier }); - if (Number(args[0]) >= tier.resourceLimits.ram) return this.error(message.channel as TextChannel, 'You cannot set your notification limit to be set to or above your hard limit.'); - if (Number(args[0]) < 0) return this.error(message.channel as TextChannel, 'You cannot set your notification limit to a negative number.'); - if (Number(args[0]) === 0) { - await account.updateOne({ $set: { ramLimitNotification: 0 } }); - return this.success(message.channel as TextChannel, 'You have disabled notifications.'); - } - await account.updateOne({ $set: { ramLimitNotification: Number(args[0]) } }); - return this.success(message.channel as TextChannel, `You will now receive notifications when you go above ${Number(args[0])} MB.`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} diff --git a/src/commands/load.ts b/src/commands/load.ts index ab6a279..ec4130d 100644 --- a/src/commands/load.ts +++ b/src/commands/load.ts @@ -1,5 +1,6 @@ -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command } from '../class'; export default class Load extends Command { constructor(client: Client) { @@ -7,7 +8,7 @@ export default class Load extends Command { this.name = 'load'; this.description = '(Re)loads command, config or util'; this.aliases = ['reload']; - this.permissions = { roles: ['662163685439045632'] }; + this.permissions = { users: ['253600545972027394', '278620217221971968'] }; this.enabled = true; } @@ -16,9 +17,9 @@ export default class Load extends Command { if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); const allowed = ['config', 'util', 'command']; const type = args[0].toLowerCase(); - if (!allowed.includes(type)) return this.error(message.channel, 'Invalid type provided to (re)load.'); + if (!allowed.includes(type)) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid type to (re)load***`); - const corepath = '/opt/CloudServices/dist'; + const corepath = '/var/CloudServices/dist'; if (type === 'config') { this.client.config = require(`${corepath}/config.json`); delete require.cache[`${corepath}/config.json`]; @@ -30,22 +31,20 @@ export default class Load extends Command { try { delete require.cache[`${corepath}/commands/index.js`]; delete require.cache[`${corepath}/commands/${args[1]}.js`]; - Object.keys(require.cache).filter((path) => path.includes(`${args[1]}_`)).forEach((path) => delete require.cache[path]); - const cmdIndex = require('.'); - let cmd = cmdIndex[args[1]]; - if (!cmd) return this.error(message.channel, 'Could not find file.'); - cmd = require(`${corepath}/commands/${args[1]}`).default; + const cmdIndex = require('../commands'); + let Cmd = cmdIndex[args[1]]; + if (!Cmd) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Could not find file***`); + Cmd = require(`${corepath}/commands/${args[1]}`).default; this.client.commands.remove(args[1]); - this.client.loadCommand(cmd); + this.client.loadCommand(Cmd); delete require.cache[`${corepath}/commands/index.js`]; delete require.cache[`${corepath}/commands/${args[1]}.js`]; - Object.keys(require.cache).filter((path) => path.includes(`${args[1]}_`)).forEach((path) => delete require.cache[path]); } catch (error) { - if (error.message.includes('Cannot find module')) return this.error(message.channel, 'Could not find file.'); + if (error.message.includes('Cannot find module')) return message.channel.createMessage(`${this.client.stores.emojis} ***Cannot find file***`); throw error; } } - return this.success(message.channel, `Reloaded ${type}.`); + return message.channel.createMessage(`${this.client.stores.emojis.success} Reloaded ${type}`); } catch (error) { return this.client.util.handleError(error, message, this); } diff --git a/src/commands/lock.ts b/src/commands/lock.ts index 7f2a827..4a3f33f 100644 --- a/src/commands/lock.ts +++ b/src/commands/lock.ts @@ -1,55 +1,55 @@ -import moment, { unitOfTime } from 'moment'; -import { Message } from 'discord.js'; -import { Client, Command } from '../class'; - -export default class Lock extends Command { - constructor(client: Client) { - super(client); - this.name = 'lock'; - this.description = 'Locks an account.'; - this.permissions = { roles: ['662163685439045632', '701454780828221450'] }; - this.enabled = true; - this.usage = `${this.client.config.prefix}lock [User Name | User ID/Mention]