From b28ae509a0d9feff4f50ed7b7928f8798666c1d0 Mon Sep 17 00:00:00 2001 From: Zapdos26 Date: Fri, 20 Dec 2019 19:04:18 -0500 Subject: [PATCH] Added Unlock feature; Made a dynamic dropdown; Moved vote option creation to PollHelpers Prevent button press processing when vote is locked; Changed how "isLocked" status is determined; Add notification to user if their vote was blocked due to it being locked. Remove empty votes from results; Remove null from section, because null causes errors when sending the message. --- src/Actions.ts | 28 +++++++++--- src/Poll.ts | 108 ++++++++++++++++++++++++++------------------- src/PollHelpers.ts | 34 +++++++++++--- 3 files changed, 113 insertions(+), 57 deletions(-) diff --git a/src/Actions.ts b/src/Actions.ts index d2dc70a..b5b4921 100644 --- a/src/Actions.ts +++ b/src/Actions.ts @@ -1,7 +1,7 @@ -import { Poll } from "./Poll"; -import { WebClient, WebAPICallResult, ChatPostMessageArguments } from "@slack/web-api"; -import { KnownBlock } from "@slack/types"; -import { Request, Response } from "express"; +import {Poll} from "./Poll"; +import {ChatPostMessageArguments, WebAPICallResult, WebClient} from "@slack/web-api"; +import {KnownBlock} from "@slack/types"; +import {Request, Response} from "express"; import * as Sentry from "@sentry/node"; const errorMsg = "An error occurred; please contact the administrators for assistance."; @@ -36,7 +36,12 @@ export class Actions { const poll = new Poll(payload.message.blocks); poll.vote(payload.actions[0].text.text, payload.user.id); payload.message.blocks = poll.getBlocks(); - payload.message.text = "Vote changed!"; + // Sends user message if their vote was changed or blocked due to the poll being locked + const vote_text = poll.getLockedStatus() ? "You cannot vote after the poll has been locked!" : "Vote changed!"; + this.wc.chat.postEphemeral({ + channel: payload.channel.id, + text: vote_text, user: payload.user.id + }); // We respond with the new payload res(payload.message); // In case it is being slow users will see this message @@ -59,6 +64,9 @@ export class Actions { case "lock": this.onLockSelected(payload, poll); break; + case "unlock": + this.onUnlockSelected(payload, poll); + break; case "delete": this.onDeleteSelected(payload, poll); break; @@ -123,6 +131,16 @@ export class Actions { } } + private onUnlockSelected(payload: any, poll: Poll): void { + payload.message.text = "Poll unlocked!"; + if (Actions.isPollAuthor(payload, poll)) { + poll.unlockpoll(); + payload.message.blocks = poll.getBlocks(); + } else { + this.postEphemeralOnlyAuthor("unlock", "poll", payload.channel.id, payload.user.id); + } + } + private onDeleteSelected(payload: any, poll: Poll): void { if (Actions.isPollAuthor(payload, poll)) { payload.message.text = "This poll has been deleted."; diff --git a/src/Poll.ts b/src/Poll.ts index 6902995..97d19ae 100644 --- a/src/Poll.ts +++ b/src/Poll.ts @@ -1,7 +1,14 @@ import { - KnownBlock, SectionBlock, ContextBlock, Button, ActionsBlock, StaticSelect, PlainTextElement, MrkdwnElement + ActionsBlock, + Button, + ContextBlock, + KnownBlock, + MrkdwnElement, + PlainTextElement, + SectionBlock, + StaticSelect } from "@slack/types"; -import { PollHelpers } from "./PollHelpers"; +import {PollHelpers} from "./PollHelpers"; import * as Sentry from "@sentry/node"; export class Poll { @@ -11,6 +18,23 @@ export class Poll { private checkIfMsgContains(value: string): boolean { return this.getTitleFromMsg().includes(value); } + + private message: KnownBlock[] = []; + private multiple = false; + private anonymous = false; + private isLocked = false; + + constructor(message: KnownBlock[]) { + this.message = message; + // Since its databaseless the way we know if it is anonymous or multiple is by parsing the title + this.multiple = this.checkIfMsgContains("(Multiple Answers)"); + this.anonymous = this.checkIfMsgContains("(Anonymous)"); + // If there's no buttons then the poll is locked + if (this.message.length - 1 !== this.getDividerId()) { + this.isLocked = ((this.message[this.getDividerId() + 1] as SectionBlock).text as MrkdwnElement).text === ":lock:"; + } + } + static slashCreate(author: string, parameters: string[]): Poll { if (process.env.SENTRY_DSN) { Sentry.configureScope(scope => { @@ -34,23 +58,9 @@ export class Poll { const titleBlock = PollHelpers.buildSectionBlock(mrkdwnValue); message.push(titleBlock, PollHelpers.buildContextBlock(`Asked by: ${author}`)); - const actionBlocks: ActionsBlock[] = [{ type: "actions", elements: [] }]; - let actionBlockCount = 0; // Construct all the buttons const start = titleBlock.text!.text === parameters[0] ? 1 : 2; - for (let i = start; i < parameters.length; i++) { - if (i % 5 === 0) { - const newActionBlock: ActionsBlock = { type: "actions", elements: [] }; - actionBlocks.push(newActionBlock); - actionBlockCount++; - } - // Remove special characters, should be able to remove this once slack figures itself out - parameters[i] = parameters[i].replace("&", "+").replace(">", "greater than ") - .replace("<", "less than "); - // We set value to empty string so that it is always defined - const button: Button = { type: "button", value: " ", text: PollHelpers.buildTextElem(parameters[i]) }; - actionBlocks[actionBlockCount].elements.push(button); - } + const actionBlocks = PollHelpers.buildVoteOptions(parameters, start); // The various poll options const selection: StaticSelect = { type: "static_select", @@ -70,19 +80,6 @@ export class Poll { return new Poll(message); } - private message: KnownBlock[] = []; - private multiple = false; - private anonymous = false; - private isLocked = false; - constructor(message: KnownBlock[]) { - this.message = message; - // Since its databaseless the way we know if it is anonymous or multiple is by parsing the title - this.multiple = this.checkIfMsgContains("(Multiple Answers)"); - this.anonymous = this.checkIfMsgContains("(Anonymous)"); - // If there's no buttons then the poll is locked - this.isLocked = this.message[3].type === "divider"; - } - public getBlocks(): KnownBlock[] { return this.message; } @@ -100,19 +97,8 @@ export class Poll { return { votes, userIdIndex: votes.indexOf(userId) }; } - public resetVote(userId: string): void { - this.processButtons(this.message.length, button => { - const { votes, userIdIndex } = this.getVotesAndUserIndex(button, userId); - if (userIdIndex === -1) return false; - votes.splice(userIdIndex, 1); - button.value = votes.join(","); - // Optimization: why search the rest if we know they only have one vote? - return !this.multiple; - }); - this.generateVoteResults(); - } - public vote(buttonText: string, userId: string): void { + if (this.isLocked) return; this.processButtons(this.message.length, button => { const { votes, userIdIndex } = this.getVotesAndUserIndex(button, userId); if (!this.multiple && userIdIndex > -1 && button.text.text !== buttonText) { @@ -126,11 +112,42 @@ export class Poll { this.generateVoteResults(); } + + public resetVote(userId: string): void { + this.processButtons(this.message.length, button => { + const {votes, userIdIndex} = this.getVotesAndUserIndex(button, userId); + if (userIdIndex === -1) return false; + votes.splice(userIdIndex, 1); + button.value = votes.join(","); + // Optimization: why search the rest if we know they only have one vote? + return !this.multiple; + }); + this.generateVoteResults(); + } + public lockPoll(): void { - if (this.isLocked) return; this.isLocked = true; this.generateVoteResults(); - this.message = this.message.slice(0, 2).concat(this.message.slice(this.getDividerId() - 1)); + this.message = this.message.slice(0, this.getDividerId() - 1).concat(this.getDynamicSelect()).concat(this.message.slice(this.getDividerId())); + } + + public unlockpoll(): void { + this.isLocked = false; + this.message = this.message.slice(0, this.getDividerId() - 1).concat(this.getDynamicSelect()).concat({type: "divider"}).concat(this.message.slice(this.getDividerId() + 2)); + } + + private getDynamicSelect(): ActionsBlock[] { + const selection: StaticSelect = { + type: "static_select", + placeholder: PollHelpers.buildTextElem("Poll Options"), + options: [ + PollHelpers.buildSelectOption("Reset your vote", "reset"), + this.isLocked ? PollHelpers.buildSelectOption(":unlock: Unlock poll", "unlock") : PollHelpers.buildSelectOption(":lock: Lock poll", "lock"), + PollHelpers.buildSelectOption("Move to bottom", "bottom"), + PollHelpers.buildSelectOption("Delete poll", "delete") + ] + }; + return [{type: "actions", elements: [selection]}]; } // Creates the message that will be sent to the poll author with the final results @@ -141,6 +158,7 @@ export class Poll { ].concat(results); } + private processButtons(loopEnd: number, buttonCallback: (b: Button) => boolean): void { for (let i = 2; i < loopEnd; i++) { if (this.message[i].type !== "actions") continue; @@ -179,7 +197,7 @@ export class Poll { } // const sections = Object.keys(votes).map(key => this.buildVoteTally(overrideAnon, votes, key)); if (this.isLocked) sections.unshift(PollHelpers.buildSectionBlock(":lock:")); - return sections; + return sections.filter(f => f !== null); } private generateVoteResults(): void { diff --git a/src/PollHelpers.ts b/src/PollHelpers.ts index 00abd84..ccca0c6 100644 --- a/src/PollHelpers.ts +++ b/src/PollHelpers.ts @@ -1,6 +1,4 @@ -import { - SectionBlock, ContextBlock, PlainTextElement, Option -} from "@slack/types"; +import {ActionsBlock, ContextBlock, Option, PlainTextElement, SectionBlock} from "@slack/types"; export class PollHelpers { public static appendIfMatching(optionArray: string[], keyword: string, appendText: string): string { @@ -8,18 +6,40 @@ export class PollHelpers { } public static buildSectionBlock(mrkdwnValue: string): SectionBlock { - return { type: "section", text: { type: "mrkdwn", text: mrkdwnValue } }; + return {type: "section", text: {type: "mrkdwn", text: mrkdwnValue}}; } public static buildContextBlock(mrkdwnValue: string): ContextBlock { - return { type: "context", elements: [ { type: "mrkdwn", text: mrkdwnValue } ] }; + return {type: "context", elements: [{type: "mrkdwn", text: mrkdwnValue}]}; } public static buildSelectOption(text: string, value: string): Option { - return { text: this.buildTextElem(text), value: value }; + return {text: this.buildTextElem(text), value: value}; } public static buildTextElem(text: string): PlainTextElement { - return { type: "plain_text", text, emoji: true }; + return {type: "plain_text", text, emoji: true}; + } + + public static buildVoteOptions(parameters: string [], start: number): ActionsBlock[] { + const actionBlocks: ActionsBlock[] = [{type: "actions", elements: []}]; + let actionBlockCount = 0; + // Construct all the buttons + for (let i = start; i < parameters.length; i++) { + if (i % 5 === 0 && i != 0) { + actionBlocks.push({type: "actions", elements: []}); + actionBlockCount++; + } + // Remove special characters, should be able to remove this once slack figures itself out + parameters[i] = parameters[i].replace("&", "+").replace(">", "greater than ") + .replace("<", "less than "); + // We set value to empty string so that it is always defined + actionBlocks[actionBlockCount].elements.push({ + type: "button", + value: " ", + text: PollHelpers.buildTextElem(parameters[i]) + }); + } + return actionBlocks; } } \ No newline at end of file