From f4c38a0865b09431c857661113269b6d50e2412c Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 15 Jun 2019 17:44:20 -0400 Subject: [PATCH 001/167] Adds Latest Fixes from sfreeman422/mocker (#1) * Remove Spam From Muzzled Users (#5) * Removed unnecessary console log * Converted muzzled array to a Map and added a muzzlers Map to track requestors. Added corresponding tests * Added error strings to tests * Removed isMuzzled references in favor of muzzled.has * Removed unnecessary types * Added a 10 message limit to muzzled users * Adjusted linting options in package.json * Added precommit linting for tests also * Changed MAX_SUPPRESIONS to 7 * Bumps Axios to 0.18.1 (#6) * Bumps axios to avoid security vulnerabilities * adjusted package-lock.json * Added muzzle cooldown and tests (#7) * Max Muzzles Per Hour (#8) * Added max muzzles per hour feature * Converted to window.setTimeout and window.clearTimeout * Added proper types to take advantage of NodeJS.Timeout * Added tests for removeMuzzler * Feature/max muzzles per hour (#9) * Converted to window.setTimeout and window.clearTimeout * Added proper types to take advantage of NodeJS.Timeout * Fixed muzzle max issue * Fixed a regression in defin in which no response was being sent (#10) * Fix a bug with muzzle removal (#11) * added timeout for muzzle removal * Added timeout amount * Added fix for removal function (#12) * Added fit for removal function (#13) --- package-lock.json | 59 +++----- package.json | 4 +- src/routes/define-route.ts | 8 +- src/routes/mock-route.ts | 6 +- src/routes/muzzle-route.ts | 38 +++-- src/shared/models/muzzle/muzzle-models.ts | 9 ++ src/shared/models/slack/slack-models.ts | 13 -- src/utils/muzzle/muzzle-utils.spec.ts | 164 +++++++++++++++++++--- src/utils/muzzle/muzzle-utils.ts | 89 ++++++++++-- 9 files changed, 280 insertions(+), 110 deletions(-) create mode 100644 src/shared/models/muzzle/muzzle-models.ts diff --git a/package-lock.json b/package-lock.json index 26e292c1..f15f8943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,12 +83,6 @@ "@types/node": "*" } }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true - }, "@types/chai": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", @@ -170,18 +164,6 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, - "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", - "dev": true, - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" - } - }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -196,21 +178,6 @@ "@types/mime": "*" } }, - "@types/slack-node": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/slack-node/-/slack-node-0.1.2.tgz", - "integrity": "sha512-qSuxW6s2itY8WoOr+ELflN8vG0dpomPn+5v997YsUA5DwjWaS0JE/TF8ZrGTg6bdiL9cAGG4VhSRleeLetX5yw==", - "dev": true, - "requires": { - "@types/request": "*" - } - }, - "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -384,12 +351,19 @@ "dev": true }, "axios": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "requires": { - "follow-redirects": "^1.3.0", - "is-buffer": "^1.1.5" + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } } }, "balanced-match": { @@ -1434,9 +1408,9 @@ "dev": true }, "follow-redirects": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", - "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", "requires": { "debug": "=3.1.0" }, @@ -2447,7 +2421,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-callable": { "version": "1.1.4", diff --git a/package.json b/package.json index 85db3126..1ad1c62d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dependencies": { "@slack/web-api": "^5.0.1", "@types/express": "^4.16.1", - "axios": "^0.18.0", + "axios": "^0.18.1", "body-parser": "^1.18.3", "express": "^4.16.4", "typescript": "^3.4.5" @@ -46,7 +46,7 @@ } }, "lint-staged": { - "*.{js,css,json,md}": [ + "*.{ts, spec.ts, css,json,md}": [ "npm run lint", "git add" ] diff --git a/src/routes/define-route.ts b/src/routes/define-route.ts index 0871704f..3b99cf81 100644 --- a/src/routes/define-route.ts +++ b/src/routes/define-route.ts @@ -9,7 +9,7 @@ import { define, formatDefs } from "../utils/define/define-utils"; -import { isMuzzled } from "../utils/muzzle/muzzle-utils"; +import { muzzled } from "../utils/muzzle/muzzle-utils"; import { sendResponse } from "../utils/slack/slack-utils"; export const defineRoutes: Router = express.Router(); @@ -24,11 +24,11 @@ defineRoutes.post("/define", async (req: Request, res: Response) => { attachments: formatDefs(defined.list) }; - if (!isMuzzled(request.user_id)) { + if (muzzled.has(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { sendResponse(request.response_url, response); res.status(200).send(); - } else if (isMuzzled(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); } } catch (e) { res.send(`error: ${e.message}`); diff --git a/src/routes/mock-route.ts b/src/routes/mock-route.ts index 3ddc9a59..6e6e9ac5 100644 --- a/src/routes/mock-route.ts +++ b/src/routes/mock-route.ts @@ -4,7 +4,7 @@ import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; import { mock } from "../utils/mock/mock-utils"; -import { isMuzzled } from "../utils/muzzle/muzzle-utils"; +import { muzzled } from "../utils/muzzle/muzzle-utils"; import { sendResponse } from "../utils/slack/slack-utils"; export const mockRoutes: Router = express.Router(); @@ -21,10 +21,10 @@ mockRoutes.post("/mock", (req, res) => { response_type: "in_channel", text: `<@${request.user_id}>` }; - if (!isMuzzled(request.user_id)) { + if (!muzzled.has(request.user_id)) { sendResponse(request.response_url, response); res.status(200).send(); - } else if (isMuzzled(request.user_id)) { + } else if (muzzled.has(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } }); diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 44f60b10..9e507c7e 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -1,43 +1,52 @@ -import { WebClient } from "@slack/web-api"; +import { + ChatDeleteArguments, + ChatPostMessageArguments, + WebClient +} from "@slack/web-api"; import express, { Request, Response, Router } from "express"; import { - IDeleteMessageRequest, IEventRequest, - IPostMessageRequest, ISlashCommandRequest } from "../shared/models/slack/slack-models"; import { addUserToMuzzled, - isMuzzled, - muzzle + muzzle, + muzzled } from "../utils/muzzle/muzzle-utils"; import { getUserId, getUserName } from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); const muzzleToken: any = process.env.muzzleBotToken; const web: WebClient = new WebClient(muzzleToken); +const MAX_SUPPRESSIONS: number = 7; muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; console.log(request); - if (isMuzzled(request.event.user)) { + if (muzzled.has(request.event.user)) { console.log(`${request.event.user} is muzzled! Suppressing his voice...`); - const deleteRequest: IDeleteMessageRequest = { + const deleteRequest: ChatDeleteArguments = { token: muzzleToken, channel: request.event.channel, ts: request.event.ts, as_user: true }; - const postRequest: IPostMessageRequest = { - token: muzzleToken, - channel: request.event.channel, - text: `<@${request.event.user}> says "${muzzle(request.event.text)}"` - }; - web.chat.delete(deleteRequest).catch(e => console.error(e)); - web.chat.postMessage(postRequest).catch(e => console.error(e)); + if (muzzled.get(request.event.user)!.suppressionCount < MAX_SUPPRESSIONS) { + muzzled.set(request.event.user, { + suppressionCount: ++muzzled.get(request.event.user)!.suppressionCount, + muzzledBy: muzzled.get(request.event.user)!.muzzledBy + }); + + const postRequest: ChatPostMessageArguments = { + token: muzzleToken, + channel: request.event.channel, + text: `<@${request.event.user}> says "${muzzle(request.event.text)}"` + }; + web.chat.postMessage(postRequest).catch(e => console.error(e)); + } } res.send({ challenge: request.challenge }); }); @@ -46,7 +55,6 @@ muzzleRoutes.post("/muzzle", (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = getUserId(request.text); const userName: string = getUserName(request.text); - console.log(request); try { res.send(addUserToMuzzled(userId, userName, request.user_id)); } catch (e) { diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts new file mode 100644 index 00000000..07fac71c --- /dev/null +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -0,0 +1,9 @@ +export interface IMuzzled { + suppressionCount: number; + muzzledBy: string; +} + +export interface IMuzzler { + muzzleCount: number; + muzzleCountRemover?: NodeJS.Timeout; +} diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index 4a9027bf..95782b11 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -4,19 +4,6 @@ export interface IChannelResponse { attachments: IAttachment[]; } -export interface IDeleteMessageRequest { - token: string; - channel: string; - ts: string; - as_user: boolean; -} - -export interface IPostMessageRequest { - token: string; - channel: string; - text: string; -} - export interface ISlashCommandRequest { text: string; user_id: string; diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index 5151bcb7..0dcb24ee 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -1,48 +1,174 @@ import { expect } from "chai"; import { addUserToMuzzled, - isMuzzled, + MAX_MUZZLES, muzzled, - removeMuzzle + muzzlers, + removeMuzzle, + removeMuzzler } from "./muzzle-utils"; describe("muzzle-utils", () => { const testData = { user: "test-user", + user2: "test-user2", + user3: "test-user3", friendlyName: "test-muzzler", requestor: "test-requestor" }; beforeEach(() => { - muzzled.length = 0; - addUserToMuzzled(testData.user, testData.friendlyName, testData.requestor); + muzzled.clear(); + muzzlers.clear(); }); describe("addUserToMuzzled()", () => { - it("should add a user to the muzzled array", () => { - expect(muzzled.length).to.equal(1); - expect(isMuzzled(testData.user)).to.equal(true); + describe("muzzled", () => { + it("should add a user to the muzzled map", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.size).to.equal(1); + expect(muzzled.has(testData.user)).to.equal(true); + }); + + it("should return an added user with IMuzzled attributes", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.get(testData.user)!.suppressionCount).to.equal(0); + expect(muzzled.get(testData.user)!.muzzledBy).to.equal( + testData.requestor + ); + }); + + it("should throw an error if a user tries to muzzle an already muzzled user", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.has(testData.user)).to.equal(true); + expect(() => + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ) + ).to.throw(`${testData.friendlyName} is already muzzled!`); + }); + + it("should throw an error if a requestor tries to muzzle someone while the requestor is muzzled", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.has(testData.user)).to.equal(true); + expect(() => + addUserToMuzzled( + testData.requestor, + testData.friendlyName, + testData.user + ) + ).to.throw(`You can't muzzle someone if you are already muzzled!`); + }); + }); + + describe("muzzlers", () => { + it("should add a user to the muzzlers map", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + + expect(muzzlers.size).to.equal(1); + expect(muzzlers.has(testData.requestor)).to.equal(true); + }); + + it("should return an added user with IMuzzler attributes", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(1); + }); + + it("should increment a requestors muzzle count on a second addUserToMuzzled() call", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + addUserToMuzzled( + testData.user2, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.size).to.equal(2); + expect(muzzlers.has(testData.requestor)).to.equal(true); + expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(2); + }); + + it("should prevent a requestor from muzzling on their third count", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + addUserToMuzzled( + testData.user2, + testData.friendlyName, + testData.requestor + ); + expect(() => + addUserToMuzzled( + testData.user3, + testData.friendlyName, + testData.requestor + ) + ).to.throw( + `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + ); + }); }); }); describe("removeMuzzle()", () => { it("should remove a user from the muzzled array", () => { - expect(muzzled.length).to.equal(1); - expect(isMuzzled(testData.user)).to.equal(true); + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.size).to.equal(1); + expect(muzzled.has(testData.user)).to.equal(true); removeMuzzle(testData.user); - expect(isMuzzled(testData.user)).to.equal(false); - expect(muzzled.length).to.equal(0); + expect(muzzled.has(testData.user)).to.equal(false); + expect(muzzled.size).to.equal(0); }); }); - describe("isMuzzled()", () => { - it("should return true if a user is muzzled", () => { - expect(isMuzzled(testData.user)).to.equal(true); - }); - - it("should return false if a user is not muzzled", () => { - removeMuzzle(testData.user); - expect(isMuzzled(testData.user)).to.equal(false); + describe("removeMuzzler()", () => { + it("should remove a user from the muzzler array", () => { + addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ); + expect(muzzled.size).to.equal(1); + expect(muzzled.has(testData.user)).to.equal(true); + expect(muzzlers.size).to.equal(1); + expect(muzzlers.has(testData.requestor)).to.equal(true); + removeMuzzler(testData.requestor); + expect(muzzlers.has(testData.requestor)).to.equal(false); + expect(muzzlers.size).to.equal(0); }); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 39529189..77ea72e1 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -1,6 +1,13 @@ +import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; // Store for the muzzled users. -export const muzzled: string[] = []; +export const muzzled: Map = new Map(); +// STore for people who are muzzling others. +export const muzzlers: Map = new Map(); +// Time period in which a user must wait before making more muzzles. +const MAX_MUZZLE_TIME = 3600000; +const MAX_TIME_BETWEEN_MUZZLES = 3600000; +export const MAX_MUZZLES = 2; /** * Takes in text and randomly muzzles certain words. */ @@ -13,13 +20,6 @@ export function muzzle(text: string) { return returnText; } -/** - * Tells whether or not a user has been added to the muzzled arary - */ -export function isMuzzled(user: string) { - return muzzled.includes(user); -} - /** * Adds a user to the muzzled array and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ @@ -31,18 +31,59 @@ export function addUserToMuzzled( const timeToMuzzle = Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); const minutes = Math.floor(timeToMuzzle / 60000); const seconds = ((timeToMuzzle % 60000) / 1000).toFixed(0); - if (muzzled.includes(toMuzzle)) { + if (muzzled.has(toMuzzle)) { console.error( `${requestor} attempted to muzzle ${toMuzzle} but ${toMuzzle} is already muzzled.` ); throw new Error(`${friendlyMuzzle} is already muzzled!`); - } else if (muzzled.includes(requestor)) { + } else if (muzzled.has(requestor)) { console.error( `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} is currently muzzled` ); throw new Error(`You can't muzzle someone if you are already muzzled!`); + } else if ( + muzzlers.has(requestor) && + muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES + ) { + console.error( + `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} has reached maximum muzzle of ${MAX_MUZZLES}` + ); + throw new Error( + `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + ); } else { - muzzled.push(toMuzzle); + // Add a newly muzzled user. + muzzled.set(toMuzzle, { + suppressionCount: 0, + muzzledBy: requestor + }); + const muzzleCount = muzzlers.has(requestor) + ? ++muzzlers.get(requestor)!.muzzleCount + : 1; + // Add requestor to muzzlers + muzzlers.set(requestor, { + muzzleCount, + muzzleCountRemover: setTimeout( + () => decrementMuzzleCount(requestor), + MAX_TIME_BETWEEN_MUZZLES + ) + }); + + if ( + muzzlers.has(requestor) && + muzzlers.get(requestor)!.muzzleCountRemover + ) { + const currentTimer = muzzlers.get(requestor)!.muzzleCountRemover; + clearTimeout(currentTimer as NodeJS.Timeout); + const removalFunction = + muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES + ? () => removeMuzzler(requestor) + : () => decrementMuzzleCount(requestor); + muzzlers.set(requestor, { + muzzleCount: muzzlers.get(requestor)!.muzzleCount, + muzzleCountRemover: setTimeout(removalFunction, MAX_MUZZLE_TIME) + }); + } console.log( `${friendlyMuzzle} is now muzzled for ${timeToMuzzle} milliseconds` ); @@ -55,8 +96,32 @@ export function addUserToMuzzled( } } +export function decrementMuzzleCount(requestor: string) { + if (muzzlers.has(requestor)) { + const decrementedMuzzle = --muzzlers.get(requestor)!.muzzleCount; + muzzlers.set(requestor, { + muzzleCount: decrementedMuzzle, + muzzleCountRemover: muzzlers.get(requestor)!.muzzleCountRemover + }); + console.log( + `Successfully decremented ${requestor} muzzleCount to ${decrementedMuzzle}` + ); + } else { + console.error( + `Attemped to decrement muzzle count for ${requestor} but they did not exist!` + ); + } +} + +export function removeMuzzler(user: string) { + muzzlers.delete(user); + console.log( + `${MAX_MUZZLE_TIME} has passed since ${user} last successful muzzle. They have been removed from muzzlers.` + ); +} + export function removeMuzzle(user: string) { - muzzled.splice(muzzled.indexOf(user), 1); + muzzled.delete(user); console.log(`Removed ${user}'s muzzle! He is free at last.`); } From 37c7f3ec90538e42c369a63253f2ec9d62d2f1c6 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Tue, 18 Jun 2019 19:21:16 -0400 Subject: [PATCH 002/167] Remove Try Catch from `muzzle-routes` (#2) * Removed try catach block for addUserToMuzzled * Added lolex to successfully mock setTimeout in tests * Added lolex cleanup * Route cleanup * Updated describe blocks to reflect reject rather than throw --- package-lock.json | 12 +++ package.json | 4 +- src/routes/muzzle-route.ts | 13 ++- src/utils/muzzle/muzzle-utils.spec.ts | 69 ++++++++------ src/utils/muzzle/muzzle-utils.ts | 128 ++++++++++++++------------ 5 files changed, 131 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index f15f8943..951fd1e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,12 @@ "@types/node": "*" } }, + "@types/lolex": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/lolex/-/lolex-3.1.1.tgz", + "integrity": "sha512-NU2qVtKxbt4IBvjEOW1QeUnV6KGUF6hpgJyvwZt3JrXe2qmwQF0+BiazQw+iFy9qL5ie+QHOxTzXkcvJUEh76g==", + "dev": true + }, "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", @@ -2986,6 +2992,12 @@ } } }, + "lolex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", + "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", + "dev": true + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", diff --git a/package.json b/package.json index 1ad1c62d..c84f22e3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "license": "ISC", "dependencies": { "@slack/web-api": "^5.0.1", - "@types/express": "^4.16.1", "axios": "^0.18.1", "body-parser": "^1.18.3", "express": "^4.16.4", @@ -27,11 +26,14 @@ }, "devDependencies": { "@types/chai": "^4.1.7", + "@types/express": "^4.16.1", + "@types/lolex": "^3.1.1", "@types/mocha": "^5.2.6", "@types/node": "^12.0.2", "chai": "^4.2.0", "husky": "^2.3.0", "lint-staged": "^8.1.7", + "lolex": "^4.1.0", "mocha": "^6.1.4", "nodemon": "^1.19.0", "prettier": "1.17.1", diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 9e507c7e..69b5a7c6 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -51,13 +51,16 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { res.send({ challenge: request.challenge }); }); -muzzleRoutes.post("/muzzle", (req: Request, res: Response) => { +muzzleRoutes.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = getUserId(request.text); const userName: string = getUserName(request.text); - try { - res.send(addUserToMuzzled(userId, userName, request.user_id)); - } catch (e) { - res.send(e.message); + const results = await addUserToMuzzled( + userId, + userName, + request.user_id + ).catch(e => res.send(e)); + if (results) { + res.send(results); } }); diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index 0dcb24ee..3aa03f7c 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import * as lolex from "lolex"; import { addUserToMuzzled, MAX_MUZZLES, @@ -17,11 +18,21 @@ describe("muzzle-utils", () => { requestor: "test-requestor" }; + const clock = lolex.install(); + beforeEach(() => { muzzled.clear(); muzzlers.clear(); }); + afterEach(() => { + clock.reset(); + }); + + after(() => { + clock.uninstall(); + }); + describe("addUserToMuzzled()", () => { describe("muzzled", () => { it("should add a user to the muzzled map", () => { @@ -46,36 +57,38 @@ describe("muzzle-utils", () => { ); }); - it("should throw an error if a user tries to muzzle an already muzzled user", () => { - addUserToMuzzled( + it("should reject if a user tries to muzzle an already muzzled user", async () => { + await addUserToMuzzled( testData.user, testData.friendlyName, testData.requestor ); expect(muzzled.has(testData.user)).to.equal(true); - expect(() => - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ) - ).to.throw(`${testData.friendlyName} is already muzzled!`); + await addUserToMuzzled( + testData.user, + testData.friendlyName, + testData.requestor + ).catch(e => { + expect(e).to.equal(`${testData.friendlyName} is already muzzled!`); + }); }); - it("should throw an error if a requestor tries to muzzle someone while the requestor is muzzled", () => { - addUserToMuzzled( + it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { + await addUserToMuzzled( testData.user, testData.friendlyName, testData.requestor ); expect(muzzled.has(testData.user)).to.equal(true); - expect(() => - addUserToMuzzled( - testData.requestor, - testData.friendlyName, - testData.user - ) - ).to.throw(`You can't muzzle someone if you are already muzzled!`); + await addUserToMuzzled( + testData.requestor, + testData.friendlyName, + testData.user + ).catch(e => { + expect(e).to.equal( + `You can't muzzle someone if you are already muzzled!` + ); + }); }); }); @@ -116,25 +129,25 @@ describe("muzzle-utils", () => { expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(2); }); - it("should prevent a requestor from muzzling on their third count", () => { - addUserToMuzzled( + it("should prevent a requestor from muzzling on their third count", async () => { + await addUserToMuzzled( testData.user, testData.friendlyName, testData.requestor ); - addUserToMuzzled( + await addUserToMuzzled( testData.user2, testData.friendlyName, testData.requestor ); - expect(() => - addUserToMuzzled( - testData.user3, - testData.friendlyName, - testData.requestor + await addUserToMuzzled( + testData.user3, + testData.friendlyName, + testData.requestor + ).catch(e => + expect(e).to.equal( + `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ) - ).to.throw( - `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 77ea72e1..55a1f1c7 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -1,7 +1,7 @@ import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; // Store for the muzzled users. export const muzzled: Map = new Map(); -// STore for people who are muzzling others. +// Store for people who are muzzling others. export const muzzlers: Map = new Map(); // Time period in which a user must wait before making more muzzles. @@ -28,72 +28,78 @@ export function addUserToMuzzled( friendlyMuzzle: string, requestor: string ) { - const timeToMuzzle = Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); - const minutes = Math.floor(timeToMuzzle / 60000); - const seconds = ((timeToMuzzle % 60000) / 1000).toFixed(0); - if (muzzled.has(toMuzzle)) { - console.error( - `${requestor} attempted to muzzle ${toMuzzle} but ${toMuzzle} is already muzzled.` - ); - throw new Error(`${friendlyMuzzle} is already muzzled!`); - } else if (muzzled.has(requestor)) { - console.error( - `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} is currently muzzled` - ); - throw new Error(`You can't muzzle someone if you are already muzzled!`); - } else if ( - muzzlers.has(requestor) && - muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES - ) { - console.error( - `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} has reached maximum muzzle of ${MAX_MUZZLES}` - ); - throw new Error( - `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + return new Promise((resolve, reject) => { + const timeToMuzzle = Math.floor( + Math.random() * (180000 - 30000 + 1) + 30000 ); - } else { - // Add a newly muzzled user. - muzzled.set(toMuzzle, { - suppressionCount: 0, - muzzledBy: requestor - }); - const muzzleCount = muzzlers.has(requestor) - ? ++muzzlers.get(requestor)!.muzzleCount - : 1; - // Add requestor to muzzlers - muzzlers.set(requestor, { - muzzleCount, - muzzleCountRemover: setTimeout( - () => decrementMuzzleCount(requestor), - MAX_TIME_BETWEEN_MUZZLES - ) - }); - - if ( + const minutes = Math.floor(timeToMuzzle / 60000); + const seconds = ((timeToMuzzle % 60000) / 1000).toFixed(0); + if (muzzled.has(toMuzzle)) { + console.error( + `${requestor} attempted to muzzle ${toMuzzle} but ${toMuzzle} is already muzzled.` + ); + reject(`${friendlyMuzzle} is already muzzled!`); + } else if (muzzled.has(requestor)) { + console.error( + `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} is currently muzzled` + ); + reject(`You can't muzzle someone if you are already muzzled!`); + } else if ( muzzlers.has(requestor) && - muzzlers.get(requestor)!.muzzleCountRemover + muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES ) { - const currentTimer = muzzlers.get(requestor)!.muzzleCountRemover; - clearTimeout(currentTimer as NodeJS.Timeout); - const removalFunction = - muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES - ? () => removeMuzzler(requestor) - : () => decrementMuzzleCount(requestor); + console.error( + `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} has reached maximum muzzle of ${MAX_MUZZLES}` + ); + reject( + `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + ); + } else { + // Add a newly muzzled user. + muzzled.set(toMuzzle, { + suppressionCount: 0, + muzzledBy: requestor + }); + const muzzleCount = muzzlers.has(requestor) + ? ++muzzlers.get(requestor)!.muzzleCount + : 1; + // Add requestor to muzzlers muzzlers.set(requestor, { - muzzleCount: muzzlers.get(requestor)!.muzzleCount, - muzzleCountRemover: setTimeout(removalFunction, MAX_MUZZLE_TIME) + muzzleCount, + muzzleCountRemover: setTimeout( + () => decrementMuzzleCount(requestor), + MAX_TIME_BETWEEN_MUZZLES + ) }); + + if ( + muzzlers.has(requestor) && + muzzlers.get(requestor)!.muzzleCountRemover + ) { + const currentTimer = muzzlers.get(requestor)!.muzzleCountRemover; + clearTimeout(currentTimer as NodeJS.Timeout); + const removalFunction = + muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES + ? () => removeMuzzler(requestor) + : () => decrementMuzzleCount(requestor); + muzzlers.set(requestor, { + muzzleCount: muzzlers.get(requestor)!.muzzleCount, + muzzleCountRemover: setTimeout(removalFunction, MAX_MUZZLE_TIME) + }); + } + console.log( + `${friendlyMuzzle} is now muzzled for ${timeToMuzzle} milliseconds` + ); + setTimeout(() => removeMuzzle(toMuzzle), timeToMuzzle); + resolve( + `Succesfully muzzled ${friendlyMuzzle} for ${ + +seconds === 60 + ? minutes + 1 + "m00s" + : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s" + } minutes` + ); } - console.log( - `${friendlyMuzzle} is now muzzled for ${timeToMuzzle} milliseconds` - ); - setTimeout(() => removeMuzzle(toMuzzle), timeToMuzzle); - return `Succesfully muzzled ${friendlyMuzzle} for ${ - +seconds === 60 - ? minutes + 1 + "m00s" - : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s" - } minutes`; - } + }); } export function decrementMuzzleCount(requestor: string) { From 8af9204c2ae03207df3952326bf82d782def73d9 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Tue, 25 Jun 2019 13:04:06 -0400 Subject: [PATCH 003/167] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 2097e37ba5ca682a5be201f493cba4dc2d77ae44 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 12:58:51 -0400 Subject: [PATCH 004/167] Blocked any @ message and added some light tests (#9) --- src/utils/muzzle/muzzle-utils.spec.ts | 24 ++++++++++++++++++++++++ src/utils/muzzle/muzzle-utils.ts | 10 +++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index 3aa03f7c..efa1d4a4 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -2,7 +2,9 @@ import { expect } from "chai"; import * as lolex from "lolex"; import { addUserToMuzzled, + containsAt, MAX_MUZZLES, + muzzle, muzzled, muzzlers, removeMuzzle, @@ -184,4 +186,26 @@ describe("muzzle-utils", () => { expect(muzzlers.size).to.equal(0); }); }); + + describe("containsAt()", () => { + it("should return true if a word has @ in it", () => { + const testWord = "@channel"; + expect(containsAt(testWord)).to.equal(true); + }); + + it("should return false if a word does not include @", () => { + const testWord = "test"; + expect(containsAt(testWord)).to.equal(false); + }); + }); + + describe("muzzle()", () => { + it("should always muzzle @", () => { + const testSentence = + "@channel @channel @channel @channel @channel @channel @channel @jrjrjr @fudka"; + expect(muzzle(testSentence)).to.equal( + " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " + ); + }); + }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 55a1f1c7..2c522b99 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -15,11 +15,19 @@ export function muzzle(text: string) { let returnText = ""; const words = text.split(" "); for (const word of words) { - returnText += isRandomEven() ? ` *${word}* ` : " ..mMm.. "; + returnText += + isRandomEven() && !containsAt(word) ? ` *${word}* ` : " ..mMm.. "; } return returnText; } +/** + * Determines whether or not a user is trying to @ someone while muzzled. + */ +export function containsAt(word: string): boolean { + return word.includes("@"); +} + /** * Adds a user to the muzzled array and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ From 89d5c64824dd79cf98799ceb9d69033106554139 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 13:08:35 -0400 Subject: [PATCH 005/167] Feature/fix @channels (#10) * Added fix for @channels * Added test for @channel fix --- src/utils/muzzle/muzzle-utils.spec.ts | 10 ++++++++++ src/utils/muzzle/muzzle-utils.ts | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index efa1d4a4..3a5b8c22 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -197,6 +197,11 @@ describe("muzzle-utils", () => { const testWord = "test"; expect(containsAt(testWord)).to.equal(false); }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(containsAt(testWord)).to.equal(true); + }); }); describe("muzzle()", () => { @@ -207,5 +212,10 @@ describe("muzzle-utils", () => { " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " ); }); + + it("should always muzzle ", () => { + const testSentence = " hey guys"; + expect(muzzle(testSentence)).to.equal(" ..mMm.. ..mMm.. ..mMm.. "); + }); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 2c522b99..0cbb637a 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -22,10 +22,10 @@ export function muzzle(text: string) { } /** - * Determines whether or not a user is trying to @ someone while muzzled. + * Determines whether or not a user is trying to @ someone while muzzled or @ channel. */ export function containsAt(word: string): boolean { - return word.includes("@"); + return word.includes("@") || word.includes(""); } /** From a59afb4617e99ce0344476209bf2f81fb6a2db74 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 14:05:01 -0400 Subject: [PATCH 006/167] Feature/fix gif (#11) * Added ability to block bot_message from muzzled users. Also moved code from route to utils re: sendMessage and deleteMessage * Changed to reference the proper authed_users * Fixed log * Adjusted logging * Added comments and moved WebClient initialization into muzzle-utils * Fixed an issue in which muzzles were getting delted while a person was muzzled * Added comments --- src/routes/muzzle-route.ts | 42 +++++++++++-------------- src/shared/models/slack/slack-models.ts | 5 ++- src/utils/muzzle/muzzle-utils.ts | 34 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 69b5a7c6..11c3bf93 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -1,8 +1,3 @@ -import { - ChatDeleteArguments, - ChatPostMessageArguments, - WebClient -} from "@slack/web-api"; import express, { Request, Response, Router } from "express"; import { IEventRequest, @@ -10,14 +5,14 @@ import { } from "../shared/models/slack/slack-models"; import { addUserToMuzzled, + deleteMessage, muzzle, - muzzled + muzzled, + sendMessage } from "../utils/muzzle/muzzle-utils"; import { getUserId, getUserName } from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); -const muzzleToken: any = process.env.muzzleBotToken; -const web: WebClient = new WebClient(muzzleToken); const MAX_SUPPRESSIONS: number = 7; muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { @@ -25,28 +20,29 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { console.log(request); if (muzzled.has(request.event.user)) { console.log(`${request.event.user} is muzzled! Suppressing his voice...`); - const deleteRequest: ChatDeleteArguments = { - token: muzzleToken, - channel: request.event.channel, - ts: request.event.ts, - as_user: true - }; - - web.chat.delete(deleteRequest).catch(e => console.error(e)); + deleteMessage(request.event.channel, request.event.ts); if (muzzled.get(request.event.user)!.suppressionCount < MAX_SUPPRESSIONS) { muzzled.set(request.event.user, { suppressionCount: ++muzzled.get(request.event.user)!.suppressionCount, muzzledBy: muzzled.get(request.event.user)!.muzzledBy }); - - const postRequest: ChatPostMessageArguments = { - token: muzzleToken, - channel: request.event.channel, - text: `<@${request.event.user}> says "${muzzle(request.event.text)}"` - }; - web.chat.postMessage(postRequest).catch(e => console.error(e)); + sendMessage( + request.event.channel, + `<@${request.event.user}> says "${muzzle(request.event.text)}"` + ); } + } else if ( + request.event.subtype === "bot_message" && + muzzled.has(request.authed_users[0]) && + request.event.username !== "muzzle" + ) { + console.log( + `${ + request.authed_users[0] + } is muzzled and tried to send a bot message! Suppressing...` + ); + deleteMessage(request.event.channel, request.event.ts); } res.send({ challenge: request.challenge }); }); diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index 95782b11..f95d8e3b 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -24,13 +24,16 @@ export interface IEventRequest { export interface IEvent { client_msg_id: string; - type: string; // Is there more specific types? + type: string; + subtype: string; text: string; user: string; + username: string; ts: string; channel: string; event_ts: string; channel_type: string; + authed_users: string[]; } export interface IAttachment { diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 0cbb637a..70ba3f1e 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -1,3 +1,8 @@ +import { + ChatDeleteArguments, + ChatPostMessageArguments, + WebClient +} from "@slack/web-api"; import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; // Store for the muzzled users. export const muzzled: Map = new Map(); @@ -8,6 +13,8 @@ export const muzzlers: Map = new Map(); const MAX_MUZZLE_TIME = 3600000; const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_MUZZLES = 2; + +const web: WebClient = new WebClient(process.env.muzzleBotToken); /** * Takes in text and randomly muzzles certain words. */ @@ -142,3 +149,30 @@ export function removeMuzzle(user: string) { export function isRandomEven() { return Math.floor(Math.random() * 2) % 2 === 0; } +/** + * Handles deletion of messages. + */ +export function deleteMessage(channel: string, ts: string) { + const muzzleToken: any = process.env.muzzleBotToken; + const deleteRequest: ChatDeleteArguments = { + token: muzzleToken, + channel, + ts, + as_user: true + }; + + web.chat.delete(deleteRequest).catch(e => console.error(e)); +} + +/** + * Handles sending messages to the chat. + */ +export function sendMessage(channel: string, text: string) { + const muzzleToken: any = process.env.muzzleBotToken; + const postRequest: ChatPostMessageArguments = { + token: muzzleToken, + channel, + text + }; + web.chat.postMessage(postRequest).catch(e => console.error(e)); +} From 780e3e34a58b73ac46f1c73111a089152a31e49d Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 14:35:04 -0400 Subject: [PATCH 007/167] Feature/retry deletion (#12) * Added 30 second retry for deleted items * Adjusted timeout * Added error handling that ignores when a message is not found * Adjusted retry time to 5 seconds --- src/utils/muzzle/muzzle-utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 70ba3f1e..bc11df3c 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -161,7 +161,15 @@ export function deleteMessage(channel: string, ts: string) { as_user: true }; - web.chat.delete(deleteRequest).catch(e => console.error(e)); + web.chat.delete(deleteRequest).catch(e => { + if (e.data.error === "message_not_found") { + console.log("Message already deleted, no need to retry"); + } else { + console.error(e); + console.error("Retrying in 5 seconds..."); + setTimeout(() => deleteMessage(channel, ts), 5000); + } + }); } /** From 52ea59ed6847452e1199c31a5882ee79719d7b39 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 16:40:10 -0400 Subject: [PATCH 008/167] Bug/fix gif blocking (#13) * Added better userId and userName retrieval as well as ability to block gif * Removed unnecessary export added logging * Added proper resp.members * Added support for other bots and better logging * Adjusted tests. Still failing due to a lack of proper mock. TODO * Added test function to slack-utils and fixed up tests --- src/routes/muzzle-route.ts | 16 ++-- src/server.ts | 3 + src/shared/models/slack/slack-models.ts | 22 +++++ src/utils/muzzle/muzzle-utils.spec.ts | 111 +++++++----------------- src/utils/muzzle/muzzle-utils.ts | 76 ++++++++-------- src/utils/slack/slack-utils.spec.ts | 24 +++-- src/utils/slack/slack-utils.ts | 41 ++++++++- 7 files changed, 153 insertions(+), 140 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 11c3bf93..91192c8e 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -10,7 +10,7 @@ import { muzzled, sendMessage } from "../utils/muzzle/muzzle-utils"; -import { getUserId, getUserName } from "../utils/slack/slack-utils"; +import { getUserId } from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); const MAX_SUPPRESSIONS: number = 7; @@ -34,7 +34,10 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { } } else if ( request.event.subtype === "bot_message" && - muzzled.has(request.authed_users[0]) && + request.event.attachments && + muzzled.has( + getUserId(request.event.attachments[0].text || request.event.text) + ) && request.event.username !== "muzzle" ) { console.log( @@ -50,12 +53,9 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { muzzleRoutes.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = getUserId(request.text); - const userName: string = getUserName(request.text); - const results = await addUserToMuzzled( - userId, - userName, - request.user_id - ).catch(e => res.send(e)); + const results = await addUserToMuzzled(userId, request.user_id).catch(e => + res.send(e) + ); if (results) { res.send(results); } diff --git a/src/server.ts b/src/server.ts index 65dd29a2..ae137ac3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,7 @@ import express, { Application } from "express"; import { defineRoutes } from "./routes/define-route"; import { mockRoutes } from "./routes/mock-route"; import { muzzleRoutes } from "./routes/muzzle-route"; +import { getAllUsers } from "./utils/slack/slack-utils"; const app: Application = express(); const PORT: number = 3000; @@ -13,6 +14,8 @@ app.use(mockRoutes); app.use(muzzleRoutes); app.use(defineRoutes); +getAllUsers(); + app.listen(PORT, (e: Error) => e ? console.error(e) : console.log("Listening on port 3000") ); diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index f95d8e3b..6fbc0809 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -34,8 +34,30 @@ export interface IEvent { event_ts: string; channel_type: string; authed_users: string[]; + attachments: IEvent[]; } export interface IAttachment { text: string; } + +export interface ISlackUser { + id: string; + team_id: string; + name: string; + deleted: boolean; + color: string; + real_name: string; + tz: string; + tz_label: string; + tz_offset: number; + profile: any; + is_admin: boolean; + is_owner: boolean; + is_primary_owner: boolean; + is_restricted: boolean; + is_ultra_restricted: boolean; + is_bot: boolean; + is_app_user: boolean; + updated: number; +} diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index 3a5b8c22..de468f01 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -1,5 +1,7 @@ import { expect } from "chai"; import * as lolex from "lolex"; +import { ISlackUser } from "../../shared/models/slack/slack-models"; +import { setUserList } from "../slack/slack-utils"; import { addUserToMuzzled, containsAt, @@ -13,11 +15,10 @@ import { describe("muzzle-utils", () => { const testData = { - user: "test-user", - user2: "test-user2", - user3: "test-user3", - friendlyName: "test-muzzler", - requestor: "test-requestor" + user: "123", + user2: "456", + user3: "789", + requestor: "666" }; const clock = lolex.install(); @@ -25,6 +26,12 @@ describe("muzzle-utils", () => { beforeEach(() => { muzzled.clear(); muzzlers.clear(); + setUserList([ + { id: "123", name: "test123" }, + { id: "456", name: "test456" }, + { id: "789", name: "test789" }, + { id: "666", name: "requestor" } + ] as ISlackUser[]); }); afterEach(() => { @@ -38,21 +45,13 @@ describe("muzzle-utils", () => { describe("addUserToMuzzled()", () => { describe("muzzled", () => { it("should add a user to the muzzled map", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.size).to.equal(1); expect(muzzled.has(testData.user)).to.equal(true); }); it("should return an added user with IMuzzled attributes", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.get(testData.user)!.suppressionCount).to.equal(0); expect(muzzled.get(testData.user)!.muzzledBy).to.equal( testData.requestor @@ -60,33 +59,17 @@ describe("muzzle-utils", () => { }); it("should reject if a user tries to muzzle an already muzzled user", async () => { - await addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + await addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.has(testData.user)).to.equal(true); - await addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ).catch(e => { - expect(e).to.equal(`${testData.friendlyName} is already muzzled!`); + await addUserToMuzzled(testData.user, testData.requestor).catch(e => { + expect(e).to.equal("test123 is already muzzled!"); }); }); it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { - await addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + await addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.has(testData.user)).to.equal(true); - await addUserToMuzzled( - testData.requestor, - testData.friendlyName, - testData.user - ).catch(e => { + await addUserToMuzzled(testData.requestor, testData.user).catch(e => { expect(e).to.equal( `You can't muzzle someone if you are already muzzled!` ); @@ -96,57 +79,29 @@ describe("muzzle-utils", () => { describe("muzzlers", () => { it("should add a user to the muzzlers map", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzlers.size).to.equal(1); expect(muzzlers.has(testData.requestor)).to.equal(true); }); it("should return an added user with IMuzzler attributes", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(1); }); it("should increment a requestors muzzle count on a second addUserToMuzzled() call", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); - addUserToMuzzled( - testData.user2, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); + addUserToMuzzled(testData.user2, testData.requestor); expect(muzzled.size).to.equal(2); expect(muzzlers.has(testData.requestor)).to.equal(true); expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(2); }); it("should prevent a requestor from muzzling on their third count", async () => { - await addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); - await addUserToMuzzled( - testData.user2, - testData.friendlyName, - testData.requestor - ); - await addUserToMuzzled( - testData.user3, - testData.friendlyName, - testData.requestor - ).catch(e => + await addUserToMuzzled(testData.user, testData.requestor); + await addUserToMuzzled(testData.user2, testData.requestor); + await addUserToMuzzled(testData.user3, testData.requestor).catch(e => expect(e).to.equal( `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ) @@ -157,11 +112,7 @@ describe("muzzle-utils", () => { describe("removeMuzzle()", () => { it("should remove a user from the muzzled array", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.size).to.equal(1); expect(muzzled.has(testData.user)).to.equal(true); removeMuzzle(testData.user); @@ -172,11 +123,7 @@ describe("muzzle-utils", () => { describe("removeMuzzler()", () => { it("should remove a user from the muzzler array", () => { - addUserToMuzzled( - testData.user, - testData.friendlyName, - testData.requestor - ); + addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.size).to.equal(1); expect(muzzled.has(testData.user)).to.equal(true); expect(muzzlers.size).to.equal(1); @@ -215,7 +162,7 @@ describe("muzzle-utils", () => { it("should always muzzle ", () => { const testSentence = " hey guys"; - expect(muzzle(testSentence)).to.equal(" ..mMm.. ..mMm.. ..mMm.. "); + expect(muzzle(testSentence).includes("")).to.equal(false); }); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index bc11df3c..18f5517b 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -4,6 +4,7 @@ import { WebClient } from "@slack/web-api"; import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; +import { getUserName } from "../slack/slack-utils"; // Store for the muzzled users. export const muzzled: Map = new Map(); // Store for people who are muzzling others. @@ -14,7 +15,8 @@ const MAX_MUZZLE_TIME = 3600000; const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_MUZZLES = 2; -const web: WebClient = new WebClient(process.env.muzzleBotToken); +export const web: WebClient = new WebClient(process.env.muzzleBotToken); + /** * Takes in text and randomly muzzles certain words. */ @@ -38,76 +40,74 @@ export function containsAt(word: string): boolean { /** * Adds a user to the muzzled array and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ -export function addUserToMuzzled( - toMuzzle: string, - friendlyMuzzle: string, - requestor: string -) { +export function addUserToMuzzled(userId: string, requestorId: string) { + const userName = getUserName(userId); + const requestorName = getUserName(requestorId); return new Promise((resolve, reject) => { const timeToMuzzle = Math.floor( Math.random() * (180000 - 30000 + 1) + 30000 ); const minutes = Math.floor(timeToMuzzle / 60000); const seconds = ((timeToMuzzle % 60000) / 1000).toFixed(0); - if (muzzled.has(toMuzzle)) { + if (muzzled.has(userId)) { console.error( - `${requestor} attempted to muzzle ${toMuzzle} but ${toMuzzle} is already muzzled.` + `${requestorName} attempted to muzzle ${userName} but ${userName} is already muzzled.` ); - reject(`${friendlyMuzzle} is already muzzled!`); - } else if (muzzled.has(requestor)) { + reject(`${userName} is already muzzled!`); + } else if (muzzled.has(requestorName)) { console.error( - `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} is currently muzzled` + `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} is currently muzzled` ); reject(`You can't muzzle someone if you are already muzzled!`); } else if ( - muzzlers.has(requestor) && - muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES + muzzlers.has(requestorId) && + muzzlers.get(requestorId)!.muzzleCount === MAX_MUZZLES ) { console.error( - `User: ${requestor} attempted to muzzle ${toMuzzle} but failed because requestor: ${requestor} has reached maximum muzzle of ${MAX_MUZZLES}` + `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} has reached maximum muzzle of ${MAX_MUZZLES}` ); reject( `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ); } else { // Add a newly muzzled user. - muzzled.set(toMuzzle, { + muzzled.set(userId, { suppressionCount: 0, - muzzledBy: requestor + muzzledBy: requestorId }); - const muzzleCount = muzzlers.has(requestor) - ? ++muzzlers.get(requestor)!.muzzleCount + const muzzleCount = muzzlers.has(requestorId) + ? ++muzzlers.get(requestorId)!.muzzleCount : 1; // Add requestor to muzzlers - muzzlers.set(requestor, { + muzzlers.set(requestorId, { muzzleCount, muzzleCountRemover: setTimeout( - () => decrementMuzzleCount(requestor), + () => decrementMuzzleCount(requestorId), MAX_TIME_BETWEEN_MUZZLES ) }); if ( - muzzlers.has(requestor) && - muzzlers.get(requestor)!.muzzleCountRemover + muzzlers.has(requestorId) && + muzzlers.get(requestorId)!.muzzleCountRemover ) { - const currentTimer = muzzlers.get(requestor)!.muzzleCountRemover; + const currentTimer = muzzlers.get(requestorId)!.muzzleCountRemover; clearTimeout(currentTimer as NodeJS.Timeout); const removalFunction = - muzzlers.get(requestor)!.muzzleCount === MAX_MUZZLES - ? () => removeMuzzler(requestor) - : () => decrementMuzzleCount(requestor); - muzzlers.set(requestor, { - muzzleCount: muzzlers.get(requestor)!.muzzleCount, + muzzlers.get(requestorId)!.muzzleCount === MAX_MUZZLES + ? () => removeMuzzler(requestorId) + : () => decrementMuzzleCount(requestorId); + muzzlers.set(requestorId, { + muzzleCount: muzzlers.get(requestorId)!.muzzleCount, muzzleCountRemover: setTimeout(removalFunction, MAX_MUZZLE_TIME) }); } console.log( - `${friendlyMuzzle} is now muzzled for ${timeToMuzzle} milliseconds` + `${userName} is now muzzled for ${timeToMuzzle} milliseconds` ); - setTimeout(() => removeMuzzle(toMuzzle), timeToMuzzle); + setTimeout(() => removeMuzzle(userId), timeToMuzzle); resolve( - `Succesfully muzzled ${friendlyMuzzle} for ${ + `Succesfully muzzled ${userName} for ${ +seconds === 60 ? minutes + 1 + "m00s" : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s" @@ -117,19 +117,19 @@ export function addUserToMuzzled( }); } -export function decrementMuzzleCount(requestor: string) { - if (muzzlers.has(requestor)) { - const decrementedMuzzle = --muzzlers.get(requestor)!.muzzleCount; - muzzlers.set(requestor, { +export function decrementMuzzleCount(requestorId: string) { + if (muzzlers.has(requestorId)) { + const decrementedMuzzle = --muzzlers.get(requestorId)!.muzzleCount; + muzzlers.set(requestorId, { muzzleCount: decrementedMuzzle, - muzzleCountRemover: muzzlers.get(requestor)!.muzzleCountRemover + muzzleCountRemover: muzzlers.get(requestorId)!.muzzleCountRemover }); console.log( - `Successfully decremented ${requestor} muzzleCount to ${decrementedMuzzle}` + `Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}` ); } else { console.error( - `Attemped to decrement muzzle count for ${requestor} but they did not exist!` + `Attemped to decrement muzzle count for ${requestorId} but they did not exist!` ); } } diff --git a/src/utils/slack/slack-utils.spec.ts b/src/utils/slack/slack-utils.spec.ts index 50700a8c..85c1473e 100644 --- a/src/utils/slack/slack-utils.spec.ts +++ b/src/utils/slack/slack-utils.spec.ts @@ -1,16 +1,22 @@ import { expect } from "chai"; -import { getUserId, getUserName } from "./slack-utils"; +import { getUserId } from "./slack-utils"; -const mockSlackIdString = "<@U12345678|jrjrjr>"; describe("slack-utils", () => { - describe("getUserName()", () => { - it("should return the username from a slack formatted id string", () => { - expect(getUserName(mockSlackIdString)).to.equal("jrjrjr"); - }); - }); describe("getUserId()", () => { - it("should return the user id from a slack formatted id string", () => { - expect(getUserId(mockSlackIdString)).to.equal("U12345678"); + it("should return a userId when one is passed in without a username", () => { + expect(getUserId("<@U2TYNKJ>")).to.equal("U2TYNKJ"); + }); + + it("should return a userId when one is passed in with a username with spaces", () => { + expect(getUserId("<@U2TYNKJ | jrjrjr>")).to.equal("U2TYNKJ"); + }); + + it("should return a userId when one is passed in with a username without spaces", () => { + expect(getUserId("<@U2TYNKJ|jrjrjr>")).to.equal("U2TYNKJ"); + }); + + it("should return an empty string when no userId exists", () => { + expect(getUserId("total waste of time")).to.equal(""); }); }); }); diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts index 5051d014..84361868 100644 --- a/src/utils/slack/slack-utils.ts +++ b/src/utils/slack/slack-utils.ts @@ -1,5 +1,13 @@ import axios from "axios"; -import { IChannelResponse } from "../../shared/models/slack/slack-models"; +import { + IChannelResponse, + ISlackUser +} from "../../shared/models/slack/slack-models"; +import { web } from "../muzzle/muzzle-utils"; + +const userIdRegEx = /@\w+/gm; + +export let userList: ISlackUser[]; export function sendResponse( responseUrl: string, @@ -16,9 +24,36 @@ export function sendResponse( } export function getUserName(user: string): string { - return user.slice(user.indexOf("|") + 1, user.length - 1); + const userObj: ISlackUser | undefined = getUserById(user); + return userObj ? userObj.name : ""; } export function getUserId(user: string): string { - return user.slice(2, user.indexOf("|")); + const regArray = user.match(userIdRegEx); + return regArray ? regArray[0].slice(1) : ""; +} + +export function getUserById(userId: string) { + return userList.find((user: ISlackUser) => user.id === userId); +} + +/** + * TO BE USED EXCLUSIVELY FOR TESTING. WE SHOULD *NEVER* be setting the userList manually + * This should be handled by getAllUsers() only. + */ +export function setUserList(list: ISlackUser[]) { + userList = list; +} + +export function getAllUsers() { + web.users + .list() + .then(resp => { + userList = resp.members as ISlackUser[]; + }) + .catch(e => { + console.error("Failed to retrieve users", e); + console.error("Retrying in 5 seconds"); + setTimeout(() => getAllUsers(), 5000); + }); } From a40362c2bfe5e37935f20627a3f1d90871afeb75 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 17:37:43 -0400 Subject: [PATCH 009/167] Refactor/muzzle (#14) * Refactored muzzle-route and muzzle-utils * Changed muzzlers map to requestors for better clarity * Added support for @here --- src/routes/muzzle-route.ts | 35 ++--- src/utils/muzzle/muzzle-utils.spec.ts | 31 +++-- src/utils/muzzle/muzzle-utils.ts | 184 +++++++++++++++++--------- 3 files changed, 149 insertions(+), 101 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 91192c8e..319ba601 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -6,40 +6,25 @@ import { import { addUserToMuzzled, deleteMessage, - muzzle, - muzzled, - sendMessage + isUserMuzzled, + sendMuzzledMessage, + shouldBotMessageBeMuzzled } from "../utils/muzzle/muzzle-utils"; import { getUserId } from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); -const MAX_SUPPRESSIONS: number = 7; muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; - console.log(request); - if (muzzled.has(request.event.user)) { + if (isUserMuzzled(request.event.user)) { console.log(`${request.event.user} is muzzled! Suppressing his voice...`); deleteMessage(request.event.channel, request.event.ts); - - if (muzzled.get(request.event.user)!.suppressionCount < MAX_SUPPRESSIONS) { - muzzled.set(request.event.user, { - suppressionCount: ++muzzled.get(request.event.user)!.suppressionCount, - muzzledBy: muzzled.get(request.event.user)!.muzzledBy - }); - sendMessage( - request.event.channel, - `<@${request.event.user}> says "${muzzle(request.event.text)}"` - ); - } - } else if ( - request.event.subtype === "bot_message" && - request.event.attachments && - muzzled.has( - getUserId(request.event.attachments[0].text || request.event.text) - ) && - request.event.username !== "muzzle" - ) { + sendMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text + ); + } else if (shouldBotMessageBeMuzzled(request)) { console.log( `${ request.authed_users[0] diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index de468f01..572e92ab 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -5,12 +5,11 @@ import { setUserList } from "../slack/slack-utils"; import { addUserToMuzzled, containsAt, - MAX_MUZZLES, muzzle, muzzled, - muzzlers, removeMuzzle, - removeMuzzler + removeMuzzler, + requestors } from "./muzzle-utils"; describe("muzzle-utils", () => { @@ -25,7 +24,7 @@ describe("muzzle-utils", () => { beforeEach(() => { muzzled.clear(); - muzzlers.clear(); + requestors.clear(); setUserList([ { id: "123", name: "test123" }, { id: "456", name: "test456" }, @@ -77,25 +76,25 @@ describe("muzzle-utils", () => { }); }); - describe("muzzlers", () => { - it("should add a user to the muzzlers map", () => { + describe("requestors", () => { + it("should add a user to the requestors map", () => { addUserToMuzzled(testData.user, testData.requestor); - expect(muzzlers.size).to.equal(1); - expect(muzzlers.has(testData.requestor)).to.equal(true); + expect(requestors.size).to.equal(1); + expect(requestors.has(testData.requestor)).to.equal(true); }); it("should return an added user with IMuzzler attributes", () => { addUserToMuzzled(testData.user, testData.requestor); - expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(1); + expect(requestors.get(testData.requestor)!.muzzleCount).to.equal(1); }); it("should increment a requestors muzzle count on a second addUserToMuzzled() call", () => { addUserToMuzzled(testData.user, testData.requestor); addUserToMuzzled(testData.user2, testData.requestor); expect(muzzled.size).to.equal(2); - expect(muzzlers.has(testData.requestor)).to.equal(true); - expect(muzzlers.get(testData.requestor)!.muzzleCount).to.equal(2); + expect(requestors.has(testData.requestor)).to.equal(true); + expect(requestors.get(testData.requestor)!.muzzleCount).to.equal(2); }); it("should prevent a requestor from muzzling on their third count", async () => { @@ -103,7 +102,7 @@ describe("muzzle-utils", () => { await addUserToMuzzled(testData.user2, testData.requestor); await addUserToMuzzled(testData.user3, testData.requestor).catch(e => expect(e).to.equal( - `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + `You're doing that too much. Only 2 muzzles are allowed per hour.` ) ); }); @@ -126,11 +125,11 @@ describe("muzzle-utils", () => { addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.size).to.equal(1); expect(muzzled.has(testData.user)).to.equal(true); - expect(muzzlers.size).to.equal(1); - expect(muzzlers.has(testData.requestor)).to.equal(true); + expect(requestors.size).to.equal(1); + expect(requestors.has(testData.requestor)).to.equal(true); removeMuzzler(testData.requestor); - expect(muzzlers.has(testData.requestor)).to.equal(false); - expect(muzzlers.size).to.equal(0); + expect(requestors.has(testData.requestor)).to.equal(false); + expect(requestors.size).to.equal(0); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 18f5517b..820c04d6 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -4,16 +4,18 @@ import { WebClient } from "@slack/web-api"; import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; -import { getUserName } from "../slack/slack-utils"; +import { IEventRequest } from "../../shared/models/slack/slack-models"; +import { getUserId, getUserName } from "../slack/slack-utils"; // Store for the muzzled users. export const muzzled: Map = new Map(); // Store for people who are muzzling others. -export const muzzlers: Map = new Map(); +export const requestors: Map = new Map(); // Time period in which a user must wait before making more muzzles. const MAX_MUZZLE_TIME = 3600000; const MAX_TIME_BETWEEN_MUZZLES = 3600000; -export const MAX_MUZZLES = 2; +const MAX_SUPPRESSIONS: number = 7; +const MAX_MUZZLES = 2; export const web: WebClient = new WebClient(process.env.muzzleBotToken); @@ -31,10 +33,100 @@ export function muzzle(text: string) { } /** - * Determines whether or not a user is trying to @ someone while muzzled or @ channel. + * Determines whether or not a user is trying to @user, @channel or @here while muzzled. */ export function containsAt(word: string): boolean { - return word.includes("@") || word.includes(""); + return ( + word.includes("@") || + word.includes("") || + word.includes("") + ); +} + +/** + * Gives us a random value between 30 seconds and 3 minutes. + */ +export function getTimeToMuzzle() { + return Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); +} + +/** + * Gives us a time string formatted as 1m20s to show the user. + */ +export function getTimeString(time: number) { + const minutes = Math.floor(time / 60000); + const seconds = ((time % 60000) / 1000).toFixed(0); + return +seconds === 60 + ? minutes + 1 + "m00s" + : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s"; +} + +/** + * Returns boolean whether max muzzles have been reached. + */ +function isMaxMuzzlesReached(userId: string) { + return ( + requestors.has(userId) && + requestors.get(userId)!.muzzleCount === MAX_MUZZLES + ); +} + +/** + * Returns boolean whether user is muzzled or not. + */ +export function isUserMuzzled(userId: string) { + return muzzled.has(userId); +} + +/** + * Determines whether or not a bot message should be removed. + */ +export function shouldBotMessageBeMuzzled(request: IEventRequest) { + return ( + request.event.subtype === "bot_message" && + request.event.attachments && + isUserMuzzled( + getUserId(request.event.attachments[0].text || request.event.text) + ) && + request.event.username !== "muzzle" + ); +} + +/** + * Adds a requestor to the requestors array with a muzzleCount to track how many muzzles have been performed, as well as a removal funciton. + */ +function setMuzzlerCount(requestorId: string) { + const muzzleCount = requestors.has(requestorId) + ? ++requestors.get(requestorId)!.muzzleCount + : 1; + + if (requestors.has(requestorId)) { + clearTimeout(requestors.get(requestorId)! + .muzzleCountRemover as NodeJS.Timeout); + } + + const removalFunction = + requestors.has(requestorId) && + requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES + ? () => removeMuzzler(requestorId) + : () => decrementMuzzleCount(requestorId); + requestors.set(requestorId, { + muzzleCount, + muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES) + }); +} + +/** + * Adds a userId to the muzzled array, adds the requestorId to the requestorsArray, sets timeout for removeMuzzler. + */ +function muzzleUser(userId: string, requestorId: string, timeToMuzzle: number) { + muzzled.set(userId, { + suppressionCount: 0, + muzzledBy: requestorId + }); + + setMuzzlerCount(requestorId); + setTimeout(() => removeMuzzle(userId), timeToMuzzle); } /** @@ -44,25 +136,17 @@ export function addUserToMuzzled(userId: string, requestorId: string) { const userName = getUserName(userId); const requestorName = getUserName(requestorId); return new Promise((resolve, reject) => { - const timeToMuzzle = Math.floor( - Math.random() * (180000 - 30000 + 1) + 30000 - ); - const minutes = Math.floor(timeToMuzzle / 60000); - const seconds = ((timeToMuzzle % 60000) / 1000).toFixed(0); - if (muzzled.has(userId)) { + if (isUserMuzzled(userId)) { console.error( `${requestorName} attempted to muzzle ${userName} but ${userName} is already muzzled.` ); reject(`${userName} is already muzzled!`); - } else if (muzzled.has(requestorName)) { + } else if (isUserMuzzled(requestorId)) { console.error( `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} is currently muzzled` ); reject(`You can't muzzle someone if you are already muzzled!`); - } else if ( - muzzlers.has(requestorId) && - muzzlers.get(requestorId)!.muzzleCount === MAX_MUZZLES - ) { + } else if (isMaxMuzzlesReached(requestorId)) { console.error( `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} has reached maximum muzzle of ${MAX_MUZZLES}` ); @@ -70,59 +154,24 @@ export function addUserToMuzzled(userId: string, requestorId: string) { `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ); } else { - // Add a newly muzzled user. - muzzled.set(userId, { - suppressionCount: 0, - muzzledBy: requestorId - }); - const muzzleCount = muzzlers.has(requestorId) - ? ++muzzlers.get(requestorId)!.muzzleCount - : 1; - // Add requestor to muzzlers - muzzlers.set(requestorId, { - muzzleCount, - muzzleCountRemover: setTimeout( - () => decrementMuzzleCount(requestorId), - MAX_TIME_BETWEEN_MUZZLES - ) - }); - - if ( - muzzlers.has(requestorId) && - muzzlers.get(requestorId)!.muzzleCountRemover - ) { - const currentTimer = muzzlers.get(requestorId)!.muzzleCountRemover; - clearTimeout(currentTimer as NodeJS.Timeout); - const removalFunction = - muzzlers.get(requestorId)!.muzzleCount === MAX_MUZZLES - ? () => removeMuzzler(requestorId) - : () => decrementMuzzleCount(requestorId); - muzzlers.set(requestorId, { - muzzleCount: muzzlers.get(requestorId)!.muzzleCount, - muzzleCountRemover: setTimeout(removalFunction, MAX_MUZZLE_TIME) - }); - } + const timeToMuzzle = getTimeToMuzzle(); + muzzleUser(userId, requestorId, timeToMuzzle); console.log( `${userName} is now muzzled for ${timeToMuzzle} milliseconds` ); - setTimeout(() => removeMuzzle(userId), timeToMuzzle); resolve( - `Succesfully muzzled ${userName} for ${ - +seconds === 60 - ? minutes + 1 + "m00s" - : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s" - } minutes` + `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` ); } }); } export function decrementMuzzleCount(requestorId: string) { - if (muzzlers.has(requestorId)) { - const decrementedMuzzle = --muzzlers.get(requestorId)!.muzzleCount; - muzzlers.set(requestorId, { + if (requestors.has(requestorId)) { + const decrementedMuzzle = --requestors.get(requestorId)!.muzzleCount; + requestors.set(requestorId, { muzzleCount: decrementedMuzzle, - muzzleCountRemover: muzzlers.get(requestorId)!.muzzleCountRemover + muzzleCountRemover: requestors.get(requestorId)!.muzzleCountRemover }); console.log( `Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}` @@ -135,9 +184,9 @@ export function decrementMuzzleCount(requestorId: string) { } export function removeMuzzler(user: string) { - muzzlers.delete(user); + requestors.delete(user); console.log( - `${MAX_MUZZLE_TIME} has passed since ${user} last successful muzzle. They have been removed from muzzlers.` + `${MAX_MUZZLE_TIME} has passed since ${user} last successful muzzle. They have been removed from requestors.` ); } @@ -149,6 +198,21 @@ export function removeMuzzle(user: string) { export function isRandomEven() { return Math.floor(Math.random() * 2) % 2 === 0; } + +export function sendMuzzledMessage( + channel: string, + user: string, + text: string +) { + if (muzzled.get(user)!.suppressionCount < MAX_SUPPRESSIONS) { + muzzled.set(user, { + suppressionCount: ++muzzled.get(user)!.suppressionCount, + muzzledBy: muzzled.get(user)!.muzzledBy + }); + sendMessage(channel, `<@${user}> says "${muzzle(text)}"`); + } +} + /** * Handles deletion of messages. */ From 512bd1cd1447398bc353a6a9a917bb9dbb565108 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 17:51:29 -0400 Subject: [PATCH 010/167] Added better logging (#15) --- src/routes/muzzle-route.ts | 16 +++++++---- src/utils/muzzle/muzzle-utils.ts | 48 +++++++++++++++++++------------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 319ba601..7a08ffb2 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -10,14 +10,18 @@ import { sendMuzzledMessage, shouldBotMessageBeMuzzled } from "../utils/muzzle/muzzle-utils"; -import { getUserId } from "../utils/slack/slack-utils"; +import { getUserId, getUserName } from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; if (isUserMuzzled(request.event.user)) { - console.log(`${request.event.user} is muzzled! Suppressing his voice...`); + console.log( + `${getUserName(request.event.user)} | ${ + request.event.user + } is muzzled! Suppressing his voice...` + ); deleteMessage(request.event.channel, request.event.ts); sendMuzzledMessage( request.event.channel, @@ -26,9 +30,11 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { ); } else if (shouldBotMessageBeMuzzled(request)) { console.log( - `${ - request.authed_users[0] - } is muzzled and tried to send a bot message! Suppressing...` + `${getUserName( + request.event.text || request.event.attachments[0].text + )} | ${request.event.text || + request.event.attachments[0] + .text} is muzzled and tried to send a bot message! Suppressing...` ); deleteMessage(request.event.channel, request.event.ts); } diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 820c04d6..8c772b54 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -138,17 +138,17 @@ export function addUserToMuzzled(userId: string, requestorId: string) { return new Promise((resolve, reject) => { if (isUserMuzzled(userId)) { console.error( - `${requestorName} attempted to muzzle ${userName} but ${userName} is already muzzled.` + `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` ); reject(`${userName} is already muzzled!`); } else if (isUserMuzzled(requestorId)) { console.error( - `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} is currently muzzled` + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled` ); reject(`You can't muzzle someone if you are already muzzled!`); } else if (isMaxMuzzlesReached(requestorId)) { console.error( - `User: ${requestorName} attempted to muzzle ${userName} but failed because requestor: ${requestorName} has reached maximum muzzle of ${MAX_MUZZLES}` + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${MAX_MUZZLES}` ); reject( `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` @@ -157,10 +157,12 @@ export function addUserToMuzzled(userId: string, requestorId: string) { const timeToMuzzle = getTimeToMuzzle(); muzzleUser(userId, requestorId, timeToMuzzle); console.log( - `${userName} is now muzzled for ${timeToMuzzle} milliseconds` + `${userName} | ${userId} is now muzzled for ${timeToMuzzle} milliseconds` ); resolve( - `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` + `Succesfully muzzled ${userName} | ${userId} for ${getTimeString( + timeToMuzzle + )}` ); } }); @@ -174,25 +176,33 @@ export function decrementMuzzleCount(requestorId: string) { muzzleCountRemover: requestors.get(requestorId)!.muzzleCountRemover }); console.log( - `Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}` + `Successfully decremented ${getUserName( + requestorId + )} | ${requestorId} muzzleCount to ${decrementedMuzzle}` ); } else { console.error( - `Attemped to decrement muzzle count for ${requestorId} but they did not exist!` + `Attemped to decrement muzzle count for ${getUserName( + requestorId + )} | ${requestorId} but they did not exist!` ); } } -export function removeMuzzler(user: string) { - requestors.delete(user); +export function removeMuzzler(userId: string) { + requestors.delete(userId); console.log( - `${MAX_MUZZLE_TIME} has passed since ${user} last successful muzzle. They have been removed from requestors.` + `${MAX_MUZZLE_TIME} has passed since ${getUserName( + userId + )} | ${userId} last successful muzzle. They have been removed from requestors.` ); } -export function removeMuzzle(user: string) { - muzzled.delete(user); - console.log(`Removed ${user}'s muzzle! He is free at last.`); +export function removeMuzzle(userId: string) { + muzzled.delete(userId); + console.log( + `Removed ${getUserName(userId)} | ${userId}'s muzzle! He is free at last.` + ); } export function isRandomEven() { @@ -201,15 +211,15 @@ export function isRandomEven() { export function sendMuzzledMessage( channel: string, - user: string, + userId: string, text: string ) { - if (muzzled.get(user)!.suppressionCount < MAX_SUPPRESSIONS) { - muzzled.set(user, { - suppressionCount: ++muzzled.get(user)!.suppressionCount, - muzzledBy: muzzled.get(user)!.muzzledBy + if (muzzled.get(userId)!.suppressionCount < MAX_SUPPRESSIONS) { + muzzled.set(userId, { + suppressionCount: ++muzzled.get(userId)!.suppressionCount, + muzzledBy: muzzled.get(userId)!.muzzledBy }); - sendMessage(channel, `<@${user}> says "${muzzle(text)}"`); + sendMessage(channel, `<@${userId}> says "${muzzle(text)}"`); } } From 5ad660155202520dfad4ae9b507486905fcd8971 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 29 Jun 2019 17:54:08 -0400 Subject: [PATCH 011/167] Removed id from response (#16) --- src/utils/muzzle/muzzle-utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 8c772b54..390169a9 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -160,9 +160,7 @@ export function addUserToMuzzled(userId: string, requestorId: string) { `${userName} | ${userId} is now muzzled for ${timeToMuzzle} milliseconds` ); resolve( - `Succesfully muzzled ${userName} | ${userId} for ${getTimeString( - timeToMuzzle - )}` + `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` ); } }); From 7a6fd5c0f8071ed8061a9ea5b8d79fc015e18e48 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 30 Jun 2019 13:32:19 -0400 Subject: [PATCH 012/167] Feature/increase muzzle when channel (#18) * Added removalFn as a property to muzzled users to allow tracking and moved setMuzzlerCount outside of muzzleUser * Adjusted timeout * Added containsAt and addMuzzleTime functions * Exported ABUSE_PENALTY_TIME * Adjusted getRemainingTime function * Added addMuzzleTime interaction with muzzled map * Added a setTimout to alerting channel about user tags * Adjusted wording of alert * Added siren emoji * Adjusted shortcircuit on shouldBotMessageBeMuzzled * Added logging on addMuzzleTime * Adjusted getUserId * Fixed getRemainingTime to return ms not s * Added logging for getRemainingTime * Added 1000x multiplier on process.uptime() for ms * Removed logging for getRemainingTime and changed emoji to rotating_light --- src/routes/muzzle-route.ts | 25 ++++++++++++++++ src/shared/models/muzzle/muzzle-models.ts | 1 + src/utils/muzzle/muzzle-utils.ts | 35 ++++++++++++++++++----- src/utils/slack/slack-utils.ts | 3 ++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 7a08ffb2..d390823d 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -4,9 +4,14 @@ import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; import { + ABUSE_PENALTY_TIME, + addMuzzleTime, addUserToMuzzled, + containsAt, deleteMessage, + getTimeString, isUserMuzzled, + sendMessage, sendMuzzledMessage, shouldBotMessageBeMuzzled } from "../utils/muzzle/muzzle-utils"; @@ -28,6 +33,26 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user, request.event.text ); + if (containsAt(request.event.text)) { + console.log( + `${getUserName( + request.event.user + )} atttempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + addMuzzleTime(request.event.user); + setTimeout( + () => + sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to tag someone, or the channel while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ), + 1000 + ); + } } else if (shouldBotMessageBeMuzzled(request)) { console.log( `${getUserName( diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 07fac71c..5de384f5 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -1,6 +1,7 @@ export interface IMuzzled { suppressionCount: number; muzzledBy: string; + removalFn: NodeJS.Timeout; } export interface IMuzzler { diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 390169a9..1cb8969b 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -14,6 +14,7 @@ export const requestors: Map = new Map(); // Time period in which a user must wait before making more muzzles. const MAX_MUZZLE_TIME = 3600000; const MAX_TIME_BETWEEN_MUZZLES = 3600000; +export const ABUSE_PENALTY_TIME = 300000; const MAX_SUPPRESSIONS: number = 7; const MAX_MUZZLES = 2; @@ -32,6 +33,26 @@ export function muzzle(text: string) { return returnText; } +export function addMuzzleTime(userId: string) { + if (userId && muzzled.has(userId)) { + const removalFn = muzzled.get(userId)!.removalFn; + const newTime = getRemainingTime(removalFn) + ABUSE_PENALTY_TIME; + clearTimeout(muzzled.get(userId)!.removalFn); + console.log(`Setting ${getUserName(userId)}'s muzzle time to ${newTime}`); + muzzled.set(userId, { + suppressionCount: muzzled.get(userId)!.suppressionCount, + muzzledBy: muzzled.get(userId)!.muzzledBy, + removalFn: setTimeout(() => removeMuzzle(userId), newTime) + }); + } +} + +function getRemainingTime(timeout: any) { + return Math.ceil( + timeout._idleStart + timeout._idleTimeout - process.uptime() * 1000 + ); +} + /** * Determines whether or not a user is trying to @user, @channel or @here while muzzled. */ @@ -86,7 +107,7 @@ export function shouldBotMessageBeMuzzled(request: IEventRequest) { request.event.subtype === "bot_message" && request.event.attachments && isUserMuzzled( - getUserId(request.event.attachments[0].text || request.event.text) + getUserId(request.event.text || request.event.attachments[0].text) ) && request.event.username !== "muzzle" ); @@ -117,16 +138,14 @@ function setMuzzlerCount(requestorId: string) { } /** - * Adds a userId to the muzzled array, adds the requestorId to the requestorsArray, sets timeout for removeMuzzler. + * Adds a userId to the muzzled array, and sets timeout for removeMuzzle. */ function muzzleUser(userId: string, requestorId: string, timeToMuzzle: number) { muzzled.set(userId, { suppressionCount: 0, - muzzledBy: requestorId + muzzledBy: requestorId, + removalFn: setTimeout(() => removeMuzzle(userId), timeToMuzzle) }); - - setMuzzlerCount(requestorId); - setTimeout(() => removeMuzzle(userId), timeToMuzzle); } /** @@ -156,6 +175,7 @@ export function addUserToMuzzled(userId: string, requestorId: string) { } else { const timeToMuzzle = getTimeToMuzzle(); muzzleUser(userId, requestorId, timeToMuzzle); + setMuzzlerCount(requestorId); console.log( `${userName} | ${userId} is now muzzled for ${timeToMuzzle} milliseconds` ); @@ -215,7 +235,8 @@ export function sendMuzzledMessage( if (muzzled.get(userId)!.suppressionCount < MAX_SUPPRESSIONS) { muzzled.set(userId, { suppressionCount: ++muzzled.get(userId)!.suppressionCount, - muzzledBy: muzzled.get(userId)!.muzzledBy + muzzledBy: muzzled.get(userId)!.muzzledBy, + removalFn: muzzled.get(userId)!.removalFn }); sendMessage(channel, `<@${userId}> says "${muzzle(text)}"`); } diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts index 84361868..b2d7427f 100644 --- a/src/utils/slack/slack-utils.ts +++ b/src/utils/slack/slack-utils.ts @@ -29,6 +29,9 @@ export function getUserName(user: string): string { } export function getUserId(user: string): string { + if (!user) { + return ""; + } const regArray = user.match(userIdRegEx); return regArray ? regArray[0].slice(1) : ""; } From 2679537951346b9574ff5c4884d35315274c80a2 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 30 Jun 2019 15:28:28 -0400 Subject: [PATCH 013/167] Block all Bots from Muzzled Users (#19) * Added better handling of userId inside of shouldBotMessageBeMuzzled * Added ternary for userIdByAttachment * Added logging * Added some pretty terrible logic to shouldBotMessageBeMuzzled as PoC * Changed userId to any termporarily * Added better check on attachments * Added logging to handle route * More maddening logic for handling spoilers * Added callback ID logging * Adjusted logic for userIdByCallbackId * Adjusted index for callback id * Removed excessive logging and moved spoiler logic out --- src/routes/muzzle-route.ts | 8 ++--- src/shared/models/slack/slack-models.ts | 2 ++ src/utils/muzzle/muzzle-utils.ts | 45 ++++++++++++++++++++++--- src/utils/slack/slack-utils.ts | 11 ++++-- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index d390823d..954ceeb3 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -55,11 +55,7 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { } } else if (shouldBotMessageBeMuzzled(request)) { console.log( - `${getUserName( - request.event.text || request.event.attachments[0].text - )} | ${request.event.text || - request.event.attachments[0] - .text} is muzzled and tried to send a bot message! Suppressing...` + `A user is muzzled and tried to send a bot message! Suppressing...` ); deleteMessage(request.event.channel, request.event.ts); } @@ -68,7 +64,7 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { muzzleRoutes.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - const userId: string = getUserId(request.text); + const userId: any = getUserId(request.text); const results = await addUserToMuzzled(userId, request.user_id).catch(e => res.send(e) ); diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index 6fbc0809..2bec1c64 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -35,6 +35,8 @@ export interface IEvent { channel_type: string; authed_users: string[]; attachments: IEvent[]; + pretext: string; + callback_id: string; } export interface IAttachment { diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 1cb8969b..4d3ef4fc 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -5,7 +5,11 @@ import { } from "@slack/web-api"; import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; -import { getUserId, getUserName } from "../slack/slack-utils"; +import { + getUserId, + getUserIdByCallbackId, + getUserName +} from "../slack/slack-utils"; // Store for the muzzled users. export const muzzled: Map = new Map(); // Store for people who are muzzling others. @@ -99,16 +103,49 @@ export function isUserMuzzled(userId: string) { return muzzled.has(userId); } +function getBotId( + fromText: string | undefined, + fromAttachmentText: string | undefined, + fromPretext: string | undefined, + fromCallbackId: string | undefined +) { + return fromText || fromAttachmentText || fromPretext || fromCallbackId; +} + /** * Determines whether or not a bot message should be removed. */ export function shouldBotMessageBeMuzzled(request: IEventRequest) { + let userIdByEventText; + let userIdByAttachmentText; + let userIdByAttachmentPretext; + let userIdByCallbackId; + + if (request.event.text) { + userIdByEventText = getUserId(request.event.text); + } else if (request.event.attachments && request.event.attachments.length) { + userIdByAttachmentText = getUserId(request.event.attachments[0].text); + userIdByAttachmentPretext = getUserId(request.event.attachments[0].pretext); + + if (request.event.attachments[0].callback_id) { + userIdByCallbackId = getUserIdByCallbackId( + request.event.attachments[0].callback_id + ); + } + } + + const finalUserId = getBotId( + userIdByEventText, + userIdByAttachmentText, + userIdByAttachmentPretext, + userIdByCallbackId + ); + return ( request.event.subtype === "bot_message" && request.event.attachments && - isUserMuzzled( - getUserId(request.event.text || request.event.attachments[0].text) - ) && + finalUserId && + isUserMuzzled(finalUserId) && request.event.username !== "muzzle" ); } diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts index b2d7427f..8153f602 100644 --- a/src/utils/slack/slack-utils.ts +++ b/src/utils/slack/slack-utils.ts @@ -28,18 +28,23 @@ export function getUserName(user: string): string { return userObj ? userObj.name : ""; } -export function getUserId(user: string): string { +export function getUserId(user: string) { if (!user) { - return ""; + return undefined; } const regArray = user.match(userIdRegEx); - return regArray ? regArray[0].slice(1) : ""; + return regArray ? regArray[0].slice(1) : undefined; } export function getUserById(userId: string) { return userList.find((user: ISlackUser) => user.id === userId); } +// This will really only work for SpoilerBot since it stores userId here and nowhere else. +export function getUserIdByCallbackId(callbackId: string) { + return callbackId.slice(callbackId.indexOf("_") + 1, callbackId.length); +} + /** * TO BE USED EXCLUSIVELY FOR TESTING. WE SHOULD *NEVER* be setting the userList manually * This should be handled by getAllUsers() only. From 2bf4effae1f2dd23939224b2bea05630d0e33bf2 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 1 Jul 2019 22:34:21 -0400 Subject: [PATCH 014/167] Added fix for channel tags (#20) --- src/utils/muzzle/muzzle-utils.spec.ts | 28 ++++++++++++++++++--------- src/utils/muzzle/muzzle-utils.ts | 10 +++------- src/utils/slack/slack-utils.spec.ts | 2 +- src/utils/slack/slack-utils.ts | 6 +++--- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index 572e92ab..bda51cc0 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -4,7 +4,7 @@ import { ISlackUser } from "../../shared/models/slack/slack-models"; import { setUserList } from "../slack/slack-utils"; import { addUserToMuzzled, - containsAt, + containsTag, muzzle, muzzled, removeMuzzle, @@ -133,27 +133,37 @@ describe("muzzle-utils", () => { }); }); - describe("containsAt()", () => { - it("should return true if a word has @ in it", () => { - const testWord = "@channel"; - expect(containsAt(testWord)).to.equal(true); + describe("containsTag()", () => { + it("should return false if a word has @ in it and is not a tag", () => { + const testWord = ".@channel"; + expect(containsTag(testWord)).to.equal(false); }); it("should return false if a word does not include @", () => { const testWord = "test"; - expect(containsAt(testWord)).to.equal(false); + expect(containsTag(testWord)).to.equal(false); }); it("should return true if a word has in it", () => { const testWord = ""; - expect(containsAt(testWord)).to.equal(true); + expect(containsTag(testWord)).to.equal(true); + }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(containsTag(testWord)).to.equal(true); + }); + + it("should return true if a word has a tagged user", () => { + const testUser = "<@UTJFJKL>"; + expect(containsTag(testUser)).to.equal(true); }); }); describe("muzzle()", () => { - it("should always muzzle @", () => { + it("should always muzzle a tagged user", () => { const testSentence = - "@channel @channel @channel @channel @channel @channel @channel @jrjrjr @fudka"; + "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; expect(muzzle(testSentence)).to.equal( " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " ); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index 4d3ef4fc..ed52f124 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -32,7 +32,7 @@ export function muzzle(text: string) { const words = text.split(" "); for (const word of words) { returnText += - isRandomEven() && !containsAt(word) ? ` *${word}* ` : " ..mMm.. "; + isRandomEven() && !containsTag(word) ? ` *${word}* ` : " ..mMm.. "; } return returnText; } @@ -60,12 +60,8 @@ function getRemainingTime(timeout: any) { /** * Determines whether or not a user is trying to @user, @channel or @here while muzzled. */ -export function containsAt(word: string): boolean { - return ( - word.includes("@") || - word.includes("") || - word.includes("") - ); +export function containsTag(word: string): boolean { + return !!getUserId(word) || word === "" || word === ""; } /** diff --git a/src/utils/slack/slack-utils.spec.ts b/src/utils/slack/slack-utils.spec.ts index 85c1473e..bb48e79f 100644 --- a/src/utils/slack/slack-utils.spec.ts +++ b/src/utils/slack/slack-utils.spec.ts @@ -15,7 +15,7 @@ describe("slack-utils", () => { expect(getUserId("<@U2TYNKJ|jrjrjr>")).to.equal("U2TYNKJ"); }); - it("should return an empty string when no userId exists", () => { + it("should return '' when no userId exists", () => { expect(getUserId("total waste of time")).to.equal(""); }); }); diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts index 8153f602..036f7085 100644 --- a/src/utils/slack/slack-utils.ts +++ b/src/utils/slack/slack-utils.ts @@ -5,7 +5,7 @@ import { } from "../../shared/models/slack/slack-models"; import { web } from "../muzzle/muzzle-utils"; -const userIdRegEx = /@\w+/gm; +const userIdRegEx = /[<]@\w+/gm; export let userList: ISlackUser[]; @@ -30,10 +30,10 @@ export function getUserName(user: string): string { export function getUserId(user: string) { if (!user) { - return undefined; + return ""; } const regArray = user.match(userIdRegEx); - return regArray ? regArray[0].slice(1) : undefined; + return regArray ? regArray[0].slice(2) : ""; } export function getUserById(userId: string) { From 5cfcd506aeed0890c649679c35eb0cebb39a7cb2 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 1 Jul 2019 22:36:53 -0400 Subject: [PATCH 015/167] Added fix for containsTag import (#21) --- src/routes/muzzle-route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 954ceeb3..8b0735c9 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -7,7 +7,7 @@ import { ABUSE_PENALTY_TIME, addMuzzleTime, addUserToMuzzled, - containsAt, + containsTag, deleteMessage, getTimeString, isUserMuzzled, @@ -33,7 +33,7 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user, request.event.text ); - if (containsAt(request.event.text)) { + if (containsTag(request.event.text)) { console.log( `${getUserName( request.event.user From f80f992eacaa7fca10929e81fde896bb6811d70b Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 5 Jul 2019 13:55:11 -0400 Subject: [PATCH 016/167] Feature/add data (#22) * Added typeorm, mysql driver, and tsconfig options to make use of typeorm * Renamed server.ts to index.ts and adjusted package.json * Added User and Muzzle models * Added relationship information to to USer and Muzzle classes * Added some base functions for interaction with mysql db via typeorm * Added logging * logged results * Changed addUserToMuzzled to be async * Added resolve/reject to inner then of addMuzzleTransaction * Adjusted addMuzzleToTransaction call in muzzle-utils * Reverted to try/catch * Changed date type to play nice with mysql datetime * Added date property on insert * Removed User since it is not necessary * Added log on db insert * Changed a failed DB add to resolve with a warning rather than reject * Added default date value and changed time to muzzleLengthMs * Changed to milliseconds * Added createdAt and updatedAt fields * Added suppression counters to the db * Added id to updateSuppressions as well as logging * Separated updateSuppressions into two separate increment functions * Removed old updateSuppressions import * Added tracking for deleted messages after a user reaches their max muzzles * Added support for incrementing muzzletime * Adjusted order of operations on containsTag * Added logging to containsTag * Changed contains tag to check an entire sentence not just a single word * Changed verbiage and handling of tags while muzzled to delete messages and not muzzle * Changed the muzzle to fail on issues with DB * Removed get functions from muzzle-actions. These will be addressed independent of saving data * Removed async on sendMuzzledMessage since it is no longer necessary * Converted from ormconfig.json to ormconfig.ts, added logging and updated gitignore * Converted from transactionId to muzzleId * Removed try catch in favor of catch * Replaced addMuzzleTransaction with addMuzzleToDb * Better naming * Added change to make addMuzzleTime more generic --- .gitignore | 3 +- package-lock.json | 414 ++++++++++++++-------- package.json | 7 +- src/db/Muzzle/actions/muzzle-actions.ts | 44 +++ src/db/Muzzle/models/Muzzle.ts | 28 ++ src/{server.ts => index.ts} | 14 +- src/routes/muzzle-route.ts | 53 +-- src/shared/models/muzzle/muzzle-models.ts | 1 + src/utils/muzzle/muzzle-utils.spec.ts | 10 + src/utils/muzzle/muzzle-utils.ts | 103 ++++-- tsconfig.json | 6 +- 11 files changed, 493 insertions(+), 190 deletions(-) create mode 100644 src/db/Muzzle/actions/muzzle-actions.ts create mode 100644 src/db/Muzzle/models/Muzzle.ts rename src/{server.ts => index.ts} (61%) diff --git a/.gitignore b/.gitignore index b2d59d1f..96b2ba20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules -/dist \ No newline at end of file +/dist +/src/ormconfig.ts \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 951fd1e2..de03b84f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -93,6 +94,7 @@ "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, "requires": { "@types/node": "*" } @@ -101,6 +103,7 @@ "version": "4.16.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -111,6 +114,7 @@ "version": "4.16.4", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.4.tgz", "integrity": "sha512-x/8h6FHm14rPWnW2HP5likD/rsqJ3t/77OWx2PLxym0hXbeBWQmcPyHmwX+CtCQpjIfgrUdEoDFcLPwPZWiqzQ==", + "dev": true, "requires": { "@types/node": "*", "@types/range-parser": "*" @@ -141,7 +145,8 @@ "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true }, "@types/mocha": { "version": "5.2.6", @@ -168,7 +173,8 @@ "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true }, "@types/retry": { "version": "0.12.0", @@ -179,6 +185,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, "requires": { "@types/express-serve-static-core": "*", "@types/mime": "*" @@ -223,14 +230,12 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -241,6 +246,11 @@ "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", "dev": true }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -262,6 +272,11 @@ } } }, + "app-root-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", + "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==" + }, "arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -272,7 +287,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -375,8 +389,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -433,6 +446,16 @@ } } }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -475,7 +498,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -516,6 +538,15 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -604,7 +635,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -681,6 +711,18 @@ "restore-cursor": "^2.0.0" } }, + "cli-highlight": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.1.tgz", + "integrity": "sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A==", + "requires": { + "chalk": "^2.3.0", + "highlight.js": "^9.6.0", + "mz": "^2.4.0", + "parse5": "^4.0.0", + "yargs": "^13.0.0" + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -732,7 +774,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, "requires": { "string-width": "^2.1.1", "strip-ansi": "^4.0.0", @@ -742,8 +783,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -759,7 +799,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -767,8 +806,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -793,8 +831,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { "version": "3.1.2", @@ -839,8 +876,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.1", @@ -897,8 +933,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -1021,6 +1056,11 @@ "is-obj": "^1.0.0" } }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1041,8 +1081,7 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "encodeurl": { "version": "1.0.2", @@ -1053,7 +1092,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -1100,8 +1138,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-plugin-prettier": { "version": "2.7.0", @@ -1116,8 +1153,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esutils": { "version": "2.0.2", @@ -1321,6 +1357,11 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "figlet": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.2.3.tgz", + "integrity": "sha512-+F5zdvZ66j77b8x2KCPvWUHC0UCKUMWrewxmewgPlagp3wmDpcrHMbyv/ygq/6xoxBPGQA+UJU3SMoBzKoROQQ==" + }, "figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", @@ -1385,7 +1426,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, "requires": { "locate-path": "^3.0.0" } @@ -1469,8 +1509,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -2021,8 +2060,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { "version": "2.0.0", @@ -2058,7 +2096,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2163,7 +2200,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" }, @@ -2171,16 +2207,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" } } }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -2226,6 +2260,11 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "highlight.js": { + "version": "9.15.8", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz", + "integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA==" + }, "hosted-git-info": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", @@ -2323,6 +2362,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -2361,7 +2405,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2381,8 +2424,7 @@ "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "ipaddr.js": { "version": "1.8.0", @@ -2511,8 +2553,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "4.0.1", @@ -2663,14 +2704,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -2694,7 +2733,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2725,7 +2763,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, "requires": { "invert-kv": "^2.0.0" } @@ -2948,7 +2985,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -3033,7 +3069,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, "requires": { "p-defer": "^1.0.0" } @@ -3071,7 +3106,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", @@ -3130,14 +3164,12 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3173,7 +3205,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -3181,8 +3212,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -3248,6 +3278,27 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mysql": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.17.1.tgz", + "integrity": "sha512-7vMqHQ673SAk5C8fOzTG2LpPcf3bNt0oL3sFpxPEEFp1mdlDcrLK0On7z8ZYKaaHrHwNcQ/MTUz7/oobZ2OyyA==", + "requires": { + "bignumber.js": "7.2.1", + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + } + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -3282,8 +3333,7 @@ "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-environment-flags": { "version": "1.0.5", @@ -3370,7 +3420,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, "requires": { "path-key": "^2.0.0" } @@ -3389,14 +3438,12 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -3487,7 +3534,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -3513,7 +3559,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, "requires": { "execa": "^1.0.0", "lcid": "^2.0.0", @@ -3524,7 +3569,6 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -3537,7 +3581,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -3552,7 +3595,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, "requires": { "pump": "^3.0.0" } @@ -3562,26 +3604,22 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -3590,7 +3628,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, "requires": { "p-limit": "^2.0.0" } @@ -3618,8 +3655,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-json": { "version": "4.0.1", @@ -3633,6 +3669,11 @@ "semver": "^5.1.0" } }, + "parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=" + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -3643,6 +3684,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", @@ -3663,14 +3709,12 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -3681,8 +3725,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { "version": "1.0.6", @@ -3790,8 +3833,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "property-expr": { "version": "1.5.1", @@ -3824,7 +3866,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3879,7 +3920,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3901,6 +3941,11 @@ "readable-stream": "^2.0.2" } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "regenerator-runtime": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", @@ -3957,14 +4002,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { "version": "1.11.0", @@ -4051,11 +4094,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, "semver-compare": { "version": "1.0.0", @@ -4113,8 +4160,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.0", @@ -4148,7 +4194,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -4156,14 +4201,12 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simple-git": { "version": "1.113.0", @@ -4397,8 +4440,12 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, "staged-git-files": { "version": "1.1.2", @@ -4442,7 +4489,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -4452,7 +4498,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4472,7 +4517,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -4480,8 +4524,7 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-json-comments": { "version": "2.0.1", @@ -4493,7 +4536,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -4519,6 +4561,22 @@ "execa": "^0.7.0" } }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -4598,8 +4656,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { "version": "5.16.0", @@ -4669,6 +4726,42 @@ "mime-types": "~2.1.18" } }, + "typeorm": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.18.tgz", + "integrity": "sha512-S553GwtG5ab268+VmaLCN7gKDqFPIzUw0eGMTobJ9yr0Np62Ojfx8j1Oa9bIeh5p7Pz1/kmGabAHoP1MYK05pA==", + "requires": { + "app-root-path": "^2.0.1", + "buffer": "^5.1.0", + "chalk": "^2.4.2", + "cli-highlight": "^2.0.0", + "debug": "^4.1.1", + "dotenv": "^6.2.0", + "glob": "^7.1.2", + "js-yaml": "^3.13.1", + "mkdirp": "^0.5.1", + "reflect-metadata": "^0.1.13", + "tslib": "^1.9.0", + "xml2js": "^0.4.17", + "yargonaut": "^1.1.2", + "yargs": "^13.2.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "typescript": { "version": "3.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", @@ -4826,8 +4919,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", @@ -4853,7 +4945,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -4861,8 +4952,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.3", @@ -4886,7 +4976,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, "requires": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" @@ -4895,14 +4984,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4911,7 +4998,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4922,7 +5008,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4932,8 +5017,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.4.2", @@ -4952,11 +5036,24 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "2.1.2", @@ -4964,11 +5061,57 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yargonaut": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", + "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", + "requires": { + "chalk": "^1.1.1", + "figlet": "^1.1.1", + "parent-require": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "yargs": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, "requires": { "cliui": "^4.0.0", "find-up": "^3.0.0", @@ -4986,14 +5129,12 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -5004,7 +5145,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -5015,7 +5155,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -5024,8 +5163,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" } } }, diff --git a/package.json b/package.json index c84f22e3..e5e2aa6d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mock", "version": "1.0.0", "description": "Mock your friends", - "main": "src/server.ts", + "main": "src/index.ts", "scripts": { "format:check": "prettier --check 'src/**/*.ts'", "format:fix": "prettier --write 'src/**/*.ts'", @@ -10,7 +10,7 @@ "lint:fix": "tslint --fix -c tslint.json 'src/**/*.ts'", "start": "npm run start:dev", "start:prod": "node ./dist/server.js", - "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/server.ts", + "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", "test": "mocha -r ts-node/register ./src/**/*.spec.ts", "test:watch": "mocha -r ts-node/register ./src/**/*.spec.ts --watch", "tsc": "tsc" @@ -22,6 +22,9 @@ "axios": "^0.18.1", "body-parser": "^1.18.3", "express": "^4.16.4", + "mysql": "^2.17.1", + "reflect-metadata": "^0.1.13", + "typeorm": "^0.2.18", "typescript": "^3.4.5" }, "devDependencies": { diff --git a/src/db/Muzzle/actions/muzzle-actions.ts b/src/db/Muzzle/actions/muzzle-actions.ts new file mode 100644 index 00000000..2da61920 --- /dev/null +++ b/src/db/Muzzle/actions/muzzle-actions.ts @@ -0,0 +1,44 @@ +import { getRepository } from "typeorm"; +import { Muzzle } from "../models/Muzzle"; + +export function addMuzzleToDb( + requestorId: string, + muzzledId: string, + time: number +) { + const muzzle = new Muzzle(); + muzzle.requestorId = requestorId; + muzzle.muzzledId = muzzledId; + muzzle.messagesSuppressed = 0; + muzzle.wordsSuppressed = 0; + muzzle.charactersSuppressed = 0; + muzzle.milliseconds = time; + return getRepository(Muzzle).save(muzzle); +} + +export function incrementMuzzleTime(id: number, ms: number) { + return getRepository(Muzzle).increment({ id }, "milliseconds", ms); +} + +export function incrementMessageSuppressions(id: number) { + return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); +} + +export function incrementWordSuppressions(id: number, suppressions: number) { + return getRepository(Muzzle).increment( + { id }, + "wordsSuppressed", + suppressions + ); +} + +export function incrementCharacterSuppressions( + id: number, + charactersSuppressed: number +) { + return getRepository(Muzzle).increment( + { id }, + "charactersSuppressed", + charactersSuppressed + ); +} diff --git a/src/db/Muzzle/models/Muzzle.ts b/src/db/Muzzle/models/Muzzle.ts new file mode 100644 index 00000000..f75e5de9 --- /dev/null +++ b/src/db/Muzzle/models/Muzzle.ts @@ -0,0 +1,28 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Muzzle { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public requestorId!: string; + + @Column() + public muzzledId!: string; + + @Column() + public milliseconds!: number; + + @Column() + public messagesSuppressed!: number; + + @Column() + public wordsSuppressed!: number; + + @Column() + public charactersSuppressed!: number; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + public createdAt!: Date; +} diff --git a/src/server.ts b/src/index.ts similarity index 61% rename from src/server.ts rename to src/index.ts index ae137ac3..6ed55abf 100644 --- a/src/server.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ import bodyParser from "body-parser"; import express, { Application } from "express"; +import "reflect-metadata"; +import { createConnection } from "typeorm"; +import { config } from "./ormconfig"; import { defineRoutes } from "./routes/define-route"; import { mockRoutes } from "./routes/mock-route"; import { muzzleRoutes } from "./routes/muzzle-route"; @@ -14,7 +17,16 @@ app.use(mockRoutes); app.use(muzzleRoutes); app.use(defineRoutes); -getAllUsers(); +createConnection(config) + .then(connection => { + if (connection.isConnected) { + getAllUsers(); + console.log(`Connected to MySQL DB: ${config.database}`); + } else { + throw Error("Unable to connect to database"); + } + }) + .catch(e => console.error(e)); app.listen(PORT, (e: Error) => e ? console.error(e) : console.log("Listening on port 3000") diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index 8b0735c9..ecc7eb20 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -9,11 +9,13 @@ import { addUserToMuzzled, containsTag, deleteMessage, + getMuzzleId, getTimeString, isUserMuzzled, sendMessage, sendMuzzledMessage, - shouldBotMessageBeMuzzled + shouldBotMessageBeMuzzled, + trackDeletedMessage } from "../utils/muzzle/muzzle-utils"; import { getUserId, getUserName } from "../utils/slack/slack-utils"; @@ -21,7 +23,7 @@ export const muzzleRoutes: Router = express.Router(); muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; - if (isUserMuzzled(request.event.user)) { + if (isUserMuzzled(request.event.user) && !containsTag(request.event.text)) { console.log( `${getUserName(request.event.user)} | ${ request.event.user @@ -33,26 +35,27 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user, request.event.text ); - if (containsTag(request.event.text)) { - console.log( - `${getUserName( - request.event.user - )} atttempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - addMuzzleTime(request.event.user); - setTimeout( - () => - sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to tag someone, or the channel while muzzled! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` - ), - 1000 - ); - } + } else if ( + isUserMuzzled(request.event.user) && + containsTag(request.event.text) + ) { + const muzzleId = getMuzzleId(request.event.user); + console.log( + `${getUserName( + request.event.user + )} atttempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + addMuzzleTime(request.event.user, ABUSE_PENALTY_TIME); + deleteMessage(request.event.channel, request.event.ts); + trackDeletedMessage(muzzleId, request.event.text); + sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); } else if (shouldBotMessageBeMuzzled(request)) { console.log( `A user is muzzled and tried to send a bot message! Suppressing...` @@ -65,9 +68,9 @@ muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { muzzleRoutes.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: any = getUserId(request.text); - const results = await addUserToMuzzled(userId, request.user_id).catch(e => - res.send(e) - ); + const results = await addUserToMuzzled(userId, request.user_id).catch(e => { + res.send(e); + }); if (results) { res.send(results); } diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 5de384f5..bae2d7b1 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -1,6 +1,7 @@ export interface IMuzzled { suppressionCount: number; muzzledBy: string; + id: number; removalFn: NodeJS.Timeout; } diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle-utils.spec.ts index bda51cc0..14a80867 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle-utils.spec.ts @@ -65,6 +65,16 @@ describe("muzzle-utils", () => { }); }); + it("should reject if a user tries to muzzle a user that does not exist", async () => { + await addUserToMuzzled("", testData.requestor); + expect(muzzled.has("")).to.equal(false); + await addUserToMuzzled("", testData.requestor).catch(e => { + expect(e).to.equal( + `Invalid username passed in. You can only muzzle existing slack users` + ); + }); + }); + it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { await addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.has(testData.user)).to.equal(true); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle-utils.ts index ed52f124..f094f210 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle-utils.ts @@ -3,6 +3,13 @@ import { ChatPostMessageArguments, WebClient } from "@slack/web-api"; +import { + addMuzzleToDb, + incrementCharacterSuppressions, + incrementMessageSuppressions, + incrementMuzzleTime, + incrementWordSuppressions +} from "../../db/Muzzle/actions/muzzle-actions"; import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; import { @@ -15,7 +22,7 @@ export const muzzled: Map = new Map(); // Store for people who are muzzling others. export const requestors: Map = new Map(); -// Time period in which a user must wait before making more muzzles. +// Muzzle Constants const MAX_MUZZLE_TIME = 3600000; const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const ABUSE_PENALTY_TIME = 300000; @@ -27,25 +34,40 @@ export const web: WebClient = new WebClient(process.env.muzzleBotToken); /** * Takes in text and randomly muzzles certain words. */ -export function muzzle(text: string) { +export function muzzle(text: string, muzzleId: number) { + const replacementText = " ..mMm... "; let returnText = ""; const words = text.split(" "); + let wordsSuppressed = 0; + let charactersSuppressed = 0; + let replacementWord; for (const word of words) { - returnText += - isRandomEven() && !containsTag(word) ? ` *${word}* ` : " ..mMm.. "; + replacementWord = + isRandomEven() && !containsTag(word) ? ` *${word}* ` : replacementText; + if (replacementWord === replacementText) { + wordsSuppressed++; + charactersSuppressed += word.length; + } + returnText += replacementWord; } + incrementMessageSuppressions(muzzleId); + incrementCharacterSuppressions(muzzleId, charactersSuppressed); + incrementWordSuppressions(muzzleId, wordsSuppressed); return returnText; } -export function addMuzzleTime(userId: string) { +export function addMuzzleTime(userId: string, timeToAdd: number) { if (userId && muzzled.has(userId)) { const removalFn = muzzled.get(userId)!.removalFn; - const newTime = getRemainingTime(removalFn) + ABUSE_PENALTY_TIME; + const newTime = getRemainingTime(removalFn) + timeToAdd; + const muzzleId = muzzled.get(userId)!.id; + incrementMuzzleTime(muzzleId, ABUSE_PENALTY_TIME); clearTimeout(muzzled.get(userId)!.removalFn); console.log(`Setting ${getUserName(userId)}'s muzzle time to ${newTime}`); muzzled.set(userId, { suppressionCount: muzzled.get(userId)!.suppressionCount, muzzledBy: muzzled.get(userId)!.muzzledBy, + id: muzzled.get(userId)!.id, removalFn: setTimeout(() => removeMuzzle(userId), newTime) }); } @@ -57,11 +79,17 @@ function getRemainingTime(timeout: any) { ); } +export function getMuzzleId(userId: string) { + return muzzled.get(userId)!.id; +} + /** * Determines whether or not a user is trying to @user, @channel or @here while muzzled. */ -export function containsTag(word: string): boolean { - return !!getUserId(word) || word === "" || word === ""; +export function containsTag(text: string): boolean { + return ( + text.includes("") || text.includes("") || !!getUserId(text) + ); } /** @@ -173,10 +201,16 @@ function setMuzzlerCount(requestorId: string) { /** * Adds a userId to the muzzled array, and sets timeout for removeMuzzle. */ -function muzzleUser(userId: string, requestorId: string, timeToMuzzle: number) { +function muzzleUser( + userId: string, + requestorId: string, + id: number, + timeToMuzzle: number +) { muzzled.set(userId, { suppressionCount: 0, muzzledBy: requestorId, + id, removalFn: setTimeout(() => removeMuzzle(userId), timeToMuzzle) }); } @@ -187,8 +221,12 @@ function muzzleUser(userId: string, requestorId: string, timeToMuzzle: number) { export function addUserToMuzzled(userId: string, requestorId: string) { const userName = getUserName(userId); const requestorName = getUserName(requestorId); - return new Promise((resolve, reject) => { - if (isUserMuzzled(userId)) { + return new Promise(async (resolve, reject) => { + if (!userId) { + reject( + `Invalid username passed in. You can only muzzle existing slack users` + ); + } else if (isUserMuzzled(userId)) { console.error( `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` ); @@ -207,14 +245,22 @@ export function addUserToMuzzled(userId: string, requestorId: string) { ); } else { const timeToMuzzle = getTimeToMuzzle(); - muzzleUser(userId, requestorId, timeToMuzzle); - setMuzzlerCount(requestorId); - console.log( - `${userName} | ${userId} is now muzzled for ${timeToMuzzle} milliseconds` - ); - resolve( - `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` - ); + const muzzleFromDb = await addMuzzleToDb( + requestorId, + userId, + timeToMuzzle + ).catch((e: any) => { + console.error(e); + reject(`Muzzle failed!`); + }); + + if (muzzleFromDb) { + muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); + setMuzzlerCount(requestorId); + resolve( + `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` + ); + } } }); } @@ -265,14 +311,31 @@ export function sendMuzzledMessage( userId: string, text: string ) { + const muzzleId = muzzled.get(userId)!.id; if (muzzled.get(userId)!.suppressionCount < MAX_SUPPRESSIONS) { muzzled.set(userId, { suppressionCount: ++muzzled.get(userId)!.suppressionCount, muzzledBy: muzzled.get(userId)!.muzzledBy, + id: muzzleId, removalFn: muzzled.get(userId)!.removalFn }); - sendMessage(channel, `<@${userId}> says "${muzzle(text)}"`); + sendMessage(channel, `<@${userId}> says "${muzzle(text, muzzleId)}"`); + } else { + trackDeletedMessage(muzzleId, text); + } +} + +export function trackDeletedMessage(muzzleId: number, text: string) { + const words = text.split(" "); + let wordsSuppressed = 0; + let charactersSuppressed = 0; + for (const word of words) { + wordsSuppressed++; + charactersSuppressed += word.length; } + incrementMessageSuppressions(muzzleId); + incrementWordSuppressions(muzzleId, wordsSuppressed); + incrementCharacterSuppressions(muzzleId, charactersSuppressed); } /** diff --git a/tsconfig.json b/tsconfig.json index a74fe226..0a29fab2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,7 +46,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ /* Source Map Options */ @@ -56,7 +56,7 @@ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, + "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ } } From 411479faad92df3a55cd8a94af4b86df885f9117 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 5 Jul 2019 16:50:18 -0400 Subject: [PATCH 017/167] Clean up code and comments (#24) * Changed define route to avoid unnecessarily calling the API if a user is muzzled * Added comments to muzzle-utils, renamed removeMuzzler to removeRequestor and optimized trackDeletedMessage * Moved some of the muzzle code out into a separate utilities file to make muzzle more readable * Separated out muzzle-utilties and muzzle tests * added additional testing to getTimeString * Removed direct references to muzzled * Adjusted comments --- src/db/Muzzle/actions/muzzle-actions.ts | 12 ++ src/routes/define-route.ts | 26 ++--- src/routes/mock-route.ts | 28 ++--- src/routes/muzzle-route.ts | 15 ++- src/shared/models/muzzle/muzzle-models.ts | 2 +- src/utils/muzzle/muzzle-utilities.spec.ts | 29 +++++ src/utils/muzzle/muzzle-utilities.ts | 33 ++++++ .../{muzzle-utils.spec.ts => muzzle.spec.ts} | 42 ++----- .../muzzle/{muzzle-utils.ts => muzzle.ts} | 107 ++++++------------ src/utils/slack/slack-utils.spec.ts | 29 ++++- src/utils/slack/slack-utils.ts | 23 +++- 11 files changed, 205 insertions(+), 141 deletions(-) create mode 100644 src/utils/muzzle/muzzle-utilities.spec.ts create mode 100644 src/utils/muzzle/muzzle-utilities.ts rename src/utils/muzzle/{muzzle-utils.spec.ts => muzzle.spec.ts} (82%) rename src/utils/muzzle/{muzzle-utils.ts => muzzle.ts} (79%) diff --git a/src/db/Muzzle/actions/muzzle-actions.ts b/src/db/Muzzle/actions/muzzle-actions.ts index 2da61920..3fe4cb00 100644 --- a/src/db/Muzzle/actions/muzzle-actions.ts +++ b/src/db/Muzzle/actions/muzzle-actions.ts @@ -42,3 +42,15 @@ export function incrementCharacterSuppressions( charactersSuppressed ); } + +/** + * Determines suppression counts for messages that are ONLY deleted and not muzzled. + * Used when a muzzled user has hit their max suppressions or when they have tagged channel. + */ +export function trackDeletedMessage(muzzleId: number, text: string) { + const words = text.split(" ").length; + const characters = text.split("").length; + incrementMessageSuppressions(muzzleId); + incrementWordSuppressions(muzzleId, words); + incrementCharacterSuppressions(muzzleId, characters); +} diff --git a/src/routes/define-route.ts b/src/routes/define-route.ts index 3b99cf81..8ba36f12 100644 --- a/src/routes/define-route.ts +++ b/src/routes/define-route.ts @@ -9,28 +9,28 @@ import { define, formatDefs } from "../utils/define/define-utils"; -import { muzzled } from "../utils/muzzle/muzzle-utils"; +import { isUserMuzzled } from "../utils/muzzle/muzzle"; import { sendResponse } from "../utils/slack/slack-utils"; export const defineRoutes: Router = express.Router(); defineRoutes.post("/define", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - try { - const defined: IUrbanDictionaryResponse = await define(request.text); - const response: IChannelResponse = { - response_type: "in_channel", - text: `*${capitalizeFirstLetter(request.text)}*`, - attachments: formatDefs(defined.list) - }; - if (muzzled.has(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); - } else { + if (isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + try { + const defined: IUrbanDictionaryResponse = await define(request.text); + const response: IChannelResponse = { + response_type: "in_channel", + text: `*${capitalizeFirstLetter(request.text)}*`, + attachments: formatDefs(defined.list) + }; sendResponse(request.response_url, response); res.status(200).send(); + } catch (e) { + res.send(`error: ${e.message}`); } - } catch (e) { - res.send(`error: ${e.message}`); } }); diff --git a/src/routes/mock-route.ts b/src/routes/mock-route.ts index 6e6e9ac5..f5273a8b 100644 --- a/src/routes/mock-route.ts +++ b/src/routes/mock-route.ts @@ -4,27 +4,27 @@ import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; import { mock } from "../utils/mock/mock-utils"; -import { muzzled } from "../utils/muzzle/muzzle-utils"; +import { isUserMuzzled } from "../utils/muzzle/muzzle"; import { sendResponse } from "../utils/slack/slack-utils"; export const mockRoutes: Router = express.Router(); mockRoutes.post("/mock", (req, res) => { const request: ISlashCommandRequest = req.body; - const mocked: string = mock(request.text); - const response: IChannelResponse = { - attachments: [ - { - text: mocked - } - ], - response_type: "in_channel", - text: `<@${request.user_id}>` - }; - if (!muzzled.has(request.user_id)) { + if (isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + const mocked: string = mock(request.text); + const response: IChannelResponse = { + attachments: [ + { + text: mocked + } + ], + response_type: "in_channel", + text: `<@${request.user_id}>` + }; sendResponse(request.response_url, response); res.status(200).send(); - } else if (muzzled.has(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); } }); diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts index ecc7eb20..0c0c7f73 100644 --- a/src/routes/muzzle-route.ts +++ b/src/routes/muzzle-route.ts @@ -1,4 +1,5 @@ import express, { Request, Response, Router } from "express"; +import { trackDeletedMessage } from "../db/Muzzle/actions/muzzle-actions"; import { IEventRequest, ISlashCommandRequest @@ -7,17 +8,19 @@ import { ABUSE_PENALTY_TIME, addMuzzleTime, addUserToMuzzled, - containsTag, deleteMessage, getMuzzleId, - getTimeString, isUserMuzzled, sendMessage, sendMuzzledMessage, - shouldBotMessageBeMuzzled, - trackDeletedMessage -} from "../utils/muzzle/muzzle-utils"; -import { getUserId, getUserName } from "../utils/slack/slack-utils"; + shouldBotMessageBeMuzzled +} from "../utils/muzzle/muzzle"; +import { getTimeString } from "../utils/muzzle/muzzle-utilities"; +import { + containsTag, + getUserId, + getUserName +} from "../utils/slack/slack-utils"; export const muzzleRoutes: Router = express.Router(); diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index bae2d7b1..06fc436b 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -5,7 +5,7 @@ export interface IMuzzled { removalFn: NodeJS.Timeout; } -export interface IMuzzler { +export interface IRequestor { muzzleCount: number; muzzleCountRemover?: NodeJS.Timeout; } diff --git a/src/utils/muzzle/muzzle-utilities.spec.ts b/src/utils/muzzle/muzzle-utilities.spec.ts new file mode 100644 index 00000000..4bf0bfba --- /dev/null +++ b/src/utils/muzzle/muzzle-utilities.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; +import { getTimeString, getTimeToMuzzle } from "./muzzle-utilities"; + +describe("muzzle-utilities", () => { + describe("getTimeToMuzzle()", () => { + it("should return a value greater than 0 and less than 180000", () => { + expect(getTimeToMuzzle()).to.be.greaterThan(0); + expect(getTimeToMuzzle()).to.be.lessThan(180000); + }); + }); + + describe("getTimeString()", () => { + it("should return 1m30s when 90000ms are passed in", () => { + expect(getTimeString(90000)).to.equal("1m30s"); + }); + + it("should return 2m00s when 120000ms is passed in", () => { + expect(getTimeString(120000)).to.equal("2m00s"); + }); + + it("should return 2m00s when 120000.123 is passed in", () => { + expect(getTimeString(120000.123)).to.equal("2m00s"); + }); + + it("should return 2m00s when 120000.999 is passed in", () => { + expect(getTimeString(120000.999)).to.equal("2m00s"); + }); + }); +}); diff --git a/src/utils/muzzle/muzzle-utilities.ts b/src/utils/muzzle/muzzle-utilities.ts new file mode 100644 index 00000000..65330cde --- /dev/null +++ b/src/utils/muzzle/muzzle-utilities.ts @@ -0,0 +1,33 @@ +/** + * Gets the amount of time remaining on a NodeJS Timeout. + */ +export function getRemainingTime(timeout: any) { + return Math.ceil( + timeout._idleStart + timeout._idleTimeout - process.uptime() * 1000 + ); +} + +/** + * Gives us a random value between 30 seconds and 3 minutes. + */ +export function getTimeToMuzzle() { + return Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); +} + +/** + * Gives us a time string formatted as 1m20s to show the user. + */ +export function getTimeString(time: number) { + const minutes = Math.floor(time / 60000); + const seconds = ((time % 60000) / 1000).toFixed(0); + return +seconds === 60 + ? minutes + 1 + "m00s" + : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s"; +} + +/** + * Generates a random number tells us if it is even. + */ +export function isRandomEven() { + return Math.floor(Math.random() * 2) % 2 === 0; +} diff --git a/src/utils/muzzle/muzzle-utils.spec.ts b/src/utils/muzzle/muzzle.spec.ts similarity index 82% rename from src/utils/muzzle/muzzle-utils.spec.ts rename to src/utils/muzzle/muzzle.spec.ts index 14a80867..39564ec3 100644 --- a/src/utils/muzzle/muzzle-utils.spec.ts +++ b/src/utils/muzzle/muzzle.spec.ts @@ -4,15 +4,14 @@ import { ISlackUser } from "../../shared/models/slack/slack-models"; import { setUserList } from "../slack/slack-utils"; import { addUserToMuzzled, - containsTag, muzzle, muzzled, removeMuzzle, - removeMuzzler, + removeRequestor, requestors -} from "./muzzle-utils"; +} from "./muzzle"; -describe("muzzle-utils", () => { +describe("muzzle", () => { const testData = { user: "123", user2: "456", @@ -130,58 +129,31 @@ describe("muzzle-utils", () => { }); }); - describe("removeMuzzler()", () => { + describe("removeRequestor()", () => { it("should remove a user from the muzzler array", () => { addUserToMuzzled(testData.user, testData.requestor); expect(muzzled.size).to.equal(1); expect(muzzled.has(testData.user)).to.equal(true); expect(requestors.size).to.equal(1); expect(requestors.has(testData.requestor)).to.equal(true); - removeMuzzler(testData.requestor); + removeRequestor(testData.requestor); expect(requestors.has(testData.requestor)).to.equal(false); expect(requestors.size).to.equal(0); }); }); - describe("containsTag()", () => { - it("should return false if a word has @ in it and is not a tag", () => { - const testWord = ".@channel"; - expect(containsTag(testWord)).to.equal(false); - }); - - it("should return false if a word does not include @", () => { - const testWord = "test"; - expect(containsTag(testWord)).to.equal(false); - }); - - it("should return true if a word has in it", () => { - const testWord = ""; - expect(containsTag(testWord)).to.equal(true); - }); - - it("should return true if a word has in it", () => { - const testWord = ""; - expect(containsTag(testWord)).to.equal(true); - }); - - it("should return true if a word has a tagged user", () => { - const testUser = "<@UTJFJKL>"; - expect(containsTag(testUser)).to.equal(true); - }); - }); - describe("muzzle()", () => { it("should always muzzle a tagged user", () => { const testSentence = "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzle(testSentence)).to.equal( + expect(muzzle(testSentence, 1)).to.equal( " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " ); }); it("should always muzzle ", () => { const testSentence = " hey guys"; - expect(muzzle(testSentence).includes("")).to.equal(false); + expect(muzzle(testSentence, 1).includes("")).to.equal(false); }); }); }); diff --git a/src/utils/muzzle/muzzle-utils.ts b/src/utils/muzzle/muzzle.ts similarity index 79% rename from src/utils/muzzle/muzzle-utils.ts rename to src/utils/muzzle/muzzle.ts index f094f210..e4fe6fcb 100644 --- a/src/utils/muzzle/muzzle-utils.ts +++ b/src/utils/muzzle/muzzle.ts @@ -8,19 +8,28 @@ import { incrementCharacterSuppressions, incrementMessageSuppressions, incrementMuzzleTime, - incrementWordSuppressions + incrementWordSuppressions, + trackDeletedMessage } from "../../db/Muzzle/actions/muzzle-actions"; -import { IMuzzled, IMuzzler } from "../../shared/models/muzzle/muzzle-models"; +import { IMuzzled, IRequestor } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; import { + containsTag, + getBotId, getUserId, getUserIdByCallbackId, getUserName } from "../slack/slack-utils"; +import { + getRemainingTime, + getTimeString, + getTimeToMuzzle, + isRandomEven +} from "./muzzle-utilities"; // Store for the muzzled users. export const muzzled: Map = new Map(); // Store for people who are muzzling others. -export const requestors: Map = new Map(); +export const requestors: Map = new Map(); // Muzzle Constants const MAX_MUZZLE_TIME = 3600000; @@ -56,6 +65,9 @@ export function muzzle(text: string, muzzleId: number) { return returnText; } +/** + * Adds the specified amount of time to a specified muzzled user. + */ export function addMuzzleTime(userId: string, timeToAdd: number) { if (userId && muzzled.has(userId)) { const removalFn = muzzled.get(userId)!.removalFn; @@ -73,41 +85,11 @@ export function addMuzzleTime(userId: string, timeToAdd: number) { } } -function getRemainingTime(timeout: any) { - return Math.ceil( - timeout._idleStart + timeout._idleTimeout - process.uptime() * 1000 - ); -} - -export function getMuzzleId(userId: string) { - return muzzled.get(userId)!.id; -} - /** - * Determines whether or not a user is trying to @user, @channel or @here while muzzled. + * Gets the corresponding database ID for the user's current muzzle. */ -export function containsTag(text: string): boolean { - return ( - text.includes("") || text.includes("") || !!getUserId(text) - ); -} - -/** - * Gives us a random value between 30 seconds and 3 minutes. - */ -export function getTimeToMuzzle() { - return Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); -} - -/** - * Gives us a time string formatted as 1m20s to show the user. - */ -export function getTimeString(time: number) { - const minutes = Math.floor(time / 60000); - const seconds = ((time % 60000) / 1000).toFixed(0); - return +seconds === 60 - ? minutes + 1 + "m00s" - : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s"; +export function getMuzzleId(userId: string) { + return muzzled.get(userId)!.id; } /** @@ -127,15 +109,6 @@ export function isUserMuzzled(userId: string) { return muzzled.has(userId); } -function getBotId( - fromText: string | undefined, - fromAttachmentText: string | undefined, - fromPretext: string | undefined, - fromCallbackId: string | undefined -) { - return fromText || fromAttachmentText || fromPretext || fromCallbackId; -} - /** * Determines whether or not a bot message should be removed. */ @@ -175,9 +148,9 @@ export function shouldBotMessageBeMuzzled(request: IEventRequest) { } /** - * Adds a requestor to the requestors array with a muzzleCount to track how many muzzles have been performed, as well as a removal funciton. + * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. */ -function setMuzzlerCount(requestorId: string) { +function setRequestorCount(requestorId: string) { const muzzleCount = requestors.has(requestorId) ? ++requestors.get(requestorId)!.muzzleCount : 1; @@ -190,7 +163,7 @@ function setMuzzlerCount(requestorId: string) { const removalFunction = requestors.has(requestorId) && requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES - ? () => removeMuzzler(requestorId) + ? () => removeRequestor(requestorId) : () => decrementMuzzleCount(requestorId); requestors.set(requestorId, { muzzleCount, @@ -199,7 +172,7 @@ function setMuzzlerCount(requestorId: string) { } /** - * Adds a userId to the muzzled array, and sets timeout for removeMuzzle. + * Adds a userId to the muzzled map, and sets timeout for removeMuzzle. */ function muzzleUser( userId: string, @@ -216,7 +189,7 @@ function muzzleUser( } /** - * Adds a user to the muzzled array and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes + * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ export function addUserToMuzzled(userId: string, requestorId: string) { const userName = getUserName(userId); @@ -256,7 +229,7 @@ export function addUserToMuzzled(userId: string, requestorId: string) { if (muzzleFromDb) { muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); - setMuzzlerCount(requestorId); + setRequestorCount(requestorId); resolve( `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` ); @@ -264,7 +237,9 @@ export function addUserToMuzzled(userId: string, requestorId: string) { } }); } - +/** + * Decrements the muzzleCount on a requestor. + */ export function decrementMuzzleCount(requestorId: string) { if (requestors.has(requestorId)) { const decrementedMuzzle = --requestors.get(requestorId)!.muzzleCount; @@ -286,7 +261,10 @@ export function decrementMuzzleCount(requestorId: string) { } } -export function removeMuzzler(userId: string) { +/** + * Removes a requestor from the map. + */ +export function removeRequestor(userId: string) { requestors.delete(userId); console.log( `${MAX_MUZZLE_TIME} has passed since ${getUserName( @@ -295,6 +273,9 @@ export function removeMuzzler(userId: string) { ); } +/** + * Removes a muzzle from the specified user. + */ export function removeMuzzle(userId: string) { muzzled.delete(userId); console.log( @@ -302,10 +283,9 @@ export function removeMuzzle(userId: string) { ); } -export function isRandomEven() { - return Math.floor(Math.random() * 2) % 2 === 0; -} - +/** + * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. + */ export function sendMuzzledMessage( channel: string, userId: string, @@ -325,19 +305,6 @@ export function sendMuzzledMessage( } } -export function trackDeletedMessage(muzzleId: number, text: string) { - const words = text.split(" "); - let wordsSuppressed = 0; - let charactersSuppressed = 0; - for (const word of words) { - wordsSuppressed++; - charactersSuppressed += word.length; - } - incrementMessageSuppressions(muzzleId); - incrementWordSuppressions(muzzleId, wordsSuppressed); - incrementCharacterSuppressions(muzzleId, charactersSuppressed); -} - /** * Handles deletion of messages. */ diff --git a/src/utils/slack/slack-utils.spec.ts b/src/utils/slack/slack-utils.spec.ts index bb48e79f..d71ea5a5 100644 --- a/src/utils/slack/slack-utils.spec.ts +++ b/src/utils/slack/slack-utils.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { getUserId } from "./slack-utils"; +import { containsTag, getUserId } from "./slack-utils"; describe("slack-utils", () => { describe("getUserId()", () => { @@ -19,4 +19,31 @@ describe("slack-utils", () => { expect(getUserId("total waste of time")).to.equal(""); }); }); + + describe("containsTag()", () => { + it("should return false if a word has @ in it and is not a tag", () => { + const testWord = ".@channel"; + expect(containsTag(testWord)).to.equal(false); + }); + + it("should return false if a word does not include @", () => { + const testWord = "test"; + expect(containsTag(testWord)).to.equal(false); + }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(containsTag(testWord)).to.equal(true); + }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(containsTag(testWord)).to.equal(true); + }); + + it("should return true if a word has a tagged user", () => { + const testUser = "<@UTJFJKL>"; + expect(containsTag(testUser)).to.equal(true); + }); + }); }); diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts index 036f7085..daa8fa5a 100644 --- a/src/utils/slack/slack-utils.ts +++ b/src/utils/slack/slack-utils.ts @@ -3,7 +3,7 @@ import { IChannelResponse, ISlackUser } from "../../shared/models/slack/slack-models"; -import { web } from "../muzzle/muzzle-utils"; +import { web } from "../muzzle/muzzle"; const userIdRegEx = /[<]@\w+/gm; @@ -65,3 +65,24 @@ export function getAllUsers() { setTimeout(() => getAllUsers(), 5000); }); } + +/** + * Retrieves a Slack user id from the various fields in which a userId can exist inside of a bot response. + */ +export function getBotId( + fromText: string | undefined, + fromAttachmentText: string | undefined, + fromPretext: string | undefined, + fromCallbackId: string | undefined +) { + return fromText || fromAttachmentText || fromPretext || fromCallbackId; +} + +/** + * Determines whether or not a user is trying to @user, @channel or @here while muzzled. + */ +export function containsTag(text: string): boolean { + return ( + text.includes("") || text.includes("") || !!getUserId(text) + ); +} From ca9c9216ba83a1b3a84a726873842aa7f9c6dc5d Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 7 Jul 2019 11:50:00 -0400 Subject: [PATCH 018/167] Convert to Singleton Service Architecture (#25) * Added testdouble as a dev-dependency * Converted to use jest rather than mocha/chai for tests * Moved jest and typescript to dev-dependencies * Removed accidental save-dev dependency * Converted muzzle-utils to muzzle service and created web-client-service * Converted slack-utils to slack.service and slack.service.spec * Converted to SlackService * Completed conversion from utils to classes and renamed from utils -> services, and routes -> controllers * Converted to singleton classes for all service * Added more tests for slackService * Added tests for defineService * mock-utils -> mock.service and corresponding tests * Added simple tests for WebService * Moved db calls to muzzle persistence service * Added mocks to muzzle.service.spec * Removed lolex dependency --- jest.config.js | 4 + package-lock.json | 2901 +++++++++++++++-- package.json | 18 +- src/controllers/define.controller.ts | 36 + src/controllers/mock.controller.ts | 34 + src/controllers/muzzle.controller.ts | 82 + src/db/Muzzle/actions/muzzle-actions.ts | 56 - src/index.ts | 18 +- src/routes/define-route.ts | 36 - src/routes/mock-route.ts | 30 - src/routes/muzzle-route.ts | 80 - .../define/define.service.spec.ts} | 36 +- src/services/define/define.service.ts | 74 + src/services/mock/mock.service.spec.ts | 23 + src/services/mock/mock.service.ts | 30 + .../muzzle/muzzle-utilities.spec.ts | 13 +- .../muzzle/muzzle-utilities.ts | 0 .../muzzle/muzzle.persistence.service.ts | 63 + src/services/muzzle/muzzle.service.spec.ts | 227 ++ src/services/muzzle/muzzle.service.ts | 341 ++ src/services/slack/slack.service.spec.ts | 149 + src/services/slack/slack.service.ts | 105 + src/services/web/web.service.spec.ts | 27 + src/services/web/web.service.ts | 58 + src/{db/Muzzle => shared/db}/models/Muzzle.ts | 0 src/utils/define/define-utils.ts | 61 - src/utils/mock/mock-utils.spec.ts | 20 - src/utils/mock/mock-utils.ts | 19 - src/utils/muzzle/muzzle.spec.ts | 159 - src/utils/muzzle/muzzle.ts | 342 -- src/utils/slack/slack-utils.spec.ts | 49 - src/utils/slack/slack-utils.ts | 88 - 32 files changed, 3935 insertions(+), 1244 deletions(-) create mode 100644 jest.config.js create mode 100644 src/controllers/define.controller.ts create mode 100644 src/controllers/mock.controller.ts create mode 100644 src/controllers/muzzle.controller.ts delete mode 100644 src/db/Muzzle/actions/muzzle-actions.ts delete mode 100644 src/routes/define-route.ts delete mode 100644 src/routes/mock-route.ts delete mode 100644 src/routes/muzzle-route.ts rename src/{utils/define/define-utils.spec.ts => services/define/define.service.spec.ts} (76%) create mode 100644 src/services/define/define.service.ts create mode 100644 src/services/mock/mock.service.spec.ts create mode 100644 src/services/mock/mock.service.ts rename src/{utils => services}/muzzle/muzzle-utilities.spec.ts (61%) rename src/{utils => services}/muzzle/muzzle-utilities.ts (100%) create mode 100644 src/services/muzzle/muzzle.persistence.service.ts create mode 100644 src/services/muzzle/muzzle.service.spec.ts create mode 100644 src/services/muzzle/muzzle.service.ts create mode 100644 src/services/slack/slack.service.spec.ts create mode 100644 src/services/slack/slack.service.ts create mode 100644 src/services/web/web.service.spec.ts create mode 100644 src/services/web/web.service.ts rename src/{db/Muzzle => shared/db}/models/Muzzle.ts (100%) delete mode 100644 src/utils/define/define-utils.ts delete mode 100644 src/utils/mock/mock-utils.spec.ts delete mode 100644 src/utils/mock/mock-utils.ts delete mode 100644 src/utils/muzzle/muzzle.spec.ts delete mode 100644 src/utils/muzzle/muzzle.ts delete mode 100644 src/utils/slack/slack-utils.spec.ts delete mode 100644 src/utils/slack/slack-utils.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..91a2d2c0 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index de03b84f..2067b834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,104 @@ "@babel/highlight": "^7.0.0" } }, + "@babel/core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.0.tgz", + "integrity": "sha512-6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helpers": "^7.5.0", + "@babel/parser": "^7.5.0", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz", + "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helpers": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.0.tgz", + "integrity": "sha512-EgCUEa8cNwuMrwo87l2d7i2oShi8m2Q58H7h3t4TWtqATZalJYFwfL9DulRe02f3KdqM9xmMCw3v/7Ll+EiaWg==", + "dev": true, + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0" + } + }, "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", @@ -24,6 +122,21 @@ "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz", + "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==", + "dev": true + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/runtime": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", @@ -33,6 +146,316 @@ "regenerator-runtime": "^0.13.2" } }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", + "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.0", + "@babel/types": "^7.5.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz", + "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@cnakazawa/watch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", + "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@jest/console": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", + "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", + "dev": true, + "requires": { + "@jest/source-map": "^24.3.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@jest/core": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.8.0.tgz", + "integrity": "sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.8.0", + "jest-config": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve-dependencies": "^24.8.0", + "jest-runner": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "jest-watcher": "^24.8.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "@jest/environment": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.8.0.tgz", + "integrity": "sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0" + } + }, + "@jest/fake-timers": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.8.0.tgz", + "integrity": "sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-mock": "^24.8.0" + } + }, + "@jest/reporters": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.8.0.tgz", + "integrity": "sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.1.1", + "jest-haste-map": "^24.8.0", + "jest-resolve": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.2.1", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", + "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", + "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/types": "^24.8.0", + "@types/istanbul-lib-coverage": "^2.0.0" + } + }, + "@jest/test-sequencer": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz", + "integrity": "sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==", + "dev": true, + "requires": { + "@jest/test-result": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-runner": "^24.8.0", + "jest-runtime": "^24.8.0" + } + }, + "@jest/transform": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.8.0.tgz", + "integrity": "sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.8.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-util": "^24.8.0", + "micromatch": "^3.1.10", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + } + }, + "@jest/types": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", + "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^12.0.9" + } + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -74,6 +497,47 @@ "p-retry": "^4.0.0" } }, + "@types/babel__core": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", + "integrity": "sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.0.2.tgz", + "integrity": "sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.7.tgz", + "integrity": "sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, "@types/body-parser": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", @@ -84,12 +548,6 @@ "@types/node": "*" } }, - "@types/chai": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", - "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", - "dev": true - }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -136,6 +594,46 @@ "@types/node": "*" } }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "24.0.15", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.15.tgz", + "integrity": "sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA==", + "dev": true, + "requires": { + "@types/jest-diff": "*" + } + }, + "@types/jest-diff": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", + "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", + "dev": true + }, "@types/lolex": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/lolex/-/lolex-3.1.1.tgz", @@ -148,12 +646,6 @@ "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", "dev": true }, - "@types/mocha": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", - "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", - "dev": true - }, "@types/node": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", @@ -191,6 +683,24 @@ "@types/mime": "*" } }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/yargs": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", + "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", + "dev": true + }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -206,21 +716,57 @@ "negotiator": "0.6.1" } }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", + "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", "dev": true, "requires": { - "string-width": "^2.0.0" + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz", + "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==", + "dev": true + } } }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + } + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -309,6 +855,12 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -341,10 +893,19 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, "assign-symbols": { @@ -353,12 +914,24 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -370,6 +943,18 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, "axios": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", @@ -386,6 +971,59 @@ } } }, + "babel-jest": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", + "integrity": "sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==", + "dev": true, + "requires": { + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.6.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "babel-plugin-istanbul": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz", + "integrity": "sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "babel-plugin-jest-hoist": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz", + "integrity": "sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz", + "integrity": "sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.6.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -451,6 +1089,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "bignumber.js": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", @@ -532,12 +1179,47 @@ } } }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", "dev": true }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.0.tgz", + "integrity": "sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", @@ -611,25 +1293,26 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, "capture-stack-trace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", "dev": true }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true }, "chalk": { "version": "2.4.2", @@ -641,12 +1324,6 @@ "supports-color": "^5.3.0" } }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "chokidar": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", @@ -780,6 +1457,12 @@ "wrap-ansi": "^2.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -857,6 +1540,15 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -916,6 +1608,54 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.3.0.tgz", + "integrity": "sha512-wXsoRfsRfsLVNaVzoKdqvEmK/5PFaEXNspVT22Ots6K/cnJdpoDKuQFw+qlMiXnmaif1OgeC466X1zISgAOcGg==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -947,21 +1687,18 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1041,12 +1778,33 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "diff-sequences": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", + "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -1067,6 +1825,16 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1140,6 +1908,34 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, "eslint-plugin-prettier": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", @@ -1155,6 +1951,12 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -1171,6 +1973,12 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "exec-sh": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", + "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==", + "dev": true + }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -1186,6 +1994,12 @@ "strip-eof": "^1.0.0" } }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1221,6 +2035,20 @@ } } }, + "expect": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", + "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-regex-util": "^24.3.0" + } + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -1265,6 +2093,12 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -1351,12 +2185,45 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, "fast-diff": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "^2.0.0" + } + }, "figlet": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.2.3.tgz", @@ -1430,23 +2297,6 @@ "locate-path": "^3.0.0" } }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - } - } - }, "fn-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", @@ -1477,6 +2327,12 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -2062,12 +2918,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "get-own-enumerable-property-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", @@ -2092,6 +2942,15 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -2135,6 +2994,12 @@ "ini": "^1.3.4" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -2181,12 +3046,48 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "dev": true }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2254,12 +3155,6 @@ } } }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, "highlight.js": { "version": "9.15.8", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz", @@ -2271,6 +3166,15 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -2282,6 +3186,17 @@ "statuses": ">= 1.4.0 < 2" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "husky": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/husky/-/husky-2.3.0.tgz", @@ -2389,7 +3304,28 @@ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", "dev": true }, - "imurmurhash": { + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", @@ -2421,6 +3357,15 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -2555,6 +3500,12 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -2695,12 +3646,24 @@ "has-symbols": "^1.0.0" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -2711,17 +3674,778 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "jest": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.8.0.tgz", + "integrity": "sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==", + "dev": true, + "requires": { + "import-local": "^2.0.0", + "jest-cli": "^24.8.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "jest-cli": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.8.0.tgz", + "integrity": "sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==", + "dev": true, + "requires": { + "@jest/core": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^12.0.2" + } + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-changed-files": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.8.0.tgz", + "integrity": "sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "jest-config": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.8.0.tgz", + "integrity": "sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.8.0", + "@jest/types": "^24.8.0", + "babel-jest": "^24.8.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.8.0", + "jest-environment-node": "^24.8.0", + "jest-get-type": "^24.8.0", + "jest-jasmine2": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.8.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", + "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.3.0", + "jest-get-type": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + }, + "jest-each": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", + "integrity": "sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.8.0", + "jest-util": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-environment-jsdom": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz", + "integrity": "sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/fake-timers": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-util": "^24.8.0", + "jsdom": "^11.5.1" + } + }, + "jest-environment-node": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.8.0.tgz", + "integrity": "sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==", + "dev": true, + "requires": { + "@jest/environment": "^24.8.0", + "@jest/fake-timers": "^24.8.0", + "@jest/types": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-util": "^24.8.0" + } + }, + "jest-get-type": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", + "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==", + "dev": true + }, + "jest-haste-map": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.8.1.tgz", + "integrity": "sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.4.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", + "integrity": "sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.8.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "pretty-format": "^24.8.0", + "throat": "^4.0.0" + } + }, + "jest-leak-detector": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz", + "integrity": "sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==", + "dev": true, + "requires": { + "pretty-format": "^24.8.0" + } + }, + "jest-matcher-utils": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", + "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.8.0", + "jest-get-type": "^24.8.0", + "pretty-format": "^24.8.0" + } + }, + "jest-message-util": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", + "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "jest-mock": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.8.0.tgz", + "integrity": "sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", + "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==", + "dev": true + }, + "jest-resolve": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.8.0.tgz", + "integrity": "sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz", + "integrity": "sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.8.0" + } + }, + "jest-runner": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.8.0.tgz", + "integrity": "sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.8.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.8.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.8.0", + "jest-jasmine2": "^24.8.0", + "jest-leak-detector": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-resolve": "^24.8.0", + "jest-runtime": "^24.8.0", + "jest-util": "^24.8.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + }, + "dependencies": { + "jest-docblock": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.3.0.tgz", + "integrity": "sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==", + "dev": true, + "requires": { + "detect-newline": "^2.1.0" + } + } + } + }, + "jest-runtime": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.8.0.tgz", + "integrity": "sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.8.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/yargs": "^12.0.2", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.8.0", + "jest-haste-map": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-mock": "^24.8.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.8.0", + "jest-snapshot": "^24.8.0", + "jest-util": "^24.8.0", + "jest-validate": "^24.8.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^12.0.2" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-serializer": { + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.4.0.tgz", + "integrity": "sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==", + "dev": true + }, + "jest-snapshot": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.8.0.tgz", + "integrity": "sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.8.0", + "chalk": "^2.0.1", + "expect": "^24.8.0", + "jest-diff": "^24.8.0", + "jest-matcher-utils": "^24.8.0", + "jest-message-util": "^24.8.0", + "jest-resolve": "^24.8.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.8.0", + "semver": "^5.5.0" + } + }, + "jest-util": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.8.0.tgz", + "integrity": "sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/fake-timers": "^24.8.0", + "@jest/source-map": "^24.3.0", + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "jest-validate": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.8.0.tgz", + "integrity": "sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "camelcase": "^5.0.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.8.0", + "leven": "^2.1.0", + "pretty-format": "^24.8.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.8.0.tgz", + "integrity": "sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==", + "dev": true, + "requires": { + "@jest/test-result": "^24.8.0", + "@jest/types": "^24.8.0", + "@types/yargs": "^12.0.9", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.8.0", + "string-length": "^2.0.0" + } }, - "jest-docblock": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", - "dev": true + "jest-worker": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", + "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", + "dev": true, + "requires": { + "merge-stream": "^1.0.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } }, "js-tokens": { "version": "4.0.0", @@ -2738,18 +4462,109 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -2767,6 +4582,28 @@ "invert-kv": "^2.0.0" } }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -2981,6 +4818,18 @@ } } }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -2996,6 +4845,12 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -3028,11 +4883,14 @@ } } }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", - "dev": true + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } }, "lowercase-keys": { "version": "1.0.1", @@ -3065,6 +4923,15 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -3117,6 +4984,15 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3216,63 +5092,6 @@ } } }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3325,24 +5144,51 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", + "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", "dev": true, "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" } }, "nodemon": { @@ -3440,6 +5286,18 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwsapi": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3491,18 +5349,6 @@ "isobject": "^3.0.0" } }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -3555,6 +5401,46 @@ } } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, "os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", @@ -3606,6 +5492,15 @@ "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -3643,6 +5538,12 @@ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==" }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, "p-retry": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.1.0.tgz", @@ -3738,10 +5639,19 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, "pify": { @@ -3765,6 +5675,15 @@ "pinkie": "^2.0.0" } }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -3812,12 +5731,24 @@ "semver-compare": "^1.0.0" } }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -3830,11 +5761,41 @@ "integrity": "sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==", "dev": true }, + "pretty-format": { + "version": "24.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", + "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", + "dev": true, + "requires": { + "@jest/types": "^24.8.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, + "prompts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", + "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", + "dev": true, + "requires": { + "kleur": "^3.0.2", + "sisteransi": "^1.0.0" + } + }, "property-expr": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz", @@ -3856,6 +5817,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", + "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", + "dev": true + }, "pstree.remy": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.6.tgz", @@ -3871,6 +5838,12 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -3904,6 +5877,12 @@ "strip-json-comments": "~2.0.1" } }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", + "dev": true + }, "read-pkg": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.1.1.tgz", @@ -3916,6 +5895,29 @@ "type-fest": "^0.4.1" } }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + } + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -3941,6 +5943,15 @@ "readable-stream": "^2.0.2" } }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -3999,6 +6010,72 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "dev": true, + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4018,6 +6095,15 @@ "path-parse": "^1.0.6" } }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", @@ -4060,6 +6146,12 @@ "glob": "^7.1.3" } }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, "run-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", @@ -4094,6 +6186,62 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -4203,6 +6351,12 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -4234,6 +6388,12 @@ } } }, + "sisteransi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.2.tgz", + "integrity": "sha512-ZcYcZcT69nSLAR2oLN2JwNmLkJEKGooFMCdvOkFrToUt/WfcRWqhIg4P4KwY4dmLbuyXIx4o4YmPsvMRJYJd/w==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4447,6 +6607,29 @@ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, "staged-git-files": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.1.2.tgz", @@ -4479,12 +6662,28 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, "string-argv": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz", "integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=", "dev": true }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -4521,6 +6720,12 @@ "ansi-regex": "^3.0.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -4546,6 +6751,12 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "synchronous-promise": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.8.tgz", @@ -4561,6 +6772,18 @@ "execa": "^0.7.0" } }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, "thenify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", @@ -4577,12 +6800,30 @@ "thenify": ">= 3.1.0 < 4" } }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -4640,6 +6881,59 @@ "nopt": "~1.0.10" } }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "ts-jest": { + "version": "24.0.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.0.2.tgz", + "integrity": "sha512-h6ZCZiA1EQgjczxq+uGLXQlNgeg02WWJBbeT8j6nyIBRQdglqbvzDoHahTEIiS6Eor6x8mK6PfZ7brQ9Q6tzHw==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" + }, + "dependencies": { + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, "ts-node": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.1.0.tgz", @@ -4705,12 +6999,30 @@ "tslib": "^1.8.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-fest": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", @@ -4765,7 +7077,28 @@ "typescript": { "version": "3.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==" + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "dev": true + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } }, "undefsafe": { "version": "2.0.2", @@ -4895,6 +7228,15 @@ "xdg-basedir": "^3.0.0" } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -4921,11 +7263,27 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -4941,6 +7299,78 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4954,15 +7384,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "widest-line": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", @@ -4972,6 +7393,12 @@ "string-width": "^2.1.1" } }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -5030,12 +7457,27 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", @@ -5167,67 +7609,6 @@ } } }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "yn": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", diff --git a/package.json b/package.json index e5e2aa6d..4b450612 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "start": "npm run start:dev", "start:prod": "node ./dist/server.js", "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", - "test": "mocha -r ts-node/register ./src/**/*.spec.ts", - "test:watch": "mocha -r ts-node/register ./src/**/*.spec.ts --watch", + "test": "jest", + "test:watch": "jest --watch", "tsc": "tsc" }, "author": "", @@ -24,26 +24,24 @@ "express": "^4.16.4", "mysql": "^2.17.1", "reflect-metadata": "^0.1.13", - "typeorm": "^0.2.18", - "typescript": "^3.4.5" + "typeorm": "^0.2.18" }, "devDependencies": { - "@types/chai": "^4.1.7", "@types/express": "^4.16.1", + "@types/jest": "^24.0.15", "@types/lolex": "^3.1.1", - "@types/mocha": "^5.2.6", "@types/node": "^12.0.2", - "chai": "^4.2.0", "husky": "^2.3.0", + "jest": "^24.8.0", "lint-staged": "^8.1.7", - "lolex": "^4.1.0", - "mocha": "^6.1.4", "nodemon": "^1.19.0", "prettier": "1.17.1", + "ts-jest": "^24.0.2", "ts-node": "^8.1.0", "tslint": "^5.16.0", "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.0.1" + "tslint-plugin-prettier": "^2.0.1", + "typescript": "^3.4.5" }, "husky": { "hooks": { diff --git a/src/controllers/define.controller.ts b/src/controllers/define.controller.ts new file mode 100644 index 00000000..ca9cba2c --- /dev/null +++ b/src/controllers/define.controller.ts @@ -0,0 +1,36 @@ +import express, { Request, Response, Router } from "express"; +import { DefineService } from "../services/define/define.service"; +import { IUrbanDictionaryResponse } from "../shared/models/define/define-models"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { SlackService } from "../services/slack/slack.service"; + +export const defineController: Router = express.Router(); +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const defineService = DefineService.getInstance(); + +defineController.post("/define", async (req: Request, res: Response) => { + const request: ISlashCommandRequest = req.body; + + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + const defined: IUrbanDictionaryResponse = (await defineService + .define(request.text) + .catch(e => { + res.send(`Error: ${e.message}`); + })) as IUrbanDictionaryResponse; + const response: IChannelResponse = { + response_type: "in_channel", + text: `*${defineService.capitalizeFirstLetter(request.text)}*`, + attachments: defineService.formatDefs(defined.list) + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); diff --git a/src/controllers/mock.controller.ts b/src/controllers/mock.controller.ts new file mode 100644 index 00000000..7ca3bf3e --- /dev/null +++ b/src/controllers/mock.controller.ts @@ -0,0 +1,34 @@ +import express, { Router } from "express"; +import { MockService } from "../services/mock/mock.service"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { SlackService } from "../services/slack/slack.service"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const mockController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const mockService = MockService.getInstance(); + +mockController.post("/mock", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + const mocked: string = mockService.mock(request.text); + const response: IChannelResponse = { + attachments: [ + { + text: mocked + } + ], + response_type: "in_channel", + text: `<@${request.user_id}>` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts new file mode 100644 index 00000000..f058e9a8 --- /dev/null +++ b/src/controllers/muzzle.controller.ts @@ -0,0 +1,82 @@ +import express, { Request, Response, Router } from "express"; +import { getTimeString } from "../services/muzzle/muzzle-utilities"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { SlackService } from "../services/slack/slack.service"; +import { WebService } from "../services/web/web.service"; +import { + IEventRequest, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const muzzleController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const webService = WebService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + +muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { + const request: IEventRequest = req.body; + if ( + muzzleService.isUserMuzzled(request.event.user) && + !slackService.containsTag(request.event.text) + ) { + console.log( + `${slackService.getUserName(request.event.user)} | ${ + request.event.user + } is muzzled! Suppressing his voice...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); + muzzleService.sendMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text + ); + } else if ( + muzzleService.isUserMuzzled(request.event.user) && + slackService.containsTag(request.event.text) + ) { + const muzzleId = muzzleService.getMuzzleId(request.event.user); + console.log( + `${slackService.getUserName( + request.event.user + )} atttempted to tag someone. Muzzle increased by ${ + muzzleService.ABUSE_PENALTY_TIME + }!` + ); + muzzleService.addMuzzleTime( + request.event.user, + muzzleService.ABUSE_PENALTY_TIME + ); + webService.deleteMessage(request.event.channel, request.event.ts); + muzzlePersistenceService.trackDeletedMessage(muzzleId, request.event.text); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + muzzleService.ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } else if (muzzleService.shouldBotMessageBeMuzzled(request)) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); + } + res.send({ challenge: request.challenge }); +}); + +muzzleController.post("/muzzle", async (req: Request, res: Response) => { + const request: ISlashCommandRequest = req.body; + const userId: any = slackService.getUserId(request.text); + const results = await muzzleService + .addUserToMuzzled(userId, request.user_id) + .catch(e => { + res.send(e); + }); + if (results) { + res.send(results); + } +}); diff --git a/src/db/Muzzle/actions/muzzle-actions.ts b/src/db/Muzzle/actions/muzzle-actions.ts deleted file mode 100644 index 3fe4cb00..00000000 --- a/src/db/Muzzle/actions/muzzle-actions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getRepository } from "typeorm"; -import { Muzzle } from "../models/Muzzle"; - -export function addMuzzleToDb( - requestorId: string, - muzzledId: string, - time: number -) { - const muzzle = new Muzzle(); - muzzle.requestorId = requestorId; - muzzle.muzzledId = muzzledId; - muzzle.messagesSuppressed = 0; - muzzle.wordsSuppressed = 0; - muzzle.charactersSuppressed = 0; - muzzle.milliseconds = time; - return getRepository(Muzzle).save(muzzle); -} - -export function incrementMuzzleTime(id: number, ms: number) { - return getRepository(Muzzle).increment({ id }, "milliseconds", ms); -} - -export function incrementMessageSuppressions(id: number) { - return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); -} - -export function incrementWordSuppressions(id: number, suppressions: number) { - return getRepository(Muzzle).increment( - { id }, - "wordsSuppressed", - suppressions - ); -} - -export function incrementCharacterSuppressions( - id: number, - charactersSuppressed: number -) { - return getRepository(Muzzle).increment( - { id }, - "charactersSuppressed", - charactersSuppressed - ); -} - -/** - * Determines suppression counts for messages that are ONLY deleted and not muzzled. - * Used when a muzzled user has hit their max suppressions or when they have tagged channel. - */ -export function trackDeletedMessage(muzzleId: number, text: string) { - const words = text.split(" ").length; - const characters = text.split("").length; - incrementMessageSuppressions(muzzleId); - incrementWordSuppressions(muzzleId, words); - incrementCharacterSuppressions(muzzleId, characters); -} diff --git a/src/index.ts b/src/index.ts index 6ed55abf..cbf2db42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,25 +2,27 @@ import bodyParser from "body-parser"; import express, { Application } from "express"; import "reflect-metadata"; import { createConnection } from "typeorm"; +import { defineController } from "./controllers/define.controller"; +import { mockController } from "./controllers/mock.controller"; +import { muzzleController } from "./controllers/muzzle.controller"; import { config } from "./ormconfig"; -import { defineRoutes } from "./routes/define-route"; -import { mockRoutes } from "./routes/mock-route"; -import { muzzleRoutes } from "./routes/muzzle-route"; -import { getAllUsers } from "./utils/slack/slack-utils"; +import { SlackService } from "./services/slack/slack.service"; const app: Application = express(); const PORT: number = 3000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); -app.use(mockRoutes); -app.use(muzzleRoutes); -app.use(defineRoutes); +app.use(mockController); +app.use(muzzleController); +app.use(defineController); + +const slackService = SlackService.getInstance(); createConnection(config) .then(connection => { if (connection.isConnected) { - getAllUsers(); + slackService.getAllUsers(); console.log(`Connected to MySQL DB: ${config.database}`); } else { throw Error("Unable to connect to database"); diff --git a/src/routes/define-route.ts b/src/routes/define-route.ts deleted file mode 100644 index 8ba36f12..00000000 --- a/src/routes/define-route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import express, { Request, Response, Router } from "express"; -import { IUrbanDictionaryResponse } from "../shared/models/define/define-models"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; -import { - capitalizeFirstLetter, - define, - formatDefs -} from "../utils/define/define-utils"; -import { isUserMuzzled } from "../utils/muzzle/muzzle"; -import { sendResponse } from "../utils/slack/slack-utils"; - -export const defineRoutes: Router = express.Router(); - -defineRoutes.post("/define", async (req: Request, res: Response) => { - const request: ISlashCommandRequest = req.body; - - if (isUserMuzzled(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); - } else { - try { - const defined: IUrbanDictionaryResponse = await define(request.text); - const response: IChannelResponse = { - response_type: "in_channel", - text: `*${capitalizeFirstLetter(request.text)}*`, - attachments: formatDefs(defined.list) - }; - sendResponse(request.response_url, response); - res.status(200).send(); - } catch (e) { - res.send(`error: ${e.message}`); - } - } -}); diff --git a/src/routes/mock-route.ts b/src/routes/mock-route.ts deleted file mode 100644 index f5273a8b..00000000 --- a/src/routes/mock-route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import express, { Router } from "express"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; -import { mock } from "../utils/mock/mock-utils"; -import { isUserMuzzled } from "../utils/muzzle/muzzle"; -import { sendResponse } from "../utils/slack/slack-utils"; - -export const mockRoutes: Router = express.Router(); - -mockRoutes.post("/mock", (req, res) => { - const request: ISlashCommandRequest = req.body; - if (isUserMuzzled(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); - } else { - const mocked: string = mock(request.text); - const response: IChannelResponse = { - attachments: [ - { - text: mocked - } - ], - response_type: "in_channel", - text: `<@${request.user_id}>` - }; - sendResponse(request.response_url, response); - res.status(200).send(); - } -}); diff --git a/src/routes/muzzle-route.ts b/src/routes/muzzle-route.ts deleted file mode 100644 index 0c0c7f73..00000000 --- a/src/routes/muzzle-route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import express, { Request, Response, Router } from "express"; -import { trackDeletedMessage } from "../db/Muzzle/actions/muzzle-actions"; -import { - IEventRequest, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; -import { - ABUSE_PENALTY_TIME, - addMuzzleTime, - addUserToMuzzled, - deleteMessage, - getMuzzleId, - isUserMuzzled, - sendMessage, - sendMuzzledMessage, - shouldBotMessageBeMuzzled -} from "../utils/muzzle/muzzle"; -import { getTimeString } from "../utils/muzzle/muzzle-utilities"; -import { - containsTag, - getUserId, - getUserName -} from "../utils/slack/slack-utils"; - -export const muzzleRoutes: Router = express.Router(); - -muzzleRoutes.post("/muzzle/handle", (req: Request, res: Response) => { - const request: IEventRequest = req.body; - if (isUserMuzzled(request.event.user) && !containsTag(request.event.text)) { - console.log( - `${getUserName(request.event.user)} | ${ - request.event.user - } is muzzled! Suppressing his voice...` - ); - deleteMessage(request.event.channel, request.event.ts); - sendMuzzledMessage( - request.event.channel, - request.event.user, - request.event.text - ); - } else if ( - isUserMuzzled(request.event.user) && - containsTag(request.event.text) - ) { - const muzzleId = getMuzzleId(request.event.user); - console.log( - `${getUserName( - request.event.user - )} atttempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - addMuzzleTime(request.event.user, ABUSE_PENALTY_TIME); - deleteMessage(request.event.channel, request.event.ts); - trackDeletedMessage(muzzleId, request.event.text); - sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` - ); - } else if (shouldBotMessageBeMuzzled(request)) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - deleteMessage(request.event.channel, request.event.ts); - } - res.send({ challenge: request.challenge }); -}); - -muzzleRoutes.post("/muzzle", async (req: Request, res: Response) => { - const request: ISlashCommandRequest = req.body; - const userId: any = getUserId(request.text); - const results = await addUserToMuzzled(userId, request.user_id).catch(e => { - res.send(e); - }); - if (results) { - res.send(results); - } -}); diff --git a/src/utils/define/define-utils.spec.ts b/src/services/define/define.service.spec.ts similarity index 76% rename from src/utils/define/define-utils.spec.ts rename to src/services/define/define.service.spec.ts index f97d5033..5d980e56 100644 --- a/src/utils/define/define-utils.spec.ts +++ b/src/services/define/define.service.spec.ts @@ -1,46 +1,44 @@ -import { expect } from "chai"; import { IDefinition } from "../../shared/models/define/define-models"; -import { - capitalizeFirstLetter, - define, - formatDefs, - formatUrbanD -} from "./define-utils"; +import { DefineService } from "./define.service"; describe("define-utils", () => { + let defineService: DefineService; + + beforeEach(() => { + defineService = DefineService.getInstance(); + }); + describe("capitalizeFirstLetter()", () => { it("should capitalize the first letter of a given string", () => { - expect(capitalizeFirstLetter("test string")).to.equal("Test string"); + expect(defineService.capitalizeFirstLetter("test string")).toBe( + "Test string" + ); }); }); describe("define()", () => { it("should return a promise when attempting to define", () => { - expect(define("test")).to.be.a("Promise"); + expect(defineService.define("test")).toBeDefined(); // Firm this up }); }); describe("formatDefs()", () => { it("should return an array of 3 length when no maxDefs parameter is provided", () => { - expect(formatDefs(testArray).length).to.equal(3); + expect(defineService.formatDefs(testArray).length).toBe(3); }); it("should return an array of 4 length when a maxDefs parameter of 4 is provided", () => { - expect(formatDefs(testArray, 4).length).to.equal(4); + expect(defineService.formatDefs(testArray, 4).length).toBe(4); }); it("should return testArray.length if maxDefs parameter is larger than testArray.length", () => { - expect(formatDefs(testArray, 10).length).to.equal(5); + expect(defineService.formatDefs(testArray, 10).length).toBe(5); }); it(`should return [{ "Sorry, no definitions found" }] if defArr === 0`, () => { - expect(formatDefs([])[0].text).to.equal("Sorry, no definitions found."); - }); - }); - - describe("formatUrbanD()", () => { - it("should return a formatted urbandictionary string", () => { - expect(formatUrbanD("A [way] to [test]")).to.equal("A way to test"); + expect(defineService.formatDefs([])[0].text).toBe( + "Sorry, no definitions found." + ); }); }); }); diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts new file mode 100644 index 00000000..5e8745bb --- /dev/null +++ b/src/services/define/define.service.ts @@ -0,0 +1,74 @@ +import Axios, { AxiosResponse } from "axios"; +import { + IDefinition, + IUrbanDictionaryResponse +} from "../../shared/models/define/define-models"; +import { IAttachment } from "../../shared/models/slack/slack-models"; + +export class DefineService { + public static getInstance() { + if (!DefineService.instance) { + DefineService.instance = new DefineService(); + } + return DefineService.instance; + } + + private static instance: DefineService; + + private constructor() {} + + /** + * Capitalizes the first letter of a given sentence. + */ + public capitalizeFirstLetter(sentence: string): string { + return `${sentence.charAt(0).toUpperCase()}${sentence.slice(1)}`; + } + + /** + * Returns a promise to look up a definition on urban dictionary. + */ + public define(word: string): Promise { + return Axios.get(`http://api.urbandictionary.com/v0/define?term=${word}`) + .then((res: AxiosResponse) => { + return res.data; + }) + .catch(e => { + console.log("error", e); + return e; + }); + } + + /** + * Takes in an array of definitions and breaks them down into a shortened list depending on maxDefs + */ + public formatDefs(defArr: IDefinition[], maxDefs = 3) { + if (!defArr || defArr.length === 0) { + return [{ text: "Sorry, no definitions found." }]; + } + + const formattedArr: IAttachment[] = []; + const maxDefinitions: number = + defArr.length <= maxDefs ? defArr.length : maxDefs; + + for (let i = 0; i < maxDefinitions; i++) { + formattedArr.push({ + text: this.formatUrbanD( + `${i + 1}. ${this.capitalizeFirstLetter(defArr[i].definition)}` + ) + }); + } + return formattedArr; + } + /** + * Takes in a definition and removes brackets. + */ + private formatUrbanD(definition: string): string { + let formattedDefinition: string = ""; + for (const letter of definition) { + if (letter !== "[" && letter !== "]") { + formattedDefinition += letter; + } + } + return formattedDefinition; + } +} diff --git a/src/services/mock/mock.service.spec.ts b/src/services/mock/mock.service.spec.ts new file mode 100644 index 00000000..bfe97640 --- /dev/null +++ b/src/services/mock/mock.service.spec.ts @@ -0,0 +1,23 @@ +import { MockService } from "./mock.service"; + +describe("MockService", () => { + let mockService: MockService; + beforeEach(() => { + mockService = MockService.getInstance(); + }); + describe("mock()", () => { + it("should mock a users input (single word)", () => { + expect(mockService.mock("test")).toBe("tEsT"); + }); + + it("should mock a users input (sentence)", () => { + expect(mockService.mock("test sentence with multiple words.")).toBe( + "tEsT sEnTeNcE wItH mUlTiPlE wOrDs." + ); + }); + + it("should return input if it is an empty string", () => { + expect(mockService.mock("")).toBe(""); + }); + }); +}); diff --git a/src/services/mock/mock.service.ts b/src/services/mock/mock.service.ts new file mode 100644 index 00000000..b9f7c7eb --- /dev/null +++ b/src/services/mock/mock.service.ts @@ -0,0 +1,30 @@ +export class MockService { + public static getInstance() { + if (!MockService.instance) { + MockService.instance = new MockService(); + } + return MockService.instance; + } + private static instance: MockService; + private constructor() {} + + public mock(input: string): string { + let output = ""; + if (!input || input.length === 0) { + return input; + } else { + let shouldChangeCase = true; + for (const letter of input) { + if (letter === " ") { + output += letter; + } else { + output += shouldChangeCase + ? letter.toLowerCase() + : letter.toUpperCase(); + shouldChangeCase = !shouldChangeCase; + } + } + return output; + } + } +} diff --git a/src/utils/muzzle/muzzle-utilities.spec.ts b/src/services/muzzle/muzzle-utilities.spec.ts similarity index 61% rename from src/utils/muzzle/muzzle-utilities.spec.ts rename to src/services/muzzle/muzzle-utilities.spec.ts index 4bf0bfba..cad35977 100644 --- a/src/utils/muzzle/muzzle-utilities.spec.ts +++ b/src/services/muzzle/muzzle-utilities.spec.ts @@ -1,29 +1,28 @@ -import { expect } from "chai"; import { getTimeString, getTimeToMuzzle } from "./muzzle-utilities"; describe("muzzle-utilities", () => { describe("getTimeToMuzzle()", () => { it("should return a value greater than 0 and less than 180000", () => { - expect(getTimeToMuzzle()).to.be.greaterThan(0); - expect(getTimeToMuzzle()).to.be.lessThan(180000); + expect(getTimeToMuzzle()).toBeGreaterThan(0); + expect(getTimeToMuzzle()).toBeLessThan(180000); }); }); describe("getTimeString()", () => { it("should return 1m30s when 90000ms are passed in", () => { - expect(getTimeString(90000)).to.equal("1m30s"); + expect(getTimeString(90000)).toBe("1m30s"); }); it("should return 2m00s when 120000ms is passed in", () => { - expect(getTimeString(120000)).to.equal("2m00s"); + expect(getTimeString(120000)).toBe("2m00s"); }); it("should return 2m00s when 120000.123 is passed in", () => { - expect(getTimeString(120000.123)).to.equal("2m00s"); + expect(getTimeString(120000.123)).toBe("2m00s"); }); it("should return 2m00s when 120000.999 is passed in", () => { - expect(getTimeString(120000.999)).to.equal("2m00s"); + expect(getTimeString(120000.999)).toBe("2m00s"); }); }); }); diff --git a/src/utils/muzzle/muzzle-utilities.ts b/src/services/muzzle/muzzle-utilities.ts similarity index 100% rename from src/utils/muzzle/muzzle-utilities.ts rename to src/services/muzzle/muzzle-utilities.ts diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts new file mode 100644 index 00000000..9313decf --- /dev/null +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -0,0 +1,63 @@ +import { getRepository } from "typeorm"; +import { Muzzle } from "../../shared/db/models/Muzzle"; + +export class MuzzlePersistenceService { + public static getInstance() { + if (!MuzzlePersistenceService.instance) { + MuzzlePersistenceService.instance = new MuzzlePersistenceService(); + } + return MuzzlePersistenceService.instance; + } + + private static instance: MuzzlePersistenceService; + private constructor() {} + + public addMuzzleToDb(requestorId: string, muzzledId: string, time: number) { + const muzzle = new Muzzle(); + muzzle.requestorId = requestorId; + muzzle.muzzledId = muzzledId; + muzzle.messagesSuppressed = 0; + muzzle.wordsSuppressed = 0; + muzzle.charactersSuppressed = 0; + muzzle.milliseconds = time; + return getRepository(Muzzle).save(muzzle); + } + + public incrementMuzzleTime(id: number, ms: number) { + return getRepository(Muzzle).increment({ id }, "milliseconds", ms); + } + + public incrementMessageSuppressions(id: number) { + return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); + } + + public incrementWordSuppressions(id: number, suppressions: number) { + return getRepository(Muzzle).increment( + { id }, + "wordsSuppressed", + suppressions + ); + } + + public incrementCharacterSuppressions( + id: number, + charactersSuppressed: number + ) { + return getRepository(Muzzle).increment( + { id }, + "charactersSuppressed", + charactersSuppressed + ); + } + /** + * Determines suppression counts for messages that are ONLY deleted and not muzzled. + * Used when a muzzled user has hit their max suppressions or when they have tagged channel. + */ + public trackDeletedMessage(muzzleId: number, text: string) { + const words = text.split(" ").length; + const characters = text.split("").length; + this.incrementMessageSuppressions(muzzleId); + this.incrementWordSuppressions(muzzleId, words); + this.incrementCharacterSuppressions(muzzleId, characters); + } +} diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts new file mode 100644 index 00000000..168e7167 --- /dev/null +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -0,0 +1,227 @@ +import { UpdateResult } from "typeorm"; +import { Muzzle } from "../../shared/db/models/Muzzle"; +import { ISlackUser } from "../../shared/models/slack/slack-models"; +import { SlackService } from "../slack/slack.service"; +import { MuzzlePersistenceService } from "./muzzle.persistence.service"; +import { MuzzleService } from "./muzzle.service"; + +describe("MuzzleService", () => { + const testData = { + user: "123", + user2: "456", + user3: "789", + requestor: "666" + }; + + let muzzleInstance: MuzzleService; + let slackInstance: SlackService; + + beforeEach(() => { + muzzleInstance = MuzzleService.getInstance(); + slackInstance = SlackService.getInstance(); + slackInstance.userList = [ + { id: "123", name: "test123" }, + { id: "456", name: "test456" }, + { id: "789", name: "test789" }, + { id: "666", name: "requestor" } + ] as ISlackUser[]; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runAllTimers(); + }); + + describe("addUserToMuzzled()", () => { + describe("muzzled", () => { + describe("positive path", () => { + beforeEach(() => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + }); + + it("should add a user to the muzzled map", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); + }); + + it("should return an added user with IMuzzled attributes", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect( + muzzleInstance.getMuzzledUserById(testData.user)!.suppressionCount + ).toBe(0); + expect( + muzzleInstance.getMuzzledUserById(testData.user)!.muzzledBy + ).toBe(testData.requestor); + expect(muzzleInstance.getMuzzledUserById(testData.user)!.id).toBe(1); + expect( + muzzleInstance.getMuzzledUserById(testData.user)!.removalFn + ).toBeDefined(); + }); + }); + + describe("negative path", () => { + it("should reject if a user tries to muzzle an already muzzled user", async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); + await muzzleInstance + .addUserToMuzzled(testData.user, testData.requestor) + .catch(e => { + expect(e).toBe("test123 is already muzzled!"); + }); + }); + + it("should reject if a user tries to muzzle a user that does not exist", async () => { + await muzzleInstance + .addUserToMuzzled("", testData.requestor) + .catch(e => { + expect(e).toBe( + `Invalid username passed in. You can only muzzle existing slack users` + ); + expect(muzzleInstance.isUserMuzzled("")).toBe(false); + }); + }); + + it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); + await muzzleInstance + .addUserToMuzzled(testData.requestor, testData.user) + .catch(e => { + expect(e).toBe( + `You can't muzzle someone if you are already muzzled!` + ); + }); + }); + }); + }); + + describe("requestors", () => { + describe("positive path", () => { + beforeEach(() => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + }); + it("should add a user to the requestors map", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); + }); + + it("should return an added user with IMuzzler attributes", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + expect( + muzzleInstance.getRequestorById(testData.requestor)!.muzzleCount + ).toBe(1); + }); + + it("should increment a requestors muzzle count on a second muzzleInstance.addUserToMuzzled() call", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + await muzzleInstance.addUserToMuzzled( + testData.user2, + testData.requestor + ); + expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); + expect( + muzzleInstance.getRequestorById(testData.requestor)!.muzzleCount + ).toBe(2); + }); + }); + + describe("negative path", () => { + beforeEach(() => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValueOnce(mockMuzzle as Muzzle); + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValueOnce(mockMuzzle as Muzzle); + }); + it("should prevent a requestor from muzzling on their third count", async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + await muzzleInstance.addUserToMuzzled( + testData.user2, + testData.requestor + ); + await muzzleInstance + .addUserToMuzzled(testData.user3, testData.requestor) + .catch(e => + expect(e).toBe( + `You're doing that too much. Only 2 muzzles are allowed per hour.` + ) + ); + }); + }); + }); + }); + + describe("muzzle()", () => { + beforeEach(() => { + const mockResolve = { raw: "whatever" }; + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementMessageSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementCharacterSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementWordSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + }); + it("should always muzzle a tagged user", () => { + const testSentence = + "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; + expect(muzzleInstance.muzzle(testSentence, 1)).toBe( + " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " + ); + }); + + it("should always muzzle ", () => { + const testSentence = " hey guys"; + expect( + muzzleInstance.muzzle(testSentence, 1).includes("") + ).toBe(false); + }); + }); +}); diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts new file mode 100644 index 00000000..36cde3c0 --- /dev/null +++ b/src/services/muzzle/muzzle.service.ts @@ -0,0 +1,341 @@ +import { IMuzzled, IRequestor } from "../../shared/models/muzzle/muzzle-models"; +import { IEventRequest } from "../../shared/models/slack/slack-models"; +import { SlackService } from "../slack/slack.service"; +import { WebService } from "../web/web.service"; +import { + getRemainingTime, + getTimeString, + getTimeToMuzzle, + isRandomEven +} from "./muzzle-utilities"; +import { MuzzlePersistenceService } from "./muzzle.persistence.service"; + +export class MuzzleService { + public static getInstance() { + if (!MuzzleService.instance) { + MuzzleService.instance = new MuzzleService(); + } + return MuzzleService.instance; + } + + private static instance: MuzzleService; + public ABUSE_PENALTY_TIME = 300000; + private webService = WebService.getInstance(); + private slackService = SlackService.getInstance(); + private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + private MAX_MUZZLE_TIME = 3600000; + private MAX_TIME_BETWEEN_MUZZLES = 3600000; + private MAX_SUPPRESSIONS = 7; + private MAX_MUZZLES = 2; + private muzzled: Map = new Map(); + private requestors: Map = new Map(); + + private constructor() {} + /** + * Takes in text and randomly muzzles certain words. + */ + public muzzle(text: string, muzzleId: number) { + const replacementText = " ..mMm.. "; + let returnText = ""; + const words = text.split(" "); + let wordsSuppressed = 0; + let charactersSuppressed = 0; + let replacementWord; + for (const word of words) { + replacementWord = + isRandomEven() && !this.slackService.containsTag(word) + ? ` *${word}* ` + : replacementText; + if (replacementWord === replacementText) { + wordsSuppressed++; + charactersSuppressed += word.length; + } + returnText += replacementWord; + } + this.muzzlePersistenceService.incrementMessageSuppressions(muzzleId); + this.muzzlePersistenceService.incrementCharacterSuppressions( + muzzleId, + charactersSuppressed + ); + this.muzzlePersistenceService.incrementWordSuppressions( + muzzleId, + wordsSuppressed + ); + return returnText; + } + /** + * Adds the specified amount of time to a specified muzzled user. + */ + public addMuzzleTime(userId: string, timeToAdd: number) { + if (userId && this.muzzled.has(userId)) { + const removalFn = this.muzzled.get(userId)!.removalFn; + const newTime = getRemainingTime(removalFn) + timeToAdd; + const muzzleId = this.muzzled.get(userId)!.id; + this.muzzlePersistenceService.incrementMuzzleTime( + muzzleId, + this.ABUSE_PENALTY_TIME + ); + clearTimeout(this.muzzled.get(userId)!.removalFn); + console.log( + `Setting ${this.slackService.getUserName( + userId + )}'s muzzle time to ${newTime}` + ); + this.muzzled.set(userId, { + suppressionCount: this.muzzled.get(userId)!.suppressionCount, + muzzledBy: this.muzzled.get(userId)!.muzzledBy, + id: this.muzzled.get(userId)!.id, + removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) + }); + } + } + /** + * Gets the corresponding database ID for the user's current muzzle. + */ + public getMuzzleId(userId: string) { + return this.muzzled.get(userId)!.id; + } + + /** + * Retrieves the specified user from the muzzled map by slack id. + */ + public getMuzzledUserById(slackId: string) { + return this.muzzled.get(slackId); + } + + /** + * Retrieves the specified user from the requestors map by slack id. + */ + public getRequestorById(slackId: string) { + return this.requestors.get(slackId); + } + + /** + * Returns boolean whether user is muzzled or not. + */ + public isUserMuzzled(userId: string) { + return this.muzzled.has(userId); + } + + /** + * Returns boolean whether user is a requestor or not. + */ + public isUserRequestor(userId: string) { + return this.requestors.has(userId); + } + /** + * Determines whether or not a bot message should be removed. + */ + public shouldBotMessageBeMuzzled(request: IEventRequest) { + let userIdByEventText; + let userIdByAttachmentText; + let userIdByAttachmentPretext; + let userIdByCallbackId; + + if (request.event.text) { + userIdByEventText = this.slackService.getUserId(request.event.text); + } else if (request.event.attachments && request.event.attachments.length) { + userIdByAttachmentText = this.slackService.getUserId( + request.event.attachments[0].text + ); + userIdByAttachmentPretext = this.slackService.getUserId( + request.event.attachments[0].pretext + ); + + if (request.event.attachments[0].callback_id) { + userIdByCallbackId = this.slackService.getUserIdByCallbackId( + request.event.attachments[0].callback_id + ); + } + } + + const finalUserId = this.slackService.getBotId( + userIdByEventText, + userIdByAttachmentText, + userIdByAttachmentPretext, + userIdByCallbackId + ); + + return ( + request.event.subtype === "bot_message" && + request.event.attachments && + finalUserId && + this.isUserMuzzled(finalUserId) && + request.event.username !== "muzzle" + ); + } + + /** + * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes + */ + public addUserToMuzzled(userId: string, requestorId: string) { + const userName = this.slackService.getUserName(userId); + const requestorName = this.slackService.getUserName(requestorId); + return new Promise(async (resolve, reject) => { + if (!userId) { + reject( + `Invalid username passed in. You can only muzzle existing slack users` + ); + } else if (this.isUserMuzzled(userId)) { + console.error( + `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` + ); + reject(`${userName} is already muzzled!`); + } else if (this.isUserMuzzled(requestorId)) { + console.error( + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled` + ); + reject(`You can't muzzle someone if you are already muzzled!`); + } else if (this.isMaxMuzzlesReached(requestorId)) { + console.error( + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${ + this.MAX_MUZZLES + }` + ); + reject( + `You're doing that too much. Only ${ + this.MAX_MUZZLES + } muzzles are allowed per hour.` + ); + } else { + const timeToMuzzle = getTimeToMuzzle(); + const muzzleFromDb = await this.muzzlePersistenceService + .addMuzzleToDb(requestorId, userId, timeToMuzzle) + .catch((e: any) => { + console.error(e); + reject(`Muzzle failed!`); + }); + + if (muzzleFromDb) { + this.muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); + this.setRequestorCount(requestorId); + resolve( + `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` + ); + } + } + }); + } + + /** + * Decrements the muzzleCount on a requestor. + */ + public decrementMuzzleCount(requestorId: string) { + if (this.requestors.has(requestorId)) { + const decrementedMuzzle = --this.requestors.get(requestorId)!.muzzleCount; + this.requestors.set(requestorId, { + muzzleCount: decrementedMuzzle, + muzzleCountRemover: this.requestors.get(requestorId)!.muzzleCountRemover + }); + console.log( + `Successfully decremented ${this.slackService.getUserName( + requestorId + )} | ${requestorId} muzzleCount to ${decrementedMuzzle}` + ); + } else { + console.error( + `Attemped to decrement muzzle count for ${this.slackService.getUserName( + requestorId + )} | ${requestorId} but they did not exist!` + ); + } + } + + /** + * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. + */ + public sendMuzzledMessage(channel: string, userId: string, text: string) { + const muzzleId = this.muzzled.get(userId)!.id; + if (this.muzzled.get(userId)!.suppressionCount < this.MAX_SUPPRESSIONS) { + this.muzzled.set(userId, { + suppressionCount: ++this.muzzled.get(userId)!.suppressionCount, + muzzledBy: this.muzzled.get(userId)!.muzzledBy, + id: muzzleId, + removalFn: this.muzzled.get(userId)!.removalFn + }); + this.webService.sendMessage( + channel, + `<@${userId}> says "${this.muzzle(text, muzzleId)}"` + ); + } else { + this.muzzlePersistenceService.trackDeletedMessage(muzzleId, text); + } + } + + /** + * Removes a muzzle from the specified user. + */ + private removeMuzzle(userId: string) { + this.muzzled.delete(userId); + console.log( + `Removed ${this.slackService.getUserName( + userId + )} | ${userId}'s muzzle! He is free at last.` + ); + } + + /** + * Adds a userId to the muzzled map, and sets timeout for removeMuzzle. + */ + private muzzleUser( + userId: string, + requestorId: string, + id: number, + timeToMuzzle: number + ) { + this.muzzled.set(userId, { + suppressionCount: 0, + muzzledBy: requestorId, + id, + removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) + }); + } + + /** + * Removes a requestor from the map. + */ + private removeRequestor(userId: string) { + this.requestors.delete(userId); + console.log( + `${this.MAX_MUZZLE_TIME} has passed since ${this.slackService.getUserName( + userId + )} | ${userId} last successful muzzle. They have been removed from requestors.` + ); + } + /** + * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. + */ + private setRequestorCount(requestorId: string) { + const muzzleCount = this.requestors.has(requestorId) + ? ++this.requestors.get(requestorId)!.muzzleCount + : 1; + + if (this.requestors.has(requestorId)) { + clearTimeout(this.requestors.get(requestorId)! + .muzzleCountRemover as NodeJS.Timeout); + } + + const removalFunction = + this.requestors.has(requestorId) && + this.requestors.get(requestorId)!.muzzleCount === this.MAX_MUZZLES + ? () => this.removeRequestor(requestorId) + : () => this.decrementMuzzleCount(requestorId); + this.requestors.set(requestorId, { + muzzleCount, + muzzleCountRemover: setTimeout( + removalFunction, + this.MAX_TIME_BETWEEN_MUZZLES + ) + }); + } + + /** + * Returns boolean whether max muzzles have been reached. + */ + private isMaxMuzzlesReached(userId: string) { + return ( + this.requestors.has(userId) && + this.requestors.get(userId)!.muzzleCount === this.MAX_MUZZLES + ); + } +} diff --git a/src/services/slack/slack.service.spec.ts b/src/services/slack/slack.service.spec.ts new file mode 100644 index 00000000..c58587c5 --- /dev/null +++ b/src/services/slack/slack.service.spec.ts @@ -0,0 +1,149 @@ +import { ISlackUser } from "../../shared/models/slack/slack-models"; +import { SlackService } from "./slack.service"; + +describe("slack-utils", () => { + let slackService: SlackService; + + beforeEach(() => { + slackService = SlackService.getInstance(); + slackService.userList = [ + { + id: "123", + name: "test_user123" + }, + { + id: "456", + name: "test_user456" + }, + { + id: "789", + name: "test_user789" + } + ] as ISlackUser[]; + }); + + describe("getUserName()", () => { + it("should return the user.name property of a known user by id", () => { + expect(slackService.getUserName("123")).toBe("test_user123"); + }); + + it("should return an empty string for a user that does not exist", () => { + expect(slackService.getUserName("1010")).toBe(""); + }); + }); + + describe("getUserId()", () => { + it("should return a userId when one is passed in without a username", () => { + expect(slackService.getUserId("<@U2TYNKJ>")).toBe("U2TYNKJ"); + }); + + it("should return a userId when one is passed in with a username with spaces", () => { + expect(slackService.getUserId("<@U2TYNKJ | jrjrjr>")).toBe("U2TYNKJ"); + }); + + it("should return a userId when one is passed in with a username without spaces", () => { + expect(slackService.getUserId("<@U2TYNKJ|jrjrjr>")).toBe("U2TYNKJ"); + }); + + it("should return '' when no userId exists", () => { + expect(slackService.getUserId("total waste of time")).toBe(""); + }); + }); + + describe("getUserById()", () => { + it("should return a user object when given a valid id", () => { + expect(slackService.getUserById("123")).toEqual(slackService.userList[0]); + }); + + it("should return undefined when given an invalid id", () => { + expect(slackService.getUserById("1010")).toBeUndefined(); + }); + }); + + describe("containsTag()", () => { + it("should return false if a word has @ in it and is not a tag", () => { + const testWord = ".@channel"; + expect(slackService.containsTag(testWord)).toBe(false); + }); + + it("should return false if a word does not include @", () => { + const testWord = "test"; + expect(slackService.containsTag(testWord)).toBe(false); + }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(slackService.containsTag(testWord)).toBe(true); + }); + + it("should return true if a word has in it", () => { + const testWord = ""; + expect(slackService.containsTag(testWord)).toBe(true); + }); + + it("should return true if a word has a tagged user", () => { + const testUser = "<@UTJFJKL>"; + expect(slackService.containsTag(testUser)).toBe(true); + }); + }); + + describe("getUserIdByCallbackId()", () => { + it("should return a userId when there is one present", () => { + const callbackId = "JSLKDJLFJ_U25JKLMN"; + expect(slackService.getUserIdByCallbackId(callbackId)).toBe("U25JKLMN"); + }); + + it("should return an empty string when there is no id present", () => { + const callbackId = "LJKSDLFJSF"; + expect(slackService.getUserIdByCallbackId(callbackId)).toBe(""); + }); + + it("should handle an empty string callbackId", () => { + expect(slackService.getUserIdByCallbackId("")).toBe(""); + }); + }); + + describe("getBotId()", () => { + it("should return an id fromText if it is the only id present", () => { + expect( + slackService.getBotId("12345", undefined, undefined, undefined) + ).toBe("12345"); + }); + + it("should return an id fromAttachmentText if it is the only id present", () => { + expect( + slackService.getBotId(undefined, "12345", undefined, undefined) + ).toBe("12345"); + }); + + it("should return an id fromPretext if it is the only id present", () => { + expect( + slackService.getBotId(undefined, undefined, "12345", undefined) + ).toBe("12345"); + }); + + it("should return an id fromCallBackId if it is the only id present", () => { + expect( + slackService.getBotId(undefined, undefined, undefined, "12345") + ).toBe("12345"); + }); + + it("should return the first available id - fromText", () => { + expect(slackService.getBotId("1", "2", "3", "4")).toBe("1"); + }); + + it("should return the first available id - fromAttachmentText", () => { + expect(slackService.getBotId(undefined, "2", "3", "4")).toBe("2"); + }); + + it("should return the first available id - fromPretext", () => { + expect(slackService.getBotId(undefined, undefined, "3", "4")).toBe("3"); + }); + + it("should return the first available id - fromCallbackId", () => { + expect(slackService.getBotId(undefined, undefined, undefined, "4")).toBe( + "4" + ); + }); + }); +}); diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts new file mode 100644 index 00000000..35dd4e63 --- /dev/null +++ b/src/services/slack/slack.service.ts @@ -0,0 +1,105 @@ +import axios from "axios"; +import { + IChannelResponse, + ISlackUser +} from "../../shared/models/slack/slack-models"; +import { WebService } from "../web/web.service"; + +export class SlackService { + public static getInstance() { + if (!SlackService.instance) { + SlackService.instance = new SlackService(); + } + return SlackService.instance; + } + private static instance: SlackService; + public userList: ISlackUser[] = []; + private userIdRegEx = /[<]@\w+/gm; + private web: WebService = WebService.getInstance(); + + private constructor() {} + + public sendResponse(responseUrl: string, response: IChannelResponse): void { + axios + .post(responseUrl, response) + .then(() => + console.log(`Successfully responded to: ${responseUrl}`, response) + ) + .catch((e: Error) => + console.error(`Error responding: ${e.message} at ${responseUrl}`) + ); + } + + /** + * Gets the username of the user by id. + */ + public getUserName(userId: string): string { + const userObj: ISlackUser | undefined = this.getUserById(userId); + return userObj ? userObj.name : ""; + } + + /** + * Retrieves the user id from a string. + * Expected format is <@U235KLKJ> + */ + public getUserId(user: string) { + if (!user) { + return ""; + } + const regArray = user.match(this.userIdRegEx); + return regArray ? regArray[0].slice(2) : ""; + } + + /** + * Returns the user object by id + */ + public getUserById(userId: string) { + return this.userList.find((user: ISlackUser) => user.id === userId); + } + + /** + * Kind of a janky way to get the requesting users ID via callback id. + */ + public getUserIdByCallbackId(callbackId: string) { + if (callbackId.includes("_")) { + return callbackId.slice(callbackId.indexOf("_") + 1, callbackId.length); + } else { + return ""; + } + } + /** + * Retrieves a Slack user id from the various fields in which a userId can exist inside of a bot response. + */ + public getBotId( + fromText: string | undefined, + fromAttachmentText: string | undefined, + fromPretext: string | undefined, + fromCallbackId: string | undefined + ) { + return fromText || fromAttachmentText || fromPretext || fromCallbackId; + } + /** + * Determines whether or not a user is trying to @user, @channel or @here while muzzled. + */ + public containsTag(text: string): boolean { + return ( + text.includes("") || + text.includes("") || + !!this.getUserId(text) + ); + } + + /** + * Retrieves a list of all users. + */ + public async getAllUsers() { + this.userList = (await this.web + .getAllUsers() + .then(resp => resp.members as ISlackUser[]) + .catch(e => { + console.error("Failed to retrieve users", e); + console.error("Retrying in 5 seconds"); + setTimeout(() => this.getAllUsers(), 5000); + })) as ISlackUser[]; + } +} diff --git a/src/services/web/web.service.spec.ts b/src/services/web/web.service.spec.ts new file mode 100644 index 00000000..0860b3fd --- /dev/null +++ b/src/services/web/web.service.spec.ts @@ -0,0 +1,27 @@ +import { WebService } from "./web.service"; + +describe("WebService", () => { + let webService: WebService; + + beforeEach(() => { + webService = WebService.getInstance(); + }); + + describe("sendMessage()", () => { + it("should be defined", () => { + expect(webService.sendMessage).toBeDefined(); + }); + }); + + describe("deleteMessage()", () => { + it("should be defined", () => { + expect(webService.deleteMessage).toBeDefined(); + }); + }); + + describe("getAllUsers()", () => { + it("should be defined", () => { + expect(webService.getAllUsers).toBeDefined(); + }); + }); +}); diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts new file mode 100644 index 00000000..9f16a3d0 --- /dev/null +++ b/src/services/web/web.service.ts @@ -0,0 +1,58 @@ +import { + ChatDeleteArguments, + ChatPostMessageArguments, + WebClient +} from "@slack/web-api"; + +export class WebService { + public static getInstance() { + if (!WebService.instance) { + WebService.instance = new WebService(); + } + return WebService.instance; + } + private static instance: WebService; + private web: WebClient = new WebClient(process.env.muzzleBotToken); + + private constructor() {} + + /** + * Handles deletion of messages. + */ + public deleteMessage(channel: string, ts: string) { + const muzzleToken: any = process.env.muzzleBotToken; + const deleteRequest: ChatDeleteArguments = { + token: muzzleToken, + channel, + ts, + as_user: true + }; + + this.web.chat.delete(deleteRequest).catch(e => { + if (e.data.error === "message_not_found") { + console.log("Message already deleted, no need to retry"); + } else { + console.error(e); + console.error("Retrying in 5 seconds..."); + setTimeout(() => this.deleteMessage(channel, ts), 5000); + } + }); + } + + /** + * Handles sending messages to the chat. + */ + public sendMessage(channel: string, text: string) { + const muzzleToken: any = process.env.muzzleBotToken; + const postRequest: ChatPostMessageArguments = { + token: muzzleToken, + channel, + text + }; + this.web.chat.postMessage(postRequest).catch(e => console.error(e)); + } + + public getAllUsers() { + return this.web.users.list(); + } +} diff --git a/src/db/Muzzle/models/Muzzle.ts b/src/shared/db/models/Muzzle.ts similarity index 100% rename from src/db/Muzzle/models/Muzzle.ts rename to src/shared/db/models/Muzzle.ts diff --git a/src/utils/define/define-utils.ts b/src/utils/define/define-utils.ts deleted file mode 100644 index fc1b8f8c..00000000 --- a/src/utils/define/define-utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Axios, { AxiosResponse } from "axios"; -import { - IDefinition, - IUrbanDictionaryResponse -} from "../../shared/models/define/define-models"; -import { IAttachment } from "../../shared/models/slack/slack-models"; -/** - * Capitalizes the first letter of a given sentence. - */ -export function capitalizeFirstLetter(sentence: string): string { - return `${sentence.charAt(0).toUpperCase()}${sentence.slice(1)}`; -} - -/** - * Returns a promise to look up a definition on urban dictionary. - */ -export function define(word: string): Promise { - return Axios.get(`http://api.urbandictionary.com/v0/define?term=${word}`) - .then((res: AxiosResponse) => { - return res.data; - }) - .catch(e => { - console.log("error", e); - return e; - }); -} - -/** - * Takes in an array of definitions and breaks them down into a shortened list depending on maxDefs - */ -export function formatDefs(defArr: IDefinition[], maxDefs = 3) { - if (!defArr || defArr.length === 0) { - return [{ text: "Sorry, no definitions found." }]; - } - - const formattedArr: IAttachment[] = []; - const maxDefinitions: number = - defArr.length <= maxDefs ? defArr.length : maxDefs; - - for (let i = 0; i < maxDefinitions; i++) { - formattedArr.push({ - text: formatUrbanD( - `${i + 1}. ${capitalizeFirstLetter(defArr[i].definition)}` - ) - }); - } - return formattedArr; -} - -/** - * Takes in a definition and removes brackets. - */ -export function formatUrbanD(definition: string): string { - let formattedDefinition: string = ""; - for (const letter of definition) { - if (letter !== "[" && letter !== "]") { - formattedDefinition += letter; - } - } - return formattedDefinition; -} diff --git a/src/utils/mock/mock-utils.spec.ts b/src/utils/mock/mock-utils.spec.ts deleted file mode 100644 index eb33cb7e..00000000 --- a/src/utils/mock/mock-utils.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from "chai"; -import { mock } from "./mock-utils"; - -describe("mock-utils", () => { - describe("mock()", () => { - it("should mock a users input (single word)", () => { - expect(mock("test")).to.equal("tEsT"); - }); - - it("should mock a users input (sentence)", () => { - expect(mock("test sentence with multiple words.")).to.equal( - "tEsT sEnTeNcE wItH mUlTiPlE wOrDs." - ); - }); - - it("should return input if it is an empty string", () => { - expect(mock("")).to.equal(""); - }); - }); -}); diff --git a/src/utils/mock/mock-utils.ts b/src/utils/mock/mock-utils.ts deleted file mode 100644 index 723625c3..00000000 --- a/src/utils/mock/mock-utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function mock(input: string): string { - let output = ""; - if (!input || input.length === 0) { - return input; - } else { - let shouldChangeCase = true; - for (const letter of input) { - if (letter === " ") { - output += letter; - } else { - output += shouldChangeCase - ? letter.toLowerCase() - : letter.toUpperCase(); - shouldChangeCase = !shouldChangeCase; - } - } - return output; - } -} diff --git a/src/utils/muzzle/muzzle.spec.ts b/src/utils/muzzle/muzzle.spec.ts deleted file mode 100644 index 39564ec3..00000000 --- a/src/utils/muzzle/muzzle.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { expect } from "chai"; -import * as lolex from "lolex"; -import { ISlackUser } from "../../shared/models/slack/slack-models"; -import { setUserList } from "../slack/slack-utils"; -import { - addUserToMuzzled, - muzzle, - muzzled, - removeMuzzle, - removeRequestor, - requestors -} from "./muzzle"; - -describe("muzzle", () => { - const testData = { - user: "123", - user2: "456", - user3: "789", - requestor: "666" - }; - - const clock = lolex.install(); - - beforeEach(() => { - muzzled.clear(); - requestors.clear(); - setUserList([ - { id: "123", name: "test123" }, - { id: "456", name: "test456" }, - { id: "789", name: "test789" }, - { id: "666", name: "requestor" } - ] as ISlackUser[]); - }); - - afterEach(() => { - clock.reset(); - }); - - after(() => { - clock.uninstall(); - }); - - describe("addUserToMuzzled()", () => { - describe("muzzled", () => { - it("should add a user to the muzzled map", () => { - addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.size).to.equal(1); - expect(muzzled.has(testData.user)).to.equal(true); - }); - - it("should return an added user with IMuzzled attributes", () => { - addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.get(testData.user)!.suppressionCount).to.equal(0); - expect(muzzled.get(testData.user)!.muzzledBy).to.equal( - testData.requestor - ); - }); - - it("should reject if a user tries to muzzle an already muzzled user", async () => { - await addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.has(testData.user)).to.equal(true); - await addUserToMuzzled(testData.user, testData.requestor).catch(e => { - expect(e).to.equal("test123 is already muzzled!"); - }); - }); - - it("should reject if a user tries to muzzle a user that does not exist", async () => { - await addUserToMuzzled("", testData.requestor); - expect(muzzled.has("")).to.equal(false); - await addUserToMuzzled("", testData.requestor).catch(e => { - expect(e).to.equal( - `Invalid username passed in. You can only muzzle existing slack users` - ); - }); - }); - - it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { - await addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.has(testData.user)).to.equal(true); - await addUserToMuzzled(testData.requestor, testData.user).catch(e => { - expect(e).to.equal( - `You can't muzzle someone if you are already muzzled!` - ); - }); - }); - }); - - describe("requestors", () => { - it("should add a user to the requestors map", () => { - addUserToMuzzled(testData.user, testData.requestor); - - expect(requestors.size).to.equal(1); - expect(requestors.has(testData.requestor)).to.equal(true); - }); - - it("should return an added user with IMuzzler attributes", () => { - addUserToMuzzled(testData.user, testData.requestor); - expect(requestors.get(testData.requestor)!.muzzleCount).to.equal(1); - }); - - it("should increment a requestors muzzle count on a second addUserToMuzzled() call", () => { - addUserToMuzzled(testData.user, testData.requestor); - addUserToMuzzled(testData.user2, testData.requestor); - expect(muzzled.size).to.equal(2); - expect(requestors.has(testData.requestor)).to.equal(true); - expect(requestors.get(testData.requestor)!.muzzleCount).to.equal(2); - }); - - it("should prevent a requestor from muzzling on their third count", async () => { - await addUserToMuzzled(testData.user, testData.requestor); - await addUserToMuzzled(testData.user2, testData.requestor); - await addUserToMuzzled(testData.user3, testData.requestor).catch(e => - expect(e).to.equal( - `You're doing that too much. Only 2 muzzles are allowed per hour.` - ) - ); - }); - }); - }); - - describe("removeMuzzle()", () => { - it("should remove a user from the muzzled array", () => { - addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.size).to.equal(1); - expect(muzzled.has(testData.user)).to.equal(true); - removeMuzzle(testData.user); - expect(muzzled.has(testData.user)).to.equal(false); - expect(muzzled.size).to.equal(0); - }); - }); - - describe("removeRequestor()", () => { - it("should remove a user from the muzzler array", () => { - addUserToMuzzled(testData.user, testData.requestor); - expect(muzzled.size).to.equal(1); - expect(muzzled.has(testData.user)).to.equal(true); - expect(requestors.size).to.equal(1); - expect(requestors.has(testData.requestor)).to.equal(true); - removeRequestor(testData.requestor); - expect(requestors.has(testData.requestor)).to.equal(false); - expect(requestors.size).to.equal(0); - }); - }); - - describe("muzzle()", () => { - it("should always muzzle a tagged user", () => { - const testSentence = - "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzle(testSentence, 1)).to.equal( - " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " - ); - }); - - it("should always muzzle ", () => { - const testSentence = " hey guys"; - expect(muzzle(testSentence, 1).includes("")).to.equal(false); - }); - }); -}); diff --git a/src/utils/muzzle/muzzle.ts b/src/utils/muzzle/muzzle.ts deleted file mode 100644 index e4fe6fcb..00000000 --- a/src/utils/muzzle/muzzle.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { - ChatDeleteArguments, - ChatPostMessageArguments, - WebClient -} from "@slack/web-api"; -import { - addMuzzleToDb, - incrementCharacterSuppressions, - incrementMessageSuppressions, - incrementMuzzleTime, - incrementWordSuppressions, - trackDeletedMessage -} from "../../db/Muzzle/actions/muzzle-actions"; -import { IMuzzled, IRequestor } from "../../shared/models/muzzle/muzzle-models"; -import { IEventRequest } from "../../shared/models/slack/slack-models"; -import { - containsTag, - getBotId, - getUserId, - getUserIdByCallbackId, - getUserName -} from "../slack/slack-utils"; -import { - getRemainingTime, - getTimeString, - getTimeToMuzzle, - isRandomEven -} from "./muzzle-utilities"; -// Store for the muzzled users. -export const muzzled: Map = new Map(); -// Store for people who are muzzling others. -export const requestors: Map = new Map(); - -// Muzzle Constants -const MAX_MUZZLE_TIME = 3600000; -const MAX_TIME_BETWEEN_MUZZLES = 3600000; -export const ABUSE_PENALTY_TIME = 300000; -const MAX_SUPPRESSIONS: number = 7; -const MAX_MUZZLES = 2; - -export const web: WebClient = new WebClient(process.env.muzzleBotToken); - -/** - * Takes in text and randomly muzzles certain words. - */ -export function muzzle(text: string, muzzleId: number) { - const replacementText = " ..mMm... "; - let returnText = ""; - const words = text.split(" "); - let wordsSuppressed = 0; - let charactersSuppressed = 0; - let replacementWord; - for (const word of words) { - replacementWord = - isRandomEven() && !containsTag(word) ? ` *${word}* ` : replacementText; - if (replacementWord === replacementText) { - wordsSuppressed++; - charactersSuppressed += word.length; - } - returnText += replacementWord; - } - incrementMessageSuppressions(muzzleId); - incrementCharacterSuppressions(muzzleId, charactersSuppressed); - incrementWordSuppressions(muzzleId, wordsSuppressed); - return returnText; -} - -/** - * Adds the specified amount of time to a specified muzzled user. - */ -export function addMuzzleTime(userId: string, timeToAdd: number) { - if (userId && muzzled.has(userId)) { - const removalFn = muzzled.get(userId)!.removalFn; - const newTime = getRemainingTime(removalFn) + timeToAdd; - const muzzleId = muzzled.get(userId)!.id; - incrementMuzzleTime(muzzleId, ABUSE_PENALTY_TIME); - clearTimeout(muzzled.get(userId)!.removalFn); - console.log(`Setting ${getUserName(userId)}'s muzzle time to ${newTime}`); - muzzled.set(userId, { - suppressionCount: muzzled.get(userId)!.suppressionCount, - muzzledBy: muzzled.get(userId)!.muzzledBy, - id: muzzled.get(userId)!.id, - removalFn: setTimeout(() => removeMuzzle(userId), newTime) - }); - } -} - -/** - * Gets the corresponding database ID for the user's current muzzle. - */ -export function getMuzzleId(userId: string) { - return muzzled.get(userId)!.id; -} - -/** - * Returns boolean whether max muzzles have been reached. - */ -function isMaxMuzzlesReached(userId: string) { - return ( - requestors.has(userId) && - requestors.get(userId)!.muzzleCount === MAX_MUZZLES - ); -} - -/** - * Returns boolean whether user is muzzled or not. - */ -export function isUserMuzzled(userId: string) { - return muzzled.has(userId); -} - -/** - * Determines whether or not a bot message should be removed. - */ -export function shouldBotMessageBeMuzzled(request: IEventRequest) { - let userIdByEventText; - let userIdByAttachmentText; - let userIdByAttachmentPretext; - let userIdByCallbackId; - - if (request.event.text) { - userIdByEventText = getUserId(request.event.text); - } else if (request.event.attachments && request.event.attachments.length) { - userIdByAttachmentText = getUserId(request.event.attachments[0].text); - userIdByAttachmentPretext = getUserId(request.event.attachments[0].pretext); - - if (request.event.attachments[0].callback_id) { - userIdByCallbackId = getUserIdByCallbackId( - request.event.attachments[0].callback_id - ); - } - } - - const finalUserId = getBotId( - userIdByEventText, - userIdByAttachmentText, - userIdByAttachmentPretext, - userIdByCallbackId - ); - - return ( - request.event.subtype === "bot_message" && - request.event.attachments && - finalUserId && - isUserMuzzled(finalUserId) && - request.event.username !== "muzzle" - ); -} - -/** - * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. - */ -function setRequestorCount(requestorId: string) { - const muzzleCount = requestors.has(requestorId) - ? ++requestors.get(requestorId)!.muzzleCount - : 1; - - if (requestors.has(requestorId)) { - clearTimeout(requestors.get(requestorId)! - .muzzleCountRemover as NodeJS.Timeout); - } - - const removalFunction = - requestors.has(requestorId) && - requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES - ? () => removeRequestor(requestorId) - : () => decrementMuzzleCount(requestorId); - requestors.set(requestorId, { - muzzleCount, - muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES) - }); -} - -/** - * Adds a userId to the muzzled map, and sets timeout for removeMuzzle. - */ -function muzzleUser( - userId: string, - requestorId: string, - id: number, - timeToMuzzle: number -) { - muzzled.set(userId, { - suppressionCount: 0, - muzzledBy: requestorId, - id, - removalFn: setTimeout(() => removeMuzzle(userId), timeToMuzzle) - }); -} - -/** - * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes - */ -export function addUserToMuzzled(userId: string, requestorId: string) { - const userName = getUserName(userId); - const requestorName = getUserName(requestorId); - return new Promise(async (resolve, reject) => { - if (!userId) { - reject( - `Invalid username passed in. You can only muzzle existing slack users` - ); - } else if (isUserMuzzled(userId)) { - console.error( - `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` - ); - reject(`${userName} is already muzzled!`); - } else if (isUserMuzzled(requestorId)) { - console.error( - `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled` - ); - reject(`You can't muzzle someone if you are already muzzled!`); - } else if (isMaxMuzzlesReached(requestorId)) { - console.error( - `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${MAX_MUZZLES}` - ); - reject( - `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` - ); - } else { - const timeToMuzzle = getTimeToMuzzle(); - const muzzleFromDb = await addMuzzleToDb( - requestorId, - userId, - timeToMuzzle - ).catch((e: any) => { - console.error(e); - reject(`Muzzle failed!`); - }); - - if (muzzleFromDb) { - muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); - setRequestorCount(requestorId); - resolve( - `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` - ); - } - } - }); -} -/** - * Decrements the muzzleCount on a requestor. - */ -export function decrementMuzzleCount(requestorId: string) { - if (requestors.has(requestorId)) { - const decrementedMuzzle = --requestors.get(requestorId)!.muzzleCount; - requestors.set(requestorId, { - muzzleCount: decrementedMuzzle, - muzzleCountRemover: requestors.get(requestorId)!.muzzleCountRemover - }); - console.log( - `Successfully decremented ${getUserName( - requestorId - )} | ${requestorId} muzzleCount to ${decrementedMuzzle}` - ); - } else { - console.error( - `Attemped to decrement muzzle count for ${getUserName( - requestorId - )} | ${requestorId} but they did not exist!` - ); - } -} - -/** - * Removes a requestor from the map. - */ -export function removeRequestor(userId: string) { - requestors.delete(userId); - console.log( - `${MAX_MUZZLE_TIME} has passed since ${getUserName( - userId - )} | ${userId} last successful muzzle. They have been removed from requestors.` - ); -} - -/** - * Removes a muzzle from the specified user. - */ -export function removeMuzzle(userId: string) { - muzzled.delete(userId); - console.log( - `Removed ${getUserName(userId)} | ${userId}'s muzzle! He is free at last.` - ); -} - -/** - * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. - */ -export function sendMuzzledMessage( - channel: string, - userId: string, - text: string -) { - const muzzleId = muzzled.get(userId)!.id; - if (muzzled.get(userId)!.suppressionCount < MAX_SUPPRESSIONS) { - muzzled.set(userId, { - suppressionCount: ++muzzled.get(userId)!.suppressionCount, - muzzledBy: muzzled.get(userId)!.muzzledBy, - id: muzzleId, - removalFn: muzzled.get(userId)!.removalFn - }); - sendMessage(channel, `<@${userId}> says "${muzzle(text, muzzleId)}"`); - } else { - trackDeletedMessage(muzzleId, text); - } -} - -/** - * Handles deletion of messages. - */ -export function deleteMessage(channel: string, ts: string) { - const muzzleToken: any = process.env.muzzleBotToken; - const deleteRequest: ChatDeleteArguments = { - token: muzzleToken, - channel, - ts, - as_user: true - }; - - web.chat.delete(deleteRequest).catch(e => { - if (e.data.error === "message_not_found") { - console.log("Message already deleted, no need to retry"); - } else { - console.error(e); - console.error("Retrying in 5 seconds..."); - setTimeout(() => deleteMessage(channel, ts), 5000); - } - }); -} - -/** - * Handles sending messages to the chat. - */ -export function sendMessage(channel: string, text: string) { - const muzzleToken: any = process.env.muzzleBotToken; - const postRequest: ChatPostMessageArguments = { - token: muzzleToken, - channel, - text - }; - web.chat.postMessage(postRequest).catch(e => console.error(e)); -} diff --git a/src/utils/slack/slack-utils.spec.ts b/src/utils/slack/slack-utils.spec.ts deleted file mode 100644 index d71ea5a5..00000000 --- a/src/utils/slack/slack-utils.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { expect } from "chai"; -import { containsTag, getUserId } from "./slack-utils"; - -describe("slack-utils", () => { - describe("getUserId()", () => { - it("should return a userId when one is passed in without a username", () => { - expect(getUserId("<@U2TYNKJ>")).to.equal("U2TYNKJ"); - }); - - it("should return a userId when one is passed in with a username with spaces", () => { - expect(getUserId("<@U2TYNKJ | jrjrjr>")).to.equal("U2TYNKJ"); - }); - - it("should return a userId when one is passed in with a username without spaces", () => { - expect(getUserId("<@U2TYNKJ|jrjrjr>")).to.equal("U2TYNKJ"); - }); - - it("should return '' when no userId exists", () => { - expect(getUserId("total waste of time")).to.equal(""); - }); - }); - - describe("containsTag()", () => { - it("should return false if a word has @ in it and is not a tag", () => { - const testWord = ".@channel"; - expect(containsTag(testWord)).to.equal(false); - }); - - it("should return false if a word does not include @", () => { - const testWord = "test"; - expect(containsTag(testWord)).to.equal(false); - }); - - it("should return true if a word has in it", () => { - const testWord = ""; - expect(containsTag(testWord)).to.equal(true); - }); - - it("should return true if a word has in it", () => { - const testWord = ""; - expect(containsTag(testWord)).to.equal(true); - }); - - it("should return true if a word has a tagged user", () => { - const testUser = "<@UTJFJKL>"; - expect(containsTag(testUser)).to.equal(true); - }); - }); -}); diff --git a/src/utils/slack/slack-utils.ts b/src/utils/slack/slack-utils.ts deleted file mode 100644 index daa8fa5a..00000000 --- a/src/utils/slack/slack-utils.ts +++ /dev/null @@ -1,88 +0,0 @@ -import axios from "axios"; -import { - IChannelResponse, - ISlackUser -} from "../../shared/models/slack/slack-models"; -import { web } from "../muzzle/muzzle"; - -const userIdRegEx = /[<]@\w+/gm; - -export let userList: ISlackUser[]; - -export function sendResponse( - responseUrl: string, - response: IChannelResponse -): void { - axios - .post(responseUrl, response) - .then(() => - console.log(`Successfully responded to: ${responseUrl}`, response) - ) - .catch((e: Error) => - console.error(`Error responding: ${e.message} at ${responseUrl}`) - ); -} - -export function getUserName(user: string): string { - const userObj: ISlackUser | undefined = getUserById(user); - return userObj ? userObj.name : ""; -} - -export function getUserId(user: string) { - if (!user) { - return ""; - } - const regArray = user.match(userIdRegEx); - return regArray ? regArray[0].slice(2) : ""; -} - -export function getUserById(userId: string) { - return userList.find((user: ISlackUser) => user.id === userId); -} - -// This will really only work for SpoilerBot since it stores userId here and nowhere else. -export function getUserIdByCallbackId(callbackId: string) { - return callbackId.slice(callbackId.indexOf("_") + 1, callbackId.length); -} - -/** - * TO BE USED EXCLUSIVELY FOR TESTING. WE SHOULD *NEVER* be setting the userList manually - * This should be handled by getAllUsers() only. - */ -export function setUserList(list: ISlackUser[]) { - userList = list; -} - -export function getAllUsers() { - web.users - .list() - .then(resp => { - userList = resp.members as ISlackUser[]; - }) - .catch(e => { - console.error("Failed to retrieve users", e); - console.error("Retrying in 5 seconds"); - setTimeout(() => getAllUsers(), 5000); - }); -} - -/** - * Retrieves a Slack user id from the various fields in which a userId can exist inside of a bot response. - */ -export function getBotId( - fromText: string | undefined, - fromAttachmentText: string | undefined, - fromPretext: string | undefined, - fromCallbackId: string | undefined -) { - return fromText || fromAttachmentText || fromPretext || fromCallbackId; -} - -/** - * Determines whether or not a user is trying to @user, @channel or @here while muzzled. - */ -export function containsTag(text: string): boolean { - return ( - text.includes("") || text.includes("") || !!getUserId(text) - ); -} From 35170ea0379fd16cc9509d868cbee3ef3ac386f0 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 7 Jul 2019 17:53:57 -0400 Subject: [PATCH 019/167] Improved Tests (#26) * Added more tests to muzzle.service * Fixed empty block --- package.json | 2 +- src/services/muzzle/muzzle.service.spec.ts | 262 ++++++++++++++++++--- src/services/muzzle/muzzle.service.ts | 51 ++-- src/services/slack/slack.service.spec.ts | 108 +++++---- 4 files changed, 317 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index 4b450612..4cf478a5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "npm run start:dev", "start:prod": "node ./dist/server.js", "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", - "test": "jest", + "test": "jest --silent", "test:watch": "jest --watch", "tsc": "tsc" }, diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 168e7167..1198d046 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -1,6 +1,9 @@ import { UpdateResult } from "typeorm"; import { Muzzle } from "../../shared/db/models/Muzzle"; -import { ISlackUser } from "../../shared/models/slack/slack-models"; +import { + IEventRequest, + ISlackUser +} from "../../shared/models/slack/slack-models"; import { SlackService } from "../slack/slack.service"; import { MuzzlePersistenceService } from "./muzzle.persistence.service"; import { MuzzleService } from "./muzzle.service"; @@ -32,6 +35,225 @@ describe("MuzzleService", () => { jest.runAllTimers(); }); + describe("muzzle()", () => { + beforeEach(() => { + const mockResolve = { raw: "whatever" }; + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementMessageSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementCharacterSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + jest + .spyOn( + MuzzlePersistenceService.getInstance(), + "incrementWordSuppressions" + ) + .mockResolvedValue(mockResolve as UpdateResult); + }); + it("should always muzzle a tagged user", () => { + const testSentence = + "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; + expect(muzzleInstance.muzzle(testSentence, 1)).toBe( + " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " + ); + }); + + it("should always muzzle ", () => { + const testSentence = ""; + expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + }); + + it("should always muzzle ", () => { + const testSentence = ""; + expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + }); + }); + + describe("getMuzzleId()", () => { + it("should return the database id of the muzzledUser by id", async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + expect(muzzleInstance.getMuzzleId("123")).toBe(1); + }); + }); + + describe("getMuzzledUserById()", () => { + beforeEach(async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + }); + + it("should return the muzzled user when a valid id is passed in", async () => { + const muzzledUser = muzzleInstance.getMuzzledUserById("123"); + expect(muzzledUser!.id).toBe(1); + expect(muzzledUser!.muzzledBy).toBe("666"); + expect(muzzledUser!.removalFn).toBeDefined(); + expect(muzzledUser!.suppressionCount).toBe(0); + }); + }); + + describe("getRequestorById()", () => { + beforeEach(async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + }); + it("should return the requestor when a valid id is passed in", async () => { + const requestor = muzzleInstance.getRequestorById("666"); + expect(requestor!.muzzleCount).toBe(1); + expect(requestor!.muzzleCountRemover).toBeDefined(); + }); + }); + + describe("isUserMuzzled()", () => { + beforeEach(async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + }); + it("should return true when a muzzled userId is passed in", async () => { + expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); + }); + + it("should return false when an unmuzzled userId is passed in", async () => { + expect(muzzleInstance.isUserMuzzled(testData.user2)).toBe(false); + }); + }); + + describe("isUserRequestor()", () => { + beforeEach(async () => { + const mockMuzzle = { id: 1 }; + jest + .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + .mockResolvedValue(mockMuzzle as Muzzle); + await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + }); + + it("should return true when a requestor userId is passed in", () => { + expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); + }); + + it("should return false when a non-requestor userId is passed in", () => { + expect(muzzleInstance.isUserRequestor(testData.user)).toBe(false); + }); + }); + + describe("shouldBotMessageBeMuzzled()", () => { + let mockRequest: IEventRequest; + beforeEach(() => { + /* tslint:disable-next-line:no-object-literal-type-assertion */ + mockRequest = { + event: { + subtype: "bot_message", + username: "not_muzzle", + text: "<@123>", + attachments: [ + { + callback_id: "LKJSF_123", + pretext: "<@123>", + text: "<@123>" + } + ] + } + } as IEventRequest; + }); + describe("positive path", () => { + beforeEach(async () => { + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor + ); + }); + it("should return true if an id is present in the event.text ", () => { + mockRequest.event.attachments = []; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + true + ); + }); + + it("should return true if an id is present in the event.attachments[0].text", () => { + mockRequest.event.text = "whatever"; + mockRequest.event.attachments[0].pretext = "whatever"; + mockRequest.event.attachments[0].callback_id = "whatever"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + true + ); + }); + + it("should return true if an id is present in the event.attachments[0].pretext", () => { + mockRequest.event.text = "whatever"; + mockRequest.event.attachments[0].text = "whatever"; + mockRequest.event.attachments[0].callback_id = "whatever"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + true + ); + }); + + it("should return the id present in the event.attachments[0].callback_id if an id is present", () => { + mockRequest.event.text = "whatever"; + mockRequest.event.attachments[0].text = "whatever"; + mockRequest.event.attachments[0].pretext = "whatever"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + true + ); + }); + }); + + describe("negative path", () => { + it("should return false if there is no id present in any fields", () => { + mockRequest.event.text = "no id"; + mockRequest.event.callback_id = "TEST_TEST"; + mockRequest.event.attachments[0].text = "test"; + mockRequest.event.attachments[0].pretext = "test"; + mockRequest.event.attachments[0].callback_id = "TEST"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + false + ); + }); + + it("should return false if the message is not a bot_message", () => { + mockRequest.event.subtype = "not_bot_message"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + false + ); + }); + + it("should return false if the requesting user is not muzzled", () => { + mockRequest.event.text = "<@456>"; + mockRequest.event.attachments[0].text = "<@456>"; + mockRequest.event.attachments[0].pretext = "<@456>"; + mockRequest.event.attachments[0].callback_id = "TEST_456"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + false + ); + }); + + it("should return false if the bot username is muzzle", () => { + mockRequest.event.username = "muzzle"; + expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + false + ); + }); + }); + }); + describe("addUserToMuzzled()", () => { describe("muzzled", () => { describe("positive path", () => { @@ -186,42 +408,4 @@ describe("MuzzleService", () => { }); }); }); - - describe("muzzle()", () => { - beforeEach(() => { - const mockResolve = { raw: "whatever" }; - jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementMessageSuppressions" - ) - .mockResolvedValue(mockResolve as UpdateResult); - jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementCharacterSuppressions" - ) - .mockResolvedValue(mockResolve as UpdateResult); - jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementWordSuppressions" - ) - .mockResolvedValue(mockResolve as UpdateResult); - }); - it("should always muzzle a tagged user", () => { - const testSentence = - "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzleInstance.muzzle(testSentence, 1)).toBe( - " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " - ); - }); - - it("should always muzzle ", () => { - const testSentence = " hey guys"; - expect( - muzzleInstance.muzzle(testSentence, 1).includes("") - ).toBe(false); - }); - }); }); diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 36cde3c0..688c2dc7 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -134,7 +134,9 @@ export class MuzzleService { if (request.event.text) { userIdByEventText = this.slackService.getUserId(request.event.text); - } else if (request.event.attachments && request.event.attachments.length) { + } + + if (request.event.attachments && request.event.attachments.length) { userIdByAttachmentText = this.slackService.getUserId( request.event.attachments[0].text ); @@ -156,9 +158,8 @@ export class MuzzleService { userIdByCallbackId ); - return ( + return !!( request.event.subtype === "bot_message" && - request.event.attachments && finalUserId && this.isUserMuzzled(finalUserId) && request.event.username !== "muzzle" @@ -217,10 +218,31 @@ export class MuzzleService { }); } + /** + * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. + */ + public sendMuzzledMessage(channel: string, userId: string, text: string) { + const muzzleId = this.muzzled.get(userId)!.id; + if (this.muzzled.get(userId)!.suppressionCount < this.MAX_SUPPRESSIONS) { + this.muzzled.set(userId, { + suppressionCount: ++this.muzzled.get(userId)!.suppressionCount, + muzzledBy: this.muzzled.get(userId)!.muzzledBy, + id: muzzleId, + removalFn: this.muzzled.get(userId)!.removalFn + }); + this.webService.sendMessage( + channel, + `<@${userId}> says "${this.muzzle(text, muzzleId)}"` + ); + } else { + this.muzzlePersistenceService.trackDeletedMessage(muzzleId, text); + } + } + /** * Decrements the muzzleCount on a requestor. */ - public decrementMuzzleCount(requestorId: string) { + private decrementMuzzleCount(requestorId: string) { if (this.requestors.has(requestorId)) { const decrementedMuzzle = --this.requestors.get(requestorId)!.muzzleCount; this.requestors.set(requestorId, { @@ -241,27 +263,6 @@ export class MuzzleService { } } - /** - * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. - */ - public sendMuzzledMessage(channel: string, userId: string, text: string) { - const muzzleId = this.muzzled.get(userId)!.id; - if (this.muzzled.get(userId)!.suppressionCount < this.MAX_SUPPRESSIONS) { - this.muzzled.set(userId, { - suppressionCount: ++this.muzzled.get(userId)!.suppressionCount, - muzzledBy: this.muzzled.get(userId)!.muzzledBy, - id: muzzleId, - removalFn: this.muzzled.get(userId)!.removalFn - }); - this.webService.sendMessage( - channel, - `<@${userId}> says "${this.muzzle(text, muzzleId)}"` - ); - } else { - this.muzzlePersistenceService.trackDeletedMessage(muzzleId, text); - } - } - /** * Removes a muzzle from the specified user. */ diff --git a/src/services/slack/slack.service.spec.ts b/src/services/slack/slack.service.spec.ts index c58587c5..e7bbb853 100644 --- a/src/services/slack/slack.service.spec.ts +++ b/src/services/slack/slack.service.spec.ts @@ -30,6 +30,10 @@ describe("slack-utils", () => { it("should return an empty string for a user that does not exist", () => { expect(slackService.getUserName("1010")).toBe(""); }); + + it("should handle empty strings values", () => { + expect(slackService.getUserName("")).toBe(""); + }); }); describe("getUserId()", () => { @@ -45,7 +49,7 @@ describe("slack-utils", () => { expect(slackService.getUserId("<@U2TYNKJ|jrjrjr>")).toBe("U2TYNKJ"); }); - it("should return '' when no userId exists", () => { + it("should return empty string when no userId exists", () => { expect(slackService.getUserId("total waste of time")).toBe(""); }); }); @@ -104,46 +108,68 @@ describe("slack-utils", () => { }); describe("getBotId()", () => { - it("should return an id fromText if it is the only id present", () => { - expect( - slackService.getBotId("12345", undefined, undefined, undefined) - ).toBe("12345"); - }); - - it("should return an id fromAttachmentText if it is the only id present", () => { - expect( - slackService.getBotId(undefined, "12345", undefined, undefined) - ).toBe("12345"); - }); - - it("should return an id fromPretext if it is the only id present", () => { - expect( - slackService.getBotId(undefined, undefined, "12345", undefined) - ).toBe("12345"); - }); - - it("should return an id fromCallBackId if it is the only id present", () => { - expect( - slackService.getBotId(undefined, undefined, undefined, "12345") - ).toBe("12345"); - }); - - it("should return the first available id - fromText", () => { - expect(slackService.getBotId("1", "2", "3", "4")).toBe("1"); - }); - - it("should return the first available id - fromAttachmentText", () => { - expect(slackService.getBotId(undefined, "2", "3", "4")).toBe("2"); - }); - - it("should return the first available id - fromPretext", () => { - expect(slackService.getBotId(undefined, undefined, "3", "4")).toBe("3"); - }); - - it("should return the first available id - fromCallbackId", () => { - expect(slackService.getBotId(undefined, undefined, undefined, "4")).toBe( - "4" - ); + describe("it should handle undefined values", () => { + it("should return an id fromText if it is the only id present", () => { + expect( + slackService.getBotId("12345", undefined, undefined, undefined) + ).toBe("12345"); + }); + + it("should return an id fromAttachmentText if it is the only id present", () => { + expect( + slackService.getBotId(undefined, "12345", undefined, undefined) + ).toBe("12345"); + }); + + it("should return an id fromPretext if it is the only id present", () => { + expect( + slackService.getBotId(undefined, undefined, "12345", undefined) + ).toBe("12345"); + }); + + it("should return an id fromCallBackId if it is the only id present", () => { + expect( + slackService.getBotId(undefined, undefined, undefined, "12345") + ).toBe("12345"); + }); + }); + + describe("it should handle empty strings", () => { + it("should return an id fromText if it is the only id present", () => { + expect(slackService.getBotId("12345", "", "", "")).toBe("12345"); + }); + + it("should return an id fromAttachmentText if it is the only id present", () => { + expect(slackService.getBotId("", "12345", "", "")).toBe("12345"); + }); + + it("should return an id fromPretext if it is the only id present", () => { + expect(slackService.getBotId("", "", "12345", "")).toBe("12345"); + }); + + it("should return an id fromCallBackId if it is the only id present", () => { + expect(slackService.getBotId("", "", "", "12345")).toBe("12345"); + }); + }); + + describe("it should return in the proper order", () => { + it("should return the first available id - fromText", () => { + expect(slackService.getBotId("1", "2", "3", "4")).toBe("1"); + }); + + it("should return the first available id - fromAttachmentText", () => { + expect(slackService.getBotId(undefined, "2", "3", "4")).toBe("2"); + }); + + it("should return the first available id - fromPretext", () => { + expect(slackService.getBotId(undefined, undefined, "3", "4")).toBe("3"); + }); + + it("should return the first available id - fromCallbackId", () => { + expect( + slackService.getBotId(undefined, undefined, undefined, "4") + ).toBe("4"); + }); }); }); }); From 39b5c9961ccfa97ce3e60495e4a7d5b4ce7c4b1a Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 13 Jul 2019 09:40:28 -0400 Subject: [PATCH 020/167] Fixed vulnerable npm packages (#27) --- package-lock.json | 43 ++++++++++--------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2067b834..380df0f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5057,9 +5057,9 @@ "dev": true }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -6311,9 +6311,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -7110,38 +7110,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-string": { From 8318cd906ee24f82a8951b15ccbb3e7c3fb95ebb Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 15 Jul 2019 18:52:09 -0400 Subject: [PATCH 021/167] Feature/report (#28) * added stats route and mostMuzzledByInstances * Added logging * Adjusted getMostMuzzledByInstances() * Consolidated getMostMuzzledByInstances * Changes to getMostMuzzledByInstances * Changes to getMostMuzzledByInstances * Added orderBy * Added descending count * Added getMostMuzzledByWords * Adjusted params in getMostMuzzledByWords * Adjusted params in getMostMuzzledByWords * Summed the wordsSuppressed field * Summed the wordsSuppressed field * Summed the wordsSuppressed field * Summed the wordsSuppressed field * Removed groupBy for wordsSuppressed * Added getMostMuzzledByChars * Added getMostMuzzledByTime * Added getMostMuzzledByMessages * Added muzzler request data * Fixed typo * Fixed naming * Added prelim kdr implementation * Adjustment to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Changes to KDR * Fixed invalid sql statements * Added await * Consolidated kill and deaths to kdr * Adjusted divison * Adjusted divison * Fixed typo * Fixed typo * Adjusted getKdr to perform count division on one line * added casting to decimal * Casted only the divisor * Casted only the divisor * Removed unnecessary async await * Testing diff decimal * Testing result of select call * Testing result of select call * Testing result of select call * Testing total count * Converted to using SUM * Added kills and deaths to KDR key * Added case statement * Added if statement * Changed null to 0 * Added nemesis * Changed nemesis * Changed groupBy for nemesis * Changed groupBy for nemesis * Changed groupBy to addOrderBy * Removed addOrderBy * Added naming for nemesis * Added max count * Changed groupBy * Reverted max change * Added limit * Changed order of limit * Changed order of limit again * Swapped out typeOrm calls for custom mysql query * Adjusted custom mysql query * Reverting to typeorm methods * Converting to a raw sql query * Adjusted query and commented out the typeorm calls * Added distinct * Adjusted distinct * Distinct changes * Added custom sql query * Added nemesis sql query * Added name for count * Added some mild formatting * Added kdr and nemesis to report * Forced controller to send back report: * Removed table import * Added tables * Returning report * Adjusted how we handle cli-table builds * Fixed typo * Fixed typo * Removed TH headings * Converted to post route * Enabled slack message * Converted to use easy-table instead * Added backticks * Added better formatting * Adjusted SQL queries to be more easy to manage * Backed out sql changes and did formatting in the code --- package-lock.json | 39 +++ package.json | 2 + src/controllers/muzzle.controller.ts | 18 ++ .../muzzle/muzzle.persistence.service.ts | 294 ++++++++++++++++++ 4 files changed, 353 insertions(+) diff --git a/package-lock.json b/package-lock.json index 380df0f7..7bf5cd28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -557,6 +557,12 @@ "@types/node": "*" } }, + "@types/easy-table": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/easy-table/-/easy-table-0.0.32.tgz", + "integrity": "sha512-zKh0f/ixYFnr3Ldf5ZJTi1ZpnRqAynTTtVyGvWDf/TT12asE8ac98t3/WGWfFdRPp/qsccxg82C/Kl3NPNhqEw==", + "dev": true + }, "@types/express": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", @@ -1457,6 +1463,12 @@ "wrap-ansi": "^2.0.0" } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "optional": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1699,6 +1711,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "optional": true, + "requires": { + "clone": "^1.0.2" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1825,6 +1846,15 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "easy-table": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.1.tgz", + "integrity": "sha512-C9Lvm0WFcn2RgxbMnTbXZenMIWcBtkzMr+dWqq/JsVoGFSVUVlPqeOa5LP5kM0I3zoOazFpckOEb2/0LDFfToQ==", + "requires": { + "ansi-regex": "^3.0.0", + "wcwidth": ">=1.0.1" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -7305,6 +7335,15 @@ "makeerror": "1.0.x" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "optional": true, + "requires": { + "defaults": "^1.0.3" + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index 4cf478a5..e3eba08b 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "@slack/web-api": "^5.0.1", "axios": "^0.18.1", "body-parser": "^1.18.3", + "easy-table": "^1.1.1", "express": "^4.16.4", "mysql": "^2.17.1", "reflect-metadata": "^0.1.13", "typeorm": "^0.2.18" }, "devDependencies": { + "@types/easy-table": "0.0.32", "@types/express": "^4.16.1", "@types/jest": "^24.0.15", "@types/lolex": "^3.1.1", diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index f058e9a8..9a7b3b34 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -5,6 +5,7 @@ import { MuzzleService } from "../services/muzzle/muzzle.service"; import { SlackService } from "../services/slack/slack.service"; import { WebService } from "../services/web/web.service"; import { + IChannelResponse, IEventRequest, ISlashCommandRequest } from "../shared/models/slack/slack-models"; @@ -80,3 +81,20 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { res.send(results); } }); + +muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { + const request: ISlashCommandRequest = req.body; + const userId: any = slackService.getUserId(request.text); + if (muzzleService.isUserMuzzled(userId)) { + res.send(`Sorry! Can't do that while muzzled.`); + } + const report = await muzzlePersistenceService.retrieveWeeklyMuzzleReport(); + const response: IChannelResponse = { + response_type: "in_channel", + text: "*Muzzle Report*", + attachments: muzzlePersistenceService.generateFormattedReport(report) + }; + + slackService.sendResponse(request.response_url, response); + res.send("Report sent!"); +}); diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 9313decf..65b7548b 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,15 +1,21 @@ +import Table from "easy-table"; import { getRepository } from "typeorm"; import { Muzzle } from "../../shared/db/models/Muzzle"; +import { IAttachment } from "../../shared/models/slack/slack-models"; +import { SlackService } from "../slack/slack.service"; export class MuzzlePersistenceService { public static getInstance() { if (!MuzzlePersistenceService.instance) { MuzzlePersistenceService.instance = new MuzzlePersistenceService(); + MuzzlePersistenceService.slackService = SlackService.getInstance(); } return MuzzlePersistenceService.instance; } private static instance: MuzzlePersistenceService; + private static slackService: SlackService; + private constructor() {} public addMuzzleToDb(requestorId: string, muzzledId: string, time: number) { @@ -60,4 +66,292 @@ export class MuzzlePersistenceService { this.incrementWordSuppressions(muzzleId, words); this.incrementCharacterSuppressions(muzzleId, characters); } + + /** Wrapper to generate a generic muzzle report in */ + public async retrieveWeeklyMuzzleReport() { + const mostMuzzledByInstances = await this.getMostMuzzledByInstances(); + const mostMuzzledByMessages = await this.getMostMuzzledByMessages(); + const mostMuzzledByWords = await this.getMostMuzzledByWords(); + const mostMuzzledByChars = await this.getMostMuzzledByChars(); + const mostMuzzledByTime = await this.getMostMuzzledByTime(); + + const muzzlerByInstances = await this.getMuzzlerByInstances(); + const muzzlerByMessages = await this.getMuzzlerByMessages(); + const muzzlerByWords = await this.getMuzzlerByWords(); + const muzzlerByChars = await this.getMuzzlerByChars(); + const muzzlerByTime = await this.getMuzzlerByTime(); + + const kdr = await this.getKdr(); + const nemesis = await this.getNemesis(); + + return { + muzzled: { + byInstances: mostMuzzledByInstances, + byMessages: mostMuzzledByMessages, + byWords: mostMuzzledByWords, + byChars: mostMuzzledByChars, + byTime: mostMuzzledByTime + }, + muzzlers: { + byInstances: muzzlerByInstances, + byMessages: muzzlerByMessages, + byWords: muzzlerByWords, + byChars: muzzlerByChars, + byTime: muzzlerByTime + }, + kdr, + nemesis + }; + } + + public generateFormattedReport(report: any): IAttachment[] { + const formattedReport = this.formatReport(report); + const topMuzzledByInstances = { + pretext: "*Top Muzzled by Times Muzzled*", + text: `\`\`\`${Table.print(formattedReport.muzzled.byInstances)}\`\`\`` + }; + + const topMuzzlersByInstances = { + pretext: "*Top Muzzlers*", + text: `\`\`\`${Table.print(formattedReport.muzzlers.byInstances)}\`\`\`` + }; + + const topKdr = { + pretext: "*Top KDR*", + text: `\`\`\`${Table.print(formattedReport.KDR)}\`\`\`` + }; + + const nemesis = { + pretext: "*Top Nemesis*", + text: `\`\`\`${Table.print(formattedReport.nemesis)}\`\`\`` + }; + + const attachments = [ + topMuzzledByInstances, + topMuzzlersByInstances, + topKdr, + nemesis + ]; + + return attachments; + } + + private formatReport(report: any) { + const reportFormatted = { + muzzled: { + byInstances: report.muzzled.byInstances.map((instance: any) => { + return { + user: MuzzlePersistenceService.slackService.getUserById( + instance.muzzledId + )!.name, + timeMuzzled: instance.count + }; + }) + }, + muzzlers: { + byInstances: report.muzzlers.byInstances.map((instance: any) => { + return { + muzzler: MuzzlePersistenceService.slackService.getUserById( + instance.muzzle_requestorId + )!.name, + muzzlesIssued: instance.instanceCount + }; + }) + }, + KDR: report.kdr.map((instance: any) => { + return { + muzzler: MuzzlePersistenceService.slackService.getUserById( + instance.muzzle_requestorId + )!.name, + kdr: instance.kdr, + successfulMuzzles: instance.kills, + totalMuzzles: instance.deaths + }; + }), + nemesis: report.nemesis.map((instance: any) => { + return { + muzzler: MuzzlePersistenceService.slackService.getUserById( + instance.requestorId + )!.name, + muzzled: MuzzlePersistenceService.slackService.getUserById( + instance.muzzledId + )!.name, + timesMuzzled: instance.killCount + }; + }) + }; + + return reportFormatted; + } + + private getMostMuzzledByInstances(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.muzzledId AS muzzledId") + .addSelect("COUNT(*) as count") + .groupBy("muzzle.muzzledId") + .orderBy("count", "DESC") + .getRawMany(); + } + + private getMuzzlerByInstances(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("COUNT(*)", "instanceCount") + .groupBy("muzzle.requestorId") + .orderBy("instanceCount", "DESC") + .getRawMany(); + } + + private getMuzzlerByMessages(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("SUM(muzzle.messagesSuppressed)", "messagesSuppressed") + .groupBy("muzzle.requestorId") + .orderBy("messagesSuppressed", "DESC") + .getRawMany(); + } + + private getMostMuzzledByMessages(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.muzzledId", "muzzledId") + .addSelect("SUM(muzzle.messagesSuppressed)", "messagesSuppressed") + .groupBy("muzzledId") + .orderBy("messagesSuppressed", "DESC") + .getRawMany(); + } + + private getMostMuzzledByWords(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.muzzledId") + .addSelect("SUM(muzzle.wordsSuppressed)", "totalWordsSuppressed") + .groupBy("muzzle.muzzledId") + .orderBy("totalWordsSuppressed", "DESC") + .getRawMany(); + } + + private getMuzzlerByWords(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("SUM(muzzle.wordsSuppressed)", "totalWordsSuppressed") + .groupBy("muzzle.requestorId") + .orderBy("totalWordsSuppressed", "DESC") + .getRawMany(); + } + + private getMostMuzzledByChars(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.muzzledId") + .addSelect("SUM(muzzle.charactersSuppressed)", "totalCharsSuppressed") + .groupBy("muzzle.muzzledId") + .orderBy("totalCharsSuppressed", "DESC") + .getRawMany(); + } + + private getMuzzlerByChars(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("SUM(muzzle.charactersSuppressed)", "totalCharsSuppressed") + .groupBy("muzzle.requestorId") + .orderBy("totalCharsSuppressed", "DESC") + .getRawMany(); + } + + private getMostMuzzledByTime(range?: string) { + if (range) { + console.log(range); + } + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.muzzledId") + .addSelect("SUM(muzzle.milliseconds)", "muzzleTime") + .groupBy("muzzle.muzzledId") + .orderBy("muzzleTime", "DESC") + .getRawMany(); + } + + private getMuzzlerByTime(range?: string) { + if (range) { + console.log(range); + } + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("SUM(muzzle.milliseconds)", "muzzleTime") + .groupBy("muzzle.requestorId") + .orderBy("muzzleTime", "DESC") + .getRawMany(); + } + + private getKdr(range?: string) { + if (range) { + console.log(range); + } + + return getRepository(Muzzle) + .createQueryBuilder("muzzle") + .select("muzzle.requestorId") + .addSelect("SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))/COUNT(*)", "kdr") + .addSelect("SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))", "kills") + .addSelect("COUNT(*)", "deaths") + .groupBy("muzzle.requestorId") + .orderBy("kdr", "DESC") + .getRawMany(); + } + + private getNemesis(range?: string) { + if (range) { + console.log(range); + } + + const getNemesisSqlQuery = `SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount + FROM (SELECT requestorId, muzzledId, COUNT(*) as count FROM muzzle GROUP BY requestorId, muzzledId) AS a + INNER JOIN(SELECT muzzledId, MAX(count) AS count + FROM (SELECT requestorId, muzzledId, COUNT(*) AS count FROM muzzle GROUP BY requestorId, muzzledId) AS c + GROUP BY c.muzzledId) AS b + ON a.muzzledId = b.muzzledId AND a.count = b.count + GROUP BY a.requestorId, a.muzzledId + ORDER BY a.count DESC;`; + + return getRepository(Muzzle).query(getNemesisSqlQuery); + } } From 6af0c6de763c05116a52cce5cc9f46e52c4c1e19 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 15 Jul 2019 20:14:56 -0400 Subject: [PATCH 022/167] Feature/fix formatting (#29) * Fixed formatting * Added type --- src/services/define/define.service.ts | 3 ++- src/services/muzzle/muzzle.persistence.service.ts | 12 ++++++++---- src/shared/models/slack/slack-models.ts | 2 ++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index 5e8745bb..5a8c625d 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -54,7 +54,8 @@ export class DefineService { formattedArr.push({ text: this.formatUrbanD( `${i + 1}. ${this.capitalizeFirstLetter(defArr[i].definition)}` - ) + ), + mrkdown_in: ["text"] }); } return formattedArr; diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 65b7548b..a57fff2a 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -108,22 +108,26 @@ export class MuzzlePersistenceService { const formattedReport = this.formatReport(report); const topMuzzledByInstances = { pretext: "*Top Muzzled by Times Muzzled*", - text: `\`\`\`${Table.print(formattedReport.muzzled.byInstances)}\`\`\`` + text: `\`\`\`${Table.print(formattedReport.muzzled.byInstances)}\`\`\``, + mrkdwn_in: ["text", "pretext"] }; const topMuzzlersByInstances = { pretext: "*Top Muzzlers*", - text: `\`\`\`${Table.print(formattedReport.muzzlers.byInstances)}\`\`\`` + text: `\`\`\`${Table.print(formattedReport.muzzlers.byInstances)}\`\`\``, + mrkdwn_in: ["text", "pretext"] }; const topKdr = { pretext: "*Top KDR*", - text: `\`\`\`${Table.print(formattedReport.KDR)}\`\`\`` + text: `\`\`\`${Table.print(formattedReport.KDR)}\`\`\``, + mrkdwn_in: ["text", "pretext"] }; const nemesis = { pretext: "*Top Nemesis*", - text: `\`\`\`${Table.print(formattedReport.nemesis)}\`\`\`` + text: `\`\`\`${Table.print(formattedReport.nemesis)}\`\`\``, + mrkdwn_in: ["text", "pretext"] }; const attachments = [ diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index 2bec1c64..17c81f15 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -41,6 +41,8 @@ export interface IEvent { export interface IAttachment { text: string; + pretext?: string; + mrkdown_in?: string[]; } export interface ISlackUser { From b5a068e5e997724199203a1ed841c12ac4b64a33 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 21 Jul 2019 20:48:52 -0400 Subject: [PATCH 023/167] added support for clapper, related tests and validation in controller for clap and mock (#30) --- src/controllers/clap.controller.ts | 36 ++++++++++++++++++++++++++ src/controllers/mock.controller.ts | 2 ++ src/services/clap/clap.service.spec.ts | 25 ++++++++++++++++++ src/services/clap/clap.service.ts | 13 ++++++++++ 4 files changed, 76 insertions(+) create mode 100644 src/controllers/clap.controller.ts create mode 100644 src/services/clap/clap.service.spec.ts create mode 100644 src/services/clap/clap.service.ts diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts new file mode 100644 index 00000000..910ac5b8 --- /dev/null +++ b/src/controllers/clap.controller.ts @@ -0,0 +1,36 @@ +import express, { Router } from "express"; +import { ClapService } from "../services/clap/clap.service"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { SlackService } from "../services/slack/slack.service"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const clapController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const clapService = new ClapService(); + +clapController.post("/clap", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to clap."); + } else { + const clapped: string = clapService.clap(request.text); + const response: IChannelResponse = { + attachments: [ + { + text: clapped + } + ], + response_type: "in_channel", + text: `<@${request.user_id}>` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); diff --git a/src/controllers/mock.controller.ts b/src/controllers/mock.controller.ts index 7ca3bf3e..3488794a 100644 --- a/src/controllers/mock.controller.ts +++ b/src/controllers/mock.controller.ts @@ -17,6 +17,8 @@ mockController.post("/mock", (req, res) => { const request: ISlashCommandRequest = req.body; if (muzzleService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to mock."); } else { const mocked: string = mockService.mock(request.text); const response: IChannelResponse = { diff --git a/src/services/clap/clap.service.spec.ts b/src/services/clap/clap.service.spec.ts new file mode 100644 index 00000000..5267ecbd --- /dev/null +++ b/src/services/clap/clap.service.spec.ts @@ -0,0 +1,25 @@ +import { ClapService } from "./clap.service"; + +describe("ClapService", () => { + let clapService: ClapService; + + beforeEach(() => { + clapService = new ClapService(); + }); + + describe("clap()", () => { + it("should clap a users input with a single word", () => { + expect(clapService.clap("test")).toBe("test :clap: "); + }); + + it("should clap a users input with multiple words", () => { + expect(clapService.clap("test this out")).toBe( + "test :clap: this :clap: out :clap: " + ); + }); + + it("should return input if it is an empty string", () => { + expect(clapService.clap("")).toBe(""); + }); + }); +}); diff --git a/src/services/clap/clap.service.ts b/src/services/clap/clap.service.ts new file mode 100644 index 00000000..ad7c6a7f --- /dev/null +++ b/src/services/clap/clap.service.ts @@ -0,0 +1,13 @@ +export class ClapService { + public clap(text: string) { + if (!text || text.length === 0) { + return text; + } + let output = ""; + const words = text.split(" "); + for (const word of words) { + output += `${word} :clap: `; + } + return output; + } +} From 5ec44446283d7a6e36a1ed2bc7adff74ac3c3a9f Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 21 Jul 2019 20:50:10 -0400 Subject: [PATCH 024/167] Feature/report service (#31) * Removed unnecessary logging * Converted to uploading a file rather than sending a message * Removed unused import * Removed filename attribute * Added logging * Added initial comment * Added loging for body * Passed in proper Id * Added markdown formatting * Added better formatting * Removed formatting * Formatting * More formatting * removed markdown formatting * Added botuser token --- src/controllers/muzzle.controller.ts | 17 ++-- .../muzzle/muzzle.persistence.service.ts | 91 +------------------ src/services/report/report.service.ts | 72 +++++++++++++++ src/services/slack/slack.service.ts | 4 +- src/services/web/web.service.ts | 18 ++++ 5 files changed, 99 insertions(+), 103 deletions(-) create mode 100644 src/services/report/report.service.ts diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 9a7b3b34..aa33e1cf 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -2,10 +2,10 @@ import express, { Request, Response, Router } from "express"; import { getTimeString } from "../services/muzzle/muzzle-utilities"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { ReportService } from "../services/report/report.service"; import { SlackService } from "../services/slack/slack.service"; import { WebService } from "../services/web/web.service"; import { - IChannelResponse, IEventRequest, ISlashCommandRequest } from "../shared/models/slack/slack-models"; @@ -16,6 +16,7 @@ const muzzleService = MuzzleService.getInstance(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; @@ -84,17 +85,13 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; + console.log(req.body); const userId: any = slackService.getUserId(request.text); if (muzzleService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); + } else { + const report = await reportService.getReport(); + webService.uploadFile(req.body.channel_id, report); + res.status(200).send(); } - const report = await muzzlePersistenceService.retrieveWeeklyMuzzleReport(); - const response: IChannelResponse = { - response_type: "in_channel", - text: "*Muzzle Report*", - attachments: muzzlePersistenceService.generateFormattedReport(report) - }; - - slackService.sendResponse(request.response_url, response); - res.send("Report sent!"); }); diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index a57fff2a..ca04e9b3 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,20 +1,15 @@ -import Table from "easy-table"; import { getRepository } from "typeorm"; import { Muzzle } from "../../shared/db/models/Muzzle"; -import { IAttachment } from "../../shared/models/slack/slack-models"; -import { SlackService } from "../slack/slack.service"; export class MuzzlePersistenceService { public static getInstance() { if (!MuzzlePersistenceService.instance) { MuzzlePersistenceService.instance = new MuzzlePersistenceService(); - MuzzlePersistenceService.slackService = SlackService.getInstance(); } return MuzzlePersistenceService.instance; } private static instance: MuzzlePersistenceService; - private static slackService: SlackService; private constructor() {} @@ -68,7 +63,7 @@ export class MuzzlePersistenceService { } /** Wrapper to generate a generic muzzle report in */ - public async retrieveWeeklyMuzzleReport() { + public async retrieveMuzzleReport() { const mostMuzzledByInstances = await this.getMostMuzzledByInstances(); const mostMuzzledByMessages = await this.getMostMuzzledByMessages(); const mostMuzzledByWords = await this.getMostMuzzledByWords(); @@ -104,90 +99,6 @@ export class MuzzlePersistenceService { }; } - public generateFormattedReport(report: any): IAttachment[] { - const formattedReport = this.formatReport(report); - const topMuzzledByInstances = { - pretext: "*Top Muzzled by Times Muzzled*", - text: `\`\`\`${Table.print(formattedReport.muzzled.byInstances)}\`\`\``, - mrkdwn_in: ["text", "pretext"] - }; - - const topMuzzlersByInstances = { - pretext: "*Top Muzzlers*", - text: `\`\`\`${Table.print(formattedReport.muzzlers.byInstances)}\`\`\``, - mrkdwn_in: ["text", "pretext"] - }; - - const topKdr = { - pretext: "*Top KDR*", - text: `\`\`\`${Table.print(formattedReport.KDR)}\`\`\``, - mrkdwn_in: ["text", "pretext"] - }; - - const nemesis = { - pretext: "*Top Nemesis*", - text: `\`\`\`${Table.print(formattedReport.nemesis)}\`\`\``, - mrkdwn_in: ["text", "pretext"] - }; - - const attachments = [ - topMuzzledByInstances, - topMuzzlersByInstances, - topKdr, - nemesis - ]; - - return attachments; - } - - private formatReport(report: any) { - const reportFormatted = { - muzzled: { - byInstances: report.muzzled.byInstances.map((instance: any) => { - return { - user: MuzzlePersistenceService.slackService.getUserById( - instance.muzzledId - )!.name, - timeMuzzled: instance.count - }; - }) - }, - muzzlers: { - byInstances: report.muzzlers.byInstances.map((instance: any) => { - return { - muzzler: MuzzlePersistenceService.slackService.getUserById( - instance.muzzle_requestorId - )!.name, - muzzlesIssued: instance.instanceCount - }; - }) - }, - KDR: report.kdr.map((instance: any) => { - return { - muzzler: MuzzlePersistenceService.slackService.getUserById( - instance.muzzle_requestorId - )!.name, - kdr: instance.kdr, - successfulMuzzles: instance.kills, - totalMuzzles: instance.deaths - }; - }), - nemesis: report.nemesis.map((instance: any) => { - return { - muzzler: MuzzlePersistenceService.slackService.getUserById( - instance.requestorId - )!.name, - muzzled: MuzzlePersistenceService.slackService.getUserById( - instance.muzzledId - )!.name, - timesMuzzled: instance.killCount - }; - }) - }; - - return reportFormatted; - } - private getMostMuzzledByInstances(range?: string) { if (range) { console.log(range); diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts new file mode 100644 index 00000000..367ad5ae --- /dev/null +++ b/src/services/report/report.service.ts @@ -0,0 +1,72 @@ +import Table from "easy-table"; +import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; +import { SlackService } from "../slack/slack.service"; + +export class ReportService { + private slackService = SlackService.getInstance(); + private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + + public async getReport() { + const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport(); + return this.generateFormattedReport(muzzleReport); + } + + private generateFormattedReport(report: any): string { + const formattedReport = this.formatReport(report); + return ` +Muzzle Report + +Top Muzzled by Times Muzzled +${Table.print(formattedReport.muzzled.byInstances)} + +Top Muzzlers +${Table.print(formattedReport.muzzlers.byInstances)} + +Top KDR +${Table.print(formattedReport.KDR)} + +Top Nemesis +${Table.print(formattedReport.nemesis)} +`; + } + + private formatReport(report: any) { + const reportFormatted = { + muzzled: { + byInstances: report.muzzled.byInstances.map((instance: any) => { + return { + user: this.slackService.getUserById(instance.muzzledId)!.name, + timeMuzzled: instance.count + }; + }) + }, + muzzlers: { + byInstances: report.muzzlers.byInstances.map((instance: any) => { + return { + muzzler: this.slackService.getUserById(instance.muzzle_requestorId)! + .name, + muzzlesIssued: instance.instanceCount + }; + }) + }, + KDR: report.kdr.map((instance: any) => { + return { + muzzler: this.slackService.getUserById(instance.muzzle_requestorId)! + .name, + kdr: instance.kdr, + successfulMuzzles: instance.kills, + totalMuzzles: instance.deaths + }; + }), + nemesis: report.nemesis.map((instance: any) => { + return { + muzzler: this.slackService.getUserById(instance.requestorId)!.name, + muzzled: this.slackService.getUserById(instance.muzzledId)!.name, + timesMuzzled: instance.killCount + }; + }) + }; + + return reportFormatted; + } +} diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index 35dd4e63..ab026f78 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -22,9 +22,7 @@ export class SlackService { public sendResponse(responseUrl: string, response: IChannelResponse): void { axios .post(responseUrl, response) - .then(() => - console.log(`Successfully responded to: ${responseUrl}`, response) - ) + .then(() => console.log(`Successfully responded to: ${responseUrl}`)) .catch((e: Error) => console.error(`Error responding: ${e.message} at ${responseUrl}`) ); diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index 9f16a3d0..ff54ef46 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -1,6 +1,7 @@ import { ChatDeleteArguments, ChatPostMessageArguments, + FilesUploadArguments, WebClient } from "@slack/web-api"; @@ -55,4 +56,21 @@ export class WebService { public getAllUsers() { return this.web.users.list(); } + + public uploadFile(channel: string, content: string) { + const muzzleToken: any = process.env.muzzleBotUserToken; + const uploadRequest: FilesUploadArguments = { + channels: channel, + content, + filetype: "markdown", + title: "Muzzle Report", + initial_comment: "A New Muzzle Report has been Generated", + token: muzzleToken + }; + + this.web.files + .upload(uploadRequest) + .then(result => console.log(result)) + .catch(e => console.error(e)); + } } From d24804cca1c196a2ccdd708b4037e4ce0b428b89 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 21 Jul 2019 20:57:58 -0400 Subject: [PATCH 025/167] added clapController to index (#32) --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index cbf2db42..58162044 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import bodyParser from "body-parser"; import express, { Application } from "express"; import "reflect-metadata"; import { createConnection } from "typeorm"; +import { clapController } from "./controllers/clap.controller"; import { defineController } from "./controllers/define.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; @@ -16,6 +17,7 @@ app.use(bodyParser.json()); app.use(mockController); app.use(muzzleController); app.use(defineController); +app.use(clapController); const slackService = SlackService.getInstance(); From 04e573b7ed0ec08b6bfab856191e80754c70ed23 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 26 Jul 2019 23:54:55 -0400 Subject: [PATCH 026/167] Add KDR Report (#34) * Separated out kdr and accuracy * Added support for KDR and accuracy on the report * Fixed typo * fixed other typo * Removed KDR calculation in getKdr temporarily * Added groupBy * Broke out into custom sql query * Fixed typo * Adjusted query' * Adjusted query * Adjusted query * Remove ambiguity * Added a.count = b.count * Added logging * Adjused sql * SQL query * Query * Query * SQl adjustment * Added proper sql query * Adjusted naming in report.service * adjust kdr to be calculated in sql * Added order by kdr * Adjusted successfulMuzzles to reflect the right value * Adjusted deaths to reflect the right value * Adjusted naming on report * Added nemesis by raw and by successful --- .../muzzle/muzzle.persistence.service.ts | 92 ++++++++++++++++--- src/services/report/report.service.ts | 51 +++++++--- 2 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index ca04e9b3..318decf9 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -76,8 +76,11 @@ export class MuzzlePersistenceService { const muzzlerByChars = await this.getMuzzlerByChars(); const muzzlerByTime = await this.getMuzzlerByTime(); + const accuracy = await this.getAccuracy(); const kdr = await this.getKdr(); - const nemesis = await this.getNemesis(); + + const rawNemesis = await this.getNemesisByRaw(); + const successNemesis = await this.getNemesisBySuccessful(); return { muzzled: { @@ -94,8 +97,10 @@ export class MuzzlePersistenceService { byChars: muzzlerByChars, byTime: muzzlerByTime }, + accuracy, kdr, - nemesis + rawNemesis, + successNemesis }; } @@ -237,7 +242,7 @@ export class MuzzlePersistenceService { .getRawMany(); } - private getKdr(range?: string) { + private getAccuracy(range?: string) { if (range) { console.log(range); } @@ -245,28 +250,93 @@ export class MuzzlePersistenceService { return getRepository(Muzzle) .createQueryBuilder("muzzle") .select("muzzle.requestorId") - .addSelect("SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))/COUNT(*)", "kdr") + .addSelect( + "SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))/COUNT(*)", + "accuracy" + ) .addSelect("SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))", "kills") .addSelect("COUNT(*)", "deaths") .groupBy("muzzle.requestorId") - .orderBy("kdr", "DESC") + .orderBy("accuracy", "DESC") .getRawMany(); } - private getNemesis(range?: string) { + private getKdr(range?: string) { if (range) { console.log(range); } - const getNemesisSqlQuery = `SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount - FROM (SELECT requestorId, muzzledId, COUNT(*) as count FROM muzzle GROUP BY requestorId, muzzledId) AS a - INNER JOIN(SELECT muzzledId, MAX(count) AS count - FROM (SELECT requestorId, muzzledId, COUNT(*) AS count FROM muzzle GROUP BY requestorId, muzzledId) AS c - GROUP BY c.muzzledId) AS b + const getKdrQuery = ` + SELECT b.requestorId, a.count AS deaths, b.count as kills, b.count/a.count as kdr + FROM (SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE messagesSuppressed > 0 GROUP BY muzzledId) as a + INNER JOIN ( + SELECT requestorId, COUNT(*) as count + FROM muzzle + WHERE messagesSuppressed > 0 + GROUP BY requestorId + ) AS b + ON a.muzzledId = b.requestorId + GROUP BY b.requestorId, a.count, b.count, kdr + ORDER BY kdr DESC; + `; + return getRepository(Muzzle).query(getKdrQuery); + } + + private getNemesisByRaw(range?: string) { + if (range) { + console.log(range); + } + + const getNemesisSqlQuery = ` + SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount + FROM ( + SELECT requestorId, muzzledId, COUNT(*) as count + FROM muzzle + GROUP BY requestorId, muzzledId + ) AS a + INNER JOIN( + SELECT muzzledId, MAX(count) AS count + FROM ( + SELECT requestorId, muzzledId, COUNT(*) AS count + FROM muzzle + GROUP BY requestorId, muzzledId + ) AS c + GROUP BY c.muzzledId + ) AS b ON a.muzzledId = b.muzzledId AND a.count = b.count GROUP BY a.requestorId, a.muzzledId ORDER BY a.count DESC;`; return getRepository(Muzzle).query(getNemesisSqlQuery); } + + private getNemesisBySuccessful(range?: string) { + if (range) { + console.log(range); + } + + const query = ` + SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount + FROM ( + SELECT requestorId, muzzledId, COUNT(*) as count + FROM muzzle + WHERE messagesSuppressed > 0 + GROUP BY requestorId, muzzledId + ) AS a + INNER JOIN( + SELECT muzzledId, MAX(count) AS count + FROM ( + SELECT requestorId, muzzledId, COUNT(*) AS count + FROM muzzle + WHERE messagesSuppressed > 0 + GROUP BY requestorId, muzzledId + ) AS c + GROUP BY c.muzzledId + ) AS b + ON a.muzzledId = b.muzzledId AND a.count = b.count + GROUP BY a.requestorId, a.muzzledId + ORDER BY a.count DESC;`; + + return getRepository(Muzzle).query(query); + } } diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 367ad5ae..5d453765 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -22,11 +22,17 @@ ${Table.print(formattedReport.muzzled.byInstances)} Top Muzzlers ${Table.print(formattedReport.muzzlers.byInstances)} +Top Accuracy +${Table.print(formattedReport.accuracy)} + Top KDR ${Table.print(formattedReport.KDR)} -Top Nemesis -${Table.print(formattedReport.nemesis)} +Top Nemesis (Raw) +${Table.print(formattedReport.rawNemesis)} + +Top Nemesis (Only Successful) +${Table.print(formattedReport.successNemesis)} `; } @@ -35,34 +41,49 @@ ${Table.print(formattedReport.nemesis)} muzzled: { byInstances: report.muzzled.byInstances.map((instance: any) => { return { - user: this.slackService.getUserById(instance.muzzledId)!.name, - timeMuzzled: instance.count + User: this.slackService.getUserById(instance.muzzledId)!.name, + ["Times Muzzled"]: instance.count }; }) }, muzzlers: { byInstances: report.muzzlers.byInstances.map((instance: any) => { return { - muzzler: this.slackService.getUserById(instance.muzzle_requestorId)! + User: this.slackService.getUserById(instance.muzzle_requestorId)! .name, - muzzlesIssued: instance.instanceCount + ["Muzzles Issued"]: instance.instanceCount }; }) }, - KDR: report.kdr.map((instance: any) => { + accuracy: report.accuracy.map((instance: any) => { return { - muzzler: this.slackService.getUserById(instance.muzzle_requestorId)! + User: this.slackService.getUserById(instance.muzzle_requestorId)! .name, - kdr: instance.kdr, - successfulMuzzles: instance.kills, - totalMuzzles: instance.deaths + Accuracy: instance.accuracy, + Kills: instance.kills, + Deaths: instance.deaths + }; + }), + KDR: report.kdr.map((instance: any) => { + return { + User: this.slackService.getUserById(instance.requestorId)!.name, + KDR: instance.kdr, + Kills: instance.kills, + Deaths: instance.deaths + }; + }), + rawNemesis: report.rawNemesis.map((instance: any) => { + return { + Killer: this.slackService.getUserById(instance.requestorId)!.name, + Victim: this.slackService.getUserById(instance.muzzledId)!.name, + ["Muzzle Attempts"]: instance.killCount }; }), - nemesis: report.nemesis.map((instance: any) => { + successNemesis: report.successNemesis.map((instance: any) => { return { - muzzler: this.slackService.getUserById(instance.requestorId)!.name, - muzzled: this.slackService.getUserById(instance.muzzledId)!.name, - timesMuzzled: instance.killCount + Killer: this.slackService.getUserById(instance.requestorId)!.name, + Victim: this.slackService.getUserById(instance.muzzledId)!.name, + ["Successful Muzzles"]: instance.killCount }; }) }; From 3e8b3a37a820c8361cfa5af74b65d1dcb85edd05 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 7 Aug 2019 19:34:38 -0400 Subject: [PATCH 027/167] Feature/time based reporting (#35) * Removed last clap and support for clapping one word only * Adjusted sql queries to be more direct * Fixed naming added logging * Adjusted sql query to better support date range * Dates will be treated as strings in sql * Fixed invalid sql syntax * Fixed invalid KDR calculation for time based reporting * Added error handling for invalid types * Added formatting on error handling * Added better reporting titles * Fixed overflow error * Added allowance for non-param requested reports to default to AllTime * Fixed empty string check * Further logic tweak * Adjusted titling * Added better testing for report.service * Adjusted formatting of tables * Added a default to 1 when calculating KDR and a user has no deaths * Formatting * Fixed KDR calculation * Debugging mysql divide by 0 * Adjusted created at in kdr * Removed verbose logging on web.service.ts * Added log on getKdr to figure out issue with query * Fixed KDR bug * Removed the query log * Cleaned up muzzle report formatting * Further cleanup of report and logging --- package-lock.json | 5 + package.json | 1 + src/controllers/clap.controller.ts | 2 + src/controllers/muzzle.controller.ts | 26 +- src/services/clap/clap.service.spec.ts | 6 +- src/services/clap/clap.service.ts | 4 +- .../muzzle/muzzle.persistence.service.ts | 436 +++++++++++------- src/services/report/report.service.spec.ts | 80 ++++ src/services/report/report.service.ts | 99 +++- src/services/web/web.service.ts | 6 +- src/shared/models/muzzle/muzzle-models.ts | 14 + 11 files changed, 468 insertions(+), 211 deletions(-) create mode 100644 src/services/report/report.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 7bf5cd28..22f669da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5122,6 +5122,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index e3eba08b..e1060c55 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "body-parser": "^1.18.3", "easy-table": "^1.1.1", "express": "^4.16.4", + "moment": "^2.24.0", "mysql": "^2.17.1", "reflect-metadata": "^0.1.13", "typeorm": "^0.2.18" diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts index 910ac5b8..bd2fad42 100644 --- a/src/controllers/clap.controller.ts +++ b/src/controllers/clap.controller.ts @@ -19,6 +19,8 @@ clapController.post("/clap", (req, res) => { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to clap."); + } else if (request.text.split(" ").length === 1) { + res.send("Sorry, you need more than one words to use clapper."); } else { const clapped: string = clapService.clap(request.text); const response: IChannelResponse = { diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index aa33e1cf..c3c27fe0 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -5,6 +5,7 @@ import { MuzzleService } from "../services/muzzle/muzzle.service"; import { ReportService } from "../services/report/report.service"; import { SlackService } from "../services/slack/slack.service"; import { WebService } from "../services/web/web.service"; +import { ReportType } from "../shared/models/muzzle/muzzle-models"; import { IEventRequest, ISlashCommandRequest @@ -85,13 +86,30 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - console.log(req.body); - const userId: any = slackService.getUserId(request.text); + const userId: any = slackService.getUserId(request.user_id); if (muzzleService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); + } else if (request.text.split(" ").length > 1) { + res.send( + `Sorry! No support for multiple parameters at this time. Please choose one of: \`day\`, \`week\`, \`month\`, \`year\`, \`all\`` + ); + } else if ( + request.text !== "" && + !reportService.isValidReportType(request.text) + ) { + res.send( + `Sorry! You passed in \`${ + request.text + }\` but we can only generate reports for the following values: \`day\`, \`week\`, \`month\`, \`year\`, \`all\`` + ); } else { - const report = await reportService.getReport(); - webService.uploadFile(req.body.channel_id, report); + const reportType: ReportType = reportService.getReportType(request.text); + const report = await reportService.getReport(reportType); + webService.uploadFile( + req.body.channel_id, + report, + reportService.getReportTitle(reportType) + ); res.status(200).send(); } }); diff --git a/src/services/clap/clap.service.spec.ts b/src/services/clap/clap.service.spec.ts index 5267ecbd..0bf802f3 100644 --- a/src/services/clap/clap.service.spec.ts +++ b/src/services/clap/clap.service.spec.ts @@ -8,13 +8,9 @@ describe("ClapService", () => { }); describe("clap()", () => { - it("should clap a users input with a single word", () => { - expect(clapService.clap("test")).toBe("test :clap: "); - }); - it("should clap a users input with multiple words", () => { expect(clapService.clap("test this out")).toBe( - "test :clap: this :clap: out :clap: " + "test :clap: this :clap: out" ); }); diff --git a/src/services/clap/clap.service.ts b/src/services/clap/clap.service.ts index ad7c6a7f..b2abf915 100644 --- a/src/services/clap/clap.service.ts +++ b/src/services/clap/clap.service.ts @@ -5,8 +5,8 @@ export class ClapService { } let output = ""; const words = text.split(" "); - for (const word of words) { - output += `${word} :clap: `; + for (let i = 0; i < words.length; i++) { + output += i !== words.length - 1 ? `${words[i]} :clap: ` : words[i]; } return output; } diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 318decf9..fb367bcc 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,5 +1,10 @@ +import moment from "moment"; import { getRepository } from "typeorm"; import { Muzzle } from "../../shared/db/models/Muzzle"; +import { + IReportRange, + ReportType +} from "../../shared/models/muzzle/muzzle-models"; export class MuzzlePersistenceService { public static getInstance() { @@ -40,6 +45,45 @@ export class MuzzlePersistenceService { ); } + public getRange(reportType: ReportType) { + const range: IReportRange = { + reportType + }; + if (reportType === ReportType.AllTime) { + range.reportType = ReportType.AllTime; + } else if (reportType === ReportType.Day) { + range.start = moment() + .startOf("day") + .format("YYYY-MM-DD HH:mm:ss"); + range.end = moment() + .endOf("day") + .format("YYYY-MM-DD HH:mm:ss"); + } else if (reportType === ReportType.Week) { + range.start = moment() + .startOf("week") + .format("YYYY-MM-DD HH:mm:ss"); + range.end = moment() + .endOf("week") + .format("YYYY-MM-DD HH:mm:ss"); + } else if (reportType === ReportType.Month) { + range.start = moment() + .startOf("month") + .format("YYYY-MM-DD HH:mm:ss"); + range.end = moment() + .endOf("month") + .format("YYYY-MM-DD HH:mm:ss"); + } else if (reportType === ReportType.Year) { + range.start = moment() + .startOf("year") + .format("YYYY-MM-DD HH:mm:ss"); + range.end = moment() + .endOf("year") + .format("YYYY-MM-DD HH:mm:ss"); + } + + return range; + } + public incrementCharacterSuppressions( id: number, charactersSuppressed: number @@ -63,24 +107,28 @@ export class MuzzlePersistenceService { } /** Wrapper to generate a generic muzzle report in */ - public async retrieveMuzzleReport() { - const mostMuzzledByInstances = await this.getMostMuzzledByInstances(); - const mostMuzzledByMessages = await this.getMostMuzzledByMessages(); - const mostMuzzledByWords = await this.getMostMuzzledByWords(); - const mostMuzzledByChars = await this.getMostMuzzledByChars(); - const mostMuzzledByTime = await this.getMostMuzzledByTime(); + public async retrieveMuzzleReport( + reportType: ReportType = ReportType.AllTime + ) { + const range: IReportRange = this.getRange(reportType); + + const mostMuzzledByInstances = await this.getMostMuzzledByInstances(range); + const mostMuzzledByMessages = await this.getMostMuzzledByMessages(range); + const mostMuzzledByWords = await this.getMostMuzzledByWords(range); + const mostMuzzledByChars = await this.getMostMuzzledByChars(range); + const mostMuzzledByTime = await this.getMostMuzzledByTime(range); - const muzzlerByInstances = await this.getMuzzlerByInstances(); - const muzzlerByMessages = await this.getMuzzlerByMessages(); - const muzzlerByWords = await this.getMuzzlerByWords(); - const muzzlerByChars = await this.getMuzzlerByChars(); - const muzzlerByTime = await this.getMuzzlerByTime(); + const muzzlerByInstances = await this.getMuzzlerByInstances(range); + const muzzlerByMessages = await this.getMuzzlerByMessages(range); + const muzzlerByWords = await this.getMuzzlerByWords(range); + const muzzlerByChars = await this.getMuzzlerByChars(range); + const muzzlerByTime = await this.getMuzzlerByTime(range); - const accuracy = await this.getAccuracy(); - const kdr = await this.getKdr(); + const accuracy = await this.getAccuracy(range); + const kdr = await this.getKdr(range); - const rawNemesis = await this.getNemesisByRaw(); - const successNemesis = await this.getNemesisBySuccessful(); + const rawNemesis = await this.getNemesisByRaw(range); + const successNemesis = await this.getNemesisBySuccessful(range); return { muzzled: { @@ -104,190 +152,190 @@ export class MuzzlePersistenceService { }; } - private getMostMuzzledByInstances(range?: string) { - if (range) { - console.log(range); - } + private getMostMuzzledByInstances(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT muzzledId, COUNT(*) as count FROM muzzle GROUP BY muzzledId ORDER BY count DESC;` + : `SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY muzzledId ORDER BY count DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.muzzledId AS muzzledId") - .addSelect("COUNT(*) as count") - .groupBy("muzzle.muzzledId") - .orderBy("count", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMuzzlerByInstances(range?: string) { - if (range) { - console.log(range); - } + private getMuzzlerByInstances(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, COUNT(*) as instanceCount FROM muzzle GROUP BY requestorId ORDER BY instanceCount DESC;` + : `SELECT requestorId, COUNT(*) as instanceCount FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY instanceCount DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect("COUNT(*)", "instanceCount") - .groupBy("muzzle.requestorId") - .orderBy("instanceCount", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMuzzlerByMessages(range?: string) { - if (range) { - console.log(range); - } + private getMuzzlerByMessages(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle GROUP BY requestorId ORDER BY messagesSuppressed DESC;` + : `SELECT requestorId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY messagesSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect("SUM(muzzle.messagesSuppressed)", "messagesSuppressed") - .groupBy("muzzle.requestorId") - .orderBy("messagesSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMostMuzzledByMessages(range?: string) { - if (range) { - console.log(range); - } + private getMostMuzzledByMessages(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT muzzledId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle GROUP BY muzzledId ORDER BY messagesSuppressed DESC;` + : `SELECT muzzledId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY muzzledId ORDER BY messagesSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.muzzledId", "muzzledId") - .addSelect("SUM(muzzle.messagesSuppressed)", "messagesSuppressed") - .groupBy("muzzledId") - .orderBy("messagesSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMostMuzzledByWords(range?: string) { - if (range) { - console.log(range); - } + private getMostMuzzledByWords(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT muzzledId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle GROUP BY muzzledId ORDER BY wordsSuppressed DESC;` + : `SELECT muzzledId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY muzzledId ORDER BY wordsSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.muzzledId") - .addSelect("SUM(muzzle.wordsSuppressed)", "totalWordsSuppressed") - .groupBy("muzzle.muzzledId") - .orderBy("totalWordsSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMuzzlerByWords(range?: string) { - if (range) { - console.log(range); - } + private getMuzzlerByWords(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle GROUP BY requestorId ORDER BY wordsSuppressed DESC;` + : `SELECT requestorId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY wordsSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect("SUM(muzzle.wordsSuppressed)", "totalWordsSuppressed") - .groupBy("muzzle.requestorId") - .orderBy("totalWordsSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMostMuzzledByChars(range?: string) { - if (range) { - console.log(range); - } + private getMostMuzzledByChars(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT muzzledId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle GROUP BY muzzledId ORDER BY charactersSuppressed DESC;` + : `SELECT muzzledId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY muzzledId ORDER BY charactersSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.muzzledId") - .addSelect("SUM(muzzle.charactersSuppressed)", "totalCharsSuppressed") - .groupBy("muzzle.muzzledId") - .orderBy("totalCharsSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMuzzlerByChars(range?: string) { - if (range) { - console.log(range); - } + private getMuzzlerByChars(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle GROUP BY requestorId ORDER BY charactersSuppressed DESC;` + : `SELECT requestorId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY charactersSuppressed DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect("SUM(muzzle.charactersSuppressed)", "totalCharsSuppressed") - .groupBy("muzzle.requestorId") - .orderBy("totalCharsSuppressed", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getMostMuzzledByTime(range?: string) { - if (range) { - console.log(range); - } - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.muzzledId") - .addSelect("SUM(muzzle.milliseconds)", "muzzleTime") - .groupBy("muzzle.muzzledId") - .orderBy("muzzleTime", "DESC") - .getRawMany(); - } + private getMostMuzzledByTime(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT muzzledId, SUM(milliseconds) as muzzleTime FROM muzzle GROUP BY muzzledId ORDER BY muzzleTime DESC;` + : `SELECT muzzledId, SUM(milliseconds) as muzzleTime FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY muzzledId ORDER BY muzzleTime DESC;`; - private getMuzzlerByTime(range?: string) { - if (range) { - console.log(range); - } - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect("SUM(muzzle.milliseconds)", "muzzleTime") - .groupBy("muzzle.requestorId") - .orderBy("muzzleTime", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getAccuracy(range?: string) { - if (range) { - console.log(range); - } + private getMuzzlerByTime(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, SUM(milliseconds) as muzzleTime FROM muzzle GROUP BY requestorId ORDER BY muzzleTime DESC;` + : `SELECT requestorId, SUM(milliseconds) as muzzleTime FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY muzzleTime DESC;`; - return getRepository(Muzzle) - .createQueryBuilder("muzzle") - .select("muzzle.requestorId") - .addSelect( - "SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))/COUNT(*)", - "accuracy" - ) - .addSelect("SUM(IF(muzzle.messagesSuppressed > 0, 1, 0))", "kills") - .addSelect("COUNT(*)", "deaths") - .groupBy("muzzle.requestorId") - .orderBy("accuracy", "DESC") - .getRawMany(); + return getRepository(Muzzle).query(query); } - private getKdr(range?: string) { - if (range) { - console.log(range); - } + private getAccuracy(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? `SELECT requestorId, SUM(IF(messagesSuppressed > 0, 1, 0))/COUNT(*) as accuracy, SUM(IF(muzzle.messagesSuppressed > 0, 1, 0)) as kills, COUNT(*) as deaths + FROM muzzle GROUP BY requestorId ORDER BY accuracy DESC;` + : `SELECT requestorId, SUM(IF(messagesSuppressed > 0, 1, 0))/COUNT(*) as accuracy, SUM(IF(muzzle.messagesSuppressed > 0, 1, 0)) as kills, COUNT(*) as deaths FROM muzzle WHERE createdAt >= '${ + range.start + }' AND createdAt < '${ + range.end + }' GROUP BY requestorId ORDER BY accuracy DESC;`; - const getKdrQuery = ` - SELECT b.requestorId, a.count AS deaths, b.count as kills, b.count/a.count as kdr - FROM (SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE messagesSuppressed > 0 GROUP BY muzzledId) as a - INNER JOIN ( - SELECT requestorId, COUNT(*) as count - FROM muzzle - WHERE messagesSuppressed > 0 - GROUP BY requestorId - ) AS b - ON a.muzzledId = b.requestorId - GROUP BY b.requestorId, a.count, b.count, kdr - ORDER BY kdr DESC; - `; - return getRepository(Muzzle).query(getKdrQuery); + return getRepository(Muzzle).query(query); } - private getNemesisByRaw(range?: string) { - if (range) { - console.log(range); - } + private getKdr(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? ` + SELECT b.requestorId, IF(a.count > 0, a.count, 0) AS deaths, b.count as kills, b.count/IF(a.count > 0, a.count, 1) as kdr + FROM (SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE messagesSuppressed > 0 GROUP BY muzzledId) as a + RIGHT JOIN ( + SELECT requestorId, COUNT(*) as count + FROM muzzle + GROUP BY requestorId + ) AS b + ON a.muzzledId = b.requestorId + GROUP BY b.requestorId, a.count, b.count, kdr + ORDER BY kdr DESC; + ` + : ` + SELECT b.requestorId, IF(a.count > 0, a.count, 0) AS deaths, b.count as kills, b.count/IF(a.count > 0, a.count, 1) as kdr + FROM (SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE messagesSuppressed > 0 AND createdAt >= '${ + range.start + }' AND createdAt <= '${range.end}' GROUP BY muzzledId) as a + RIGHT JOIN ( + SELECT requestorId, COUNT(*) as count + FROM muzzle + WHERE messagesSuppressed > 0 AND createdAt >= '${ + range.start + }' AND createdAt <= '${range.end}' + GROUP BY requestorId + ) AS b + ON a.muzzledId = b.requestorId + GROUP BY b.requestorId, a.count, b.count, kdr + ORDER BY kdr DESC; + `; + + return getRepository(Muzzle).query(query); + } - const getNemesisSqlQuery = ` + private getNemesisByRaw(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? ` SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount FROM ( SELECT requestorId, muzzledId, COUNT(*) as count @@ -305,17 +353,36 @@ export class MuzzlePersistenceService { ) AS b ON a.muzzledId = b.muzzledId AND a.count = b.count GROUP BY a.requestorId, a.muzzledId + ORDER BY a.count DESC;` + : ` + SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount + FROM ( + SELECT requestorId, muzzledId, COUNT(*) as count + FROM muzzle + WHERE createdAt >= '${range.start}' AND createdAt < '${range.end}' + GROUP BY requestorId, muzzledId + ) AS a + INNER JOIN( + SELECT muzzledId, MAX(count) AS count + FROM ( + SELECT requestorId, muzzledId, COUNT(*) AS count + FROM muzzle + WHERE createdAt >= '${range.start}' AND createdAt < '${range.end}' + GROUP BY requestorId, muzzledId + ) AS c + GROUP BY c.muzzledId + ) AS b + ON a.muzzledId = b.muzzledId AND a.count = b.count + GROUP BY a.requestorId, a.muzzledId ORDER BY a.count DESC;`; - return getRepository(Muzzle).query(getNemesisSqlQuery); + return getRepository(Muzzle).query(query); } - private getNemesisBySuccessful(range?: string) { - if (range) { - console.log(range); - } - - const query = ` + private getNemesisBySuccessful(range: IReportRange) { + const query = + range.reportType === ReportType.AllTime + ? ` SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount FROM ( SELECT requestorId, muzzledId, COUNT(*) as count @@ -335,6 +402,31 @@ export class MuzzlePersistenceService { ) AS b ON a.muzzledId = b.muzzledId AND a.count = b.count GROUP BY a.requestorId, a.muzzledId + ORDER BY a.count DESC;` + : ` + SELECT a.requestorId, a.muzzledId, MAX(a.count) as killCount + FROM ( + SELECT requestorId, muzzledId, COUNT(*) as count + FROM muzzle + WHERE createdAt >= '${range.start}' AND createdAt < '${ + range.end + }' AND messagesSuppressed > 0 + GROUP BY requestorId, muzzledId + ) AS a + INNER JOIN( + SELECT muzzledId, MAX(count) AS count + FROM ( + SELECT requestorId, muzzledId, COUNT(*) AS count + FROM muzzle + WHERE createdAt >= '${range.start}' AND createdAt < '${ + range.end + }' AND messagesSuppressed > 0 + GROUP BY requestorId, muzzledId + ) AS c + GROUP BY c.muzzledId + ) AS b + ON a.muzzledId = b.muzzledId AND a.count = b.count + GROUP BY a.requestorId, a.muzzledId ORDER BY a.count DESC;`; return getRepository(Muzzle).query(query); diff --git a/src/services/report/report.service.spec.ts b/src/services/report/report.service.spec.ts new file mode 100644 index 00000000..4c14544c --- /dev/null +++ b/src/services/report/report.service.spec.ts @@ -0,0 +1,80 @@ +import { ReportType } from "../../shared/models/muzzle/muzzle-models"; +import { ReportService } from "./report.service"; + +describe("ReportService", () => { + let mockService: ReportService; + beforeEach(() => { + mockService = new ReportService(); + }); + describe("getReportType()", () => { + describe(" - with valid report types", () => { + it("should return ReportType.Day when day is passed in with any case", () => { + expect(mockService.getReportType("day")).toBe(ReportType.Day); + expect(mockService.getReportType("Day")).toBe(ReportType.Day); + expect(mockService.getReportType("DAY")).toBe(ReportType.Day); + }); + it("should return ReportType.Week when week is passed in with any case", () => { + expect(mockService.getReportType("week")).toBe(ReportType.Week); + expect(mockService.getReportType("Week")).toBe(ReportType.Week); + expect(mockService.getReportType("WEEK")).toBe(ReportType.Week); + }); + it("should return ReportType.Month when month is passed in with any case", () => { + expect(mockService.getReportType("month")).toBe(ReportType.Month); + expect(mockService.getReportType("Month")).toBe(ReportType.Month); + expect(mockService.getReportType("MONTH")).toBe(ReportType.Month); + }); + it("should return ReportType.Year when year is passed in with any case", () => { + expect(mockService.getReportType("year")).toBe(ReportType.Year); + expect(mockService.getReportType("Year")).toBe(ReportType.Year); + expect(mockService.getReportType("YEAR")).toBe(ReportType.Year); + }); + it("should return ReportType.AllTime when all is passed in with any case", () => { + expect(mockService.getReportType("all")).toBe(ReportType.AllTime); + expect(mockService.getReportType("All")).toBe(ReportType.AllTime); + expect(mockService.getReportType("ALL")).toBe(ReportType.AllTime); + }); + }); + + describe("- with invalid report type", () => { + it("should return ReportType.AllTime when an invalid report type is passed in", () => { + expect(mockService.getReportType("whatever")).toBe(ReportType.AllTime); + }); + }); + }); + + describe("isValidReportType()", () => { + it("should return true when day is passed in with any case", () => { + expect(mockService.isValidReportType("day")).toBe(true); + expect(mockService.isValidReportType("Day")).toBe(true); + expect(mockService.isValidReportType("DAY")).toBe(true); + }); + it("should return true when week is passed in with any case", () => { + expect(mockService.isValidReportType("week")).toBe(true); + expect(mockService.isValidReportType("Week")).toBe(true); + expect(mockService.isValidReportType("WEEK")).toBe(true); + }); + it("should return true when month is passed in with any case", () => { + expect(mockService.isValidReportType("month")).toBe(true); + expect(mockService.isValidReportType("Month")).toBe(true); + expect(mockService.isValidReportType("MONTH")).toBe(true); + }); + it("should return true when year is passed in with any case", () => { + expect(mockService.isValidReportType("year")).toBe(true); + expect(mockService.isValidReportType("Year")).toBe(true); + expect(mockService.isValidReportType("YEAR")).toBe(true); + }); + it("should return true when all is passed in with any case", () => { + expect(mockService.isValidReportType("all")).toBe(true); + expect(mockService.isValidReportType("All")).toBe(true); + expect(mockService.isValidReportType("ALL")).toBe(true); + }); + it("should return false when a non-valid reportType is passed in", () => { + expect(mockService.isValidReportType("whatever")).toBe(false); + }); + it("should return false for a sentence", () => { + expect( + mockService.isValidReportType("test sentence that should fail day") + ).toBe(false); + }); + }); +}); diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 5d453765..6a69dd14 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -1,4 +1,6 @@ import Table from "easy-table"; +import moment from "moment"; +import { ReportType } from "../../shared/models/muzzle/muzzle-models"; import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; import { SlackService } from "../slack/slack.service"; @@ -6,33 +8,82 @@ export class ReportService { private slackService = SlackService.getInstance(); private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); - public async getReport() { - const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport(); - return this.generateFormattedReport(muzzleReport); + public async getReport(reportType: ReportType) { + const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport( + reportType + ); + return this.generateFormattedReport(muzzleReport, reportType); } - private generateFormattedReport(report: any): string { + // There has got to be a better way to do this. This is sooo ugly and requires added maintenance + public isValidReportType(type: string) { + const lowerCaseType = type.toLowerCase(); + return ( + lowerCaseType === ReportType.Day || + lowerCaseType === ReportType.Week || + lowerCaseType === ReportType.Month || + lowerCaseType === ReportType.Year || + lowerCaseType === ReportType.AllTime + ); + } + + public getReportType(type: string): ReportType { + const lowerCaseType: string = type.toLowerCase(); + if ( + lowerCaseType === ReportType.Day || + lowerCaseType === ReportType.Week || + lowerCaseType === ReportType.Month || + lowerCaseType === ReportType.Year || + lowerCaseType === ReportType.AllTime + ) { + return lowerCaseType as ReportType; + } + return ReportType.AllTime; + } + + public getReportTitle(type: ReportType) { + const range = this.muzzlePersistenceService.getRange(type); + const titles = { + [ReportType.Day]: `Daily Muzzle Report for ${moment(range.start).format( + "MM-DD-YYYY" + )}`, + [ReportType.Week]: `Weekly Muzzle Report for ${moment(range.start).format( + "MM-DD-YYYY" + )} to ${moment(range.end).format("MM-DD-YYYY")}`, + [ReportType.Month]: `Monthly Muzzle Report for ${moment( + range.start + ).format("MM-DD-YYYY")} to ${moment(range.end).format("MM-DD-YYYY")}`, + [ReportType.Year]: `Annual Muzzle Report for ${moment(range.start).format( + "MM-DD-YYYY" + )} to ${moment(range.end).format("MM-DD-YYYY")}`, + [ReportType.AllTime]: "All Time Muzzle Report" + }; + + return titles[type]; + } + + private generateFormattedReport(report: any, reportType: ReportType): string { const formattedReport = this.formatReport(report); return ` -Muzzle Report +${this.getReportTitle(reportType)} -Top Muzzled by Times Muzzled -${Table.print(formattedReport.muzzled.byInstances)} + Top Muzzled + ${Table.print(formattedReport.muzzled.byInstances)} -Top Muzzlers -${Table.print(formattedReport.muzzlers.byInstances)} + Top Muzzlers + ${Table.print(formattedReport.muzzlers.byInstances)} -Top Accuracy -${Table.print(formattedReport.accuracy)} + Top Accuracy + ${Table.print(formattedReport.accuracy)} -Top KDR -${Table.print(formattedReport.KDR)} + Top KDR + ${Table.print(formattedReport.KDR)} -Top Nemesis (Raw) -${Table.print(formattedReport.rawNemesis)} + Top Nemesis by Attempts + ${Table.print(formattedReport.rawNemesis)} -Top Nemesis (Only Successful) -${Table.print(formattedReport.successNemesis)} + Top Nemesis by Kills + ${Table.print(formattedReport.successNemesis)} `; } @@ -42,26 +93,24 @@ ${Table.print(formattedReport.successNemesis)} byInstances: report.muzzled.byInstances.map((instance: any) => { return { User: this.slackService.getUserById(instance.muzzledId)!.name, - ["Times Muzzled"]: instance.count + Muzzles: instance.count }; }) }, muzzlers: { byInstances: report.muzzlers.byInstances.map((instance: any) => { return { - User: this.slackService.getUserById(instance.muzzle_requestorId)! - .name, + User: this.slackService.getUserById(instance.requestorId)!.name, ["Muzzles Issued"]: instance.instanceCount }; }) }, accuracy: report.accuracy.map((instance: any) => { return { - User: this.slackService.getUserById(instance.muzzle_requestorId)! - .name, + User: this.slackService.getUserById(instance.requestorId)!.name, Accuracy: instance.accuracy, Kills: instance.kills, - Deaths: instance.deaths + Attempts: instance.deaths }; }), KDR: report.kdr.map((instance: any) => { @@ -76,14 +125,14 @@ ${Table.print(formattedReport.successNemesis)} return { Killer: this.slackService.getUserById(instance.requestorId)!.name, Victim: this.slackService.getUserById(instance.muzzledId)!.name, - ["Muzzle Attempts"]: instance.killCount + Attempts: instance.killCount }; }), successNemesis: report.successNemesis.map((instance: any) => { return { Killer: this.slackService.getUserById(instance.requestorId)!.name, Victim: this.slackService.getUserById(instance.muzzledId)!.name, - ["Successful Muzzles"]: instance.killCount + Kills: instance.killCount }; }) }; diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index ff54ef46..f7442d08 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -57,20 +57,20 @@ export class WebService { return this.web.users.list(); } - public uploadFile(channel: string, content: string) { + public uploadFile(channel: string, content: string, title?: string) { const muzzleToken: any = process.env.muzzleBotUserToken; const uploadRequest: FilesUploadArguments = { channels: channel, content, filetype: "markdown", title: "Muzzle Report", - initial_comment: "A New Muzzle Report has been Generated", + initial_comment: title, token: muzzleToken }; this.web.files .upload(uploadRequest) - .then(result => console.log(result)) + .then(() => console.log("Uploaded new report successfully!")) .catch(e => console.error(e)); } } diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 06fc436b..d961b9b3 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -9,3 +9,17 @@ export interface IRequestor { muzzleCount: number; muzzleCountRemover?: NodeJS.Timeout; } + +export enum ReportType { + Week = "week", + Day = "day", + Month = "month", + Year = "year", + AllTime = "all" +} + +export interface IReportRange { + start?: string; + end?: string; + reportType: ReportType; +} From 561230932048113829e742c86d0f157b51ac2eb0 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 7 Aug 2019 19:54:29 -0400 Subject: [PATCH 028/167] Feature/confessions (#36) * Added confession controller * Updated route name for confession controller --- src/controllers/confession.controller.ts | 24 ++++++++++++++++++++++++ src/index.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/controllers/confession.controller.ts diff --git a/src/controllers/confession.controller.ts b/src/controllers/confession.controller.ts new file mode 100644 index 00000000..15abaed0 --- /dev/null +++ b/src/controllers/confession.controller.ts @@ -0,0 +1,24 @@ +import express, { Router } from "express"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { WebService } from "../services/web/web.service"; +import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; + +export const confessionController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const webService = WebService.getInstance(); + +confessionController.post("/confess", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to confess."); + } else { + const confession: string = `Someone has confessed: + \`${request.text}\``; + // Hardcodeded, maybe not the best idea. + webService.sendMessage("#confessions", confession); + res.status(200).send(); + } +}); diff --git a/src/index.ts b/src/index.ts index 58162044..59c79787 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import express, { Application } from "express"; import "reflect-metadata"; import { createConnection } from "typeorm"; import { clapController } from "./controllers/clap.controller"; +import { confessionController } from "./controllers/confession.controller"; import { defineController } from "./controllers/define.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; @@ -18,6 +19,7 @@ app.use(mockController); app.use(muzzleController); app.use(defineController); app.use(clapController); +app.use(confessionController); const slackService = SlackService.getInstance(); From 6dc8a0559c44700545b400cf0d8a55e844b0f301 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 8 Aug 2019 21:15:57 -0400 Subject: [PATCH 029/167] Feature/the list (#37) * Added model for List * Added a super basic controller and service * Added report for list * Fixed username * Added some slight error handling to remove * Added removal by text instead of ID and adjusted report to have better naming * Added limit on characters when adding items * Fixed error text for list removal * Adjusted message format for adding and removing from the list * Verbiage adjustments --- src/controllers/list.controller.ts | 69 +++++++++++++++++++ src/controllers/muzzle.controller.ts | 2 +- src/index.ts | 2 + src/services/list/list.persistence.service.ts | 36 ++++++++++ src/services/report/report.service.ts | 33 ++++++--- src/shared/db/models/List.ts | 16 +++++ src/shared/models/slack/slack-models.ts | 2 +- 7 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 src/controllers/list.controller.ts create mode 100644 src/services/list/list.persistence.service.ts create mode 100644 src/shared/db/models/List.ts diff --git a/src/controllers/list.controller.ts b/src/controllers/list.controller.ts new file mode 100644 index 00000000..84cae3ab --- /dev/null +++ b/src/controllers/list.controller.ts @@ -0,0 +1,69 @@ +import express, { Router } from "express"; +import { ListPersistenceService } from "../services/list/list.persistence.service"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { ReportService } from "../services/report/report.service"; +import { SlackService } from "../services/slack/slack.service"; +import { WebService } from "../services/web/web.service"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const listController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const webService = WebService.getInstance(); +const listPersistenceService = ListPersistenceService.getInstance(); +const reportService = new ReportService(); + +listController.post("/list/retrieve", async (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + const report = await reportService.getListReport(); + webService.uploadFile(req.body.channel_id, report, "The List"); + res.status(200).send(); + } +}); + +listController.post("/list/add", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to list something."); + } else if (request.text.length >= 255) { + res.send("Sorry, items added to The List must be less than 255 characters"); + } else { + listPersistenceService.store(request.user_id, request.text); + const response: IChannelResponse = { + response_type: "in_channel", + text: `\`${request.text}\` has been \`listed\`` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); + +listController.post("/list/remove", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send the item you wish to remove."); + } else { + listPersistenceService + .remove(request.text) + .then(() => { + const response: IChannelResponse = { + response_type: "in_channel", + text: `\`${request.text}\` has been removed from \`The List\`` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + }) + .catch(e => res.send(e)); + } +}); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index c3c27fe0..49cf293c 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -104,7 +104,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { ); } else { const reportType: ReportType = reportService.getReportType(request.text); - const report = await reportService.getReport(reportType); + const report = await reportService.getMuzzleReport(reportType); webService.uploadFile( req.body.channel_id, report, diff --git a/src/index.ts b/src/index.ts index 59c79787..a0e75293 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { createConnection } from "typeorm"; import { clapController } from "./controllers/clap.controller"; import { confessionController } from "./controllers/confession.controller"; import { defineController } from "./controllers/define.controller"; +import { listController } from "./controllers/list.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; import { config } from "./ormconfig"; @@ -20,6 +21,7 @@ app.use(muzzleController); app.use(defineController); app.use(clapController); app.use(confessionController); +app.use(listController); const slackService = SlackService.getInstance(); diff --git a/src/services/list/list.persistence.service.ts b/src/services/list/list.persistence.service.ts new file mode 100644 index 00000000..291e13ae --- /dev/null +++ b/src/services/list/list.persistence.service.ts @@ -0,0 +1,36 @@ +import { getRepository } from "typeorm"; +import { List } from "../../shared/db/models/List"; + +export class ListPersistenceService { + public static getInstance() { + if (!ListPersistenceService.instance) { + ListPersistenceService.instance = new ListPersistenceService(); + } + return ListPersistenceService.instance; + } + + private static instance: ListPersistenceService; + + private constructor() {} + + public store(requestorId: string, text: string) { + const listItem = new List(); + listItem.requestorId = requestorId; + listItem.text = text; + return getRepository(List).save(listItem); + } + + public retrieve() { + return getRepository(List).find(); + } + + public remove(text: string) { + return new Promise(async (resolve, reject) => { + const item = await getRepository(List).findOne({ text }); + if (item) { + return resolve(getRepository(List).remove(item)); + } + reject(`Unable to find \`${text}\``); + }); + } +} diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 6a69dd14..a7907967 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -1,21 +1,28 @@ import Table from "easy-table"; import moment from "moment"; +import { List } from "../../shared/db/models/List"; import { ReportType } from "../../shared/models/muzzle/muzzle-models"; +import { ListPersistenceService } from "../list/list.persistence.service"; import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; import { SlackService } from "../slack/slack.service"; export class ReportService { private slackService = SlackService.getInstance(); private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + private listPersistenceService = ListPersistenceService.getInstance(); - public async getReport(reportType: ReportType) { + public async getListReport() { + const listReport = await this.listPersistenceService.retrieve(); + return this.formatListReport(listReport); + } + + public async getMuzzleReport(reportType: ReportType) { const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport( reportType ); return this.generateFormattedReport(muzzleReport, reportType); } - // There has got to be a better way to do this. This is sooo ugly and requires added maintenance public isValidReportType(type: string) { const lowerCaseType = type.toLowerCase(); return ( @@ -29,13 +36,7 @@ export class ReportService { public getReportType(type: string): ReportType { const lowerCaseType: string = type.toLowerCase(); - if ( - lowerCaseType === ReportType.Day || - lowerCaseType === ReportType.Week || - lowerCaseType === ReportType.Month || - lowerCaseType === ReportType.Year || - lowerCaseType === ReportType.AllTime - ) { + if (this.isValidReportType(type)) { return lowerCaseType as ReportType; } return ReportType.AllTime; @@ -62,6 +63,20 @@ export class ReportService { return titles[type]; } + private formatListReport(report: any) { + const reportWithoutDate = report.map((listItem: List) => { + return { + Item: listItem.text, + "Added By": this.slackService.getUserName(listItem.requestorId) + }; + }); + + return ` +The List + +${Table.print(reportWithoutDate)} +`; + } private generateFormattedReport(report: any, reportType: ReportType): string { const formattedReport = this.formatReport(report); return ` diff --git a/src/shared/db/models/List.ts b/src/shared/db/models/List.ts new file mode 100644 index 00000000..a66acab3 --- /dev/null +++ b/src/shared/db/models/List.ts @@ -0,0 +1,16 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class List { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public requestorId!: string; + + @Column() + public text!: string; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + public createdAt!: Date; +} diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index 17c81f15..eaefd3b1 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -1,7 +1,7 @@ export interface IChannelResponse { response_type: string; text: string; - attachments: IAttachment[]; + attachments?: IAttachment[]; } export interface ISlashCommandRequest { From 420673ccad208b7a8566d7ff1935b431658297c1 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 10 Aug 2019 17:54:58 -0400 Subject: [PATCH 030/167] Remove daily report (#40) --- src/controllers/muzzle.controller.ts | 4 ++-- src/services/muzzle/muzzle.persistence.service.ts | 7 ------- src/services/report/report.service.spec.ts | 10 ---------- src/services/report/report.service.ts | 4 ---- src/shared/models/muzzle/muzzle-models.ts | 1 - 5 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 49cf293c..9324bf27 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -91,7 +91,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { res.send( - `Sorry! No support for multiple parameters at this time. Please choose one of: \`day\`, \`week\`, \`month\`, \`year\`, \`all\`` + `Sorry! No support for multiple parameters at this time. Please choose one of: \`week\`, \`month\`, \`year\`, \`all\`` ); } else if ( request.text !== "" && @@ -100,7 +100,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { res.send( `Sorry! You passed in \`${ request.text - }\` but we can only generate reports for the following values: \`day\`, \`week\`, \`month\`, \`year\`, \`all\`` + }\` but we can only generate reports for the following values: \`week\`, \`month\`, \`year\`, \`all\`` ); } else { const reportType: ReportType = reportService.getReportType(request.text); diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index fb367bcc..c1d524d4 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -51,13 +51,6 @@ export class MuzzlePersistenceService { }; if (reportType === ReportType.AllTime) { range.reportType = ReportType.AllTime; - } else if (reportType === ReportType.Day) { - range.start = moment() - .startOf("day") - .format("YYYY-MM-DD HH:mm:ss"); - range.end = moment() - .endOf("day") - .format("YYYY-MM-DD HH:mm:ss"); } else if (reportType === ReportType.Week) { range.start = moment() .startOf("week") diff --git a/src/services/report/report.service.spec.ts b/src/services/report/report.service.spec.ts index 4c14544c..e7172b93 100644 --- a/src/services/report/report.service.spec.ts +++ b/src/services/report/report.service.spec.ts @@ -8,11 +8,6 @@ describe("ReportService", () => { }); describe("getReportType()", () => { describe(" - with valid report types", () => { - it("should return ReportType.Day when day is passed in with any case", () => { - expect(mockService.getReportType("day")).toBe(ReportType.Day); - expect(mockService.getReportType("Day")).toBe(ReportType.Day); - expect(mockService.getReportType("DAY")).toBe(ReportType.Day); - }); it("should return ReportType.Week when week is passed in with any case", () => { expect(mockService.getReportType("week")).toBe(ReportType.Week); expect(mockService.getReportType("Week")).toBe(ReportType.Week); @@ -43,11 +38,6 @@ describe("ReportService", () => { }); describe("isValidReportType()", () => { - it("should return true when day is passed in with any case", () => { - expect(mockService.isValidReportType("day")).toBe(true); - expect(mockService.isValidReportType("Day")).toBe(true); - expect(mockService.isValidReportType("DAY")).toBe(true); - }); it("should return true when week is passed in with any case", () => { expect(mockService.isValidReportType("week")).toBe(true); expect(mockService.isValidReportType("Week")).toBe(true); diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index a7907967..3855ca87 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -26,7 +26,6 @@ export class ReportService { public isValidReportType(type: string) { const lowerCaseType = type.toLowerCase(); return ( - lowerCaseType === ReportType.Day || lowerCaseType === ReportType.Week || lowerCaseType === ReportType.Month || lowerCaseType === ReportType.Year || @@ -45,9 +44,6 @@ export class ReportService { public getReportTitle(type: ReportType) { const range = this.muzzlePersistenceService.getRange(type); const titles = { - [ReportType.Day]: `Daily Muzzle Report for ${moment(range.start).format( - "MM-DD-YYYY" - )}`, [ReportType.Week]: `Weekly Muzzle Report for ${moment(range.start).format( "MM-DD-YYYY" )} to ${moment(range.end).format("MM-DD-YYYY")}`, diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index d961b9b3..267171b6 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -12,7 +12,6 @@ export interface IRequestor { export enum ReportType { Week = "week", - Day = "day", Month = "month", Year = "year", AllTime = "all" From cedc1846cd1b6bf28563adff36b6c7517b52a3c3 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 14:59:55 -0400 Subject: [PATCH 031/167] Fixed urban dictionary multi-word definitions (#41) --- src/services/define/define.service.ts | 5 ++++- src/services/list/list.persistence.service.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index 5a8c625d..be4f5754 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -28,7 +28,10 @@ export class DefineService { * Returns a promise to look up a definition on urban dictionary. */ public define(word: string): Promise { - return Axios.get(`http://api.urbandictionary.com/v0/define?term=${word}`) + const formattedWord = word.split(" ").join("+"); + return Axios.get( + `http://api.urbandictionary.com/v0/define?term=${formattedWord}` + ) .then((res: AxiosResponse) => { return res.data; }) diff --git a/src/services/list/list.persistence.service.ts b/src/services/list/list.persistence.service.ts index 291e13ae..22133734 100644 --- a/src/services/list/list.persistence.service.ts +++ b/src/services/list/list.persistence.service.ts @@ -1,6 +1,10 @@ import { getRepository } from "typeorm"; import { List } from "../../shared/db/models/List"; +interface ILister { + count: number; +} + export class ListPersistenceService { public static getInstance() { if (!ListPersistenceService.instance) { @@ -11,6 +15,8 @@ export class ListPersistenceService { private static instance: ListPersistenceService; + private listersToday: Map = new Map(); + private constructor() {} public store(requestorId: string, text: string) { @@ -33,4 +39,13 @@ export class ListPersistenceService { reject(`Unable to find \`${text}\``); }); } + + private countLister(requestorId: string) { + const isListerPresent = this.listersToday.has(requestorId); + const count = isListerPresent + ? ++this.listersToday.get(requestorId)!.count + : 1; + + this.listersToday.set(requestorId, { count }); + } } From 8b3166c2b30efe40d73724dc745faea2899d3635 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 15:01:11 -0400 Subject: [PATCH 032/167] Feature/list enhancements (#42) * Added title to the report * Removed username from the list --- src/services/report/report.service.ts | 5 +---- src/services/web/web.service.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 3855ca87..35f25398 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -61,10 +61,7 @@ export class ReportService { private formatListReport(report: any) { const reportWithoutDate = report.map((listItem: List) => { - return { - Item: listItem.text, - "Added By": this.slackService.getUserName(listItem.requestorId) - }; + return { Item: listItem.text }; }); return ` diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index f7442d08..d4f15fbd 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -63,7 +63,7 @@ export class WebService { channels: channel, content, filetype: "markdown", - title: "Muzzle Report", + title, initial_comment: title, token: muzzleToken }; From 9295f57913beea6245289c50db79fe4a3f6d8729 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 15:02:51 -0400 Subject: [PATCH 033/167] Removed (#43) --- src/services/list/list.persistence.service.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/services/list/list.persistence.service.ts b/src/services/list/list.persistence.service.ts index 22133734..3df4e93d 100644 --- a/src/services/list/list.persistence.service.ts +++ b/src/services/list/list.persistence.service.ts @@ -39,13 +39,4 @@ export class ListPersistenceService { reject(`Unable to find \`${text}\``); }); } - - private countLister(requestorId: string) { - const isListerPresent = this.listersToday.has(requestorId); - const count = isListerPresent - ? ++this.listersToday.get(requestorId)!.count - : 1; - - this.listersToday.set(requestorId, { count }); - } } From 56a32303a6c751c54ab2ad2aaceed8ad4a521631 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 15:04:32 -0400 Subject: [PATCH 034/167] Feature/remove unintentional list counter (#44) * Removed * removed listers --- src/services/list/list.persistence.service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/services/list/list.persistence.service.ts b/src/services/list/list.persistence.service.ts index 3df4e93d..291e13ae 100644 --- a/src/services/list/list.persistence.service.ts +++ b/src/services/list/list.persistence.service.ts @@ -1,10 +1,6 @@ import { getRepository } from "typeorm"; import { List } from "../../shared/db/models/List"; -interface ILister { - count: number; -} - export class ListPersistenceService { public static getInstance() { if (!ListPersistenceService.instance) { @@ -15,8 +11,6 @@ export class ListPersistenceService { private static instance: ListPersistenceService; - private listersToday: Map = new Map(); - private constructor() {} public store(requestorId: string, text: string) { From a032235aae30519de3a261b1d102be376b34667d Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 15:56:07 -0400 Subject: [PATCH 035/167] Converted weeks and months to only report on the previous week/month (#45) --- src/services/muzzle/muzzle.persistence.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index c1d524d4..e6a01369 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -49,21 +49,26 @@ export class MuzzlePersistenceService { const range: IReportRange = { reportType }; + if (reportType === ReportType.AllTime) { range.reportType = ReportType.AllTime; } else if (reportType === ReportType.Week) { range.start = moment() .startOf("week") + .subtract(1, "week") .format("YYYY-MM-DD HH:mm:ss"); range.end = moment() .endOf("week") + .subtract(1, "week") .format("YYYY-MM-DD HH:mm:ss"); } else if (reportType === ReportType.Month) { range.start = moment() .startOf("month") + .subtract(1, "month") .format("YYYY-MM-DD HH:mm:ss"); range.end = moment() .endOf("month") + .subtract(1, "month") .format("YYYY-MM-DD HH:mm:ss"); } else if (reportType === ReportType.Year) { range.start = moment() From 5f6fd55ad0c6a5f0bbac65fd7ab56c1d44808494 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 22 Aug 2019 17:10:23 -0400 Subject: [PATCH 036/167] Trailing30 (#46) * Added support for trailing30 * Added title support for trailing 30 * Updated tests * Updated error message to include trailing30 --- src/controllers/muzzle.controller.ts | 4 ++-- .../muzzle/muzzle.persistence.service.ts | 6 ++++++ src/services/report/report.service.spec.ts | 16 ++++++++++++++++ src/services/report/report.service.ts | 4 ++++ src/shared/models/muzzle/muzzle-models.ts | 1 + 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 9324bf27..be6559d2 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -91,7 +91,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { res.send( - `Sorry! No support for multiple parameters at this time. Please choose one of: \`week\`, \`month\`, \`year\`, \`all\`` + `Sorry! No support for multiple parameters at this time. Please choose one of: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\`` ); } else if ( request.text !== "" && @@ -100,7 +100,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { res.send( `Sorry! You passed in \`${ request.text - }\` but we can only generate reports for the following values: \`week\`, \`month\`, \`year\`, \`all\`` + }\` but we can only generate reports for the following values: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\`` ); } else { const reportType: ReportType = reportService.getReportType(request.text); diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index e6a01369..1fb84625 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -70,6 +70,12 @@ export class MuzzlePersistenceService { .endOf("month") .subtract(1, "month") .format("YYYY-MM-DD HH:mm:ss"); + } else if (reportType === ReportType.Trailing30) { + range.start = moment() + .startOf("day") + .subtract(30, "days") + .format("YYYY-MM-DD HH:mm:ss"); + range.end = moment().format("YYYY-MM-DD HH:mm:ss"); } else if (reportType === ReportType.Year) { range.start = moment() .startOf("year") diff --git a/src/services/report/report.service.spec.ts b/src/services/report/report.service.spec.ts index e7172b93..4ab3861b 100644 --- a/src/services/report/report.service.spec.ts +++ b/src/services/report/report.service.spec.ts @@ -8,6 +8,17 @@ describe("ReportService", () => { }); describe("getReportType()", () => { describe(" - with valid report types", () => { + it("should return ReportType.trailing30 when trailing30 is passed in with any case", () => { + expect(mockService.getReportType("trailing30")).toBe( + ReportType.Trailing30 + ); + expect(mockService.getReportType("Trailing30")).toBe( + ReportType.Trailing30 + ); + expect(mockService.getReportType("TRAILING30")).toBe( + ReportType.Trailing30 + ); + }); it("should return ReportType.Week when week is passed in with any case", () => { expect(mockService.getReportType("week")).toBe(ReportType.Week); expect(mockService.getReportType("Week")).toBe(ReportType.Week); @@ -38,6 +49,11 @@ describe("ReportService", () => { }); describe("isValidReportType()", () => { + it("should return true when Trailing30 is passed in with any case", () => { + expect(mockService.isValidReportType("trailing30")).toBe(true); + expect(mockService.isValidReportType("Trailing30")).toBe(true); + expect(mockService.isValidReportType("TRAILING30")).toBe(true); + }); it("should return true when week is passed in with any case", () => { expect(mockService.isValidReportType("week")).toBe(true); expect(mockService.isValidReportType("Week")).toBe(true); diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 35f25398..8a87478e 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -26,6 +26,7 @@ export class ReportService { public isValidReportType(type: string) { const lowerCaseType = type.toLowerCase(); return ( + lowerCaseType === ReportType.Trailing30 || lowerCaseType === ReportType.Week || lowerCaseType === ReportType.Month || lowerCaseType === ReportType.Year || @@ -50,6 +51,9 @@ export class ReportService { [ReportType.Month]: `Monthly Muzzle Report for ${moment( range.start ).format("MM-DD-YYYY")} to ${moment(range.end).format("MM-DD-YYYY")}`, + [ReportType.Trailing30]: `Trailing 30 Days Report for ${moment( + range.start + ).format("MM-DD-YYYY")} to ${moment(range.end).format("MM-DD-YYYY")}`, [ReportType.Year]: `Annual Muzzle Report for ${moment(range.start).format( "MM-DD-YYYY" )} to ${moment(range.end).format("MM-DD-YYYY")}`, diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 267171b6..b861dc97 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -11,6 +11,7 @@ export interface IRequestor { } export enum ReportType { + Trailing30 = "trailing30", Week = "week", Month = "month", Year = "year", From 77de68799c4acc50f6b19ec537b2b78a2f4bd59d Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 23 Sep 2019 11:34:37 -0400 Subject: [PATCH 037/167] Added fix to avoid text.like.this from not being muzzled (#47) --- src/services/muzzle/muzzle.service.spec.ts | 6 ++++++ src/services/muzzle/muzzle.service.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 1198d046..20a1fdc0 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -57,6 +57,7 @@ describe("MuzzleService", () => { ) .mockResolvedValue(mockResolve as UpdateResult); }); + it("should always muzzle a tagged user", () => { const testSentence = "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; @@ -74,6 +75,11 @@ describe("MuzzleService", () => { const testSentence = ""; expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); }); + + it("should always muzzle a word with length > 10", () => { + const testSentence = "this.is.a.way.to.game.the.system"; + expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + }); }); describe("getMuzzleId()", () => { diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 688c2dc7..4e95fbf4 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -43,7 +43,9 @@ export class MuzzleService { let replacementWord; for (const word of words) { replacementWord = - isRandomEven() && !this.slackService.containsTag(word) + isRandomEven() && + word.length < 10 && + !this.slackService.containsTag(word) ? ` *${word}* ` : replacementText; if (replacementWord === replacementText) { From 8df7cc14ecdaca0e6045478361a77b86f13c62b8 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 25 Sep 2019 11:05:27 -0400 Subject: [PATCH 038/167] fixed bug in which adding a user to a channel increases tbeir muzzle (#48) --- src/controllers/muzzle.controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index be6559d2..e7d1c1db 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -21,6 +21,7 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; + console.log(request); if ( muzzleService.isUserMuzzled(request.event.user) && !slackService.containsTag(request.event.text) @@ -38,7 +39,8 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { ); } else if ( muzzleService.isUserMuzzled(request.event.user) && - slackService.containsTag(request.event.text) + slackService.containsTag(request.event.text) && + !request.event.subtype ) { const muzzleId = muzzleService.getMuzzleId(request.event.user); console.log( From bffa33a7ccf6efaf99397930b57ab699caf15e9e Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 14 Oct 2019 13:38:37 -0400 Subject: [PATCH 039/167] Fixed a bug in which a user could get stats while they were muzzled (#49) --- src/controllers/muzzle.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index e7d1c1db..f699328e 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -88,7 +88,7 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - const userId: any = slackService.getUserId(request.user_id); + const userId: string = request.user_id; if (muzzleService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { From 2eb51996f1684da01e2b425becd07a0e1f3c12ca Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 14 Oct 2019 14:08:32 -0400 Subject: [PATCH 040/167] Fixed spelling in the success message (#50) --- src/services/muzzle/muzzle.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 4e95fbf4..2f1a8a65 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -213,7 +213,9 @@ export class MuzzleService { this.muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); this.setRequestorCount(requestorId); resolve( - `Succesfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}` + `Successfully muzzled ${userName} for ${getTimeString( + timeToMuzzle + )}` ); } } From 439b974b2f068b9bbffe58030443d7ce4c04f638 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 14 Oct 2019 14:39:00 -0400 Subject: [PATCH 041/167] Cleaned up logging (#51) --- src/controllers/muzzle.controller.ts | 2 ++ src/services/slack/slack.service.ts | 1 - src/services/web/web.service.ts | 7 ++----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index f699328e..4ed49aca 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -75,6 +75,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { muzzleController.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; + console.log(request); const userId: any = slackService.getUserId(request.text); const results = await muzzleService .addUserToMuzzled(userId, request.user_id) @@ -89,6 +90,7 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = request.user_id; + console.log(request); if (muzzleService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index ab026f78..10142f83 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -22,7 +22,6 @@ export class SlackService { public sendResponse(responseUrl: string, response: IChannelResponse): void { axios .post(responseUrl, response) - .then(() => console.log(`Successfully responded to: ${responseUrl}`)) .catch((e: Error) => console.error(`Error responding: ${e.message} at ${responseUrl}`) ); diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index d4f15fbd..907e0ce3 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -34,7 +34,7 @@ export class WebService { console.log("Message already deleted, no need to retry"); } else { console.error(e); - console.error("Retrying in 5 seconds..."); + console.error("Unable to delete message. Retrying in 5 seconds..."); setTimeout(() => this.deleteMessage(channel, ts), 5000); } }); @@ -68,9 +68,6 @@ export class WebService { token: muzzleToken }; - this.web.files - .upload(uploadRequest) - .then(() => console.log("Uploaded new report successfully!")) - .catch(e => console.error(e)); + this.web.files.upload(uploadRequest).catch(e => console.error(e)); } } From 94bb37992943209af843d75c9bc0d6b5ff6b6019 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 24 Oct 2019 17:47:12 -0400 Subject: [PATCH 042/167] Feature/cli (#52) * Added ability to add a boiler plate controller, made generate function more generic, working on adding generating boiler plate services and specs. Consider singleton vs non-singleton services, as well as persistence.service * Quick and dirty service, spec and controller generator, more work needed for sure --- dev-utils/boiler-plates/boiler.controller.ts | 50 +++++++++++++++ dev-utils/boiler-plates/boiler.service.ts | 35 +++++++++++ dev-utils/boiler-plates/boiler.spec.ts | 19 ++++++ dev-utils/generate.ts | 64 ++++++++++++++++++++ dev-utils/generateFeature.ts | 33 ++++++++++ package-lock.json | 13 ++++ package.json | 2 + src/controllers/spotify.ts | 38 ++++++++++++ 8 files changed, 254 insertions(+) create mode 100644 dev-utils/boiler-plates/boiler.controller.ts create mode 100644 dev-utils/boiler-plates/boiler.service.ts create mode 100644 dev-utils/boiler-plates/boiler.spec.ts create mode 100644 dev-utils/generate.ts create mode 100644 dev-utils/generateFeature.ts create mode 100644 src/controllers/spotify.ts diff --git a/dev-utils/boiler-plates/boiler.controller.ts b/dev-utils/boiler-plates/boiler.controller.ts new file mode 100644 index 00000000..4f2c9f11 --- /dev/null +++ b/dev-utils/boiler-plates/boiler.controller.ts @@ -0,0 +1,50 @@ +import path from "path"; + +export const getBoilerPlateController = (serviceName: string) => { + const relMuzzleService = path + .relative(`src/controllers`, "src/services/muzzle/muzzle.service.ts") + .slice(0, -3); + const relSlackService = path + .relative(`src/controllers`, "src/services/slack/slack.service.ts") + .slice(0, -3); + const relSlackModels = path + .relative(`src/controllers`, "src/shared/models/slack/slack-models.ts") + .slice(0, -3); + + const boilerPlateController = ` + import express, { Router } from "express"; + import { MuzzleService } from "${relMuzzleService}"; + import { SlackService } from "${relSlackService}"; + import { + IChannelResponse, + ISlashCommandRequest + } from "${relSlackModels}"; + + export const ${serviceName}Controller: Router = express.Router(); + + const muzzleService = MuzzleService.getInstance(); + const slackService = SlackService.getInstance(); + + ${serviceName}Controller.post("/${serviceName}", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send("Sorry, can't do that while muzzled."); + } else if (!request.text) { + res.send("Sorry, you must send a message to use this service."); + } else { + const response: IChannelResponse = { + attachments: [ + { + text: 'default' + } + ], + response_type: "in_channel", + text: 'A message sent by your service that should be replaced.' + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } + });`; + + return boilerPlateController; +}; diff --git a/dev-utils/boiler-plates/boiler.service.ts b/dev-utils/boiler-plates/boiler.service.ts new file mode 100644 index 00000000..50b4e635 --- /dev/null +++ b/dev-utils/boiler-plates/boiler.service.ts @@ -0,0 +1,35 @@ +function capitalizeFirstLetter(text: string) { + return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; +} + +export const getBoilerPlateService = ( + serviceName: string, + isSingleton: boolean +) => { + const capitalizedService = capitalizeFirstLetter(serviceName); + + const boilerPlateServiceSingleton = ` + export class ${capitalizedService}Service { + public static getInstance() { + if(!${capitalizedService}Service.instance) { + ${capitalizedService}Service.instance = new ${capitalizedService}Service(); + } + return ${capitalizedService}Service.instance; + } + + private static instance: ${capitalizedService}; + + private constructor() {}; + } + `; + + const boilerPlateServiceNonSingleton = ` + export class ${capitalizedService}Service { + public constructor() {}; + } + `; + + return isSingleton + ? boilerPlateServiceSingleton + : boilerPlateServiceNonSingleton; +}; diff --git a/dev-utils/boiler-plates/boiler.spec.ts b/dev-utils/boiler-plates/boiler.spec.ts new file mode 100644 index 00000000..d476e603 --- /dev/null +++ b/dev-utils/boiler-plates/boiler.spec.ts @@ -0,0 +1,19 @@ +function capitalizeFirstLetter(text: string) { + return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; +} + +export const getBoilerPlateSpec = (serviceName: string) => { + const capitalizedService = capitalizeFirstLetter(serviceName); + + const boilerPlateSpec = ` + import { ${capitalizedService}Service } from "./${serviceName}.service"; + + describe(${capitalizedService}Service, () => { + it('should create', () => { + expect(new ${capitalizedService}Service()).toBeTruthy(); + }) + }) + `; + + return boilerPlateSpec; +}; diff --git a/dev-utils/generate.ts b/dev-utils/generate.ts new file mode 100644 index 00000000..c7b0169c --- /dev/null +++ b/dev-utils/generate.ts @@ -0,0 +1,64 @@ +import fs from "fs"; +import { getBoilerPlateController } from "./boiler-plates/boiler.controller"; +import { getBoilerPlateService } from "./boiler-plates/boiler.service"; +import { getBoilerPlateSpec } from "./boiler-plates/boiler.spec"; + +export enum ComponentType { + Spec = "spec", + Service = "service", + Controller = "controller" +} + +function getFileName(name: string, type: ComponentType) { + switch (type) { + case ComponentType.Spec: { + return `${name}.service.spec.ts`; + } + case ComponentType.Controller: { + return `${name}.controller.ts`; + } + case ComponentType.Service: { + return `${name}.service.ts`; + } + } +} + +function getTemplate(name: string, type: ComponentType) { + switch (type) { + case ComponentType.Controller: + return getBoilerPlateController(name); + case ComponentType.Service: + return getBoilerPlateService(name, false); + case ComponentType.Spec: + return getBoilerPlateSpec(name); + } +} + +export function generate( + name: string, + directory: string, + type: ComponentType, + shouldFailOnExistDir: boolean +) { + const fileName = getFileName(name, type); + + const location = `${directory}/${fileName}`; + console.log(`Creating ${directory}...`); + try { + fs.mkdirSync(directory); + } catch (e) { + if (e.code === "EEXIST") { + if (shouldFailOnExistDir) { + throw e; + } else { + console.log(`${directory} already exists! Skipping...`); + } + } else { + console.log("Error on creating folder: ", e); + } + } + console.log("Done!"); + console.log(`Creating ${fileName} in ${directory}...`); + fs.writeFileSync(location, getTemplate(name, type)); + console.log("Done!"); +} diff --git a/dev-utils/generateFeature.ts b/dev-utils/generateFeature.ts new file mode 100644 index 00000000..e9e6d6ee --- /dev/null +++ b/dev-utils/generateFeature.ts @@ -0,0 +1,33 @@ +import { prompt } from "enquirer"; +import path from "path"; +import { ComponentType, generate } from "./generate"; + +async function generateFeature() { + const response: any = await prompt({ + type: "input", + name: "name", + message: "What would you like to name your feature?" + }); + + const isSingleton: any = await prompt({ + type: "input", + name: "isSingleton", + message: "Should this service be a singleton? (Y/N)" + }); + + console.log( + `You answered ${ + isSingleton.isSingleton + } to the singleton question, but JR Is too lazy to actually make this do anything just yet.` + ); + + const { name } = response; + const newServiceDir = path.resolve(`./src/services/${name}`); + const newControllerDir = path.resolve(`./src/controllers`); + + generate(name, newControllerDir, ComponentType.Controller, false); + generate(name, newServiceDir, ComponentType.Service, true); + generate(name, newServiceDir, ComponentType.Spec, false); +} + +generateFeature(); diff --git a/package-lock.json b/package-lock.json index 22f669da..900ad23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -773,6 +773,11 @@ "string-width": "^2.0.0" } }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==" + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -1894,6 +1899,14 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.2.tgz", + "integrity": "sha512-PLhTMPUXlnaIv9D3Cq3/Zr1xb7soeDDgunobyCmYLUG19n24dvC8i+ZZgm2DekGpDnx7JvFSHV7lxfM58PMtbA==", + "requires": { + "ansi-colors": "^3.2.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", diff --git a/package.json b/package.json index e1060c55..56fb87d9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "format:fix": "prettier --write 'src/**/*.ts'", "lint": "tslint -c tslint.json 'src/**/*.ts'", "lint:fix": "tslint --fix -c tslint.json 'src/**/*.ts'", + "create:feature": "ts-node ./dev-utils/generateFeature.ts", "start": "npm run start:dev", "start:prod": "node ./dist/server.js", "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", @@ -22,6 +23,7 @@ "axios": "^0.18.1", "body-parser": "^1.18.3", "easy-table": "^1.1.1", + "enquirer": "^2.3.2", "express": "^4.16.4", "moment": "^2.24.0", "mysql": "^2.17.1", diff --git a/src/controllers/spotify.ts b/src/controllers/spotify.ts new file mode 100644 index 00000000..11494531 --- /dev/null +++ b/src/controllers/spotify.ts @@ -0,0 +1,38 @@ +import express, { Router } from "express"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { SlackService } from "../services/slack/slack.service"; +import { SpotifyService } from "../services/spotify/spotify.service"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const clapController: Router = express.Router(); + +const muzzleService = MuzzleService.getInstance(); +const slackService = SlackService.getInstance(); +const spotifyService = new SpotifyService(); + +clapController.post("/clap", (req, res) => { + const request: ISlashCommandRequest = req.body; + if (muzzleService.isUserMuzzled(request.user_id)) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to clap."); + } else if (request.text.split(" ").length === 1) { + res.send("Sorry, you need more than one words to use clapper."); + } else { + const clapped: string = clapService.clap(request.text); + const response: IChannelResponse = { + attachments: [ + { + text: clapped + } + ], + response_type: "in_channel", + text: `<@${request.user_id}>` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); From 25b6ec75ed238f1e120ca047b0ffd677a4a00aea Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 31 Oct 2019 15:21:51 -0400 Subject: [PATCH 043/167] Fixed a bug in which KDR calculated for all time reports was using attempts rather than kills for the kill column (#53) --- src/services/muzzle/muzzle.persistence.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 1fb84625..8b094922 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -309,6 +309,7 @@ export class MuzzlePersistenceService { RIGHT JOIN ( SELECT requestorId, COUNT(*) as count FROM muzzle + WHERE messagesSuppressed > 0 GROUP BY requestorId ) AS b ON a.muzzledId = b.requestorId From d0b0d3dff8a96b7660f58ad98c4c9ea4c0e7bfa7 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 27 Nov 2019 15:47:58 -0500 Subject: [PATCH 044/167] Backfire Feature (#54) * Added shouldBackfire utility * Created a Backfire entity and wired up to the DB * Added support for backfire persistence and handling * Added attemptedToMuzzle key * Removed spotify.ts: * Added fix for containsTag and updated tests --- jest.config.js | 7 +- src/controllers/muzzle.controller.ts | 46 +++++--- src/controllers/spotify.ts | 38 ------- src/services/muzzle/muzzle-utilities.ts | 6 ++ .../muzzle/muzzle.persistence.service.ts | 53 ++++++--- src/services/muzzle/muzzle.service.spec.ts | 30 ++++-- src/services/muzzle/muzzle.service.ts | 101 ++++++++++++++++-- src/services/slack/slack.service.spec.ts | 8 ++ src/services/slack/slack.service.ts | 6 +- src/shared/db/models/Backfire.ts | 25 +++++ src/shared/models/muzzle/muzzle-models.ts | 4 +- 11 files changed, 237 insertions(+), 87 deletions(-) delete mode 100644 src/controllers/spotify.ts create mode 100644 src/shared/db/models/Backfire.ts diff --git a/jest.config.js b/jest.config.js index 91a2d2c0..556498f1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; \ No newline at end of file + preset: "ts-jest", + testEnvironment: "node", + modulePathIgnorePatterns: ["/dev-utils"] +}; diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 4ed49aca..72848ece 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -21,15 +21,17 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; - console.log(request); - if ( - muzzleService.isUserMuzzled(request.event.user) && - !slackService.containsTag(request.event.text) - ) { + const isUserMuzzled = muzzleService.isUserMuzzled(request.event.user); + const isUserBackfired = muzzleService.getIsBackfire(request.event.user); + const isUsersFirstMuzzledMessage = muzzleService.getIsMuzzledFirstMessage( + request.event.user + ); + const containsTag = slackService.containsTag(request.event.text); + const userName = slackService.getUserName(request.event.user); + + if (isUserMuzzled && !containsTag) { console.log( - `${slackService.getUserName(request.event.user)} | ${ - request.event.user - } is muzzled! Suppressing his voice...` + `${userName} | ${request.event.user} is muzzled! Suppressing his voice...` ); webService.deleteMessage(request.event.channel, request.event.ts); muzzleService.sendMuzzledMessage( @@ -37,25 +39,37 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user, request.event.text ); - } else if ( - muzzleService.isUserMuzzled(request.event.user) && - slackService.containsTag(request.event.text) && - !request.event.subtype - ) { + if (isUserBackfired && isUsersFirstMuzzledMessage) { + const attemptedToMuzzle = muzzleService.getAttemptedToMuzzle( + request.event.user + ); + webService.sendMessage( + request.event.channel, + `:boom: <@${ + request.event.user + }> attempted to muzzle <@${attemptedToMuzzle}> but it backfired! :boom:` + ); + } + } else if (isUserMuzzled && containsTag && !request.event.subtype) { const muzzleId = muzzleService.getMuzzleId(request.event.user); console.log( `${slackService.getUserName( request.event.user - )} atttempted to tag someone. Muzzle increased by ${ + )} attempted to tag someone. Muzzle increased by ${ muzzleService.ABUSE_PENALTY_TIME }!` ); muzzleService.addMuzzleTime( request.event.user, - muzzleService.ABUSE_PENALTY_TIME + muzzleService.ABUSE_PENALTY_TIME, + isUserBackfired ); webService.deleteMessage(request.event.channel, request.event.ts); - muzzlePersistenceService.trackDeletedMessage(muzzleId, request.event.text); + muzzlePersistenceService.trackDeletedMessage( + muzzleId, + request.event.text, + isUserBackfired + ); webService.sendMessage( request.event.channel, `:rotating_light: <@${ diff --git a/src/controllers/spotify.ts b/src/controllers/spotify.ts deleted file mode 100644 index 11494531..00000000 --- a/src/controllers/spotify.ts +++ /dev/null @@ -1,38 +0,0 @@ -import express, { Router } from "express"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; -import { SlackService } from "../services/slack/slack.service"; -import { SpotifyService } from "../services/spotify/spotify.service"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; - -export const clapController: Router = express.Router(); - -const muzzleService = MuzzleService.getInstance(); -const slackService = SlackService.getInstance(); -const spotifyService = new SpotifyService(); - -clapController.post("/clap", (req, res) => { - const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { - res.send(`Sorry, can't do that while muzzled.`); - } else if (!request.text) { - res.send("Sorry, you must send a message to clap."); - } else if (request.text.split(" ").length === 1) { - res.send("Sorry, you need more than one words to use clapper."); - } else { - const clapped: string = clapService.clap(request.text); - const response: IChannelResponse = { - attachments: [ - { - text: clapped - } - ], - response_type: "in_channel", - text: `<@${request.user_id}>` - }; - slackService.sendResponse(request.response_url, response); - res.status(200).send(); - } -}); diff --git a/src/services/muzzle/muzzle-utilities.ts b/src/services/muzzle/muzzle-utilities.ts index 65330cde..592b109f 100644 --- a/src/services/muzzle/muzzle-utilities.ts +++ b/src/services/muzzle/muzzle-utilities.ts @@ -31,3 +31,9 @@ export function getTimeString(time: number) { export function isRandomEven() { return Math.floor(Math.random() * 2) % 2 === 0; } + +export function shouldBackfire() { + const chanceOfBackfire = (Math.random() * (0.01 - 0.05) + 0.05).toFixed(2); + const randomRoll = Math.random().toFixed(2); + return randomRoll <= chanceOfBackfire; +} diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 8b094922..891fb662 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,5 +1,6 @@ import moment from "moment"; import { getRepository } from "typeorm"; +import { Backfire } from "../../shared/db/models/Backfire"; import { Muzzle } from "../../shared/db/models/Muzzle"; import { IReportRange, @@ -29,16 +30,39 @@ export class MuzzlePersistenceService { return getRepository(Muzzle).save(muzzle); } - public incrementMuzzleTime(id: number, ms: number) { - return getRepository(Muzzle).increment({ id }, "milliseconds", ms); + public addBackfireToDb(muzzledId: string, time: number) { + const backfire = new Backfire(); + backfire.muzzledId = muzzledId; + backfire.messagesSuppressed = 0; + backfire.wordsSuppressed = 0; + backfire.charactersSuppressed = 0; + backfire.milliseconds = time; + + return getRepository(Backfire).save(backfire); + } + + public incrementMuzzleTime(id: number, ms: number, isBackfire: boolean) { + return getRepository(isBackfire ? Backfire : Muzzle).increment( + { id }, + "milliseconds", + ms + ); } - public incrementMessageSuppressions(id: number) { - return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); + public incrementMessageSuppressions(id: number, isBackfire: boolean) { + return getRepository(isBackfire ? Backfire : Muzzle).increment( + { id }, + "messagesSuppressed", + 1 + ); } - public incrementWordSuppressions(id: number, suppressions: number) { - return getRepository(Muzzle).increment( + public incrementWordSuppressions( + id: number, + suppressions: number, + isBackfire: boolean + ) { + return getRepository(isBackfire ? Backfire : Muzzle).increment( { id }, "wordsSuppressed", suppressions @@ -90,9 +114,10 @@ export class MuzzlePersistenceService { public incrementCharacterSuppressions( id: number, - charactersSuppressed: number + charactersSuppressed: number, + isBackfire: boolean ) { - return getRepository(Muzzle).increment( + return getRepository(isBackfire ? Backfire : Muzzle).increment( { id }, "charactersSuppressed", charactersSuppressed @@ -102,12 +127,16 @@ export class MuzzlePersistenceService { * Determines suppression counts for messages that are ONLY deleted and not muzzled. * Used when a muzzled user has hit their max suppressions or when they have tagged channel. */ - public trackDeletedMessage(muzzleId: number, text: string) { + public trackDeletedMessage( + muzzleId: number, + text: string, + isBackfire: boolean + ) { const words = text.split(" ").length; const characters = text.split("").length; - this.incrementMessageSuppressions(muzzleId); - this.incrementWordSuppressions(muzzleId, words); - this.incrementCharacterSuppressions(muzzleId, characters); + this.incrementMessageSuppressions(muzzleId, isBackfire); + this.incrementWordSuppressions(muzzleId, words, isBackfire); + this.incrementCharacterSuppressions(muzzleId, characters, isBackfire); } /** Wrapper to generate a generic muzzle report in */ diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 20a1fdc0..7bd993ef 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -5,6 +5,7 @@ import { ISlackUser } from "../../shared/models/slack/slack-models"; import { SlackService } from "../slack/slack.service"; +import * as muzzleUtils from "./muzzle-utilities"; import { MuzzlePersistenceService } from "./muzzle.persistence.service"; import { MuzzleService } from "./muzzle.service"; @@ -61,24 +62,24 @@ describe("MuzzleService", () => { it("should always muzzle a tagged user", () => { const testSentence = "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzleInstance.muzzle(testSentence, 1)).toBe( + expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe( " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " ); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); }); it("should always muzzle a word with length > 10", () => { const testSentence = "this.is.a.way.to.game.the.system"; - expect(muzzleInstance.muzzle(testSentence, 1)).toBe(" ..mMm.. "); + expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); }); }); @@ -88,6 +89,7 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); expect(muzzleInstance.getMuzzleId("123")).toBe(1); }); @@ -99,6 +101,7 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); }); @@ -117,6 +120,7 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); }); it("should return the requestor when a valid id is passed in", async () => { @@ -132,6 +136,7 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); }); it("should return true when a muzzled userId is passed in", async () => { @@ -149,6 +154,7 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); }); @@ -180,7 +186,7 @@ describe("MuzzleService", () => { } } as IEventRequest; }); - describe("positive path", () => { + describe("when a user is muzzled", () => { beforeEach(async () => { await muzzleInstance.addUserToMuzzled( testData.user, @@ -268,6 +274,10 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + + jest + .spyOn(muzzleUtils, "shouldBackfire") + .mockImplementation(() => false); }); it("should add a user to the muzzled map", async () => { @@ -302,6 +312,9 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest + .spyOn(muzzleUtils, "shouldBackfire") + .mockImplementation(() => false); await muzzleInstance.addUserToMuzzled( testData.user, testData.requestor @@ -349,6 +362,9 @@ describe("MuzzleService", () => { jest .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); + jest + .spyOn(muzzleUtils, "shouldBackfire") + .mockImplementation(() => false); }); it("should add a user to the requestors map", async () => { await muzzleInstance.addUserToMuzzled( @@ -391,8 +407,8 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValueOnce(mockMuzzle as Muzzle); jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValueOnce(mockMuzzle as Muzzle); + .spyOn(muzzleUtils, "shouldBackfire") + .mockImplementation(() => false); }); it("should prevent a requestor from muzzling on their third count", async () => { await muzzleInstance.addUserToMuzzled( diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 2f1a8a65..478a1e73 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -6,7 +6,8 @@ import { getRemainingTime, getTimeString, getTimeToMuzzle, - isRandomEven + isRandomEven, + shouldBackfire } from "./muzzle-utilities"; import { MuzzlePersistenceService } from "./muzzle.persistence.service"; @@ -34,7 +35,7 @@ export class MuzzleService { /** * Takes in text and randomly muzzles certain words. */ - public muzzle(text: string, muzzleId: number) { + public muzzle(text: string, muzzleId: number, isBackfire: boolean) { const replacementText = " ..mMm.. "; let returnText = ""; const words = text.split(" "); @@ -54,28 +55,34 @@ export class MuzzleService { } returnText += replacementWord; } - this.muzzlePersistenceService.incrementMessageSuppressions(muzzleId); + this.muzzlePersistenceService.incrementMessageSuppressions( + muzzleId, + isBackfire + ); this.muzzlePersistenceService.incrementCharacterSuppressions( muzzleId, - charactersSuppressed + charactersSuppressed, + isBackfire ); this.muzzlePersistenceService.incrementWordSuppressions( muzzleId, - wordsSuppressed + wordsSuppressed, + isBackfire ); return returnText; } /** * Adds the specified amount of time to a specified muzzled user. */ - public addMuzzleTime(userId: string, timeToAdd: number) { + public addMuzzleTime(userId: string, timeToAdd: number, isBackfire: boolean) { if (userId && this.muzzled.has(userId)) { const removalFn = this.muzzled.get(userId)!.removalFn; const newTime = getRemainingTime(removalFn) + timeToAdd; const muzzleId = this.muzzled.get(userId)!.id; this.muzzlePersistenceService.incrementMuzzleTime( muzzleId, - this.ABUSE_PENALTY_TIME + this.ABUSE_PENALTY_TIME, + isBackfire ); clearTimeout(this.muzzled.get(userId)!.removalFn); console.log( @@ -87,6 +94,8 @@ export class MuzzleService { suppressionCount: this.muzzled.get(userId)!.suppressionCount, muzzledBy: this.muzzled.get(userId)!.muzzledBy, id: this.muzzled.get(userId)!.id, + isBackfire: this.muzzled.get(userId)!.isBackfire, + attemptedToMuzzle: this.muzzled.get(userId)!.attemptedToMuzzle, removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) }); } @@ -98,6 +107,29 @@ export class MuzzleService { return this.muzzled.get(userId)!.id; } + /** + * Retrieves whether or not a muzzle is backfired. + */ + public getIsBackfire(userId: string) { + return this.muzzled.has(userId) && this.muzzled.get(userId)!.isBackfire; + } + + public getAttemptedToMuzzle(userId: string) { + return ( + this.muzzled.has(userId) && this.muzzled.get(userId)!.attemptedToMuzzle + ); + } + + /** + * Tells us whether or not this is the users first muzzled message. + */ + public getIsMuzzledFirstMessage(userId: string) { + return ( + this.muzzled.has(userId) && + this.muzzled.get(userId)!.suppressionCount === 0 + ); + } + /** * Retrieves the specified user from the muzzled map by slack id. */ @@ -172,6 +204,7 @@ export class MuzzleService { * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ public addUserToMuzzled(userId: string, requestorId: string) { + const shouldBackFire = shouldBackfire(); const userName = this.slackService.getUserName(userId); const requestorName = this.slackService.getUserName(requestorId); return new Promise(async (resolve, reject) => { @@ -200,6 +233,33 @@ export class MuzzleService { this.MAX_MUZZLES } muzzles are allowed per hour.` ); + } else if (shouldBackFire) { + console.log( + `Backfiring on ${requestorName} for attempting to muzzle ${userName}` + ); + const timeToMuzzle = getTimeToMuzzle(); + const backfireFromDb = await this.muzzlePersistenceService + .addBackfireToDb(requestorId, timeToMuzzle) + .catch((e: any) => { + console.error(e); + reject(`Muzzle failed!`); + }); + + if (backfireFromDb) { + const attemptedToMuzzle = userId; + this.backfireUser( + requestorId, + attemptedToMuzzle, + backfireFromDb.id, + timeToMuzzle + ); + this.setRequestorCount(requestorId); + resolve( + `Successfully muzzled ${userName} for ${getTimeString( + timeToMuzzle + )}` + ); + } } else { const timeToMuzzle = getTimeToMuzzle(); const muzzleFromDb = await this.muzzlePersistenceService @@ -227,19 +287,25 @@ export class MuzzleService { */ public sendMuzzledMessage(channel: string, userId: string, text: string) { const muzzleId = this.muzzled.get(userId)!.id; + const isBackfire = this.muzzled.get(userId)!.isBackfire; if (this.muzzled.get(userId)!.suppressionCount < this.MAX_SUPPRESSIONS) { this.muzzled.set(userId, { suppressionCount: ++this.muzzled.get(userId)!.suppressionCount, muzzledBy: this.muzzled.get(userId)!.muzzledBy, id: muzzleId, + isBackfire, removalFn: this.muzzled.get(userId)!.removalFn }); this.webService.sendMessage( channel, - `<@${userId}> says "${this.muzzle(text, muzzleId)}"` + `<@${userId}> says "${this.muzzle(text, muzzleId, isBackfire)}"` ); } else { - this.muzzlePersistenceService.trackDeletedMessage(muzzleId, text); + this.muzzlePersistenceService.trackDeletedMessage( + muzzleId, + text, + isBackfire + ); } } @@ -292,6 +358,23 @@ export class MuzzleService { suppressionCount: 0, muzzledBy: requestorId, id, + isBackfire: false, + removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) + }); + } + + private backfireUser( + userId: string, + attemptedToMuzzle: string, + id: number, + timeToMuzzle: number + ) { + this.muzzled.set(userId, { + suppressionCount: 0, + muzzledBy: userId, + attemptedToMuzzle, + id, + isBackfire: true, removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) }); } diff --git a/src/services/slack/slack.service.spec.ts b/src/services/slack/slack.service.spec.ts index e7bbb853..bc137c2f 100644 --- a/src/services/slack/slack.service.spec.ts +++ b/src/services/slack/slack.service.spec.ts @@ -75,6 +75,14 @@ describe("slack-utils", () => { expect(slackService.containsTag(testWord)).toBe(false); }); + it("should return false if no text is passed in", () => { + expect(slackService.containsTag("")).toBe(false); + }); + + it("should return false if undefined is passed in", () => { + expect(slackService.containsTag(undefined)).toBe(false); + }); + it("should return true if a word has in it", () => { const testWord = ""; expect(slackService.containsTag(testWord)).toBe(true); diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index 10142f83..ceb94950 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -78,7 +78,11 @@ export class SlackService { /** * Determines whether or not a user is trying to @user, @channel or @here while muzzled. */ - public containsTag(text: string): boolean { + public containsTag(text: string | undefined): boolean { + if (!text) { + return false; + } + return ( text.includes("") || text.includes("") || diff --git a/src/shared/db/models/Backfire.ts b/src/shared/db/models/Backfire.ts new file mode 100644 index 00000000..3f99c8a4 --- /dev/null +++ b/src/shared/db/models/Backfire.ts @@ -0,0 +1,25 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Backfire { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public muzzledId!: string; + + @Column() + public milliseconds!: number; + + @Column() + public messagesSuppressed!: number; + + @Column() + public wordsSuppressed!: number; + + @Column() + public charactersSuppressed!: number; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + public createdAt!: Date; +} diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index b861dc97..51986359 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -1,8 +1,10 @@ export interface IMuzzled { suppressionCount: number; muzzledBy: string; - id: number; + id: number; // Refers to either a muzzleID or backfireID from the database. Dependent on isBackfire. + isBackfire: boolean; removalFn: NodeJS.Timeout; + attemptedToMuzzle?: string; } export interface IRequestor { From add737fe50afc7ed6ac3977b603857ebce390b61 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 6 Dec 2019 22:38:36 -0500 Subject: [PATCH 045/167] Fixed undefined error and upped percentage to 5% per muzzle (#55) --- src/controllers/muzzle.controller.ts | 2 +- src/services/muzzle/muzzle-utilities.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 72848ece..0e83b677 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -41,7 +41,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { ); if (isUserBackfired && isUsersFirstMuzzledMessage) { const attemptedToMuzzle = muzzleService.getAttemptedToMuzzle( - request.event.user + request.event.text ); webService.sendMessage( request.event.channel, diff --git a/src/services/muzzle/muzzle-utilities.ts b/src/services/muzzle/muzzle-utilities.ts index 592b109f..ca834698 100644 --- a/src/services/muzzle/muzzle-utilities.ts +++ b/src/services/muzzle/muzzle-utilities.ts @@ -33,7 +33,6 @@ export function isRandomEven() { } export function shouldBackfire() { - const chanceOfBackfire = (Math.random() * (0.01 - 0.05) + 0.05).toFixed(2); - const randomRoll = Math.random().toFixed(2); - return randomRoll <= chanceOfBackfire; + const chanceOfBackfire = 0.05; + return Math.random() <= chanceOfBackfire; } From 0a5678826c170d7e8f395af87a1344111e692359 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 9 Dec 2019 19:24:45 -0500 Subject: [PATCH 046/167] Backfire alerts fire immediately now and users are aware of their backfired status (#56) --- src/controllers/muzzle.controller.ts | 16 +---- src/services/muzzle/muzzle.service.spec.ts | 71 ++++++++++++++++------ src/services/muzzle/muzzle.service.ts | 49 ++++----------- src/shared/models/muzzle/muzzle-models.ts | 1 - src/shared/models/slack/slack-models.ts | 10 ++- 5 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 0e83b677..ab7c442f 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -23,9 +23,6 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; const isUserMuzzled = muzzleService.isUserMuzzled(request.event.user); const isUserBackfired = muzzleService.getIsBackfire(request.event.user); - const isUsersFirstMuzzledMessage = muzzleService.getIsMuzzledFirstMessage( - request.event.user - ); const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); @@ -39,17 +36,6 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user, request.event.text ); - if (isUserBackfired && isUsersFirstMuzzledMessage) { - const attemptedToMuzzle = muzzleService.getAttemptedToMuzzle( - request.event.text - ); - webService.sendMessage( - request.event.channel, - `:boom: <@${ - request.event.user - }> attempted to muzzle <@${attemptedToMuzzle}> but it backfired! :boom:` - ); - } } else if (isUserMuzzled && containsTag && !request.event.subtype) { const muzzleId = muzzleService.getMuzzleId(request.event.user); console.log( @@ -92,7 +78,7 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { console.log(request); const userId: any = slackService.getUserId(request.text); const results = await muzzleService - .addUserToMuzzled(userId, request.user_id) + .addUserToMuzzled(userId, request.user_id, request.channel_name) .catch(e => { res.send(e); }); diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 7bd993ef..1c2083ee 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -90,7 +90,11 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor, + "test" + ); expect(muzzleInstance.getMuzzleId("123")).toBe(1); }); }); @@ -102,7 +106,11 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor, + "test" + ); }); it("should return the muzzled user when a valid id is passed in", async () => { @@ -121,7 +129,11 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor, + "test" + ); }); it("should return the requestor when a valid id is passed in", async () => { const requestor = muzzleInstance.getRequestorById("666"); @@ -137,7 +149,11 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor, + "test" + ); }); it("should return true when a muzzled userId is passed in", async () => { expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); @@ -155,7 +171,11 @@ describe("MuzzleService", () => { .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") .mockResolvedValue(mockMuzzle as Muzzle); jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled(testData.user, testData.requestor); + await muzzleInstance.addUserToMuzzled( + testData.user, + testData.requestor, + "test" + ); }); it("should return true when a requestor userId is passed in", () => { @@ -190,7 +210,8 @@ describe("MuzzleService", () => { beforeEach(async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); }); it("should return true if an id is present in the event.text ", () => { @@ -283,7 +304,8 @@ describe("MuzzleService", () => { it("should add a user to the muzzled map", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); }); @@ -291,7 +313,8 @@ describe("MuzzleService", () => { it("should return an added user with IMuzzled attributes", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect( muzzleInstance.getMuzzledUserById(testData.user)!.suppressionCount @@ -317,11 +340,12 @@ describe("MuzzleService", () => { .mockImplementation(() => false); await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); await muzzleInstance - .addUserToMuzzled(testData.user, testData.requestor) + .addUserToMuzzled(testData.user, testData.requestor, "test") .catch(e => { expect(e).toBe("test123 is already muzzled!"); }); @@ -329,7 +353,7 @@ describe("MuzzleService", () => { it("should reject if a user tries to muzzle a user that does not exist", async () => { await muzzleInstance - .addUserToMuzzled("", testData.requestor) + .addUserToMuzzled("", testData.requestor, "test") .catch(e => { expect(e).toBe( `Invalid username passed in. You can only muzzle existing slack users` @@ -341,11 +365,12 @@ describe("MuzzleService", () => { it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); await muzzleInstance - .addUserToMuzzled(testData.requestor, testData.user) + .addUserToMuzzled(testData.requestor, testData.user, "test") .catch(e => { expect(e).toBe( `You can't muzzle someone if you are already muzzled!` @@ -369,7 +394,8 @@ describe("MuzzleService", () => { it("should add a user to the requestors map", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); }); @@ -377,7 +403,8 @@ describe("MuzzleService", () => { it("should return an added user with IMuzzler attributes", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); expect( muzzleInstance.getRequestorById(testData.requestor)!.muzzleCount @@ -387,11 +414,13 @@ describe("MuzzleService", () => { it("should increment a requestors muzzle count on a second muzzleInstance.addUserToMuzzled() call", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); await muzzleInstance.addUserToMuzzled( testData.user2, - testData.requestor + testData.requestor, + "test" ); expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); expect( @@ -413,14 +442,16 @@ describe("MuzzleService", () => { it("should prevent a requestor from muzzling on their third count", async () => { await muzzleInstance.addUserToMuzzled( testData.user, - testData.requestor + testData.requestor, + "test" ); await muzzleInstance.addUserToMuzzled( testData.user2, - testData.requestor + testData.requestor, + "test" ); await muzzleInstance - .addUserToMuzzled(testData.user3, testData.requestor) + .addUserToMuzzled(testData.user3, testData.requestor, "test") .catch(e => expect(e).toBe( `You're doing that too much. Only 2 muzzles are allowed per hour.` diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 478a1e73..2edd1a21 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -95,7 +95,6 @@ export class MuzzleService { muzzledBy: this.muzzled.get(userId)!.muzzledBy, id: this.muzzled.get(userId)!.id, isBackfire: this.muzzled.get(userId)!.isBackfire, - attemptedToMuzzle: this.muzzled.get(userId)!.attemptedToMuzzle, removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) }); } @@ -114,22 +113,6 @@ export class MuzzleService { return this.muzzled.has(userId) && this.muzzled.get(userId)!.isBackfire; } - public getAttemptedToMuzzle(userId: string) { - return ( - this.muzzled.has(userId) && this.muzzled.get(userId)!.attemptedToMuzzle - ); - } - - /** - * Tells us whether or not this is the users first muzzled message. - */ - public getIsMuzzledFirstMessage(userId: string) { - return ( - this.muzzled.has(userId) && - this.muzzled.get(userId)!.suppressionCount === 0 - ); - } - /** * Retrieves the specified user from the muzzled map by slack id. */ @@ -203,7 +186,11 @@ export class MuzzleService { /** * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ - public addUserToMuzzled(userId: string, requestorId: string) { + public addUserToMuzzled( + userId: string, + requestorId: string, + channel: string + ) { const shouldBackFire = shouldBackfire(); const userName = this.slackService.getUserName(userId); const requestorName = this.slackService.getUserName(requestorId); @@ -235,7 +222,7 @@ export class MuzzleService { ); } else if (shouldBackFire) { console.log( - `Backfiring on ${requestorName} for attempting to muzzle ${userName}` + `Backfiring on ${requestorName} | ${requestorId} for attempting to muzzle ${userName} | ${userId}` ); const timeToMuzzle = getTimeToMuzzle(); const backfireFromDb = await this.muzzlePersistenceService @@ -246,19 +233,13 @@ export class MuzzleService { }); if (backfireFromDb) { - const attemptedToMuzzle = userId; - this.backfireUser( - requestorId, - attemptedToMuzzle, - backfireFromDb.id, - timeToMuzzle - ); + this.backfireUser(requestorId, backfireFromDb.id, timeToMuzzle); this.setRequestorCount(requestorId); - resolve( - `Successfully muzzled ${userName} for ${getTimeString( - timeToMuzzle - )}` + this.webService.sendMessage( + channel, + `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:` ); + resolve(`:boom: Backfired! Better luck next time... :boom:`); } } else { const timeToMuzzle = getTimeToMuzzle(); @@ -363,16 +344,10 @@ export class MuzzleService { }); } - private backfireUser( - userId: string, - attemptedToMuzzle: string, - id: number, - timeToMuzzle: number - ) { + private backfireUser(userId: string, id: number, timeToMuzzle: number) { this.muzzled.set(userId, { suppressionCount: 0, muzzledBy: userId, - attemptedToMuzzle, id, isBackfire: true, removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 51986359..e5635878 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -4,7 +4,6 @@ export interface IMuzzled { id: number; // Refers to either a muzzleID or backfireID from the database. Dependent on isBackfire. isBackfire: boolean; removalFn: NodeJS.Timeout; - attemptedToMuzzle?: string; } export interface IRequestor { diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index eaefd3b1..b2dc400c 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -5,9 +5,17 @@ export interface IChannelResponse { } export interface ISlashCommandRequest { - text: string; + token: string; + team_id: string; + team_domain: string; + channel_id: string; + channel_name: string; user_id: string; + user_name: string; + command: string; + text: string; response_url: string; + trigger_id: string; } export interface IEventRequest { From 83eea34e9bc1c9a859d3ef46ede369b913d3baf7 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 18 Dec 2019 22:34:30 -0500 Subject: [PATCH 047/167] Refactor Muzzle Service and Muzzle Persistence Service (#58) * Added constants to the muzzle directory * Removed singleton architecture for MuzzleService * Moved stateful muzzle methods into muzzle.persistence.service * Combined functionality for addBackfire * Removed comment * Converted muzzleInstance to use new MuzzleService() * Fixed tests for muzzle.service.spec * Added tests for sendMuzzledMessage * Added support for beter muzzled text formatting * Removed persistence service spec --- dev-utils/boiler-plates/boiler.controller.ts | 11 +- package-lock.json | 354 ++++++-------- package.json | 2 + src/controllers/clap.controller.ts | 6 +- src/controllers/confession.controller.ts | 6 +- src/controllers/define.controller.ts | 9 +- src/controllers/list.controller.ts | 10 +- src/controllers/mock.controller.ts | 6 +- src/controllers/muzzle.controller.ts | 29 +- src/services/muzzle/constants.ts | 5 + .../muzzle/muzzle.persistence.service.ts | 179 ++++++- src/services/muzzle/muzzle.service.spec.ts | 457 ++++++++---------- src/services/muzzle/muzzle.service.ts | 348 ++++--------- 13 files changed, 670 insertions(+), 752 deletions(-) create mode 100644 src/services/muzzle/constants.ts diff --git a/dev-utils/boiler-plates/boiler.controller.ts b/dev-utils/boiler-plates/boiler.controller.ts index 4f2c9f11..fe6ff850 100644 --- a/dev-utils/boiler-plates/boiler.controller.ts +++ b/dev-utils/boiler-plates/boiler.controller.ts @@ -10,9 +10,16 @@ export const getBoilerPlateController = (serviceName: string) => { const relSlackModels = path .relative(`src/controllers`, "src/shared/models/slack/slack-models.ts") .slice(0, -3); + const relMuzzPersist = path + .relative( + `src/controllers`, + "src/services/muzzle/muzzle.persistence.service.ts" + ) + .slice(0, -3); const boilerPlateController = ` import express, { Router } from "express"; + import { MuzzlePersistenceService } from "${relMuzzPersist}"; import { MuzzleService } from "${relMuzzleService}"; import { SlackService } from "${relSlackService}"; import { @@ -22,12 +29,12 @@ export const getBoilerPlateController = (serviceName: string) => { export const ${serviceName}Controller: Router = express.Router(); - const muzzleService = MuzzleService.getInstance(); + const muzzleService = new MuzzleService(); const slackService = SlackService.getInstance(); ${serviceName}Controller.post("/${serviceName}", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send("Sorry, can't do that while muzzled."); } else if (!request.text) { res.send("Sorry, you must send a message to use this service."); diff --git a/package-lock.json b/package-lock.json index 900ad23e..6792f1c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } @@ -115,7 +114,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -216,7 +214,6 @@ "version": "24.7.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", - "dev": true, "requires": { "@jest/source-map": "^24.3.0", "chalk": "^2.0.1", @@ -226,8 +223,7 @@ "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" } } }, @@ -353,7 +349,6 @@ "version": "24.3.0", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", - "dev": true, "requires": { "callsites": "^3.0.0", "graceful-fs": "^4.1.15", @@ -363,14 +358,12 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -378,7 +371,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", - "dev": true, "requires": { "@jest/console": "^24.7.1", "@jest/types": "^24.8.0", @@ -449,7 +441,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^1.1.1", @@ -603,14 +594,12 @@ "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", - "dev": true + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" }, "@types/istanbul-lib-report": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "*" } @@ -619,7 +608,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" @@ -629,7 +617,6 @@ "version": "24.0.15", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.15.tgz", "integrity": "sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA==", - "dev": true, "requires": { "@types/jest-diff": "*" } @@ -637,8 +624,15 @@ "@types/jest-diff": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", - "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", - "dev": true + "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==" + }, + "@types/jest-when": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@types/jest-when/-/jest-when-2.7.0.tgz", + "integrity": "sha512-oahwfICJSaEmmtyN7JeOg6P7ey+GKigzk26zdKLxYLewmr2F3WeDPo/U+kVpIxPuZG3EiVFdRy97q9N9DoEkSg==", + "requires": { + "@types/jest": "*" + } }, "@types/lolex": { "version": "3.1.1", @@ -692,14 +686,12 @@ "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, "@types/yargs": { "version": "12.0.12", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", - "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", - "dev": true + "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==" }, "abab": { "version": "2.0.0", @@ -851,20 +843,17 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" }, "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" }, "array-equal": { "version": "1.0.0", @@ -895,8 +884,7 @@ "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "arrify": { "version": "1.0.1", @@ -922,8 +910,7 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "astral-regex": { "version": "1.0.0", @@ -951,8 +938,7 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "aws-sign2": { "version": "0.7.0", @@ -1044,7 +1030,6 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -1059,7 +1044,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -1068,7 +1052,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1077,7 +1060,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1086,7 +1068,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -1165,7 +1146,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -1183,7 +1163,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -1252,6 +1231,17 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "bunyan": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.10.6", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -1261,7 +1251,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -1365,7 +1354,6 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -1377,7 +1365,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -1489,7 +1476,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -1525,8 +1511,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", @@ -1579,8 +1564,7 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-util-is": { "version": "1.0.2", @@ -1695,8 +1679,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "dedent": { "version": "0.7.0", @@ -1738,7 +1721,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -1748,7 +1730,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1757,7 +1738,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1766,7 +1746,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -1819,8 +1798,7 @@ "diff-sequences": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", - "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", - "dev": true + "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==" }, "domexception": { "version": "1.0.1", @@ -1845,6 +1823,15 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -2003,8 +1990,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -2047,7 +2033,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -2062,7 +2047,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -2071,7 +2055,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -2082,7 +2065,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", - "dev": true, "requires": { "@jest/types": "^24.8.0", "ansi-styles": "^3.2.0", @@ -2146,7 +2128,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -2156,7 +2137,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -2167,7 +2147,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -2183,7 +2162,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -2192,7 +2170,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -2201,7 +2178,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2210,7 +2186,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2219,7 +2194,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -2286,7 +2260,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -2298,7 +2271,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -2367,8 +2339,7 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" }, "forever-agent": { "version": "0.6.1", @@ -2395,7 +2366,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, "requires": { "map-cache": "^0.2.2" } @@ -2430,7 +2400,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2451,12 +2422,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2471,17 +2444,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2598,7 +2574,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2610,6 +2587,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2624,6 +2602,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2631,12 +2610,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2655,6 +2636,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2735,7 +2717,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2747,6 +2730,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2832,7 +2816,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2868,6 +2853,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2887,6 +2873,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2930,12 +2917,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2982,8 +2971,7 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, "getpass": { "version": "0.1.7", @@ -3086,8 +3074,7 @@ "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "growly": { "version": "1.3.0", @@ -3170,7 +3157,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -3181,7 +3167,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -3191,7 +3176,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -3423,7 +3407,6 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -3432,7 +3415,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -3457,8 +3439,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.1.4", @@ -3479,7 +3460,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -3488,7 +3468,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -3505,7 +3484,6 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -3515,8 +3493,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, @@ -3529,8 +3506,7 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" }, "is-extglob": { "version": "2.1.1", @@ -3578,7 +3554,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -3587,7 +3562,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -3637,7 +3611,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "requires": { "isobject": "^3.0.1" } @@ -3698,8 +3671,7 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, "is-wsl": { "version": "1.1.0", @@ -3720,8 +3692,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "isstream": { "version": "0.1.2", @@ -4032,7 +4003,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", - "dev": true, "requires": { "chalk": "^2.0.1", "diff-sequences": "^24.3.0", @@ -4089,8 +4059,7 @@ "jest-get-type": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", - "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==", - "dev": true + "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==" }, "jest-haste-map": { "version": "24.8.1", @@ -4149,7 +4118,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", - "dev": true, "requires": { "chalk": "^2.0.1", "jest-diff": "^24.8.0", @@ -4161,7 +4129,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@jest/test-result": "^24.8.0", @@ -4176,8 +4143,7 @@ "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" } } }, @@ -4199,8 +4165,7 @@ "jest-regex-util": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", - "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==", - "dev": true + "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==" }, "jest-resolve": { "version": "24.8.0", @@ -4469,6 +4434,15 @@ "string-length": "^2.0.0" } }, + "jest-when": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jest-when/-/jest-when-2.7.0.tgz", + "integrity": "sha512-psU0pXdomBORY9TGuSut/k8vViVki9l92WggL0m5/Lk8zTrDYtcCpPIFdZQDKqXvmW5Jzoh7SCsLKITvBJ0jyQ==", + "requires": { + "bunyan": "^1.8.12", + "expect": "^24.8.0" + } + }, "jest-worker": { "version": "24.6.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", @@ -4493,8 +4467,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", @@ -4599,8 +4572,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "kleur": { "version": "3.0.3", @@ -4986,14 +4958,12 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -5045,7 +5015,6 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -5103,7 +5072,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -5113,7 +5081,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -5145,6 +5112,41 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "mysql": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.17.1.tgz", @@ -5170,14 +5172,12 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, "optional": true }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -5198,6 +5198,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -5355,7 +5361,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -5366,7 +5371,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -5375,7 +5379,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -5392,7 +5395,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, "requires": { "isobject": "^3.0.0" } @@ -5411,7 +5413,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, "requires": { "isobject": "^3.0.1" } @@ -5646,8 +5647,7 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, "path-dirname": { "version": "1.0.2", @@ -5788,8 +5788,7 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "prelude-ls": { "version": "1.1.2", @@ -5813,7 +5812,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", - "dev": true, "requires": { "@jest/types": "^24.8.0", "ansi-regex": "^4.0.0", @@ -5824,8 +5822,7 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" } } }, @@ -5928,8 +5925,7 @@ "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", - "dev": true + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "read-pkg": { "version": "5.1.1", @@ -6015,7 +6011,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" @@ -6049,14 +6044,12 @@ "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "request": { "version": "2.88.0", @@ -6161,8 +6154,7 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "restore-cursor": { "version": "2.0.0", @@ -6177,8 +6169,7 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, "retry": { "version": "0.12.0", @@ -6220,11 +6211,16 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, "requires": { "ret": "~0.1.10" } @@ -6362,7 +6358,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -6374,7 +6369,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6458,7 +6452,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -6474,7 +6467,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -6483,7 +6475,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6494,7 +6485,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -6505,7 +6495,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -6514,7 +6503,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6523,7 +6511,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6532,7 +6519,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -6545,7 +6531,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, "requires": { "kind-of": "^3.2.0" }, @@ -6554,7 +6539,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6564,14 +6548,12 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, "requires": { "atob": "^2.1.1", "decode-uri-component": "^0.2.0", @@ -6601,8 +6583,7 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, "spdx-correct": { "version": "3.1.0", @@ -6640,7 +6621,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -6675,8 +6655,7 @@ "stack-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" }, "staged-git-files": { "version": "1.1.2", @@ -6688,7 +6667,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -6698,7 +6676,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -6876,7 +6853,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -6885,7 +6861,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6896,7 +6871,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -6908,7 +6882,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -7161,7 +7134,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -7187,7 +7159,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -7197,7 +7168,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -7208,7 +7178,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, "requires": { "isarray": "1.0.0" } @@ -7218,8 +7187,7 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" } } }, @@ -7265,8 +7233,7 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" }, "url-parse-lax": { "version": "1.0.0", @@ -7280,8 +7247,7 @@ "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, "util-deprecate": { "version": "1.0.2", diff --git a/package.json b/package.json index 56fb87d9..b20c2971 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,13 @@ "license": "ISC", "dependencies": { "@slack/web-api": "^5.0.1", + "@types/jest-when": "^2.7.0", "axios": "^0.18.1", "body-parser": "^1.18.3", "easy-table": "^1.1.1", "enquirer": "^2.3.2", "express": "^4.16.4", + "jest-when": "^2.7.0", "moment": "^2.24.0", "mysql": "^2.17.1", "reflect-metadata": "^0.1.13", diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts index bd2fad42..b1c76eef 100644 --- a/src/controllers/clap.controller.ts +++ b/src/controllers/clap.controller.ts @@ -1,6 +1,6 @@ import express, { Router } from "express"; import { ClapService } from "../services/clap/clap.service"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; import { IChannelResponse, @@ -9,13 +9,13 @@ import { export const clapController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const slackService = SlackService.getInstance(); const clapService = new ClapService(); clapController.post("/clap", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to clap."); diff --git a/src/controllers/confession.controller.ts b/src/controllers/confession.controller.ts index 15abaed0..f2d628ee 100644 --- a/src/controllers/confession.controller.ts +++ b/src/controllers/confession.controller.ts @@ -1,16 +1,16 @@ import express, { Router } from "express"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { WebService } from "../services/web/web.service"; import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; export const confessionController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const webService = WebService.getInstance(); confessionController.post("/confess", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to confess."); diff --git a/src/controllers/define.controller.ts b/src/controllers/define.controller.ts index ca9cba2c..0cfb346b 100644 --- a/src/controllers/define.controller.ts +++ b/src/controllers/define.controller.ts @@ -1,23 +1,22 @@ import express, { Request, Response, Router } from "express"; import { DefineService } from "../services/define/define.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { SlackService } from "../services/slack/slack.service"; import { IUrbanDictionaryResponse } from "../shared/models/define/define-models"; import { IChannelResponse, ISlashCommandRequest } from "../shared/models/slack/slack-models"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; -import { SlackService } from "../services/slack/slack.service"; - export const defineController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const slackService = SlackService.getInstance(); const defineService = DefineService.getInstance(); defineController.post("/define", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else { const defined: IUrbanDictionaryResponse = (await defineService diff --git a/src/controllers/list.controller.ts b/src/controllers/list.controller.ts index 84cae3ab..8f979193 100644 --- a/src/controllers/list.controller.ts +++ b/src/controllers/list.controller.ts @@ -1,6 +1,6 @@ import express, { Router } from "express"; import { ListPersistenceService } from "../services/list/list.persistence.service"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { ReportService } from "../services/report/report.service"; import { SlackService } from "../services/slack/slack.service"; import { WebService } from "../services/web/web.service"; @@ -11,7 +11,7 @@ import { export const listController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const listPersistenceService = ListPersistenceService.getInstance(); @@ -19,7 +19,7 @@ const reportService = new ReportService(); listController.post("/list/retrieve", async (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else { const report = await reportService.getListReport(); @@ -30,7 +30,7 @@ listController.post("/list/retrieve", async (req, res) => { listController.post("/list/add", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to list something."); @@ -49,7 +49,7 @@ listController.post("/list/add", (req, res) => { listController.post("/list/remove", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send the item you wish to remove."); diff --git a/src/controllers/mock.controller.ts b/src/controllers/mock.controller.ts index 3488794a..e7e954ac 100644 --- a/src/controllers/mock.controller.ts +++ b/src/controllers/mock.controller.ts @@ -1,6 +1,6 @@ import express, { Router } from "express"; import { MockService } from "../services/mock/mock.service"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; import { IChannelResponse, @@ -9,13 +9,13 @@ import { export const mockController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const slackService = SlackService.getInstance(); const mockService = MockService.getInstance(); mockController.post("/mock", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzleService.isUserMuzzled(request.user_id)) { + if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to mock."); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index ab7c442f..199cb4aa 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -1,4 +1,5 @@ import express, { Request, Response, Router } from "express"; +import { ABUSE_PENALTY_TIME } from "../services/muzzle/constants"; import { getTimeString } from "../services/muzzle/muzzle-utilities"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { MuzzleService } from "../services/muzzle/muzzle.service"; @@ -13,7 +14,7 @@ import { export const muzzleController: Router = express.Router(); -const muzzleService = MuzzleService.getInstance(); +const muzzleService = new MuzzleService(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); @@ -21,8 +22,12 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; - const isUserMuzzled = muzzleService.isUserMuzzled(request.event.user); - const isUserBackfired = muzzleService.getIsBackfire(request.event.user); + const isUserMuzzled = muzzlePersistenceService.isUserMuzzled( + request.event.user + ); + const isUserBackfired = muzzlePersistenceService.getIsBackfire( + request.event.user + ); const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); @@ -30,24 +35,22 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { console.log( `${userName} | ${request.event.user} is muzzled! Suppressing his voice...` ); - webService.deleteMessage(request.event.channel, request.event.ts); muzzleService.sendMuzzledMessage( request.event.channel, request.event.user, - request.event.text + request.event.text, + request.event.ts ); } else if (isUserMuzzled && containsTag && !request.event.subtype) { - const muzzleId = muzzleService.getMuzzleId(request.event.user); + const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); console.log( `${slackService.getUserName( request.event.user - )} attempted to tag someone. Muzzle increased by ${ - muzzleService.ABUSE_PENALTY_TIME - }!` + )} attempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` ); - muzzleService.addMuzzleTime( + muzzlePersistenceService.addMuzzleTime( request.event.user, - muzzleService.ABUSE_PENALTY_TIME, + ABUSE_PENALTY_TIME, isUserBackfired ); webService.deleteMessage(request.event.channel, request.event.ts); @@ -61,7 +64,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { `:rotating_light: <@${ request.event.user }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( - muzzleService.ABUSE_PENALTY_TIME + ABUSE_PENALTY_TIME )} :rotating_light:` ); } else if (muzzleService.shouldBotMessageBeMuzzled(request)) { @@ -91,7 +94,7 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = request.user_id; console.log(request); - if (muzzleService.isUserMuzzled(userId)) { + if (muzzlePersistenceService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { res.send( diff --git a/src/services/muzzle/constants.ts b/src/services/muzzle/constants.ts new file mode 100644 index 00000000..6882f83e --- /dev/null +++ b/src/services/muzzle/constants.ts @@ -0,0 +1,5 @@ +export const MAX_MUZZLE_TIME = 3600000; +export const MAX_TIME_BETWEEN_MUZZLES = 3600000; +export const MAX_SUPPRESSIONS = 7; +export const MAX_MUZZLES = 2; +export const ABUSE_PENALTY_TIME = 300000; diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 891fb662..e878c278 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -3,9 +3,18 @@ import { getRepository } from "typeorm"; import { Backfire } from "../../shared/db/models/Backfire"; import { Muzzle } from "../../shared/db/models/Muzzle"; import { + IMuzzled, IReportRange, + IRequestor, ReportType } from "../../shared/models/muzzle/muzzle-models"; +import { + ABUSE_PENALTY_TIME, + MAX_MUZZLE_TIME, + MAX_MUZZLES, + MAX_TIME_BETWEEN_MUZZLES +} from "./constants"; +import { getRemainingTime } from "./muzzle-utilities"; export class MuzzlePersistenceService { public static getInstance() { @@ -16,21 +25,71 @@ export class MuzzlePersistenceService { } private static instance: MuzzlePersistenceService; + private muzzled: Map = new Map(); + private requestors: Map = new Map(); private constructor() {} - public addMuzzleToDb(requestorId: string, muzzledId: string, time: number) { - const muzzle = new Muzzle(); - muzzle.requestorId = requestorId; - muzzle.muzzledId = muzzledId; - muzzle.messagesSuppressed = 0; - muzzle.wordsSuppressed = 0; - muzzle.charactersSuppressed = 0; - muzzle.milliseconds = time; - return getRepository(Muzzle).save(muzzle); + public addMuzzle(requestorId: string, muzzledId: string, time: number) { + return new Promise(async (resolve, reject) => { + const muzzle = new Muzzle(); + muzzle.requestorId = requestorId; + muzzle.muzzledId = muzzledId; + muzzle.messagesSuppressed = 0; + muzzle.wordsSuppressed = 0; + muzzle.charactersSuppressed = 0; + muzzle.milliseconds = time; + await getRepository(Muzzle) + .save(muzzle) + .then(muzzleFromDb => { + this.muzzled.set(muzzledId, { + suppressionCount: 0, + muzzledBy: requestorId, + id: muzzleFromDb.id, + isBackfire: false, + removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time) + }); + this.setRequestorCount(requestorId); + resolve(); + }) + .catch(e => reject(e)); + }); + } + + /** + * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. + */ + public setRequestorCount(requestorId: string) { + const muzzleCount = this.requestors.has(requestorId) + ? ++this.requestors.get(requestorId)!.muzzleCount + : 1; + + if (this.requestors.has(requestorId)) { + clearTimeout(this.requestors.get(requestorId)! + .muzzleCountRemover as NodeJS.Timeout); + } + + const removalFunction = + this.requestors.has(requestorId) && + this.requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES + ? () => this.removeRequestor(requestorId) + : () => this.decrementMuzzleCount(requestorId); + this.requestors.set(requestorId, { + muzzleCount, + muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES) + }); + } + /** + * Returns boolean whether max muzzles have been reached. + */ + public isMaxMuzzlesReached(userId: string) { + return ( + this.requestors.has(userId) && + this.requestors.get(userId)!.muzzleCount === MAX_MUZZLES + ); } - public addBackfireToDb(muzzledId: string, time: number) { + public addBackfire(muzzledId: string, time: number) { const backfire = new Backfire(); backfire.muzzledId = muzzledId; backfire.messagesSuppressed = 0; @@ -38,7 +97,67 @@ export class MuzzlePersistenceService { backfire.charactersSuppressed = 0; backfire.milliseconds = time; - return getRepository(Backfire).save(backfire); + return getRepository(Backfire) + .save(backfire) + .then(backfireFromDb => { + this.muzzled.set(muzzledId, { + suppressionCount: 0, + muzzledBy: muzzledId, + id: backfireFromDb.id, + isBackfire: true, + removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time) + }); + this.setRequestorCount(muzzledId); + }); + } + + /** + * Adds the specified amount of time to a specified muzzled user. + */ + public addMuzzleTime(userId: string, timeToAdd: number, isBackfire: boolean) { + if (userId && this.muzzled.has(userId)) { + const removalFn = this.muzzled.get(userId)!.removalFn; + const newTime = getRemainingTime(removalFn) + timeToAdd; + const muzzleId = this.muzzled.get(userId)!.id; + this.incrementMuzzleTime(muzzleId, ABUSE_PENALTY_TIME, isBackfire); + clearTimeout(this.muzzled.get(userId)!.removalFn); + console.log(`Setting ${userId}'s muzzle time to ${newTime}`); + this.muzzled.set(userId, { + suppressionCount: this.muzzled.get(userId)!.suppressionCount, + muzzledBy: this.muzzled.get(userId)!.muzzledBy, + id: this.muzzled.get(userId)!.id, + isBackfire: this.muzzled.get(userId)!.isBackfire, + removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) + }); + } + } + + public setMuzzle(userId: string, options: IMuzzled) { + this.muzzled.set(userId, options); + } + + public getMuzzle(userId: string) { + return this.muzzled.get(userId); + } + /** + * Gets the corresponding database ID for the user's current muzzle. + */ + public getMuzzleId(userId: string) { + return this.muzzled.get(userId)!.id; + } + + /** + * Returns boolean whether user is muzzled or not. + */ + public isUserMuzzled(userId: string): boolean { + return this.muzzled.has(userId); + } + + /** + * Retrieves whether or not a muzzle is backfired. + */ + public getIsBackfire(userId: string) { + return this.muzzled.has(userId) && this.muzzled.get(userId)!.isBackfire; } public incrementMuzzleTime(id: number, ms: number, isBackfire: boolean) { @@ -185,6 +304,44 @@ export class MuzzlePersistenceService { }; } + /** + * Removes a requestor from the map. + */ + private removeRequestor(userId: string) { + this.requestors.delete(userId); + console.log( + `${MAX_MUZZLE_TIME} has passed since ${userId} last successful muzzle. They have been removed from requestors.` + ); + } + + /** + * Removes a muzzle from the specified user. + */ + private removeMuzzle(userId: string) { + this.muzzled.delete(userId); + console.log(`Removed ${userId}'s muzzle! He is free at last.`); + } + + /** + * Decrements the muzzleCount on a requestor. + */ + private decrementMuzzleCount(requestorId: string) { + if (this.requestors.has(requestorId)) { + const decrementedMuzzle = --this.requestors.get(requestorId)!.muzzleCount; + this.requestors.set(requestorId, { + muzzleCount: decrementedMuzzle, + muzzleCountRemover: this.requestors.get(requestorId)!.muzzleCountRemover + }); + console.log( + `Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}` + ); + } else { + console.error( + `Attemped to decrement muzzle count for ${requestorId} but they did not exist!` + ); + } + } + private getMostMuzzledByInstances(range: IReportRange) { const query = range.reportType === ReportType.AllTime diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 1c2083ee..94b37c15 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -1,27 +1,31 @@ +import { when } from "jest-when"; import { UpdateResult } from "typeorm"; import { Muzzle } from "../../shared/db/models/Muzzle"; +import { IMuzzled } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest, ISlackUser } from "../../shared/models/slack/slack-models"; import { SlackService } from "../slack/slack.service"; +import { WebService } from "../web/web.service"; +import { MAX_SUPPRESSIONS } from "./constants"; import * as muzzleUtils from "./muzzle-utilities"; import { MuzzlePersistenceService } from "./muzzle.persistence.service"; import { MuzzleService } from "./muzzle.service"; describe("MuzzleService", () => { const testData = { - user: "123", + user123: "123", user2: "456", user3: "789", requestor: "666" }; - let muzzleInstance: MuzzleService; + let muzzleService: MuzzleService; let slackInstance: SlackService; beforeEach(() => { - muzzleInstance = MuzzleService.getInstance(); + muzzleService = new MuzzleService(); slackInstance = SlackService.getInstance(); slackInstance.userList = [ { id: "123", name: "test123" }, @@ -62,128 +66,24 @@ describe("MuzzleService", () => { it("should always muzzle a tagged user", () => { const testSentence = "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe( - " ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. " + expect(muzzleService.muzzle(testSentence, 1, false)).toBe( + "..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.." ); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); + expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); + expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); }); it("should always muzzle a word with length > 10", () => { const testSentence = "this.is.a.way.to.game.the.system"; - expect(muzzleInstance.muzzle(testSentence, 1, false)).toBe(" ..mMm.. "); - }); - }); - - describe("getMuzzleId()", () => { - it("should return the database id of the muzzledUser by id", async () => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValue(mockMuzzle as Muzzle); - jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - expect(muzzleInstance.getMuzzleId("123")).toBe(1); - }); - }); - - describe("getMuzzledUserById()", () => { - beforeEach(async () => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValue(mockMuzzle as Muzzle); - jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - }); - - it("should return the muzzled user when a valid id is passed in", async () => { - const muzzledUser = muzzleInstance.getMuzzledUserById("123"); - expect(muzzledUser!.id).toBe(1); - expect(muzzledUser!.muzzledBy).toBe("666"); - expect(muzzledUser!.removalFn).toBeDefined(); - expect(muzzledUser!.suppressionCount).toBe(0); - }); - }); - - describe("getRequestorById()", () => { - beforeEach(async () => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValue(mockMuzzle as Muzzle); - jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - }); - it("should return the requestor when a valid id is passed in", async () => { - const requestor = muzzleInstance.getRequestorById("666"); - expect(requestor!.muzzleCount).toBe(1); - expect(requestor!.muzzleCountRemover).toBeDefined(); - }); - }); - - describe("isUserMuzzled()", () => { - beforeEach(async () => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValue(mockMuzzle as Muzzle); - jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - }); - it("should return true when a muzzled userId is passed in", async () => { - expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); - }); - - it("should return false when an unmuzzled userId is passed in", async () => { - expect(muzzleInstance.isUserMuzzled(testData.user2)).toBe(false); - }); - }); - - describe("isUserRequestor()", () => { - beforeEach(async () => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValue(mockMuzzle as Muzzle); - jest.spyOn(muzzleUtils, "shouldBackfire").mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - }); - - it("should return true when a requestor userId is passed in", () => { - expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); - }); - - it("should return false when a non-requestor userId is passed in", () => { - expect(muzzleInstance.isUserRequestor(testData.user)).toBe(false); + expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); }); }); @@ -206,64 +106,62 @@ describe("MuzzleService", () => { } } as IEventRequest; }); + describe("when a user is muzzled", () => { - beforeEach(async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); + beforeEach(() => { + jest + .spyOn(MuzzlePersistenceService.getInstance(), "isUserMuzzled") + .mockImplementation(() => true); }); + it("should return true if an id is present in the event.text ", () => { mockRequest.event.attachments = []; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( - true - ); + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); it("should return true if an id is present in the event.attachments[0].text", () => { mockRequest.event.text = "whatever"; mockRequest.event.attachments[0].pretext = "whatever"; mockRequest.event.attachments[0].callback_id = "whatever"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( - true - ); + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); it("should return true if an id is present in the event.attachments[0].pretext", () => { mockRequest.event.text = "whatever"; mockRequest.event.attachments[0].text = "whatever"; mockRequest.event.attachments[0].callback_id = "whatever"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( - true - ); + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); it("should return the id present in the event.attachments[0].callback_id if an id is present", () => { mockRequest.event.text = "whatever"; mockRequest.event.attachments[0].text = "whatever"; mockRequest.event.attachments[0].pretext = "whatever"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( - true - ); + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); }); - describe("negative path", () => { + describe("when a user is not muzzled", () => { + beforeEach(() => { + jest + .spyOn(MuzzlePersistenceService.getInstance(), "isUserMuzzled") + .mockImplementation(() => false); + }); + it("should return false if there is no id present in any fields", () => { mockRequest.event.text = "no id"; mockRequest.event.callback_id = "TEST_TEST"; mockRequest.event.attachments[0].text = "test"; mockRequest.event.attachments[0].pretext = "test"; mockRequest.event.attachments[0].callback_id = "TEST"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( false ); }); it("should return false if the message is not a bot_message", () => { mockRequest.event.subtype = "not_bot_message"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( false ); }); @@ -273,14 +171,14 @@ describe("MuzzleService", () => { mockRequest.event.attachments[0].text = "<@456>"; mockRequest.event.attachments[0].pretext = "<@456>"; mockRequest.event.attachments[0].callback_id = "TEST_456"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( false ); }); it("should return false if the bot username is muzzle", () => { mockRequest.event.username = "muzzle"; - expect(muzzleInstance.shouldBotMessageBeMuzzled(mockRequest)).toBe( + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( false ); }); @@ -289,175 +187,226 @@ describe("MuzzleService", () => { describe("addUserToMuzzled()", () => { describe("muzzled", () => { - describe("positive path", () => { + describe("when the user is not already muzzled", () => { + let mockAddMuzzle: jest.SpyInstance; + beforeEach(() => { const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + const persistenceService = MuzzlePersistenceService.getInstance(); + mockAddMuzzle = jest + .spyOn(persistenceService, "addMuzzle") .mockResolvedValue(mockMuzzle as Muzzle); + jest + .spyOn(persistenceService, "isUserMuzzled") + .mockImplementation(() => false); + jest .spyOn(muzzleUtils, "shouldBackfire") .mockImplementation(() => false); }); - it("should add a user to the muzzled map", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, + it("should call MuzzlePersistenceService.addMuzzle()", async () => { + await muzzleService.addUserToMuzzled( + testData.user123, testData.requestor, "test" ); - expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); - }); - it("should return an added user with IMuzzled attributes", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - expect( - muzzleInstance.getMuzzledUserById(testData.user)!.suppressionCount - ).toBe(0); - expect( - muzzleInstance.getMuzzledUserById(testData.user)!.muzzledBy - ).toBe(testData.requestor); - expect(muzzleInstance.getMuzzledUserById(testData.user)!.id).toBe(1); - expect( - muzzleInstance.getMuzzledUserById(testData.user)!.removalFn - ).toBeDefined(); + expect(mockAddMuzzle).toHaveBeenCalled(); }); }); - describe("negative path", () => { - it("should reject if a user tries to muzzle an already muzzled user", async () => { + describe("when a user is already muzzled", () => { + let addMuzzleMock: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + const persistenceService = MuzzlePersistenceService.getInstance(); + addMuzzleMock = jest + .spyOn(persistenceService, "addMuzzle") .mockResolvedValue(mockMuzzle as Muzzle); + jest .spyOn(muzzleUtils, "shouldBackfire") .mockImplementation(() => false); - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); - await muzzleInstance - .addUserToMuzzled(testData.user, testData.requestor, "test") + + jest + .spyOn(persistenceService, "isUserMuzzled") + .mockImplementation(() => true); + }); + + it("should reject if a user tries to muzzle an already muzzled user", async () => { + await muzzleService + .addUserToMuzzled(testData.user123, testData.requestor, "test") .catch(e => { expect(e).toBe("test123 is already muzzled!"); + expect(addMuzzleMock).not.toHaveBeenCalled(); }); }); it("should reject if a user tries to muzzle a user that does not exist", async () => { - await muzzleInstance + await muzzleService .addUserToMuzzled("", testData.requestor, "test") .catch(e => { expect(e).toBe( `Invalid username passed in. You can only muzzle existing slack users` ); - expect(muzzleInstance.isUserMuzzled("")).toBe(false); - }); - }); - - it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - expect(muzzleInstance.isUserMuzzled(testData.user)).toBe(true); - await muzzleInstance - .addUserToMuzzled(testData.requestor, testData.user, "test") - .catch(e => { - expect(e).toBe( - `You can't muzzle someone if you are already muzzled!` - ); }); }); }); - }); - describe("requestors", () => { - describe("positive path", () => { + describe("when a requestor is already muzzled", () => { + let addMuzzleMock: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") + const persistenceService = MuzzlePersistenceService.getInstance(); + addMuzzleMock = jest + .spyOn(persistenceService, "addMuzzle") .mockResolvedValue(mockMuzzle as Muzzle); + jest .spyOn(muzzleUtils, "shouldBackfire") .mockImplementation(() => false); - }); - it("should add a user to the requestors map", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); - }); - it("should return an added user with IMuzzler attributes", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" + const mockIsUserMuzzled = jest.spyOn( + persistenceService, + "isUserMuzzled" ); - expect( - muzzleInstance.getRequestorById(testData.requestor)!.muzzleCount - ).toBe(1); + + when(mockIsUserMuzzled) + .calledWith(testData.requestor) + .mockImplementation(() => true); }); - it("should increment a requestors muzzle count on a second muzzleInstance.addUserToMuzzled() call", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - await muzzleInstance.addUserToMuzzled( - testData.user2, - testData.requestor, - "test" - ); - expect(muzzleInstance.isUserRequestor(testData.requestor)).toBe(true); - expect( - muzzleInstance.getRequestorById(testData.requestor)!.muzzleCount - ).toBe(2); + it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { + await muzzleService + .addUserToMuzzled(testData.user123, testData.requestor, "test") + .catch(e => { + expect(e).toBe( + `You can't muzzle someone if you are already muzzled!` + ); + expect(addMuzzleMock).not.toHaveBeenCalled(); + }); }); }); + }); - describe("negative path", () => { - beforeEach(() => { - const mockMuzzle = { id: 1 }; - jest - .spyOn(MuzzlePersistenceService.getInstance(), "addMuzzleToDb") - .mockResolvedValueOnce(mockMuzzle as Muzzle); - jest - .spyOn(muzzleUtils, "shouldBackfire") - .mockImplementation(() => false); - }); - it("should prevent a requestor from muzzling on their third count", async () => { - await muzzleInstance.addUserToMuzzled( - testData.user, - testData.requestor, - "test" - ); - await muzzleInstance.addUserToMuzzled( - testData.user2, - testData.requestor, - "test" + describe("maxMuzzleLimit", () => { + beforeEach(() => { + const mockMuzzle = { id: 1 }; + const persistenceService = MuzzlePersistenceService.getInstance(); + jest + .spyOn(persistenceService, "addMuzzle") + .mockResolvedValue(mockMuzzle as Muzzle); + + jest + .spyOn(muzzleUtils, "shouldBackfire") + .mockImplementation(() => false); + + jest + .spyOn(persistenceService, "isMaxMuzzlesReached") + .mockImplementation(() => true); + + jest + .spyOn(persistenceService, "isUserMuzzled") + .mockImplementation(() => false); + }); + + it("should prevent a requestor from muzzling when isMaxMuzzlesReached is true", async () => { + await muzzleService + .addUserToMuzzled(testData.user3, testData.requestor, "test") + .catch(e => + expect(e).toBe( + `You're doing that too much. Only 2 muzzles are allowed per hour.` + ) ); - await muzzleInstance - .addUserToMuzzled(testData.user3, testData.requestor, "test") - .catch(e => - expect(e).toBe( - `You're doing that too much. Only 2 muzzles are allowed per hour.` - ) - ); - }); + }); + }); + }); + + describe("sendMuzzledMessage", () => { + let persistenceService: MuzzlePersistenceService; + let webService: WebService; + + beforeEach(() => { + persistenceService = MuzzlePersistenceService.getInstance(); + webService = WebService.getInstance(); + }); + + describe("if a user is already muzzled", () => { + let mockMuzzle: IMuzzled; + let mockSetMuzzle: jest.SpyInstance; + let mockSendMessage: jest.SpyInstance; + let mockTrackDeleted: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + mockMuzzle = { + suppressionCount: 0, + muzzledBy: "test", + id: 1234, + isBackfire: false, + removalFn: setTimeout(() => 1234, 5000) + }; + + mockSetMuzzle = jest.spyOn(persistenceService, "setMuzzle"); + mockSendMessage = jest + .spyOn(webService, "sendMessage") + .mockImplementation(() => true); + mockTrackDeleted = jest.spyOn( + persistenceService, + "trackDeletedMessage" + ); + + jest.spyOn(persistenceService, "getMuzzle").mockReturnValue(mockMuzzle); + }); + + it("should call muzzlePersistenceService.setMuzzle and webService.sendMessage if suppressionCount is 0", () => { + muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + expect(mockSetMuzzle).toHaveBeenCalled(); + expect(mockSendMessage).toHaveBeenCalled(); + }); + + it("should not call setMuzzle, not call sendMessage, but call trackDeletedMessage if suppressionCount >= MAX_SUPPRESSIONS", () => { + mockMuzzle.suppressionCount = MAX_SUPPRESSIONS; + muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + expect(mockSetMuzzle).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockTrackDeleted).toHaveBeenCalled(); + }); + }); + + describe("if a user is not muzzled", () => { + let mockSetMuzzle: jest.SpyInstance; + let mockSendMessage: jest.SpyInstance; + let mockTrackDeleted: jest.SpyInstance; + let mockGetMuzzle: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + mockSetMuzzle = jest.spyOn(persistenceService, "setMuzzle"); + mockSendMessage = jest + .spyOn(webService, "sendMessage") + .mockImplementation(() => true); + mockTrackDeleted = jest.spyOn( + persistenceService, + "trackDeletedMessage" + ); + + mockGetMuzzle = jest + .spyOn(persistenceService, "getMuzzle") + .mockReturnValue(undefined); + }); + it("should not call any methods except getMuzzle", () => { + muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + expect(mockGetMuzzle).toHaveBeenCalled(); + expect(mockSetMuzzle).not.toHaveBeenCalled(); + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(mockTrackDeleted).not.toHaveBeenCalled(); }); }); }); diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 2edd1a21..06255c34 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -1,9 +1,9 @@ -import { IMuzzled, IRequestor } from "../../shared/models/muzzle/muzzle-models"; +import { IMuzzled } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; import { SlackService } from "../slack/slack.service"; import { WebService } from "../web/web.service"; +import { MAX_MUZZLES, MAX_SUPPRESSIONS } from "./constants"; import { - getRemainingTime, getTimeString, getTimeToMuzzle, isRandomEven, @@ -12,46 +12,32 @@ import { import { MuzzlePersistenceService } from "./muzzle.persistence.service"; export class MuzzleService { - public static getInstance() { - if (!MuzzleService.instance) { - MuzzleService.instance = new MuzzleService(); - } - return MuzzleService.instance; - } - - private static instance: MuzzleService; - public ABUSE_PENALTY_TIME = 300000; private webService = WebService.getInstance(); private slackService = SlackService.getInstance(); private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); - private MAX_MUZZLE_TIME = 3600000; - private MAX_TIME_BETWEEN_MUZZLES = 3600000; - private MAX_SUPPRESSIONS = 7; - private MAX_MUZZLES = 2; - private muzzled: Map = new Map(); - private requestors: Map = new Map(); - private constructor() {} /** * Takes in text and randomly muzzles certain words. */ public muzzle(text: string, muzzleId: number, isBackfire: boolean) { - const replacementText = " ..mMm.. "; - let returnText = ""; + const replacementText = "..mMm.."; const words = text.split(" "); + + let returnText = ""; let wordsSuppressed = 0; let charactersSuppressed = 0; let replacementWord; - for (const word of words) { - replacementWord = - isRandomEven() && - word.length < 10 && - !this.slackService.containsTag(word) - ? ` *${word}* ` - : replacementText; + + for (let i = 0; i < words.length; i++) { + replacementWord = this.getReplacementWord( + words[i], + i === 0, + i === words.length - 1, + replacementText + ); if (replacementWord === replacementText) { wordsSuppressed++; - charactersSuppressed += word.length; + charactersSuppressed += words[i].length; } returnText += replacementWord; } @@ -71,75 +57,7 @@ export class MuzzleService { ); return returnText; } - /** - * Adds the specified amount of time to a specified muzzled user. - */ - public addMuzzleTime(userId: string, timeToAdd: number, isBackfire: boolean) { - if (userId && this.muzzled.has(userId)) { - const removalFn = this.muzzled.get(userId)!.removalFn; - const newTime = getRemainingTime(removalFn) + timeToAdd; - const muzzleId = this.muzzled.get(userId)!.id; - this.muzzlePersistenceService.incrementMuzzleTime( - muzzleId, - this.ABUSE_PENALTY_TIME, - isBackfire - ); - clearTimeout(this.muzzled.get(userId)!.removalFn); - console.log( - `Setting ${this.slackService.getUserName( - userId - )}'s muzzle time to ${newTime}` - ); - this.muzzled.set(userId, { - suppressionCount: this.muzzled.get(userId)!.suppressionCount, - muzzledBy: this.muzzled.get(userId)!.muzzledBy, - id: this.muzzled.get(userId)!.id, - isBackfire: this.muzzled.get(userId)!.isBackfire, - removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) - }); - } - } - /** - * Gets the corresponding database ID for the user's current muzzle. - */ - public getMuzzleId(userId: string) { - return this.muzzled.get(userId)!.id; - } - /** - * Retrieves whether or not a muzzle is backfired. - */ - public getIsBackfire(userId: string) { - return this.muzzled.has(userId) && this.muzzled.get(userId)!.isBackfire; - } - - /** - * Retrieves the specified user from the muzzled map by slack id. - */ - public getMuzzledUserById(slackId: string) { - return this.muzzled.get(slackId); - } - - /** - * Retrieves the specified user from the requestors map by slack id. - */ - public getRequestorById(slackId: string) { - return this.requestors.get(slackId); - } - - /** - * Returns boolean whether user is muzzled or not. - */ - public isUserMuzzled(userId: string) { - return this.muzzled.has(userId); - } - - /** - * Returns boolean whether user is a requestor or not. - */ - public isUserRequestor(userId: string) { - return this.requestors.has(userId); - } /** * Determines whether or not a bot message should be removed. */ @@ -178,7 +96,7 @@ export class MuzzleService { return !!( request.event.subtype === "bot_message" && finalUserId && - this.isUserMuzzled(finalUserId) && + this.muzzlePersistenceService.isUserMuzzled(finalUserId) && request.event.username !== "muzzle" ); } @@ -199,66 +117,58 @@ export class MuzzleService { reject( `Invalid username passed in. You can only muzzle existing slack users` ); - } else if (this.isUserMuzzled(userId)) { + } else if (this.muzzlePersistenceService.isUserMuzzled(userId)) { console.error( `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` ); reject(`${userName} is already muzzled!`); - } else if (this.isUserMuzzled(requestorId)) { + } else if (this.muzzlePersistenceService.isUserMuzzled(requestorId)) { console.error( `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled` ); reject(`You can't muzzle someone if you are already muzzled!`); - } else if (this.isMaxMuzzlesReached(requestorId)) { + } else if ( + this.muzzlePersistenceService.isMaxMuzzlesReached(requestorId) + ) { console.error( - `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${ - this.MAX_MUZZLES - }` + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${MAX_MUZZLES}` ); reject( - `You're doing that too much. Only ${ - this.MAX_MUZZLES - } muzzles are allowed per hour.` + `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ); } else if (shouldBackFire) { console.log( `Backfiring on ${requestorName} | ${requestorId} for attempting to muzzle ${userName} | ${userId}` ); const timeToMuzzle = getTimeToMuzzle(); - const backfireFromDb = await this.muzzlePersistenceService - .addBackfireToDb(requestorId, timeToMuzzle) + await this.muzzlePersistenceService + .addBackfire(requestorId, timeToMuzzle) + .then(() => { + this.webService.sendMessage( + channel, + `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:` + ); + resolve(`:boom: Backfired! Better luck next time... :boom:`); + }) .catch((e: any) => { console.error(e); reject(`Muzzle failed!`); }); - - if (backfireFromDb) { - this.backfireUser(requestorId, backfireFromDb.id, timeToMuzzle); - this.setRequestorCount(requestorId); - this.webService.sendMessage( - channel, - `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:` - ); - resolve(`:boom: Backfired! Better luck next time... :boom:`); - } } else { const timeToMuzzle = getTimeToMuzzle(); - const muzzleFromDb = await this.muzzlePersistenceService - .addMuzzleToDb(requestorId, userId, timeToMuzzle) + await this.muzzlePersistenceService + .addMuzzle(requestorId, userId, timeToMuzzle) + .then(() => { + resolve( + `Successfully muzzled ${userName} for ${getTimeString( + timeToMuzzle + )}` + ); + }) .catch((e: any) => { console.error(e); reject(`Muzzle failed!`); }); - - if (muzzleFromDb) { - this.muzzleUser(userId, requestorId, muzzleFromDb.id, timeToMuzzle); - this.setRequestorCount(requestorId); - resolve( - `Successfully muzzled ${userName} for ${getTimeString( - timeToMuzzle - )}` - ); - } } }); } @@ -266,139 +176,59 @@ export class MuzzleService { /** * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. */ - public sendMuzzledMessage(channel: string, userId: string, text: string) { - const muzzleId = this.muzzled.get(userId)!.id; - const isBackfire = this.muzzled.get(userId)!.isBackfire; - if (this.muzzled.get(userId)!.suppressionCount < this.MAX_SUPPRESSIONS) { - this.muzzled.set(userId, { - suppressionCount: ++this.muzzled.get(userId)!.suppressionCount, - muzzledBy: this.muzzled.get(userId)!.muzzledBy, - id: muzzleId, - isBackfire, - removalFn: this.muzzled.get(userId)!.removalFn - }); - this.webService.sendMessage( - channel, - `<@${userId}> says "${this.muzzle(text, muzzleId, isBackfire)}"` - ); - } else { - this.muzzlePersistenceService.trackDeletedMessage( - muzzleId, - text, - isBackfire - ); - } - } - - /** - * Decrements the muzzleCount on a requestor. - */ - private decrementMuzzleCount(requestorId: string) { - if (this.requestors.has(requestorId)) { - const decrementedMuzzle = --this.requestors.get(requestorId)!.muzzleCount; - this.requestors.set(requestorId, { - muzzleCount: decrementedMuzzle, - muzzleCountRemover: this.requestors.get(requestorId)!.muzzleCountRemover - }); - console.log( - `Successfully decremented ${this.slackService.getUserName( - requestorId - )} | ${requestorId} muzzleCount to ${decrementedMuzzle}` - ); - } else { - console.error( - `Attemped to decrement muzzle count for ${this.slackService.getUserName( - requestorId - )} | ${requestorId} but they did not exist!` - ); - } - } - - /** - * Removes a muzzle from the specified user. - */ - private removeMuzzle(userId: string) { - this.muzzled.delete(userId); - console.log( - `Removed ${this.slackService.getUserName( - userId - )} | ${userId}'s muzzle! He is free at last.` - ); - } - - /** - * Adds a userId to the muzzled map, and sets timeout for removeMuzzle. - */ - private muzzleUser( + public sendMuzzledMessage( + channel: string, userId: string, - requestorId: string, - id: number, - timeToMuzzle: number + text: string, + timestamp: string ) { - this.muzzled.set(userId, { - suppressionCount: 0, - muzzledBy: requestorId, - id, - isBackfire: false, - removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) - }); - } - - private backfireUser(userId: string, id: number, timeToMuzzle: number) { - this.muzzled.set(userId, { - suppressionCount: 0, - muzzledBy: userId, - id, - isBackfire: true, - removalFn: setTimeout(() => this.removeMuzzle(userId), timeToMuzzle) - }); - } - - /** - * Removes a requestor from the map. - */ - private removeRequestor(userId: string) { - this.requestors.delete(userId); - console.log( - `${this.MAX_MUZZLE_TIME} has passed since ${this.slackService.getUserName( - userId - )} | ${userId} last successful muzzle. They have been removed from requestors.` - ); - } - /** - * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. - */ - private setRequestorCount(requestorId: string) { - const muzzleCount = this.requestors.has(requestorId) - ? ++this.requestors.get(requestorId)!.muzzleCount - : 1; - - if (this.requestors.has(requestorId)) { - clearTimeout(this.requestors.get(requestorId)! - .muzzleCountRemover as NodeJS.Timeout); + const muzzle: + | IMuzzled + | undefined = this.muzzlePersistenceService.getMuzzle(userId); + if (muzzle) { + const isBackfire = muzzle!.isBackfire; + this.webService.deleteMessage(channel, timestamp); + if (muzzle!.suppressionCount < MAX_SUPPRESSIONS) { + this.muzzlePersistenceService.setMuzzle(userId, { + suppressionCount: ++muzzle!.suppressionCount, + muzzledBy: muzzle!.muzzledBy, + id: muzzle!.id, + isBackfire, + removalFn: muzzle!.removalFn + }); + this.webService.sendMessage( + channel, + `<@${userId}> says "${this.muzzle(text, muzzle!.id, isBackfire)}"` + ); + } else { + this.muzzlePersistenceService.trackDeletedMessage( + muzzle!.id, + text, + isBackfire + ); + } } - - const removalFunction = - this.requestors.has(requestorId) && - this.requestors.get(requestorId)!.muzzleCount === this.MAX_MUZZLES - ? () => this.removeRequestor(requestorId) - : () => this.decrementMuzzleCount(requestorId); - this.requestors.set(requestorId, { - muzzleCount, - muzzleCountRemover: setTimeout( - removalFunction, - this.MAX_TIME_BETWEEN_MUZZLES - ) - }); } - /** - * Returns boolean whether max muzzles have been reached. - */ - private isMaxMuzzlesReached(userId: string) { - return ( - this.requestors.has(userId) && - this.requestors.get(userId)!.muzzleCount === this.MAX_MUZZLES - ); + private getReplacementWord( + word: string, + isFirstWord: boolean, + isLastWord: boolean, + replacementText: string + ) { + if ( + isRandomEven() && + word.length < 10 && + word !== " " && + !this.slackService.containsTag(word) + ) { + return `*${word}*`; + } else if (isFirstWord && !isLastWord) { + return `${replacementText} `; + } else if (isLastWord) { + return replacementText; + } else { + return `${replacementText} `; + } } } From 9e3ca56f0054a357c0b94812fb319e3ebb6558ad Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 18 Dec 2019 22:43:55 -0500 Subject: [PATCH 048/167] Muzzle Spacing (#59) * Added better spacing for text and adjusted accounting for muzzled words * Consolidated logic on getReplacementWord --- src/services/muzzle/muzzle.service.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 06255c34..56808901 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -35,7 +35,7 @@ export class MuzzleService { i === words.length - 1, replacementText ); - if (replacementWord === replacementText) { + if (replacementWord.includes(replacementText)) { wordsSuppressed++; charactersSuppressed += words[i].length; } @@ -216,19 +216,17 @@ export class MuzzleService { isLastWord: boolean, replacementText: string ) { - if ( + const text = isRandomEven() && word.length < 10 && word !== " " && !this.slackService.containsTag(word) - ) { - return `*${word}*`; - } else if (isFirstWord && !isLastWord) { - return `${replacementText} `; - } else if (isLastWord) { - return replacementText; - } else { - return `${replacementText} `; + ? word + : replacementText; + + if ((isFirstWord && !isLastWord) || (!isFirstWord && !isLastWord)) { + return `${text} `; } + return text; } } From cb04b73976fe5eab7c05fd7a1aa54868f9b5f61e Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 18 Dec 2019 22:47:26 -0500 Subject: [PATCH 049/167] Added support for bolded text when sent through (#60) --- src/services/muzzle/muzzle.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 56808901..d43081f7 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -221,7 +221,7 @@ export class MuzzleService { word.length < 10 && word !== " " && !this.slackService.containsTag(word) - ? word + ? `*${word}*` : replacementText; if ((isFirstWord && !isLastWord) || (!isFirstWord && !isLastWord)) { From 55fb8ba04fa1833e744443188f15b1cf19dc2d1a Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 3 Jan 2020 12:08:50 -0500 Subject: [PATCH 050/167] Added support for refreshing the local cache of users when a new user is added (#61) --- src/controllers/muzzle.controller.ts | 6 ++++++ src/services/slack/slack.service.ts | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 199cb4aa..ce984b02 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -22,6 +22,12 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; + const isNewUserAdded = request.type === "team_join"; + + if (isNewUserAdded) { + slackService.getAllUsers(); + } + const isUserMuzzled = muzzlePersistenceService.isUserMuzzled( request.event.user ); diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index ceb94950..ed07a050 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -94,9 +94,13 @@ export class SlackService { * Retrieves a list of all users. */ public async getAllUsers() { + console.log("Retrieving new user list..."); this.userList = (await this.web .getAllUsers() - .then(resp => resp.members as ISlackUser[]) + .then(resp => { + console.log("New user list has been retrieved!"); + return resp.members as ISlackUser[]; + }) .catch(e => { console.error("Failed to retrieve users", e); console.error("Retrying in 5 seconds"); From adcfd031031640bc470923adec2530e7efbe4997 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 5 Jan 2020 15:19:07 -0500 Subject: [PATCH 051/167] Add Counter (#62) * Added baseline classes for counter. * Moved backfire into its own service * Added small test for backfire * Added support for counter-muzzles inside of the counter service to prevent text from going through while countered successfully * Added counterController and adjust responses * fixed small issue with counter that would prevent a counter from functioning properly * added retrieval of userid based on <@UJSET> format userids * Added logs for id * Changed counterMuzzle to always be 5 minutes * Removed unused import --- src/controllers/counter.controller.ts | 42 ++++ src/controllers/muzzle.controller.ts | 170 ++++++++++++---- src/index.ts | 2 + .../backfire/backfire.persistence.service.ts | 110 ++++++++++ .../backfire/backfire.service.spec.ts | 28 +++ src/services/backfire/backfire.service.ts | 151 ++++++++++++++ src/services/counter/constants.ts | 1 + .../counter/counter.persistence.service.ts | 141 +++++++++++++ src/services/counter/counter.service.spec.ts | 7 + src/services/counter/counter.service.ts | 192 ++++++++++++++++++ src/services/muzzle/constants.ts | 1 + .../muzzle/muzzle.persistence.service.ts | 97 ++++----- src/services/muzzle/muzzle.service.spec.ts | 12 +- src/services/muzzle/muzzle.service.ts | 53 ++--- src/services/slack/slack.service.ts | 2 +- src/shared/db/models/Counter.ts | 19 ++ src/shared/models/backfire/backfire.model.ts | 5 + src/shared/models/counter/counter-models.ts | 11 + src/shared/models/muzzle/muzzle-models.ts | 4 +- 19 files changed, 912 insertions(+), 136 deletions(-) create mode 100644 src/controllers/counter.controller.ts create mode 100644 src/services/backfire/backfire.persistence.service.ts create mode 100644 src/services/backfire/backfire.service.spec.ts create mode 100644 src/services/backfire/backfire.service.ts create mode 100644 src/services/counter/constants.ts create mode 100644 src/services/counter/counter.persistence.service.ts create mode 100644 src/services/counter/counter.service.spec.ts create mode 100644 src/services/counter/counter.service.ts create mode 100644 src/shared/db/models/Counter.ts create mode 100644 src/shared/models/backfire/backfire.model.ts create mode 100644 src/shared/models/counter/counter-models.ts diff --git a/src/controllers/counter.controller.ts b/src/controllers/counter.controller.ts new file mode 100644 index 00000000..23b32236 --- /dev/null +++ b/src/controllers/counter.controller.ts @@ -0,0 +1,42 @@ +import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterService } from "../services/counter/counter.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { SlackService } from "../services/slack/slack.service"; +import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; + +export const counterController: Router = express.Router(); + +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backFirePersistenceService = BackFirePersistenceService.getInstance(); +const slackService = SlackService.getInstance(); +const counterService = new CounterService(); + +counterController.post("/counter", async (req, res) => { + const request: ISlashCommandRequest = req.body; + console.log(request.text); + const userId = slackService.getUserId(request.text); + console.log(userId); + const counter = counterService.getCounterByRequestorAndUserId( + userId, + request.user_id + ); + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backFirePersistenceService.isBackfire(request.user_id) + ) { + res.send("You can't counter someone if you are already muzzled!"); + } else if (!request.text) { + res.send( + "Sorry, you must specify who you would like to counter in order to use this service." + ); + } else if (counter) { + counterService.removeCounter(counter, true, request.channel_name); + res.send("Sorry, your counter has been countered."); + } else { + await counterService + .createCounter(userId, request.user_id) + .then(value => res.send(value)) + .catch(e => res.send(e)); + } +}); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index ce984b02..f276f7ca 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -1,4 +1,8 @@ import express, { Request, Response, Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { BackfireService } from "../services/backfire/backfire.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; +import { CounterService } from "../services/counter/counter.service"; import { ABUSE_PENALTY_TIME } from "../services/muzzle/constants"; import { getTimeString } from "../services/muzzle/muzzle-utilities"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; @@ -15,9 +19,13 @@ import { export const muzzleController: Router = express.Router(); const muzzleService = new MuzzleService(); +const backfireService = new BackfireService(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); +const counterService = new CounterService(); const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { @@ -31,53 +39,133 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const isUserMuzzled = muzzlePersistenceService.isUserMuzzled( request.event.user ); - const isUserBackfired = muzzlePersistenceService.getIsBackfire( + const isUserBackfired = backfirePersistenceService.isBackfire( + request.event.user + ); + const isUserCounterMuzzled = counterPersistenceService.isCounterMuzzled( request.event.user ); const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); - if (isUserMuzzled && !containsTag) { - console.log( - `${userName} | ${request.event.user} is muzzled! Suppressing his voice...` - ); - muzzleService.sendMuzzledMessage( - request.event.channel, - request.event.user, - request.event.text, - request.event.ts - ); - } else if (isUserMuzzled && containsTag && !request.event.subtype) { - const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); - console.log( - `${slackService.getUserName( - request.event.user - )} attempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - muzzlePersistenceService.addMuzzleTime( - request.event.user, - ABUSE_PENALTY_TIME, - isUserBackfired - ); - webService.deleteMessage(request.event.channel, request.event.ts); - muzzlePersistenceService.trackDeletedMessage( - muzzleId, - request.event.text, - isUserBackfired - ); - webService.sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + if (isUserMuzzled) { + if (!containsTag) { + console.log( + `${userName} | ${ + request.event.user + } is muzzled! Suppressing his voice...` + ); + muzzleService.sendMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if (containsTag && !request.event.subtype) { + const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + muzzlePersistenceService.addMuzzleTime( + request.event.user, ABUSE_PENALTY_TIME - )} :rotating_light:` - ); - } else if (muzzleService.shouldBotMessageBeMuzzled(request)) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - webService.deleteMessage(request.event.channel, request.event.ts); + ); + webService.deleteMessage(request.event.channel, request.event.ts); + muzzlePersistenceService.trackDeletedMessage( + muzzleId, + request.event.text + ); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } else if (muzzleService.shouldBotMessageBeMuzzled(request)) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); + } + } else if (isUserBackfired) { + if (!containsTag) { + console.log( + `${userName} | ${ + request.event.user + } is backfired! Suppressing his voice...` + ); + backfireService.sendBackfiredMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if (containsTag && !request.event.subtype) { + const backfireId = backfireService.getBackfire(request.event.user)!.id; + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone. Backfire increased by ${ABUSE_PENALTY_TIME}!` + ); + backfireService.addBackfireTime(request.event.user, ABUSE_PENALTY_TIME); + webService.deleteMessage(request.event.channel, request.event.ts); + backfireService.trackDeletedMessage(backfireId, request.event.text); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } else if (backfireService.shouldBotMessageBeMuzzled(request)) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); + } + } else if (isUserCounterMuzzled) { + if (!containsTag) { + console.log( + `${userName} | ${ + request.event.user + } is counter-muzzled! Suppressing his voice...` + ); + counterService.sendCounterMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if (containsTag && !request.event.subtype) { + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + counterPersistenceService.addCounterMuzzleTime( + request.event.user, + ABUSE_PENALTY_TIME + ); + webService.deleteMessage(request.event.channel, request.event.ts); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while countered! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } else if (counterService.shouldBotMessageBeMuzzled(request)) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); + } } res.send({ challenge: request.challenge }); }); diff --git a/src/index.ts b/src/index.ts index a0e75293..4323593b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import "reflect-metadata"; import { createConnection } from "typeorm"; import { clapController } from "./controllers/clap.controller"; import { confessionController } from "./controllers/confession.controller"; +import { counterController } from "./controllers/counter.controller"; import { defineController } from "./controllers/define.controller"; import { listController } from "./controllers/list.controller"; import { mockController } from "./controllers/mock.controller"; @@ -16,6 +17,7 @@ const PORT: number = 3000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); +app.use(counterController); app.use(mockController); app.use(muzzleController); app.use(defineController); diff --git a/src/services/backfire/backfire.persistence.service.ts b/src/services/backfire/backfire.persistence.service.ts new file mode 100644 index 00000000..c0cdbb1a --- /dev/null +++ b/src/services/backfire/backfire.persistence.service.ts @@ -0,0 +1,110 @@ +import { getRepository } from "typeorm"; +import { Backfire } from "../../shared/db/models/Backfire"; +import { IBackfire } from "../../shared/models/backfire/backfire.model"; +import { ABUSE_PENALTY_TIME } from "../muzzle/constants"; +import { getRemainingTime } from "../muzzle/muzzle-utilities"; + +export class BackFirePersistenceService { + public static getInstance() { + if (!BackFirePersistenceService.instance) { + BackFirePersistenceService.instance = new BackFirePersistenceService(); + } + return BackFirePersistenceService.instance; + } + + private static instance: BackFirePersistenceService; + private backfires: Map = new Map(); + + private constructor() {} + + public addBackfire(userId: string, time: number) { + const backfire = new Backfire(); + backfire.muzzledId = userId; + backfire.messagesSuppressed = 0; + backfire.wordsSuppressed = 0; + backfire.charactersSuppressed = 0; + backfire.milliseconds = time; + + return getRepository(Backfire) + .save(backfire) + .then(backfireFromDb => { + this.backfires.set(userId, { + suppressionCount: 0, + id: backfireFromDb.id, + removalFn: setTimeout(() => this.removeBackfire(userId), time) + }); + }); + } + + public removeBackfire(userId: string) { + this.backfires.delete(userId); + console.log(`Backfire has expired and been removed for ${userId}`); + } + + public isBackfire(userId: string): boolean { + return this.backfires.has(userId); + } + + public addBackfireTime(userId: string, timeToAdd: number) { + if (userId && this.backfires.has(userId)) { + const removalFn = this.backfires.get(userId)!.removalFn; + const newTime = getRemainingTime(removalFn) + timeToAdd; + const backfireId = this.backfires.get(userId)!.id; + this.incrementBackfireTime(backfireId, ABUSE_PENALTY_TIME); + clearTimeout(this.backfires.get(userId)!.removalFn); + console.log(`Setting ${userId}'s backfire time to ${newTime}`); + this.backfires.set(userId, { + suppressionCount: this.backfires.get(userId)!.suppressionCount, + id: this.backfires.get(userId)!.id, + removalFn: setTimeout(() => this.removeBackfire(userId), newTime) + }); + } + } + + public getBackfireByUserId(userId: string): IBackfire | undefined { + return this.backfires.get(userId); + } + + public setBackfire(userId: string, options: IBackfire) { + this.backfires.set(userId, options); + } + + /** + * Determines suppression counts for messages that are ONLY deleted. + * Used when a backfired user has hit their max suppressions or when they have tagged channel. + */ + public trackDeletedMessage(backfireId: number, text: string) { + const words = text.split(" ").length; + const characters = text.split("").length; + this.incrementMessageSuppressions(backfireId); + this.incrementWordSuppressions(backfireId, words); + this.incrementCharacterSuppressions(backfireId, characters); + } + + public incrementBackfireTime(id: number, ms: number) { + return getRepository(Backfire).increment({ id }, "milliseconds", ms); + } + + public incrementMessageSuppressions(id: number) { + return getRepository(Backfire).increment({ id }, "messagesSuppressed", 1); + } + + public incrementWordSuppressions(id: number, suppressions: number) { + return getRepository(Backfire).increment( + { id }, + "wordsSuppressed", + suppressions + ); + } + + public incrementCharacterSuppressions( + id: number, + charactersSuppressed: number + ) { + return getRepository(Backfire).increment( + { id }, + "charactersSuppressed", + charactersSuppressed + ); + } +} diff --git a/src/services/backfire/backfire.service.spec.ts b/src/services/backfire/backfire.service.spec.ts new file mode 100644 index 00000000..7a6339e0 --- /dev/null +++ b/src/services/backfire/backfire.service.spec.ts @@ -0,0 +1,28 @@ +import { ISlackUser } from "../../shared/models/slack/slack-models"; +import { SlackService } from "../slack/slack.service"; +import { BackfireService } from "./backfire.service"; + +describe("BackfireService", () => { + let backfireService: BackfireService; + let slackInstance: SlackService; + + beforeEach(() => { + backfireService = new BackfireService(); + slackInstance = SlackService.getInstance(); + slackInstance.userList = [ + { id: "123", name: "test123" }, + { id: "456", name: "test456" }, + { id: "789", name: "test789" }, + { id: "666", name: "requestor" } + ] as ISlackUser[]; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runAllTimers(); + }); + + it("should create", () => { + expect(backfireService).toBeTruthy(); + }); +}); diff --git a/src/services/backfire/backfire.service.ts b/src/services/backfire/backfire.service.ts new file mode 100644 index 00000000..1619af44 --- /dev/null +++ b/src/services/backfire/backfire.service.ts @@ -0,0 +1,151 @@ +import { IBackfire } from "../../shared/models/backfire/backfire.model"; +import { IEventRequest } from "../../shared/models/slack/slack-models"; +import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "../muzzle/constants"; +import { isRandomEven } from "../muzzle/muzzle-utilities"; +import { SlackService } from "../slack/slack.service"; +import { WebService } from "../web/web.service"; +import { BackFirePersistenceService } from "./backfire.persistence.service"; + +export class BackfireService { + private webService = WebService.getInstance(); + private slackService = SlackService.getInstance(); + private backfirePersistenceService = BackFirePersistenceService.getInstance(); + + /** + * Takes in text and randomly muzzles words. + */ + public backfireMessage(text: string, backfireId: number) { + const words = text.split(" "); + + let returnText = ""; + let wordsSuppressed = 0; + let charactersSuppressed = 0; + let replacementWord; + + for (let i = 0; i < words.length; i++) { + replacementWord = this.getReplacementWord( + words[i], + i === 0, + i === words.length - 1, + REPLACEMENT_TEXT + ); + if (replacementWord.includes(REPLACEMENT_TEXT)) { + wordsSuppressed++; + charactersSuppressed += words[i].length; + } + returnText += replacementWord; + } + this.backfirePersistenceService.incrementMessageSuppressions(backfireId); + this.backfirePersistenceService.incrementCharacterSuppressions( + backfireId, + charactersSuppressed + ); + this.backfirePersistenceService.incrementWordSuppressions( + backfireId, + wordsSuppressed + ); + return returnText; + } + + public addBackfireTime(userId: string, time: number) { + this.backfirePersistenceService.addBackfireTime(userId, time); + } + + public sendBackfiredMessage( + channel: string, + userId: string, + text: string, + timestamp: string + ) { + const backfire: + | IBackfire + | undefined = this.backfirePersistenceService.getBackfireByUserId(userId); + if (backfire) { + this.webService.deleteMessage(channel, timestamp); + if (backfire!.suppressionCount < MAX_SUPPRESSIONS) { + this.backfirePersistenceService.setBackfire(userId, { + suppressionCount: ++backfire!.suppressionCount, + id: backfire!.id, + removalFn: backfire!.removalFn + }); + this.webService.sendMessage( + channel, + `<@${userId}> says "${this.backfireMessage(text, backfire!.id)}"` + ); + } else { + this.backfirePersistenceService.trackDeletedMessage(backfire!.id, text); + } + } + } + + public getBackfire(userId: string) { + return this.backfirePersistenceService.getBackfireByUserId(userId); + } + + public trackDeletedMessage(id: number, text: string) { + this.backfirePersistenceService.trackDeletedMessage(id, text); + } + + /** + * Determines whether or not a bot message should be removed. + */ + public shouldBotMessageBeMuzzled(request: IEventRequest) { + let userIdByEventText; + let userIdByAttachmentText; + let userIdByAttachmentPretext; + let userIdByCallbackId; + + if (request.event.text) { + userIdByEventText = this.slackService.getUserId(request.event.text); + } + + if (request.event.attachments && request.event.attachments.length) { + userIdByAttachmentText = this.slackService.getUserId( + request.event.attachments[0].text + ); + userIdByAttachmentPretext = this.slackService.getUserId( + request.event.attachments[0].pretext + ); + + if (request.event.attachments[0].callback_id) { + userIdByCallbackId = this.slackService.getUserIdByCallbackId( + request.event.attachments[0].callback_id + ); + } + } + + const finalUserId = this.slackService.getBotId( + userIdByEventText, + userIdByAttachmentText, + userIdByAttachmentPretext, + userIdByCallbackId + ); + + return !!( + request.event.subtype === "bot_message" && + finalUserId && + this.backfirePersistenceService.isBackfire(finalUserId) && + request.event.username !== "muzzle" + ); + } + + private getReplacementWord( + word: string, + isFirstWord: boolean, + isLastWord: boolean, + replacementText: string + ) { + const text = + isRandomEven() && + word.length < 10 && + word !== " " && + !this.slackService.containsTag(word) + ? `*${word}*` + : replacementText; + + if ((isFirstWord && !isLastWord) || (!isFirstWord && !isLastWord)) { + return `${text} `; + } + return text; + } +} diff --git a/src/services/counter/constants.ts b/src/services/counter/constants.ts new file mode 100644 index 00000000..cb31796f --- /dev/null +++ b/src/services/counter/constants.ts @@ -0,0 +1 @@ +export const COUNTER_TIME = 300000; diff --git a/src/services/counter/counter.persistence.service.ts b/src/services/counter/counter.persistence.service.ts new file mode 100644 index 00000000..b64d7911 --- /dev/null +++ b/src/services/counter/counter.persistence.service.ts @@ -0,0 +1,141 @@ +import { getRepository } from "typeorm"; +import { Counter } from "../../shared/db/models/Counter"; +import { + ICounter, + ICounterMuzzle +} from "../../shared/models/counter/counter-models"; +import { getRemainingTime } from "../muzzle/muzzle-utilities"; +import { COUNTER_TIME } from "./constants"; + +export class CounterPersistenceService { + public static getInstance() { + if (!CounterPersistenceService.instance) { + CounterPersistenceService.instance = new CounterPersistenceService(); + } + return CounterPersistenceService.instance; + } + + private static instance: CounterPersistenceService; + private counters: Map = new Map(); + private counterMuzzles: Map = new Map(); + + private constructor() {} + + public addCounter( + requestorId: string, + counteredUserId: string, + isSuccessful: boolean + ) { + return new Promise(async (resolve, reject) => { + const counter = new Counter(); + counter.requestorId = requestorId; + counter.counteredId = counteredUserId; + counter.countered = isSuccessful; + + await getRepository(Counter) + .save(counter) + .then(counterFromDb => { + this.setCounterState(requestorId, counteredUserId, counterFromDb.id); + resolve(); + }) + .catch(e => reject(`Error on saving counter to DB: ${e}`)); + }); + } + + public addCounterMuzzleTime(userId: string, timeToAdd: number) { + if (userId && this.counterMuzzles.has(userId)) { + const removalFn = this.counterMuzzles.get(userId)!.removalFn; + const newTime = getRemainingTime(removalFn) + timeToAdd; + clearTimeout(this.counterMuzzles.get(userId)!.removalFn); + console.log(`Setting ${userId}'s muzzle time to ${newTime}`); + this.counterMuzzles.set(userId, { + suppressionCount: this.counterMuzzles.get(userId)!.suppressionCount, + counterId: this.counterMuzzles.get(userId)!.counterId, + removalFn: setTimeout(() => this.removeCounterMuzzle(userId), newTime) + }); + } + } + + public setCounterMuzzle(userId: string, options: ICounterMuzzle) { + this.counterMuzzles.set(userId, options); + } + + public async setCounteredToTrue(id: number) { + const counter = await getRepository(Counter).findOne(id); + counter!.countered = true; + return getRepository(Counter).save(counter as Counter); + } + + public getCounter(counterId: number): ICounter | undefined { + return this.counters.get(counterId); + } + + public isCounterMuzzled(userId: string) { + return this.counterMuzzles.has(userId); + } + + public getCounterMuzzle(userId: string) { + return this.counterMuzzles.get(userId); + } + + public counterMuzzle(userId: string, counterId: number) { + this.counterMuzzles.set(userId, { + suppressionCount: 0, + counterId, + removalFn: setTimeout( + () => this.removeCounterMuzzle(userId), + COUNTER_TIME + ) + }); + } + + /** + * Retrieves the counterId for a counter that includes the specified requestorId and userId. + */ + public getCounterByRequestorAndUserId( + requestorId: string, + userId: string + ): number | undefined { + let counterId; + this.counters.forEach((item, key) => { + if (item.requestorId === requestorId && item.counteredId === userId) { + counterId = key; + } + }); + + return counterId; + } + + public async removeCounter(id: number, isUsed: boolean, channel?: string) { + const counter = this.counters.get(id); + + if (isUsed && channel) { + clearTimeout(counter!.removalFn); + this.counters.delete(id); + await this.setCounteredToTrue(id).catch(e => + console.error("Error during setCounteredToTrue", e) + ); + } else { + this.counters.delete(id); + } + } + + private removeCounterMuzzle(userId: string) { + this.counterMuzzles.delete(userId); + } + + private setCounterState( + requestorId: string, + userId: string, + counterId: number + ) { + this.counters.set(counterId, { + requestorId, + counteredId: userId, + removalFn: setTimeout( + () => this.removeCounter(counterId, false), + COUNTER_TIME + ) + }); + } +} diff --git a/src/services/counter/counter.service.spec.ts b/src/services/counter/counter.service.spec.ts new file mode 100644 index 00000000..82e97b4c --- /dev/null +++ b/src/services/counter/counter.service.spec.ts @@ -0,0 +1,7 @@ +import { CounterService } from "./counter.service"; + +describe(CounterService, () => { + it("should create", () => { + expect(new CounterService()).toBeTruthy(); + }); +}); diff --git a/src/services/counter/counter.service.ts b/src/services/counter/counter.service.ts new file mode 100644 index 00000000..11c3a3a3 --- /dev/null +++ b/src/services/counter/counter.service.ts @@ -0,0 +1,192 @@ +import { ICounterMuzzle } from "../../shared/models/counter/counter-models"; +import { IEventRequest } from "../../shared/models/slack/slack-models"; +import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "../muzzle/constants"; +import { isRandomEven } from "../muzzle/muzzle-utilities"; +import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; +import { SlackService } from "../slack/slack.service"; +import { WebService } from "../web/web.service"; +import { COUNTER_TIME } from "./constants"; +import { CounterPersistenceService } from "./counter.persistence.service"; + +export class CounterService { + private slackService = SlackService.getInstance(); + private webService = WebService.getInstance(); + private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + private counterPersistenceService = CounterPersistenceService.getInstance(); + + /** + * Creates a counter in DB and stores it in memory. + */ + public createCounter( + counteredId: string, + requestorId: string + ): Promise { + const counterUserName = this.slackService.getUserName(counteredId); + return new Promise(async (resolve, reject) => { + if (!counteredId || !requestorId) { + reject( + `Invalid username passed in. You can only counter existing slack users.` + ); + } else if ( + this.counterPersistenceService.getCounterByRequestorAndUserId( + requestorId, + counteredId + ) + ) { + reject("You already have a counter for this user."); + } else { + await this.counterPersistenceService + .addCounter(requestorId, counteredId, false) + .then(() => { + resolve( + `Counter set for ${counterUserName} for the next ${COUNTER_TIME}ms` + ); + }) + .catch(e => reject(e)); + } + }); + } + + public getCounterByRequestorAndUserId(requestorId: string, userId: string) { + return this.counterPersistenceService.getCounterByRequestorAndUserId( + userId, + requestorId + ); + } + + public createCounterMuzzleMessage(text: string) { + const words = text.split(" "); + + let returnText = ""; + let replacementWord; + + for (let i = 0; i < words.length; i++) { + replacementWord = this.getReplacementWord( + words[i], + i === 0, + i === words.length - 1, + REPLACEMENT_TEXT + ); + returnText += replacementWord; + } + return returnText; + } + + public getReplacementWord( + word: string, + isFirstWord: boolean, + isLastWord: boolean, + replacementText: string + ) { + const text = + isRandomEven() && + word.length < 10 && + word !== " " && + !this.slackService.containsTag(word) + ? `*${word}*` + : replacementText; + + if ((isFirstWord && !isLastWord) || (!isFirstWord && !isLastWord)) { + return `${text} `; + } + return text; + } + + public sendCounterMuzzledMessage( + channel: string, + userId: string, + text: string, + timestamp: string + ) { + const counterMuzzle: + | ICounterMuzzle + | undefined = this.counterPersistenceService.getCounterMuzzle(userId); + if (counterMuzzle) { + this.webService.deleteMessage(channel, timestamp); + if (counterMuzzle!.suppressionCount < MAX_SUPPRESSIONS) { + this.counterPersistenceService.setCounterMuzzle(userId, { + suppressionCount: ++counterMuzzle!.suppressionCount, + counterId: counterMuzzle!.counterId, + removalFn: counterMuzzle!.removalFn + }); + this.webService.sendMessage( + channel, + `<@${userId}> says "${this.createCounterMuzzleMessage(text)}"` + ); + } + } + } + + /** + * Determines whether or not a bot message should be removed. + */ + public shouldBotMessageBeMuzzled(request: IEventRequest) { + let userIdByEventText; + let userIdByAttachmentText; + let userIdByAttachmentPretext; + let userIdByCallbackId; + + if (request.event.text) { + userIdByEventText = this.slackService.getUserId(request.event.text); + } + + if (request.event.attachments && request.event.attachments.length) { + userIdByAttachmentText = this.slackService.getUserId( + request.event.attachments[0].text + ); + userIdByAttachmentPretext = this.slackService.getUserId( + request.event.attachments[0].pretext + ); + + if (request.event.attachments[0].callback_id) { + userIdByCallbackId = this.slackService.getUserIdByCallbackId( + request.event.attachments[0].callback_id + ); + } + } + + const finalUserId = this.slackService.getBotId( + userIdByEventText, + userIdByAttachmentText, + userIdByAttachmentPretext, + userIdByCallbackId + ); + + return !!( + request.event.subtype === "bot_message" && + finalUserId && + this.counterPersistenceService.isCounterMuzzled(finalUserId) && + request.event.username !== "muzzle" + ); + } + + public removeCounter(id: number, isUsed: boolean, channel?: string) { + const counter = this.counterPersistenceService.getCounter(id); + this.counterPersistenceService.removeCounter(id, isUsed, channel); + if (isUsed && channel) { + this.counterPersistenceService.counterMuzzle(counter!.counteredId, id); + this.muzzlePersistenceService.removeMuzzlePrivileges( + counter!.counteredId + ); + this.webService.sendMessage( + channel, + `:crossed_swords: $<@${ + counter!.requestorId + }> successfully countered <@${counter!.counteredId}>! <@${ + counter!.counteredId + }> has lost muzzle privileges for one hour and is muzzled for the next 5 minutes! :crossed_swords:` + ); + } else { + this.counterPersistenceService.counterMuzzle(counter!.requestorId, id); + this.muzzlePersistenceService.removeMuzzlePrivileges( + counter!.requestorId + ); + this.webService.sendMessage( + "#general", + `:flesh: <@${counter!.requestorId}> lives in fear of <@${ + counter!.counteredId + }> and is now muzzled and has lost muzzle privileges for one hour. :flesh:` + ); + } + } +} diff --git a/src/services/muzzle/constants.ts b/src/services/muzzle/constants.ts index 6882f83e..3a588ea4 100644 --- a/src/services/muzzle/constants.ts +++ b/src/services/muzzle/constants.ts @@ -3,3 +3,4 @@ export const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_SUPPRESSIONS = 7; export const MAX_MUZZLES = 2; export const ABUSE_PENALTY_TIME = 300000; +export const REPLACEMENT_TEXT = "..mMm.."; diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index e878c278..9ae8fc81 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,6 +1,5 @@ import moment from "moment"; import { getRepository } from "typeorm"; -import { Backfire } from "../../shared/db/models/Backfire"; import { Muzzle } from "../../shared/db/models/Muzzle"; import { IMuzzled, @@ -46,7 +45,7 @@ export class MuzzlePersistenceService { suppressionCount: 0, muzzledBy: requestorId, id: muzzleFromDb.id, - isBackfire: false, + isCounter: false, removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time) }); this.setRequestorCount(requestorId); @@ -56,6 +55,23 @@ export class MuzzlePersistenceService { }); } + public removeMuzzlePrivileges(requestorId: string) { + const requestorObj: IRequestor | undefined = this.requestors.get( + requestorId + ); + if (requestorObj) { + clearTimeout(this.requestors.get(requestorId)! + .muzzleCountRemover as NodeJS.Timeout); + } + + this.requestors.set(requestorId, { + muzzleCount: MAX_MUZZLES, + muzzleCountRemover: setTimeout( + () => this.removeRequestor(requestorId), + MAX_TIME_BETWEEN_MUZZLES + ) + }); + } /** * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. */ @@ -79,6 +95,7 @@ export class MuzzlePersistenceService { muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES) }); } + /** * Returns boolean whether max muzzles have been reached. */ @@ -89,44 +106,22 @@ export class MuzzlePersistenceService { ); } - public addBackfire(muzzledId: string, time: number) { - const backfire = new Backfire(); - backfire.muzzledId = muzzledId; - backfire.messagesSuppressed = 0; - backfire.wordsSuppressed = 0; - backfire.charactersSuppressed = 0; - backfire.milliseconds = time; - - return getRepository(Backfire) - .save(backfire) - .then(backfireFromDb => { - this.muzzled.set(muzzledId, { - suppressionCount: 0, - muzzledBy: muzzledId, - id: backfireFromDb.id, - isBackfire: true, - removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time) - }); - this.setRequestorCount(muzzledId); - }); - } - /** * Adds the specified amount of time to a specified muzzled user. */ - public addMuzzleTime(userId: string, timeToAdd: number, isBackfire: boolean) { + public addMuzzleTime(userId: string, timeToAdd: number) { if (userId && this.muzzled.has(userId)) { const removalFn = this.muzzled.get(userId)!.removalFn; const newTime = getRemainingTime(removalFn) + timeToAdd; const muzzleId = this.muzzled.get(userId)!.id; - this.incrementMuzzleTime(muzzleId, ABUSE_PENALTY_TIME, isBackfire); + this.incrementMuzzleTime(muzzleId, ABUSE_PENALTY_TIME); clearTimeout(this.muzzled.get(userId)!.removalFn); console.log(`Setting ${userId}'s muzzle time to ${newTime}`); this.muzzled.set(userId, { suppressionCount: this.muzzled.get(userId)!.suppressionCount, muzzledBy: this.muzzled.get(userId)!.muzzledBy, id: this.muzzled.get(userId)!.id, - isBackfire: this.muzzled.get(userId)!.isBackfire, + isCounter: false, removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) }); } @@ -153,35 +148,16 @@ export class MuzzlePersistenceService { return this.muzzled.has(userId); } - /** - * Retrieves whether or not a muzzle is backfired. - */ - public getIsBackfire(userId: string) { - return this.muzzled.has(userId) && this.muzzled.get(userId)!.isBackfire; + public incrementMuzzleTime(id: number, ms: number) { + return getRepository(Muzzle).increment({ id }, "milliseconds", ms); } - public incrementMuzzleTime(id: number, ms: number, isBackfire: boolean) { - return getRepository(isBackfire ? Backfire : Muzzle).increment( - { id }, - "milliseconds", - ms - ); + public incrementMessageSuppressions(id: number) { + return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); } - public incrementMessageSuppressions(id: number, isBackfire: boolean) { - return getRepository(isBackfire ? Backfire : Muzzle).increment( - { id }, - "messagesSuppressed", - 1 - ); - } - - public incrementWordSuppressions( - id: number, - suppressions: number, - isBackfire: boolean - ) { - return getRepository(isBackfire ? Backfire : Muzzle).increment( + public incrementWordSuppressions(id: number, suppressions: number) { + return getRepository(Muzzle).increment( { id }, "wordsSuppressed", suppressions @@ -233,10 +209,9 @@ export class MuzzlePersistenceService { public incrementCharacterSuppressions( id: number, - charactersSuppressed: number, - isBackfire: boolean + charactersSuppressed: number ) { - return getRepository(isBackfire ? Backfire : Muzzle).increment( + return getRepository(Muzzle).increment( { id }, "charactersSuppressed", charactersSuppressed @@ -246,16 +221,12 @@ export class MuzzlePersistenceService { * Determines suppression counts for messages that are ONLY deleted and not muzzled. * Used when a muzzled user has hit their max suppressions or when they have tagged channel. */ - public trackDeletedMessage( - muzzleId: number, - text: string, - isBackfire: boolean - ) { + public trackDeletedMessage(muzzleId: number, text: string) { const words = text.split(" ").length; const characters = text.split("").length; - this.incrementMessageSuppressions(muzzleId, isBackfire); - this.incrementWordSuppressions(muzzleId, words, isBackfire); - this.incrementCharacterSuppressions(muzzleId, characters, isBackfire); + this.incrementMessageSuppressions(muzzleId); + this.incrementWordSuppressions(muzzleId, words); + this.incrementCharacterSuppressions(muzzleId, characters); } /** Wrapper to generate a generic muzzle report in */ diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index 94b37c15..aa180654 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -66,24 +66,24 @@ describe("MuzzleService", () => { it("should always muzzle a tagged user", () => { const testSentence = "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; - expect(muzzleService.muzzle(testSentence, 1, false)).toBe( + expect(muzzleService.muzzle(testSentence, 1)).toBe( "..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.." ); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); + expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); }); it("should always muzzle ", () => { const testSentence = ""; - expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); + expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); }); it("should always muzzle a word with length > 10", () => { const testSentence = "this.is.a.way.to.game.the.system"; - expect(muzzleService.muzzle(testSentence, 1, false)).toBe("..mMm.."); + expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); }); }); @@ -251,7 +251,7 @@ describe("MuzzleService", () => { .addUserToMuzzled("", testData.requestor, "test") .catch(e => { expect(e).toBe( - `Invalid username passed in. You can only muzzle existing slack users` + `Invalid username passed in. You can only muzzle existing slack users.` ); }); }); @@ -349,7 +349,7 @@ describe("MuzzleService", () => { suppressionCount: 0, muzzledBy: "test", id: 1234, - isBackfire: false, + isCounter: false, removalFn: setTimeout(() => 1234, 5000) }; diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index d43081f7..dc6575ed 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -1,8 +1,11 @@ import { IMuzzled } from "../../shared/models/muzzle/muzzle-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; +import { BackFirePersistenceService } from "../backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../counter/counter.persistence.service"; +import { CounterService } from "../counter/counter.service"; import { SlackService } from "../slack/slack.service"; import { WebService } from "../web/web.service"; -import { MAX_MUZZLES, MAX_SUPPRESSIONS } from "./constants"; +import { MAX_MUZZLES, MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "./constants"; import { getTimeString, getTimeToMuzzle, @@ -14,13 +17,15 @@ import { MuzzlePersistenceService } from "./muzzle.persistence.service"; export class MuzzleService { private webService = WebService.getInstance(); private slackService = SlackService.getInstance(); + private counterService = new CounterService(); + private backfirePersistenceService = BackFirePersistenceService.getInstance(); private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); + private counterPersistenceService = CounterPersistenceService.getInstance(); /** * Takes in text and randomly muzzles certain words. */ - public muzzle(text: string, muzzleId: number, isBackfire: boolean) { - const replacementText = "..mMm.."; + public muzzle(text: string, muzzleId: number) { const words = text.split(" "); let returnText = ""; @@ -33,27 +38,22 @@ export class MuzzleService { words[i], i === 0, i === words.length - 1, - replacementText + REPLACEMENT_TEXT ); - if (replacementWord.includes(replacementText)) { + if (replacementWord.includes(REPLACEMENT_TEXT)) { wordsSuppressed++; charactersSuppressed += words[i].length; } returnText += replacementWord; } - this.muzzlePersistenceService.incrementMessageSuppressions( - muzzleId, - isBackfire - ); + this.muzzlePersistenceService.incrementMessageSuppressions(muzzleId); this.muzzlePersistenceService.incrementCharacterSuppressions( muzzleId, - charactersSuppressed, - isBackfire + charactersSuppressed ); this.muzzlePersistenceService.incrementWordSuppressions( muzzleId, - wordsSuppressed, - isBackfire + wordsSuppressed ); return returnText; } @@ -112,10 +112,15 @@ export class MuzzleService { const shouldBackFire = shouldBackfire(); const userName = this.slackService.getUserName(userId); const requestorName = this.slackService.getUserName(requestorId); + const counter = this.counterPersistenceService.getCounterByRequestorAndUserId( + userId, + requestorId + ); + return new Promise(async (resolve, reject) => { if (!userId) { reject( - `Invalid username passed in. You can only muzzle existing slack users` + `Invalid username passed in. You can only muzzle existing slack users.` ); } else if (this.muzzlePersistenceService.isUserMuzzled(userId)) { console.error( @@ -136,14 +141,21 @@ export class MuzzleService { reject( `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` ); + } else if (counter) { + console.log( + `${requestorId} attempted to muzzle ${userId} but was countered!` + ); + this.counterService.removeCounter(counter, true, channel); + reject(`You've been countered! Better luck next time...`); } else if (shouldBackFire) { console.log( `Backfiring on ${requestorName} | ${requestorId} for attempting to muzzle ${userName} | ${userId}` ); const timeToMuzzle = getTimeToMuzzle(); - await this.muzzlePersistenceService + await this.backfirePersistenceService .addBackfire(requestorId, timeToMuzzle) .then(() => { + this.muzzlePersistenceService.setRequestorCount(requestorId); this.webService.sendMessage( channel, `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:` @@ -186,26 +198,21 @@ export class MuzzleService { | IMuzzled | undefined = this.muzzlePersistenceService.getMuzzle(userId); if (muzzle) { - const isBackfire = muzzle!.isBackfire; this.webService.deleteMessage(channel, timestamp); if (muzzle!.suppressionCount < MAX_SUPPRESSIONS) { this.muzzlePersistenceService.setMuzzle(userId, { suppressionCount: ++muzzle!.suppressionCount, muzzledBy: muzzle!.muzzledBy, id: muzzle!.id, - isBackfire, + isCounter: muzzle!.isCounter, removalFn: muzzle!.removalFn }); this.webService.sendMessage( channel, - `<@${userId}> says "${this.muzzle(text, muzzle!.id, isBackfire)}"` + `<@${userId}> says "${this.muzzle(text, muzzle!.id)}"` ); } else { - this.muzzlePersistenceService.trackDeletedMessage( - muzzle!.id, - text, - isBackfire - ); + this.muzzlePersistenceService.trackDeletedMessage(muzzle!.id, text); } } } diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index ed07a050..4983284b 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -103,7 +103,7 @@ export class SlackService { }) .catch(e => { console.error("Failed to retrieve users", e); - console.error("Retrying in 5 seconds"); + console.error("Retrying in 5 seconds..."); setTimeout(() => this.getAllUsers(), 5000); })) as ISlackUser[]; } diff --git a/src/shared/db/models/Counter.ts b/src/shared/db/models/Counter.ts new file mode 100644 index 00000000..e1d68c7c --- /dev/null +++ b/src/shared/db/models/Counter.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Counter { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public requestorId!: string; + + @Column() + public counteredId!: string; + + @Column() + public countered!: boolean; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + public createdAt!: Date; +} diff --git a/src/shared/models/backfire/backfire.model.ts b/src/shared/models/backfire/backfire.model.ts new file mode 100644 index 00000000..3b17772b --- /dev/null +++ b/src/shared/models/backfire/backfire.model.ts @@ -0,0 +1,5 @@ +export interface IBackfire { + suppressionCount: number; + id: number; + removalFn: NodeJS.Timeout; +} diff --git a/src/shared/models/counter/counter-models.ts b/src/shared/models/counter/counter-models.ts new file mode 100644 index 00000000..1a2aecd4 --- /dev/null +++ b/src/shared/models/counter/counter-models.ts @@ -0,0 +1,11 @@ +export interface ICounter { + requestorId: string; + counteredId: string; + removalFn: NodeJS.Timeout; +} + +export interface ICounterMuzzle { + counterId: number; + suppressionCount: number; + removalFn: NodeJS.Timeout; +} diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index e5635878..244c2e42 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -1,8 +1,8 @@ export interface IMuzzled { suppressionCount: number; muzzledBy: string; - id: number; // Refers to either a muzzleID or backfireID from the database. Dependent on isBackfire. - isBackfire: boolean; + id: number; + isCounter: boolean; removalFn: NodeJS.Timeout; } From 0689f8128d9c109e74dd6b39bd091c7a369642a2 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 5 Jan 2020 15:28:20 -0500 Subject: [PATCH 052/167] Added logging to monitor how long it takes an event to be responded to and muzzle to be handled by our service (#63) --- src/controllers/muzzle.controller.ts | 3 ++- src/services/muzzle/muzzle.service.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index f276f7ca..5e849979 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -31,7 +31,7 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; const isNewUserAdded = request.type === "team_join"; - + console.time("respond-to-event"); if (isNewUserAdded) { slackService.getAllUsers(); } @@ -167,6 +167,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { webService.deleteMessage(request.event.channel, request.event.ts); } } + console.timeEnd("respond-to-event"); res.send({ challenge: request.challenge }); }); diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index dc6575ed..b62d7321 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -194,6 +194,7 @@ export class MuzzleService { text: string, timestamp: string ) { + console.time("send-muzzled-message"); const muzzle: | IMuzzled | undefined = this.muzzlePersistenceService.getMuzzle(userId); @@ -215,6 +216,7 @@ export class MuzzleService { this.muzzlePersistenceService.trackDeletedMessage(muzzle!.id, text); } } + console.timeEnd("send-muzzled-message"); } private getReplacementWord( From 56a6b3daa63f3ccb40e5fb0ec0f4e227e926c905 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 5 Jan 2020 15:59:03 -0500 Subject: [PATCH 053/167] Added sensible time display for counter (#64) --- src/services/counter/counter.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/counter/counter.service.ts b/src/services/counter/counter.service.ts index 11c3a3a3..c3d6f52e 100644 --- a/src/services/counter/counter.service.ts +++ b/src/services/counter/counter.service.ts @@ -1,7 +1,7 @@ import { ICounterMuzzle } from "../../shared/models/counter/counter-models"; import { IEventRequest } from "../../shared/models/slack/slack-models"; import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "../muzzle/constants"; -import { isRandomEven } from "../muzzle/muzzle-utilities"; +import { getTimeString, isRandomEven } from "../muzzle/muzzle-utilities"; import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; import { SlackService } from "../slack/slack.service"; import { WebService } from "../web/web.service"; @@ -39,7 +39,9 @@ export class CounterService { .addCounter(requestorId, counteredId, false) .then(() => { resolve( - `Counter set for ${counterUserName} for the next ${COUNTER_TIME}ms` + `Counter set for ${counterUserName} for the next ${getTimeString( + COUNTER_TIME + )}` ); }) .catch(e => reject(e)); From 58a330338ca159962f71eb33089cf4ab1a3ce61c Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 5 Jan 2020 18:36:57 -0500 Subject: [PATCH 054/167] Fix Expire Message and Other Counter Bugs (#65) * Added fix for expiring counters * Added proper call for addCounter * Added fix * Added fix for getCounterByRequestorAndUserId * Added log for @ while countered * moved clearTimeout out --- src/controllers/muzzle.controller.ts | 1 + .../counter/counter.persistence.service.ts | 28 +++++++++++++------ src/services/counter/counter.service.ts | 23 ++++----------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 5e849979..781d5167 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -147,6 +147,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { request.event.user )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!` ); + console.log(request.event); counterPersistenceService.addCounterMuzzleTime( request.event.user, ABUSE_PENALTY_TIME diff --git a/src/services/counter/counter.persistence.service.ts b/src/services/counter/counter.persistence.service.ts index b64d7911..3b5916dd 100644 --- a/src/services/counter/counter.persistence.service.ts +++ b/src/services/counter/counter.persistence.service.ts @@ -5,6 +5,8 @@ import { ICounterMuzzle } from "../../shared/models/counter/counter-models"; import { getRemainingTime } from "../muzzle/muzzle-utilities"; +import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; +import { WebService } from "../web/web.service"; import { COUNTER_TIME } from "./constants"; export class CounterPersistenceService { @@ -16,21 +18,19 @@ export class CounterPersistenceService { } private static instance: CounterPersistenceService; + private muzzlePersistenceService: MuzzlePersistenceService = MuzzlePersistenceService.getInstance(); + private webService: WebService = WebService.getInstance(); private counters: Map = new Map(); private counterMuzzles: Map = new Map(); private constructor() {} - public addCounter( - requestorId: string, - counteredUserId: string, - isSuccessful: boolean - ) { + public addCounter(requestorId: string, counteredUserId: string) { return new Promise(async (resolve, reject) => { const counter = new Counter(); counter.requestorId = requestorId; counter.counteredId = counteredUserId; - counter.countered = isSuccessful; + counter.countered = false; await getRepository(Counter) .save(counter) @@ -108,15 +108,25 @@ export class CounterPersistenceService { public async removeCounter(id: number, isUsed: boolean, channel?: string) { const counter = this.counters.get(id); - + clearTimeout(counter!.removalFn); if (isUsed && channel) { - clearTimeout(counter!.removalFn); this.counters.delete(id); await this.setCounteredToTrue(id).catch(e => console.error("Error during setCounteredToTrue", e) ); } else { + // This whole section is an anti-pattern. Fix this. this.counters.delete(id); + this.counterMuzzle(counter!.requestorId, id); + this.muzzlePersistenceService.removeMuzzlePrivileges( + counter!.requestorId + ); + this.webService.sendMessage( + "#general", + `:flesh: <@${counter!.requestorId}> lives in fear of <@${ + counter!.counteredId + }> and is now muzzled and has lost muzzle privileges for one hour. :flesh:` + ); } } @@ -133,7 +143,7 @@ export class CounterPersistenceService { requestorId, counteredId: userId, removalFn: setTimeout( - () => this.removeCounter(counterId, false), + () => this.removeCounter(counterId, false, "#general"), COUNTER_TIME ) }); diff --git a/src/services/counter/counter.service.ts b/src/services/counter/counter.service.ts index c3d6f52e..3a67a9f7 100644 --- a/src/services/counter/counter.service.ts +++ b/src/services/counter/counter.service.ts @@ -36,7 +36,7 @@ export class CounterService { reject("You already have a counter for this user."); } else { await this.counterPersistenceService - .addCounter(requestorId, counteredId, false) + .addCounter(requestorId, counteredId) .then(() => { resolve( `Counter set for ${counterUserName} for the next ${getTimeString( @@ -51,8 +51,8 @@ export class CounterService { public getCounterByRequestorAndUserId(requestorId: string, userId: string) { return this.counterPersistenceService.getCounterByRequestorAndUserId( - userId, - requestorId + requestorId, + userId ); } @@ -172,22 +172,11 @@ export class CounterService { ); this.webService.sendMessage( channel, - `:crossed_swords: $<@${ - counter!.requestorId - }> successfully countered <@${counter!.counteredId}>! <@${ + `:crossed_swords: <@${counter!.requestorId}> successfully countered <@${ counter!.counteredId - }> has lost muzzle privileges for one hour and is muzzled for the next 5 minutes! :crossed_swords:` - ); - } else { - this.counterPersistenceService.counterMuzzle(counter!.requestorId, id); - this.muzzlePersistenceService.removeMuzzlePrivileges( - counter!.requestorId - ); - this.webService.sendMessage( - "#general", - `:flesh: <@${counter!.requestorId}> lives in fear of <@${ + }>! <@${ counter!.counteredId - }> and is now muzzled and has lost muzzle privileges for one hour. :flesh:` + }> has lost muzzle privileges for one hour and is muzzled for the next 5 minutes! :crossed_swords:` ); } } From dbf54a83d4b6158dd6438aa21c51d3e6895daa86 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Mon, 6 Jan 2020 20:08:37 -0500 Subject: [PATCH 055/167] Fix an issue where a user is not muzzled for changing topic nor for using gif (#66) * Added fix for changing the channel topic * fixed gif issue --- src/controllers/muzzle.controller.ts | 38 +++++++++++++--------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 781d5167..49188dae 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -30,7 +30,7 @@ const reportService = new ReportService(); muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const request: IEventRequest = req.body; - const isNewUserAdded = request.type === "team_join"; + const isNewUserAdded = request.event.type === "team_join"; console.time("respond-to-event"); if (isNewUserAdded) { slackService.getAllUsers(); @@ -49,6 +49,7 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { const userName = slackService.getUserName(request.event.user); if (isUserMuzzled) { + console.log("isMuzzled"); if (!containsTag) { console.log( `${userName} | ${ @@ -61,12 +62,15 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { request.event.text, request.event.ts ); - } else if (containsTag && !request.event.subtype) { + } else if ( + containsTag && + (!request.event.subtype || request.event.subtype === "channel_topic") + ) { const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); console.log( `${slackService.getUserName( request.event.user - )} attempted to tag someone. Muzzle increased by ${ABUSE_PENALTY_TIME}!` + )} attempted to tag someone or change the channel topic. Muzzle increased by ${ABUSE_PENALTY_TIME}!` ); muzzlePersistenceService.addMuzzleTime( request.event.user, @@ -81,15 +85,10 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { request.event.channel, `:rotating_light: <@${ request.event.user - }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + }> attempted to @ while muzzled or change the channel topic! Muzzle increased by ${getTimeString( ABUSE_PENALTY_TIME )} :rotating_light:` ); - } else if (muzzleService.shouldBotMessageBeMuzzled(request)) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - webService.deleteMessage(request.event.channel, request.event.ts); } } else if (isUserBackfired) { if (!containsTag) { @@ -122,11 +121,6 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { ABUSE_PENALTY_TIME )} :rotating_light:` ); - } else if (backfireService.shouldBotMessageBeMuzzled(request)) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - webService.deleteMessage(request.event.channel, request.event.ts); } } else if (isUserCounterMuzzled) { if (!containsTag) { @@ -161,12 +155,16 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { ABUSE_PENALTY_TIME )} :rotating_light:` ); - } else if (counterService.shouldBotMessageBeMuzzled(request)) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - webService.deleteMessage(request.event.channel, request.event.ts); } + } else if ( + muzzleService.shouldBotMessageBeMuzzled(request) || + backfireService.shouldBotMessageBeMuzzled(request) || + counterService.shouldBotMessageBeMuzzled(request) + ) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); } console.timeEnd("respond-to-event"); res.send({ challenge: request.challenge }); @@ -174,7 +172,6 @@ muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { muzzleController.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - console.log(request); const userId: any = slackService.getUserId(request.text); const results = await muzzleService .addUserToMuzzled(userId, request.user_id, request.channel_name) @@ -189,7 +186,6 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = request.user_id; - console.log(request); if (muzzlePersistenceService.isUserMuzzled(userId)) { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { From 5a3a2536eae31994c08abd6689747d48159c8ccc Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 24 Jan 2020 14:04:19 -0500 Subject: [PATCH 056/167] Feature/add rep (#67) * Added baseline scaffolding for rep * Added better logging to reaction.service * moved positive and negative reactions out to constants.ts * Added reaction entity and began work on reaction.persistence.service * Adjusted reaction.service and muzzle.controller to handle reactions more generically * Moved event handling logic out of the muzzle.controller and into a event.controller * Added eventController middleware into index.ts * Refactored the reaction.service to solely log rep values to a db. Added a delete method to the reaction.persistence.service * Added type * Added log to handleReaction * Adjusted res * Adjusted handleRemovedReaction and removeReaction to remove rather than log an opposite reaction * Added check to only log positve or negative reactions * Added check for positive and negative when removing rep * Converted array to map for performance reasons. Also consolidated valuation logic. * Added check for user and item_user * Added a more specific log for a no log event * Added support for logging which channel the reaction was in * Made use of item.channel instead: * Added a default value to channel when channel is not available * added support for a separate rep table that only logs numeric value of the users rep * Added decrementRep call to removeReaction and fixed an issue where decrementRep instead incremented * Fixed negative value in decrement rep * Added conditional logic to removeReaction to handle both positive and negative removals --- src/controllers/event.controller.ts | 195 ++++++++++++++++++ src/controllers/muzzle.controller.ts | 157 +------------- src/index.ts | 2 + src/services/reaction/constants.ts | 111 ++++++++++ .../reaction/reaction.persistence.service.ts | 117 +++++++++++ src/services/reaction/reaction.service.ts | 54 +++++ src/services/reaction/reaction.spec.ts | 0 src/shared/db/models/Reaction.ts | 28 +++ src/shared/db/models/Rep.ts | 13 ++ src/shared/models/slack/slack-models.ts | 3 + 10 files changed, 524 insertions(+), 156 deletions(-) create mode 100644 src/controllers/event.controller.ts create mode 100644 src/services/reaction/constants.ts create mode 100644 src/services/reaction/reaction.persistence.service.ts create mode 100644 src/services/reaction/reaction.service.ts create mode 100644 src/services/reaction/reaction.spec.ts create mode 100644 src/shared/db/models/Reaction.ts create mode 100644 src/shared/db/models/Rep.ts diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts new file mode 100644 index 00000000..7b4556ea --- /dev/null +++ b/src/controllers/event.controller.ts @@ -0,0 +1,195 @@ +import express, { Request, Response, Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { BackfireService } from "../services/backfire/backfire.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; +import { CounterService } from "../services/counter/counter.service"; +import { ABUSE_PENALTY_TIME } from "../services/muzzle/constants"; +import { getTimeString } from "../services/muzzle/muzzle-utilities"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { MuzzleService } from "../services/muzzle/muzzle.service"; +import { ReactionService } from "../services/reaction/reaction.service"; +import { SlackService } from "../services/slack/slack.service"; +import { WebService } from "../services/web/web.service"; +import { IEventRequest } from "../shared/models/slack/slack-models"; + +export const eventController: Router = express.Router(); + +const muzzleService = new MuzzleService(); +const backfireService = new BackfireService(); +const counterService = new CounterService(); +const reactionService = new ReactionService(); +const webService = WebService.getInstance(); +const slackService = SlackService.getInstance(); +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); + +function handleMuzzledMessage(request: IEventRequest) { + const containsTag = slackService.containsTag(request.event.text); + const userName = slackService.getUserName(request.event.user); + + if (!containsTag) { + console.log( + `${userName} | ${request.event.user} is muzzled! Suppressing his voice...` + ); + muzzleService.sendMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if ( + containsTag && + (!request.event.subtype || request.event.subtype === "channel_topic") + ) { + const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone or change the channel topic. Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + muzzlePersistenceService.addMuzzleTime( + request.event.user, + ABUSE_PENALTY_TIME + ); + webService.deleteMessage(request.event.channel, request.event.ts); + muzzlePersistenceService.trackDeletedMessage(muzzleId, request.event.text); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled or change the channel topic! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } +} + +function handleBackfire(request: IEventRequest) { + const containsTag = slackService.containsTag(request.event.text); + const userName = slackService.getUserName(request.event.user); + if (!containsTag) { + console.log( + `${userName} | ${ + request.event.user + } is backfired! Suppressing his voice...` + ); + backfireService.sendBackfiredMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if (containsTag && !request.event.subtype) { + const backfireId = backfireService.getBackfire(request.event.user)!.id; + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone. Backfire increased by ${ABUSE_PENALTY_TIME}!` + ); + backfireService.addBackfireTime(request.event.user, ABUSE_PENALTY_TIME); + webService.deleteMessage(request.event.channel, request.event.ts); + backfireService.trackDeletedMessage(backfireId, request.event.text); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } +} + +function handleCounterMuzzle(request: IEventRequest) { + const containsTag = slackService.containsTag(request.event.text); + const userName = slackService.getUserName(request.event.user); + if (!containsTag) { + console.log( + `${userName} | ${ + request.event.user + } is counter-muzzled! Suppressing his voice...` + ); + counterService.sendCounterMuzzledMessage( + request.event.channel, + request.event.user, + request.event.text, + request.event.ts + ); + } else if (containsTag && !request.event.subtype) { + console.log( + `${slackService.getUserName( + request.event.user + )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!` + ); + console.log(request.event); + counterPersistenceService.addCounterMuzzleTime( + request.event.user, + ABUSE_PENALTY_TIME + ); + webService.deleteMessage(request.event.channel, request.event.ts); + webService.sendMessage( + request.event.channel, + `:rotating_light: <@${ + request.event.user + }> attempted to @ while countered! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME + )} :rotating_light:` + ); + } +} + +function handleBotMessage(request: IEventRequest) { + console.log( + `A user is muzzled and tried to send a bot message! Suppressing...` + ); + webService.deleteMessage(request.event.channel, request.event.ts); +} + +function handleReaction(request: IEventRequest) { + reactionService.handleReaction( + request.event, + request.event.type === "reaction_added" + ); +} + +function handleNewUserAdd() { + slackService.getAllUsers(); +} +// Change route to /event/handle instead. +eventController.post("/muzzle/handle", (req: Request, res: Response) => { + const request: IEventRequest = req.body; + const isNewUserAdded = request.event.type === "team_join"; + const isReaction = + request.event.type === "reaction_added" || + request.event.type === "reaction_removed"; + const isMuzzled = muzzlePersistenceService.isUserMuzzled(request.event.user); + const isUserBackfired = backfirePersistenceService.isBackfire( + request.event.user + ); + const isUserCounterMuzzled = counterPersistenceService.isCounterMuzzled( + request.event.user + ); + + console.time("respond-to-event"); + if (isNewUserAdded) { + handleNewUserAdd(); + } else if (isMuzzled && !isReaction) { + handleMuzzledMessage(request); + } else if (isUserBackfired && !isReaction) { + handleBackfire(request); + } else if (isUserCounterMuzzled && !isReaction) { + handleCounterMuzzle(request); + } else if ( + (muzzleService.shouldBotMessageBeMuzzled(request) || + backfireService.shouldBotMessageBeMuzzled(request) || + counterService.shouldBotMessageBeMuzzled(request)) && + !isReaction + ) { + handleBotMessage(request); + } else if (isReaction) { + handleReaction(request); + } + console.timeEnd("respond-to-event"); + res.send({ challenge: request.challenge }); +}); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 49188dae..89fc407b 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -1,175 +1,20 @@ import express, { Request, Response, Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { BackfireService } from "../services/backfire/backfire.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { CounterService } from "../services/counter/counter.service"; -import { ABUSE_PENALTY_TIME } from "../services/muzzle/constants"; -import { getTimeString } from "../services/muzzle/muzzle-utilities"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { MuzzleService } from "../services/muzzle/muzzle.service"; import { ReportService } from "../services/report/report.service"; import { SlackService } from "../services/slack/slack.service"; import { WebService } from "../services/web/web.service"; import { ReportType } from "../shared/models/muzzle/muzzle-models"; -import { - IEventRequest, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; export const muzzleController: Router = express.Router(); const muzzleService = new MuzzleService(); -const backfireService = new BackfireService(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); -const backfirePersistenceService = BackFirePersistenceService.getInstance(); -const counterPersistenceService = CounterPersistenceService.getInstance(); -const counterService = new CounterService(); const reportService = new ReportService(); -muzzleController.post("/muzzle/handle", (req: Request, res: Response) => { - const request: IEventRequest = req.body; - const isNewUserAdded = request.event.type === "team_join"; - console.time("respond-to-event"); - if (isNewUserAdded) { - slackService.getAllUsers(); - } - - const isUserMuzzled = muzzlePersistenceService.isUserMuzzled( - request.event.user - ); - const isUserBackfired = backfirePersistenceService.isBackfire( - request.event.user - ); - const isUserCounterMuzzled = counterPersistenceService.isCounterMuzzled( - request.event.user - ); - const containsTag = slackService.containsTag(request.event.text); - const userName = slackService.getUserName(request.event.user); - - if (isUserMuzzled) { - console.log("isMuzzled"); - if (!containsTag) { - console.log( - `${userName} | ${ - request.event.user - } is muzzled! Suppressing his voice...` - ); - muzzleService.sendMuzzledMessage( - request.event.channel, - request.event.user, - request.event.text, - request.event.ts - ); - } else if ( - containsTag && - (!request.event.subtype || request.event.subtype === "channel_topic") - ) { - const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); - console.log( - `${slackService.getUserName( - request.event.user - )} attempted to tag someone or change the channel topic. Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - muzzlePersistenceService.addMuzzleTime( - request.event.user, - ABUSE_PENALTY_TIME - ); - webService.deleteMessage(request.event.channel, request.event.ts); - muzzlePersistenceService.trackDeletedMessage( - muzzleId, - request.event.text - ); - webService.sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while muzzled or change the channel topic! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` - ); - } - } else if (isUserBackfired) { - if (!containsTag) { - console.log( - `${userName} | ${ - request.event.user - } is backfired! Suppressing his voice...` - ); - backfireService.sendBackfiredMessage( - request.event.channel, - request.event.user, - request.event.text, - request.event.ts - ); - } else if (containsTag && !request.event.subtype) { - const backfireId = backfireService.getBackfire(request.event.user)!.id; - console.log( - `${slackService.getUserName( - request.event.user - )} attempted to tag someone. Backfire increased by ${ABUSE_PENALTY_TIME}!` - ); - backfireService.addBackfireTime(request.event.user, ABUSE_PENALTY_TIME); - webService.deleteMessage(request.event.channel, request.event.ts); - backfireService.trackDeletedMessage(backfireId, request.event.text); - webService.sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` - ); - } - } else if (isUserCounterMuzzled) { - if (!containsTag) { - console.log( - `${userName} | ${ - request.event.user - } is counter-muzzled! Suppressing his voice...` - ); - counterService.sendCounterMuzzledMessage( - request.event.channel, - request.event.user, - request.event.text, - request.event.ts - ); - } else if (containsTag && !request.event.subtype) { - console.log( - `${slackService.getUserName( - request.event.user - )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - console.log(request.event); - counterPersistenceService.addCounterMuzzleTime( - request.event.user, - ABUSE_PENALTY_TIME - ); - webService.deleteMessage(request.event.channel, request.event.ts); - webService.sendMessage( - request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while countered! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` - ); - } - } else if ( - muzzleService.shouldBotMessageBeMuzzled(request) || - backfireService.shouldBotMessageBeMuzzled(request) || - counterService.shouldBotMessageBeMuzzled(request) - ) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); - webService.deleteMessage(request.event.channel, request.event.ts); - } - console.timeEnd("respond-to-event"); - res.send({ challenge: request.challenge }); -}); - muzzleController.post("/muzzle", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: any = slackService.getUserId(request.text); diff --git a/src/index.ts b/src/index.ts index 4323593b..a6491ecc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { clapController } from "./controllers/clap.controller"; import { confessionController } from "./controllers/confession.controller"; import { counterController } from "./controllers/counter.controller"; import { defineController } from "./controllers/define.controller"; +import { eventController } from "./controllers/event.controller"; import { listController } from "./controllers/list.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; @@ -24,6 +25,7 @@ app.use(defineController); app.use(clapController); app.use(confessionController); app.use(listController); +app.use(eventController); const slackService = SlackService.getInstance(); diff --git a/src/services/reaction/constants.ts b/src/services/reaction/constants.ts new file mode 100644 index 00000000..2f1c5d35 --- /dev/null +++ b/src/services/reaction/constants.ts @@ -0,0 +1,111 @@ +interface IReactionValue { + [key: string]: number; +} +export const reactionValues: IReactionValue = { + // Positive emojis + grinning: 1, + grin: 1, + joy: 1, + rolling_on_the_floor_laughing: 1, + smiley: 1, + smile: 1, + laughing: 1, + yum: 1, + sunglasses: 1, + heart_eyes: 1, + hugging_face: 1, + drooling_face: 1, + triumph: 1, + joy_cat: 1, + heart_eyes_cat: 1, + muscle: 1, + point_up: 1, + point_up_2: 1, + the_horns: 1, + ok_hand: 1, + "+1": 1, + clap: 1, + raised_hands: 1, + pray: 1, + heart: 1, + purple_heart: 1, + blue_heart: 1, + green_heart: 1, + yellow_heart: 1, + orange_heart: 1, + black_heart: 1, + sparkling_heart: 1, + two_hearts: 1, + sweat_drops: 1, + crown: 1, + rocket: 1, + fire: 1, + tada: 1, + confetti_ball: 1, + medal: 1, + trophy: 1, + sports_medal: 1, + first_place_medal: 1, + moneybag: 1, + key: 1, + "100": 1, + bong: 1, + chefkiss: 1, + clapping: 1, + f: 1, + feelsgood: 1, + healing_of_the_nation: 1, + godmode: 1, + "1000": 1, + heavy_check_mark: 1, + white_check_mark: 1, + chart_with_upwards_trend: 1, + // Negative Emojis + face_with_rolling_eyes: -1, + rage: -1, + angry: -1, + face_with_symbols_on_mouth: -1, + face_vomiting: -1, + clown_face: -1, + face_with_hand_over_mouth: -1, + skull: -1, + skull_and_crossbones: -1, + middle_finger: -1, + "-1": -1, + bomb: -1, + boom: -1, + snowflake: -1, + small_red_triangle: -1, + "99": -1, + "90": -1, + "bounce-eyes": -1, + butthurt: -1, + caged2: -1, + alert: -1, + bomb2: -1, + "fake-news": -1, + flag: -1, + flesh: -1, + heh: -1, + garbage: -1, + k: -1, + koolaidtantrum: -1, + koz: -1, + muzzle: -1, + nazi: -1, + noose: -1, + riddle: -1, + salt: -1, + thonk: -1, + trash: -1, + trump: -1, + zer0: -1, + x: -1, + no_entry_sign: -1, + dumpster: -1, + thx: -1, + "man-gesturing-no": -1, + no_good: -1, + chart_with_downwards_trend: -1, + zzz: -1 +}; diff --git a/src/services/reaction/reaction.persistence.service.ts b/src/services/reaction/reaction.persistence.service.ts new file mode 100644 index 00000000..41d26065 --- /dev/null +++ b/src/services/reaction/reaction.persistence.service.ts @@ -0,0 +1,117 @@ +import { getRepository } from "typeorm"; +import { Reaction } from "../../shared/db/models/Reaction"; +import { Rep } from "../../shared/db/models/Rep"; +import { IEvent } from "../../shared/models/slack/slack-models"; + +export class ReactionPersistenceService { + public static getInstance() { + if (!ReactionPersistenceService.instance) { + ReactionPersistenceService.instance = new ReactionPersistenceService(); + } + return ReactionPersistenceService.instance; + } + + private static instance: ReactionPersistenceService; + + private constructor() {} + + public saveReaction(event: IEvent, value: number) { + return new Promise(async (resolve, reject) => { + const reaction = new Reaction(); + reaction.affectedUser = event.item_user; + reaction.reactingUser = event.user; + reaction.reaction = event.reaction; + reaction.value = value; + reaction.type = event.item.type; + reaction.channel = event.item.channel; + + // Kind ugly dawg, wtf. + await getRepository(Reaction) + .save(reaction) + .then(async () => { + if (value === 1) { + await this.incrementRep(event.item_user) + .then(() => resolve()) + .catch(e => reject(e)); + } else { + await this.decrementRep(event.item_user) + .then(() => resolve()) + .catch(e => reject(e)); + } + }) + .catch(e => console.error(e)); + }); + } + + public async removeReaction(event: IEvent, value: number) { + await getRepository(Reaction) + .delete({ + reaction: event.reaction, + affectedUser: event.item_user, + reactingUser: event.user, + type: event.item.type, + channel: event.item.channel + }) + .then(() => { + value === 1 + ? this.decrementRep(event.item_user) + : this.incrementRep(event.item_user); + }) + .catch(e => e); + } + + private async isRepUserPresent(affectedUser: string) { + return getRepository(Rep) + .findOne({ user: affectedUser }) + .then(user => !!user) + .catch(e => console.error(e)); + } + + private incrementRep(affectedUser: string) { + return new Promise(async (resolve, reject) => { + // Check for affectedUser + const isUserExisting = await this.isRepUserPresent(affectedUser); + + if (isUserExisting) { + // If it exists, increment rep by one. + return getRepository(Rep) + .increment({ user: affectedUser }, "rep", 1) + .then(() => resolve()) + .catch(e => reject(e)); + } else { + // If it does not exist, create a new user with a rep of 1. + const newRepUser = new Rep(); + newRepUser.user = affectedUser; + newRepUser.rep = 1; + return getRepository(Rep) + .save(newRepUser) + .then(() => resolve()) + .catch(e => reject(e)); + } + }); + } + + private decrementRep(affectedUser: string) { + return new Promise(async (resolve, reject) => { + // Check for affectedUser + const isUserExisting = await this.isRepUserPresent(affectedUser); + + if (isUserExisting) { + // If it exists, decrement rep by one. + return getRepository(Rep) + .decrement({ user: affectedUser }, "rep", 1) + .then(() => resolve()) + .catch(e => reject(e)); + } else { + // If it does not exist, create a new user with a rep of -1. + const newRepUser = new Rep(); + newRepUser.user = affectedUser; + newRepUser.rep = -1; + return getRepository(Rep) + .save(newRepUser) + .then(() => resolve()) + .catch(e => reject(e)); + } + }); + } +} diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts new file mode 100644 index 00000000..31f8b750 --- /dev/null +++ b/src/services/reaction/reaction.service.ts @@ -0,0 +1,54 @@ +import { IEvent } from "../../shared/models/slack/slack-models"; +import { reactionValues } from "./constants"; +import { ReactionPersistenceService } from "./reaction.persistence.service"; + +export class ReactionService { + private reactionPersistenceService = ReactionPersistenceService.getInstance(); + + public handleReaction(event: IEvent, isAdded: boolean) { + console.log(event); + if (event.user && event.item_user && event.user !== event.item_user) { + if (isAdded) { + this.handleAddedReaction(event); + } else if (!isAdded) { + this.handleRemovedReaction(event); + } + } else { + console.log( + `${event.user} responded to ${ + event.item_user + } message and no action was taken. This was a self-reaction or a reaction to a bot message.` + ); + } + } + + private shouldReactionBeLogged(reactionValue: number | undefined) { + return reactionValue === 1 || reactionValue === -1; + } + + private handleAddedReaction(event: IEvent) { + const reactionValue = reactionValues[event.reaction]; + // Log event to DB. + if (this.shouldReactionBeLogged(reactionValue)) { + console.log( + `Adding reaction to ${event.item_user} for ${event.user}'s reaction: ${ + event.reaction + }, yielding him ${reactionValue}` + ); + this.reactionPersistenceService.saveReaction(event, reactionValue); + } + } + + private handleRemovedReaction(event: IEvent) { + const reactionValue = reactionValues[event.reaction]; + if (this.shouldReactionBeLogged(reactionValue)) { + // Log event to DB. + this.reactionPersistenceService.removeReaction(event, reactionValue); + console.log( + `Removing rep from ${event.item_user} for ${event.user}'s reaction: ${ + event.reaction + }` + ); + } + } +} diff --git a/src/services/reaction/reaction.spec.ts b/src/services/reaction/reaction.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/db/models/Reaction.ts b/src/shared/db/models/Reaction.ts new file mode 100644 index 00000000..694bde91 --- /dev/null +++ b/src/shared/db/models/Reaction.ts @@ -0,0 +1,28 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Reaction { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public reactingUser!: string; + + @Column() + public affectedUser!: string; + + @Column() + public reaction!: string; + + @Column() + public value!: number; + + @Column() + public type!: string; + + @Column({ default: "NOT_AVAILABLE" }) + public channel!: string; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + public createdAt!: Date; +} diff --git a/src/shared/db/models/Rep.ts b/src/shared/db/models/Rep.ts new file mode 100644 index 00000000..ec184c55 --- /dev/null +++ b/src/shared/db/models/Rep.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Rep { + @PrimaryGeneratedColumn() + public id!: number; + + @Column() + public user!: string; + + @Column() + public rep!: number; +} diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index b2dc400c..e3879fe6 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -45,6 +45,9 @@ export interface IEvent { attachments: IEvent[]; pretext: string; callback_id: string; + item_user: string; + reaction: string; + item: any; // Needs work, not optional either. } export interface IAttachment { From fe45693ef3a231f590b1546aa5e3dde765e3e90d Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 29 Jan 2020 10:42:04 -0500 Subject: [PATCH 057/167] Fixed handler for backfire and counter when someone changes channel topic (#68) --- src/controllers/event.controller.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index 7b4556ea..25527bea 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -80,7 +80,10 @@ function handleBackfire(request: IEventRequest) { request.event.text, request.event.ts ); - } else if (containsTag && !request.event.subtype) { + } else if ( + containsTag && + (!request.event.subtype || request.event.subtype === "channel_topic") + ) { const backfireId = backfireService.getBackfire(request.event.user)!.id; console.log( `${slackService.getUserName( @@ -116,7 +119,10 @@ function handleCounterMuzzle(request: IEventRequest) { request.event.text, request.event.ts ); - } else if (containsTag && !request.event.subtype) { + } else if ( + containsTag && + (!request.event.subtype || request.event.subtype === "channel_topic") + ) { console.log( `${slackService.getUserName( request.event.user From ffef26a830688bdd168d92ca03b3876467db2bd6 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 1 Feb 2020 10:29:08 -0500 Subject: [PATCH 058/167] Setup cicd (#69) * Removed dev-utils as they were underutilized and no longer being maintained. Adjusted package.json and tsconfig.json * Added dockerfile --- Dockerfile | 8 +++ dev-utils/boiler-plates/boiler.controller.ts | 57 ----------------- dev-utils/boiler-plates/boiler.service.ts | 35 ----------- dev-utils/boiler-plates/boiler.spec.ts | 19 ------ dev-utils/generate.ts | 64 -------------------- dev-utils/generateFeature.ts | 33 ---------- package.json | 12 ++-- tsconfig.json | 2 +- 8 files changed, 15 insertions(+), 215 deletions(-) create mode 100644 Dockerfile delete mode 100644 dev-utils/boiler-plates/boiler.controller.ts delete mode 100644 dev-utils/boiler-plates/boiler.service.ts delete mode 100644 dev-utils/boiler-plates/boiler.spec.ts delete mode 100644 dev-utils/generate.ts delete mode 100644 dev-utils/generateFeature.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..880b66de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:10.13.0 + +WORKDIR /usr/src/mocker +COPY package.json . +RUN npm install +COPY . . + +CMD ["npm", "run", "start:prod"] diff --git a/dev-utils/boiler-plates/boiler.controller.ts b/dev-utils/boiler-plates/boiler.controller.ts deleted file mode 100644 index fe6ff850..00000000 --- a/dev-utils/boiler-plates/boiler.controller.ts +++ /dev/null @@ -1,57 +0,0 @@ -import path from "path"; - -export const getBoilerPlateController = (serviceName: string) => { - const relMuzzleService = path - .relative(`src/controllers`, "src/services/muzzle/muzzle.service.ts") - .slice(0, -3); - const relSlackService = path - .relative(`src/controllers`, "src/services/slack/slack.service.ts") - .slice(0, -3); - const relSlackModels = path - .relative(`src/controllers`, "src/shared/models/slack/slack-models.ts") - .slice(0, -3); - const relMuzzPersist = path - .relative( - `src/controllers`, - "src/services/muzzle/muzzle.persistence.service.ts" - ) - .slice(0, -3); - - const boilerPlateController = ` - import express, { Router } from "express"; - import { MuzzlePersistenceService } from "${relMuzzPersist}"; - import { MuzzleService } from "${relMuzzleService}"; - import { SlackService } from "${relSlackService}"; - import { - IChannelResponse, - ISlashCommandRequest - } from "${relSlackModels}"; - - export const ${serviceName}Controller: Router = express.Router(); - - const muzzleService = new MuzzleService(); - const slackService = SlackService.getInstance(); - - ${serviceName}Controller.post("/${serviceName}", (req, res) => { - const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { - res.send("Sorry, can't do that while muzzled."); - } else if (!request.text) { - res.send("Sorry, you must send a message to use this service."); - } else { - const response: IChannelResponse = { - attachments: [ - { - text: 'default' - } - ], - response_type: "in_channel", - text: 'A message sent by your service that should be replaced.' - }; - slackService.sendResponse(request.response_url, response); - res.status(200).send(); - } - });`; - - return boilerPlateController; -}; diff --git a/dev-utils/boiler-plates/boiler.service.ts b/dev-utils/boiler-plates/boiler.service.ts deleted file mode 100644 index 50b4e635..00000000 --- a/dev-utils/boiler-plates/boiler.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -function capitalizeFirstLetter(text: string) { - return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; -} - -export const getBoilerPlateService = ( - serviceName: string, - isSingleton: boolean -) => { - const capitalizedService = capitalizeFirstLetter(serviceName); - - const boilerPlateServiceSingleton = ` - export class ${capitalizedService}Service { - public static getInstance() { - if(!${capitalizedService}Service.instance) { - ${capitalizedService}Service.instance = new ${capitalizedService}Service(); - } - return ${capitalizedService}Service.instance; - } - - private static instance: ${capitalizedService}; - - private constructor() {}; - } - `; - - const boilerPlateServiceNonSingleton = ` - export class ${capitalizedService}Service { - public constructor() {}; - } - `; - - return isSingleton - ? boilerPlateServiceSingleton - : boilerPlateServiceNonSingleton; -}; diff --git a/dev-utils/boiler-plates/boiler.spec.ts b/dev-utils/boiler-plates/boiler.spec.ts deleted file mode 100644 index d476e603..00000000 --- a/dev-utils/boiler-plates/boiler.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -function capitalizeFirstLetter(text: string) { - return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; -} - -export const getBoilerPlateSpec = (serviceName: string) => { - const capitalizedService = capitalizeFirstLetter(serviceName); - - const boilerPlateSpec = ` - import { ${capitalizedService}Service } from "./${serviceName}.service"; - - describe(${capitalizedService}Service, () => { - it('should create', () => { - expect(new ${capitalizedService}Service()).toBeTruthy(); - }) - }) - `; - - return boilerPlateSpec; -}; diff --git a/dev-utils/generate.ts b/dev-utils/generate.ts deleted file mode 100644 index c7b0169c..00000000 --- a/dev-utils/generate.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from "fs"; -import { getBoilerPlateController } from "./boiler-plates/boiler.controller"; -import { getBoilerPlateService } from "./boiler-plates/boiler.service"; -import { getBoilerPlateSpec } from "./boiler-plates/boiler.spec"; - -export enum ComponentType { - Spec = "spec", - Service = "service", - Controller = "controller" -} - -function getFileName(name: string, type: ComponentType) { - switch (type) { - case ComponentType.Spec: { - return `${name}.service.spec.ts`; - } - case ComponentType.Controller: { - return `${name}.controller.ts`; - } - case ComponentType.Service: { - return `${name}.service.ts`; - } - } -} - -function getTemplate(name: string, type: ComponentType) { - switch (type) { - case ComponentType.Controller: - return getBoilerPlateController(name); - case ComponentType.Service: - return getBoilerPlateService(name, false); - case ComponentType.Spec: - return getBoilerPlateSpec(name); - } -} - -export function generate( - name: string, - directory: string, - type: ComponentType, - shouldFailOnExistDir: boolean -) { - const fileName = getFileName(name, type); - - const location = `${directory}/${fileName}`; - console.log(`Creating ${directory}...`); - try { - fs.mkdirSync(directory); - } catch (e) { - if (e.code === "EEXIST") { - if (shouldFailOnExistDir) { - throw e; - } else { - console.log(`${directory} already exists! Skipping...`); - } - } else { - console.log("Error on creating folder: ", e); - } - } - console.log("Done!"); - console.log(`Creating ${fileName} in ${directory}...`); - fs.writeFileSync(location, getTemplate(name, type)); - console.log("Done!"); -} diff --git a/dev-utils/generateFeature.ts b/dev-utils/generateFeature.ts deleted file mode 100644 index e9e6d6ee..00000000 --- a/dev-utils/generateFeature.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { prompt } from "enquirer"; -import path from "path"; -import { ComponentType, generate } from "./generate"; - -async function generateFeature() { - const response: any = await prompt({ - type: "input", - name: "name", - message: "What would you like to name your feature?" - }); - - const isSingleton: any = await prompt({ - type: "input", - name: "isSingleton", - message: "Should this service be a singleton? (Y/N)" - }); - - console.log( - `You answered ${ - isSingleton.isSingleton - } to the singleton question, but JR Is too lazy to actually make this do anything just yet.` - ); - - const { name } = response; - const newServiceDir = path.resolve(`./src/services/${name}`); - const newControllerDir = path.resolve(`./src/controllers`); - - generate(name, newControllerDir, ComponentType.Controller, false); - generate(name, newServiceDir, ComponentType.Service, true); - generate(name, newServiceDir, ComponentType.Spec, false); -} - -generateFeature(); diff --git a/package.json b/package.json index b20c2971..1729adc0 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "description": "Mock your friends", "main": "src/index.ts", "scripts": { + "build": "tsc -p tsconfig.json", "format:check": "prettier --check 'src/**/*.ts'", "format:fix": "prettier --write 'src/**/*.ts'", "lint": "tslint -c tslint.json 'src/**/*.ts'", "lint:fix": "tslint --fix -c tslint.json 'src/**/*.ts'", "create:feature": "ts-node ./dev-utils/generateFeature.ts", "start": "npm run start:dev", - "start:prod": "node ./dist/server.js", + "start:prod": "npm run build && node dist/index.js", "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", "test": "jest --silent", - "test:watch": "jest --watch", - "tsc": "tsc" + "test:watch": "jest --watch" }, "author": "", "license": "ISC", @@ -30,7 +30,8 @@ "moment": "^2.24.0", "mysql": "^2.17.1", "reflect-metadata": "^0.1.13", - "typeorm": "^0.2.18" + "typeorm": "^0.2.18", + "typescript": "^3.4.5" }, "devDependencies": { "@types/easy-table": "0.0.32", @@ -47,8 +48,7 @@ "ts-node": "^8.1.0", "tslint": "^5.16.0", "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.0.1", - "typescript": "^3.4.5" + "tslint-plugin-prettier": "^2.0.1" }, "husky": { "hooks": { diff --git a/tsconfig.json b/tsconfig.json index 0a29fab2..93143d24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "sourceMap": true /* Generates corresponding '.map' file. */, // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist" /* Redirect output structure to the directory. */, - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, // "composite": true, /* Enable project compilation */ // "incremental": true, /* Enable incremental compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ From 650d23f6194c19f27818cd29dacf698ab52e5619 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 1 Feb 2020 10:43:13 -0500 Subject: [PATCH 059/167] Rep Checking (#70) * Added backfire and counter muzzle checking support to all controller * Added support for users to check their rep * Added reaction.controller to middleware --- src/controllers/clap.controller.ts | 10 ++++++- src/controllers/confession.controller.ts | 10 ++++++- src/controllers/counter.controller.ts | 5 +++- src/controllers/define.controller.ts | 10 ++++++- src/controllers/list.controller.ts | 22 ++++++++++++--- src/controllers/mock.controller.ts | 10 ++++++- src/controllers/muzzle.controller.ts | 10 ++++++- src/controllers/reaction.controller.ts | 27 +++++++++++++++++++ src/index.ts | 2 ++ .../reaction/reaction.persistence.service.ts | 16 +++++++++++ src/services/reaction/reaction.service.ts | 13 +++++++++ src/shared/db/models/Rep.ts | 3 +++ 12 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 src/controllers/reaction.controller.ts diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts index b1c76eef..5acaaef9 100644 --- a/src/controllers/clap.controller.ts +++ b/src/controllers/clap.controller.ts @@ -1,5 +1,7 @@ import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; import { ClapService } from "../services/clap/clap.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; import { @@ -10,12 +12,18 @@ import { export const clapController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const clapService = new ClapService(); clapController.post("/clap", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to clap."); diff --git a/src/controllers/confession.controller.ts b/src/controllers/confession.controller.ts index f2d628ee..04d42ebd 100644 --- a/src/controllers/confession.controller.ts +++ b/src/controllers/confession.controller.ts @@ -1,4 +1,6 @@ import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { WebService } from "../services/web/web.service"; import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; @@ -6,11 +8,17 @@ import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; export const confessionController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const webService = WebService.getInstance(); confessionController.post("/confess", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to confess."); diff --git a/src/controllers/counter.controller.ts b/src/controllers/counter.controller.ts index 23b32236..19de533b 100644 --- a/src/controllers/counter.controller.ts +++ b/src/controllers/counter.controller.ts @@ -1,5 +1,6 @@ import express, { Router } from "express"; import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { CounterService } from "../services/counter/counter.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; @@ -9,6 +10,7 @@ export const counterController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const backFirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistnceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const counterService = new CounterService(); @@ -23,7 +25,8 @@ counterController.post("/counter", async (req, res) => { ); if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || - backFirePersistenceService.isBackfire(request.user_id) + backFirePersistenceService.isBackfire(request.user_id) || + counterPersistnceService.isCounterMuzzled(request.user_id) ) { res.send("You can't counter someone if you are already muzzled!"); } else if (!request.text) { diff --git a/src/controllers/define.controller.ts b/src/controllers/define.controller.ts index 0cfb346b..a71b1d3d 100644 --- a/src/controllers/define.controller.ts +++ b/src/controllers/define.controller.ts @@ -1,4 +1,6 @@ import express, { Request, Response, Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { DefineService } from "../services/define/define.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; @@ -10,13 +12,19 @@ import { export const defineController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const defineService = DefineService.getInstance(); defineController.post("/define", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else { const defined: IUrbanDictionaryResponse = (await defineService diff --git a/src/controllers/list.controller.ts b/src/controllers/list.controller.ts index 8f979193..7d8ec483 100644 --- a/src/controllers/list.controller.ts +++ b/src/controllers/list.controller.ts @@ -1,4 +1,6 @@ import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { ListPersistenceService } from "../services/list/list.persistence.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { ReportService } from "../services/report/report.service"; @@ -12,6 +14,8 @@ import { export const listController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const listPersistenceService = ListPersistenceService.getInstance(); @@ -19,7 +23,11 @@ const reportService = new ReportService(); listController.post("/list/retrieve", async (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else { const report = await reportService.getListReport(); @@ -30,7 +38,11 @@ listController.post("/list/retrieve", async (req, res) => { listController.post("/list/add", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to list something."); @@ -49,7 +61,11 @@ listController.post("/list/add", (req, res) => { listController.post("/list/remove", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send the item you wish to remove."); diff --git a/src/controllers/mock.controller.ts b/src/controllers/mock.controller.ts index e7e954ac..49564d6f 100644 --- a/src/controllers/mock.controller.ts +++ b/src/controllers/mock.controller.ts @@ -1,4 +1,6 @@ import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { MockService } from "../services/mock/mock.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { SlackService } from "../services/slack/slack.service"; @@ -10,12 +12,18 @@ import { export const mockController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const mockService = MockService.getInstance(); mockController.post("/mock", (req, res) => { const request: ISlashCommandRequest = req.body; - if (muzzlePersistenceService.isUserMuzzled(request.user_id)) { + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to mock."); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index 89fc407b..d65a52f4 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -1,4 +1,6 @@ import express, { Request, Response, Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; import { MuzzleService } from "../services/muzzle/muzzle.service"; import { ReportService } from "../services/report/report.service"; @@ -13,6 +15,8 @@ const muzzleService = new MuzzleService(); const slackService = SlackService.getInstance(); const webService = WebService.getInstance(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); const reportService = new ReportService(); muzzleController.post("/muzzle", async (req: Request, res: Response) => { @@ -31,7 +35,11 @@ muzzleController.post("/muzzle", async (req: Request, res: Response) => { muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { const request: ISlashCommandRequest = req.body; const userId: string = request.user_id; - if (muzzlePersistenceService.isUserMuzzled(userId)) { + if ( + muzzlePersistenceService.isUserMuzzled(userId) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { res.send(`Sorry! Can't do that while muzzled.`); } else if (request.text.split(" ").length > 1) { res.send( diff --git a/src/controllers/reaction.controller.ts b/src/controllers/reaction.controller.ts new file mode 100644 index 00000000..95715588 --- /dev/null +++ b/src/controllers/reaction.controller.ts @@ -0,0 +1,27 @@ +import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { ReactionService } from "../services/reaction/reaction.service"; +import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; + +export const reactionController: Router = express.Router(); + +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); +const reactionService = new ReactionService(); + +reactionController.post("/rep/get", async (req, res) => { + const request: ISlashCommandRequest = req.body; + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { + res.send(`Sorry, can't do that while muzzled.`); + } else { + const repValue = await reactionService.getRep(request.user_id); + res.send(repValue); + } +}); diff --git a/src/index.ts b/src/index.ts index a6491ecc..2a4b782b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { eventController } from "./controllers/event.controller"; import { listController } from "./controllers/list.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; +import { reactionController } from "./controllers/reaction.controller"; import { config } from "./ormconfig"; import { SlackService } from "./services/slack/slack.service"; @@ -26,6 +27,7 @@ app.use(clapController); app.use(confessionController); app.use(listController); app.use(eventController); +app.use(reactionController); const slackService = SlackService.getInstance(); diff --git a/src/services/reaction/reaction.persistence.service.ts b/src/services/reaction/reaction.persistence.service.ts index 41d26065..f464c3e6 100644 --- a/src/services/reaction/reaction.persistence.service.ts +++ b/src/services/reaction/reaction.persistence.service.ts @@ -15,6 +15,22 @@ export class ReactionPersistenceService { private constructor() {} + public getRep(userId: string): Promise { + return new Promise(async (resolve, reject) => { + await getRepository(Rep) + .findOne({ user: userId }) + .then(async value => { + await getRepository(Rep) + .increment({ user: userId }, "timesChecked", 1) + .catch(e => + console.error(`Error logging check for user ${userId}. \n ${e}`) + ); + resolve(value); + }) + .catch(e => reject(e)); + }); + } + public saveReaction(event: IEvent, value: number) { return new Promise(async (resolve, reject) => { const reaction = new Reaction(); diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts index 31f8b750..85b25887 100644 --- a/src/services/reaction/reaction.service.ts +++ b/src/services/reaction/reaction.service.ts @@ -5,6 +5,19 @@ import { ReactionPersistenceService } from "./reaction.persistence.service"; export class ReactionService { private reactionPersistenceService = ReactionPersistenceService.getInstance(); + public async getRep(userId: string) { + return this.reactionPersistenceService + .getRep(userId) + .then(value => { + if (value) { + return `Your rep is currently ${value!.rep}.`; + } else { + return `You do not currently have any rep.`; + } + }) + .catch(() => `Unable to retrieve your rep due to an error!`); + } + public handleReaction(event: IEvent, isAdded: boolean) { console.log(event); if (event.user && event.item_user && event.user !== event.item_user) { diff --git a/src/shared/db/models/Rep.ts b/src/shared/db/models/Rep.ts index ec184c55..ed9f046a 100644 --- a/src/shared/db/models/Rep.ts +++ b/src/shared/db/models/Rep.ts @@ -10,4 +10,7 @@ export class Rep { @Column() public rep!: number; + + @Column({ default: () => 0 }) + public timesChecked!: number; } From c13fdf99e0e2da342a479f45defe797df446c625 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 1 Feb 2020 10:58:39 -0500 Subject: [PATCH 060/167] Fixed typo (#71) --- src/services/reaction/reaction.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts index 85b25887..d118de97 100644 --- a/src/services/reaction/reaction.service.ts +++ b/src/services/reaction/reaction.service.ts @@ -10,7 +10,13 @@ export class ReactionService { .getRep(userId) .then(value => { if (value) { - return `Your rep is currently ${value!.rep}.`; + const emoji = + value!.rep > 0 + ? ":chart_with_upwards_trend:" + : value!.rep < 0 + ? ":chart_with_downwards_trend:" + : ":zer0:"; + return `${emoji} You currently have *${value!.rep}* rep. ${emoji}`; } else { return `You do not currently have any rep.`; } From 9bada7be8f1e2f141d33a2fef2d416688f95706c Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 5 Feb 2020 20:50:12 -0500 Subject: [PATCH 061/167] Add Per User Rep (#72) * Added ability to get rep on per user basis * Updated ReactionByUser interface * Updated report to make use of easy-table, changed verbiage and removed emoji * Adjusted formatting * Removed reaction.service.spec because I am lazy --- jest.config.js | 2 +- .../reaction/reaction.persistence.service.ts | 13 ++++ src/services/reaction/reaction.service.ts | 74 ++++++++++++++++--- src/services/reaction/reaction.spec.ts | 0 .../models/reaction/ReactionByUser.model.ts | 4 + 5 files changed, 83 insertions(+), 10 deletions(-) delete mode 100644 src/services/reaction/reaction.spec.ts create mode 100644 src/shared/models/reaction/ReactionByUser.model.ts diff --git a/jest.config.js b/jest.config.js index 556498f1..331fe551 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", - modulePathIgnorePatterns: ["/dev-utils"] + modulePathIgnorePatterns: ["/dist"] }; diff --git a/src/services/reaction/reaction.persistence.service.ts b/src/services/reaction/reaction.persistence.service.ts index f464c3e6..e6d84ab0 100644 --- a/src/services/reaction/reaction.persistence.service.ts +++ b/src/services/reaction/reaction.persistence.service.ts @@ -1,6 +1,7 @@ import { getRepository } from "typeorm"; import { Reaction } from "../../shared/db/models/Reaction"; import { Rep } from "../../shared/db/models/Rep"; +import { IReactionByUser } from "../../shared/models/reaction/ReactionByUser.model"; import { IEvent } from "../../shared/models/slack/slack-models"; export class ReactionPersistenceService { @@ -31,6 +32,18 @@ export class ReactionPersistenceService { }); } + public getRepByUser(userId: string): Promise { + return new Promise(async (resolve, reject) => { + await getRepository(Reaction) + .query( + `SELECT reactingUser, SUM(value) as rep FROM reaction WHERE affectedUser=? GROUP BY reactingUser ORDER BY rep DESC;`, + [userId] + ) + .then(value => resolve(value)) + .catch(e => reject(e)); + }); + } + public saveReaction(event: IEvent, value: number) { return new Promise(async (resolve, reject) => { const reaction = new Reaction(); diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts index d118de97..bb5175b9 100644 --- a/src/services/reaction/reaction.service.ts +++ b/src/services/reaction/reaction.service.ts @@ -1,27 +1,34 @@ +import Table from "easy-table"; +import { IReactionByUser } from "../../shared/models/reaction/ReactionByUser.model"; import { IEvent } from "../../shared/models/slack/slack-models"; +import { SlackService } from "../slack/slack.service"; import { reactionValues } from "./constants"; import { ReactionPersistenceService } from "./reaction.persistence.service"; export class ReactionService { private reactionPersistenceService = ReactionPersistenceService.getInstance(); + private slackService = SlackService.getInstance(); public async getRep(userId: string) { - return this.reactionPersistenceService + const totalRep = await this.reactionPersistenceService .getRep(userId) .then(value => { if (value) { - const emoji = - value!.rep > 0 - ? ":chart_with_upwards_trend:" - : value!.rep < 0 - ? ":chart_with_downwards_trend:" - : ":zer0:"; - return `${emoji} You currently have *${value!.rep}* rep. ${emoji}`; + return `\n*You currently have _${value!.rep}_ rep.*`; } else { return `You do not currently have any rep.`; } }) .catch(() => `Unable to retrieve your rep due to an error!`); + + const repByUser = await this.reactionPersistenceService + .getRepByUser(userId) + .then((perUserRep: IReactionByUser[] | undefined) => + this.formatRepByUser(perUserRep) + ) + .catch(e => console.error(e)); + + return `${totalRep}\n\n${repByUser}`; } public handleReaction(event: IEvent, isAdded: boolean) { @@ -41,6 +48,56 @@ export class ReactionService { } } + private formatRepByUser(perUserRep: IReactionByUser[] | undefined) { + if (!perUserRep) { + return "You do not have any existing relationships."; + } else { + const formattedData = perUserRep.map(userRep => { + return { + user: this.slackService.getUserName(userRep.reactingUser), + rep: `${this.getSentiment(userRep.rep)} (${userRep.rep})` + }; + }); + return `${Table.print(formattedData)}`; + } + } + + private getSentiment(rep: number) { + if (rep >= 1000) { + return "Worshipped"; + } else if (rep >= 900 && rep < 1000) { + return "Enamored"; + } else if (rep >= 800 && rep < 900) { + return "Adored"; + } else if (rep >= 700 && rep < 800) { + return "Loved"; + } else if (rep >= 600 && rep < 700) { + return "Endeared"; + } else if (rep >= 500 && rep < 600) { + return "Admired"; + } else if (rep >= 400 && rep < 500) { + return "Esteemed"; + } else if (rep >= 300 && rep < 400) { + return "Well Liked"; + } else if (rep >= 200 && rep < 300) { + return "Liked"; + } else if (rep >= 100 && rep < 200) { + return "Respected"; + } else if (rep >= -300 && rep < 100) { + return "Neutral"; + } else if (rep >= -500 && rep < -300) { + return "Unfriendly"; + } else if (rep >= -700 && rep < -500) { + return "Disliked"; + } else if (rep >= -1000 && rep < -700) { + return "Scorned"; + } else if (rep >= -1000) { + return "Hated"; + } else { + return "Neutral"; + } + } + private shouldReactionBeLogged(reactionValue: number | undefined) { return reactionValue === 1 || reactionValue === -1; } @@ -61,7 +118,6 @@ export class ReactionService { private handleRemovedReaction(event: IEvent) { const reactionValue = reactionValues[event.reaction]; if (this.shouldReactionBeLogged(reactionValue)) { - // Log event to DB. this.reactionPersistenceService.removeReaction(event, reactionValue); console.log( `Removing rep from ${event.item_user} for ${event.user}'s reaction: ${ diff --git a/src/services/reaction/reaction.spec.ts b/src/services/reaction/reaction.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/shared/models/reaction/ReactionByUser.model.ts b/src/shared/models/reaction/ReactionByUser.model.ts new file mode 100644 index 00000000..4f59d218 --- /dev/null +++ b/src/shared/models/reaction/ReactionByUser.model.ts @@ -0,0 +1,4 @@ +export interface IReactionByUser { + reactingUser: string; + rep: number; +} From 7153780bd58fa5fa6f2c69ca572c09686c74c709 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Thu, 6 Feb 2020 11:48:35 -0500 Subject: [PATCH 062/167] Chart first, rep last (#73) --- src/services/reaction/reaction.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts index bb5175b9..ce6ed117 100644 --- a/src/services/reaction/reaction.service.ts +++ b/src/services/reaction/reaction.service.ts @@ -28,7 +28,7 @@ export class ReactionService { ) .catch(e => console.error(e)); - return `${totalRep}\n\n${repByUser}`; + return `${repByUser}\n\n${totalRep}`; } public handleReaction(event: IEvent, isAdded: boolean) { From c76111a5a5ba7b3c9e9cf18feb8219e76710922b Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Wed, 19 Feb 2020 18:04:32 -0500 Subject: [PATCH 063/167] Removed more than one word requirement and added an extra clap and updated tests (#74) --- src/controllers/clap.controller.ts | 2 -- src/services/clap/clap.service.spec.ts | 2 +- src/services/clap/clap.service.ts | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts index 5acaaef9..097f0c09 100644 --- a/src/controllers/clap.controller.ts +++ b/src/controllers/clap.controller.ts @@ -27,8 +27,6 @@ clapController.post("/clap", (req, res) => { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { res.send("Sorry, you must send a message to clap."); - } else if (request.text.split(" ").length === 1) { - res.send("Sorry, you need more than one words to use clapper."); } else { const clapped: string = clapService.clap(request.text); const response: IChannelResponse = { diff --git a/src/services/clap/clap.service.spec.ts b/src/services/clap/clap.service.spec.ts index 0bf802f3..11f765be 100644 --- a/src/services/clap/clap.service.spec.ts +++ b/src/services/clap/clap.service.spec.ts @@ -10,7 +10,7 @@ describe("ClapService", () => { describe("clap()", () => { it("should clap a users input with multiple words", () => { expect(clapService.clap("test this out")).toBe( - "test :clap: this :clap: out" + "test :clap: this :clap: out :clap:" ); }); diff --git a/src/services/clap/clap.service.ts b/src/services/clap/clap.service.ts index b2abf915..c1ca4cbb 100644 --- a/src/services/clap/clap.service.ts +++ b/src/services/clap/clap.service.ts @@ -6,7 +6,8 @@ export class ClapService { let output = ""; const words = text.split(" "); for (let i = 0; i < words.length; i++) { - output += i !== words.length - 1 ? `${words[i]} :clap: ` : words[i]; + output += + i !== words.length - 1 ? `${words[i]} :clap: ` : `${words[i]} :clap:`; } return output; } From e60f699513c7cbcde89c74f628fe0bc7a9985600 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 20 Mar 2020 17:00:49 -0400 Subject: [PATCH 064/167] Feature/add walkie (#75) * Added walkie talkie feature and controller * spelling error * added walkie controller * added an initial chk * Removes attachment * formatting test * More formatting * Added logging to message * Added muzzle message sent as user * Made token parameter optional * Testing * removed muzzle tests * Final cleanup Co-authored-by: Steven Freeman --- src/controllers/walkie.controller.ts | 44 +++++++++++++++++++++++++++ src/index.ts | 2 ++ src/services/walkie/walkie.service.ts | 8 +++++ src/services/web/web.service.ts | 4 +-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/controllers/walkie.controller.ts create mode 100644 src/services/walkie/walkie.service.ts diff --git a/src/controllers/walkie.controller.ts b/src/controllers/walkie.controller.ts new file mode 100644 index 00000000..7e07e87b --- /dev/null +++ b/src/controllers/walkie.controller.ts @@ -0,0 +1,44 @@ +import express, { Router } from "express"; +import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; +import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; +import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; +import { SlackService } from "../services/slack/slack.service"; +import { WalkieService } from "../services/walkie/walkie.service"; +import { + IChannelResponse, + ISlashCommandRequest +} from "../shared/models/slack/slack-models"; + +export const walkieController: Router = express.Router(); + +const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); +const backfirePersistenceService = BackFirePersistenceService.getInstance(); +const counterPersistenceService = CounterPersistenceService.getInstance(); +const slackService = SlackService.getInstance(); +const walkieService = new WalkieService(); + +walkieController.post("/walkie", (req, res) => { + const request: ISlashCommandRequest = req.body; + if ( + muzzlePersistenceService.isUserMuzzled(request.user_id) || + backfirePersistenceService.isBackfire(request.user_id) || + counterPersistenceService.isCounterMuzzled(request.user_id) + ) { + res.send(`Sorry, can't do that while muzzled.`); + } else if (!request.text) { + res.send("Sorry, you must send a message to walkie talk."); + } else { + const walkied: string = walkieService.walkieTalkie(request.text); + const response: IChannelResponse = { + attachments: [ + { + text: walkied + } + ], + response_type: "in_channel", + text: `<@${request.user_id}>` + }; + slackService.sendResponse(request.response_url, response); + res.status(200).send(); + } +}); diff --git a/src/index.ts b/src/index.ts index 2a4b782b..c87b0c1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { listController } from "./controllers/list.controller"; import { mockController } from "./controllers/mock.controller"; import { muzzleController } from "./controllers/muzzle.controller"; import { reactionController } from "./controllers/reaction.controller"; +import { walkieController } from "./controllers/walkie.controller"; import { config } from "./ormconfig"; import { SlackService } from "./services/slack/slack.service"; @@ -28,6 +29,7 @@ app.use(confessionController); app.use(listController); app.use(eventController); app.use(reactionController); +app.use(walkieController); const slackService = SlackService.getInstance(); diff --git a/src/services/walkie/walkie.service.ts b/src/services/walkie/walkie.service.ts new file mode 100644 index 00000000..9284439e --- /dev/null +++ b/src/services/walkie/walkie.service.ts @@ -0,0 +1,8 @@ +export class WalkieService { + public walkieTalkie(text: string) { + if (!text || text.length === 0) { + return text; + } + return `:walkietalkie: *chk* ${text} over. *chk* :walkietalkie:`; + } +} diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index 907e0ce3..c6f9eac3 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -44,9 +44,9 @@ export class WebService { * Handles sending messages to the chat. */ public sendMessage(channel: string, text: string) { - const muzzleToken: any = process.env.muzzleBotToken; + const token: any = process.env.muzzleBotToken; const postRequest: ChatPostMessageArguments = { - token: muzzleToken, + token, channel, text }; From 5c505f28f6d081206d648829357babec9d7ef8ae Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 20 Mar 2020 23:17:33 -0400 Subject: [PATCH 065/167] Feature/add better walkie (#76) * Removes attachment * formatting test * More formatting * Added logging to message * walkie * Removed unnecesary log * Fixed tagging issue * Fixed formatting * Removed tagging functionality Co-authored-by: Steven Freeman --- src/services/walkie/constants.ts | 29 ++++++++++++++++++++++ src/services/walkie/walkie.service.spec.ts | 29 ++++++++++++++++++++++ src/services/walkie/walkie.service.ts | 29 +++++++++++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/services/walkie/constants.ts create mode 100644 src/services/walkie/walkie.service.spec.ts diff --git a/src/services/walkie/constants.ts b/src/services/walkie/constants.ts new file mode 100644 index 00000000..642bb9f5 --- /dev/null +++ b/src/services/walkie/constants.ts @@ -0,0 +1,29 @@ +interface INatoMapping { + [name: string]: string; +} +export const NATO_MAPPINGS: INatoMapping = { + U2YJFUESC: "Zulu Mike", + U2YJQN2KB: "Sierra Foxtrot", + U2YLZPKJ4: "Charlie Mike", + U2Z9W2KA6: "Charlie Kilo", + U2ZCMGB52: "Juliet Foxtrot", + U2ZDJ2ZGV: "Whiskey Hotel", + U2ZG4L8H3: "Mike Bravo", + U2ZH29DLL: "Kilo Juliet", + U2ZH3MXB6: "Mike Romeo", + U2ZHLLG77: "Bravo Juliet", + U2ZV8E68N: "Alpha Juliet", + U3007SQAK: "Echo Oscar", + U300D7UDD: "Juliet Papa Lima", + U3021E532: "Alpha Hotel", + U3073MH8E: "Mike Kilo", + U31CJM1LP: "Yankee Lima", + U31E74VQF: "Romeo Charlie", + U37DBNXB7: "Mike Lima", + U37SW3HD1: "Charlie Sierra", + U3KABQXLY: "Juliet Charlie", + U3U5VJA2E: "Romeo Golf", + U45HMKFJR: "Charlie Mike", + U4NFD9J2G: "Juliet Sierra", + URU0SCENN: "Mike Lima" +}; diff --git a/src/services/walkie/walkie.service.spec.ts b/src/services/walkie/walkie.service.spec.ts new file mode 100644 index 00000000..a9c25401 --- /dev/null +++ b/src/services/walkie/walkie.service.spec.ts @@ -0,0 +1,29 @@ +import { WalkieService } from "./walkie.service"; + +describe("slack-utils", () => { + let walkieService: WalkieService; + + beforeEach(() => { + walkieService = new WalkieService(); + }); + + describe("walkieTalkie", () => { + it("convert a user id to NATO alphabet", () => { + const talked = walkieService.walkieTalkie( + "This this <@U2ZCMGB52 | whoever> test test" + ); + expect(talked).toBe( + `:walkietalkie: *chk* This this Juliet Foxtrot test test over. *chk* :walkietalkie:` + ); + }); + + it("should handle multiple user ids", () => { + const talked = walkieService.walkieTalkie( + "This this <@U2ZCMGB52 | whoever> test test <@U45HMKFJR | charliemike>" + ); + expect(talked).toBe( + `:walkietalkie: *chk* This this Juliet Foxtrot test test Charlie Mike over. *chk* :walkietalkie:` + ); + }); + }); +}); diff --git a/src/services/walkie/walkie.service.ts b/src/services/walkie/walkie.service.ts index 9284439e..a3662d67 100644 --- a/src/services/walkie/walkie.service.ts +++ b/src/services/walkie/walkie.service.ts @@ -1,8 +1,35 @@ +import { NATO_MAPPINGS } from "./constants"; + export class WalkieService { + private userIdRegEx = /[<]@\w+/gm; + + public getUserId(user: string) { + if (!user) { + return ""; + } + const regArray = user.match(this.userIdRegEx); + return regArray ? regArray[0].slice(2) : ""; + } + + public getNatoName(longUserId: string): string { + const userId = this.getUserId(longUserId); + return `${NATO_MAPPINGS[userId]}`; + } + public walkieTalkie(text: string) { if (!text || text.length === 0) { return text; } - return `:walkietalkie: *chk* ${text} over. *chk* :walkietalkie:`; + + const userIds = text.match(/[<]@\w+[ ]?\|[ ]?\w+[>]/gm); + let fullText = text; + + if (userIds && userIds.length) { + for (const userId of userIds) { + fullText = fullText.replace(userId, this.getNatoName(userId)); + } + } + + return `:walkietalkie: *chk* ${fullText} over. *chk* :walkietalkie:`; } } From 4ff822bba04533179013c4f857023d436f581fba Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 21 Mar 2020 14:31:23 -0400 Subject: [PATCH 066/167] Fixed callsign (#77) Co-authored-by: Steven Freeman --- src/services/walkie/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/walkie/constants.ts b/src/services/walkie/constants.ts index 642bb9f5..95ef1526 100644 --- a/src/services/walkie/constants.ts +++ b/src/services/walkie/constants.ts @@ -23,7 +23,7 @@ export const NATO_MAPPINGS: INatoMapping = { U37SW3HD1: "Charlie Sierra", U3KABQXLY: "Juliet Charlie", U3U5VJA2E: "Romeo Golf", - U45HMKFJR: "Charlie Mike", + U45HMKFJR: "Charlie Foxtrot", U4NFD9J2G: "Juliet Sierra", URU0SCENN: "Mike Lima" }; From 01a16666eadd58674d075a897cb77f658e537634 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 21 Mar 2020 14:33:46 -0400 Subject: [PATCH 067/167] Changed neals callsign fr --- src/services/walkie/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/walkie/constants.ts b/src/services/walkie/constants.ts index 95ef1526..aec1ad5d 100644 --- a/src/services/walkie/constants.ts +++ b/src/services/walkie/constants.ts @@ -4,7 +4,7 @@ interface INatoMapping { export const NATO_MAPPINGS: INatoMapping = { U2YJFUESC: "Zulu Mike", U2YJQN2KB: "Sierra Foxtrot", - U2YLZPKJ4: "Charlie Mike", + U2YLZPKJ4: "Charlie Foxtrot", U2Z9W2KA6: "Charlie Kilo", U2ZCMGB52: "Juliet Foxtrot", U2ZDJ2ZGV: "Whiskey Hotel", From c42b6f71a814d5618d0d2bce5700bec138320fbb Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 21 Mar 2020 14:34:06 -0400 Subject: [PATCH 068/167] Changed milau --- src/services/walkie/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/walkie/constants.ts b/src/services/walkie/constants.ts index aec1ad5d..90d453bb 100644 --- a/src/services/walkie/constants.ts +++ b/src/services/walkie/constants.ts @@ -23,7 +23,7 @@ export const NATO_MAPPINGS: INatoMapping = { U37SW3HD1: "Charlie Sierra", U3KABQXLY: "Juliet Charlie", U3U5VJA2E: "Romeo Golf", - U45HMKFJR: "Charlie Foxtrot", + U45HMKFJR: "Charlie Mike", U4NFD9J2G: "Juliet Sierra", URU0SCENN: "Mike Lima" }; From ca45e5a4871004a81299eca7031fe081009ce9a7 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 21 Mar 2020 14:37:13 -0400 Subject: [PATCH 069/167] Added fix for non-existent callsigns --- src/services/walkie/walkie.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/walkie/walkie.service.ts b/src/services/walkie/walkie.service.ts index a3662d67..c51142a6 100644 --- a/src/services/walkie/walkie.service.ts +++ b/src/services/walkie/walkie.service.ts @@ -13,7 +13,7 @@ export class WalkieService { public getNatoName(longUserId: string): string { const userId = this.getUserId(longUserId); - return `${NATO_MAPPINGS[userId]}`; + return userId !== "" ? `${NATO_MAPPINGS[userId]}` : longUserId; } public walkieTalkie(text: string) { From f465b59bb5067cd70f728e98fd3927463a9abdc7 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sat, 21 Mar 2020 14:42:13 -0400 Subject: [PATCH 070/167] Added support for missing nato call signs --- src/services/walkie/walkie.service.spec.ts | 9 +++++++++ src/services/walkie/walkie.service.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services/walkie/walkie.service.spec.ts b/src/services/walkie/walkie.service.spec.ts index a9c25401..a4ff5d5a 100644 --- a/src/services/walkie/walkie.service.spec.ts +++ b/src/services/walkie/walkie.service.spec.ts @@ -25,5 +25,14 @@ describe("slack-utils", () => { `:walkietalkie: *chk* This this Juliet Foxtrot test test Charlie Mike over. *chk* :walkietalkie:` ); }); + + it("should handle nonexistent call signs", () => { + const talked = walkieService.walkieTalkie( + "This this <@2222 | whoever> test test <@2222 | charliemike>" + ); + expect(talked).toBe( + `:walkietalkie: *chk* This this <@2222 | whoever> test test <@2222 | charliemike> over. *chk* :walkietalkie:` + ); + }); }); }); diff --git a/src/services/walkie/walkie.service.ts b/src/services/walkie/walkie.service.ts index c51142a6..90e432ae 100644 --- a/src/services/walkie/walkie.service.ts +++ b/src/services/walkie/walkie.service.ts @@ -13,7 +13,8 @@ export class WalkieService { public getNatoName(longUserId: string): string { const userId = this.getUserId(longUserId); - return userId !== "" ? `${NATO_MAPPINGS[userId]}` : longUserId; + console.log(userId); + return NATO_MAPPINGS[userId] || longUserId; } public walkieTalkie(text: string) { From f637e481d1c69a68e220e9c538266e32824e5eb7 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 22 Mar 2020 12:11:34 -0400 Subject: [PATCH 071/167] Added random chance of coughing (#78) Co-authored-by: Steven Freeman --- src/services/backfire/backfire.service.ts | 8 ++++++-- src/services/counter/counter.service.ts | 2 +- src/services/muzzle/constants.ts | 2 +- src/services/muzzle/muzzle.service.ts | 8 ++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/services/backfire/backfire.service.ts b/src/services/backfire/backfire.service.ts index 1619af44..aca9d433 100644 --- a/src/services/backfire/backfire.service.ts +++ b/src/services/backfire/backfire.service.ts @@ -27,9 +27,13 @@ export class BackfireService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] ); - if (replacementWord.includes(REPLACEMENT_TEXT)) { + if ( + replacementWord.includes( + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] + ) + ) { wordsSuppressed++; charactersSuppressed += words[i].length; } diff --git a/src/services/counter/counter.service.ts b/src/services/counter/counter.service.ts index 3a67a9f7..05c1413b 100644 --- a/src/services/counter/counter.service.ts +++ b/src/services/counter/counter.service.ts @@ -67,7 +67,7 @@ export class CounterService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] ); returnText += replacementWord; } diff --git a/src/services/muzzle/constants.ts b/src/services/muzzle/constants.ts index 3a588ea4..61b1d74a 100644 --- a/src/services/muzzle/constants.ts +++ b/src/services/muzzle/constants.ts @@ -3,4 +3,4 @@ export const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_SUPPRESSIONS = 7; export const MAX_MUZZLES = 2; export const ABUSE_PENALTY_TIME = 300000; -export const REPLACEMENT_TEXT = "..mMm.."; +export const REPLACEMENT_TEXT = ["..mMm..", "..*COUGH*.."]; diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index b62d7321..708d48f9 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -38,9 +38,13 @@ export class MuzzleService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] ); - if (replacementWord.includes(REPLACEMENT_TEXT)) { + if ( + replacementWord.includes( + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] + ) + ) { wordsSuppressed++; charactersSuppressed += words[i].length; } From 7e0879d268cffec22e620076fa2166ea2a31a74f Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Sun, 22 Mar 2020 12:23:35 -0400 Subject: [PATCH 072/167] removed bold --- src/services/muzzle/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/muzzle/constants.ts b/src/services/muzzle/constants.ts index 61b1d74a..9cb016e9 100644 --- a/src/services/muzzle/constants.ts +++ b/src/services/muzzle/constants.ts @@ -3,4 +3,4 @@ export const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_SUPPRESSIONS = 7; export const MAX_MUZZLES = 2; export const ABUSE_PENALTY_TIME = 300000; -export const REPLACEMENT_TEXT = ["..mMm..", "..*COUGH*.."]; +export const REPLACEMENT_TEXT = ["..mMm..", "..COUGH.."]; From d7d49915b05138c26cb5a7dcbcf8f21ada0a65c1 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2020 14:54:24 -0400 Subject: [PATCH 073/167] Removed capitalize first letter for definition --- src/services/define/define.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index be4f5754..277b0922 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -55,9 +55,7 @@ export class DefineService { for (let i = 0; i < maxDefinitions; i++) { formattedArr.push({ - text: this.formatUrbanD( - `${i + 1}. ${this.capitalizeFirstLetter(defArr[i].definition)}` - ), + text: this.formatUrbanD(`${i + 1}. ${defArr[i].definition}`), mrkdown_in: ["text"] }); } From 17cc46a5ba801a5219bba72c5ee6994297020e6c Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Fri, 27 Mar 2020 15:01:45 -0400 Subject: [PATCH 074/167] Fix/define (#79) * Added support for exact definitions instead * Adjusted logic for multi-word capitalization in define * Removed capitalize letter call Co-authored-by: sfreeman422 --- package-lock.json | 3 +- src/controllers/define.controller.ts | 2 +- src/services/define/define.service.spec.ts | 20 ++++++------- src/services/define/define.service.ts | 34 +++++++++++++++------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6792f1c2..d9928975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7098,8 +7098,7 @@ "typescript": { "version": "3.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", - "dev": true + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==" }, "uglify-js": { "version": "3.6.0", diff --git a/src/controllers/define.controller.ts b/src/controllers/define.controller.ts index a71b1d3d..7f288c1e 100644 --- a/src/controllers/define.controller.ts +++ b/src/controllers/define.controller.ts @@ -35,7 +35,7 @@ defineController.post("/define", async (req: Request, res: Response) => { const response: IChannelResponse = { response_type: "in_channel", text: `*${defineService.capitalizeFirstLetter(request.text)}*`, - attachments: defineService.formatDefs(defined.list) + attachments: defineService.formatDefs(defined.list, request.text) }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/services/define/define.service.spec.ts b/src/services/define/define.service.spec.ts index 5d980e56..88b1f4a2 100644 --- a/src/services/define/define.service.spec.ts +++ b/src/services/define/define.service.spec.ts @@ -11,7 +11,7 @@ describe("define-utils", () => { describe("capitalizeFirstLetter()", () => { it("should capitalize the first letter of a given string", () => { expect(defineService.capitalizeFirstLetter("test string")).toBe( - "Test string" + "Test String" ); }); }); @@ -24,19 +24,19 @@ describe("define-utils", () => { describe("formatDefs()", () => { it("should return an array of 3 length when no maxDefs parameter is provided", () => { - expect(defineService.formatDefs(testArray).length).toBe(3); + expect(defineService.formatDefs(testArray, "test").length).toBe(3); }); it("should return an array of 4 length when a maxDefs parameter of 4 is provided", () => { - expect(defineService.formatDefs(testArray, 4).length).toBe(4); + expect(defineService.formatDefs(testArray, "test", 4).length).toBe(4); }); it("should return testArray.length if maxDefs parameter is larger than testArray.length", () => { - expect(defineService.formatDefs(testArray, 10).length).toBe(5); + expect(defineService.formatDefs(testArray, "test", 10).length).toBe(5); }); it(`should return [{ "Sorry, no definitions found" }] if defArr === 0`, () => { - expect(defineService.formatDefs([])[0].text).toBe( + expect(defineService.formatDefs([], "test")[0].text).toBe( "Sorry, no definitions found." ); }); @@ -49,7 +49,7 @@ const testArray: IDefinition[] = [ permalink: "https://urbandictionary.com/whatever", thumbs_up: 12, author: "jr", - word: "one", + word: "test", defid: 1, written_on: "whatever", // ISO Date example: "test", @@ -62,7 +62,7 @@ const testArray: IDefinition[] = [ permalink: "https://urbandictionary.com/whatever", thumbs_up: 12, author: "jr", - word: "two", + word: "test", defid: 1, written_on: "whatever", // ISO Date example: "test", @@ -75,7 +75,7 @@ const testArray: IDefinition[] = [ permalink: "https://urbandictionary.com/whatever", thumbs_up: 12, author: "jr", - word: "three", + word: "test", defid: 1, written_on: "whatever", // ISO Date example: "test", @@ -88,7 +88,7 @@ const testArray: IDefinition[] = [ permalink: "https://urbandictionary.com/whatever", thumbs_up: 12, author: "jr", - word: "four", + word: "test", defid: 1, written_on: "whatever", // ISO Date example: "test", @@ -101,7 +101,7 @@ const testArray: IDefinition[] = [ permalink: "https://urbandictionary.com/whatever", thumbs_up: 12, author: "jr", - word: "five", + word: "test", defid: 1, written_on: "whatever", // ISO Date example: "five", diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index 277b0922..2f18344e 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -21,7 +21,15 @@ export class DefineService { * Capitalizes the first letter of a given sentence. */ public capitalizeFirstLetter(sentence: string): string { - return `${sentence.charAt(0).toUpperCase()}${sentence.slice(1)}`; + const words = sentence.split(" "); + return words + .map(word => + word + .charAt(0) + .toUpperCase() + .concat(word.slice(1)) + ) + .join(" "); } /** @@ -44,22 +52,28 @@ export class DefineService { /** * Takes in an array of definitions and breaks them down into a shortened list depending on maxDefs */ - public formatDefs(defArr: IDefinition[], maxDefs = 3) { + public formatDefs(defArr: IDefinition[], definedWord: string, maxDefs = 3) { if (!defArr || defArr.length === 0) { return [{ text: "Sorry, no definitions found." }]; } const formattedArr: IAttachment[] = []; - const maxDefinitions: number = - defArr.length <= maxDefs ? defArr.length : maxDefs; - for (let i = 0; i < maxDefinitions; i++) { - formattedArr.push({ - text: this.formatUrbanD(`${i + 1}. ${defArr[i].definition}`), - mrkdown_in: ["text"] - }); + for (let i = 0; i < defArr.length; i++) { + if (defArr[i].word.toLowerCase() === definedWord.toLowerCase()) { + formattedArr.push({ + text: this.formatUrbanD(`${i + 1}. ${defArr[i].definition}`), + mrkdown_in: ["text"] + }); + } + + if (formattedArr.length === maxDefs) { + return formattedArr; + } } - return formattedArr; + return formattedArr.length + ? formattedArr + : [{ text: "Sorry, no definitions found." }]; } /** * Takes in a definition and removes brackets. From fd568449c7995bd23bad05d7ba5e141124bddb7f Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Fri, 27 Mar 2020 15:06:46 -0400 Subject: [PATCH 075/167] Added optional capitalization --- src/services/define/define.service.spec.ts | 8 +++++- src/services/define/define.service.ts | 32 ++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/services/define/define.service.spec.ts b/src/services/define/define.service.spec.ts index 88b1f4a2..675a67a8 100644 --- a/src/services/define/define.service.spec.ts +++ b/src/services/define/define.service.spec.ts @@ -9,11 +9,17 @@ describe("define-utils", () => { }); describe("capitalizeFirstLetter()", () => { - it("should capitalize the first letter of a given string", () => { + it("should capitalize all first letters of a given string", () => { expect(defineService.capitalizeFirstLetter("test string")).toBe( "Test String" ); }); + + it("should capitalize only the first letter of the first word when all = false", () => { + expect(defineService.capitalizeFirstLetter("test string", false)).toBe( + "Test string" + ); + }); }); describe("define()", () => { diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index 2f18344e..e11b2004 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -20,16 +20,21 @@ export class DefineService { /** * Capitalizes the first letter of a given sentence. */ - public capitalizeFirstLetter(sentence: string): string { - const words = sentence.split(" "); - return words - .map(word => - word - .charAt(0) - .toUpperCase() - .concat(word.slice(1)) - ) - .join(" "); + public capitalizeFirstLetter(sentence: string, all = true): string { + if (all) { + const words = sentence.split(" "); + return words + .map(word => + word + .charAt(0) + .toUpperCase() + .concat(word.slice(1)) + ) + .join(" "); + } + return ( + sentence.charAt(0).toUpperCase() + sentence.slice(1, sentence.length) + ); } /** @@ -62,7 +67,12 @@ export class DefineService { for (let i = 0; i < defArr.length; i++) { if (defArr[i].word.toLowerCase() === definedWord.toLowerCase()) { formattedArr.push({ - text: this.formatUrbanD(`${i + 1}. ${defArr[i].definition}`), + text: this.formatUrbanD( + `${i + 1}. ${this.capitalizeFirstLetter( + defArr[i].definition, + false + )}` + ), mrkdown_in: ["text"] }); } From df85ce8ed63d6f54d1164183682096ad3c80cad8 Mon Sep 17 00:00:00 2001 From: Steven Freeman Date: Tue, 14 Apr 2020 18:09:57 -0400 Subject: [PATCH 076/167] added trim to clapper (#80) Co-authored-by: sfreeman422 --- src/services/clap/clap.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/clap/clap.service.ts b/src/services/clap/clap.service.ts index c1ca4cbb..f3e4277a 100644 --- a/src/services/clap/clap.service.ts +++ b/src/services/clap/clap.service.ts @@ -4,7 +4,7 @@ export class ClapService { return text; } let output = ""; - const words = text.split(" "); + const words = text.trim().split(" "); for (let i = 0; i < words.length; i++) { output += i !== words.length - 1 ? `${words[i]} :clap: ` : `${words[i]} :clap:`; From b1dabb44dbb24de4c26f401d9af2ac537150dfbf Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Thu, 30 Apr 2020 19:00:56 -0400 Subject: [PATCH 077/167] Fix/use eslint (#82) * Removed TSLint and related packages * Added eslint and updated code * Alot of eslint changes * Fixed report generation for muzzle with new type * Fixed report.model.ts * Changed to slackId * Changed in report.service * Removed dev-utils command and tslint.json Co-authored-by: sfreeman422 --- .eslintrc.js | 15 + .prettierrc.js | 7 + package-lock.json | 922 ++++++++++++++++-- package.json | 17 +- src/controllers/clap.controller.ts | 34 +- src/controllers/confession.controller.ts | 23 +- src/controllers/counter.controller.ts | 29 +- src/controllers/define.controller.ts | 38 +- src/controllers/event.controller.ts | 155 ++- src/controllers/list.controller.ts | 55 +- src/controllers/mock.controller.ts | 34 +- src/controllers/muzzle.controller.ts | 55 +- src/controllers/reaction.controller.ts | 16 +- src/controllers/walkie.controller.ts | 34 +- src/index.ts | 40 +- .../backfire/backfire.persistence.service.ts | 63 +- .../backfire/backfire.service.spec.ts | 20 +- src/services/backfire/backfire.service.ts | 94 +- src/services/clap/clap.service.spec.ts | 16 +- src/services/clap/clap.service.ts | 9 +- .../counter/counter.persistence.service.ts | 80 +- src/services/counter/counter.service.spec.ts | 4 +- src/services/counter/counter.service.ts | 119 +-- src/services/define/define.service.spec.ts | 180 ++-- src/services/define/define.service.ts | 59 +- src/services/list/list.persistence.service.ts | 14 +- src/services/mock/mock.service.spec.ts | 20 +- src/services/mock/mock.service.ts | 11 +- src/services/muzzle/constants.ts | 2 +- src/services/muzzle/muzzle-utilities.spec.ts | 26 +- src/services/muzzle/muzzle-utilities.ts | 19 +- .../muzzle/muzzle.persistence.service.ts | 317 +++--- src/services/muzzle/muzzle.service.spec.ts | 376 +++---- src/services/muzzle/muzzle.service.ts | 148 +-- src/services/reaction/constants.ts | 25 +- .../reaction/reaction.persistence.service.ts | 44 +- src/services/reaction/reaction.service.ts | 76 +- src/services/report/report.service.spec.ts | 114 +-- src/services/report/report.service.ts | 84 +- src/services/slack/slack.service.spec.ts | 176 ++-- src/services/slack/slack.service.ts | 67 +- src/services/walkie/constants.ts | 52 +- src/services/walkie/walkie.service.spec.ts | 30 +- src/services/walkie/walkie.service.ts | 10 +- src/services/web/web.service.spec.ts | 16 +- src/services/web/web.service.ts | 39 +- src/shared/db/models/Backfire.ts | 4 +- src/shared/db/models/Counter.ts | 4 +- src/shared/db/models/List.ts | 4 +- src/shared/db/models/Muzzle.ts | 4 +- src/shared/db/models/Reaction.ts | 6 +- src/shared/db/models/Rep.ts | 2 +- src/shared/models/backfire/backfire.model.ts | 2 +- src/shared/models/counter/counter-models.ts | 4 +- src/shared/models/define/define-models.ts | 6 +- src/shared/models/muzzle/muzzle-models.ts | 18 +- .../models/reaction/ReactionByUser.model.ts | 2 +- src/shared/models/report/report.model.ts | 42 + src/shared/models/slack/slack-models.ts | 18 +- tslint.json | 16 - 60 files changed, 2128 insertions(+), 1788 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierrc.js create mode 100644 src/shared/models/report/report.model.ts delete mode 100644 tslint.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..16c44f1a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + parserOptions: { + ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + }, + + extends: [ + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + ], + + rules: {}, +}; diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..62742f67 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: true, + trailingComma: "all", + singleQuote: true, + printWidth: 120, + tabWidth: 2 +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d9928975..6b088da1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -539,6 +539,12 @@ "@types/node": "*" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -554,6 +560,12 @@ "integrity": "sha512-zKh0f/ixYFnr3Ldf5ZJTi1ZpnRqAynTTtVyGvWDf/TT12asE8ac98t3/WGWfFdRPp/qsccxg82C/Kl3NPNhqEw==", "dev": true }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/express": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", @@ -634,6 +646,12 @@ "@types/jest": "*" } }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, "@types/lolex": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/lolex/-/lolex-3.1.1.tgz", @@ -693,6 +711,120 @@ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==" }, + "@typescript-eslint/eslint-plugin": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.30.0.tgz", + "integrity": "sha512-PGejii0qIZ9Q40RB2jIHyUpRWs1GJuHP1pkoCiaeicfwO9z7Fx03NQzupuyzAmv+q9/gFNHu7lo1ByMXe8PNyg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.30.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.30.0.tgz", + "integrity": "sha512-L3/tS9t+hAHksy8xuorhOzhdefN0ERPDWmR9CclsIGOUqGKy6tqc/P+SoXeJRye5gazkuPO0cK9MQRnolykzkA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.30.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.30.0.tgz", + "integrity": "sha512-9kDOxzp0K85UnpmPJqUzdWaCNorYYgk1yZmf4IKzpeTlSAclnFsrLjfwD9mQExctLoLoGAUXq1co+fbr+3HeFw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.30.0", + "@typescript-eslint/typescript-estree": "2.30.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.30.0.tgz", + "integrity": "sha512-nI5WOechrA0qAhnr+DzqwmqHsx7Ulr/+0H7bWCcClDhhWkSyZR5BmTvnBEyONwJCTWHfc5PAQExX24VD26IAVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, "abab": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", @@ -738,6 +870,12 @@ } } }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -1225,12 +1363,6 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, "bunyan": { "version": "1.8.12", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", @@ -1324,6 +1456,12 @@ "supports-color": "^5.3.0" } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "chokidar": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", @@ -1445,6 +1583,12 @@ } } }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -1800,6 +1944,15 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==" }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -1966,14 +2119,259 @@ } } }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, "eslint-plugin-prettier": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", - "integrity": "sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz", + "integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "fast-diff": "^1.1.1", - "jest-docblock": "^21.0.0" + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + } } }, "esprima": { @@ -1981,6 +2379,32 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", @@ -2143,6 +2567,28 @@ } } }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -2256,6 +2702,15 @@ "object-assign": "^4.1.0" } }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -2312,6 +2767,23 @@ "locate-path": "^3.0.0" } }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, "fn-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", @@ -2934,6 +3406,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "g-status": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/g-status/-/g-status-2.0.2.tgz", @@ -3309,6 +3787,12 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -3384,6 +3868,184 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4010,12 +4672,6 @@ "pretty-format": "^24.8.0" } }, - "jest-docblock": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", - "dev": true - }, "jest-each": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", @@ -4542,6 +5198,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4619,12 +5281,6 @@ "type-check": "~0.3.2" } }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, "lint-staged": { "version": "8.1.7", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-8.1.7.tgz", @@ -5112,6 +5768,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -5536,6 +6198,12 @@ } } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -5619,6 +6287,23 @@ "semver": "^5.1.0" } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, "parent-require": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", @@ -5808,6 +6493,15 @@ "integrity": "sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", @@ -5831,6 +6525,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prompts": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", @@ -6016,6 +6716,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "registry-auth-token": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", @@ -6191,6 +6897,12 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "run-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", @@ -6788,6 +7500,81 @@ "integrity": "sha512-xYavZtFC1vKgJu0AOSYdrLeikNCsNwmUeZaV1XF9cMqEhBVVxLq6rEbYzOGrF1MV2MNPkhsJqqiXuQ4a76CEUg==", "dev": true }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -6809,6 +7596,12 @@ "require-main-filename": "^2.0.0" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "thenify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", @@ -6831,12 +7624,27 @@ "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -6973,53 +7781,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, - "tslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.16.0.tgz", - "integrity": "sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^3.2.0", - "glob": "^7.1.1", - "js-yaml": "^3.13.0", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.0" - } - }, - "tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", - "dev": true - }, - "tslint-plugin-prettier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.0.1.tgz", - "integrity": "sha512-4FX9JIx/1rKHIPJNfMb+ooX1gPk5Vg3vNi7+dyFYpLO+O57F4g+b/fo1+W/G0SUOkBLHB/YKScxjX/P+7ZT/Tw==", - "dev": true, - "requires": { - "eslint-plugin-prettier": "^2.2.0", - "lines-and-columns": "^1.1.6", - "tslib": "^1.7.1" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -7274,6 +8035,12 @@ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -7392,6 +8159,12 @@ "string-width": "^2.1.1" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -7445,6 +8218,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "write-file-atomic": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", diff --git a/package.json b/package.json index 1729adc0..59099f78 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,8 @@ "build": "tsc -p tsconfig.json", "format:check": "prettier --check 'src/**/*.ts'", "format:fix": "prettier --write 'src/**/*.ts'", - "lint": "tslint -c tslint.json 'src/**/*.ts'", - "lint:fix": "tslint --fix -c tslint.json 'src/**/*.ts'", - "create:feature": "ts-node ./dev-utils/generateFeature.ts", + "lint": "eslint '*/**/*.{js,ts}'", + "lint:fix": "eslint '*/**/*.{js,ts}' --quiet --fix", "start": "npm run start:dev", "start:prod": "npm run build && node dist/index.js", "start:dev": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts", @@ -39,16 +38,18 @@ "@types/jest": "^24.0.15", "@types/lolex": "^3.1.1", "@types/node": "^12.0.2", + "@typescript-eslint/eslint-plugin": "^2.30.0", + "@typescript-eslint/parser": "^2.30.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.3", "husky": "^2.3.0", "jest": "^24.8.0", "lint-staged": "^8.1.7", "nodemon": "^1.19.0", - "prettier": "1.17.1", + "prettier": "^1.17.1", "ts-jest": "^24.0.2", - "ts-node": "^8.1.0", - "tslint": "^5.16.0", - "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.0.1" + "ts-node": "^8.1.0" }, "husky": { "hooks": { diff --git a/src/controllers/clap.controller.ts b/src/controllers/clap.controller.ts index 097f0c09..2bc3b823 100644 --- a/src/controllers/clap.controller.ts +++ b/src/controllers/clap.controller.ts @@ -1,13 +1,10 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { ClapService } from "../services/clap/clap.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { SlackService } from "../services/slack/slack.service"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { ChannelResponse, SlashCommandRequest } from '../shared/models/slack/slack-models'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { ClapService } from '../services/clap/clap.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { SlackService } from '../services/slack/slack.service'; export const clapController: Router = express.Router(); @@ -17,8 +14,8 @@ const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const clapService = new ClapService(); -clapController.post("/clap", (req, res) => { - const request: ISlashCommandRequest = req.body; +clapController.post('/clap', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -26,17 +23,18 @@ clapController.post("/clap", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send a message to clap."); + res.send('Sorry, you must send a message to clap.'); } else { const clapped: string = clapService.clap(request.text); - const response: IChannelResponse = { + const response: ChannelResponse = { attachments: [ { - text: clapped - } + text: clapped, + }, ], - response_type: "in_channel", - text: `<@${request.user_id}>` + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', + text: `<@${request.user_id}>`, }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/controllers/confession.controller.ts b/src/controllers/confession.controller.ts index 04d42ebd..5434f17c 100644 --- a/src/controllers/confession.controller.ts +++ b/src/controllers/confession.controller.ts @@ -1,9 +1,9 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { WebService } from "../services/web/web.service"; -import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { WebService } from '../services/web/web.service'; +import { SlashCommandRequest } from '../shared/models/slack/slack-models'; export const confessionController: Router = express.Router(); @@ -12,8 +12,8 @@ const backfirePersistenceService = BackFirePersistenceService.getInstance(); const counterPersistenceService = CounterPersistenceService.getInstance(); const webService = WebService.getInstance(); -confessionController.post("/confess", (req, res) => { - const request: ISlashCommandRequest = req.body; +confessionController.post('/confess', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -21,12 +21,11 @@ confessionController.post("/confess", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send a message to confess."); + res.send('Sorry, you must send a message to confess.'); } else { - const confession: string = `Someone has confessed: + const confession = `Someone has confessed: \`${request.text}\``; - // Hardcodeded, maybe not the best idea. - webService.sendMessage("#confessions", confession); + webService.sendMessage('#confessions', confession); res.status(200).send(); } }); diff --git a/src/controllers/counter.controller.ts b/src/controllers/counter.controller.ts index 19de533b..4945ea9d 100644 --- a/src/controllers/counter.controller.ts +++ b/src/controllers/counter.controller.ts @@ -1,10 +1,10 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { CounterService } from "../services/counter/counter.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { SlackService } from "../services/slack/slack.service"; -import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { CounterService } from '../services/counter/counter.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { SlackService } from '../services/slack/slack.service'; +import { SlashCommandRequest } from '../shared/models/slack/slack-models'; export const counterController: Router = express.Router(); @@ -14,15 +14,12 @@ const counterPersistnceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const counterService = new CounterService(); -counterController.post("/counter", async (req, res) => { - const request: ISlashCommandRequest = req.body; +counterController.post('/counter', async (req, res) => { + const request: SlashCommandRequest = req.body; console.log(request.text); const userId = slackService.getUserId(request.text); console.log(userId); - const counter = counterService.getCounterByRequestorAndUserId( - userId, - request.user_id - ); + const counter = counterService.getCounterByRequestorAndUserId(userId, request.user_id); if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backFirePersistenceService.isBackfire(request.user_id) || @@ -30,12 +27,10 @@ counterController.post("/counter", async (req, res) => { ) { res.send("You can't counter someone if you are already muzzled!"); } else if (!request.text) { - res.send( - "Sorry, you must specify who you would like to counter in order to use this service." - ); + res.send('Sorry, you must specify who you would like to counter in order to use this service.'); } else if (counter) { counterService.removeCounter(counter, true, request.channel_name); - res.send("Sorry, your counter has been countered."); + res.send('Sorry, your counter has been countered.'); } else { await counterService .createCounter(userId, request.user_id) diff --git a/src/controllers/define.controller.ts b/src/controllers/define.controller.ts index 7f288c1e..284d5773 100644 --- a/src/controllers/define.controller.ts +++ b/src/controllers/define.controller.ts @@ -1,14 +1,11 @@ -import express, { Request, Response, Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { DefineService } from "../services/define/define.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { SlackService } from "../services/slack/slack.service"; -import { IUrbanDictionaryResponse } from "../shared/models/define/define-models"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import express, { Request, Response, Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { DefineService } from '../services/define/define.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { SlackService } from '../services/slack/slack.service'; +import { UrbanDictionaryResponse } from '../shared/models/define/define-models'; +import { ChannelResponse, SlashCommandRequest } from '../shared/models/slack/slack-models'; export const defineController: Router = express.Router(); const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); @@ -17,8 +14,8 @@ const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const defineService = DefineService.getInstance(); -defineController.post("/define", async (req: Request, res: Response) => { - const request: ISlashCommandRequest = req.body; +defineController.post('/define', async (req: Request, res: Response) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || @@ -27,15 +24,14 @@ defineController.post("/define", async (req: Request, res: Response) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else { - const defined: IUrbanDictionaryResponse = (await defineService - .define(request.text) - .catch(e => { - res.send(`Error: ${e.message}`); - })) as IUrbanDictionaryResponse; - const response: IChannelResponse = { - response_type: "in_channel", + const defined: UrbanDictionaryResponse = (await defineService.define(request.text).catch(e => { + res.send(`Error: ${e.message}`); + })) as UrbanDictionaryResponse; + const response: ChannelResponse = { + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', text: `*${defineService.capitalizeFirstLetter(request.text)}*`, - attachments: defineService.formatDefs(defined.list, request.text) + attachments: defineService.formatDefs(defined.list, request.text), }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index 25527bea..bde61311 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -1,16 +1,16 @@ -import express, { Request, Response, Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { BackfireService } from "../services/backfire/backfire.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { CounterService } from "../services/counter/counter.service"; -import { ABUSE_PENALTY_TIME } from "../services/muzzle/constants"; -import { getTimeString } from "../services/muzzle/muzzle-utilities"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; -import { ReactionService } from "../services/reaction/reaction.service"; -import { SlackService } from "../services/slack/slack.service"; -import { WebService } from "../services/web/web.service"; -import { IEventRequest } from "../shared/models/slack/slack-models"; +import express, { Request, Response, Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { BackfireService } from '../services/backfire/backfire.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { CounterService } from '../services/counter/counter.service'; +import { ABUSE_PENALTY_TIME } from '../services/muzzle/constants'; +import { getTimeString } from '../services/muzzle/muzzle-utilities'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { MuzzleService } from '../services/muzzle/muzzle.service'; +import { ReactionService } from '../services/reaction/reaction.service'; +import { SlackService } from '../services/slack/slack.service'; +import { WebService } from '../services/web/web.service'; +import { EventRequest } from '../shared/models/slack/slack-models'; export const eventController: Router = express.Router(); @@ -24,160 +24,115 @@ const muzzlePersistenceService = MuzzlePersistenceService.getInstance(); const backfirePersistenceService = BackFirePersistenceService.getInstance(); const counterPersistenceService = CounterPersistenceService.getInstance(); -function handleMuzzledMessage(request: IEventRequest) { +function handleMuzzledMessage(request: EventRequest): void { const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); if (!containsTag) { - console.log( - `${userName} | ${request.event.user} is muzzled! Suppressing his voice...` - ); - muzzleService.sendMuzzledMessage( - request.event.channel, - request.event.user, - request.event.text, - request.event.ts - ); - } else if ( - containsTag && - (!request.event.subtype || request.event.subtype === "channel_topic") - ) { + console.log(`${userName} | ${request.event.user} is muzzled! Suppressing his voice...`); + muzzleService.sendMuzzledMessage(request.event.channel, request.event.user, request.event.text, request.event.ts); + } else if (containsTag && (!request.event.subtype || request.event.subtype === 'channel_topic')) { const muzzleId = muzzlePersistenceService.getMuzzleId(request.event.user); console.log( `${slackService.getUserName( - request.event.user - )} attempted to tag someone or change the channel topic. Muzzle increased by ${ABUSE_PENALTY_TIME}!` - ); - muzzlePersistenceService.addMuzzleTime( - request.event.user, - ABUSE_PENALTY_TIME + request.event.user, + )} attempted to tag someone or change the channel topic. Muzzle increased by ${ABUSE_PENALTY_TIME}!`, ); + muzzlePersistenceService.addMuzzleTime(request.event.user, ABUSE_PENALTY_TIME); webService.deleteMessage(request.event.channel, request.event.ts); - muzzlePersistenceService.trackDeletedMessage(muzzleId, request.event.text); + muzzlePersistenceService.trackDeletedMessage(muzzleId as number, request.event.text); webService.sendMessage( request.event.channel, `:rotating_light: <@${ request.event.user }> attempted to @ while muzzled or change the channel topic! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` + ABUSE_PENALTY_TIME, + )} :rotating_light:`, ); } } -function handleBackfire(request: IEventRequest) { +function handleBackfire(request: EventRequest): void { const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); if (!containsTag) { - console.log( - `${userName} | ${ - request.event.user - } is backfired! Suppressing his voice...` - ); + console.log(`${userName} | ${request.event.user} is backfired! Suppressing his voice...`); backfireService.sendBackfiredMessage( request.event.channel, request.event.user, request.event.text, - request.event.ts + request.event.ts, ); - } else if ( - containsTag && - (!request.event.subtype || request.event.subtype === "channel_topic") - ) { + } else if (containsTag && (!request.event.subtype || request.event.subtype === 'channel_topic')) { const backfireId = backfireService.getBackfire(request.event.user)!.id; console.log( `${slackService.getUserName( - request.event.user - )} attempted to tag someone. Backfire increased by ${ABUSE_PENALTY_TIME}!` + request.event.user, + )} attempted to tag someone. Backfire increased by ${ABUSE_PENALTY_TIME}!`, ); backfireService.addBackfireTime(request.event.user, ABUSE_PENALTY_TIME); webService.deleteMessage(request.event.channel, request.event.ts); backfireService.trackDeletedMessage(backfireId, request.event.text); webService.sendMessage( request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while muzzled! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` + `:rotating_light: <@${request.event.user}> attempted to @ while muzzled! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME, + )} :rotating_light:`, ); } } -function handleCounterMuzzle(request: IEventRequest) { +function handleCounterMuzzle(request: EventRequest): void { const containsTag = slackService.containsTag(request.event.text); const userName = slackService.getUserName(request.event.user); if (!containsTag) { - console.log( - `${userName} | ${ - request.event.user - } is counter-muzzled! Suppressing his voice...` - ); + console.log(`${userName} | ${request.event.user} is counter-muzzled! Suppressing his voice...`); counterService.sendCounterMuzzledMessage( request.event.channel, request.event.user, request.event.text, - request.event.ts + request.event.ts, ); - } else if ( - containsTag && - (!request.event.subtype || request.event.subtype === "channel_topic") - ) { + } else if (containsTag && (!request.event.subtype || request.event.subtype === 'channel_topic')) { console.log( `${slackService.getUserName( - request.event.user - )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!` + request.event.user, + )} attempted to tag someone. Counter Muzzle increased by ${ABUSE_PENALTY_TIME}!`, ); console.log(request.event); - counterPersistenceService.addCounterMuzzleTime( - request.event.user, - ABUSE_PENALTY_TIME - ); + counterPersistenceService.addCounterMuzzleTime(request.event.user, ABUSE_PENALTY_TIME); webService.deleteMessage(request.event.channel, request.event.ts); webService.sendMessage( request.event.channel, - `:rotating_light: <@${ - request.event.user - }> attempted to @ while countered! Muzzle increased by ${getTimeString( - ABUSE_PENALTY_TIME - )} :rotating_light:` + `:rotating_light: <@${request.event.user}> attempted to @ while countered! Muzzle increased by ${getTimeString( + ABUSE_PENALTY_TIME, + )} :rotating_light:`, ); } } -function handleBotMessage(request: IEventRequest) { - console.log( - `A user is muzzled and tried to send a bot message! Suppressing...` - ); +function handleBotMessage(request: EventRequest): void { + console.log(`A user is muzzled and tried to send a bot message! Suppressing...`); webService.deleteMessage(request.event.channel, request.event.ts); } -function handleReaction(request: IEventRequest) { - reactionService.handleReaction( - request.event, - request.event.type === "reaction_added" - ); +function handleReaction(request: EventRequest): void { + reactionService.handleReaction(request.event, request.event.type === 'reaction_added'); } -function handleNewUserAdd() { +function handleNewUserAdd(): void { slackService.getAllUsers(); } // Change route to /event/handle instead. -eventController.post("/muzzle/handle", (req: Request, res: Response) => { - const request: IEventRequest = req.body; - const isNewUserAdded = request.event.type === "team_join"; - const isReaction = - request.event.type === "reaction_added" || - request.event.type === "reaction_removed"; +eventController.post('/muzzle/handle', (req: Request, res: Response) => { + const request: EventRequest = req.body; + const isNewUserAdded = request.event.type === 'team_join'; + const isReaction = request.event.type === 'reaction_added' || request.event.type === 'reaction_removed'; const isMuzzled = muzzlePersistenceService.isUserMuzzled(request.event.user); - const isUserBackfired = backfirePersistenceService.isBackfire( - request.event.user - ); - const isUserCounterMuzzled = counterPersistenceService.isCounterMuzzled( - request.event.user - ); + const isUserBackfired = backfirePersistenceService.isBackfire(request.event.user); + const isUserCounterMuzzled = counterPersistenceService.isCounterMuzzled(request.event.user); - console.time("respond-to-event"); + console.time('respond-to-event'); if (isNewUserAdded) { handleNewUserAdd(); } else if (isMuzzled && !isReaction) { @@ -196,6 +151,6 @@ eventController.post("/muzzle/handle", (req: Request, res: Response) => { } else if (isReaction) { handleReaction(request); } - console.timeEnd("respond-to-event"); + console.timeEnd('respond-to-event'); res.send({ challenge: request.challenge }); }); diff --git a/src/controllers/list.controller.ts b/src/controllers/list.controller.ts index 7d8ec483..5f21626e 100644 --- a/src/controllers/list.controller.ts +++ b/src/controllers/list.controller.ts @@ -1,15 +1,12 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { ListPersistenceService } from "../services/list/list.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { ReportService } from "../services/report/report.service"; -import { SlackService } from "../services/slack/slack.service"; -import { WebService } from "../services/web/web.service"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { ListPersistenceService } from '../services/list/list.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { ReportService } from '../services/report/report.service'; +import { SlackService } from '../services/slack/slack.service'; +import { WebService } from '../services/web/web.service'; +import { ChannelResponse, SlashCommandRequest } from '../shared/models/slack/slack-models'; export const listController: Router = express.Router(); @@ -21,8 +18,8 @@ const webService = WebService.getInstance(); const listPersistenceService = ListPersistenceService.getInstance(); const reportService = new ReportService(); -listController.post("/list/retrieve", async (req, res) => { - const request: ISlashCommandRequest = req.body; +listController.post('/list/retrieve', async (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -31,13 +28,13 @@ listController.post("/list/retrieve", async (req, res) => { res.send(`Sorry, can't do that while muzzled.`); } else { const report = await reportService.getListReport(); - webService.uploadFile(req.body.channel_id, report, "The List"); + webService.uploadFile(req.body.channel_id, report, 'The List'); res.status(200).send(); } }); -listController.post("/list/add", (req, res) => { - const request: ISlashCommandRequest = req.body; +listController.post('/list/add', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -45,22 +42,23 @@ listController.post("/list/add", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send a message to list something."); + res.send('Sorry, you must send a message to list something.'); } else if (request.text.length >= 255) { - res.send("Sorry, items added to The List must be less than 255 characters"); + res.send('Sorry, items added to The List must be less than 255 characters'); } else { listPersistenceService.store(request.user_id, request.text); - const response: IChannelResponse = { - response_type: "in_channel", - text: `\`${request.text}\` has been \`listed\`` + const response: ChannelResponse = { + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', + text: `\`${request.text}\` has been \`listed\``, }; slackService.sendResponse(request.response_url, response); res.status(200).send(); } }); -listController.post("/list/remove", (req, res) => { - const request: ISlashCommandRequest = req.body; +listController.post('/list/remove', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -68,14 +66,15 @@ listController.post("/list/remove", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send the item you wish to remove."); + res.send('Sorry, you must send the item you wish to remove.'); } else { listPersistenceService .remove(request.text) .then(() => { - const response: IChannelResponse = { - response_type: "in_channel", - text: `\`${request.text}\` has been removed from \`The List\`` + const response: ChannelResponse = { + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', + text: `\`${request.text}\` has been removed from \`The List\``, }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/controllers/mock.controller.ts b/src/controllers/mock.controller.ts index 49564d6f..2cb2a221 100644 --- a/src/controllers/mock.controller.ts +++ b/src/controllers/mock.controller.ts @@ -1,13 +1,10 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MockService } from "../services/mock/mock.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { SlackService } from "../services/slack/slack.service"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MockService } from '../services/mock/mock.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { SlackService } from '../services/slack/slack.service'; +import { ChannelResponse, SlashCommandRequest } from '../shared/models/slack/slack-models'; export const mockController: Router = express.Router(); @@ -17,8 +14,8 @@ const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const mockService = MockService.getInstance(); -mockController.post("/mock", (req, res) => { - const request: ISlashCommandRequest = req.body; +mockController.post('/mock', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -26,17 +23,18 @@ mockController.post("/mock", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send a message to mock."); + res.send('Sorry, you must send a message to mock.'); } else { const mocked: string = mockService.mock(request.text); - const response: IChannelResponse = { + const response: ChannelResponse = { attachments: [ { - text: mocked - } + text: mocked, + }, ], - response_type: "in_channel", - text: `<@${request.user_id}>` + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', + text: `<@${request.user_id}>`, }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/controllers/muzzle.controller.ts b/src/controllers/muzzle.controller.ts index d65a52f4..6bfe7344 100644 --- a/src/controllers/muzzle.controller.ts +++ b/src/controllers/muzzle.controller.ts @@ -1,13 +1,13 @@ -import express, { Request, Response, Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { MuzzleService } from "../services/muzzle/muzzle.service"; -import { ReportService } from "../services/report/report.service"; -import { SlackService } from "../services/slack/slack.service"; -import { WebService } from "../services/web/web.service"; -import { ReportType } from "../shared/models/muzzle/muzzle-models"; -import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; +import express, { Request, Response, Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { MuzzleService } from '../services/muzzle/muzzle.service'; +import { ReportService } from '../services/report/report.service'; +import { SlackService } from '../services/slack/slack.service'; +import { WebService } from '../services/web/web.service'; +import { SlashCommandRequest } from '../shared/models/slack/slack-models'; +import { ReportType } from '../shared/models/report/report.model'; export const muzzleController: Router = express.Router(); @@ -19,21 +19,19 @@ const backfirePersistenceService = BackFirePersistenceService.getInstance(); const counterPersistenceService = CounterPersistenceService.getInstance(); const reportService = new ReportService(); -muzzleController.post("/muzzle", async (req: Request, res: Response) => { - const request: ISlashCommandRequest = req.body; - const userId: any = slackService.getUserId(request.text); - const results = await muzzleService - .addUserToMuzzled(userId, request.user_id, request.channel_name) - .catch(e => { - res.send(e); - }); +muzzleController.post('/muzzle', async (req: Request, res: Response) => { + const request: SlashCommandRequest = req.body; + const userId: string = slackService.getUserId(request.text); + const results = await muzzleService.addUserToMuzzled(userId, request.user_id, request.channel_name).catch(e => { + res.send(e); + }); if (results) { res.send(results); } }); -muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { - const request: ISlashCommandRequest = req.body; +muzzleController.post('/muzzle/stats', async (req: Request, res: Response) => { + const request: SlashCommandRequest = req.body; const userId: string = request.user_id; if ( muzzlePersistenceService.isUserMuzzled(userId) || @@ -41,27 +39,20 @@ muzzleController.post("/muzzle/stats", async (req: Request, res: Response) => { counterPersistenceService.isCounterMuzzled(request.user_id) ) { res.send(`Sorry! Can't do that while muzzled.`); - } else if (request.text.split(" ").length > 1) { + } else if (request.text.split(' ').length > 1) { res.send( - `Sorry! No support for multiple parameters at this time. Please choose one of: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\`` + `Sorry! No support for multiple parameters at this time. Please choose one of: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\``, ); - } else if ( - request.text !== "" && - !reportService.isValidReportType(request.text) - ) { + } else if (request.text !== '' && !reportService.isValidReportType(request.text)) { res.send( `Sorry! You passed in \`${ request.text - }\` but we can only generate reports for the following values: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\`` + }\` but we can only generate reports for the following values: \`week\`, \`month\`, \`trailing30\`, \`year\`, \`all\``, ); } else { const reportType: ReportType = reportService.getReportType(request.text); const report = await reportService.getMuzzleReport(reportType); - webService.uploadFile( - req.body.channel_id, - report, - reportService.getReportTitle(reportType) - ); + webService.uploadFile(req.body.channel_id, report, reportService.getReportTitle(reportType)); res.status(200).send(); } }); diff --git a/src/controllers/reaction.controller.ts b/src/controllers/reaction.controller.ts index 95715588..c2f60ced 100644 --- a/src/controllers/reaction.controller.ts +++ b/src/controllers/reaction.controller.ts @@ -1,9 +1,9 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { ReactionService } from "../services/reaction/reaction.service"; -import { ISlashCommandRequest } from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { ReactionService } from '../services/reaction/reaction.service'; +import { SlashCommandRequest } from '../shared/models/slack/slack-models'; export const reactionController: Router = express.Router(); @@ -12,8 +12,8 @@ const backfirePersistenceService = BackFirePersistenceService.getInstance(); const counterPersistenceService = CounterPersistenceService.getInstance(); const reactionService = new ReactionService(); -reactionController.post("/rep/get", async (req, res) => { - const request: ISlashCommandRequest = req.body; +reactionController.post('/rep/get', async (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || diff --git a/src/controllers/walkie.controller.ts b/src/controllers/walkie.controller.ts index 7e07e87b..fdb84895 100644 --- a/src/controllers/walkie.controller.ts +++ b/src/controllers/walkie.controller.ts @@ -1,13 +1,10 @@ -import express, { Router } from "express"; -import { BackFirePersistenceService } from "../services/backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../services/counter/counter.persistence.service"; -import { MuzzlePersistenceService } from "../services/muzzle/muzzle.persistence.service"; -import { SlackService } from "../services/slack/slack.service"; -import { WalkieService } from "../services/walkie/walkie.service"; -import { - IChannelResponse, - ISlashCommandRequest -} from "../shared/models/slack/slack-models"; +import express, { Router } from 'express'; +import { BackFirePersistenceService } from '../services/backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../services/counter/counter.persistence.service'; +import { MuzzlePersistenceService } from '../services/muzzle/muzzle.persistence.service'; +import { SlackService } from '../services/slack/slack.service'; +import { WalkieService } from '../services/walkie/walkie.service'; +import { ChannelResponse, SlashCommandRequest } from '../shared/models/slack/slack-models'; export const walkieController: Router = express.Router(); @@ -17,8 +14,8 @@ const counterPersistenceService = CounterPersistenceService.getInstance(); const slackService = SlackService.getInstance(); const walkieService = new WalkieService(); -walkieController.post("/walkie", (req, res) => { - const request: ISlashCommandRequest = req.body; +walkieController.post('/walkie', (req, res) => { + const request: SlashCommandRequest = req.body; if ( muzzlePersistenceService.isUserMuzzled(request.user_id) || backfirePersistenceService.isBackfire(request.user_id) || @@ -26,17 +23,18 @@ walkieController.post("/walkie", (req, res) => { ) { res.send(`Sorry, can't do that while muzzled.`); } else if (!request.text) { - res.send("Sorry, you must send a message to walkie talk."); + res.send('Sorry, you must send a message to walkie talk.'); } else { const walkied: string = walkieService.walkieTalkie(request.text); - const response: IChannelResponse = { + const response: ChannelResponse = { attachments: [ { - text: walkied - } + text: walkied, + }, ], - response_type: "in_channel", - text: `<@${request.user_id}>` + // eslint-disable-next-line @typescript-eslint/camelcase + response_type: 'in_channel', + text: `<@${request.user_id}>`, }; slackService.sendResponse(request.response_url, response); res.status(200).send(); diff --git a/src/index.ts b/src/index.ts index c87b0c1a..6895f833 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,22 @@ -import bodyParser from "body-parser"; -import express, { Application } from "express"; -import "reflect-metadata"; -import { createConnection } from "typeorm"; -import { clapController } from "./controllers/clap.controller"; -import { confessionController } from "./controllers/confession.controller"; -import { counterController } from "./controllers/counter.controller"; -import { defineController } from "./controllers/define.controller"; -import { eventController } from "./controllers/event.controller"; -import { listController } from "./controllers/list.controller"; -import { mockController } from "./controllers/mock.controller"; -import { muzzleController } from "./controllers/muzzle.controller"; -import { reactionController } from "./controllers/reaction.controller"; -import { walkieController } from "./controllers/walkie.controller"; -import { config } from "./ormconfig"; -import { SlackService } from "./services/slack/slack.service"; +import bodyParser from 'body-parser'; +import express, { Application } from 'express'; +import 'reflect-metadata'; +import { createConnection } from 'typeorm'; +import { clapController } from './controllers/clap.controller'; +import { confessionController } from './controllers/confession.controller'; +import { counterController } from './controllers/counter.controller'; +import { defineController } from './controllers/define.controller'; +import { eventController } from './controllers/event.controller'; +import { listController } from './controllers/list.controller'; +import { mockController } from './controllers/mock.controller'; +import { muzzleController } from './controllers/muzzle.controller'; +import { reactionController } from './controllers/reaction.controller'; +import { walkieController } from './controllers/walkie.controller'; +import { config } from './ormconfig'; +import { SlackService } from './services/slack/slack.service'; const app: Application = express(); -const PORT: number = 3000; +const PORT = 3000; app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); @@ -39,11 +39,9 @@ createConnection(config) slackService.getAllUsers(); console.log(`Connected to MySQL DB: ${config.database}`); } else { - throw Error("Unable to connect to database"); + throw Error('Unable to connect to database'); } }) .catch(e => console.error(e)); -app.listen(PORT, (e: Error) => - e ? console.error(e) : console.log("Listening on port 3000") -); +app.listen(PORT, (e: Error) => (e ? console.error(e) : console.log('Listening on port 3000'))); diff --git a/src/services/backfire/backfire.persistence.service.ts b/src/services/backfire/backfire.persistence.service.ts index c0cdbb1a..1ea6cb23 100644 --- a/src/services/backfire/backfire.persistence.service.ts +++ b/src/services/backfire/backfire.persistence.service.ts @@ -1,11 +1,11 @@ -import { getRepository } from "typeorm"; -import { Backfire } from "../../shared/db/models/Backfire"; -import { IBackfire } from "../../shared/models/backfire/backfire.model"; -import { ABUSE_PENALTY_TIME } from "../muzzle/constants"; -import { getRemainingTime } from "../muzzle/muzzle-utilities"; +import { UpdateResult, getRepository } from 'typeorm'; +import { Backfire } from '../../shared/db/models/Backfire'; +import { BackfireItem } from '../../shared/models/backfire/backfire.model'; +import { ABUSE_PENALTY_TIME } from '../muzzle/constants'; +import { getRemainingTime } from '../muzzle/muzzle-utilities'; export class BackFirePersistenceService { - public static getInstance() { + public static getInstance(): BackFirePersistenceService { if (!BackFirePersistenceService.instance) { BackFirePersistenceService.instance = new BackFirePersistenceService(); } @@ -13,11 +13,9 @@ export class BackFirePersistenceService { } private static instance: BackFirePersistenceService; - private backfires: Map = new Map(); + private backfires: Map = new Map(); - private constructor() {} - - public addBackfire(userId: string, time: number) { + public addBackfire(userId: string, time: number): Promise { const backfire = new Backfire(); backfire.muzzledId = userId; backfire.messagesSuppressed = 0; @@ -31,12 +29,12 @@ export class BackFirePersistenceService { this.backfires.set(userId, { suppressionCount: 0, id: backfireFromDb.id, - removalFn: setTimeout(() => this.removeBackfire(userId), time) + removalFn: setTimeout(() => this.removeBackfire(userId), time), }); }); } - public removeBackfire(userId: string) { + public removeBackfire(userId: string): void { this.backfires.delete(userId); console.log(`Backfire has expired and been removed for ${userId}`); } @@ -45,7 +43,7 @@ export class BackFirePersistenceService { return this.backfires.has(userId); } - public addBackfireTime(userId: string, timeToAdd: number) { + public addBackfireTime(userId: string, timeToAdd: number): void { if (userId && this.backfires.has(userId)) { const removalFn = this.backfires.get(userId)!.removalFn; const newTime = getRemainingTime(removalFn) + timeToAdd; @@ -56,16 +54,16 @@ export class BackFirePersistenceService { this.backfires.set(userId, { suppressionCount: this.backfires.get(userId)!.suppressionCount, id: this.backfires.get(userId)!.id, - removalFn: setTimeout(() => this.removeBackfire(userId), newTime) + removalFn: setTimeout(() => this.removeBackfire(userId), newTime), }); } } - public getBackfireByUserId(userId: string): IBackfire | undefined { + public getBackfireByUserId(userId: string): BackfireItem | undefined { return this.backfires.get(userId); } - public setBackfire(userId: string, options: IBackfire) { + public setBackfire(userId: string, options: BackfireItem): void { this.backfires.set(userId, options); } @@ -73,38 +71,27 @@ export class BackFirePersistenceService { * Determines suppression counts for messages that are ONLY deleted. * Used when a backfired user has hit their max suppressions or when they have tagged channel. */ - public trackDeletedMessage(backfireId: number, text: string) { - const words = text.split(" ").length; - const characters = text.split("").length; + public trackDeletedMessage(backfireId: number, text: string): void { + const words = text.split(' ').length; + const characters = text.split('').length; this.incrementMessageSuppressions(backfireId); this.incrementWordSuppressions(backfireId, words); this.incrementCharacterSuppressions(backfireId, characters); } - public incrementBackfireTime(id: number, ms: number) { - return getRepository(Backfire).increment({ id }, "milliseconds", ms); + public incrementBackfireTime(id: number, ms: number): Promise { + return getRepository(Backfire).increment({ id }, 'milliseconds', ms); } - public incrementMessageSuppressions(id: number) { - return getRepository(Backfire).increment({ id }, "messagesSuppressed", 1); + public incrementMessageSuppressions(id: number): Promise { + return getRepository(Backfire).increment({ id }, 'messagesSuppressed', 1); } - public incrementWordSuppressions(id: number, suppressions: number) { - return getRepository(Backfire).increment( - { id }, - "wordsSuppressed", - suppressions - ); + public incrementWordSuppressions(id: number, suppressions: number): Promise { + return getRepository(Backfire).increment({ id }, 'wordsSuppressed', suppressions); } - public incrementCharacterSuppressions( - id: number, - charactersSuppressed: number - ) { - return getRepository(Backfire).increment( - { id }, - "charactersSuppressed", - charactersSuppressed - ); + public incrementCharacterSuppressions(id: number, charactersSuppressed: number): Promise { + return getRepository(Backfire).increment({ id }, 'charactersSuppressed', charactersSuppressed); } } diff --git a/src/services/backfire/backfire.service.spec.ts b/src/services/backfire/backfire.service.spec.ts index 7a6339e0..56872d4b 100644 --- a/src/services/backfire/backfire.service.spec.ts +++ b/src/services/backfire/backfire.service.spec.ts @@ -1,8 +1,8 @@ -import { ISlackUser } from "../../shared/models/slack/slack-models"; -import { SlackService } from "../slack/slack.service"; -import { BackfireService } from "./backfire.service"; +import { SlackUser } from '../../shared/models/slack/slack-models'; +import { SlackService } from '../slack/slack.service'; +import { BackfireService } from './backfire.service'; -describe("BackfireService", () => { +describe('BackfireService', () => { let backfireService: BackfireService; let slackInstance: SlackService; @@ -10,11 +10,11 @@ describe("BackfireService", () => { backfireService = new BackfireService(); slackInstance = SlackService.getInstance(); slackInstance.userList = [ - { id: "123", name: "test123" }, - { id: "456", name: "test456" }, - { id: "789", name: "test789" }, - { id: "666", name: "requestor" } - ] as ISlackUser[]; + { id: '123', name: 'test123' }, + { id: '456', name: 'test456' }, + { id: '789', name: 'test789' }, + { id: '666', name: 'requestor' }, + ] as SlackUser[]; jest.useFakeTimers(); }); @@ -22,7 +22,7 @@ describe("BackfireService", () => { jest.runAllTimers(); }); - it("should create", () => { + it('should create', () => { expect(backfireService).toBeTruthy(); }); }); diff --git a/src/services/backfire/backfire.service.ts b/src/services/backfire/backfire.service.ts index aca9d433..962d150c 100644 --- a/src/services/backfire/backfire.service.ts +++ b/src/services/backfire/backfire.service.ts @@ -1,10 +1,10 @@ -import { IBackfire } from "../../shared/models/backfire/backfire.model"; -import { IEventRequest } from "../../shared/models/slack/slack-models"; -import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "../muzzle/constants"; -import { isRandomEven } from "../muzzle/muzzle-utilities"; -import { SlackService } from "../slack/slack.service"; -import { WebService } from "../web/web.service"; -import { BackFirePersistenceService } from "./backfire.persistence.service"; +import { BackfireItem } from '../../shared/models/backfire/backfire.model'; +import { EventRequest } from '../../shared/models/slack/slack-models'; +import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from '../muzzle/constants'; +import { isRandomEven } from '../muzzle/muzzle-utilities'; +import { SlackService } from '../slack/slack.service'; +import { WebService } from '../web/web.service'; +import { BackFirePersistenceService } from './backfire.persistence.service'; export class BackfireService { private webService = WebService.getInstance(); @@ -14,10 +14,10 @@ export class BackfireService { /** * Takes in text and randomly muzzles words. */ - public backfireMessage(text: string, backfireId: number) { - const words = text.split(" "); + public backfireMessage(text: string, backfireId: number): string { + const words = text.split(' '); - let returnText = ""; + let returnText = ''; let wordsSuppressed = 0; let charactersSuppressed = 0; let replacementWord; @@ -27,73 +27,53 @@ export class BackfireService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)], ); - if ( - replacementWord.includes( - REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] - ) - ) { + if (replacementWord.includes(REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)])) { wordsSuppressed++; charactersSuppressed += words[i].length; } returnText += replacementWord; } this.backfirePersistenceService.incrementMessageSuppressions(backfireId); - this.backfirePersistenceService.incrementCharacterSuppressions( - backfireId, - charactersSuppressed - ); - this.backfirePersistenceService.incrementWordSuppressions( - backfireId, - wordsSuppressed - ); + this.backfirePersistenceService.incrementCharacterSuppressions(backfireId, charactersSuppressed); + this.backfirePersistenceService.incrementWordSuppressions(backfireId, wordsSuppressed); return returnText; } - public addBackfireTime(userId: string, time: number) { + public addBackfireTime(userId: string, time: number): void { this.backfirePersistenceService.addBackfireTime(userId, time); } - public sendBackfiredMessage( - channel: string, - userId: string, - text: string, - timestamp: string - ) { - const backfire: - | IBackfire - | undefined = this.backfirePersistenceService.getBackfireByUserId(userId); + public sendBackfiredMessage(channel: string, userId: string, text: string, timestamp: string): void { + const backfire: BackfireItem | undefined = this.backfirePersistenceService.getBackfireByUserId(userId); if (backfire) { this.webService.deleteMessage(channel, timestamp); if (backfire!.suppressionCount < MAX_SUPPRESSIONS) { this.backfirePersistenceService.setBackfire(userId, { suppressionCount: ++backfire!.suppressionCount, id: backfire!.id, - removalFn: backfire!.removalFn + removalFn: backfire!.removalFn, }); - this.webService.sendMessage( - channel, - `<@${userId}> says "${this.backfireMessage(text, backfire!.id)}"` - ); + this.webService.sendMessage(channel, `<@${userId}> says "${this.backfireMessage(text, backfire!.id)}"`); } else { this.backfirePersistenceService.trackDeletedMessage(backfire!.id, text); } } } - public getBackfire(userId: string) { + public getBackfire(userId: string): BackfireItem | undefined { return this.backfirePersistenceService.getBackfireByUserId(userId); } - public trackDeletedMessage(id: number, text: string) { + public trackDeletedMessage(id: number, text: string): void { this.backfirePersistenceService.trackDeletedMessage(id, text); } /** * Determines whether or not a bot message should be removed. */ - public shouldBotMessageBeMuzzled(request: IEventRequest) { + public shouldBotMessageBeMuzzled(request: EventRequest): boolean { let userIdByEventText; let userIdByAttachmentText; let userIdByAttachmentPretext; @@ -104,17 +84,11 @@ export class BackfireService { } if (request.event.attachments && request.event.attachments.length) { - userIdByAttachmentText = this.slackService.getUserId( - request.event.attachments[0].text - ); - userIdByAttachmentPretext = this.slackService.getUserId( - request.event.attachments[0].pretext - ); + userIdByAttachmentText = this.slackService.getUserId(request.event.attachments[0].text); + userIdByAttachmentPretext = this.slackService.getUserId(request.event.attachments[0].pretext); if (request.event.attachments[0].callback_id) { - userIdByCallbackId = this.slackService.getUserIdByCallbackId( - request.event.attachments[0].callback_id - ); + userIdByCallbackId = this.slackService.getUserIdByCallbackId(request.event.attachments[0].callback_id); } } @@ -122,28 +96,20 @@ export class BackfireService { userIdByEventText, userIdByAttachmentText, userIdByAttachmentPretext, - userIdByCallbackId + userIdByCallbackId, ); return !!( - request.event.subtype === "bot_message" && + request.event.subtype === 'bot_message' && finalUserId && this.backfirePersistenceService.isBackfire(finalUserId) && - request.event.username !== "muzzle" + request.event.username !== 'muzzle' ); } - private getReplacementWord( - word: string, - isFirstWord: boolean, - isLastWord: boolean, - replacementText: string - ) { + private getReplacementWord(word: string, isFirstWord: boolean, isLastWord: boolean, replacementText: string): string { const text = - isRandomEven() && - word.length < 10 && - word !== " " && - !this.slackService.containsTag(word) + isRandomEven() && word.length < 10 && word !== ' ' && !this.slackService.containsTag(word) ? `*${word}*` : replacementText; diff --git a/src/services/clap/clap.service.spec.ts b/src/services/clap/clap.service.spec.ts index 11f765be..4862bea3 100644 --- a/src/services/clap/clap.service.spec.ts +++ b/src/services/clap/clap.service.spec.ts @@ -1,21 +1,19 @@ -import { ClapService } from "./clap.service"; +import { ClapService } from './clap.service'; -describe("ClapService", () => { +describe('ClapService', () => { let clapService: ClapService; beforeEach(() => { clapService = new ClapService(); }); - describe("clap()", () => { - it("should clap a users input with multiple words", () => { - expect(clapService.clap("test this out")).toBe( - "test :clap: this :clap: out :clap:" - ); + describe('clap()', () => { + it('should clap a users input with multiple words', () => { + expect(clapService.clap('test this out')).toBe('test :clap: this :clap: out :clap:'); }); - it("should return input if it is an empty string", () => { - expect(clapService.clap("")).toBe(""); + it('should return input if it is an empty string', () => { + expect(clapService.clap('')).toBe(''); }); }); }); diff --git a/src/services/clap/clap.service.ts b/src/services/clap/clap.service.ts index f3e4277a..2701c2c8 100644 --- a/src/services/clap/clap.service.ts +++ b/src/services/clap/clap.service.ts @@ -1,13 +1,12 @@ export class ClapService { - public clap(text: string) { + public clap(text: string): string { if (!text || text.length === 0) { return text; } - let output = ""; - const words = text.trim().split(" "); + let output = ''; + const words = text.trim().split(' '); for (let i = 0; i < words.length; i++) { - output += - i !== words.length - 1 ? `${words[i]} :clap: ` : `${words[i]} :clap:`; + output += i !== words.length - 1 ? `${words[i]} :clap: ` : `${words[i]} :clap:`; } return output; } diff --git a/src/services/counter/counter.persistence.service.ts b/src/services/counter/counter.persistence.service.ts index 3b5916dd..0991c466 100644 --- a/src/services/counter/counter.persistence.service.ts +++ b/src/services/counter/counter.persistence.service.ts @@ -1,16 +1,13 @@ -import { getRepository } from "typeorm"; -import { Counter } from "../../shared/db/models/Counter"; -import { - ICounter, - ICounterMuzzle -} from "../../shared/models/counter/counter-models"; -import { getRemainingTime } from "../muzzle/muzzle-utilities"; -import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; -import { WebService } from "../web/web.service"; -import { COUNTER_TIME } from "./constants"; +import { getRepository } from 'typeorm'; +import { Counter } from '../../shared/db/models/Counter'; +import { CounterItem, CounterMuzzle } from '../../shared/models/counter/counter-models'; +import { getRemainingTime } from '../muzzle/muzzle-utilities'; +import { MuzzlePersistenceService } from '../muzzle/muzzle.persistence.service'; +import { WebService } from '../web/web.service'; +import { COUNTER_TIME } from './constants'; export class CounterPersistenceService { - public static getInstance() { + public static getInstance(): CounterPersistenceService { if (!CounterPersistenceService.instance) { CounterPersistenceService.instance = new CounterPersistenceService(); } @@ -20,12 +17,10 @@ export class CounterPersistenceService { private static instance: CounterPersistenceService; private muzzlePersistenceService: MuzzlePersistenceService = MuzzlePersistenceService.getInstance(); private webService: WebService = WebService.getInstance(); - private counters: Map = new Map(); - private counterMuzzles: Map = new Map(); + private counters: Map = new Map(); + private counterMuzzles: Map = new Map(); - private constructor() {} - - public addCounter(requestorId: string, counteredUserId: string) { + public addCounter(requestorId: string, counteredUserId: string): Promise { return new Promise(async (resolve, reject) => { const counter = new Counter(); counter.requestorId = requestorId; @@ -42,7 +37,7 @@ export class CounterPersistenceService { }); } - public addCounterMuzzleTime(userId: string, timeToAdd: number) { + public addCounterMuzzleTime(userId: string, timeToAdd: number): void { if (userId && this.counterMuzzles.has(userId)) { const removalFn = this.counterMuzzles.get(userId)!.removalFn; const newTime = getRemainingTime(removalFn) + timeToAdd; @@ -51,51 +46,45 @@ export class CounterPersistenceService { this.counterMuzzles.set(userId, { suppressionCount: this.counterMuzzles.get(userId)!.suppressionCount, counterId: this.counterMuzzles.get(userId)!.counterId, - removalFn: setTimeout(() => this.removeCounterMuzzle(userId), newTime) + removalFn: setTimeout(() => this.removeCounterMuzzle(userId), newTime), }); } } - public setCounterMuzzle(userId: string, options: ICounterMuzzle) { + public setCounterMuzzle(userId: string, options: CounterMuzzle): void { this.counterMuzzles.set(userId, options); } - public async setCounteredToTrue(id: number) { + public async setCounteredToTrue(id: number): Promise { const counter = await getRepository(Counter).findOne(id); counter!.countered = true; return getRepository(Counter).save(counter as Counter); } - public getCounter(counterId: number): ICounter | undefined { + public getCounter(counterId: number): CounterItem | undefined { return this.counters.get(counterId); } - public isCounterMuzzled(userId: string) { + public isCounterMuzzled(userId: string): boolean { return this.counterMuzzles.has(userId); } - public getCounterMuzzle(userId: string) { + public getCounterMuzzle(userId: string): CounterMuzzle | undefined { return this.counterMuzzles.get(userId); } - public counterMuzzle(userId: string, counterId: number) { + public counterMuzzle(userId: string, counterId: number): void { this.counterMuzzles.set(userId, { suppressionCount: 0, counterId, - removalFn: setTimeout( - () => this.removeCounterMuzzle(userId), - COUNTER_TIME - ) + removalFn: setTimeout(() => this.removeCounterMuzzle(userId), COUNTER_TIME), }); } /** * Retrieves the counterId for a counter that includes the specified requestorId and userId. */ - public getCounterByRequestorAndUserId( - requestorId: string, - userId: string - ): number | undefined { + public getCounterByRequestorAndUserId(requestorId: string, userId: string): number | undefined { let counterId; this.counters.forEach((item, key) => { if (item.requestorId === requestorId && item.counteredId === userId) { @@ -106,46 +95,35 @@ export class CounterPersistenceService { return counterId; } - public async removeCounter(id: number, isUsed: boolean, channel?: string) { + public async removeCounter(id: number, isUsed: boolean, channel?: string): Promise { const counter = this.counters.get(id); clearTimeout(counter!.removalFn); if (isUsed && channel) { this.counters.delete(id); - await this.setCounteredToTrue(id).catch(e => - console.error("Error during setCounteredToTrue", e) - ); + await this.setCounteredToTrue(id).catch(e => console.error('Error during setCounteredToTrue', e)); } else { // This whole section is an anti-pattern. Fix this. this.counters.delete(id); this.counterMuzzle(counter!.requestorId, id); - this.muzzlePersistenceService.removeMuzzlePrivileges( - counter!.requestorId - ); + this.muzzlePersistenceService.removeMuzzlePrivileges(counter!.requestorId); this.webService.sendMessage( - "#general", + '#general', `:flesh: <@${counter!.requestorId}> lives in fear of <@${ counter!.counteredId - }> and is now muzzled and has lost muzzle privileges for one hour. :flesh:` + }> and is now muzzled and has lost muzzle privileges for one hour. :flesh:`, ); } } - private removeCounterMuzzle(userId: string) { + private removeCounterMuzzle(userId: string): void { this.counterMuzzles.delete(userId); } - private setCounterState( - requestorId: string, - userId: string, - counterId: number - ) { + private setCounterState(requestorId: string, userId: string, counterId: number): void { this.counters.set(counterId, { requestorId, counteredId: userId, - removalFn: setTimeout( - () => this.removeCounter(counterId, false, "#general"), - COUNTER_TIME - ) + removalFn: setTimeout(() => this.removeCounter(counterId, false, '#general'), COUNTER_TIME), }); } } diff --git a/src/services/counter/counter.service.spec.ts b/src/services/counter/counter.service.spec.ts index 82e97b4c..089b0014 100644 --- a/src/services/counter/counter.service.spec.ts +++ b/src/services/counter/counter.service.spec.ts @@ -1,7 +1,7 @@ -import { CounterService } from "./counter.service"; +import { CounterService } from './counter.service'; describe(CounterService, () => { - it("should create", () => { + it('should create', () => { expect(new CounterService()).toBeTruthy(); }); }); diff --git a/src/services/counter/counter.service.ts b/src/services/counter/counter.service.ts index 05c1413b..c96f5423 100644 --- a/src/services/counter/counter.service.ts +++ b/src/services/counter/counter.service.ts @@ -1,12 +1,12 @@ -import { ICounterMuzzle } from "../../shared/models/counter/counter-models"; -import { IEventRequest } from "../../shared/models/slack/slack-models"; -import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "../muzzle/constants"; -import { getTimeString, isRandomEven } from "../muzzle/muzzle-utilities"; -import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; -import { SlackService } from "../slack/slack.service"; -import { WebService } from "../web/web.service"; -import { COUNTER_TIME } from "./constants"; -import { CounterPersistenceService } from "./counter.persistence.service"; +import { CounterMuzzle } from '../../shared/models/counter/counter-models'; +import { EventRequest } from '../../shared/models/slack/slack-models'; +import { MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from '../muzzle/constants'; +import { getTimeString, isRandomEven } from '../muzzle/muzzle-utilities'; +import { MuzzlePersistenceService } from '../muzzle/muzzle.persistence.service'; +import { SlackService } from '../slack/slack.service'; +import { WebService } from '../web/web.service'; +import { COUNTER_TIME } from './constants'; +import { CounterPersistenceService } from './counter.persistence.service'; export class CounterService { private slackService = SlackService.getInstance(); @@ -17,49 +17,32 @@ export class CounterService { /** * Creates a counter in DB and stores it in memory. */ - public createCounter( - counteredId: string, - requestorId: string - ): Promise { + public createCounter(counteredId: string, requestorId: string): Promise { const counterUserName = this.slackService.getUserName(counteredId); return new Promise(async (resolve, reject) => { if (!counteredId || !requestorId) { - reject( - `Invalid username passed in. You can only counter existing slack users.` - ); - } else if ( - this.counterPersistenceService.getCounterByRequestorAndUserId( - requestorId, - counteredId - ) - ) { - reject("You already have a counter for this user."); + reject(`Invalid username passed in. You can only counter existing slack users.`); + } else if (this.counterPersistenceService.getCounterByRequestorAndUserId(requestorId, counteredId)) { + reject('You already have a counter for this user.'); } else { await this.counterPersistenceService .addCounter(requestorId, counteredId) .then(() => { - resolve( - `Counter set for ${counterUserName} for the next ${getTimeString( - COUNTER_TIME - )}` - ); + resolve(`Counter set for ${counterUserName} for the next ${getTimeString(COUNTER_TIME)}`); }) .catch(e => reject(e)); } }); } - public getCounterByRequestorAndUserId(requestorId: string, userId: string) { - return this.counterPersistenceService.getCounterByRequestorAndUserId( - requestorId, - userId - ); + public getCounterByRequestorAndUserId(requestorId: string, userId: string): number | undefined { + return this.counterPersistenceService.getCounterByRequestorAndUserId(requestorId, userId); } - public createCounterMuzzleMessage(text: string) { - const words = text.split(" "); + public createCounterMuzzleMessage(text: string): string { + const words = text.split(' '); - let returnText = ""; + let returnText = ''; let replacementWord; for (let i = 0; i < words.length; i++) { @@ -67,24 +50,16 @@ export class CounterService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)], ); returnText += replacementWord; } return returnText; } - public getReplacementWord( - word: string, - isFirstWord: boolean, - isLastWord: boolean, - replacementText: string - ) { + public getReplacementWord(word: string, isFirstWord: boolean, isLastWord: boolean, replacementText: string): string { const text = - isRandomEven() && - word.length < 10 && - word !== " " && - !this.slackService.containsTag(word) + isRandomEven() && word.length < 10 && word !== ' ' && !this.slackService.containsTag(word) ? `*${word}*` : replacementText; @@ -94,27 +69,17 @@ export class CounterService { return text; } - public sendCounterMuzzledMessage( - channel: string, - userId: string, - text: string, - timestamp: string - ) { - const counterMuzzle: - | ICounterMuzzle - | undefined = this.counterPersistenceService.getCounterMuzzle(userId); + public sendCounterMuzzledMessage(channel: string, userId: string, text: string, timestamp: string): void { + const counterMuzzle: CounterMuzzle | undefined = this.counterPersistenceService.getCounterMuzzle(userId); if (counterMuzzle) { this.webService.deleteMessage(channel, timestamp); if (counterMuzzle!.suppressionCount < MAX_SUPPRESSIONS) { this.counterPersistenceService.setCounterMuzzle(userId, { suppressionCount: ++counterMuzzle!.suppressionCount, counterId: counterMuzzle!.counterId, - removalFn: counterMuzzle!.removalFn + removalFn: counterMuzzle!.removalFn, }); - this.webService.sendMessage( - channel, - `<@${userId}> says "${this.createCounterMuzzleMessage(text)}"` - ); + this.webService.sendMessage(channel, `<@${userId}> says "${this.createCounterMuzzleMessage(text)}"`); } } } @@ -122,7 +87,7 @@ export class CounterService { /** * Determines whether or not a bot message should be removed. */ - public shouldBotMessageBeMuzzled(request: IEventRequest) { + public shouldBotMessageBeMuzzled(request: EventRequest): boolean { let userIdByEventText; let userIdByAttachmentText; let userIdByAttachmentPretext; @@ -133,17 +98,11 @@ export class CounterService { } if (request.event.attachments && request.event.attachments.length) { - userIdByAttachmentText = this.slackService.getUserId( - request.event.attachments[0].text - ); - userIdByAttachmentPretext = this.slackService.getUserId( - request.event.attachments[0].pretext - ); + userIdByAttachmentText = this.slackService.getUserId(request.event.attachments[0].text); + userIdByAttachmentPretext = this.slackService.getUserId(request.event.attachments[0].pretext); if (request.event.attachments[0].callback_id) { - userIdByCallbackId = this.slackService.getUserIdByCallbackId( - request.event.attachments[0].callback_id - ); + userIdByCallbackId = this.slackService.getUserIdByCallbackId(request.event.attachments[0].callback_id); } } @@ -151,32 +110,28 @@ export class CounterService { userIdByEventText, userIdByAttachmentText, userIdByAttachmentPretext, - userIdByCallbackId + userIdByCallbackId, ); return !!( - request.event.subtype === "bot_message" && + request.event.subtype === 'bot_message' && finalUserId && this.counterPersistenceService.isCounterMuzzled(finalUserId) && - request.event.username !== "muzzle" + request.event.username !== 'muzzle' ); } - public removeCounter(id: number, isUsed: boolean, channel?: string) { + public removeCounter(id: number, isUsed: boolean, channel?: string): void { const counter = this.counterPersistenceService.getCounter(id); this.counterPersistenceService.removeCounter(id, isUsed, channel); if (isUsed && channel) { this.counterPersistenceService.counterMuzzle(counter!.counteredId, id); - this.muzzlePersistenceService.removeMuzzlePrivileges( - counter!.counteredId - ); + this.muzzlePersistenceService.removeMuzzlePrivileges(counter!.counteredId); this.webService.sendMessage( channel, - `:crossed_swords: <@${counter!.requestorId}> successfully countered <@${ - counter!.counteredId - }>! <@${ + `:crossed_swords: <@${counter!.requestorId}> successfully countered <@${counter!.counteredId}>! <@${ counter!.counteredId - }> has lost muzzle privileges for one hour and is muzzled for the next 5 minutes! :crossed_swords:` + }> has lost muzzle privileges for one hour and is muzzled for the next 5 minutes! :crossed_swords:`, ); } } diff --git a/src/services/define/define.service.spec.ts b/src/services/define/define.service.spec.ts index 675a67a8..d1a7735e 100644 --- a/src/services/define/define.service.spec.ts +++ b/src/services/define/define.service.spec.ts @@ -1,118 +1,112 @@ -import { IDefinition } from "../../shared/models/define/define-models"; -import { DefineService } from "./define.service"; - -describe("define-utils", () => { - let defineService: DefineService; - - beforeEach(() => { - defineService = DefineService.getInstance(); - }); - - describe("capitalizeFirstLetter()", () => { - it("should capitalize all first letters of a given string", () => { - expect(defineService.capitalizeFirstLetter("test string")).toBe( - "Test String" - ); - }); - - it("should capitalize only the first letter of the first word when all = false", () => { - expect(defineService.capitalizeFirstLetter("test string", false)).toBe( - "Test string" - ); - }); - }); - - describe("define()", () => { - it("should return a promise when attempting to define", () => { - expect(defineService.define("test")).toBeDefined(); // Firm this up - }); - }); - - describe("formatDefs()", () => { - it("should return an array of 3 length when no maxDefs parameter is provided", () => { - expect(defineService.formatDefs(testArray, "test").length).toBe(3); - }); - - it("should return an array of 4 length when a maxDefs parameter of 4 is provided", () => { - expect(defineService.formatDefs(testArray, "test", 4).length).toBe(4); - }); - - it("should return testArray.length if maxDefs parameter is larger than testArray.length", () => { - expect(defineService.formatDefs(testArray, "test", 10).length).toBe(5); - }); - - it(`should return [{ "Sorry, no definitions found" }] if defArr === 0`, () => { - expect(defineService.formatDefs([], "test")[0].text).toBe( - "Sorry, no definitions found." - ); - }); - }); -}); - -const testArray: IDefinition[] = [ +/* eslint-disable @typescript-eslint/camelcase */ +import { Definition } from '../../shared/models/define/define-models'; +import { DefineService } from './define.service'; +const testArray: Definition[] = [ { - definition: "one", - permalink: "https://urbandictionary.com/whatever", + definition: 'one', + permalink: 'https://urbandictionary.com/whatever', thumbs_up: 12, - author: "jr", - word: "test", + author: 'jr', + word: 'test', defid: 1, - written_on: "whatever", // ISO Date - example: "test", + written_on: 'whatever', // ISO Date + example: 'test', thumbs_down: 14, - current_vote: "test", - sound_urls: ["test"] + current_vote: 'test', + sound_urls: ['test'], }, { - definition: "two", - permalink: "https://urbandictionary.com/whatever", + definition: 'two', + permalink: 'https://urbandictionary.com/whatever', thumbs_up: 12, - author: "jr", - word: "test", + author: 'jr', + word: 'test', defid: 1, - written_on: "whatever", // ISO Date - example: "test", + written_on: 'whatever', // ISO Date + example: 'test', thumbs_down: 14, - current_vote: "test", - sound_urls: ["test"] + current_vote: 'test', + sound_urls: ['test'], }, { - definition: "three", - permalink: "https://urbandictionary.com/whatever", + definition: 'three', + permalink: 'https://urbandictionary.com/whatever', thumbs_up: 12, - author: "jr", - word: "test", + author: 'jr', + word: 'test', defid: 1, - written_on: "whatever", // ISO Date - example: "test", + written_on: 'whatever', // ISO Date + example: 'test', thumbs_down: 14, - current_vote: "test", - sound_urls: ["test"] + current_vote: 'test', + sound_urls: ['test'], }, { - definition: "four", - permalink: "https://urbandictionary.com/whatever", + definition: 'four', + permalink: 'https://urbandictionary.com/whatever', thumbs_up: 12, - author: "jr", - word: "test", + author: 'jr', + word: 'test', defid: 1, - written_on: "whatever", // ISO Date - example: "test", + written_on: 'whatever', // ISO Date + example: 'test', thumbs_down: 14, - current_vote: "test", - sound_urls: ["test"] + current_vote: 'test', + sound_urls: ['test'], }, { - definition: "five", - permalink: "https://urbandictionary.com/whatever", + definition: 'five', + permalink: 'https://urbandictionary.com/whatever', thumbs_up: 12, - author: "jr", - word: "test", + author: 'jr', + word: 'test', defid: 1, - written_on: "whatever", // ISO Date - example: "five", + written_on: 'whatever', // ISO Date + example: 'five', thumbs_down: 14, - current_vote: "test", - sound_urls: ["test"] - } + current_vote: 'test', + sound_urls: ['test'], + }, ]; + +describe('define-utils', () => { + let defineService: DefineService; + + beforeEach(() => { + defineService = DefineService.getInstance(); + }); + + describe('capitalizeFirstLetter()', () => { + it('should capitalize all first letters of a given string', () => { + expect(defineService.capitalizeFirstLetter('test string')).toBe('Test String'); + }); + + it('should capitalize only the first letter of the first word when all = false', () => { + expect(defineService.capitalizeFirstLetter('test string', false)).toBe('Test string'); + }); + }); + + describe('define()', () => { + it('should return a promise when attempting to define', () => { + expect(defineService.define('test')).toBeDefined(); // Firm this up + }); + }); + + describe('formatDefs()', () => { + it('should return an array of 3 length when no maxDefs parameter is provided', () => { + expect(defineService.formatDefs(testArray, 'test').length).toBe(3); + }); + + it('should return an array of 4 length when a maxDefs parameter of 4 is provided', () => { + expect(defineService.formatDefs(testArray, 'test', 4).length).toBe(4); + }); + + it('should return testArray.length if maxDefs parameter is larger than testArray.length', () => { + expect(defineService.formatDefs(testArray, 'test', 10).length).toBe(5); + }); + + it(`should return [{ "Sorry, no definitions found" }] if defArr === 0`, () => { + expect(defineService.formatDefs([], 'test')[0].text).toBe('Sorry, no definitions found.'); + }); + }); +}); diff --git a/src/services/define/define.service.ts b/src/services/define/define.service.ts index e11b2004..5b8fdcd4 100644 --- a/src/services/define/define.service.ts +++ b/src/services/define/define.service.ts @@ -1,12 +1,9 @@ -import Axios, { AxiosResponse } from "axios"; -import { - IDefinition, - IUrbanDictionaryResponse -} from "../../shared/models/define/define-models"; -import { IAttachment } from "../../shared/models/slack/slack-models"; +import Axios, { AxiosResponse } from 'axios'; +import { Definition, UrbanDictionaryResponse } from '../../shared/models/define/define-models'; +import { Attachment } from '../../shared/models/slack/slack-models'; export class DefineService { - public static getInstance() { + public static getInstance(): DefineService { if (!DefineService.instance) { DefineService.instance = new DefineService(); } @@ -15,41 +12,35 @@ export class DefineService { private static instance: DefineService; - private constructor() {} - /** * Capitalizes the first letter of a given sentence. */ public capitalizeFirstLetter(sentence: string, all = true): string { if (all) { - const words = sentence.split(" "); + const words = sentence.split(' '); return words .map(word => word .charAt(0) .toUpperCase() - .concat(word.slice(1)) + .concat(word.slice(1)), ) - .join(" "); + .join(' '); } - return ( - sentence.charAt(0).toUpperCase() + sentence.slice(1, sentence.length) - ); + return sentence.charAt(0).toUpperCase() + sentence.slice(1, sentence.length); } /** * Returns a promise to look up a definition on urban dictionary. */ - public define(word: string): Promise { - const formattedWord = word.split(" ").join("+"); - return Axios.get( - `http://api.urbandictionary.com/v0/define?term=${formattedWord}` - ) - .then((res: AxiosResponse) => { + public define(word: string): Promise { + const formattedWord = word.split(' ').join('+'); + return Axios.get(`http://api.urbandictionary.com/v0/define?term=${formattedWord}`) + .then((res: AxiosResponse) => { return res.data; }) .catch(e => { - console.log("error", e); + console.log('error', e); return e; }); } @@ -57,23 +48,19 @@ export class DefineService { /** * Takes in an array of definitions and breaks them down into a shortened list depending on maxDefs */ - public formatDefs(defArr: IDefinition[], definedWord: string, maxDefs = 3) { + public formatDefs(defArr: Definition[], definedWord: string, maxDefs = 3): { text: string }[] { if (!defArr || defArr.length === 0) { - return [{ text: "Sorry, no definitions found." }]; + return [{ text: 'Sorry, no definitions found.' }]; } - const formattedArr: IAttachment[] = []; + const formattedArr: Attachment[] = []; for (let i = 0; i < defArr.length; i++) { if (defArr[i].word.toLowerCase() === definedWord.toLowerCase()) { formattedArr.push({ - text: this.formatUrbanD( - `${i + 1}. ${this.capitalizeFirstLetter( - defArr[i].definition, - false - )}` - ), - mrkdown_in: ["text"] + text: this.formatUrbanD(`${i + 1}. ${this.capitalizeFirstLetter(defArr[i].definition, false)}`), + // eslint-disable-next-line @typescript-eslint/camelcase + mrkdown_in: ['text'], }); } @@ -81,17 +68,15 @@ export class DefineService { return formattedArr; } } - return formattedArr.length - ? formattedArr - : [{ text: "Sorry, no definitions found." }]; + return formattedArr.length ? formattedArr : [{ text: 'Sorry, no definitions found.' }]; } /** * Takes in a definition and removes brackets. */ private formatUrbanD(definition: string): string { - let formattedDefinition: string = ""; + let formattedDefinition = ''; for (const letter of definition) { - if (letter !== "[" && letter !== "]") { + if (letter !== '[' && letter !== ']') { formattedDefinition += letter; } } diff --git a/src/services/list/list.persistence.service.ts b/src/services/list/list.persistence.service.ts index 291e13ae..99435a4a 100644 --- a/src/services/list/list.persistence.service.ts +++ b/src/services/list/list.persistence.service.ts @@ -1,8 +1,8 @@ -import { getRepository } from "typeorm"; -import { List } from "../../shared/db/models/List"; +import { getRepository } from 'typeorm'; +import { List } from '../../shared/db/models/List'; export class ListPersistenceService { - public static getInstance() { + public static getInstance(): ListPersistenceService { if (!ListPersistenceService.instance) { ListPersistenceService.instance = new ListPersistenceService(); } @@ -11,20 +11,18 @@ export class ListPersistenceService { private static instance: ListPersistenceService; - private constructor() {} - - public store(requestorId: string, text: string) { + public store(requestorId: string, text: string): Promise { const listItem = new List(); listItem.requestorId = requestorId; listItem.text = text; return getRepository(List).save(listItem); } - public retrieve() { + public retrieve(): Promise { return getRepository(List).find(); } - public remove(text: string) { + public remove(text: string): Promise { return new Promise(async (resolve, reject) => { const item = await getRepository(List).findOne({ text }); if (item) { diff --git a/src/services/mock/mock.service.spec.ts b/src/services/mock/mock.service.spec.ts index bfe97640..a5b72aed 100644 --- a/src/services/mock/mock.service.spec.ts +++ b/src/services/mock/mock.service.spec.ts @@ -1,23 +1,21 @@ -import { MockService } from "./mock.service"; +import { MockService } from './mock.service'; -describe("MockService", () => { +describe('MockService', () => { let mockService: MockService; beforeEach(() => { mockService = MockService.getInstance(); }); - describe("mock()", () => { - it("should mock a users input (single word)", () => { - expect(mockService.mock("test")).toBe("tEsT"); + describe('mock()', () => { + it('should mock a users input (single word)', () => { + expect(mockService.mock('test')).toBe('tEsT'); }); - it("should mock a users input (sentence)", () => { - expect(mockService.mock("test sentence with multiple words.")).toBe( - "tEsT sEnTeNcE wItH mUlTiPlE wOrDs." - ); + it('should mock a users input (sentence)', () => { + expect(mockService.mock('test sentence with multiple words.')).toBe('tEsT sEnTeNcE wItH mUlTiPlE wOrDs.'); }); - it("should return input if it is an empty string", () => { - expect(mockService.mock("")).toBe(""); + it('should return input if it is an empty string', () => { + expect(mockService.mock('')).toBe(''); }); }); }); diff --git a/src/services/mock/mock.service.ts b/src/services/mock/mock.service.ts index b9f7c7eb..7f5edad4 100644 --- a/src/services/mock/mock.service.ts +++ b/src/services/mock/mock.service.ts @@ -1,26 +1,23 @@ export class MockService { - public static getInstance() { + public static getInstance(): MockService { if (!MockService.instance) { MockService.instance = new MockService(); } return MockService.instance; } private static instance: MockService; - private constructor() {} public mock(input: string): string { - let output = ""; + let output = ''; if (!input || input.length === 0) { return input; } else { let shouldChangeCase = true; for (const letter of input) { - if (letter === " ") { + if (letter === ' ') { output += letter; } else { - output += shouldChangeCase - ? letter.toLowerCase() - : letter.toUpperCase(); + output += shouldChangeCase ? letter.toLowerCase() : letter.toUpperCase(); shouldChangeCase = !shouldChangeCase; } } diff --git a/src/services/muzzle/constants.ts b/src/services/muzzle/constants.ts index 9cb016e9..521726cb 100644 --- a/src/services/muzzle/constants.ts +++ b/src/services/muzzle/constants.ts @@ -3,4 +3,4 @@ export const MAX_TIME_BETWEEN_MUZZLES = 3600000; export const MAX_SUPPRESSIONS = 7; export const MAX_MUZZLES = 2; export const ABUSE_PENALTY_TIME = 300000; -export const REPLACEMENT_TEXT = ["..mMm..", "..COUGH.."]; +export const REPLACEMENT_TEXT = ['..mMm..', '..COUGH..']; diff --git a/src/services/muzzle/muzzle-utilities.spec.ts b/src/services/muzzle/muzzle-utilities.spec.ts index cad35977..ef8eee6c 100644 --- a/src/services/muzzle/muzzle-utilities.spec.ts +++ b/src/services/muzzle/muzzle-utilities.spec.ts @@ -1,28 +1,28 @@ -import { getTimeString, getTimeToMuzzle } from "./muzzle-utilities"; +import { getTimeString, getTimeToMuzzle } from './muzzle-utilities'; -describe("muzzle-utilities", () => { - describe("getTimeToMuzzle()", () => { - it("should return a value greater than 0 and less than 180000", () => { +describe('muzzle-utilities', () => { + describe('getTimeToMuzzle()', () => { + it('should return a value greater than 0 and less than 180000', () => { expect(getTimeToMuzzle()).toBeGreaterThan(0); expect(getTimeToMuzzle()).toBeLessThan(180000); }); }); - describe("getTimeString()", () => { - it("should return 1m30s when 90000ms are passed in", () => { - expect(getTimeString(90000)).toBe("1m30s"); + describe('getTimeString()', () => { + it('should return 1m30s when 90000ms are passed in', () => { + expect(getTimeString(90000)).toBe('1m30s'); }); - it("should return 2m00s when 120000ms is passed in", () => { - expect(getTimeString(120000)).toBe("2m00s"); + it('should return 2m00s when 120000ms is passed in', () => { + expect(getTimeString(120000)).toBe('2m00s'); }); - it("should return 2m00s when 120000.123 is passed in", () => { - expect(getTimeString(120000.123)).toBe("2m00s"); + it('should return 2m00s when 120000.123 is passed in', () => { + expect(getTimeString(120000.123)).toBe('2m00s'); }); - it("should return 2m00s when 120000.999 is passed in", () => { - expect(getTimeString(120000.999)).toBe("2m00s"); + it('should return 2m00s when 120000.999 is passed in', () => { + expect(getTimeString(120000.999)).toBe('2m00s'); }); }); }); diff --git a/src/services/muzzle/muzzle-utilities.ts b/src/services/muzzle/muzzle-utilities.ts index ca834698..b3cafd38 100644 --- a/src/services/muzzle/muzzle-utilities.ts +++ b/src/services/muzzle/muzzle-utilities.ts @@ -1,38 +1,35 @@ /** * Gets the amount of time remaining on a NodeJS Timeout. */ -export function getRemainingTime(timeout: any) { - return Math.ceil( - timeout._idleStart + timeout._idleTimeout - process.uptime() * 1000 - ); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getRemainingTime(timeout: any): number { + return Math.ceil(timeout._idleStart + timeout._idleTimeout - process.uptime() * 1000); } /** * Gives us a random value between 30 seconds and 3 minutes. */ -export function getTimeToMuzzle() { +export function getTimeToMuzzle(): number { return Math.floor(Math.random() * (180000 - 30000 + 1) + 30000); } /** * Gives us a time string formatted as 1m20s to show the user. */ -export function getTimeString(time: number) { +export function getTimeString(time: number): string { const minutes = Math.floor(time / 60000); const seconds = ((time % 60000) / 1000).toFixed(0); - return +seconds === 60 - ? minutes + 1 + "m00s" - : minutes + "m" + (+seconds < 10 ? "0" : "") + seconds + "s"; + return +seconds === 60 ? minutes + 1 + 'm00s' : minutes + 'm' + (+seconds < 10 ? '0' : '') + seconds + 's'; } /** * Generates a random number tells us if it is even. */ -export function isRandomEven() { +export function isRandomEven(): boolean { return Math.floor(Math.random() * 2) % 2 === 0; } -export function shouldBackfire() { +export function shouldBackfire(): boolean { const chanceOfBackfire = 0.05; return Math.random() <= chanceOfBackfire; } diff --git a/src/services/muzzle/muzzle.persistence.service.ts b/src/services/muzzle/muzzle.persistence.service.ts index 9ae8fc81..f7d86fd0 100644 --- a/src/services/muzzle/muzzle.persistence.service.ts +++ b/src/services/muzzle/muzzle.persistence.service.ts @@ -1,22 +1,13 @@ -import moment from "moment"; -import { getRepository } from "typeorm"; -import { Muzzle } from "../../shared/db/models/Muzzle"; -import { - IMuzzled, - IReportRange, - IRequestor, - ReportType -} from "../../shared/models/muzzle/muzzle-models"; -import { - ABUSE_PENALTY_TIME, - MAX_MUZZLE_TIME, - MAX_MUZZLES, - MAX_TIME_BETWEEN_MUZZLES -} from "./constants"; -import { getRemainingTime } from "./muzzle-utilities"; +import moment from 'moment'; +import { UpdateResult, getRepository } from 'typeorm'; +import { Muzzle } from '../../shared/db/models/Muzzle'; +import { Muzzled, Requestor } from '../../shared/models/muzzle/muzzle-models'; +import { ABUSE_PENALTY_TIME, MAX_MUZZLES, MAX_MUZZLE_TIME, MAX_TIME_BETWEEN_MUZZLES } from './constants'; +import { getRemainingTime } from './muzzle-utilities'; +import { Accuracy, MuzzleReport, ReportCount, ReportRange, ReportType } from '../../shared/models/report/report.model'; export class MuzzlePersistenceService { - public static getInstance() { + public static getInstance(): MuzzlePersistenceService { if (!MuzzlePersistenceService.instance) { MuzzlePersistenceService.instance = new MuzzlePersistenceService(); } @@ -24,12 +15,10 @@ export class MuzzlePersistenceService { } private static instance: MuzzlePersistenceService; - private muzzled: Map = new Map(); - private requestors: Map = new Map(); + private muzzled: Map = new Map(); + private requestors: Map = new Map(); - private constructor() {} - - public addMuzzle(requestorId: string, muzzledId: string, time: number) { + public addMuzzle(requestorId: string, muzzledId: string, time: number): Promise { return new Promise(async (resolve, reject) => { const muzzle = new Muzzle(); muzzle.requestorId = requestorId; @@ -46,7 +35,7 @@ export class MuzzlePersistenceService { muzzledBy: requestorId, id: muzzleFromDb.id, isCounter: false, - removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time) + removalFn: setTimeout(() => this.removeMuzzle(muzzledId), time), }); this.setRequestorCount(requestorId); resolve(); @@ -55,61 +44,48 @@ export class MuzzlePersistenceService { }); } - public removeMuzzlePrivileges(requestorId: string) { - const requestorObj: IRequestor | undefined = this.requestors.get( - requestorId - ); + public removeMuzzlePrivileges(requestorId: string): void { + const requestorObj: Requestor | undefined = this.requestors.get(requestorId); if (requestorObj) { - clearTimeout(this.requestors.get(requestorId)! - .muzzleCountRemover as NodeJS.Timeout); + clearTimeout(this.requestors.get(requestorId)!.muzzleCountRemover as NodeJS.Timeout); } this.requestors.set(requestorId, { muzzleCount: MAX_MUZZLES, - muzzleCountRemover: setTimeout( - () => this.removeRequestor(requestorId), - MAX_TIME_BETWEEN_MUZZLES - ) + muzzleCountRemover: setTimeout(() => this.removeRequestor(requestorId), MAX_TIME_BETWEEN_MUZZLES), }); } /** * Adds a requestor to the requestors map with a muzzleCount to track how many muzzles have been performed, as well as a removal function. */ - public setRequestorCount(requestorId: string) { - const muzzleCount = this.requestors.has(requestorId) - ? ++this.requestors.get(requestorId)!.muzzleCount - : 1; + public setRequestorCount(requestorId: string): void { + const muzzleCount = this.requestors.has(requestorId) ? ++this.requestors.get(requestorId)!.muzzleCount : 1; if (this.requestors.has(requestorId)) { - clearTimeout(this.requestors.get(requestorId)! - .muzzleCountRemover as NodeJS.Timeout); + clearTimeout(this.requestors.get(requestorId)!.muzzleCountRemover as NodeJS.Timeout); } const removalFunction = - this.requestors.has(requestorId) && - this.requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES - ? () => this.removeRequestor(requestorId) - : () => this.decrementMuzzleCount(requestorId); + this.requestors.has(requestorId) && this.requestors.get(requestorId)!.muzzleCount === MAX_MUZZLES + ? (): void => this.removeRequestor(requestorId) + : (): void => this.decrementMuzzleCount(requestorId); this.requestors.set(requestorId, { muzzleCount, - muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES) + muzzleCountRemover: setTimeout(removalFunction, MAX_TIME_BETWEEN_MUZZLES), }); } /** * Returns boolean whether max muzzles have been reached. */ - public isMaxMuzzlesReached(userId: string) { - return ( - this.requestors.has(userId) && - this.requestors.get(userId)!.muzzleCount === MAX_MUZZLES - ); + public isMaxMuzzlesReached(userId: string): boolean { + return this.requestors.has(userId) && this.requestors.get(userId)!.muzzleCount === MAX_MUZZLES; } /** * Adds the specified amount of time to a specified muzzled user. */ - public addMuzzleTime(userId: string, timeToAdd: number) { + public addMuzzleTime(userId: string, timeToAdd: number): void { if (userId && this.muzzled.has(userId)) { const removalFn = this.muzzled.get(userId)!.removalFn; const newTime = getRemainingTime(removalFn) + timeToAdd; @@ -122,22 +98,22 @@ export class MuzzlePersistenceService { muzzledBy: this.muzzled.get(userId)!.muzzledBy, id: this.muzzled.get(userId)!.id, isCounter: false, - removalFn: setTimeout(() => this.removeMuzzle(userId), newTime) + removalFn: setTimeout(() => this.removeMuzzle(userId), newTime), }); } } - public setMuzzle(userId: string, options: IMuzzled) { + public setMuzzle(userId: string, options: Muzzled): void { this.muzzled.set(userId, options); } - public getMuzzle(userId: string) { + public getMuzzle(userId: string): Muzzled | undefined { return this.muzzled.get(userId); } /** * Gets the corresponding database ID for the user's current muzzle. */ - public getMuzzleId(userId: string) { + public getMuzzleId(userId: string): number | undefined { return this.muzzled.get(userId)!.id; } @@ -148,92 +124,79 @@ export class MuzzlePersistenceService { return this.muzzled.has(userId); } - public incrementMuzzleTime(id: number, ms: number) { - return getRepository(Muzzle).increment({ id }, "milliseconds", ms); + public incrementMuzzleTime(id: number, ms: number): Promise { + return getRepository(Muzzle).increment({ id }, 'milliseconds', ms); } - public incrementMessageSuppressions(id: number) { - return getRepository(Muzzle).increment({ id }, "messagesSuppressed", 1); + public incrementMessageSuppressions(id: number): Promise { + return getRepository(Muzzle).increment({ id }, 'messagesSuppressed', 1); } - public incrementWordSuppressions(id: number, suppressions: number) { - return getRepository(Muzzle).increment( - { id }, - "wordsSuppressed", - suppressions - ); + public incrementWordSuppressions(id: number, suppressions: number): Promise { + return getRepository(Muzzle).increment({ id }, 'wordsSuppressed', suppressions); } - public getRange(reportType: ReportType) { - const range: IReportRange = { - reportType + public getRange(reportType: ReportType): ReportRange { + const range: ReportRange = { + reportType, }; if (reportType === ReportType.AllTime) { range.reportType = ReportType.AllTime; } else if (reportType === ReportType.Week) { range.start = moment() - .startOf("week") - .subtract(1, "week") - .format("YYYY-MM-DD HH:mm:ss"); + .startOf('week') + .subtract(1, 'week') + .format('YYYY-MM-DD HH:mm:ss'); range.end = moment() - .endOf("week") - .subtract(1, "week") - .format("YYYY-MM-DD HH:mm:ss"); + .endOf('week') + .subtract(1, 'week') + .format('YYYY-MM-DD HH:mm:ss'); } else if (reportType === ReportType.Month) { range.start = moment() - .startOf("month") - .subtract(1, "month") - .format("YYYY-MM-DD HH:mm:ss"); + .startOf('month') + .subtract(1, 'month') + .format('YYYY-MM-DD HH:mm:ss'); range.end = moment() - .endOf("month") - .subtract(1, "month") - .format("YYYY-MM-DD HH:mm:ss"); + .endOf('month') + .subtract(1, 'month') + .format('YYYY-MM-DD HH:mm:ss'); } else if (reportType === ReportType.Trailing30) { range.start = moment() - .startOf("day") - .subtract(30, "days") - .format("YYYY-MM-DD HH:mm:ss"); - range.end = moment().format("YYYY-MM-DD HH:mm:ss"); + .startOf('day') + .subtract(30, 'days') + .format('YYYY-MM-DD HH:mm:ss'); + range.end = moment().format('YYYY-MM-DD HH:mm:ss'); } else if (reportType === ReportType.Year) { range.start = moment() - .startOf("year") - .format("YYYY-MM-DD HH:mm:ss"); + .startOf('year') + .format('YYYY-MM-DD HH:mm:ss'); range.end = moment() - .endOf("year") - .format("YYYY-MM-DD HH:mm:ss"); + .endOf('year') + .format('YYYY-MM-DD HH:mm:ss'); } return range; } - public incrementCharacterSuppressions( - id: number, - charactersSuppressed: number - ) { - return getRepository(Muzzle).increment( - { id }, - "charactersSuppressed", - charactersSuppressed - ); + public incrementCharacterSuppressions(id: number, charactersSuppressed: number): Promise { + return getRepository(Muzzle).increment({ id }, 'charactersSuppressed', charactersSuppressed); } /** * Determines suppression counts for messages that are ONLY deleted and not muzzled. * Used when a muzzled user has hit their max suppressions or when they have tagged channel. */ - public trackDeletedMessage(muzzleId: number, text: string) { - const words = text.split(" ").length; - const characters = text.split("").length; + public trackDeletedMessage(muzzleId: number, text: string): void { + const words = text.split(' ').length; + const characters = text.split('').length; this.incrementMessageSuppressions(muzzleId); this.incrementWordSuppressions(muzzleId, words); this.incrementCharacterSuppressions(muzzleId, characters); } /** Wrapper to generate a generic muzzle report in */ - public async retrieveMuzzleReport( - reportType: ReportType = ReportType.AllTime - ) { - const range: IReportRange = this.getRange(reportType); + public async retrieveMuzzleReport(reportType: ReportType = ReportType.AllTime): Promise { + const range: ReportRange = this.getRange(reportType); const mostMuzzledByInstances = await this.getMostMuzzledByInstances(range); const mostMuzzledByMessages = await this.getMostMuzzledByMessages(range); @@ -259,36 +222,36 @@ export class MuzzlePersistenceService { byMessages: mostMuzzledByMessages, byWords: mostMuzzledByWords, byChars: mostMuzzledByChars, - byTime: mostMuzzledByTime + byTime: mostMuzzledByTime, }, muzzlers: { byInstances: muzzlerByInstances, byMessages: muzzlerByMessages, byWords: muzzlerByWords, byChars: muzzlerByChars, - byTime: muzzlerByTime + byTime: muzzlerByTime, }, accuracy, kdr, rawNemesis, - successNemesis + successNemesis, }; } /** * Removes a requestor from the map. */ - private removeRequestor(userId: string) { + private removeRequestor(userId: string): void { this.requestors.delete(userId); console.log( - `${MAX_MUZZLE_TIME} has passed since ${userId} last successful muzzle. They have been removed from requestors.` + `${MAX_MUZZLE_TIME} has passed since ${userId} last successful muzzle. They have been removed from requestors.`, ); } /** * Removes a muzzle from the specified user. */ - private removeMuzzle(userId: string) { + private removeMuzzle(userId: string): void { this.muzzled.delete(userId); console.log(`Removed ${userId}'s muzzle! He is free at last.`); } @@ -296,168 +259,142 @@ export class MuzzlePersistenceService { /** * Decrements the muzzleCount on a requestor. */ - private decrementMuzzleCount(requestorId: string) { + private decrementMuzzleCount(requestorId: string): void { if (this.requestors.has(requestorId)) { const decrementedMuzzle = --this.requestors.get(requestorId)!.muzzleCount; this.requestors.set(requestorId, { muzzleCount: decrementedMuzzle, - muzzleCountRemover: this.requestors.get(requestorId)!.muzzleCountRemover + muzzleCountRemover: this.requestors.get(requestorId)!.muzzleCountRemover, }); - console.log( - `Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}` - ); + console.log(`Successfully decremented ${requestorId} muzzleCount to ${decrementedMuzzle}`); } else { - console.error( - `Attemped to decrement muzzle count for ${requestorId} but they did not exist!` - ); + console.error(`Attemped to decrement muzzle count for ${requestorId} but they did not exist!`); } } - private getMostMuzzledByInstances(range: IReportRange) { + private getMostMuzzledByInstances(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT muzzledId, COUNT(*) as count FROM muzzle GROUP BY muzzledId ORDER BY count DESC;` - : `SELECT muzzledId, COUNT(*) as count FROM muzzle WHERE createdAt >= '${ + ? `SELECT muzzledId as slackId, COUNT(*) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT muzzledId as slackId, COUNT(*) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY muzzledId ORDER BY count DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMuzzlerByInstances(range: IReportRange) { + private getMuzzlerByInstances(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT requestorId, COUNT(*) as instanceCount FROM muzzle GROUP BY requestorId ORDER BY instanceCount DESC;` - : `SELECT requestorId, COUNT(*) as instanceCount FROM muzzle WHERE createdAt >= '${ + ? `SELECT requestorId as slackId, COUNT(*) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT requestorId as slackId, COUNT(*) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY instanceCount DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMuzzlerByMessages(range: IReportRange) { + private getMuzzlerByMessages(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT requestorId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle GROUP BY requestorId ORDER BY messagesSuppressed DESC;` - : `SELECT requestorId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT requestorId as slackId, SUM(messagesSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT requestorId as slackId, SUM(messagesSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY messagesSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMostMuzzledByMessages(range: IReportRange) { + private getMostMuzzledByMessages(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT muzzledId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle GROUP BY muzzledId ORDER BY messagesSuppressed DESC;` - : `SELECT muzzledId, SUM(messagesSuppressed) as messagesSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT muzzledId as slackId, SUM(messagesSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT muzzledId as slackId, SUM(messagesSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY muzzledId ORDER BY messagesSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMostMuzzledByWords(range: IReportRange) { + private getMostMuzzledByWords(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT muzzledId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle GROUP BY muzzledId ORDER BY wordsSuppressed DESC;` - : `SELECT muzzledId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT muzzledId as slackId, SUM(wordsSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT muzzledId as slackId, SUM(wordsSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY muzzledId ORDER BY wordsSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMuzzlerByWords(range: IReportRange) { + private getMuzzlerByWords(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT requestorId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle GROUP BY requestorId ORDER BY wordsSuppressed DESC;` - : `SELECT requestorId, SUM(wordsSuppressed) as wordsSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT requestorId as slackId, SUM(wordsSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT requestorId as slackId, SUM(wordsSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY wordsSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMostMuzzledByChars(range: IReportRange) { + private getMostMuzzledByChars(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT muzzledId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle GROUP BY muzzledId ORDER BY charactersSuppressed DESC;` - : `SELECT muzzledId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT muzzledId as slackId, SUM(charactersSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT muzzledId as slackId, SUM(charactersSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY muzzledId ORDER BY charactersSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMuzzlerByChars(range: IReportRange) { + private getMuzzlerByChars(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT requestorId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle GROUP BY requestorId ORDER BY charactersSuppressed DESC;` - : `SELECT requestorId, SUM(charactersSuppressed) as charactersSuppressed FROM muzzle WHERE createdAt >= '${ + ? `SELECT requestorId as slackId, SUM(charactersSuppressed) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT requestorId as slackId, SUM(charactersSuppressed) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY charactersSuppressed DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMostMuzzledByTime(range: IReportRange) { + private getMostMuzzledByTime(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT muzzledId, SUM(milliseconds) as muzzleTime FROM muzzle GROUP BY muzzledId ORDER BY muzzleTime DESC;` - : `SELECT muzzledId, SUM(milliseconds) as muzzleTime FROM muzzle WHERE createdAt >= '${ + ? `SELECT muzzledId as slackId, SUM(milliseconds) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT muzzledId as slackId, SUM(milliseconds) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY muzzledId ORDER BY muzzleTime DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getMuzzlerByTime(range: IReportRange) { + private getMuzzlerByTime(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime - ? `SELECT requestorId, SUM(milliseconds) as muzzleTime FROM muzzle GROUP BY requestorId ORDER BY muzzleTime DESC;` - : `SELECT requestorId, SUM(milliseconds) as muzzleTime FROM muzzle WHERE createdAt >= '${ + ? `SELECT requestorId as slackId, SUM(milliseconds) as count FROM muzzle GROUP BY slackId ORDER BY count DESC;` + : `SELECT requestorId as slackId, SUM(milliseconds) as count FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY muzzleTime DESC;`; + }' AND createdAt < '${range.end}' GROUP BY slackId ORDER BY count DESC;`; return getRepository(Muzzle).query(query); } - private getAccuracy(range: IReportRange) { + private getAccuracy(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime ? `SELECT requestorId, SUM(IF(messagesSuppressed > 0, 1, 0))/COUNT(*) as accuracy, SUM(IF(muzzle.messagesSuppressed > 0, 1, 0)) as kills, COUNT(*) as deaths FROM muzzle GROUP BY requestorId ORDER BY accuracy DESC;` : `SELECT requestorId, SUM(IF(messagesSuppressed > 0, 1, 0))/COUNT(*) as accuracy, SUM(IF(muzzle.messagesSuppressed > 0, 1, 0)) as kills, COUNT(*) as deaths FROM muzzle WHERE createdAt >= '${ range.start - }' AND createdAt < '${ - range.end - }' GROUP BY requestorId ORDER BY accuracy DESC;`; + }' AND createdAt < '${range.end}' GROUP BY requestorId ORDER BY accuracy DESC;`; return getRepository(Muzzle).query(query); } - private getKdr(range: IReportRange) { + private getKdr(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime ? ` @@ -481,9 +418,7 @@ export class MuzzlePersistenceService { RIGHT JOIN ( SELECT requestorId, COUNT(*) as count FROM muzzle - WHERE messagesSuppressed > 0 AND createdAt >= '${ - range.start - }' AND createdAt <= '${range.end}' + WHERE messagesSuppressed > 0 AND createdAt >= '${range.start}' AND createdAt <= '${range.end}' GROUP BY requestorId ) AS b ON a.muzzledId = b.requestorId @@ -494,7 +429,7 @@ export class MuzzlePersistenceService { return getRepository(Muzzle).query(query); } - private getNemesisByRaw(range: IReportRange) { + private getNemesisByRaw(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime ? ` @@ -541,7 +476,7 @@ export class MuzzlePersistenceService { return getRepository(Muzzle).query(query); } - private getNemesisBySuccessful(range: IReportRange) { + private getNemesisBySuccessful(range: ReportRange): Promise { const query = range.reportType === ReportType.AllTime ? ` @@ -570,9 +505,7 @@ export class MuzzlePersistenceService { FROM ( SELECT requestorId, muzzledId, COUNT(*) as count FROM muzzle - WHERE createdAt >= '${range.start}' AND createdAt < '${ - range.end - }' AND messagesSuppressed > 0 + WHERE createdAt >= '${range.start}' AND createdAt < '${range.end}' AND messagesSuppressed > 0 GROUP BY requestorId, muzzledId ) AS a INNER JOIN( @@ -580,9 +513,7 @@ export class MuzzlePersistenceService { FROM ( SELECT requestorId, muzzledId, COUNT(*) AS count FROM muzzle - WHERE createdAt >= '${range.start}' AND createdAt < '${ - range.end - }' AND messagesSuppressed > 0 + WHERE createdAt >= '${range.start}' AND createdAt < '${range.end}' AND messagesSuppressed > 0 GROUP BY requestorId, muzzledId ) AS c GROUP BY c.muzzledId diff --git a/src/services/muzzle/muzzle.service.spec.ts b/src/services/muzzle/muzzle.service.spec.ts index aa180654..52da1103 100644 --- a/src/services/muzzle/muzzle.service.spec.ts +++ b/src/services/muzzle/muzzle.service.spec.ts @@ -1,24 +1,22 @@ -import { when } from "jest-when"; -import { UpdateResult } from "typeorm"; -import { Muzzle } from "../../shared/db/models/Muzzle"; -import { IMuzzled } from "../../shared/models/muzzle/muzzle-models"; -import { - IEventRequest, - ISlackUser -} from "../../shared/models/slack/slack-models"; -import { SlackService } from "../slack/slack.service"; -import { WebService } from "../web/web.service"; -import { MAX_SUPPRESSIONS } from "./constants"; -import * as muzzleUtils from "./muzzle-utilities"; -import { MuzzlePersistenceService } from "./muzzle.persistence.service"; -import { MuzzleService } from "./muzzle.service"; - -describe("MuzzleService", () => { +/* eslint-disable @typescript-eslint/camelcase */ +import { when } from 'jest-when'; +import { UpdateResult } from 'typeorm'; +import { Muzzle } from '../../shared/db/models/Muzzle'; +import { Muzzled } from '../../shared/models/muzzle/muzzle-models'; +import { EventRequest, SlackUser } from '../../shared/models/slack/slack-models'; +import { SlackService } from '../slack/slack.service'; +import { WebService } from '../web/web.service'; +import { MAX_SUPPRESSIONS } from './constants'; +import * as muzzleUtils from './muzzle-utilities'; +import { MuzzlePersistenceService } from './muzzle.persistence.service'; +import { MuzzleService } from './muzzle.service'; + +describe('MuzzleService', () => { const testData = { - user123: "123", - user2: "456", - user3: "789", - requestor: "666" + user123: '123', + user2: '456', + user3: '789', + requestor: '666', }; let muzzleService: MuzzleService; @@ -28,11 +26,11 @@ describe("MuzzleService", () => { muzzleService = new MuzzleService(); slackInstance = SlackService.getInstance(); slackInstance.userList = [ - { id: "123", name: "test123" }, - { id: "456", name: "test456" }, - { id: "789", name: "test789" }, - { id: "666", name: "requestor" } - ] as ISlackUser[]; + { id: '123', name: 'test123' }, + { id: '456', name: 'test456' }, + { id: '789', name: 'test789' }, + { id: '666', name: 'requestor' }, + ] as SlackUser[]; jest.useFakeTimers(); }); @@ -40,295 +38,229 @@ describe("MuzzleService", () => { jest.runAllTimers(); }); - describe("muzzle()", () => { + describe('muzzle()', () => { beforeEach(() => { - const mockResolve = { raw: "whatever" }; + const mockResolve = { raw: 'whatever' }; jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementMessageSuppressions" - ) + .spyOn(MuzzlePersistenceService.getInstance(), 'incrementMessageSuppressions') .mockResolvedValue(mockResolve as UpdateResult); jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementCharacterSuppressions" - ) + .spyOn(MuzzlePersistenceService.getInstance(), 'incrementCharacterSuppressions') .mockResolvedValue(mockResolve as UpdateResult); jest - .spyOn( - MuzzlePersistenceService.getInstance(), - "incrementWordSuppressions" - ) + .spyOn(MuzzlePersistenceService.getInstance(), 'incrementWordSuppressions') .mockResolvedValue(mockResolve as UpdateResult); }); - it("should always muzzle a tagged user", () => { - const testSentence = - "<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>"; + it('should always muzzle a tagged user', () => { + jest.spyOn(muzzleService, 'getReplacementWord').mockReturnValue('..mMm..'); + const testSentence = '<@U2TKJ> <@JKDSF> <@SDGJSK> <@LSKJDSG> <@lkjdsa> <@LKSJDF> <@SDLJG> <@jrjrjr> <@fudka>'; expect(muzzleService.muzzle(testSentence, 1)).toBe( - "..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.." + '..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm.. ..mMm..', ); }); - it("should always muzzle ", () => { - const testSentence = ""; - expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); + it('should always muzzle ', () => { + const testSentence = ''; + expect(muzzleService.muzzle(testSentence, 1)).toBe('..mMm..'); }); - it("should always muzzle ", () => { - const testSentence = ""; - expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); + it('should always muzzle ', () => { + const testSentence = ''; + expect(muzzleService.muzzle(testSentence, 1)).toBe('..mMm..'); }); - it("should always muzzle a word with length > 10", () => { - const testSentence = "this.is.a.way.to.game.the.system"; - expect(muzzleService.muzzle(testSentence, 1)).toBe("..mMm.."); + it('should always muzzle a word with length > 10', () => { + const testSentence = 'this.is.a.way.to.game.the.system'; + expect(muzzleService.muzzle(testSentence, 1)).toBe('..mMm..'); }); }); - describe("shouldBotMessageBeMuzzled()", () => { - let mockRequest: IEventRequest; + describe('shouldBotMessageBeMuzzled()', () => { + let mockRequest: EventRequest; beforeEach(() => { /* tslint:disable-next-line:no-object-literal-type-assertion */ mockRequest = { event: { - subtype: "bot_message", - username: "not_muzzle", - text: "<@123>", + subtype: 'bot_message', + username: 'not_muzzle', + text: '<@123>', attachments: [ { - callback_id: "LKJSF_123", - pretext: "<@123>", - text: "<@123>" - } - ] - } - } as IEventRequest; + callback_id: 'LKJSF_123', + pretext: '<@123>', + text: '<@123>', + }, + ], + }, + } as EventRequest; }); - describe("when a user is muzzled", () => { + describe('when a user is muzzled', () => { beforeEach(() => { - jest - .spyOn(MuzzlePersistenceService.getInstance(), "isUserMuzzled") - .mockImplementation(() => true); + jest.spyOn(MuzzlePersistenceService.getInstance(), 'isUserMuzzled').mockImplementation(() => true); }); - it("should return true if an id is present in the event.text ", () => { + it('should return true if an id is present in the event.text ', () => { mockRequest.event.attachments = []; expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); - it("should return true if an id is present in the event.attachments[0].text", () => { - mockRequest.event.text = "whatever"; - mockRequest.event.attachments[0].pretext = "whatever"; - mockRequest.event.attachments[0].callback_id = "whatever"; + it('should return true if an id is present in the event.attachments[0].text', () => { + mockRequest.event.text = 'whatever'; + mockRequest.event.attachments[0].pretext = 'whatever'; + mockRequest.event.attachments[0].callback_id = 'whatever'; expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); - it("should return true if an id is present in the event.attachments[0].pretext", () => { - mockRequest.event.text = "whatever"; - mockRequest.event.attachments[0].text = "whatever"; - mockRequest.event.attachments[0].callback_id = "whatever"; + it('should return true if an id is present in the event.attachments[0].pretext', () => { + mockRequest.event.text = 'whatever'; + mockRequest.event.attachments[0].text = 'whatever'; + mockRequest.event.attachments[0].callback_id = 'whatever'; expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); - it("should return the id present in the event.attachments[0].callback_id if an id is present", () => { - mockRequest.event.text = "whatever"; - mockRequest.event.attachments[0].text = "whatever"; - mockRequest.event.attachments[0].pretext = "whatever"; + it('should return the id present in the event.attachments[0].callback_id if an id is present', () => { + mockRequest.event.text = 'whatever'; + mockRequest.event.attachments[0].text = 'whatever'; + mockRequest.event.attachments[0].pretext = 'whatever'; expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(true); }); }); - describe("when a user is not muzzled", () => { + describe('when a user is not muzzled', () => { beforeEach(() => { - jest - .spyOn(MuzzlePersistenceService.getInstance(), "isUserMuzzled") - .mockImplementation(() => false); + jest.spyOn(MuzzlePersistenceService.getInstance(), 'isUserMuzzled').mockImplementation(() => false); }); - it("should return false if there is no id present in any fields", () => { - mockRequest.event.text = "no id"; - mockRequest.event.callback_id = "TEST_TEST"; - mockRequest.event.attachments[0].text = "test"; - mockRequest.event.attachments[0].pretext = "test"; - mockRequest.event.attachments[0].callback_id = "TEST"; - expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( - false - ); + it('should return false if there is no id present in any fields', () => { + mockRequest.event.text = 'no id'; + mockRequest.event.callback_id = 'TEST_TEST'; + mockRequest.event.attachments[0].text = 'test'; + mockRequest.event.attachments[0].pretext = 'test'; + mockRequest.event.attachments[0].callback_id = 'TEST'; + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(false); }); - it("should return false if the message is not a bot_message", () => { - mockRequest.event.subtype = "not_bot_message"; - expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( - false - ); + it('should return false if the message is not a bot_message', () => { + mockRequest.event.subtype = 'not_bot_message'; + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(false); }); - it("should return false if the requesting user is not muzzled", () => { - mockRequest.event.text = "<@456>"; - mockRequest.event.attachments[0].text = "<@456>"; - mockRequest.event.attachments[0].pretext = "<@456>"; - mockRequest.event.attachments[0].callback_id = "TEST_456"; - expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( - false - ); + it('should return false if the requesting user is not muzzled', () => { + mockRequest.event.text = '<@456>'; + mockRequest.event.attachments[0].text = '<@456>'; + mockRequest.event.attachments[0].pretext = '<@456>'; + mockRequest.event.attachments[0].callback_id = 'TEST_456'; + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(false); }); - it("should return false if the bot username is muzzle", () => { - mockRequest.event.username = "muzzle"; - expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe( - false - ); + it('should return false if the bot username is muzzle', () => { + mockRequest.event.username = 'muzzle'; + expect(muzzleService.shouldBotMessageBeMuzzled(mockRequest)).toBe(false); }); }); }); - describe("addUserToMuzzled()", () => { - describe("muzzled", () => { - describe("when the user is not already muzzled", () => { + describe('addUserToMuzzled()', () => { + describe('muzzled', () => { + describe('when the user is not already muzzled', () => { let mockAddMuzzle: jest.SpyInstance; beforeEach(() => { const mockMuzzle = { id: 1 }; const persistenceService = MuzzlePersistenceService.getInstance(); - mockAddMuzzle = jest - .spyOn(persistenceService, "addMuzzle") - .mockResolvedValue(mockMuzzle as Muzzle); + mockAddMuzzle = jest.spyOn(persistenceService, 'addMuzzle').mockResolvedValue(mockMuzzle as Muzzle); - jest - .spyOn(persistenceService, "isUserMuzzled") - .mockImplementation(() => false); + jest.spyOn(persistenceService, 'isUserMuzzled').mockImplementation(() => false); - jest - .spyOn(muzzleUtils, "shouldBackfire") - .mockImplementation(() => false); + jest.spyOn(muzzleUtils, 'shouldBackfire').mockImplementation(() => false); }); - it("should call MuzzlePersistenceService.addMuzzle()", async () => { - await muzzleService.addUserToMuzzled( - testData.user123, - testData.requestor, - "test" - ); + it('should call MuzzlePersistenceService.addMuzzle()', async () => { + await muzzleService.addUserToMuzzled(testData.user123, testData.requestor, 'test'); expect(mockAddMuzzle).toHaveBeenCalled(); }); }); - describe("when a user is already muzzled", () => { + describe('when a user is already muzzled', () => { let addMuzzleMock: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); const mockMuzzle = { id: 1 }; const persistenceService = MuzzlePersistenceService.getInstance(); - addMuzzleMock = jest - .spyOn(persistenceService, "addMuzzle") - .mockResolvedValue(mockMuzzle as Muzzle); + addMuzzleMock = jest.spyOn(persistenceService, 'addMuzzle').mockResolvedValue(mockMuzzle as Muzzle); - jest - .spyOn(muzzleUtils, "shouldBackfire") - .mockImplementation(() => false); + jest.spyOn(muzzleUtils, 'shouldBackfire').mockImplementation(() => false); - jest - .spyOn(persistenceService, "isUserMuzzled") - .mockImplementation(() => true); + jest.spyOn(persistenceService, 'isUserMuzzled').mockImplementation(() => true); }); - it("should reject if a user tries to muzzle an already muzzled user", async () => { - await muzzleService - .addUserToMuzzled(testData.user123, testData.requestor, "test") - .catch(e => { - expect(e).toBe("test123 is already muzzled!"); - expect(addMuzzleMock).not.toHaveBeenCalled(); - }); + it('should reject if a user tries to muzzle an already muzzled user', async () => { + await muzzleService.addUserToMuzzled(testData.user123, testData.requestor, 'test').catch(e => { + expect(e).toBe('test123 is already muzzled!'); + expect(addMuzzleMock).not.toHaveBeenCalled(); + }); }); - it("should reject if a user tries to muzzle a user that does not exist", async () => { - await muzzleService - .addUserToMuzzled("", testData.requestor, "test") - .catch(e => { - expect(e).toBe( - `Invalid username passed in. You can only muzzle existing slack users.` - ); - }); + it('should reject if a user tries to muzzle a user that does not exist', async () => { + await muzzleService.addUserToMuzzled('', testData.requestor, 'test').catch(e => { + expect(e).toBe(`Invalid username passed in. You can only muzzle existing slack users.`); + }); }); }); - describe("when a requestor is already muzzled", () => { + describe('when a requestor is already muzzled', () => { let addMuzzleMock: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); const mockMuzzle = { id: 1 }; const persistenceService = MuzzlePersistenceService.getInstance(); - addMuzzleMock = jest - .spyOn(persistenceService, "addMuzzle") - .mockResolvedValue(mockMuzzle as Muzzle); + addMuzzleMock = jest.spyOn(persistenceService, 'addMuzzle').mockResolvedValue(mockMuzzle as Muzzle); - jest - .spyOn(muzzleUtils, "shouldBackfire") - .mockImplementation(() => false); + jest.spyOn(muzzleUtils, 'shouldBackfire').mockImplementation(() => false); - const mockIsUserMuzzled = jest.spyOn( - persistenceService, - "isUserMuzzled" - ); + const mockIsUserMuzzled = jest.spyOn(persistenceService, 'isUserMuzzled'); when(mockIsUserMuzzled) .calledWith(testData.requestor) .mockImplementation(() => true); }); - it("should reject if a requestor tries to muzzle someone while the requestor is muzzled", async () => { - await muzzleService - .addUserToMuzzled(testData.user123, testData.requestor, "test") - .catch(e => { - expect(e).toBe( - `You can't muzzle someone if you are already muzzled!` - ); - expect(addMuzzleMock).not.toHaveBeenCalled(); - }); + it('should reject if a requestor tries to muzzle someone while the requestor is muzzled', async () => { + await muzzleService.addUserToMuzzled(testData.user123, testData.requestor, 'test').catch(e => { + expect(e).toBe(`You can't muzzle someone if you are already muzzled!`); + expect(addMuzzleMock).not.toHaveBeenCalled(); + }); }); }); }); - describe("maxMuzzleLimit", () => { + describe('maxMuzzleLimit', () => { beforeEach(() => { const mockMuzzle = { id: 1 }; const persistenceService = MuzzlePersistenceService.getInstance(); - jest - .spyOn(persistenceService, "addMuzzle") - .mockResolvedValue(mockMuzzle as Muzzle); + jest.spyOn(persistenceService, 'addMuzzle').mockResolvedValue(mockMuzzle as Muzzle); - jest - .spyOn(muzzleUtils, "shouldBackfire") - .mockImplementation(() => false); + jest.spyOn(muzzleUtils, 'shouldBackfire').mockImplementation(() => false); - jest - .spyOn(persistenceService, "isMaxMuzzlesReached") - .mockImplementation(() => true); + jest.spyOn(persistenceService, 'isMaxMuzzlesReached').mockImplementation(() => true); - jest - .spyOn(persistenceService, "isUserMuzzled") - .mockImplementation(() => false); + jest.spyOn(persistenceService, 'isUserMuzzled').mockImplementation(() => false); }); - it("should prevent a requestor from muzzling when isMaxMuzzlesReached is true", async () => { + it('should prevent a requestor from muzzling when isMaxMuzzlesReached is true', async () => { await muzzleService - .addUserToMuzzled(testData.user3, testData.requestor, "test") - .catch(e => - expect(e).toBe( - `You're doing that too much. Only 2 muzzles are allowed per hour.` - ) - ); + .addUserToMuzzled(testData.user3, testData.requestor, 'test') + .catch(e => expect(e).toBe(`You're doing that too much. Only 2 muzzles are allowed per hour.`)); }); }); }); - describe("sendMuzzledMessage", () => { + describe('sendMuzzledMessage', () => { let persistenceService: MuzzlePersistenceService; let webService: WebService; @@ -337,8 +269,8 @@ describe("MuzzleService", () => { webService = WebService.getInstance(); }); - describe("if a user is already muzzled", () => { - let mockMuzzle: IMuzzled; + describe('if a user is already muzzled', () => { + let mockMuzzle: Muzzled; let mockSetMuzzle: jest.SpyInstance; let mockSendMessage: jest.SpyInstance; let mockTrackDeleted: jest.SpyInstance; @@ -347,40 +279,35 @@ describe("MuzzleService", () => { jest.clearAllMocks(); mockMuzzle = { suppressionCount: 0, - muzzledBy: "test", + muzzledBy: 'test', id: 1234, isCounter: false, - removalFn: setTimeout(() => 1234, 5000) + removalFn: setTimeout(() => 1234, 5000), }; - mockSetMuzzle = jest.spyOn(persistenceService, "setMuzzle"); - mockSendMessage = jest - .spyOn(webService, "sendMessage") - .mockImplementation(() => true); - mockTrackDeleted = jest.spyOn( - persistenceService, - "trackDeletedMessage" - ); + mockSetMuzzle = jest.spyOn(persistenceService, 'setMuzzle'); + mockSendMessage = jest.spyOn(webService, 'sendMessage').mockImplementation(() => true); + mockTrackDeleted = jest.spyOn(persistenceService, 'trackDeletedMessage'); - jest.spyOn(persistenceService, "getMuzzle").mockReturnValue(mockMuzzle); + jest.spyOn(persistenceService, 'getMuzzle').mockReturnValue(mockMuzzle); }); - it("should call muzzlePersistenceService.setMuzzle and webService.sendMessage if suppressionCount is 0", () => { - muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + it('should call muzzlePersistenceService.setMuzzle and webService.sendMessage if suppressionCount is 0', () => { + muzzleService.sendMuzzledMessage('test', 'test', 'test', 'test'); expect(mockSetMuzzle).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalled(); }); - it("should not call setMuzzle, not call sendMessage, but call trackDeletedMessage if suppressionCount >= MAX_SUPPRESSIONS", () => { + it('should not call setMuzzle, not call sendMessage, but call trackDeletedMessage if suppressionCount >= MAX_SUPPRESSIONS', () => { mockMuzzle.suppressionCount = MAX_SUPPRESSIONS; - muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + muzzleService.sendMuzzledMessage('test', 'test', 'test', 'test'); expect(mockSetMuzzle).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); expect(mockTrackDeleted).toHaveBeenCalled(); }); }); - describe("if a user is not muzzled", () => { + describe('if a user is not muzzled', () => { let mockSetMuzzle: jest.SpyInstance; let mockSendMessage: jest.SpyInstance; let mockTrackDeleted: jest.SpyInstance; @@ -388,21 +315,14 @@ describe("MuzzleService", () => { beforeEach(() => { jest.clearAllMocks(); - mockSetMuzzle = jest.spyOn(persistenceService, "setMuzzle"); - mockSendMessage = jest - .spyOn(webService, "sendMessage") - .mockImplementation(() => true); - mockTrackDeleted = jest.spyOn( - persistenceService, - "trackDeletedMessage" - ); - - mockGetMuzzle = jest - .spyOn(persistenceService, "getMuzzle") - .mockReturnValue(undefined); + mockSetMuzzle = jest.spyOn(persistenceService, 'setMuzzle'); + mockSendMessage = jest.spyOn(webService, 'sendMessage').mockImplementation(() => true); + mockTrackDeleted = jest.spyOn(persistenceService, 'trackDeletedMessage'); + + mockGetMuzzle = jest.spyOn(persistenceService, 'getMuzzle').mockReturnValue(undefined); }); - it("should not call any methods except getMuzzle", () => { - muzzleService.sendMuzzledMessage("test", "test", "test", "test"); + it('should not call any methods except getMuzzle', () => { + muzzleService.sendMuzzledMessage('test', 'test', 'test', 'test'); expect(mockGetMuzzle).toHaveBeenCalled(); expect(mockSetMuzzle).not.toHaveBeenCalled(); expect(mockSendMessage).not.toHaveBeenCalled(); diff --git a/src/services/muzzle/muzzle.service.ts b/src/services/muzzle/muzzle.service.ts index 708d48f9..a19cbf44 100644 --- a/src/services/muzzle/muzzle.service.ts +++ b/src/services/muzzle/muzzle.service.ts @@ -1,18 +1,13 @@ -import { IMuzzled } from "../../shared/models/muzzle/muzzle-models"; -import { IEventRequest } from "../../shared/models/slack/slack-models"; -import { BackFirePersistenceService } from "../backfire/backfire.persistence.service"; -import { CounterPersistenceService } from "../counter/counter.persistence.service"; -import { CounterService } from "../counter/counter.service"; -import { SlackService } from "../slack/slack.service"; -import { WebService } from "../web/web.service"; -import { MAX_MUZZLES, MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from "./constants"; -import { - getTimeString, - getTimeToMuzzle, - isRandomEven, - shouldBackfire -} from "./muzzle-utilities"; -import { MuzzlePersistenceService } from "./muzzle.persistence.service"; +import { Muzzled } from '../../shared/models/muzzle/muzzle-models'; +import { EventRequest } from '../../shared/models/slack/slack-models'; +import { BackFirePersistenceService } from '../backfire/backfire.persistence.service'; +import { CounterPersistenceService } from '../counter/counter.persistence.service'; +import { CounterService } from '../counter/counter.service'; +import { SlackService } from '../slack/slack.service'; +import { WebService } from '../web/web.service'; +import { MAX_MUZZLES, MAX_SUPPRESSIONS, REPLACEMENT_TEXT } from './constants'; +import { getTimeString, getTimeToMuzzle, isRandomEven, shouldBackfire } from './muzzle-utilities'; +import { MuzzlePersistenceService } from './muzzle.persistence.service'; export class MuzzleService { private webService = WebService.getInstance(); @@ -25,10 +20,10 @@ export class MuzzleService { /** * Takes in text and randomly muzzles certain words. */ - public muzzle(text: string, muzzleId: number) { - const words = text.split(" "); + public muzzle(text: string, muzzleId: number): string { + const words = text.split(' '); - let returnText = ""; + let returnText = ''; let wordsSuppressed = 0; let charactersSuppressed = 0; let replacementWord; @@ -38,34 +33,24 @@ export class MuzzleService { words[i], i === 0, i === words.length - 1, - REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] + REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)], ); - if ( - replacementWord.includes( - REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)] - ) - ) { + if (replacementWord.includes(REPLACEMENT_TEXT[Math.floor(Math.random() * REPLACEMENT_TEXT.length)])) { wordsSuppressed++; charactersSuppressed += words[i].length; } returnText += replacementWord; } this.muzzlePersistenceService.incrementMessageSuppressions(muzzleId); - this.muzzlePersistenceService.incrementCharacterSuppressions( - muzzleId, - charactersSuppressed - ); - this.muzzlePersistenceService.incrementWordSuppressions( - muzzleId, - wordsSuppressed - ); + this.muzzlePersistenceService.incrementCharacterSuppressions(muzzleId, charactersSuppressed); + this.muzzlePersistenceService.incrementWordSuppressions(muzzleId, wordsSuppressed); return returnText; } /** * Determines whether or not a bot message should be removed. */ - public shouldBotMessageBeMuzzled(request: IEventRequest) { + public shouldBotMessageBeMuzzled(request: EventRequest): boolean { let userIdByEventText; let userIdByAttachmentText; let userIdByAttachmentPretext; @@ -76,17 +61,11 @@ export class MuzzleService { } if (request.event.attachments && request.event.attachments.length) { - userIdByAttachmentText = this.slackService.getUserId( - request.event.attachments[0].text - ); - userIdByAttachmentPretext = this.slackService.getUserId( - request.event.attachments[0].pretext - ); + userIdByAttachmentText = this.slackService.getUserId(request.event.attachments[0].text); + userIdByAttachmentPretext = this.slackService.getUserId(request.event.attachments[0].pretext); if (request.event.attachments[0].callback_id) { - userIdByCallbackId = this.slackService.getUserIdByCallbackId( - request.event.attachments[0].callback_id - ); + userIdByCallbackId = this.slackService.getUserIdByCallbackId(request.event.attachments[0].callback_id); } } @@ -94,67 +73,50 @@ export class MuzzleService { userIdByEventText, userIdByAttachmentText, userIdByAttachmentPretext, - userIdByCallbackId + userIdByCallbackId, ); return !!( - request.event.subtype === "bot_message" && + request.event.subtype === 'bot_message' && finalUserId && this.muzzlePersistenceService.isUserMuzzled(finalUserId) && - request.event.username !== "muzzle" + request.event.username !== 'muzzle' ); } /** * Adds a user to the muzzled map and sets a timeout to remove the muzzle within a random time of 30 seconds to 3 minutes */ - public addUserToMuzzled( - userId: string, - requestorId: string, - channel: string - ) { + public addUserToMuzzled(userId: string, requestorId: string, channel: string): Promise { const shouldBackFire = shouldBackfire(); const userName = this.slackService.getUserName(userId); const requestorName = this.slackService.getUserName(requestorId); - const counter = this.counterPersistenceService.getCounterByRequestorAndUserId( - userId, - requestorId - ); + const counter = this.counterPersistenceService.getCounterByRequestorAndUserId(userId, requestorId); return new Promise(async (resolve, reject) => { if (!userId) { - reject( - `Invalid username passed in. You can only muzzle existing slack users.` - ); + reject(`Invalid username passed in. You can only muzzle existing slack users.`); } else if (this.muzzlePersistenceService.isUserMuzzled(userId)) { console.error( - `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.` + `${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but ${userName} | ${userId} is already muzzled.`, ); reject(`${userName} is already muzzled!`); } else if (this.muzzlePersistenceService.isUserMuzzled(requestorId)) { console.error( - `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled` + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} is currently muzzled`, ); reject(`You can't muzzle someone if you are already muzzled!`); - } else if ( - this.muzzlePersistenceService.isMaxMuzzlesReached(requestorId) - ) { + } else if (this.muzzlePersistenceService.isMaxMuzzlesReached(requestorId)) { console.error( - `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${MAX_MUZZLES}` - ); - reject( - `You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.` + `User: ${requestorName} | ${requestorId} attempted to muzzle ${userName} | ${userId} but failed because requestor: ${requestorName} | ${requestorId} has reached maximum muzzle of ${MAX_MUZZLES}`, ); + reject(`You're doing that too much. Only ${MAX_MUZZLES} muzzles are allowed per hour.`); } else if (counter) { - console.log( - `${requestorId} attempted to muzzle ${userId} but was countered!` - ); + console.log(`${requestorId} attempted to muzzle ${userId} but was countered!`); this.counterService.removeCounter(counter, true, channel); reject(`You've been countered! Better luck next time...`); } else if (shouldBackFire) { - console.log( - `Backfiring on ${requestorName} | ${requestorId} for attempting to muzzle ${userName} | ${userId}` - ); + console.log(`Backfiring on ${requestorName} | ${requestorId} for attempting to muzzle ${userName} | ${userId}`); const timeToMuzzle = getTimeToMuzzle(); await this.backfirePersistenceService .addBackfire(requestorId, timeToMuzzle) @@ -162,7 +124,7 @@ export class MuzzleService { this.muzzlePersistenceService.setRequestorCount(requestorId); this.webService.sendMessage( channel, - `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:` + `:boom: <@${requestorId}> attempted to muzzle <@${userId}> but it backfired! :boom:`, ); resolve(`:boom: Backfired! Better luck next time... :boom:`); }) @@ -175,11 +137,7 @@ export class MuzzleService { await this.muzzlePersistenceService .addMuzzle(requestorId, userId, timeToMuzzle) .then(() => { - resolve( - `Successfully muzzled ${userName} for ${getTimeString( - timeToMuzzle - )}` - ); + resolve(`Successfully muzzled ${userName} for ${getTimeString(timeToMuzzle)}`); }) .catch((e: any) => { console.error(e); @@ -192,16 +150,9 @@ export class MuzzleService { /** * Wrapper for sendMessage that handles suppression in memory and, if max suppressions are reached, handles suppression storage to disk. */ - public sendMuzzledMessage( - channel: string, - userId: string, - text: string, - timestamp: string - ) { - console.time("send-muzzled-message"); - const muzzle: - | IMuzzled - | undefined = this.muzzlePersistenceService.getMuzzle(userId); + public sendMuzzledMessage(channel: string, userId: string, text: string, timestamp: string): void { + console.time('send-muzzled-message'); + const muzzle: Muzzled | undefined = this.muzzlePersistenceService.getMuzzle(userId); if (muzzle) { this.webService.deleteMessage(channel, timestamp); if (muzzle!.suppressionCount < MAX_SUPPRESSIONS) { @@ -210,30 +161,19 @@ export class MuzzleService { muzzledBy: muzzle!.muzzledBy, id: muzzle!.id, isCounter: muzzle!.isCounter, - removalFn: muzzle!.removalFn + removalFn: muzzle!.removalFn, }); - this.webService.sendMessage( - channel, - `<@${userId}> says "${this.muzzle(text, muzzle!.id)}"` - ); + this.webService.sendMessage(channel, `<@${userId}> says "${this.muzzle(text, muzzle!.id)}"`); } else { this.muzzlePersistenceService.trackDeletedMessage(muzzle!.id, text); } } - console.timeEnd("send-muzzled-message"); + console.timeEnd('send-muzzled-message'); } - private getReplacementWord( - word: string, - isFirstWord: boolean, - isLastWord: boolean, - replacementText: string - ) { + public getReplacementWord(word: string, isFirstWord: boolean, isLastWord: boolean, replacementText: string): string { const text = - isRandomEven() && - word.length < 10 && - word !== " " && - !this.slackService.containsTag(word) + isRandomEven() && word.length < 10 && word !== ' ' && !this.slackService.containsTag(word) ? `*${word}*` : replacementText; diff --git a/src/services/reaction/constants.ts b/src/services/reaction/constants.ts index 2f1c5d35..47740555 100644 --- a/src/services/reaction/constants.ts +++ b/src/services/reaction/constants.ts @@ -1,7 +1,8 @@ -interface IReactionValue { +/* eslint-disable @typescript-eslint/camelcase */ +interface ReactionValue { [key: string]: number; } -export const reactionValues: IReactionValue = { +export const reactionValues: ReactionValue = { // Positive emojis grinning: 1, grin: 1, @@ -23,7 +24,7 @@ export const reactionValues: IReactionValue = { point_up_2: 1, the_horns: 1, ok_hand: 1, - "+1": 1, + '+1': 1, clap: 1, raised_hands: 1, pray: 1, @@ -48,7 +49,7 @@ export const reactionValues: IReactionValue = { first_place_medal: 1, moneybag: 1, key: 1, - "100": 1, + '100': 1, bong: 1, chefkiss: 1, clapping: 1, @@ -56,7 +57,7 @@ export const reactionValues: IReactionValue = { feelsgood: 1, healing_of_the_nation: 1, godmode: 1, - "1000": 1, + '1000': 1, heavy_check_mark: 1, white_check_mark: 1, chart_with_upwards_trend: 1, @@ -71,19 +72,19 @@ export const reactionValues: IReactionValue = { skull: -1, skull_and_crossbones: -1, middle_finger: -1, - "-1": -1, + '-1': -1, bomb: -1, boom: -1, snowflake: -1, small_red_triangle: -1, - "99": -1, - "90": -1, - "bounce-eyes": -1, + '99': -1, + '90': -1, + 'bounce-eyes': -1, butthurt: -1, caged2: -1, alert: -1, bomb2: -1, - "fake-news": -1, + 'fake-news': -1, flag: -1, flesh: -1, heh: -1, @@ -104,8 +105,8 @@ export const reactionValues: IReactionValue = { no_entry_sign: -1, dumpster: -1, thx: -1, - "man-gesturing-no": -1, + 'man-gesturing-no': -1, no_good: -1, chart_with_downwards_trend: -1, - zzz: -1 + zzz: -1, }; diff --git a/src/services/reaction/reaction.persistence.service.ts b/src/services/reaction/reaction.persistence.service.ts index e6d84ab0..68286d59 100644 --- a/src/services/reaction/reaction.persistence.service.ts +++ b/src/services/reaction/reaction.persistence.service.ts @@ -1,11 +1,11 @@ -import { getRepository } from "typeorm"; -import { Reaction } from "../../shared/db/models/Reaction"; -import { Rep } from "../../shared/db/models/Rep"; -import { IReactionByUser } from "../../shared/models/reaction/ReactionByUser.model"; -import { IEvent } from "../../shared/models/slack/slack-models"; +import { getRepository } from 'typeorm'; +import { Reaction } from '../../shared/db/models/Reaction'; +import { Rep } from '../../shared/db/models/Rep'; +import { ReactionByUser } from '../../shared/models/reaction/ReactionByUser.model'; +import { Event } from '../../shared/models/slack/slack-models'; export class ReactionPersistenceService { - public static getInstance() { + public static getInstance(): ReactionPersistenceService { if (!ReactionPersistenceService.instance) { ReactionPersistenceService.instance = new ReactionPersistenceService(); } @@ -14,37 +14,33 @@ export class ReactionPersistenceService { private static instance: ReactionPersistenceService; - private constructor() {} - public getRep(userId: string): Promise { return new Promise(async (resolve, reject) => { await getRepository(Rep) .findOne({ user: userId }) .then(async value => { await getRepository(Rep) - .increment({ user: userId }, "timesChecked", 1) - .catch(e => - console.error(`Error logging check for user ${userId}. \n ${e}`) - ); + .increment({ user: userId }, 'timesChecked', 1) + .catch(e => console.error(`Error logging check for user ${userId}. \n ${e}`)); resolve(value); }) .catch(e => reject(e)); }); } - public getRepByUser(userId: string): Promise { + public getRepByUser(userId: string): Promise { return new Promise(async (resolve, reject) => { await getRepository(Reaction) .query( `SELECT reactingUser, SUM(value) as rep FROM reaction WHERE affectedUser=? GROUP BY reactingUser ORDER BY rep DESC;`, - [userId] + [userId], ) .then(value => resolve(value)) .catch(e => reject(e)); }); } - public saveReaction(event: IEvent, value: number) { + public saveReaction(event: Event, value: number): Promise { return new Promise(async (resolve, reject) => { const reaction = new Reaction(); reaction.affectedUser = event.item_user; @@ -72,31 +68,29 @@ export class ReactionPersistenceService { }); } - public async removeReaction(event: IEvent, value: number) { + public async removeReaction(event: Event, value: number): Promise { await getRepository(Reaction) .delete({ reaction: event.reaction, affectedUser: event.item_user, reactingUser: event.user, type: event.item.type, - channel: event.item.channel + channel: event.item.channel, }) .then(() => { - value === 1 - ? this.decrementRep(event.item_user) - : this.incrementRep(event.item_user); + value === 1 ? this.decrementRep(event.item_user) : this.incrementRep(event.item_user); }) .catch(e => e); } - private async isRepUserPresent(affectedUser: string) { + private async isRepUserPresent(affectedUser: string): Promise { return getRepository(Rep) .findOne({ user: affectedUser }) .then(user => !!user) .catch(e => console.error(e)); } - private incrementRep(affectedUser: string) { + private incrementRep(affectedUser: string): Promise { return new Promise(async (resolve, reject) => { // Check for affectedUser const isUserExisting = await this.isRepUserPresent(affectedUser); @@ -104,7 +98,7 @@ export class ReactionPersistenceService { if (isUserExisting) { // If it exists, increment rep by one. return getRepository(Rep) - .increment({ user: affectedUser }, "rep", 1) + .increment({ user: affectedUser }, 'rep', 1) .then(() => resolve()) .catch(e => reject(e)); } else { @@ -120,7 +114,7 @@ export class ReactionPersistenceService { }); } - private decrementRep(affectedUser: string) { + private decrementRep(affectedUser: string): Promise { return new Promise(async (resolve, reject) => { // Check for affectedUser const isUserExisting = await this.isRepUserPresent(affectedUser); @@ -128,7 +122,7 @@ export class ReactionPersistenceService { if (isUserExisting) { // If it exists, decrement rep by one. return getRepository(Rep) - .decrement({ user: affectedUser }, "rep", 1) + .decrement({ user: affectedUser }, 'rep', 1) .then(() => resolve()) .catch(e => reject(e)); } else { diff --git a/src/services/reaction/reaction.service.ts b/src/services/reaction/reaction.service.ts index ce6ed117..d3e5b7c6 100644 --- a/src/services/reaction/reaction.service.ts +++ b/src/services/reaction/reaction.service.ts @@ -1,15 +1,15 @@ -import Table from "easy-table"; -import { IReactionByUser } from "../../shared/models/reaction/ReactionByUser.model"; -import { IEvent } from "../../shared/models/slack/slack-models"; -import { SlackService } from "../slack/slack.service"; -import { reactionValues } from "./constants"; -import { ReactionPersistenceService } from "./reaction.persistence.service"; +import Table from 'easy-table'; +import { ReactionByUser } from '../../shared/models/reaction/ReactionByUser.model'; +import { Event } from '../../shared/models/slack/slack-models'; +import { SlackService } from '../slack/slack.service'; +import { reactionValues } from './constants'; +import { ReactionPersistenceService } from './reaction.persistence.service'; export class ReactionService { private reactionPersistenceService = ReactionPersistenceService.getInstance(); private slackService = SlackService.getInstance(); - public async getRep(userId: string) { + public async getRep(userId: string): Promise { const totalRep = await this.reactionPersistenceService .getRep(userId) .then(value => { @@ -23,15 +23,13 @@ export class ReactionService { const repByUser = await this.reactionPersistenceService .getRepByUser(userId) - .then((perUserRep: IReactionByUser[] | undefined) => - this.formatRepByUser(perUserRep) - ) + .then((perUserRep: ReactionByUser[] | undefined) => this.formatRepByUser(perUserRep)) .catch(e => console.error(e)); return `${repByUser}\n\n${totalRep}`; } - public handleReaction(event: IEvent, isAdded: boolean) { + public handleReaction(event: Event, isAdded: boolean): void { console.log(event); if (event.user && event.item_user && event.user !== event.item_user) { if (isAdded) { @@ -43,87 +41,83 @@ export class ReactionService { console.log( `${event.user} responded to ${ event.item_user - } message and no action was taken. This was a self-reaction or a reaction to a bot message.` + } message and no action was taken. This was a self-reaction or a reaction to a bot message.`, ); } } - private formatRepByUser(perUserRep: IReactionByUser[] | undefined) { + private formatRepByUser(perUserRep: ReactionByUser[] | undefined): string { if (!perUserRep) { - return "You do not have any existing relationships."; + return 'You do not have any existing relationships.'; } else { const formattedData = perUserRep.map(userRep => { return { user: this.slackService.getUserName(userRep.reactingUser), - rep: `${this.getSentiment(userRep.rep)} (${userRep.rep})` + rep: `${this.getSentiment(userRep.rep)} (${userRep.rep})`, }; }); return `${Table.print(formattedData)}`; } } - private getSentiment(rep: number) { + private getSentiment(rep: number): string { if (rep >= 1000) { - return "Worshipped"; + return 'Worshipped'; } else if (rep >= 900 && rep < 1000) { - return "Enamored"; + return 'Enamored'; } else if (rep >= 800 && rep < 900) { - return "Adored"; + return 'Adored'; } else if (rep >= 700 && rep < 800) { - return "Loved"; + return 'Loved'; } else if (rep >= 600 && rep < 700) { - return "Endeared"; + return 'Endeared'; } else if (rep >= 500 && rep < 600) { - return "Admired"; + return 'Admired'; } else if (rep >= 400 && rep < 500) { - return "Esteemed"; + return 'Esteemed'; } else if (rep >= 300 && rep < 400) { - return "Well Liked"; + return 'Well Liked'; } else if (rep >= 200 && rep < 300) { - return "Liked"; + return 'Liked'; } else if (rep >= 100 && rep < 200) { - return "Respected"; + return 'Respected'; } else if (rep >= -300 && rep < 100) { - return "Neutral"; + return 'Neutral'; } else if (rep >= -500 && rep < -300) { - return "Unfriendly"; + return 'Unfriendly'; } else if (rep >= -700 && rep < -500) { - return "Disliked"; + return 'Disliked'; } else if (rep >= -1000 && rep < -700) { - return "Scorned"; + return 'Scorned'; } else if (rep >= -1000) { - return "Hated"; + return 'Hated'; } else { - return "Neutral"; + return 'Neutral'; } } - private shouldReactionBeLogged(reactionValue: number | undefined) { + private shouldReactionBeLogged(reactionValue: number | undefined): boolean { return reactionValue === 1 || reactionValue === -1; } - private handleAddedReaction(event: IEvent) { + private handleAddedReaction(event: Event): void { const reactionValue = reactionValues[event.reaction]; // Log event to DB. if (this.shouldReactionBeLogged(reactionValue)) { console.log( `Adding reaction to ${event.item_user} for ${event.user}'s reaction: ${ event.reaction - }, yielding him ${reactionValue}` + }, yielding him ${reactionValue}`, ); this.reactionPersistenceService.saveReaction(event, reactionValue); } } - private handleRemovedReaction(event: IEvent) { + private handleRemovedReaction(event: Event): void { const reactionValue = reactionValues[event.reaction]; if (this.shouldReactionBeLogged(reactionValue)) { this.reactionPersistenceService.removeReaction(event, reactionValue); - console.log( - `Removing rep from ${event.item_user} for ${event.user}'s reaction: ${ - event.reaction - }` - ); + console.log(`Removing rep from ${event.item_user} for ${event.user}'s reaction: ${event.reaction}`); } } } diff --git a/src/services/report/report.service.spec.ts b/src/services/report/report.service.spec.ts index 4ab3861b..ce943ebb 100644 --- a/src/services/report/report.service.spec.ts +++ b/src/services/report/report.service.spec.ts @@ -1,86 +1,78 @@ -import { ReportType } from "../../shared/models/muzzle/muzzle-models"; -import { ReportService } from "./report.service"; +import { ReportType } from '../../shared/models/muzzle/muzzle-models'; +import { ReportService } from './report.service'; -describe("ReportService", () => { +describe('ReportService', () => { let mockService: ReportService; beforeEach(() => { mockService = new ReportService(); }); - describe("getReportType()", () => { - describe(" - with valid report types", () => { - it("should return ReportType.trailing30 when trailing30 is passed in with any case", () => { - expect(mockService.getReportType("trailing30")).toBe( - ReportType.Trailing30 - ); - expect(mockService.getReportType("Trailing30")).toBe( - ReportType.Trailing30 - ); - expect(mockService.getReportType("TRAILING30")).toBe( - ReportType.Trailing30 - ); + describe('getReportType()', () => { + describe(' - with valid report types', () => { + it('should return ReportType.trailing30 when trailing30 is passed in with any case', () => { + expect(mockService.getReportType('trailing30')).toBe(ReportType.Trailing30); + expect(mockService.getReportType('Trailing30')).toBe(ReportType.Trailing30); + expect(mockService.getReportType('TRAILING30')).toBe(ReportType.Trailing30); }); - it("should return ReportType.Week when week is passed in with any case", () => { - expect(mockService.getReportType("week")).toBe(ReportType.Week); - expect(mockService.getReportType("Week")).toBe(ReportType.Week); - expect(mockService.getReportType("WEEK")).toBe(ReportType.Week); + it('should return ReportType.Week when week is passed in with any case', () => { + expect(mockService.getReportType('week')).toBe(ReportType.Week); + expect(mockService.getReportType('Week')).toBe(ReportType.Week); + expect(mockService.getReportType('WEEK')).toBe(ReportType.Week); }); - it("should return ReportType.Month when month is passed in with any case", () => { - expect(mockService.getReportType("month")).toBe(ReportType.Month); - expect(mockService.getReportType("Month")).toBe(ReportType.Month); - expect(mockService.getReportType("MONTH")).toBe(ReportType.Month); + it('should return ReportType.Month when month is passed in with any case', () => { + expect(mockService.getReportType('month')).toBe(ReportType.Month); + expect(mockService.getReportType('Month')).toBe(ReportType.Month); + expect(mockService.getReportType('MONTH')).toBe(ReportType.Month); }); - it("should return ReportType.Year when year is passed in with any case", () => { - expect(mockService.getReportType("year")).toBe(ReportType.Year); - expect(mockService.getReportType("Year")).toBe(ReportType.Year); - expect(mockService.getReportType("YEAR")).toBe(ReportType.Year); + it('should return ReportType.Year when year is passed in with any case', () => { + expect(mockService.getReportType('year')).toBe(ReportType.Year); + expect(mockService.getReportType('Year')).toBe(ReportType.Year); + expect(mockService.getReportType('YEAR')).toBe(ReportType.Year); }); - it("should return ReportType.AllTime when all is passed in with any case", () => { - expect(mockService.getReportType("all")).toBe(ReportType.AllTime); - expect(mockService.getReportType("All")).toBe(ReportType.AllTime); - expect(mockService.getReportType("ALL")).toBe(ReportType.AllTime); + it('should return ReportType.AllTime when all is passed in with any case', () => { + expect(mockService.getReportType('all')).toBe(ReportType.AllTime); + expect(mockService.getReportType('All')).toBe(ReportType.AllTime); + expect(mockService.getReportType('ALL')).toBe(ReportType.AllTime); }); }); - describe("- with invalid report type", () => { - it("should return ReportType.AllTime when an invalid report type is passed in", () => { - expect(mockService.getReportType("whatever")).toBe(ReportType.AllTime); + describe('- with invalid report type', () => { + it('should return ReportType.AllTime when an invalid report type is passed in', () => { + expect(mockService.getReportType('whatever')).toBe(ReportType.AllTime); }); }); }); - describe("isValidReportType()", () => { - it("should return true when Trailing30 is passed in with any case", () => { - expect(mockService.isValidReportType("trailing30")).toBe(true); - expect(mockService.isValidReportType("Trailing30")).toBe(true); - expect(mockService.isValidReportType("TRAILING30")).toBe(true); + describe('isValidReportType()', () => { + it('should return true when Trailing30 is passed in with any case', () => { + expect(mockService.isValidReportType('trailing30')).toBe(true); + expect(mockService.isValidReportType('Trailing30')).toBe(true); + expect(mockService.isValidReportType('TRAILING30')).toBe(true); }); - it("should return true when week is passed in with any case", () => { - expect(mockService.isValidReportType("week")).toBe(true); - expect(mockService.isValidReportType("Week")).toBe(true); - expect(mockService.isValidReportType("WEEK")).toBe(true); + it('should return true when week is passed in with any case', () => { + expect(mockService.isValidReportType('week')).toBe(true); + expect(mockService.isValidReportType('Week')).toBe(true); + expect(mockService.isValidReportType('WEEK')).toBe(true); }); - it("should return true when month is passed in with any case", () => { - expect(mockService.isValidReportType("month")).toBe(true); - expect(mockService.isValidReportType("Month")).toBe(true); - expect(mockService.isValidReportType("MONTH")).toBe(true); + it('should return true when month is passed in with any case', () => { + expect(mockService.isValidReportType('month')).toBe(true); + expect(mockService.isValidReportType('Month')).toBe(true); + expect(mockService.isValidReportType('MONTH')).toBe(true); }); - it("should return true when year is passed in with any case", () => { - expect(mockService.isValidReportType("year")).toBe(true); - expect(mockService.isValidReportType("Year")).toBe(true); - expect(mockService.isValidReportType("YEAR")).toBe(true); + it('should return true when year is passed in with any case', () => { + expect(mockService.isValidReportType('year')).toBe(true); + expect(mockService.isValidReportType('Year')).toBe(true); + expect(mockService.isValidReportType('YEAR')).toBe(true); }); - it("should return true when all is passed in with any case", () => { - expect(mockService.isValidReportType("all")).toBe(true); - expect(mockService.isValidReportType("All")).toBe(true); - expect(mockService.isValidReportType("ALL")).toBe(true); + it('should return true when all is passed in with any case', () => { + expect(mockService.isValidReportType('all')).toBe(true); + expect(mockService.isValidReportType('All')).toBe(true); + expect(mockService.isValidReportType('ALL')).toBe(true); }); - it("should return false when a non-valid reportType is passed in", () => { - expect(mockService.isValidReportType("whatever")).toBe(false); + it('should return false when a non-valid reportType is passed in', () => { + expect(mockService.isValidReportType('whatever')).toBe(false); }); - it("should return false for a sentence", () => { - expect( - mockService.isValidReportType("test sentence that should fail day") - ).toBe(false); + it('should return false for a sentence', () => { + expect(mockService.isValidReportType('test sentence that should fail day')).toBe(false); }); }); }); diff --git a/src/services/report/report.service.ts b/src/services/report/report.service.ts index 8a87478e..991295ac 100644 --- a/src/services/report/report.service.ts +++ b/src/services/report/report.service.ts @@ -1,29 +1,27 @@ -import Table from "easy-table"; -import moment from "moment"; -import { List } from "../../shared/db/models/List"; -import { ReportType } from "../../shared/models/muzzle/muzzle-models"; -import { ListPersistenceService } from "../list/list.persistence.service"; -import { MuzzlePersistenceService } from "../muzzle/muzzle.persistence.service"; -import { SlackService } from "../slack/slack.service"; +import Table from 'easy-table'; +import moment from 'moment'; +import { List } from '../../shared/db/models/List'; +import { ListPersistenceService } from '../list/list.persistence.service'; +import { MuzzlePersistenceService } from '../muzzle/muzzle.persistence.service'; +import { SlackService } from '../slack/slack.service'; +import { ReportType, ReportCount, MuzzleReport } from '../../shared/models/report/report.model'; export class ReportService { private slackService = SlackService.getInstance(); private muzzlePersistenceService = MuzzlePersistenceService.getInstance(); private listPersistenceService = ListPersistenceService.getInstance(); - public async getListReport() { + public async getListReport(): Promise { const listReport = await this.listPersistenceService.retrieve(); return this.formatListReport(listReport); } - public async getMuzzleReport(reportType: ReportType) { - const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport( - reportType - ); + public async getMuzzleReport(reportType: ReportType): Promise { + const muzzleReport = await this.muzzlePersistenceService.retrieveMuzzleReport(reportType); return this.generateFormattedReport(muzzleReport, reportType); } - public isValidReportType(type: string) { + public isValidReportType(type: string): boolean { const lowerCaseType = type.toLowerCase(); return ( lowerCaseType === ReportType.Trailing30 || @@ -42,28 +40,28 @@ export class ReportService { return ReportType.AllTime; } - public getReportTitle(type: ReportType) { + public getReportTitle(type: ReportType): string { const range = this.muzzlePersistenceService.getRange(type); const titles = { - [ReportType.Week]: `Weekly Muzzle Report for ${moment(range.start).format( - "MM-DD-YYYY" - )} to ${moment(range.end).format("MM-DD-YYYY")}`, - [ReportType.Month]: `Monthly Muzzle Report for ${moment( - range.start - ).format("MM-DD-YYYY")} to ${moment(range.end).format("MM-DD-YYYY")}`, - [ReportType.Trailing30]: `Trailing 30 Days Report for ${moment( - range.start - ).format("MM-DD-YYYY")} to ${moment(range.end).format("MM-DD-YYYY")}`, - [ReportType.Year]: `Annual Muzzle Report for ${moment(range.start).format( - "MM-DD-YYYY" - )} to ${moment(range.end).format("MM-DD-YYYY")}`, - [ReportType.AllTime]: "All Time Muzzle Report" + [ReportType.Week]: `Weekly Muzzle Report for ${moment(range.start).format('MM-DD-YYYY')} to ${moment( + range.end, + ).format('MM-DD-YYYY')}`, + [ReportType.Month]: `Monthly Muzzle Report for ${moment(range.start).format('MM-DD-YYYY')} to ${moment( + range.end, + ).format('MM-DD-YYYY')}`, + [ReportType.Trailing30]: `Trailing 30 Days Report for ${moment(range.start).format('MM-DD-YYYY')} to ${moment( + range.end, + ).format('MM-DD-YYYY')}`, + [ReportType.Year]: `Annual Muzzle Report for ${moment(range.start).format('MM-DD-YYYY')} to ${moment( + range.end, + ).format('MM-DD-YYYY')}`, + [ReportType.AllTime]: 'All Time Muzzle Report', }; return titles[type]; } - private formatListReport(report: any) { + private formatListReport(report: any): string { const reportWithoutDate = report.map((listItem: List) => { return { Item: listItem.text }; }); @@ -74,7 +72,7 @@ The List ${Table.print(reportWithoutDate)} `; } - private generateFormattedReport(report: any, reportType: ReportType): string { + private generateFormattedReport(report: MuzzleReport, reportType: ReportType): string { const formattedReport = this.formatReport(report); return ` ${this.getReportTitle(reportType)} @@ -99,30 +97,30 @@ ${this.getReportTitle(reportType)} `; } - private formatReport(report: any) { + private formatReport(report: MuzzleReport): any { const reportFormatted = { muzzled: { - byInstances: report.muzzled.byInstances.map((instance: any) => { + byInstances: report.muzzled.byInstances.map((instance: ReportCount) => { return { - User: this.slackService.getUserById(instance.muzzledId)!.name, - Muzzles: instance.count + User: this.slackService.getUserById(instance.slackId)!.name, + Muzzles: instance.count, }; - }) + }), }, muzzlers: { - byInstances: report.muzzlers.byInstances.map((instance: any) => { + byInstances: report.muzzlers.byInstances.map((instance: ReportCount) => { return { - User: this.slackService.getUserById(instance.requestorId)!.name, - ["Muzzles Issued"]: instance.instanceCount + User: this.slackService.getUserById(instance.slackId)!.name, + ['Muzzles Issued']: instance.count, }; - }) + }), }, accuracy: report.accuracy.map((instance: any) => { return { User: this.slackService.getUserById(instance.requestorId)!.name, Accuracy: instance.accuracy, Kills: instance.kills, - Attempts: instance.deaths + Attempts: instance.deaths, }; }), KDR: report.kdr.map((instance: any) => { @@ -130,23 +128,23 @@ ${this.getReportTitle(reportType)} User: this.slackService.getUserById(instance.requestorId)!.name, KDR: instance.kdr, Kills: instance.kills, - Deaths: instance.deaths + Deaths: instance.deaths, }; }), rawNemesis: report.rawNemesis.map((instance: any) => { return { Killer: this.slackService.getUserById(instance.requestorId)!.name, Victim: this.slackService.getUserById(instance.muzzledId)!.name, - Attempts: instance.killCount + Attempts: instance.killCount, }; }), successNemesis: report.successNemesis.map((instance: any) => { return { Killer: this.slackService.getUserById(instance.requestorId)!.name, Victim: this.slackService.getUserById(instance.muzzledId)!.name, - Kills: instance.killCount + Kills: instance.killCount, }; - }) + }), }; return reportFormatted; diff --git a/src/services/slack/slack.service.spec.ts b/src/services/slack/slack.service.spec.ts index bc137c2f..c4b4b8b1 100644 --- a/src/services/slack/slack.service.spec.ts +++ b/src/services/slack/slack.service.spec.ts @@ -1,182 +1,172 @@ -import { ISlackUser } from "../../shared/models/slack/slack-models"; -import { SlackService } from "./slack.service"; +import { SlackUser } from '../../shared/models/slack/slack-models'; +import { SlackService } from './slack.service'; -describe("slack-utils", () => { +describe('slack-utils', () => { let slackService: SlackService; beforeEach(() => { slackService = SlackService.getInstance(); slackService.userList = [ { - id: "123", - name: "test_user123" + id: '123', + name: 'test_user123', }, { - id: "456", - name: "test_user456" + id: '456', + name: 'test_user456', }, { - id: "789", - name: "test_user789" - } - ] as ISlackUser[]; + id: '789', + name: 'test_user789', + }, + ] as SlackUser[]; }); - describe("getUserName()", () => { - it("should return the user.name property of a known user by id", () => { - expect(slackService.getUserName("123")).toBe("test_user123"); + describe('getUserName()', () => { + it('should return the user.name property of a known user by id', () => { + expect(slackService.getUserName('123')).toBe('test_user123'); }); - it("should return an empty string for a user that does not exist", () => { - expect(slackService.getUserName("1010")).toBe(""); + it('should return an empty string for a user that does not exist', () => { + expect(slackService.getUserName('1010')).toBe(''); }); - it("should handle empty strings values", () => { - expect(slackService.getUserName("")).toBe(""); + it('should handle empty strings values', () => { + expect(slackService.getUserName('')).toBe(''); }); }); - describe("getUserId()", () => { - it("should return a userId when one is passed in without a username", () => { - expect(slackService.getUserId("<@U2TYNKJ>")).toBe("U2TYNKJ"); + describe('getUserId()', () => { + it('should return a userId when one is passed in without a username', () => { + expect(slackService.getUserId('<@U2TYNKJ>')).toBe('U2TYNKJ'); }); - it("should return a userId when one is passed in with a username with spaces", () => { - expect(slackService.getUserId("<@U2TYNKJ | jrjrjr>")).toBe("U2TYNKJ"); + it('should return a userId when one is passed in with a username with spaces', () => { + expect(slackService.getUserId('<@U2TYNKJ | jrjrjr>')).toBe('U2TYNKJ'); }); - it("should return a userId when one is passed in with a username without spaces", () => { - expect(slackService.getUserId("<@U2TYNKJ|jrjrjr>")).toBe("U2TYNKJ"); + it('should return a userId when one is passed in with a username without spaces', () => { + expect(slackService.getUserId('<@U2TYNKJ|jrjrjr>')).toBe('U2TYNKJ'); }); - it("should return empty string when no userId exists", () => { - expect(slackService.getUserId("total waste of time")).toBe(""); + it('should return empty string when no userId exists', () => { + expect(slackService.getUserId('total waste of time')).toBe(''); }); }); - describe("getUserById()", () => { - it("should return a user object when given a valid id", () => { - expect(slackService.getUserById("123")).toEqual(slackService.userList[0]); + describe('getUserById()', () => { + it('should return a user object when given a valid id', () => { + expect(slackService.getUserById('123')).toEqual(slackService.userList[0]); }); - it("should return undefined when given an invalid id", () => { - expect(slackService.getUserById("1010")).toBeUndefined(); + it('should return undefined when given an invalid id', () => { + expect(slackService.getUserById('1010')).toBeUndefined(); }); }); - describe("containsTag()", () => { - it("should return false if a word has @ in it and is not a tag", () => { - const testWord = ".@channel"; + describe('containsTag()', () => { + it('should return false if a word has @ in it and is not a tag', () => { + const testWord = '.@channel'; expect(slackService.containsTag(testWord)).toBe(false); }); - it("should return false if a word does not include @", () => { - const testWord = "test"; + it('should return false if a word does not include @', () => { + const testWord = 'test'; expect(slackService.containsTag(testWord)).toBe(false); }); - it("should return false if no text is passed in", () => { - expect(slackService.containsTag("")).toBe(false); + it('should return false if no text is passed in', () => { + expect(slackService.containsTag('')).toBe(false); }); - it("should return false if undefined is passed in", () => { + it('should return false if undefined is passed in', () => { expect(slackService.containsTag(undefined)).toBe(false); }); - it("should return true if a word has in it", () => { - const testWord = ""; + it('should return true if a word has in it', () => { + const testWord = ''; expect(slackService.containsTag(testWord)).toBe(true); }); - it("should return true if a word has in it", () => { - const testWord = ""; + it('should return true if a word has in it', () => { + const testWord = ''; expect(slackService.containsTag(testWord)).toBe(true); }); - it("should return true if a word has a tagged user", () => { - const testUser = "<@UTJFJKL>"; + it('should return true if a word has a tagged user', () => { + const testUser = '<@UTJFJKL>'; expect(slackService.containsTag(testUser)).toBe(true); }); }); - describe("getUserIdByCallbackId()", () => { - it("should return a userId when there is one present", () => { - const callbackId = "JSLKDJLFJ_U25JKLMN"; - expect(slackService.getUserIdByCallbackId(callbackId)).toBe("U25JKLMN"); + describe('getUserIdByCallbackId()', () => { + it('should return a userId when there is one present', () => { + const callbackId = 'JSLKDJLFJ_U25JKLMN'; + expect(slackService.getUserIdByCallbackId(callbackId)).toBe('U25JKLMN'); }); - it("should return an empty string when there is no id present", () => { - const callbackId = "LJKSDLFJSF"; - expect(slackService.getUserIdByCallbackId(callbackId)).toBe(""); + it('should return an empty string when there is no id present', () => { + const callbackId = 'LJKSDLFJSF'; + expect(slackService.getUserIdByCallbackId(callbackId)).toBe(''); }); - it("should handle an empty string callbackId", () => { - expect(slackService.getUserIdByCallbackId("")).toBe(""); + it('should handle an empty string callbackId', () => { + expect(slackService.getUserIdByCallbackId('')).toBe(''); }); }); - describe("getBotId()", () => { - describe("it should handle undefined values", () => { - it("should return an id fromText if it is the only id present", () => { - expect( - slackService.getBotId("12345", undefined, undefined, undefined) - ).toBe("12345"); + describe('getBotId()', () => { + describe('it should handle undefined values', () => { + it('should return an id fromText if it is the only id present', () => { + expect(slackService.getBotId('12345', undefined, undefined, undefined)).toBe('12345'); }); - it("should return an id fromAttachmentText if it is the only id present", () => { - expect( - slackService.getBotId(undefined, "12345", undefined, undefined) - ).toBe("12345"); + it('should return an id fromAttachmentText if it is the only id present', () => { + expect(slackService.getBotId(undefined, '12345', undefined, undefined)).toBe('12345'); }); - it("should return an id fromPretext if it is the only id present", () => { - expect( - slackService.getBotId(undefined, undefined, "12345", undefined) - ).toBe("12345"); + it('should return an id fromPretext if it is the only id present', () => { + expect(slackService.getBotId(undefined, undefined, '12345', undefined)).toBe('12345'); }); - it("should return an id fromCallBackId if it is the only id present", () => { - expect( - slackService.getBotId(undefined, undefined, undefined, "12345") - ).toBe("12345"); + it('should return an id fromCallBackId if it is the only id present', () => { + expect(slackService.getBotId(undefined, undefined, undefined, '12345')).toBe('12345'); }); }); - describe("it should handle empty strings", () => { - it("should return an id fromText if it is the only id present", () => { - expect(slackService.getBotId("12345", "", "", "")).toBe("12345"); + describe('it should handle empty strings', () => { + it('should return an id fromText if it is the only id present', () => { + expect(slackService.getBotId('12345', '', '', '')).toBe('12345'); }); - it("should return an id fromAttachmentText if it is the only id present", () => { - expect(slackService.getBotId("", "12345", "", "")).toBe("12345"); + it('should return an id fromAttachmentText if it is the only id present', () => { + expect(slackService.getBotId('', '12345', '', '')).toBe('12345'); }); - it("should return an id fromPretext if it is the only id present", () => { - expect(slackService.getBotId("", "", "12345", "")).toBe("12345"); + it('should return an id fromPretext if it is the only id present', () => { + expect(slackService.getBotId('', '', '12345', '')).toBe('12345'); }); - it("should return an id fromCallBackId if it is the only id present", () => { - expect(slackService.getBotId("", "", "", "12345")).toBe("12345"); + it('should return an id fromCallBackId if it is the only id present', () => { + expect(slackService.getBotId('', '', '', '12345')).toBe('12345'); }); }); - describe("it should return in the proper order", () => { - it("should return the first available id - fromText", () => { - expect(slackService.getBotId("1", "2", "3", "4")).toBe("1"); + describe('it should return in the proper order', () => { + it('should return the first available id - fromText', () => { + expect(slackService.getBotId('1', '2', '3', '4')).toBe('1'); }); - it("should return the first available id - fromAttachmentText", () => { - expect(slackService.getBotId(undefined, "2", "3", "4")).toBe("2"); + it('should return the first available id - fromAttachmentText', () => { + expect(slackService.getBotId(undefined, '2', '3', '4')).toBe('2'); }); - it("should return the first available id - fromPretext", () => { - expect(slackService.getBotId(undefined, undefined, "3", "4")).toBe("3"); + it('should return the first available id - fromPretext', () => { + expect(slackService.getBotId(undefined, undefined, '3', '4')).toBe('3'); }); - it("should return the first available id - fromCallbackId", () => { - expect( - slackService.getBotId(undefined, undefined, undefined, "4") - ).toBe("4"); + it('should return the first available id - fromCallbackId', () => { + expect(slackService.getBotId(undefined, undefined, undefined, '4')).toBe('4'); }); }); }); diff --git a/src/services/slack/slack.service.ts b/src/services/slack/slack.service.ts index 4983284b..bf47fb8d 100644 --- a/src/services/slack/slack.service.ts +++ b/src/services/slack/slack.service.ts @@ -1,67 +1,60 @@ -import axios from "axios"; -import { - IChannelResponse, - ISlackUser -} from "../../shared/models/slack/slack-models"; -import { WebService } from "../web/web.service"; +import axios from 'axios'; +import { ChannelResponse, SlackUser } from '../../shared/models/slack/slack-models'; +import { WebService } from '../web/web.service'; export class SlackService { - public static getInstance() { + public static getInstance(): SlackService { if (!SlackService.instance) { SlackService.instance = new SlackService(); } return SlackService.instance; } private static instance: SlackService; - public userList: ISlackUser[] = []; + public userList: SlackUser[] = []; private userIdRegEx = /[<]@\w+/gm; private web: WebService = WebService.getInstance(); - private constructor() {} - - public sendResponse(responseUrl: string, response: IChannelResponse): void { + public sendResponse(responseUrl: string, response: ChannelResponse): void { axios .post(responseUrl, response) - .catch((e: Error) => - console.error(`Error responding: ${e.message} at ${responseUrl}`) - ); + .catch((e: Error) => console.error(`Error responding: ${e.message} at ${responseUrl}`)); } /** * Gets the username of the user by id. */ public getUserName(userId: string): string { - const userObj: ISlackUser | undefined = this.getUserById(userId); - return userObj ? userObj.name : ""; + const userObj: SlackUser | undefined = this.getUserById(userId); + return userObj ? userObj.name : ''; } /** * Retrieves the user id from a string. * Expected format is <@U235KLKJ> */ - public getUserId(user: string) { + public getUserId(user: string): string { if (!user) { - return ""; + return ''; } const regArray = user.match(this.userIdRegEx); - return regArray ? regArray[0].slice(2) : ""; + return regArray ? regArray[0].slice(2) : ''; } /** * Returns the user object by id */ - public getUserById(userId: string) { - return this.userList.find((user: ISlackUser) => user.id === userId); + public getUserById(userId: string): SlackUser | undefined { + return this.userList.find((user: SlackUser) => user.id === userId); } /** * Kind of a janky way to get the requesting users ID via callback id. */ - public getUserIdByCallbackId(callbackId: string) { - if (callbackId.includes("_")) { - return callbackId.slice(callbackId.indexOf("_") + 1, callbackId.length); + public getUserIdByCallbackId(callbackId: string): string { + if (callbackId.includes('_')) { + return callbackId.slice(callbackId.indexOf('_') + 1, callbackId.length); } else { - return ""; + return ''; } } /** @@ -71,8 +64,8 @@ export class SlackService { fromText: string | undefined, fromAttachmentText: string | undefined, fromPretext: string | undefined, - fromCallbackId: string | undefined - ) { + fromCallbackId: string | undefined, + ): string | undefined { return fromText || fromAttachmentText || fromPretext || fromCallbackId; } /** @@ -83,28 +76,24 @@ export class SlackService { return false; } - return ( - text.includes("") || - text.includes("") || - !!this.getUserId(text) - ); + return text.includes('') || text.includes('') || !!this.getUserId(text); } /** * Retrieves a list of all users. */ - public async getAllUsers() { - console.log("Retrieving new user list..."); + public async getAllUsers(): Promise { + console.log('Retrieving new user list...'); this.userList = (await this.web .getAllUsers() .then(resp => { - console.log("New user list has been retrieved!"); - return resp.members as ISlackUser[]; + console.log('New user list has been retrieved!'); + return resp.members as SlackUser[]; }) .catch(e => { - console.error("Failed to retrieve users", e); - console.error("Retrying in 5 seconds..."); + console.error('Failed to retrieve users', e); + console.error('Retrying in 5 seconds...'); setTimeout(() => this.getAllUsers(), 5000); - })) as ISlackUser[]; + })) as SlackUser[]; } } diff --git a/src/services/walkie/constants.ts b/src/services/walkie/constants.ts index 90d453bb..c732297a 100644 --- a/src/services/walkie/constants.ts +++ b/src/services/walkie/constants.ts @@ -1,29 +1,29 @@ -interface INatoMapping { +interface NatoMapping { [name: string]: string; } -export const NATO_MAPPINGS: INatoMapping = { - U2YJFUESC: "Zulu Mike", - U2YJQN2KB: "Sierra Foxtrot", - U2YLZPKJ4: "Charlie Foxtrot", - U2Z9W2KA6: "Charlie Kilo", - U2ZCMGB52: "Juliet Foxtrot", - U2ZDJ2ZGV: "Whiskey Hotel", - U2ZG4L8H3: "Mike Bravo", - U2ZH29DLL: "Kilo Juliet", - U2ZH3MXB6: "Mike Romeo", - U2ZHLLG77: "Bravo Juliet", - U2ZV8E68N: "Alpha Juliet", - U3007SQAK: "Echo Oscar", - U300D7UDD: "Juliet Papa Lima", - U3021E532: "Alpha Hotel", - U3073MH8E: "Mike Kilo", - U31CJM1LP: "Yankee Lima", - U31E74VQF: "Romeo Charlie", - U37DBNXB7: "Mike Lima", - U37SW3HD1: "Charlie Sierra", - U3KABQXLY: "Juliet Charlie", - U3U5VJA2E: "Romeo Golf", - U45HMKFJR: "Charlie Mike", - U4NFD9J2G: "Juliet Sierra", - URU0SCENN: "Mike Lima" +export const NATO_MAPPINGS: NatoMapping = { + U2YJFUESC: 'Zulu Mike', + U2YJQN2KB: 'Sierra Foxtrot', + U2YLZPKJ4: 'Charlie Foxtrot', + U2Z9W2KA6: 'Charlie Kilo', + U2ZCMGB52: 'Juliet Foxtrot', + U2ZDJ2ZGV: 'Whiskey Hotel', + U2ZG4L8H3: 'Mike Bravo', + U2ZH29DLL: 'Kilo Juliet', + U2ZH3MXB6: 'Mike Romeo', + U2ZHLLG77: 'Bravo Juliet', + U2ZV8E68N: 'Alpha Juliet', + U3007SQAK: 'Echo Oscar', + U300D7UDD: 'Juliet Papa Lima', + U3021E532: 'Alpha Hotel', + U3073MH8E: 'Mike Kilo', + U31CJM1LP: 'Yankee Lima', + U31E74VQF: 'Romeo Charlie', + U37DBNXB7: 'Mike Lima', + U37SW3HD1: 'Charlie Sierra', + U3KABQXLY: 'Juliet Charlie', + U3U5VJA2E: 'Romeo Golf', + U45HMKFJR: 'Charlie Mike', + U4NFD9J2G: 'Juliet Sierra', + URU0SCENN: 'Mike Lima', }; diff --git a/src/services/walkie/walkie.service.spec.ts b/src/services/walkie/walkie.service.spec.ts index a4ff5d5a..27927138 100644 --- a/src/services/walkie/walkie.service.spec.ts +++ b/src/services/walkie/walkie.service.spec.ts @@ -1,37 +1,31 @@ -import { WalkieService } from "./walkie.service"; +import { WalkieService } from './walkie.service'; -describe("slack-utils", () => { +describe('slack-utils', () => { let walkieService: WalkieService; beforeEach(() => { walkieService = new WalkieService(); }); - describe("walkieTalkie", () => { - it("convert a user id to NATO alphabet", () => { - const talked = walkieService.walkieTalkie( - "This this <@U2ZCMGB52 | whoever> test test" - ); - expect(talked).toBe( - `:walkietalkie: *chk* This this Juliet Foxtrot test test over. *chk* :walkietalkie:` - ); + describe('walkieTalkie', () => { + it('convert a user id to NATO alphabet', () => { + const talked = walkieService.walkieTalkie('This this <@U2ZCMGB52 | whoever> test test'); + expect(talked).toBe(`:walkietalkie: *chk* This this Juliet Foxtrot test test over. *chk* :walkietalkie:`); }); - it("should handle multiple user ids", () => { + it('should handle multiple user ids', () => { const talked = walkieService.walkieTalkie( - "This this <@U2ZCMGB52 | whoever> test test <@U45HMKFJR | charliemike>" + 'This this <@U2ZCMGB52 | whoever> test test <@U45HMKFJR | charliemike>', ); expect(talked).toBe( - `:walkietalkie: *chk* This this Juliet Foxtrot test test Charlie Mike over. *chk* :walkietalkie:` + `:walkietalkie: *chk* This this Juliet Foxtrot test test Charlie Mike over. *chk* :walkietalkie:`, ); }); - it("should handle nonexistent call signs", () => { - const talked = walkieService.walkieTalkie( - "This this <@2222 | whoever> test test <@2222 | charliemike>" - ); + it('should handle nonexistent call signs', () => { + const talked = walkieService.walkieTalkie('This this <@2222 | whoever> test test <@2222 | charliemike>'); expect(talked).toBe( - `:walkietalkie: *chk* This this <@2222 | whoever> test test <@2222 | charliemike> over. *chk* :walkietalkie:` + `:walkietalkie: *chk* This this <@2222 | whoever> test test <@2222 | charliemike> over. *chk* :walkietalkie:`, ); }); }); diff --git a/src/services/walkie/walkie.service.ts b/src/services/walkie/walkie.service.ts index 90e432ae..ffaf9095 100644 --- a/src/services/walkie/walkie.service.ts +++ b/src/services/walkie/walkie.service.ts @@ -1,14 +1,14 @@ -import { NATO_MAPPINGS } from "./constants"; +import { NATO_MAPPINGS } from './constants'; export class WalkieService { private userIdRegEx = /[<]@\w+/gm; - public getUserId(user: string) { + public getUserId(user: string): string { if (!user) { - return ""; + return ''; } const regArray = user.match(this.userIdRegEx); - return regArray ? regArray[0].slice(2) : ""; + return regArray ? regArray[0].slice(2) : ''; } public getNatoName(longUserId: string): string { @@ -17,7 +17,7 @@ export class WalkieService { return NATO_MAPPINGS[userId] || longUserId; } - public walkieTalkie(text: string) { + public walkieTalkie(text: string): string { if (!text || text.length === 0) { return text; } diff --git a/src/services/web/web.service.spec.ts b/src/services/web/web.service.spec.ts index 0860b3fd..49be060a 100644 --- a/src/services/web/web.service.spec.ts +++ b/src/services/web/web.service.spec.ts @@ -1,26 +1,26 @@ -import { WebService } from "./web.service"; +import { WebService } from './web.service'; -describe("WebService", () => { +describe('WebService', () => { let webService: WebService; beforeEach(() => { webService = WebService.getInstance(); }); - describe("sendMessage()", () => { - it("should be defined", () => { + describe('sendMessage()', () => { + it('should be defined', () => { expect(webService.sendMessage).toBeDefined(); }); }); - describe("deleteMessage()", () => { - it("should be defined", () => { + describe('deleteMessage()', () => { + it('should be defined', () => { expect(webService.deleteMessage).toBeDefined(); }); }); - describe("getAllUsers()", () => { - it("should be defined", () => { + describe('getAllUsers()', () => { + it('should be defined', () => { expect(webService.getAllUsers).toBeDefined(); }); }); diff --git a/src/services/web/web.service.ts b/src/services/web/web.service.ts index c6f9eac3..26065d94 100644 --- a/src/services/web/web.service.ts +++ b/src/services/web/web.service.ts @@ -2,11 +2,12 @@ import { ChatDeleteArguments, ChatPostMessageArguments, FilesUploadArguments, - WebClient -} from "@slack/web-api"; + WebAPICallResult, + WebClient, +} from '@slack/web-api'; export class WebService { - public static getInstance() { + public static getInstance(): WebService { if (!WebService.instance) { WebService.instance = new WebService(); } @@ -15,26 +16,25 @@ export class WebService { private static instance: WebService; private web: WebClient = new WebClient(process.env.muzzleBotToken); - private constructor() {} - /** * Handles deletion of messages. */ - public deleteMessage(channel: string, ts: string) { - const muzzleToken: any = process.env.muzzleBotToken; + public deleteMessage(channel: string, ts: string): void { + const muzzleToken: string | undefined = process.env.muzzleBotToken; const deleteRequest: ChatDeleteArguments = { token: muzzleToken, channel, ts, - as_user: true + // eslint-disable-next-line @typescript-eslint/camelcase + as_user: true, }; this.web.chat.delete(deleteRequest).catch(e => { - if (e.data.error === "message_not_found") { - console.log("Message already deleted, no need to retry"); + if (e.data.error === 'message_not_found') { + console.log('Message already deleted, no need to retry'); } else { console.error(e); - console.error("Unable to delete message. Retrying in 5 seconds..."); + console.error('Unable to delete message. Retrying in 5 seconds...'); setTimeout(() => this.deleteMessage(channel, ts), 5000); } }); @@ -43,29 +43,30 @@ export class WebService { /** * Handles sending messages to the chat. */ - public sendMessage(channel: string, text: string) { - const token: any = process.env.muzzleBotToken; + public sendMessage(channel: string, text: string): void { + const token: string | undefined = process.env.muzzleBotToken; const postRequest: ChatPostMessageArguments = { token, channel, - text + text, }; this.web.chat.postMessage(postRequest).catch(e => console.error(e)); } - public getAllUsers() { + public getAllUsers(): Promise { return this.web.users.list(); } - public uploadFile(channel: string, content: string, title?: string) { - const muzzleToken: any = process.env.muzzleBotUserToken; + public uploadFile(channel: string, content: string, title?: string): void { + const muzzleToken: string | undefined = process.env.muzzleBotUserToken; const uploadRequest: FilesUploadArguments = { channels: channel, content, - filetype: "markdown", + filetype: 'markdown', title, + // eslint-disable-next-line @typescript-eslint/camelcase initial_comment: title, - token: muzzleToken + token: muzzleToken, }; this.web.files.upload(uploadRequest).catch(e => console.error(e)); diff --git a/src/shared/db/models/Backfire.ts b/src/shared/db/models/Backfire.ts index 3f99c8a4..418ad36c 100644 --- a/src/shared/db/models/Backfire.ts +++ b/src/shared/db/models/Backfire.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Backfire { @@ -20,6 +20,6 @@ export class Backfire { @Column() public charactersSuppressed!: number; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) public createdAt!: Date; } diff --git a/src/shared/db/models/Counter.ts b/src/shared/db/models/Counter.ts index e1d68c7c..d749f6c3 100644 --- a/src/shared/db/models/Counter.ts +++ b/src/shared/db/models/Counter.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Counter { @@ -14,6 +14,6 @@ export class Counter { @Column() public countered!: boolean; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) public createdAt!: Date; } diff --git a/src/shared/db/models/List.ts b/src/shared/db/models/List.ts index a66acab3..d7d403b9 100644 --- a/src/shared/db/models/List.ts +++ b/src/shared/db/models/List.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class List { @@ -11,6 +11,6 @@ export class List { @Column() public text!: string; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) public createdAt!: Date; } diff --git a/src/shared/db/models/Muzzle.ts b/src/shared/db/models/Muzzle.ts index f75e5de9..6684ae35 100644 --- a/src/shared/db/models/Muzzle.ts +++ b/src/shared/db/models/Muzzle.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Muzzle { @@ -23,6 +23,6 @@ export class Muzzle { @Column() public charactersSuppressed!: number; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) public createdAt!: Date; } diff --git a/src/shared/db/models/Reaction.ts b/src/shared/db/models/Reaction.ts index 694bde91..e16efd1e 100644 --- a/src/shared/db/models/Reaction.ts +++ b/src/shared/db/models/Reaction.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Reaction { @@ -20,9 +20,9 @@ export class Reaction { @Column() public type!: string; - @Column({ default: "NOT_AVAILABLE" }) + @Column({ default: 'NOT_AVAILABLE' }) public channel!: string; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) public createdAt!: Date; } diff --git a/src/shared/db/models/Rep.ts b/src/shared/db/models/Rep.ts index ed9f046a..f7cd0263 100644 --- a/src/shared/db/models/Rep.ts +++ b/src/shared/db/models/Rep.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Rep { diff --git a/src/shared/models/backfire/backfire.model.ts b/src/shared/models/backfire/backfire.model.ts index 3b17772b..24c6d6ba 100644 --- a/src/shared/models/backfire/backfire.model.ts +++ b/src/shared/models/backfire/backfire.model.ts @@ -1,4 +1,4 @@ -export interface IBackfire { +export interface BackfireItem { suppressionCount: number; id: number; removalFn: NodeJS.Timeout; diff --git a/src/shared/models/counter/counter-models.ts b/src/shared/models/counter/counter-models.ts index 1a2aecd4..73882ace 100644 --- a/src/shared/models/counter/counter-models.ts +++ b/src/shared/models/counter/counter-models.ts @@ -1,10 +1,10 @@ -export interface ICounter { +export interface CounterItem { requestorId: string; counteredId: string; removalFn: NodeJS.Timeout; } -export interface ICounterMuzzle { +export interface CounterMuzzle { counterId: number; suppressionCount: number; removalFn: NodeJS.Timeout; diff --git a/src/shared/models/define/define-models.ts b/src/shared/models/define/define-models.ts index 015b2cb9..f7f9bc68 100644 --- a/src/shared/models/define/define-models.ts +++ b/src/shared/models/define/define-models.ts @@ -1,8 +1,8 @@ -export interface IUrbanDictionaryResponse { - list: IDefinition[]; +export interface UrbanDictionaryResponse { + list: Definition[]; } -export interface IDefinition { +export interface Definition { definition: string; permalink: string; thumbs_up: number; diff --git a/src/shared/models/muzzle/muzzle-models.ts b/src/shared/models/muzzle/muzzle-models.ts index 244c2e42..a9f61850 100644 --- a/src/shared/models/muzzle/muzzle-models.ts +++ b/src/shared/models/muzzle/muzzle-models.ts @@ -1,4 +1,4 @@ -export interface IMuzzled { +export interface Muzzled { suppressionCount: number; muzzledBy: string; id: number; @@ -6,21 +6,7 @@ export interface IMuzzled { removalFn: NodeJS.Timeout; } -export interface IRequestor { +export interface Requestor { muzzleCount: number; muzzleCountRemover?: NodeJS.Timeout; } - -export enum ReportType { - Trailing30 = "trailing30", - Week = "week", - Month = "month", - Year = "year", - AllTime = "all" -} - -export interface IReportRange { - start?: string; - end?: string; - reportType: ReportType; -} diff --git a/src/shared/models/reaction/ReactionByUser.model.ts b/src/shared/models/reaction/ReactionByUser.model.ts index 4f59d218..b3e3168a 100644 --- a/src/shared/models/reaction/ReactionByUser.model.ts +++ b/src/shared/models/reaction/ReactionByUser.model.ts @@ -1,4 +1,4 @@ -export interface IReactionByUser { +export interface ReactionByUser { reactingUser: string; rep: number; } diff --git a/src/shared/models/report/report.model.ts b/src/shared/models/report/report.model.ts new file mode 100644 index 00000000..054da948 --- /dev/null +++ b/src/shared/models/report/report.model.ts @@ -0,0 +1,42 @@ +export enum ReportType { + Trailing30 = 'trailing30', + Week = 'week', + Month = 'month', + Year = 'year', + AllTime = 'all', +} + +export interface ReportRange { + start?: string; + end?: string; + reportType: ReportType; +} + +export interface MuzzleReport { + muzzled: MuzzleReportItem; + muzzlers: MuzzleReportItem; + accuracy: Accuracy[]; + kdr: any[]; + rawNemesis: any[]; + successNemesis: any[]; +} + +export interface ReportCount { + slackId: string; + count: number; +} + +export interface Accuracy { + requestorId: number; + accuracy: number; + kills: number; + deaths: number; +} + +interface MuzzleReportItem { + byInstances: ReportCount[]; + byMessages: ReportCount[]; + byWords: ReportCount[]; + byChars: ReportCount[]; + byTime: ReportCount[]; +} diff --git a/src/shared/models/slack/slack-models.ts b/src/shared/models/slack/slack-models.ts index e3879fe6..5908feac 100644 --- a/src/shared/models/slack/slack-models.ts +++ b/src/shared/models/slack/slack-models.ts @@ -1,10 +1,10 @@ -export interface IChannelResponse { +export interface ChannelResponse { response_type: string; text: string; - attachments?: IAttachment[]; + attachments?: Attachment[]; } -export interface ISlashCommandRequest { +export interface SlashCommandRequest { token: string; team_id: string; team_domain: string; @@ -18,19 +18,19 @@ export interface ISlashCommandRequest { trigger_id: string; } -export interface IEventRequest { +export interface EventRequest { challenge: string; token: string; team_id: string; api_app_id: string; - event: IEvent; + event: Event; type: string; event_id: string; event_time: number; authed_users: string[]; } -export interface IEvent { +export interface Event { client_msg_id: string; type: string; subtype: string; @@ -42,7 +42,7 @@ export interface IEvent { event_ts: string; channel_type: string; authed_users: string[]; - attachments: IEvent[]; + attachments: Event[]; pretext: string; callback_id: string; item_user: string; @@ -50,13 +50,13 @@ export interface IEvent { item: any; // Needs work, not optional either. } -export interface IAttachment { +export interface Attachment { text: string; pretext?: string; mrkdown_in?: string[]; } -export interface ISlackUser { +export interface SlackUser { id: string; team_id: string; name: string; diff --git a/tslint.json b/tslint.json deleted file mode 100644 index a86652be..00000000 --- a/tslint.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": [ - "tslint:latest", - "tslint-plugin-prettier", - "tslint-config-prettier" - ], - "rules": { - "prettier": true, - "no-console": false, - "object-literal-sort-keys": false, - "no-implicit-dependencies": [true, "dev"] - }, - "linterOptions": { - "exclude": ["./*.json", "./*.md"] - } -} From 77937ab2fcb36b5b4e9ad1f0661b89d2f8bfdb73 Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Mon, 11 May 2020 14:48:37 -0400 Subject: [PATCH 078/167] Added oof (#83) --- src/services/reaction/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/reaction/constants.ts b/src/services/reaction/constants.ts index 47740555..aed1dd74 100644 --- a/src/services/reaction/constants.ts +++ b/src/services/reaction/constants.ts @@ -109,4 +109,5 @@ export const reactionValues: ReactionValue = { no_good: -1, chart_with_downwards_trend: -1, zzz: -1, + oof: -1, }; From 873407309e3e948ff181ea5ee37f155139e8cd4b Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Mon, 18 May 2020 00:20:51 -0400 Subject: [PATCH 079/167] Add DockerFile (#81) * Added dockerfile. Working on understanding secret management better * Added readme env variable config * Added better documentation, added DB_SEED. Documentation still needs work for setting up slack env * Movd jest-when and type to dev-dependnecies * Added --only=prod to the npm install command * Ran npm audit fix * Ran npm audit fix, removed no longer necessary packages and adjusted dev dependencies * Added TODO to readme * added check for env variables * Added better checks for env variables * updated readme with everything needed for local dev * Updated tsconfig, package.json and dockerfile * updated readme for missing commands * Updated tests * Added constant for MAX_WORD_LENGTH * Added MAX_WORD_LENGTH to counter * Removed case sensitivity for shouldBotMessageBeMuzzled Co-authored-by: sfreeman422 --- .DS_Store | Bin 0 -> 6148 bytes .dockerignore | 2 + DB_SEED.sql | 197 + Dockerfile | 8 +- README.md | 74 +- mocker.yaml | 32 + package-lock.json | 3898 +++++++++++++++----- package.json | 23 +- src/index.ts | 57 +- src/services/counter/counter.service.ts | 4 +- src/services/muzzle/constants.ts | 1 + src/services/muzzle/muzzle.service.spec.ts | 18 +- src/services/muzzle/muzzle.service.ts | 6 +- src/services/report/report.service.spec.ts | 2 +- src/services/web/web.service.ts | 8 +- tsconfig.prod.json | 4 + 16 files changed, 3265 insertions(+), 1069 deletions(-) create mode 100644 .DS_Store create mode 100644 .dockerignore create mode 100644 DB_SEED.sql create mode 100644 mocker.yaml create mode 100644 tsconfig.prod.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..70a0d73c08fa48ac18dfc1ab7291deac696c1c98 GIT binary patch literal 6148 zcmeHKu}%U(5S@j)2xuZP6dIeYjgipUSa6c#3-|yCV8Dd)h)82~oiVmr*!d$i7L=AG z7Jh;sU}E9S?jm~}5DQ`qnMrov-p;2)& zY24pl>hE{0y+KM$g^fvF=)w@1vFhMHBqMANUMV*|DJTvz>jdGH$>OLOA5eT8Xrl^) z86Dhe5t;n>Y!1hx;`QA`2a{`8pXZO&&8fOuZ~-CKK7%?0YBYo%J`HR|vY%gBajV!yC3CS&B^51N+rE^J#e0= z9@}aa5PG@H=4zMa)mQ;mU=0+Y{Xt?Q^bAHC)z*QXx&k0NIIRrFbeEtUY0xtmX+#g2 z(5Z+zRhTP=(CIiYO`K;i(x}rxn9GMSm4&&X2vr@|mns~Dr;&SBfE9=