diff --git a/.eslintrc.json b/.eslintrc.json index da54f5a..8e5b667 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,47 +1,45 @@ { - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - } - }, - "env": { - "es6": true, - "node": true - }, - "extends": [ - "airbnb-base" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "linebreak-style": "off", - "no-unused-vars": "off", - "max-len": "off", - "import/no-dynamic-require": "off", - "global-require": "off", - "class-methods-use-this":"off", - "no-restricted-syntax": "off", - "camelcase": "off", - "indent": "warn", - "object-curly-newline": "off", - "import/prefer-default-export": "off", - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": 2, - "import/extensions": "off", - "no-param-reassign": "off", - "no-underscore-dangle": "off" - }, - "ignorePatterns": "**/*.js" + "settings": { + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + }, + "env": { + "es6": true, + "node": true + }, + "extends": ["airbnb-base"], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "linebreak-style": "off", + "no-unused-vars": "off", + "max-len": "off", + "import/no-dynamic-require": "off", + "global-require": "off", + "class-methods-use-this": "off", + "no-restricted-syntax": "off", + "camelcase": "off", + "indent": "warn", + "object-curly-newline": "off", + "import/prefer-default-export": "off", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": 2, + "import/extensions": "off", + "no-param-reassign": "off", + "no-underscore-dangle": "off", + "keyword-spacing": "off", + "no-multiple-empty-lines": "off" + }, + "ignorePatterns": "**/*.js" } diff --git a/src/api/board.ins/routes/root.ts b/src/api/board.ins/routes/root.ts index 2c2dd21..6aef088 100644 --- a/src/api/board.ins/routes/root.ts +++ b/src/api/board.ins/routes/root.ts @@ -68,7 +68,74 @@ export default class Root extends Route { return res.status(200).json({ code: this.constants.codes.SUCCESS, - message: `Created new Executive Order with ID ${executiveOrder.oID} by the ${staffDiscord.username}#${staffDiscord.discriminator}, ${staffInformation.pn.join(', ')}.`, + message: `Created new Executive Order with ID ${executiveOrder.oID} by ${staffDiscord.username}#${staffDiscord.discriminator}, ${staffInformation.pn.join(', ')}.`, + }); + }); + + this.router.post('/motions/new', async (req, res) => { + if (!req.body.pin) { + return res.status(401).json({ + code: this.constants.codes.UNAUTHORIZED, + message: this.constants.messages.UNAUTHORIZED, + }); + } + + const director = await this.server.client.db.Score.findOne({ pin: req.body.pin }); + const staffGuild = this.server.client.guilds.get('446067825673633794') || await this.server.client.getRESTGuild('446067825673633794'); + + if (!director || !staffGuild.members.get(director.userID)?.roles?.includes('662163685439045632')) { + return res.status(401).json({ + code: this.constants.codes.UNAUTHORIZED, + message: this.constants.messages.UNAUTHORIZED, + }); + } + + if (!req.body.subject) { + return res.status(400).json({ + code: this.constants.codes.CLIENT_ERROR, + message: this.constants.messages.CLIENT_ERROR, + }); + } + + if (!req.body.body) { + return res.status(400).json({ + code: this.constants.codes.CLIENT_ERROR, + message: this.constants.messages.CLIENT_ERROR, + }); + } + + const motionID = genUUID(); + + const staffDiscord = this.server.client.users.get(director.userID) || await this.server.client.getRESTUser(director.userID); + const staffInformation = await this.server.client.db.Staff.findOne({ userID: director.userID }); + + const embed = new RichEmbed(); + embed.setTitle('Motion'); + embed.setAuthor(`${staffDiscord.username}#${staffDiscord.discriminator}, ${staffInformation.pn.join(', ')}`, staffDiscord.avatarURL); + embed.setColor('#66e1ff'); + embed.addField('Subject', req.body.subject); + embed.addField('Body', req.body.body); + embed.addField('ID', motionID); + embed.setTimestamp(new Date()); + + const channel = this.server.client.getChannel('807444198969835550'); + const motionMessage = await channel.createMessage({ embed }); + await motionMessage.addReaction(this.server.client.util.emojis.SUCCESS); + await motionMessage.addReaction(this.server.client.util.emojis.ERROR); + + const motion = await this.server.client.db.Motion.create({ + issuedBy: director.userID, + subject: req.body.subject, + body: req.body.body, + at: new Date(), + oID: motionID, + motionMessage: motionMessage.id, + processed: false, + }); + + return res.status(200).json({ + code: this.constants.codes.SUCCESS, + message: `Created new Motion with ID ${motion.oID} by ${staffDiscord.username}#${staffDiscord.discriminator}, ${staffInformation.pn.join(', ')}.`, }); }); } diff --git a/src/class/Client.ts b/src/class/Client.ts index d6b1a93..12816ac 100644 --- a/src/class/Client.ts +++ b/src/class/Client.ts @@ -12,6 +12,7 @@ import { Member, MemberInterface, Merchant, MerchantInterface, Moderation, ModerationInterface, + Motion, MotionInterface, NNTrainingData, NNTrainingDataInterface, Note, NoteInterface, PagerNumber, PagerNumberInterface, @@ -44,7 +45,27 @@ export default class Client extends eris.Client { public stripe: Stripe; - public db: { Customer: mongoose.Model, CustomerPortal: mongoose.Model, ExecutiveOrder: mongoose.Model, File: mongoose.Model, Member: mongoose.Model, Merchant: mongoose.Model, Moderation: mongoose.Model, NNTrainingData: mongoose.Model, Note: mongoose.Model, PagerNumber: mongoose.Model, Promo: mongoose.Model, Rank: mongoose.Model, Redirect: mongoose.Model, Score: mongoose.Model, ScoreHistorical: mongoose.Model, Staff: mongoose.Model, Stat: mongoose.Model, local: { muted: LocalStorage } }; + public db: { + Customer: mongoose.Model, + CustomerPortal: mongoose.Model, + ExecutiveOrder: mongoose.Model, + File: mongoose.Model, + Member: mongoose.Model, + Merchant: mongoose.Model, + Moderation: mongoose.Model, + Motion: mongoose.Model, + NNTrainingData: mongoose.Model, + Note: mongoose.Model, + PagerNumber: mongoose.Model, + Promo: mongoose.Model, + Rank: mongoose.Model, + Redirect: mongoose.Model, + Score: mongoose.Model, + ScoreHistorical: mongoose.Model, + Staff: mongoose.Model, + Stat: mongoose.Model, + local: { muted: LocalStorage } + }; constructor(token: string, options?: eris.ClientOptions) { super(token, options); @@ -52,10 +73,9 @@ export default class Client extends eris.Client { this.events = new Collection(); this.intervals = new Collection(); this.queue = new Queue(this); - this.db = { Customer, CustomerPortal, ExecutiveOrder, File, Member, Merchant, Moderation, NNTrainingData, Note, PagerNumber, Promo, Rank, Redirect, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } }; + this.db = { Customer, CustomerPortal, ExecutiveOrder, File, Member, Merchant, Moderation, Motion, NNTrainingData, Note, PagerNumber, Promo, Rank, Redirect, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } }; } - public async loadDatabase() { await mongoose.connect(this.config.mongoDB, { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 50 }); diff --git a/src/class/Util.ts b/src/class/Util.ts index 25c0d3f..1005fd1 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -71,7 +71,6 @@ export default class Util { } return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`; } - public async exec(command: string, _options: childProcess.ExecOptions = {}): Promise { const ex = promisify(childProcess.exec); @@ -105,7 +104,7 @@ export default class Util { * @param query Command input * @param message Only used to check for errors */ - public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { + public resolveCommand(query: string | string[], message?: Message): Promise<{ cmd: Command, args: string[] }> { try { let resolvedCommand: Command; if (typeof query === 'string') query = query.split(' '); @@ -195,7 +194,7 @@ export default class Util { 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}[][] = [[]]; + 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(); diff --git a/src/commands/apply.ts b/src/commands/apply.ts index c5c2602..1b41afc 100644 --- a/src/commands/apply.ts +++ b/src/commands/apply.ts @@ -38,7 +38,7 @@ export default class Apply extends Command { validation: (member: Member) => member.roles.includes('546457886440685578'), func: async (client: Client, ...data: any[]) => { const member = await client.guilds.get(client.config.guildID).getRESTMember(data[0]); - const ax = require('axios'); + const ax = require('axios'); await ax({ method: 'get', url: `https://api.cloud.libraryofcode.org/wh/t2?userID=${member.id}&auth=${client.config.internalKey}`, diff --git a/src/commands/billing_t3.ts b/src/commands/billing_t3.ts index bdc4823..63568cb 100644 --- a/src/commands/billing_t3.ts +++ b/src/commands/billing_t3.ts @@ -23,7 +23,7 @@ export default class Billing_T3 extends Command { emailAddress?: string, tier?: number, supportKey?: string, - }> (await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data; + }>(await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data; if (!response.found) return this.error(message.channel, 'CS Account not found.'); const customer = await this.client.db.Customer.findOne({ userID: message.author.id }); diff --git a/src/intervals/motions.ts b/src/intervals/motions.ts new file mode 100644 index 0000000..6470102 --- /dev/null +++ b/src/intervals/motions.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-await-in-loop */ +import type { TextChannel } from 'eris'; +import { Client, RichEmbed } from '../class'; + +export default (client: Client) => { + const interval = setInterval(async () => { + const motions = await client.db.Motion.find({ processed: false }); + const directorLogs = client.getChannel('807444198969835550'); + + for (const motion of motions) { + const motionMessage = await directorLogs.getMessage(motion.motionMessage); + + if ((Date.now() - motionMessage.createdAt) > 86400) { + const yea = await motionMessage.getReaction(client.util.emojis.SUCCESS); + const nay = await motionMessage.getReaction(client.util.emojis.ERROR); + const present = yea.length + nay.length; + const totalDirectors = 5; + const absent = totalDirectors - present; + + await client.db.Motion.updateOne({ oID: motion.oID }, { + processed: true, + voteResults: { + yea: yea.length, + nay: nay.length, + present, + absent, + }, + }); + + const directorDiscord = client.users.get(motion.issuedBy); + const directorProfile = await client.db.Staff.findOne({ userID: motion.issuedBy }); + + const embed = new RichEmbed(); + embed.setAuthor(`${directorDiscord.username}#${directorDiscord.discriminator}, ${directorProfile.pn.join(', ')}`, directorDiscord.avatarURL); + embed.setFooter(`${directorProfile.position} | Library of Code sp-us | Board of Directors`, 'https://static.libraryofcode.org/loccommunityadmin.png'); + let colour; + if (yea.length > nay.length) colour = '#27b17a'; + else if (yea.length === nay.length) colour = '#ffb34d'; + else colour = '#ff474a'; + embed.setColor(colour); + embed.addField('Motion ID', motion.oID); + embed.addField('Result', `- **Yea:** ${yea.length}\n**Nay:** ${nay.length}\n**Present:** ${present}\n**Absent:** ${absent}\n**Total:** ${present + absent}`); + embed.setTimestamp(); + + await directorLogs.createMessage({ content: directorDiscord.mention, embed }); + } + } + }, 300000); + + return interval; +}; diff --git a/src/models/ExecutiveOrder.ts b/src/models/ExecutiveOrder.ts index f639358..be4127a 100644 --- a/src/models/ExecutiveOrder.ts +++ b/src/models/ExecutiveOrder.ts @@ -13,7 +13,7 @@ const ExecutiveOrder = new Schema({ subject: { type: String, required: true }, body: { type: String, required: true }, at: { type: Date, required: true }, - oID: { type: String, required: true, unique: true } + oID: { type: String, required: true, unique: true }, }); export default model('ExecutiveOrders', ExecutiveOrder); diff --git a/src/models/Motion.ts b/src/models/Motion.ts new file mode 100644 index 0000000..d2f9711 --- /dev/null +++ b/src/models/Motion.ts @@ -0,0 +1,34 @@ +import { Document, model, Schema } from 'mongoose'; + +export interface MotionInterface extends Document { + issuedBy: string; + subject: string; + body: string; + at: Date; + oID: string; + voteResults: { + yea: number; + nay: number; + present: number; + absent: number; + }; + motionMessage: string; +} + +const Motion = new Schema({ + issuedBy: { type: String, required: true }, + subject: { type: String, required: true }, + body: { type: String, required: true }, + at: { type: Date, required: true }, + oID: { type: String, required: true, unique: true }, + voteResults: { + yea: Number, + Nay: Number, + present: Number, + absent: Number, + }, + motionMessage: { type: String, required: true, unique: true }, + processed: Boolean, +}); + +export default model('Motions', Motion); diff --git a/src/models/index.ts b/src/models/index.ts index f6f0541..f9dadc0 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -5,6 +5,7 @@ export { default as File, FileInterface } from './File'; export { default as Member, MemberInterface } from './Member'; export { default as Merchant, MerchantInterface } from './Merchant'; export { default as Moderation, ModerationInterface } from './Moderation'; +export { default as Motion, MotionInterface } from './Motion'; export { default as NNTrainingData, NNTrainingDataInterface } from './NNTrainingData'; export { default as Note, NoteInterface } from './Note'; export { default as PagerNumber, PagerNumberInterface, PagerNumberRaw } from './PagerNumber';