3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Add case icons. Clean up !cases. Allow customizing case colors and icons.

This commit is contained in:
Dragory 2020-08-11 04:16:06 +03:00
parent ad24d166ce
commit 131a79ffd4
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
24 changed files with 138 additions and 22 deletions

BIN
assets/icons/case_ban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

BIN
assets/icons/case_kick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/case_mute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/case_note.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/case_unban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/icons/case_warn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -9,3 +9,20 @@ export enum CaseTypes {
Deleted, Deleted,
Softban, Softban,
} }
export const CaseNameToType = {
ban: CaseTypes.Ban,
unban: CaseTypes.Unban,
note: CaseTypes.Note,
warn: CaseTypes.Warn,
kick: CaseTypes.Kick,
mute: CaseTypes.Mute,
unmute: CaseTypes.Unmute,
deleted: CaseTypes.Deleted,
softban: CaseTypes.Softban,
};
export const CaseTypeToName = Object.entries(CaseNameToType).reduce((map, [name, type]) => {
map[type] = name;
return map;
}, {}) as Record<CaseTypes, string>;

View file

@ -19,6 +19,8 @@ const defaultOptions = {
case_log_channel: null, case_log_channel: null,
show_relative_times: true, show_relative_times: true,
relative_time_cutoff: "7d", relative_time_cutoff: "7d",
case_colors: null,
case_icons: null,
}, },
}; };

View file

@ -0,0 +1,13 @@
import { CaseTypes } from "../../data/CaseTypes";
export const caseAbbreviations = {
[CaseTypes.Ban]: "BAN",
[CaseTypes.Unban]: "UNBN",
[CaseTypes.Note]: "NOTE",
[CaseTypes.Warn]: "WARN",
[CaseTypes.Kick]: "KICK",
[CaseTypes.Mute]: "MUTE",
[CaseTypes.Unmute]: "UNMT",
[CaseTypes.Deleted]: "DEL",
[CaseTypes.Softban]: "SFTB",
};

View file

@ -1,12 +1,13 @@
import { CaseTypes } from "./CaseTypes"; import { CaseTypes } from "../../data/CaseTypes";
export const CaseTypeColors = { export const caseColors: Record<CaseTypes, number> = {
[CaseTypes.Ban]: 0xcb4314,
[CaseTypes.Unban]: 0x9b59b6,
[CaseTypes.Note]: 0x3498db, [CaseTypes.Note]: 0x3498db,
[CaseTypes.Warn]: 0xdae622, [CaseTypes.Warn]: 0xdae622,
[CaseTypes.Mute]: 0xe6b122, [CaseTypes.Mute]: 0xe6b122,
[CaseTypes.Unmute]: 0xa175b3, [CaseTypes.Unmute]: 0xa175b3,
[CaseTypes.Kick]: 0xe67e22, [CaseTypes.Kick]: 0xe67e22,
[CaseTypes.Deleted]: 0x000000,
[CaseTypes.Softban]: 0xe67e22, [CaseTypes.Softban]: 0xe67e22,
[CaseTypes.Ban]: 0xcb4314,
[CaseTypes.Unban]: 0x9b59b6,
}; };

View file

@ -0,0 +1,13 @@
import { CaseTypes } from "../../data/CaseTypes";
export const caseIcons: Record<CaseTypes, string> = {
[CaseTypes.Ban]: "<:case_ban:742540201443721317>",
[CaseTypes.Unban]: "<:case_unban:742540201670475846>",
[CaseTypes.Note]: "<:case_note:742540201368485950>",
[CaseTypes.Warn]: "<:case_warn:742540201624338454>",
[CaseTypes.Kick]: "<:case_kick:742540201661825165>",
[CaseTypes.Mute]: "<:case_mute:742540201817145364>",
[CaseTypes.Unmute]: "<:case_unmute:742540201489858643>",
[CaseTypes.Deleted]: "<:case_deleted:742540201473343529>",
[CaseTypes.Softban]: "<:case_softban:742540201766813747>",
};

View file

@ -0,0 +1,8 @@
import { PluginData } from "knub";
import { CasesPluginType } from "../types";
import { CaseTypes, CaseTypeToName } from "../../../data/CaseTypes";
import { caseColors } from "../caseColors";
export function getCaseColor(pluginData: PluginData<CasesPluginType>, caseType: CaseTypes) {
return pluginData.config.get().case_colors?.[CaseTypeToName[caseType]] ?? caseColors[caseType];
}

View file

@ -4,11 +4,11 @@ import moment from "moment-timezone";
import { CaseTypes } from "../../../data/CaseTypes"; import { CaseTypes } from "../../../data/CaseTypes";
import { PluginData, helpers } from "knub"; import { PluginData, helpers } from "knub";
import { CasesPluginType } from "../types"; import { CasesPluginType } from "../types";
import { CaseTypeColors } from "../../../data/CaseTypeColors";
import { resolveCaseId } from "./resolveCaseId"; import { resolveCaseId } from "./resolveCaseId";
import { chunkLines, chunkMessageLines, emptyEmbedValue, messageLink } from "../../../utils"; import { chunkLines, chunkMessageLines, emptyEmbedValue, messageLink } from "../../../utils";
import { inGuildTz } from "../../../utils/timezones"; import { inGuildTz } from "../../../utils/timezones";
import { getDateFormat } from "../../../utils/dateFormats"; import { getDateFormat } from "../../../utils/dateFormats";
import { getCaseColor } from "./getCaseColor";
export async function getCaseEmbed( export async function getCaseEmbed(
pluginData: PluginData<CasesPluginType>, pluginData: PluginData<CasesPluginType>,
@ -53,9 +53,7 @@ export async function getCaseEmbed(
embed.title += " (hidden)"; embed.title += " (hidden)";
} }
if (CaseTypeColors[theCase.type]) { embed.color = getCaseColor(pluginData, theCase.type);
embed.color = CaseTypeColors[theCase.type];
}
if (theCase.notes.length) { if (theCase.notes.length) {
theCase.notes.forEach((note: any) => { theCase.notes.forEach((note: any) => {

View file

@ -0,0 +1,8 @@
import { PluginData } from "knub";
import { CasesPluginType } from "../types";
import { CaseTypes, CaseTypeToName } from "../../../data/CaseTypes";
import { caseIcons } from "../caseIcons";
export function getCaseIcon(pluginData: PluginData<CasesPluginType>, caseType: CaseTypes) {
return pluginData.config.get().case_icons?.[CaseTypeToName[caseType]] ?? caseIcons[caseType];
}

View file

@ -1,17 +1,19 @@
import { PluginData } from "knub"; import { PluginData } from "knub";
import { CasesPluginType } from "../types"; import { CasesPluginType } from "../types";
import { convertDelayStringToMS, DAYS, disableLinkPreviews, messageLink } from "../../../utils"; import { convertDelayStringToMS, DAYS, disableLinkPreviews, emptyEmbedValue, messageLink } from "../../../utils";
import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats"; import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
import { CaseTypes } from "../../../data/CaseTypes"; import { CaseTypes, CaseTypeToName } from "../../../data/CaseTypes";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { Case } from "../../../data/entities/Case"; import { Case } from "../../../data/entities/Case";
import { inGuildTz } from "../../../utils/timezones"; import { inGuildTz } from "../../../utils/timezones";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { humanizeDurationShort } from "../../../humanizeDurationShort";
import { caseAbbreviations } from "../caseAbbreviations";
import { getCaseIcon } from "./getCaseIcon";
const CASE_SUMMARY_REASON_MAX_LENGTH = 300; const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
const INCLUDE_MORE_NOTES_THRESHOLD = 20; const INCLUDE_MORE_NOTES_THRESHOLD = 20;
const UPDATED_STR = "__[Update]__"; const UPDATE_STR = "**[Update]**";
const RELATIVE_TIME_THRESHOLD = 7 * DAYS; const RELATIVE_TIME_THRESHOLD = 7 * DAYS;
@ -20,8 +22,9 @@ export async function getCaseSummary(
caseOrCaseId: Case | number, caseOrCaseId: Case | number,
withLinks = false, withLinks = false,
) { ) {
const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId; const config = pluginData.config.get();
const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId;
const theCase = await pluginData.state.cases.with("notes").find(caseId); const theCase = await pluginData.state.cases.with("notes").find(caseId);
const firstNote = theCase.notes[0]; const firstNote = theCase.notes[0];
@ -29,8 +32,8 @@ export async function getCaseSummary(
let leftoverNotes = Math.max(0, theCase.notes.length - 1); let leftoverNotes = Math.max(0, theCase.notes.length - 1);
for (let i = 1; i < theCase.notes.length; i++) { for (let i = 1; i < theCase.notes.length; i++) {
if (reason.length >= CASE_SUMMARY_REASON_MAX_LENGTH - UPDATED_STR.length - INCLUDE_MORE_NOTES_THRESHOLD) break; if (reason.length >= CASE_SUMMARY_REASON_MAX_LENGTH - UPDATE_STR.length - INCLUDE_MORE_NOTES_THRESHOLD) break;
reason += ` ${UPDATED_STR} ${theCase.notes[i].body}`; reason += ` ${UPDATE_STR} ${theCase.notes[i].body}`;
leftoverNotes--; leftoverNotes--;
} }
@ -45,23 +48,26 @@ export async function getCaseSummary(
reason = disableLinkPreviews(reason); reason = disableLinkPreviews(reason);
const timestamp = moment.utc(theCase.created_at, DBDateFormat); const timestamp = moment.utc(theCase.created_at, DBDateFormat);
const config = pluginData.config.get();
const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff); const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff);
const useRelativeTime = config.show_relative_times && Date.now() - timestamp.valueOf() < relativeTimeCutoff; const useRelativeTime = config.show_relative_times && Date.now() - timestamp.valueOf() < relativeTimeCutoff;
const prettyTimestamp = useRelativeTime const prettyTimestamp = useRelativeTime
? moment.utc().to(timestamp) ? moment.utc().to(timestamp)
: inGuildTz(pluginData, timestamp).format(getDateFormat(pluginData, "date")); : inGuildTz(pluginData, timestamp).format(getDateFormat(pluginData, "date"));
let caseTitle = `\`Case #${theCase.case_number}\``; const icon = getCaseIcon(pluginData, theCase.type);
let caseTitle = `\`#${theCase.case_number}\``;
if (withLinks && theCase.log_message_id) { if (withLinks && theCase.log_message_id) {
const [channelId, messageId] = theCase.log_message_id.split("-"); const [channelId, messageId] = theCase.log_message_id.split("-");
caseTitle = `[${caseTitle}](${messageLink(pluginData.guild.id, channelId, messageId)})`; caseTitle = `[${caseTitle}](${messageLink(pluginData.guild.id, channelId, messageId)})`;
} else { } else {
caseTitle = `\`${caseTitle}\``; caseTitle = `\`${caseTitle}\``;
} }
const caseType = `__${CaseTypes[theCase.type]}__`;
let line = `\`[${prettyTimestamp}]\` ${caseTitle} ${caseType} ${reason}`; let caseType = (caseAbbreviations[theCase.type] || String(theCase.type)).toUpperCase();
caseType = (caseType + " ").slice(0, 4);
let line = `${icon} **\`${caseType}\`** \`[${prettyTimestamp}]\` ${caseTitle} ${reason}`;
if (leftoverNotes > 1) { if (leftoverNotes > 1) {
line += ` *(+${leftoverNotes} ${leftoverNotes === 1 ? "note" : "notes"})*`; line += ` *(+${leftoverNotes} ${leftoverNotes === 1 ? "note" : "notes"})*`;
} }
@ -70,5 +76,5 @@ export async function getCaseSummary(
line += " *(hidden)*"; line += " *(hidden)*";
} }
return line; return line.trim();
} }

View file

@ -1,16 +1,19 @@
import * as t from "io-ts"; import * as t from "io-ts";
import { tDelayString, tNullable } from "../../utils"; import { tDelayString, tPartialDictionary, tNullable } from "../../utils";
import { CaseTypes } from "../../data/CaseTypes"; import { CaseNameToType, CaseTypes } from "../../data/CaseTypes";
import { BasePluginType } from "knub"; import { BasePluginType } from "knub";
import { GuildLogs } from "../../data/GuildLogs"; import { GuildLogs } from "../../data/GuildLogs";
import { GuildCases } from "../../data/GuildCases"; import { GuildCases } from "../../data/GuildCases";
import { GuildArchives } from "../../data/GuildArchives"; import { GuildArchives } from "../../data/GuildArchives";
import { tColor } from "../../utils/tColor";
export const ConfigSchema = t.type({ export const ConfigSchema = t.type({
log_automatic_actions: t.boolean, log_automatic_actions: t.boolean,
case_log_channel: tNullable(t.string), case_log_channel: tNullable(t.string),
show_relative_times: t.boolean, show_relative_times: t.boolean,
relative_time_cutoff: tDelayString, relative_time_cutoff: tDelayString,
case_colors: tNullable(tPartialDictionary(t.keyof(CaseNameToType), tColor)),
case_icons: tNullable(tPartialDictionary(t.keyof(CaseNameToType), t.string)),
}); });
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>; export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;

View file

@ -56,7 +56,7 @@ export const PostEmbedCmd = postCmd({
try { try {
parsed = JSON.parse(content); parsed = JSON.parse(content);
} catch (e) { } catch (e) {
sendErrorMessage(pluginData, msg.channel, "Syntax error in embed JSON"); sendErrorMessage(pluginData, msg.channel, `Syntax error in embed JSON: ${e.message}`);
return; return;
} }

View file

@ -37,6 +37,7 @@ import { either } from "fp-ts/lib/Either";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { SimpleCache } from "./SimpleCache"; import { SimpleCache } from "./SimpleCache";
import { logger } from "./logger"; import { logger } from "./logger";
import { unsafeCoerce } from "fp-ts/lib/function";
const fsp = fs.promises; const fsp = fs.promises;
@ -152,6 +153,29 @@ function tDeepPartialProp(prop: any) {
// https://stackoverflow.com/a/49262929/316944 // https://stackoverflow.com/a/49262929/316944
export type Not<T, E> = T & Exclude<T, E>; export type Not<T, E> = T & Exclude<T, E>;
// io-ts partial dictionary type
// From https://github.com/gcanti/io-ts/issues/429#issuecomment-655394345
export interface PartialDictionaryC<D extends t.Mixed, C extends t.Mixed>
extends t.DictionaryType<
D,
C,
{
[K in t.TypeOf<D>]?: t.TypeOf<C>;
},
{
[K in t.OutputOf<D>]?: t.OutputOf<C>;
},
unknown
> {}
export const tPartialDictionary = <D extends t.Mixed, C extends t.Mixed>(
domain: D,
codomain: C,
name?: string,
): PartialDictionaryC<D, C> => {
return unsafeCoerce(t.record(t.union([domain, t.undefined]), codomain, name));
};
/** /**
* Mirrors EmbedOptions from Eris * Mirrors EmbedOptions from Eris
*/ */

View file

@ -0,0 +1,6 @@
export function intToRgb(int: number): [number, number, number] {
const r = int >> 16;
const g = (int - (r << 16)) >> 8;
const b = int - (r << 16) - (g << 8);
return [r, g, b];
}

View file

@ -0,0 +1,17 @@
import * as t from "io-ts";
import { either } from "fp-ts/lib/Either";
import { convertDelayStringToMS } from "../utils";
import { parseColor } from "./parseColor";
import { rgbToInt } from "./rgbToInt";
import { intToRgb } from "./intToRgb";
export const tColor = new t.Type<number, string>(
"tColor",
(s): s is number => typeof s === "number",
(from, to) =>
either.chain(t.string.validate(from, to), input => {
const parsedColor = parseColor(input);
return parsedColor == null ? t.failure(from, to, "Invalid color") : t.success(rgbToInt(parsedColor));
}),
s => intToRgb(s).join(","),
);