diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14e178b..9364d83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ We accept contributions from the community, however there's a few steps you need to do first before you're able to fork the project. -1. Join the [Discord Server](https://discord.gg/F4ztpQh). +1. Join the [Discord Server](https://loc.sh/discord). 2. Send a DM to @Ramirez in the server, provide your GitLab username. 3. We'll let you know when you'll be able to fork the project. diff --git a/package.json b/package.json index d9ca635..25bb4be 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "loccommunityrelations", + "name": "loccr", "version": "1.0.0", "description": "The official system for handling Community Relations in the LOC Discord server.", "main": "build/main.js", @@ -7,7 +7,7 @@ "lint": "eslint -c ./.eslintrc.json src --ext ts" }, "repository": "https://gitlab.libraryofcode.org/engineering/communityrelations.git", - "author": "Matthew R ", + "author": "Matthew R, AD, FSEN ", "license": "AGPL-3.0", "private": false, "devDependencies": { @@ -29,7 +29,8 @@ "eslint": "^7.19.0", "eslint-config-airbnb-base": "^14.1.0", "eslint-plugin-import": "^2.20.2", - "typescript": "^3.9.8" + "tslib": "^2.1.0", + "typescript": "^3.9.2" }, "dependencies": { "@google-cloud/text-to-speech": "^3.1.2", @@ -55,7 +56,8 @@ "puppeteer": "^5.5.0", "sd-notify": "^2.8.0", "signale": "^1.4.0", - "stripe": "^8.135.0", + "stock-info": "^1.2.0", + "stripe": "^8.120.0", "uuid": "^8.0.0", "yaml": "^1.9.2" } diff --git a/src/api/comm.libraryofcode.org/routes/report.ts b/src/api/comm.libraryofcode.org/routes/report.ts index 780801b..1165d77 100644 --- a/src/api/comm.libraryofcode.org/routes/report.ts +++ b/src/api/comm.libraryofcode.org/routes/report.ts @@ -1,9 +1,8 @@ /* eslint-disable no-bitwise */ /* eslint-disable no-continue */ import jwt from 'jsonwebtoken'; -import { v4 as uuid } from 'uuid'; import { TextChannel } from 'eris'; -import { LocalStorage, Route, Server, RichEmbed } from '../../../class'; +import { LocalStorage, Route, Server } from '../../../class'; import { ScoreHistoricalRaw } from '../../../models/ScoreHistorical'; import { getTotalMessageCount } from '../../../intervals/score'; @@ -128,11 +127,12 @@ export default class Report extends Route { else if (member.cloudServices > 10) cloudServicesScore = 10; else cloudServicesScore = Math.round(member.cloudServices); + const inqs = await this.server.client.db.Inquiry.find({ userID: member.userID }).lean().exec(); const inquiries: [{ id?: string, name: string, date: Date }?] = []; - if (member.inquiries?.length > 0) { - for (const inq of member.inquiries) { + if (inqs?.length > 0) { + for (const inq of inqs) { const testDate = (new Date(new Date(inq.date).setHours(1460))); - if (testDate > new Date()) inquiries.push({ id: inq.id, name: inq.name, date: inq.date }); + if (testDate > new Date()) inquiries.push({ id: inq.iid, name: inq.name, date: inq.date }); } } @@ -173,35 +173,14 @@ export default class Report extends Route { array.push(data); } - if (member.notify) { - const chan = await this.server.client.getDMChannel(member.userID); - try { - chan.createMessage(`__**Community Score - Hard Pull Notification**__\n*You have signed up to be notified whenever your hard score has been pulled. See \`?score\` for more information.*\n\n**Department/Service:** ${merchant.name.toUpperCase()}`); - } catch (err) { - this.server.client.util.signale.error(`Unable to DM user: ${member.userID} | ${err}`); - } - } + const inq = await this.server.client.report.createInquiry(member.userID, merchant.name, 0, req.body.reason); await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 0, reason: req.body.reason, date: new Date() } } }); - const reportID = uuid(); - await this.server.client.db.Score.updateOne({ userID: member.userID }, { $addToSet: { inquiries: { id: reportID, name: merchant.name, reason: req.body.reason, date: new Date(), report: member } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setDescription(reportID); - embed.setColor('#800080'); - embed.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${member.userID}>`, true); - embed.addField('Type', 'HARD', true); - embed.addField('Department/Service', merchant.name.toUpperCase(), true); - embed.addField('Reason', req.body.reason ?? 'N/A', true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); return res.status(200).json({ code: this.constants.codes.SUCCESS, message: { - id: reportID, + id: inq.id, userID: member.userID, memberInformation: { username: mem.user.username, @@ -237,35 +216,24 @@ export default class Report extends Route { const merchant = await this.server.client.db.Merchant.findOne({ key: req.headers.authorization }).lean().exec(); if (!merchant) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); - const member = await this.server.client.db.Score.findOne({ userID: req.body.userID }).lean().exec(); - if (!member) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); + const report = await this.server.client.db.Score.findOne({ userID: req.body.userID }).lean().exec(); + if (!report) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); let totalScore: number; - if (member.total < 200) totalScore = 0; - else if (member.total > 800) totalScore = 800; - else totalScore = Math.round(member.total); + if (report.total < 200) totalScore = 0; + else if (report.total > 800) totalScore = 800; + else totalScore = Math.round(report.total); await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 1, reason: 'N/A', date: new Date() } } }); - await this.server.client.db.Score.updateOne({ userID: member.userID }, { $addToSet: { softInquiries: { name: merchant.name, date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - const mem = this.server.client.util.resolveMember(member.userID, this.server.client.guilds.get(this.server.client.config.guildID)); - if (!mem) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.codes.NOT_FOUND }); - embed.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${member.userID}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', merchant.name.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); + const mem = this.server.client.util.resolveMember(report.userID, this.server.client.guilds.get(this.server.client.config.guildID)); + await this.server.client.report.createInquiry(report.userID, merchant.name.toUpperCase(), 1); if (!merchant.privileged) { return res.status(200).json({ code: this.constants.codes.SUCCESS, message: { - userID: member.userID, + userID: report.userID, totalScore, }, }); @@ -287,36 +255,38 @@ export default class Report extends Route { if ((mem.user.publicFlags & (1 << 17)) === 1 << 17) flags.push('EARLY_VERIFIED_BOT_DEVELOPER'); } const set = []; - const accounts = await this.server.client.db.Score.find().lean().exec(); - for (const sc of accounts) { - if (sc.total < 200) { continue; } - if (sc.total > 800) { set.push(800); continue; } - set.push(sc.total); + if (req.query.p?.toString() === 'true') { + const accounts = await this.server.client.db.Score.find().lean().exec(); + for (const sc of accounts) { + if (sc.total < 200) { continue; } + if (sc.total > 800) { set.push(800); continue; } + set.push(sc.total); + } } let activityScore: number; - const moderationScore = Math.round(member.moderation); + const moderationScore = Math.round(report.moderation); let roleScore: number; let cloudServicesScore: number; - const otherScore = Math.round(member.other); + const otherScore = Math.round(report.other); let miscScore: number; - if (member.activity < 10) activityScore = 0; - else if (member.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12)); - else activityScore = Math.round(member.activity); + if (report.activity < 10) activityScore = 0; + else if (report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12)); + else activityScore = Math.round(report.activity); - if (member.roles <= 0) roleScore = 0; - else if (member.roles > 54) roleScore = 54; - else roleScore = Math.round(member.roles); + if (report.roles <= 0) roleScore = 0; + else if (report.roles > 54) roleScore = 54; + else roleScore = Math.round(report.roles); - if (member.staff <= 0) miscScore = 0; - else miscScore = Math.round(member.staff); + if (report.staff <= 0) miscScore = 0; + else miscScore = Math.round(report.staff); - if (member.cloudServices === 0) cloudServicesScore = 0; - else if (member.cloudServices > 10) cloudServicesScore = 10; - else cloudServicesScore = Math.round(member.cloudServices); + if (report.cloudServices === 0) cloudServicesScore = 0; + else if (report.cloudServices > 10) cloudServicesScore = 10; + else cloudServicesScore = Math.round(report.cloudServices); - const historicalData = await this.server.client.db.ScoreHistorical.find({ userID: member.userID }).lean().exec(); + const historicalData = await this.server.client.db.ScoreHistorical.find({ userID: report.userID }).lean().exec(); const array: ScoreHistoricalRaw[] = []; for (const data of historicalData) { delete data.report?.softInquiries; @@ -357,7 +327,7 @@ export default class Report extends Route { return res.status(200).json({ code: this.constants.codes.SUCCESS, message: { - userID: member.userID, + userID: report.userID, memberInformation: { username: mem.user.username, discriminator: mem.user.discriminator, @@ -368,7 +338,7 @@ export default class Report extends Route { nitroBoost: mem.premiumSince === null, }, totalScore, - percentile: Math.round(this.server.client.util.percentile(set, member.total)), + percentile: Math.round(this.server.client.util.percentile(set, report.total)), activityScore, moderationScore, roleScore, @@ -403,17 +373,7 @@ export default class Report extends Route { else totalScore = Math.round(member.total); await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 1, reason: 'N/A', date: new Date() } } }); - await this.server.client.db.Score.updateOne({ userID: member.userID }, { $addToSet: { softInquiries: { name: merchant.name, date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - embed.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${member.userID}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', merchant.name.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); + await this.server.client.report.createInquiry(member.userID, merchant.name.toUpperCase(), 1); if (!merchant.privileged) { return res.status(200).json({ @@ -486,42 +446,12 @@ export default class Report extends Route { this.timeout.delete(req.ip); if (staffScore.userID === score.userID) { updated = true; - await this.server.client.db.Score.updateOne({ userID: score.userID }, { $addToSet: { softInquiries: { name: `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', `${member.username} via report.libraryofcode.org @ IP ${req.ip}`.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); + await this.server.client.report.createInquiry(member.user.id, `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, 1); } else { - await this.server.client.db.Score.updateOne({ userID: score.userID }, { $addToSet: { softInquiries: { name: 'Library of Code sp-us | Staff Team via report.libraryofcode.org', date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', 'Library of Code sp-us | Staff Team via report.libraryofcode.org'.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); + await this.server.client.report.createInquiry(member.user.id, 'Library of Code sp-us | Staff Team via report.libraryofcode.org', 1); } } else if (!updated) { - await this.server.client.db.Score.updateOne({ userID: score.userID }, { $addToSet: { softInquiries: { name: `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', `${member.username} via report.libraryofcode.org @ IP ${req.ip}`.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.server.client.user.username, this.server.client.user.avatarURL); - const chan = this.server.client.guilds.get(this.server.client.config.guildID).channels.get('611584771356622849'); - chan.createMessage({ embed }).catch(() => { }); + await this.server.client.report.createInquiry(member.user.id, `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, 1); } score = await this.server.client.db.Score.findOne({ pin: [Number(args.split('-')[0]), Number(args.split('-')[1]), Number(args.split('-')[2])] }).lean().exec(); @@ -557,7 +487,7 @@ export default class Report extends Route { else if (score.cloudServices > 10) cloudServicesScore = '10'; else cloudServicesScore = `${score.cloudServices}`; - const moderations = await this.server.client.db.Moderation.find({ userID: score.userID }); + const moderations = await this.server.client.db.Moderation.find({ userID: score.userID }).lean().exec(); const historical = await this.server.client.db.ScoreHistorical.find({ userID: score.userID }).lean().exec(); @@ -592,6 +522,17 @@ export default class Report extends Route { data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc; } + const inqs = await this.server.client.db.Inquiry.find({ userID: score.userID }).lean().exec(); + const hardInquiries: [{ id?: string, name: string, reason: string, date: Date }?] = []; + const softInquiries: [{ id?: string, name: string, date: Date }?] = []; + for (const inq of inqs) { + if (inq.type === 0) { + hardInquiries.push({ id: inq.iid, name: inq.name, reason: inq.reason, date: inq.date }); + } else if (inq.type === 1) { + softInquiries.push({ name: inq.name, date: inq.date }); + } + } + return res.status(200).json({ name: `${member.username}#${member.discriminator}`, avatarURL: member.avatarURL, @@ -607,8 +548,8 @@ export default class Report extends Route { notify: score.notify, locked: !!score.locked, totalModerations: moderations?.length > 0 ? moderations.length : 0, - inquiries: score.inquiries?.length > 0 ? score.inquiries : [], - softInquiries: score.softInquiries?.length > 0 ? score.softInquiries : [], + inquiries: hardInquiries?.length > 0 ? hardInquiries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) : [], + softInquiries: softInquiries?.length > 0 ? softInquiries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) : [], historical: historical ?? [], lastUpdated: score.lastUpdate, }); diff --git a/src/class/Client.ts b/src/class/Client.ts index c2d042a..876622b 100644 --- a/src/class/Client.ts +++ b/src/class/Client.ts @@ -7,20 +7,17 @@ import { Collection, Command, LocalStorage, Queue, Util, ServerManagement, Event import { Customer, CustomerInterface, CustomerPortal, CustomerPortalInterface, - ExecutiveOrder, ExecutiveOrderInterface, File, FileInterface, + Inquiry, InquiryInterface, Member, MemberInterface, Merchant, MerchantInterface, Moderation, ModerationInterface, - Motion, MotionInterface, NNTrainingData, NNTrainingDataInterface, Note, NoteInterface, PagerNumber, PagerNumberInterface, - Proclamation, ProclamationInterface, Promo, PromoInterface, Rank, RankInterface, Redirect, RedirectInterface, - Resolution, ResolutionInterface, Score, ScoreInterface, ScoreHistorical, ScoreHistoricalInterface, Staff, StaffInterface, @@ -47,29 +44,7 @@ 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, - Motion: mongoose.Model, - NNTrainingData: mongoose.Model, - Note: mongoose.Model, - PagerNumber: mongoose.Model, - Proclamation: mongoose.Model, - Promo: mongoose.Model, - Rank: mongoose.Model, - Redirect: mongoose.Model, - Resolution: 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, File: mongoose.Model, Inquiry: 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 } }; constructor(token: string, options?: eris.ClientOptions) { super(token, options); @@ -77,9 +52,18 @@ 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, Motion, NNTrainingData, Note, PagerNumber, Proclamation, Promo, Rank, Redirect, Resolution, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } }; + this.db = { Customer, CustomerPortal, File, Inquiry, Member, Merchant, Moderation, NNTrainingData, Note, PagerNumber, Promo, Rank, Redirect, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } }; } + get report() { + return this.util.report; + } + + get pbx() { + return this.util.pbx; + } + + public async loadDatabase() { await mongoose.connect(this.config.mongoDB, { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 50 }); diff --git a/src/class/Queue.ts b/src/class/Queue.ts index 261244c..df5fe5f 100644 --- a/src/class/Queue.ts +++ b/src/class/Queue.ts @@ -1,9 +1,10 @@ +/* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ import Bull from 'bull'; import cron from 'cron'; -import { TextableChannel } from 'eris'; +import { TextableChannel, TextChannel } from 'eris'; import { Client, RichEmbed } from '.'; -import { ScoreInterface } from '../models'; +import { ScoreInterface, InqType as InquiryType } from '../models'; import { apply as Apply } from '../commands'; @@ -28,12 +29,13 @@ export default class Queue { const startDate = new Date(); for (const report of reports) { + const inqs = await this.client.db.Inquiry.find({ userID: report.userID }); const data = new this.client.db.ScoreHistorical({ userID: report.userID, report, + inquiries: inqs.map((inq) => inq._id), date: startDate, }); - // eslint-disable-next-line no-await-in-loop await data.save(); } } catch (err) { @@ -77,6 +79,37 @@ export default class Queue { } protected setProcessors() { + this.queues.score.process('score::inquiry', async (job: Bull.Job<{ inqID: string, userID: string, name: string, type: InquiryType, reason?: string }>) => { + const member = this.client.util.resolveMember(job.data.userID, this.client.guilds.get(this.client.config.guildID)); + const report = await this.client.db.Score.findOne({ userID: job.data.userID }).lean().exec(); + const embed = new RichEmbed(); + if (job.data.type === InquiryType.HARD) { + embed.setTitle('Inquiry Notification'); + embed.setDescription(job.data.inqID); + embed.setColor('#800080'); + embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${job.data.userID}>`, true); + embed.addField('Type', 'HARD', true); + embed.addField('Department/Service', job.data.name.toUpperCase(), true); + embed.addField('Reason', job.data.reason ?? 'N/A', true); + embed.setTimestamp(); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + if (report.notify === true) { + await this.client.getDMChannel(job.data.userID).then((chan) => { + chan.createMessage(`__**Community Score - Hard Pull Notification**__\n*You have signed up to be notified whenever your hard score has been pulled. See \`?score\` for more information.*\n\n**Department/Service:** ${job.data.name.toUpperCase()}`); + }).catch(() => {}); + } + } else { + embed.setTitle('Inquiry Notification'); + embed.setColor('#00FFFF'); + embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${job.data.userID}>`, true); + embed.addField('Type', 'SOFT', true); + embed.addField('Department/Service', job.data.name.toUpperCase(), true); + embed.setTimestamp(); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + } + const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); + log.createMessage({ embed }).catch(() => {}); + }); this.queues.score.process('score::update', async (job: Bull.Job<{ score: ScoreInterface, total: number, activity: number, roles: number, moderation: number, cloudServices: number, other: number, staff: number }>) => { await this.client.db.Score.updateOne({ userID: job.data.score.userID }, { $set: { total: job.data.total, activity: job.data.activity, roles: job.data.roles, moderation: job.data.moderation, cloudServices: job.data.cloudServices, other: job.data.other, staff: job.data.staff, lastUpdate: new Date() } }); if (!job.data.score.pin || job.data.score.pin?.length < 1) { @@ -123,8 +156,12 @@ export default class Queue { }); } + public addInquiry(inqID: string, userID: string, name: string, type: InquiryType, reason?: string) { + return this.queues.score.add('score::inquiry', { inqID, userID, name, type, reason }); + } + public updateScore(score: ScoreInterface, total: number, activity: number, roles: number, moderation: number, cloudServices: number, other: number, staff: number) { - this.queues.score.add('score::update', { score, total, activity, roles, moderation, cloudServices, other, staff }); + return this.queues.score.add('score::update', { score, total, activity, roles, moderation, cloudServices, other, staff }); } public processApplication(channelInformation: { messageID: string, guildID: string, channelID: string }, url: string, userID: string, func?: string) { diff --git a/src/class/Report.ts b/src/class/Report.ts new file mode 100644 index 0000000..6c4a987 --- /dev/null +++ b/src/class/Report.ts @@ -0,0 +1,97 @@ +import { v4 as uuid } from 'uuid'; +import { Client } from '.'; +import { InqType } from '../models'; + +export default class Report { + public client: Client; + + constructor(client: Client) { + this.client = client; + } + + public async createInquiry(userID: string, name: string, type: InqType, reason?: string) { + const report = await this.client.db.Score.findOne({ userID }).lean().exec(); + const member = this.client.util.resolveMember(userID, this.client.guilds.get(this.client.config.guildID)); + if (!report || !member) return null; + if (type === InqType.HARD && report.locked) return null; + const mod = await (new this.client.db.Inquiry({ + iid: uuid(), + userID, + name, + type, + reason, + date: new Date(), + report, + }).save()); + + this.client.queue.addInquiry(mod.iid, userID, name, type, reason); + + return mod; + } + + /* public async soft(userID: string) { + const report = await this.client.db.Score.findOne({ userID }); + if (!report) return null; + let totalScore: number; + let activityScore: number; + const moderationScore = Math.round(report.moderation); + let roleScore: number; + let cloudServicesScore: number; + const otherScore = Math.round(report.other); + let miscScore: number; + + if (report.total < 200) totalScore = 0; + else if (report.total > 800) totalScore = 800; + else totalScore = Math.round(report.total); + + if (report.activity < 10) activityScore = 0; + else if (report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activityScore = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12)); + else activityScore = Math.round(report.activity); + + if (report.roles <= 0) roleScore = 0; + else if (report.roles > 54) roleScore = 54; + else roleScore = Math.round(report.roles); + + if (report.staff <= 0) miscScore = 0; + else miscScore = Math.round(report.staff); + + if (report.cloudServices === 0) cloudServicesScore = 0; + else if (report.cloudServices > 10) cloudServicesScore = 10; + else cloudServicesScore = Math.round(report.cloudServices); + + const historicalData = await this.client.db.ScoreHistorical.find({ userID: member.userID }).lean().exec(); + const array: ScoreHistoricalRaw[] = []; + for (const data of historicalData) { + let total: number; + let activity: number; + const moderation = Math.round(data.report.moderation); + let role: number; + let cloud: number; + const other = Math.round(data.report.other); + let misc: number; + + if (data.report.total < 200) total = 0; + else if (data.report.total > 800) total = 800; + else total = Math.round(data.report.total); + + if (data.report.activity < 10) activity = 0; + else if (data.report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activity = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12)); + else activity = Math.round(data.report.activity); + + if (data.report.roles <= 0) role = 0; + else if (data.report.roles > 54) role = 54; + else role = Math.round(data.report.roles); + + if (data.report.staff <= 0) role = 0; + else misc = Math.round(data.report.staff); + + if (data.report.cloudServices === 0) cloud = 0; + else if (data.report.cloudServices > 10) cloud = 10; + else cloud = Math.round(data.report.cloudServices); + + data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc; + + array.push(data); + } + } */ +} diff --git a/src/class/Util.ts b/src/class/Util.ts index 1005fd1..7f4c9fd 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -4,7 +4,7 @@ import childProcess from 'child_process'; import { promisify } from 'util'; import signale from 'signale'; import { Member, Message, Guild, PrivateChannel, GroupChannel, Role, AnyGuildChannel, WebhookPayload } from 'eris'; -import { Client, Command, Moderation, PBX, RichEmbed } from '.'; +import { Client, Command, Moderation, PBX, Report, RichEmbed } from '.'; import { statusMessages as emotes } from '../configs/emotes.json'; export default class Util { @@ -18,6 +18,8 @@ export default class Util { public pbx: PBX; + public report: Report; + constructor(client: Client) { this.client = client; this.moderation = new Moderation(this.client); @@ -33,6 +35,7 @@ export default class Util { auth: { user: 'internal', pass: this.client.config.emailPass }, }); this.pbx = new PBX(this.client); + this.report = new Report(this.client); } get emojis() { @@ -43,6 +46,46 @@ export default class Util { }; } + public hrn(number: any, fixed: number, formatter: any | any[]) { + const builtInFormatters = { + en: ['KMGTPEZY'.split(''), 1e3], + zh_CN: ['百千万亿兆京垓秭'.split(''), [100, 10, 10, 1e4, 1e4, 1e4, 1e4, 1e4]], + }; + number = Math.abs(number); + if (!fixed && fixed !== 0) fixed = 1; + + if (typeof formatter === 'object') { + const name = `${new Date().getTime()}`; + builtInFormatters[name] = formatter; + formatter = name; + } + + if (!builtInFormatters[formatter]) formatter = 'en'; + + formatter = builtInFormatters[formatter]; + let power = 1; + const texts = formatter[0]; + const powers = formatter[1]; + let loop = 0; + let is_array = false; + + if (typeof powers === 'object') is_array = true; + + // eslint-disable-next-line no-constant-condition + while (1) { + if (is_array) power = powers[loop]; + else power = powers; + + if (number >= power && loop < texts.length) number /= power; + else { + number = number.toFixed(fixed); + return loop === 0 ? number : `${number} ${texts[loop - 1]}`; + } + // eslint-disable-next-line no-plusplus + ++loop; + } + } + /** * Resolves a command * @param query Command input diff --git a/src/class/index.ts b/src/class/index.ts index 1cada86..de4df29 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -7,6 +7,7 @@ export { default as LocalStorage } from './LocalStorage'; export { default as Moderation } from './Moderation'; export { default as PBX } from './PBX'; export { default as Queue } from './Queue'; +export { default as Report } from './Report'; export { default as RichEmbed } from './RichEmbed'; export { default as Route } from './Route'; export { default as Server } from './Server'; diff --git a/src/commands/callback.ts b/src/commands/callback.ts index 4da387b..35f084d 100644 --- a/src/commands/callback.ts +++ b/src/commands/callback.ts @@ -7,7 +7,7 @@ export default class Callback extends Command { constructor(client: Client) { super(client); this.name = 'callback'; - this.description = 'Requests a Callback from a Technican.\nPlease use `-` to separate the number if needed. E.x. 202-750-2585.\nDo note, we are unable to dial international numbers outside of the US and Canada.'; + this.description = 'Requests a Callback from a Technican.\nPlease use `-` to separate the number if needed. E.x. 202-750-2585.\nDo note, we are unable to dial international numbers outside of the US and Canada.\n\n*We recommend you run this command in your DMs for privacy.*'; this.usage = 'callback '; this.aliases = ['cb']; this.permissions = 0; @@ -31,17 +31,7 @@ export default class Callback extends Command { embed.addField('Phone Number Type', phone.getType(), true); const communityReport = await this.client.db.Score.findOne({ userID: message.author.id }).lean().exec(); if (communityReport) { - await this.client.db.Score.updateOne({ userID: message.author.id }, { $addToSet: { softInquiries: { name: 'LIBRARY OF CODE SP-US | VOIP/PBX MEMBER SUPPORT SVCS', date: new Date() } } }); - const embed2 = new RichEmbed(); - embed2.setTitle('Inquiry Notification'); - embed2.setColor('#00FFFF'); - embed2.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`, true); - embed2.addField('Type', 'SOFT', true); - embed2.addField('Department/Service', 'Library of Code sp-us | VOIP/PBX Member Support SVCS'.toUpperCase(), true); - embed2.setTimestamp(); - embed2.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.mainGuild.channels.get('611584771356622849'); - log.createMessage({ embed: embed2 }).catch(() => {}); + await this.client.report.createInquiry(member.user.id, 'Library of Code sp-us | VOIP/PBX Member Support SVCS', 1); embed.addField('PIN', `${communityReport.pin[0]}-${communityReport.pin[1]}-${communityReport.pin[2]}`, true); } try { diff --git a/src/commands/delmerchant.ts b/src/commands/delmerchant.ts index 4ed38dd..8c9ef2d 100644 --- a/src/commands/delmerchant.ts +++ b/src/commands/delmerchant.ts @@ -1,5 +1,4 @@ import { Message } from 'eris'; -import { randomBytes } from 'crypto'; import { Client, Command } from '../class'; export default class DelMerchant extends Command { diff --git a/src/commands/index.ts b/src/commands/index.ts index 8916297..c4c951d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -21,6 +21,7 @@ export { default as info } from './info'; export { default as intercom } from './intercom'; export { default as kick } from './kick'; export { default as listredirects } from './listredirects'; +export { default as market } from './market'; export { default as members } from './members'; export { default as mute } from './mute'; export { default as notes } from './notes'; @@ -32,6 +33,7 @@ export { default as pulldata } from './pulldata'; export { default as rank } from './rank'; export { default as roleinfo } from './roleinfo'; export { default as score } from './score'; +export { default as sip } from './sip'; export { default as site } from './site'; export { default as stats } from './stats'; export { default as storemessages } from './storemessages'; diff --git a/src/commands/intercom.ts b/src/commands/intercom.ts index a095326..071ec0a 100644 --- a/src/commands/intercom.ts +++ b/src/commands/intercom.ts @@ -25,7 +25,7 @@ export default class Intercom extends Command { channel: `PJSIP/${args[0]}`, exten: args[0], context: 'from-internal', - CallerID: `TTS PAGE FRM ${message.author.username} <*0>`, + CallerID: `TTS INTC FRM ${message.author.username} <000>`, application: 'PlayBack', priority: '1', data: `beep&${recordingLocation.split(':')[1]}`, diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 0a32377..5c1e8f0 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -1,4 +1,4 @@ -import { Message, User, Member } from 'eris'; +import { Member, Message } from 'eris'; import { Client, Command } from '../class'; export default class Kick extends Command { diff --git a/src/commands/market.ts b/src/commands/market.ts new file mode 100644 index 0000000..132edd2 --- /dev/null +++ b/src/commands/market.ts @@ -0,0 +1,59 @@ +import { Message } from 'eris'; +import stockInfo from 'stock-info'; +import { Client, Command, RichEmbed } from '../class'; + +export default class Market extends Command { + constructor(client: Client) { + super(client); + this.name = 'market'; + this.description = 'Fetches information from a ticker for a stock or fund.'; + this.usage = `${this.client.config.prefix}market `; + this.permissions = 0; + this.guildOnly = true; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); + let stock: stockInfo.Stock; + try { + stock = await stockInfo.getSingleStockInfo(args[0]); + } catch (err) { + return this.error(message.channel, `Unable to fetch information for that ticker. | ${err}`); + } + + const embed = new RichEmbed(); + embed.setTitle(stock.longName ?? stock.symbol); + let type: string; + switch (stock.quoteType) { + case 'EQUITY': + type = 'Common/Preferred Stock'; + break; + case 'ETF': + type = 'Exchange Traded Fund (ETF)'; + break; + case 'MUTUALFUND': + type = 'Mutual Fund'; + break; + default: + type = 'N/A or Unknown'; + break; + } + embed.addField('Type', type, true); + embed.addField('Market Cap', `${stock.marketCap ? this.client.util.hrn(stock.marketCap, undefined, undefined) : 'N/A'}`, true); + embed.addField('Day Quote Price', stock.regularMarketPrice ? `$${Number(stock.regularMarketPrice).toFixed(2)}` : 'N/A', true); + embed.addField('Day G/L', stock.regularMarketChange ? `$${Number(stock.regularMarketChange.toFixed(2))}` : 'N/A', true); + embed.addField('Day Range', stock.regularMarketDayRange ?? 'N/A', true); + embed.addField('Bid/Ask', `Bid: $${stock.bid?.toFixed(2) ?? 'N/A'} | Ask: $${stock.ask?.toFixed(2) ?? 'N/A'}`, true); + embed.addField('Forward P/E (Price/Earnings)', `${stock.forwardPE?.toFixed(2) ?? 'N/A'}`, true); + embed.addField('Forward EPS (Earnings Per Share)', `${stock.epsForward?.toFixed(2) ?? 'N/A'}`, true); + embed.addField('Exchange', `${stock.fullExchangeName?.toUpperCase() ?? 'N/A'}`, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + return message.channel.createMessage({ embed }); + } catch (err) { + return this.client.util.handleError(err, message, this, false); + } + } +} diff --git a/src/commands/page.ts b/src/commands/page.ts index 6687434..4915263 100644 --- a/src/commands/page.ts +++ b/src/commands/page.ts @@ -205,7 +205,7 @@ export default class Page extends Command { const chan = await this.client.util.pbx.ari.channels.originate({ endpoint: `PJSIP/${member.extension}`, extension: `${member.extension}`, - callerId: `LOC PBX - PAGE FRM ${senderNumber} <00>`, + callerId: `PAGE FRM ${senderNumber} <000>`, context: 'from-internal', priority: 1, app: 'cr-zero', diff --git a/src/commands/pulldata.ts b/src/commands/pulldata.ts index d1d2c0a..a0abc3f 100644 --- a/src/commands/pulldata.ts +++ b/src/commands/pulldata.ts @@ -27,18 +27,7 @@ export default class Score extends Command { const report = score.inquiries.find((inq) => inq.id === args[1]); if (!report) return this.error(message.channel, 'Could not find inquiry information.'); - await this.client.db.Score.updateOne({ userID: member.id }, { $addToSet: { softInquiries: { name: 'Library of Code sp-us | Bureau of Community Reports', date: new Date() } } }); - const embed2 = new RichEmbed(); - embed2.setTitle('Inquiry Notification'); - embed2.setColor('#00FFFF'); - const mem = this.client.util.resolveMember(score.userID, this.client.guilds.get(this.client.config.guildID)); - embed2.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${score.userID}>`, true); - embed2.addField('Type', 'SOFT', true); - embed2.addField('Department/Service', 'Library of Code sp-us | Bureau of Community Reports'.toUpperCase(), true); - embed2.setTimestamp(); - embed2.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed: embed2 }).catch(() => {}); + await this.client.report.createInquiry(member.id, 'Library of Code sp-us | Bureau of Community Reports', 1); const embed = new RichEmbed(); embed.setTitle(`Hard Inquiry Information - ${report.id}`); diff --git a/src/commands/score.ts b/src/commands/score.ts index 89363c5..e91c7b8 100644 --- a/src/commands/score.ts +++ b/src/commands/score.ts @@ -1,8 +1,8 @@ +/* eslint-disable no-plusplus */ /* eslint-disable no-continue */ /* eslint-disable default-case */ import moment from 'moment'; -import { v4 as uuid } from 'uuid'; -import { Message, User, TextChannel } from 'eris'; +import { Message, User } from 'eris'; import { Client, Command, RichEmbed } from '../class'; import { getTotalMessageCount } from '../intervals/score'; import Score_Hist from './score_hist'; @@ -15,6 +15,7 @@ export default class Score extends Command { this.name = 'score'; this.description = 'Retrieves your Community Report'; this.usage = `${this.client.config.prefix}score\n${this.client.config.prefix}score :`; + this.aliases = ['report']; this.subcmds = [Score_Hist, Score_Notify, Score_Pref]; this.permissions = 0; this.guildOnly = false; @@ -28,18 +29,7 @@ export default class Score extends Command { if (!args[0] || !this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 4)) { user = message.author; if (!user) return this.error(message.channel, 'Member not found.'); - await this.client.db.Score.updateOne({ userID: user.id }, { $addToSet: { softInquiries: { name: `${user.username} via Discord`, date: new Date() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setColor('#00FFFF'); - const mem = this.client.util.resolveMember(user.id, this.client.guilds.get(this.client.config.guildID)); - embed.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${user.id}>`, true); - embed.addField('Type', 'SOFT', true); - embed.addField('Department/Service', `${user.username} via Discord`.toUpperCase(), true); - embed.setTimestamp(); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed }).catch(() => { }); + await this.client.report.createInquiry(user.id, `${user.username} via Discord`, 1); check = true; } else { user = this.client.util.resolveMember(args[0], this.mainGuild)?.user; @@ -57,30 +47,12 @@ export default class Score extends Command { const score = await this.client.db.Score.findOne({ userID: user.id }); if (!score) return this.error(message.channel, 'Score not calculated yet.'); if (score.locked) return this.error(message.channel, 'The score report you have requested has been locked.'); - const reportID = uuid(); - await this.client.db.Score.updateOne({ userID: user.id }, { $addToSet: { inquiries: { id: reportID, name, reason, date: new Date(), report: score.toObject() } } }); - const embed = new RichEmbed(); - embed.setTitle('Inquiry Notification'); - embed.setDescription(reportID); - embed.setColor('#800080'); - const mem = this.client.util.resolveMember(score.userID, this.client.guilds.get(this.client.config.guildID)); - embed.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${score.userID}>`, true); - embed.addField('Type', 'HARD', true); - embed.addField('Department/Service', name.toUpperCase(), true); - embed.addField('Reason', reason ?? 'N/A', true); - embed.setTimestamp(); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed }).catch(() => { }); - if (score.notify === true) { - await this.client.getDMChannel(user.id).then((chan) => { - chan.createMessage(`__**Community Score - Hard Pull Notification**__\n*You have signed up to be notified whenever your hard score has been pulled. See \`?score\` for more information.*\n\n**Department/Service:** ${name.toUpperCase()}`); - }).catch(() => { }); - } + await this.client.report.createInquiry(score.userID, name, 0, reason); } } if (!user) return this.error(message.channel, 'Member not found.'); const score = await this.client.db.Score.findOne({ userID: user.id }); + const inqs = await this.client.db.Inquiry.find({ userID: user.id, type: 0 }); if (!score) return this.error(message.channel, 'Community Report has not been generated yet.'); let totalScore = '0'; let activityScore = '0'; @@ -134,13 +106,18 @@ export default class Score extends Command { break; } } */ - if (score?.inquiries?.length > 0) { + let inqAdded = 0; + if (inqs?.length > 0) { let desc = '__**Hard Inquiries**__\n*These inquiries will fall off your report within 2 months, try to keep your hard inquiries to a minimum. If you want to file a dispute, please DM Ramirez.*\n\n'; - score.inquiries.forEach((inq) => { + inqs.forEach((inq) => { const testDate = (new Date(new Date(inq.date).setHours(1460))); // eslint-disable-next-line no-useless-escape - if (testDate > new Date()) desc += `${inq.id ? `__[${inq.id}]__\n` : ''}**Department/Service:** ${inq.name.replace(/\*/gmi, '')}\n**Reason:** ${inq.reason}\n**Date:** ${inq.date.toLocaleString('en-us')} ET\n**Expires:** ${moment(testDate).calendar()}\n\n`; + if (testDate > new Date()) { + desc += `${inq.iid ? `__[${inq.iid}]__\n` : ''}**Department/Service:** ${inq.name.replace(/\*/gmi, '')}\n**Reason:** ${inq.reason}\n**Date:** ${moment(inq.date).format('MMMM Do YYYY, h:mm:ss')}\n**Expires:** ${moment(testDate).calendar()}\n\n`; + inqAdded++; + } }); + if (inqAdded <= 0) desc = ''; if (desc.length >= 1900) { embed.setDescription('__***Too many Hard Inquiries to show, please see https://report.libraryofcode.org***__'); } else { @@ -183,18 +160,7 @@ export default class Score extends Command { name = `Library of Code sp-us | ${role.name}`; break; } - await this.client.db.Score.updateOne({ userID: user.id }, { $addToSet: { softInquiries: { name, date: new Date() } } }); - const embed2 = new RichEmbed(); - embed2.setTitle('Inquiry Notification'); - embed2.setColor('#00FFFF'); - const mem = this.client.util.resolveMember(score.userID, this.client.guilds.get(this.client.config.guildID)); - embed2.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${score.userID}>`, true); - embed2.addField('Type', 'SOFT', true); - embed2.addField('Department/Service', name.toUpperCase(), true); - embed2.setTimestamp(); - embed2.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed: embed2 }).catch(() => { }); + await this.client.report.createInquiry(user.id, name, 1); } } if (args[1] === 'hard' && this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 6)) { diff --git a/src/commands/score_hist.ts b/src/commands/score_hist.ts index 10da4f5..f14afe8 100644 --- a/src/commands/score_hist.ts +++ b/src/commands/score_hist.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-await-in-loop */ /* eslint-disable no-continue */ /* eslint-disable default-case */ import moment from 'moment'; @@ -6,6 +7,7 @@ import { createPaginationEmbed } from 'eris-pagination'; import { Message, User, TextChannel } from 'eris'; import { Client, Command, RichEmbed } from '../class'; import { getTotalMessageCount } from '../intervals/score'; +import { InquiryInterface } from '../models'; export default class Score_Hist extends Command { constructor(client: Client) { @@ -78,8 +80,15 @@ export default class Score_Hist extends Command { let data = ''; let hardInquiries = 0; - if (hist.report?.inquiries?.length > 0) { - hist.report.inquiries.forEach((inq) => { + const inquiries: InquiryInterface[] = []; + if (hist.inquiries?.length > 0) { + for (const h of hist.inquiries) { + const inq = await this.client.db.Inquiry.findOne({ _id: h }); + inquiries.push(inq); + } + } + if (inquiries?.length > 0) { + inquiries.forEach((inq) => { const testDate = (new Date(new Date(inq.date).setHours(1460))); // eslint-disable-next-line no-plusplus if (testDate > new Date()) hardInquiries++; @@ -156,17 +165,7 @@ export default class Score_Hist extends Command { return cmdPages.push(embed); }); const name = `${user.username} VIA DISCORD - [HISTORICAL]`; - await this.client.db.Score.updateOne({ userID: user.id }, { $addToSet: { softInquiries: { name: name.toUpperCase(), date: new Date() } } }); - const embed2 = new RichEmbed(); - embed2.setTitle('Inquiry Notification'); - embed2.setColor('#00FFFF'); - embed2.addField('Member', `${user.username}#${user.discriminator} | <@${user.id}>`, true); - embed2.addField('Type', 'SOFT', true); - embed2.addField('Department/Service', name.toUpperCase(), true); - embed2.setTimestamp(); - embed2.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed: embed2 }).catch(() => {}); + await this.client.report.createInquiry(user.id, name, 1); if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); @@ -231,8 +230,15 @@ export default class Score_Hist extends Command { let data = ''; let hardInquiries = 0; - if (hist.report?.inquiries?.length > 0) { - hist.report.inquiries.forEach((inq) => { + const inquiries: InquiryInterface[] = []; + if (hist.inquiries?.length > 0) { + for (const h of hist.inquiries) { + const inq = await this.client.db.Inquiry.findOne({ _id: h }); + inquiries.push(inq); + } + } + if (inquiries?.length > 0) { + inquiries.forEach((inq) => { const testDate = (new Date(new Date(inq.date).setHours(1460))); // eslint-disable-next-line no-plusplus if (testDate > new Date()) hardInquiries++; @@ -314,18 +320,7 @@ export default class Score_Hist extends Command { name = `Library of Code sp-us | ${role.name} - [HISTORICAL]`; break; } - await this.client.db.Score.updateOne({ userID: user.id }, { $addToSet: { softInquiries: { name, date: new Date() } } }); - const embed2 = new RichEmbed(); - embed2.setTitle('Inquiry Notification'); - embed2.setColor('#00FFFF'); - const mem = this.client.util.resolveMember(user.id, this.client.guilds.get(this.client.config.guildID)); - embed2.addField('Member', `${mem.user.username}#${mem.user.discriminator} | <@${user.id}>`, true); - embed2.addField('Type', 'SOFT', true); - embed2.addField('Department/Service', name.toUpperCase(), true); - embed2.setTimestamp(); - embed2.setFooter(this.client.user.username, this.client.user.avatarURL); - const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); - log.createMessage({ embed: embed2 }).catch(() => {}); + await this.client.report.createInquiry(user.id, name, 1); if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); diff --git a/src/commands/sip.ts b/src/commands/sip.ts new file mode 100644 index 0000000..adb0c6f --- /dev/null +++ b/src/commands/sip.ts @@ -0,0 +1,81 @@ +/* eslint-disable consistent-return */ +import { Message } from 'eris'; +import type { Channel } from 'ari-client'; +import { Client, Command } from '../class'; +import { Misc } from '../pbx'; + +export default class SIP extends Command { + constructor(client: Client) { + super(client); + this.name = 'sip'; + this.description = 'Dials a SIP URI.'; + this.usage = `${this.client.config.prefix}sip `; + this.permissions = 1; + this.guildOnly = true; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + const staff = await this.client.db.Staff.findOne({ userID: message.author.id }); + if (!staff || !staff?.extension) return this.error(message.channel, 'You must have an extension to complete this action.'); + this.success(message.channel, 'Queued call.'); + + const bridge = await this.client.pbx.ari.Bridge().create(); + let receiver: Channel = this.client.pbx.ari.Channel(); + const sender = await this.client.pbx.ari.channels.originate({ + endpoint: `PJSIP/${staff.extension}`, + extension: staff.extension, + callerId: 'LOC PBX AUTO OPERATOR ', + context: 'from-internal', + priority: 1, + app: 'cr-zero', + }); + + sender.once('StasisStart', async () => { + await Misc.play(this.client.pbx, sender, 'sound:pls-hold-while-try'); + await sender.ring(); + + try { + receiver = await receiver.originate({ + endpoint: `SIP/${args.join(' ')}`, + callerId: 'LOC PBX AUTO OPERATOR ', + context: 'from-internal', + priority: 1, + app: 'cr-zero', + }); + } catch { + await Misc.play(this.client.pbx, sender, 'sound:discon-or-out-of-service'); + await sender.hangup().catch(() => {}); + return false; + } + + // receiver.once('StasisStart', ) + }); + + receiver.once('StasisStart', async () => { + await sender.ringStop(); + await bridge.addChannel({ channel: [receiver.id, sender.id] }); + await bridge.play({ media: 'sound:beep' }); + }); + + receiver.once('ChannelDestroyed', async () => { + if (!sender.connected) return; + await Misc.play(this.client.pbx, sender, ['sound:the-number-u-dialed', 'sound:T-is-not-available', 'sound:please-try-again-later']); + await sender.hangup().catch(() => {}); + await bridge.destroy().catch(() => {}); + }); + + receiver.once('StasisEnd', async () => { + await sender.hangup().catch(() => {}); + await bridge.destroy().catch(() => {}); + }); + sender.once('StasisEnd', async () => { + await receiver.hangup().catch(() => {}); + await bridge.destroy().catch(() => {}); + }); + } catch (err) { + return this.client.util.handleError(err, message, this); + } + } +} diff --git a/src/commands/site.ts b/src/commands/site.ts index 774ee9f..4e7384f 100644 --- a/src/commands/site.ts +++ b/src/commands/site.ts @@ -62,7 +62,7 @@ export default class SiteInfo extends Command { this.aliases = ['ssl', 'cert', 'certinfo', 'ci', 'tls', 'si', 'siteinfo']; this.permissions = 0; this.guildOnly = true; - this.enabled = true; + this.enabled = false; } public async run(message: Message, args: string[]) { @@ -157,6 +157,7 @@ export default class SiteInfo extends Command { const page = await browser.newPage(); await page.goto(`https://${args[0]}`); screenshotData = await page.screenshot(); + browser.close(); } catch (e) { this.client.util.signale.error(e); } diff --git a/src/commands/whois.ts b/src/commands/whois.ts index 5cc5379..07510c6 100644 --- a/src/commands/whois.ts +++ b/src/commands/whois.ts @@ -1,8 +1,7 @@ /* eslint-disable no-bitwise */ import moment from 'moment'; -import { Message, Member, TextChannel } from 'eris'; +import { Message, Member } from 'eris'; import { Client, Command, RichEmbed } from '../class'; -// import acknowledgements from '../configs/acknowledgements.json'; import { whois as emotes } from '../configs/emotes.json'; export default class Whois extends Command { @@ -34,7 +33,6 @@ export default class Whois extends Command { } const embed = new RichEmbed(); embed.setThumbnail(member.avatarURL); - // const ackResolve = this.resolveStaffInformation(member.id); const ackResolve = await this.client.db.Staff.findOne({ userID: member.id }).lean().exec(); let title = `${member.user.username}#${member.user.discriminator}`; if (ackResolve?.pn?.length > 0) title += `, ${ackResolve.pn.join(', ')}`; @@ -53,7 +51,7 @@ export default class Whois extends Command { } const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: member.user.id }).lean().exec(); if (pager?.num) { - description += `📡 ${pager.num}\n`; + description += `📟 ${pager.num}\n`; } if (ackResolve?.extension) { description += `☎️ ${ackResolve.extension}\n`; @@ -78,12 +76,11 @@ export default class Whois extends Command { } } embed.addField('Status', member.status === 'dnd' ? 'Do Not Disturb' : this.capsFirstLetter(member.status) || 'Offline', true); - // const platform = member.bot && member.status !== 'offline' ? 'API/WebSocket' : Object.entries(message.member.clientStatus).filter((a) => a[1] !== 'offline').map((a) => this.capsFirstLetter(a[0])).join(', '); - // if (platform) embed.addField('Platform', platform, true); embed.addField('Joined At', `${moment(new Date(member.joinedAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true); embed.addField('Created At', `${moment(new Date(member.user.createdAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true); const score = await this.client.db.Score.findOne({ userID: member.id }).lean().exec(); if (score) { +<<<<<<< HEAD await this.client.db.Score.updateOne({ userID: member.id }, { $addToSet: { softInquiries: { name: 'Library of Code sp-us | Bureau of Community Reports', date: new Date() } } }); const embed2 = new RichEmbed(); embed2.setTitle('Inquiry Notification'); @@ -96,6 +93,9 @@ export default class Whois extends Command { embed2.setFooter(this.client.user.username, this.client.user.avatarURL); const log = this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849'); log.createMessage({ embed: embed2 }).catch(() => { }); +======= + await this.client.report.createInquiry(member.id, 'Library of Code sp-us | Bureau of Community Reports', 1); +>>>>>>> upstream/master let totalScore = '0'; if (score.total < 200) totalScore = '---'; else if (score.total > 800) totalScore = '800'; @@ -229,8 +229,8 @@ export default class Whois extends Command { if (serverAcknowledgements.length > 0) { embed.addField('Acknowledgements', serverAcknowledgements[0]); } - if (ackResolve?.acknowledgements) { - embed.addField('Bot Acknowledgements', ackResolve.acknowledgements.join(', ')); + if (ackResolve?.additionalRoles?.length > 0) { + embed.addField('Additional Acknowledgements', ackResolve.additionalRoles.join(', ')); } embed.setFooter(this.client.user.username, this.client.user.avatarURL); embed.setTimestamp(); diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 8dbf502..b0a405f 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -12,8 +12,8 @@ export default class GuildMemberAdd extends Event { public async run(_, member: Member) { try { const search = await this.client.db.local.muted.get(`muted-${member.user.id}`); - if (search) { - await member.addRole('478373942638149643'); + if (search === true) { + member.addRole('478373942638149643', 'muted user left server and joined back'); } } catch (err) { this.client.util.handleError(err); diff --git a/src/intervals/autoRelease.ts b/src/intervals/autoRelease.ts index 865c2dd..08ecd6a 100644 --- a/src/intervals/autoRelease.ts +++ b/src/intervals/autoRelease.ts @@ -11,7 +11,7 @@ export default function checkLock(client: Client): NodeJS.Timeout { if (moderation.expiration.processed) return; if (new Date() > moderation.expiration.date) { await moderation.updateOne({ 'expiration.processed': true }); - // const moderator = client.guilds.get(client.config.guildID).members.get(moderation.moderatorID); + const system = client.guilds.get(client.config.guildID).members.get(client.user.id); switch (moderation.type) { case 5: diff --git a/src/intervals/score.ts b/src/intervals/score.ts index b1ced18..61bbb7a 100644 --- a/src/intervals/score.ts +++ b/src/intervals/score.ts @@ -63,8 +63,8 @@ export default async function calculateScore(client: Client): Promise('Inquiry', Inquiry); diff --git a/src/models/Member.ts b/src/models/Member.ts index fcd6aaa..ccb6330 100644 --- a/src/models/Member.ts +++ b/src/models/Member.ts @@ -1,7 +1,7 @@ import { Document, Schema, model } from 'mongoose'; export interface MemberInterface extends Document { - userID: string + userID: string, additional: { langs: ['js', 'py', 'rb', 'ts', 'rs', 'go', 'cfam', 'csharp', 'swift', 'java', 'kt', 'asm'], operatingSystems: ['arch', 'deb', 'cent', 'fedora', 'manjaro', 'mdarwin', 'redhat', 'ubuntu', 'win'], @@ -9,6 +9,7 @@ export interface MemberInterface extends Document { gitlab: string, bio: string, }, + bio: string, } const Member: Schema = new Schema({ @@ -20,6 +21,7 @@ const Member: Schema = new Schema({ gitlab: String, bio: String, }, + bio: String, }); export default model('Member', Member); diff --git a/src/models/Score.ts b/src/models/Score.ts index f7e0bc6..96820d2 100644 --- a/src/models/Score.ts +++ b/src/models/Score.ts @@ -49,30 +49,11 @@ export interface ScoreInterfaceRaw { export interface ScoreInterface extends Document { userID: string - /** - * total will be between 800-200 - 0 signfies "No Score", too little information is available or other variable are too low - * - CALCULATION: `(COMBINED SUBSCORES x 5) * 5.13; Math.floor()` - */ total: number, - /** - * 10 - 55 - */ activity: number, - /** - * 0 - 54 - */ roles: number, - /** - * -50 - 2 - * all users start out with 2 moderation points, the number of points decreases for each moderation. - */ moderation: number, - /** - * -20 - 50 - * processed by CSD - */ cloudServices: number, - // 0 or 20, 20 points are added if the user is a staff member staff: number, other: number, notify: boolean, diff --git a/src/models/ScoreHistorical.ts b/src/models/ScoreHistorical.ts index 1b1ca8a..70b5a6a 100644 --- a/src/models/ScoreHistorical.ts +++ b/src/models/ScoreHistorical.ts @@ -1,21 +1,24 @@ -import { Document, Schema, model } from 'mongoose'; +import { Document, Schema, model, Types } from 'mongoose'; import { ScoreInterfaceRaw } from '.'; export interface ScoreHistoricalRaw { userID: string, report: ScoreInterfaceRaw, + inquiries: [ Types.ObjectId ], date: Date, } export interface ScoreHistoricalInterface extends Document { userID: string, report: ScoreInterfaceRaw, + inquiries: [ Types.ObjectId ], date: Date } const ScoreHistorical: Schema = new Schema({ userID: String, report: Object, + inquiries: Array, date: Date, }); diff --git a/src/models/Staff.ts b/src/models/Staff.ts index ec22b56..83c8c2e 100644 --- a/src/models/Staff.ts +++ b/src/models/Staff.ts @@ -9,6 +9,7 @@ export interface StaffInterface extends Document { emailAddress: string, extension: string, acknowledgements: string[], + additionalRoles: string[] } const Staff: Schema = new Schema({ @@ -20,6 +21,7 @@ const Staff: Schema = new Schema({ emailAddress: String, extension: String, acknowledgements: Array, + additionalRoles: Array, }); export default model('Staff', Staff); diff --git a/src/models/index.ts b/src/models/index.ts index cb333b9..59a8b60 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,19 +1,16 @@ export { default as Customer, CustomerInterface } from './Customer'; export { default as CustomerPortal, CustomerPortalInterface } from './CustomerPortal'; -export { default as ExecutiveOrder, ExecutiveOrderInterface } from './ExecutiveOrder'; export { default as File, FileInterface } from './File'; +export { default as Inquiry, InquiryInterface, InqType } from './Inquiry'; 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'; -export { default as Proclamation, ProclamationInterface } from './Proclamation'; export { default as Promo, PromoInterface } from './Promo'; export { default as Rank, RankInterface } from './Rank'; export { default as Redirect, RedirectInterface, RedirectRaw } from './Redirect'; -export { default as Resolution, ResolutionInterface } from './Resolution'; export { default as Score, ScoreInterface, ScoreInterfaceRaw } from './Score'; export { default as ScoreHistorical, ScoreHistoricalInterface } from './ScoreHistorical'; export { default as Staff, StaffInterface } from './Staff'; diff --git a/src/pbx/actions/Misc.ts b/src/pbx/actions/Misc.ts index 2465a9d..4193287 100644 --- a/src/pbx/actions/Misc.ts +++ b/src/pbx/actions/Misc.ts @@ -4,11 +4,17 @@ import { promises as fs } from 'fs'; import { PBX } from '../../class'; export default class Misc { - public static async accessDenied(channel: ARI.Channel) { - const playback = await channel.play({ - media: 'sound:access-denied', - }, undefined); - playback.once('PlaybackFinished', () => channel.hangup()); + public static accessDenied(channel: ARI.Channel) { + return new Promise((resolve, reject) => { + channel.play({ + media: 'sound:access-denied', + }, undefined).then((playback) => { + playback.on('PlaybackFinished', () => { + channel.hangup().catch((err) => reject(err)); + resolve(); + }); + }).catch((err) => reject(err)); + }); } public static async TTS(pbx: PBX, text: string, gender: string | any): Promise { @@ -24,7 +30,7 @@ export default class Misc { return `sound:/tmp/${fileExtension}`; } - public static play(pbx: PBX, channel: ARI.Channel, sound: string): Promise { + public static play(pbx: PBX, channel: ARI.Channel, sound: string | string[]): Promise { const playback = pbx.ari.Playback(); return new Promise((resolve, reject) => { diff --git a/tsconfig.json b/tsconfig.json index 251319b..675c552 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ diff --git a/yarn.lock b/yarn.lock index 683f0f1..0b6ddc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -562,6 +562,14 @@ ax@0.1.8: resolved "https://registry.yarnpkg.com/ax/-/ax-0.1.8.tgz#27ca9a73fa4c78a478d62d827cad860a24fdd497" integrity sha1-J8qac/pMeKR41i2CfK2GCiT91Jc= +axios@^0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" + integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + axios@^0.19.2: version "0.19.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" @@ -2101,6 +2109,11 @@ is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.0" +is-buffer@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" @@ -3550,6 +3563,13 @@ standard-as-callback@^2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stock-info@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/stock-info/-/stock-info-1.2.0.tgz#a29f54c5416deec5a08a9f3c19736a91c913c20a" + integrity sha512-GxhFrCWlibRdS/9KC1aRMBD0p4IyGj/rKD5InK/ZUOFAapJ76QhVY/ShsNT0WRkOh12Bh6NfZkWU/Ipj90NstQ== + dependencies: + axios "^0.18.1" + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -3652,7 +3672,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -stripe@^8.135.0: +stripe@^8.120.0: version "8.137.0" resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.137.0.tgz#9344d99d96ef3a19ff4c92f2bb141c40f2b18a6a" integrity sha512-UlxXjff6O+0hGY7DRZZnepZegXfi8KoYKuW4fgMlNIiyvKR/G8EjL13uaqFI31vmVm0WxNvjrBiHd9DFX9rLgA== @@ -3781,6 +3801,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tsutils@^3.17.1: version "3.20.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" @@ -3830,7 +3855,7 @@ typed-function@^2.0.0: resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8" integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA== -typescript@^3.9.8: +typescript@^3.9.2: version "3.9.9" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== @@ -3897,9 +3922,9 @@ uuid@^8.0.0, uuid@^8.3.0: integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache@^2.0.3: - version "2.2.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" - integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== validate-npm-package-license@^3.0.1: version "3.0.4"