From eb203a3b7a619f17aa6238677285ddb1b32fd83d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 10 Aug 2020 03:18:34 +0300 Subject: [PATCH] Use server timezone and date formats in case summaries. Link to cases in case log channel from case summaries. --- backend/src/data/GuildCases.ts | 28 ------- backend/src/plugins/Cases/CasesPlugin.ts | 7 ++ .../plugins/Cases/functions/getCaseSummary.ts | 56 ++++++++++++++ backend/src/plugins/Logs/LogsPlugin.ts | 2 + .../Logs/events/LogsGuildMemberAddEvt.ts | 4 +- .../ModActions/commands/CasesModCmd.ts | 38 +++++++--- .../ModActions/commands/CasesUserCmd.ts | 76 +++++++++++++------ backend/src/utils/getChunkedEmbedFields.ts | 23 ++++++ backend/src/utils/getGuildPrefix.ts | 6 ++ 9 files changed, 178 insertions(+), 62 deletions(-) create mode 100644 backend/src/plugins/Cases/functions/getCaseSummary.ts create mode 100644 backend/src/utils/getChunkedEmbedFields.ts create mode 100644 backend/src/utils/getGuildPrefix.ts diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts index 8e660311..dc2696b6 100644 --- a/backend/src/data/GuildCases.ts +++ b/backend/src/data/GuildCases.ts @@ -158,32 +158,4 @@ export class GuildCases extends BaseGuildRepository { case_id: caseId, }); } - - // TODO: Move this to the cases plugin, use server timezone + date formats - getSummaryText(theCase: Case) { - const firstNote = theCase.notes[0]; - let reason = firstNote ? firstNote.body : ""; - - if (reason.length > CASE_SUMMARY_REASON_MAX_LENGTH) { - const match = reason.slice(CASE_SUMMARY_REASON_MAX_LENGTH, 100).match(/(?:[.,!?\s]|$)/); - const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index : CASE_SUMMARY_REASON_MAX_LENGTH; - if (nextWhitespaceIndex < reason.length) { - reason = reason.slice(0, nextWhitespaceIndex - 1) + "..."; - } - } - - reason = disableLinkPreviews(reason); - - const timestamp = moment.utc(theCase.created_at, DBDateFormat).format("YYYY-MM-DD"); - let line = `\`[${timestamp}]\` \`Case #${theCase.case_number}\` __${CaseTypes[theCase.type]}__ ${reason}`; - if (theCase.notes.length > 1) { - line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`; - } - - if (theCase.is_hidden) { - line += " *(hidden)*"; - } - - return line; - } } diff --git a/backend/src/plugins/Cases/CasesPlugin.ts b/backend/src/plugins/Cases/CasesPlugin.ts index e9622f58..5eeb330c 100644 --- a/backend/src/plugins/Cases/CasesPlugin.ts +++ b/backend/src/plugins/Cases/CasesPlugin.ts @@ -11,6 +11,7 @@ import { CaseTypes } from "../../data/CaseTypes"; import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUserId"; import { getCaseEmbed } from "./functions/getCaseEmbed"; import { trimPluginDescription } from "../../utils"; +import { getCaseSummary } from "./functions/getCaseSummary"; const defaultOptions = { config: { @@ -61,6 +62,12 @@ export const CasesPlugin = zeppelinPlugin()("cases", { return getCaseEmbed(pluginData, caseOrCaseId); }; }, + + getCaseSummary(pluginData) { + return (caseOrCaseId: Case | number, withLinks = false) => { + return getCaseSummary(pluginData, caseOrCaseId, withLinks); + }; + }, }, onLoad(pluginData) { diff --git a/backend/src/plugins/Cases/functions/getCaseSummary.ts b/backend/src/plugins/Cases/functions/getCaseSummary.ts new file mode 100644 index 00000000..0dc83b4d --- /dev/null +++ b/backend/src/plugins/Cases/functions/getCaseSummary.ts @@ -0,0 +1,56 @@ +import { PluginData } from "knub"; +import { CasesPluginType } from "../types"; +import { disableLinkPreviews } from "../../../utils"; +import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; +import { CaseTypes } from "../../../data/CaseTypes"; +import moment from "moment-timezone"; +import { Case } from "../../../data/entities/Case"; +import { inGuildTz } from "../../../utils/timezones"; + +const CASE_SUMMARY_REASON_MAX_LENGTH = 300; + +export async function getCaseSummary( + pluginData: PluginData, + caseOrCaseId: Case | number, + withLinks = false, +) { + const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId; + + const theCase = await pluginData.state.cases.with("notes").find(caseId); + + const firstNote = theCase.notes[0]; + let reason = firstNote ? firstNote.body : ""; + + if (reason.length > CASE_SUMMARY_REASON_MAX_LENGTH) { + const match = reason.slice(CASE_SUMMARY_REASON_MAX_LENGTH, 100).match(/(?:[.,!?\s]|$)/); + const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index : CASE_SUMMARY_REASON_MAX_LENGTH; + if (nextWhitespaceIndex < reason.length) { + reason = reason.slice(0, nextWhitespaceIndex - 1) + "..."; + } + } + + reason = disableLinkPreviews(reason); + + const timestamp = moment.utc(theCase.created_at, DBDateFormat); + const prettyTimestamp = inGuildTz(pluginData, timestamp).format(getDateFormat(pluginData, "date")); + + let caseTitle = `\`Case #${theCase.case_number}\``; + if (withLinks && theCase.log_message_id) { + const [channelId, messageId] = theCase.log_message_id.split("-"); + caseTitle = `[${caseTitle}](https://discord.com/channels/${pluginData.guild.id}/${channelId}/${messageId})`; + } else { + caseTitle = `\`${caseTitle}\``; + } + const caseType = `__${CaseTypes[theCase.type]}__`; + + let line = `\`[${prettyTimestamp}]\` ${caseTitle} ${caseType} ${reason}`; + if (theCase.notes.length > 1) { + line += ` *(+${theCase.notes.length - 1} ${theCase.notes.length === 2 ? "note" : "notes"})*`; + } + + if (theCase.is_hidden) { + line += " *(hidden)*"; + } + + return line; +} diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index 0fb577ac..b7ef5132 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -21,6 +21,7 @@ import { getLogMessage } from "./util/getLogMessage"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { disableCodeBlocks } from "../../utils"; import { logger } from "../../logger"; +import { CasesPlugin } from "../Cases/CasesPlugin"; const defaultOptions: PluginOptions = { config: { @@ -48,6 +49,7 @@ export const LogsPlugin = zeppelinPlugin()("logs", { prettyName: "Logs", }, + dependencies: [CasesPlugin], configSchema: ConfigSchema, defaultOptions, diff --git a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts index 55146a30..687f491d 100644 --- a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts +++ b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts @@ -3,6 +3,7 @@ import { stripObjectToScalars } from "src/utils"; import { LogType } from "src/data/LogType"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; +import { CasesPlugin } from "../../Cases/CasesPlugin"; export const LogsGuildMemberAddEvt = logsEvent({ event: "guildMemberAdd", @@ -29,8 +30,9 @@ export const LogsGuildMemberAddEvt = logsEvent({ if (cases.length) { const recentCaseLines = []; const recentCases = cases.slice(0, 2); + const casesPlugin = pluginData.getPlugin(CasesPlugin); for (const theCase of recentCases) { - recentCaseLines.push(pluginData.state.cases.getSummaryText(theCase)); + recentCaseLines.push(await casesPlugin.getCaseSummary(theCase, true)); } let recentCaseSummary = recentCaseLines.join("\n"); diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts index a18812b5..6a5edeb6 100644 --- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts @@ -1,7 +1,13 @@ import { modActionsCommand } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; -import { trimLines, createChunkedMessage } from "src/utils"; +import { trimLines, createChunkedMessage, emptyEmbedValue } from "src/utils"; +import { CasesPlugin } from "../../Cases/CasesPlugin"; +import { asyncMap } from "../../../utils/async"; +import { EmbedOptions } from "eris"; +import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; +import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; +import { getGuildPrefix } from "../../../utils/getGuildPrefix"; const opts = { mod: ct.member({ option: true }), @@ -28,16 +34,26 @@ export const CasesModCmd = modActionsCommand({ if (recentCases.length === 0) { sendErrorMessage(pluginData, msg.channel, `No cases by **${modName}**`); } else { - const lines = recentCases.map(c => pluginData.state.cases.getSummaryText(c)); - const finalMessage = trimLines(` - Most recent 5 cases by **${modName}**: - - ${lines.join("\n")} - - Use the \`case \` command to see more info about individual cases - Use the \`cases \` command to see a specific user's cases - `); - createChunkedMessage(msg.channel, finalMessage); + const casesPlugin = pluginData.getPlugin(CasesPlugin); + const lines = await asyncMap(recentCases, c => casesPlugin.getCaseSummary(c, true)); + const prefix = getGuildPrefix(pluginData); + const embed: EmbedOptions = { + author: { + name: `Most recent 5 cases by ${modName}`, + icon_url: mod ? mod.avatarURL || mod.defaultAvatarURL : undefined, + }, + fields: [ + ...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")), + { + name: emptyEmbedValue, + value: trimLines(` + Use \`${prefix}case \` to see more information about an individual case + Use \`${prefix}cases \` to see a specific user's cases + `), + }, + ], + }; + msg.channel.createMessage({ embed }); } }, }); diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts index def88a0b..61e51b32 100644 --- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts +++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts @@ -2,7 +2,19 @@ import { modActionsCommand } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; import { CasesPlugin } from "src/plugins/Cases/CasesPlugin"; -import { UnknownUser, multiSorter, trimLines, createChunkedMessage, resolveUser } from "src/utils"; +import { + UnknownUser, + multiSorter, + trimLines, + createChunkedMessage, + resolveUser, + emptyEmbedValue, + chunkArray, +} from "src/utils"; +import { getGuildPrefix } from "../../../utils/getGuildPrefix"; +import { EmbedOptions, User } from "eris"; +import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields"; +import { asyncMap } from "../../../utils/async"; const opts = { expand: ct.bool({ option: true, isSwitch: true, shortcut: "e" }), @@ -54,30 +66,50 @@ export const CasesUserCmd = modActionsCommand({ } } else { // Compact view (= regular message with a preview of each case) - const lines = []; - for (const theCase of casesToDisplay) { - theCase.notes.sort(multiSorter(["created_at", "id"])); - const caseSummary = pluginData.state.cases.getSummaryText(theCase); - lines.push(caseSummary); - } + const casesPlugin = pluginData.getPlugin(CasesPlugin); + const lines = await asyncMap(casesToDisplay, c => casesPlugin.getCaseSummary(c, true)); - if (!args.hidden && hiddenCases.length) { - if (hiddenCases.length === 1) { - lines.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`); - } else { - lines.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`); + const prefix = getGuildPrefix(pluginData); + const linesPerChunk = 15; + const lineChunks = chunkArray(lines, linesPerChunk); + + const footerField = { + name: emptyEmbedValue, + value: trimLines(` + Use \`${prefix}case \` to see more information about an individual case + `), + }; + + for (const [i, linesInChunk] of lineChunks.entries()) { + const isLastChunk = i === lineChunks.length - 1; + + if (isLastChunk && !args.hidden && hiddenCases.length) { + if (hiddenCases.length === 1) { + linesInChunk.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`); + } else { + linesInChunk.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`); + } } + + const chunkStart = i * linesPerChunk + 1; + const chunkEnd = Math.min((i + 1) * linesPerChunk, lines.length); + + const embed: EmbedOptions = { + author: { + name: + lineChunks.length === 1 + ? `Cases for ${userName} (${lines.length} total)` + : `Cases ${chunkStart}–${chunkEnd} of ${lines.length} for ${userName}`, + icon_url: user instanceof User ? user.avatarURL || user.defaultAvatarURL : undefined, + }, + fields: [ + ...getChunkedEmbedFields(emptyEmbedValue, linesInChunk.join("\n")), + ...(isLastChunk ? [footerField] : []), + ], + }; + + msg.channel.createMessage({ embed }); } - - const finalMessage = trimLines(` - Cases for **${userName}**: - - ${lines.join("\n")} - - Use the \`case \` command to see more info about individual cases - `); - - createChunkedMessage(msg.channel, finalMessage); } } }, diff --git a/backend/src/utils/getChunkedEmbedFields.ts b/backend/src/utils/getChunkedEmbedFields.ts new file mode 100644 index 00000000..7b692164 --- /dev/null +++ b/backend/src/utils/getChunkedEmbedFields.ts @@ -0,0 +1,23 @@ +import { EmbedField } from "eris"; +import { chunkMessageLines, emptyEmbedValue } from "../utils"; + +export function getChunkedEmbedFields(name: string, value: string, inline?: boolean): EmbedField[] { + const fields: EmbedField[] = []; + + const chunks = chunkMessageLines(value, 1014); + for (let i = 0; i < chunks.length; i++) { + if (i === 0) { + fields.push({ + name, + value: chunks[i], + }); + } else { + fields.push({ + name: emptyEmbedValue, + value: chunks[i], + }); + } + } + + return fields; +} diff --git a/backend/src/utils/getGuildPrefix.ts b/backend/src/utils/getGuildPrefix.ts new file mode 100644 index 00000000..5ed32ed2 --- /dev/null +++ b/backend/src/utils/getGuildPrefix.ts @@ -0,0 +1,6 @@ +import { PluginData } from "knub"; +import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; + +export function getGuildPrefix(pluginData: PluginData) { + return pluginData.guildConfig.prefix || getDefaultPrefix(pluginData.client); +}