diff --git a/.clabot b/.clabot index b1bca0bb..d75794c9 100644 --- a/.clabot +++ b/.clabot @@ -11,7 +11,8 @@ "metal0", "roflmaoqwerty", "usoka", - "vcokltfre" + "vcokltfre", + "Rstar284" ], "message": "Thank you for contributing to Zeppelin! We require contributors to sign our Contributor License Agreement (CLA). To let us review and merge your code, please visit https://github.com/ZeppelinBot/CLA to sign the CLA!" } diff --git a/backend/src/data/ApiLogins.ts b/backend/src/data/ApiLogins.ts index ab8779d0..5ad9c262 100644 --- a/backend/src/data/ApiLogins.ts +++ b/backend/src/data/ApiLogins.ts @@ -90,10 +90,15 @@ export class ApiLogins extends BaseRepository { const [loginId, token] = apiKey.split("."); if (!loginId || !token) return; + const updatedTime = moment().utc().add(LOGIN_EXPIRY_TIME, "ms"); + + const login = await this.apiLogins.createQueryBuilder().where("id = :id", { id: loginId }).getOne(); + if (!login || moment.utc(login.expires_at).isSameOrAfter(updatedTime)) return; + await this.apiLogins.update( { id: loginId }, { - expires_at: moment().utc().add(LOGIN_EXPIRY_TIME, "ms").format(DBDateFormat), + expires_at: updatedTime.format(DBDateFormat), }, ); } diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index e32279a7..9cd0fd86 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -25,7 +25,7 @@ export const BanAction = automodAction({ const reason = actionConfig.reason || "Kicked automatically"; const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined; - const deleteMessageDays = actionConfig.deleteMessageDays || undefined; + const deleteMessageDays = actionConfig.deleteMessageDays ?? undefined; const caseArgs: Partial = { modId: pluginData.client.user!.id, diff --git a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts index d905fbc0..142e7b0e 100644 --- a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts +++ b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts @@ -72,7 +72,7 @@ export const MatchAttachmentTypeTrigger = automodTrigger()({ return ( asSingleLine(` Matched attachment type \`${Util.escapeInlineCode(matchResult.extra.matchedType)}\` - (${matchResult.extra.mode === "blacklist" ? "(blacklisted)" : "(not in whitelist)"}) + (${matchResult.extra.mode === "blacklist" ? "blacklisted" : "not in whitelist"}) in message (\`${contexts[0].message!.id}\`) in ${prettyChannel}: `) + messageSummary(contexts[0].message!) ); diff --git a/backend/src/plugins/Cases/functions/createCase.ts b/backend/src/plugins/Cases/functions/createCase.ts index 9df38f26..0a0925d7 100644 --- a/backend/src/plugins/Cases/functions/createCase.ts +++ b/backend/src/plugins/Cases/functions/createCase.ts @@ -1,3 +1,4 @@ +import type { Snowflake } from "discord-api-types/globals"; import { GuildPluginData } from "knub"; import { logger } from "../../../logger"; import { resolveUser } from "../../../utils"; @@ -13,9 +14,11 @@ export async function createCase(pluginData: GuildPluginData, a const modName = mod.tag; let ppName: string | null = null; + let ppId: Snowflake | null = null; if (args.ppId) { const pp = await resolveUser(pluginData.client, args.ppId); ppName = pp.tag; + ppId = pp.id; } if (args.auditLogId) { @@ -28,20 +31,20 @@ export async function createCase(pluginData: GuildPluginData, a const createdCase = await pluginData.state.cases.create({ type: args.type, - user_id: args.userId, + user_id: user.id, user_name: userName, - mod_id: args.modId, + mod_id: mod.id, mod_name: modName, audit_log_id: args.auditLogId, - pp_id: args.ppId, + pp_id: ppId, pp_name: ppName, is_hidden: Boolean(args.hide), }); - if (args.reason || (args.noteDetails && args.noteDetails.length)) { + if (args.reason || args.noteDetails?.length) { await createCaseNote(pluginData, { caseId: createdCase.id, - modId: args.modId, + modId: mod.id, body: args.reason || "", automatic: args.automatic, postInCaseLogOverride: false, @@ -53,7 +56,7 @@ export async function createCase(pluginData: GuildPluginData, a for (const extraNote of args.extraNotes) { await createCaseNote(pluginData, { caseId: createdCase.id, - modId: args.modId, + modId: mod.id, body: extraNote, automatic: args.automatic, postInCaseLogOverride: false, diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index 21d02738..9833ce2c 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -85,7 +85,7 @@ export async function banUserId( pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, userId); ignoreEvent(pluginData, IgnoredEventType.Ban, userId); try { - const deleteMessageDays = Math.min(30, Math.max(0, banOptions.deleteMessageDays ?? 1)); + const deleteMessageDays = Math.min(7, Math.max(0, banOptions.deleteMessageDays ?? 1)); await pluginData.guild.bans.create(userId as Snowflake, { days: deleteMessageDays, reason: reason ?? undefined, diff --git a/backend/src/plugins/Phisherman/info.ts b/backend/src/plugins/Phisherman/info.ts index 5c9a1818..5c1ea94f 100644 --- a/backend/src/plugins/Phisherman/info.ts +++ b/backend/src/plugins/Phisherman/info.ts @@ -8,7 +8,7 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = { `), configurationGuide: trimPluginDescription(` ### Getting started - To get started, request an API key for Phisherman following the instructions at https://docs.phisherman.gg/#/api/getting-started?id=requesting-api-access. + To get started, request an API key for Phisherman following the instructions at https://docs.phisherman.gg/guide/getting-started.html#requesting-api-access Then, add the api key to the plugin's config: ~~~yml diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index fe44af2d..86305c2d 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -7,8 +7,9 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildTags } from "../../data/GuildTags"; +import { GuildTagAliases } from "src/data/GuildTagAliases"; import { mapToPublicFn } from "../../pluginUtils"; -import { convertDelayStringToMS } from "../../utils"; +import { convertDelayStringToMS, trimPluginDescription } from "../../utils"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; import { TagCreateCmd } from "./commands/TagCreateCmd"; @@ -22,7 +23,8 @@ import { onMessageCreate } from "./util/onMessageCreate"; import { onMessageDelete } from "./util/onMessageDelete"; import { renderTagBody } from "./util/renderTagBody"; import { LogsPlugin } from "../Logs/LogsPlugin"; -import { GuildTagAliases } from "src/data/GuildTagAliases"; +import { generateTemplateMarkdown } from "./docs"; +import { TemplateFunctions } from "./templateFunctions"; const defaultOptions: PluginOptions = { config: { @@ -59,6 +61,17 @@ export const TagsPlugin = zeppelinGuildPlugin()({ showInDocs: true, info: { prettyName: "Tags", + description: "Tags are a way to store and reuse information.", + configurationGuide: trimPluginDescription(` + ### Template Functions + You can use template functions in your tags. These functions are called when the tag is rendered. + You can use these functions to render dynamic content, or to access information from the message and/or user calling the tag. + You use them by adding a \`{}\` on your tag. + + Here are the functions you can use in your tags: + + ${generateTemplateMarkdown(TemplateFunctions)} + `), }, configSchema: ConfigSchema, diff --git a/backend/src/plugins/Tags/docs.ts b/backend/src/plugins/Tags/docs.ts new file mode 100644 index 00000000..efdb0b86 --- /dev/null +++ b/backend/src/plugins/Tags/docs.ts @@ -0,0 +1,17 @@ +import { trimPluginDescription } from "src/utils"; +import { TemplateFunction } from "./types"; + +export function generateTemplateMarkdown(definitions: TemplateFunction[]): string { + return definitions + .map(def => { + const usage = def.signature ?? `(${def.arguments.join(", ")})`; + const examples = def.examples?.map(ex => `> \`{${ex}}\``).join("\n") ?? null; + return trimPluginDescription(` + ## ${def.name} + **${def.description}**\n + __Usage__: \`{${def.name}${usage}}\`\n + ${examples ? `__Examples__:\n${examples}` : ""}\n\n + `); + }) + .join("\n\n"); +} diff --git a/backend/src/plugins/Tags/templateFunctions.ts b/backend/src/plugins/Tags/templateFunctions.ts new file mode 100644 index 00000000..67a49069 --- /dev/null +++ b/backend/src/plugins/Tags/templateFunctions.ts @@ -0,0 +1,166 @@ +import { TemplateFunction } from "./types"; + +// TODO: Generate this dynamically, lmao +export const TemplateFunctions: TemplateFunction[] = [ + { + name: "if", + description: "Checks if a condition is true or false and returns the corresponding ifTrue or ifFalse", + returnValue: "boolean", + arguments: ["condition", "ifTrue", "ifFalse"], + examples: ['if(user.bot, "User is a bot", "User is not a bot")'], + }, + { + name: "and", + description: "Checks if all provided conditions are true", + returnValue: "boolean", + arguments: ["condition1", "condition2", "..."], + examples: ["and(user.bot, user.verified)"], + }, + { + name: "or", + description: "Checks if atleast one of the provided conditions is true", + returnValue: "boolean", + arguments: ["condition1", "condition2", "..."], + examples: ["or(user.bot, user.verified)"], + }, + { + name: "not", + description: "Checks if the provided condition is false", + returnValue: "boolean", + arguments: ["condition"], + examples: ["not(user.bot)"], + }, + { + name: "concat", + description: "Concatenates several arguments into a string", + returnValue: "string", + arguments: ["argument1", "argument2", "..."], + examples: ['concat("Hello ", user.username, "!")'], + }, + { + name: "concatArr", + description: "Joins a array with the provided separator", + returnValue: "string", + arguments: ["array", "separator"], + examples: ['concatArr(["Hello", "World"], " ")'], + }, + { + name: "eq", + description: "Checks if all provided arguments are equal to each other", + returnValue: "boolean", + arguments: ["argument1", "argument2", "..."], + examples: ['eq(user.id, "106391128718245888")'], + }, + { + name: "gt", + description: "Checks if the first argument is greater than the second", + returnValue: "boolean", + arguments: ["argument1", "argument2"], + examples: ["gt(5, 2)"], + }, + { + name: "gte", + description: "Checks if the first argument is greater or equal to the second", + returnValue: "boolean", + arguments: ["argument1", "argument2"], + examples: ["gte(2, 2)"], + }, + { + name: "lt", + description: "Checks if the first argument is smaller than the second", + returnValue: "boolean", + arguments: ["argument1", "argument2"], + examples: ["lt(2, 5)"], + }, + { + name: "lte", + description: "Checks if the first argument is smaller or equal to the second", + returnValue: "boolean", + arguments: ["argument1", "argument2"], + examples: ["lte(2, 2)"], + }, + { + name: "slice", + description: "Slices a string argument at start and end", + returnValue: "string", + arguments: ["string", "start", "end"], + examples: ['slice("Hello World", 0, 5)'], + }, + { + name: "lower", + description: "Converts a string argument to lowercase", + returnValue: "string", + arguments: ["string"], + examples: ['lower("Hello World")'], + }, + { + name: "upper", + description: "Converts a string argument to uppercase", + returnValue: "string", + arguments: ["string"], + examples: ['upper("Hello World")'], + }, + { + name: "upperFirst", + description: "Converts the first character of a string argument to uppercase", + returnValue: "string", + arguments: ["string"], + examples: ['upperFirst("hello World")'], + }, + { + name: "rand", + description: "Returns a random number between from and to, optionally using seed", + returnValue: "number", + arguments: ["from", "to", "seed"], + examples: ["rand(1, 10)"], + }, + { + name: "round", + description: "Rounds a number to the given decimal places", + returnValue: "number", + arguments: ["number", "decimalPlaces"], + examples: ["round(1.2345, 2)"], + }, + { + name: "add", + description: "Adds two or more numbers", + returnValue: "number", + arguments: ["number1", "number2", "..."], + examples: ["add(1, 2)"], + }, + { + name: "sub", + description: "Subtracts two or more numbers", + returnValue: "number", + arguments: ["number1", "number2", "..."], + examples: ["sub(3, 1)"], + }, + { + name: "mul", + description: "Multiplies two or more numbers", + returnValue: "number", + arguments: ["number1", "number2", "..."], + examples: ["mul(2, 3)"], + }, + { + name: "div", + description: "Divides two or more numbers", + returnValue: "number", + arguments: ["number1", "number2", "..."], + examples: ["div(6, 2)"], + }, + { + name: "cases", + description: "Returns the argument at position", + returnValue: "any", + arguments: ["position", "argument1", "argument2", "..."], + examples: ['cases(1, "Hello", "World")'], + }, + { + name: "choose", + description: "Returns a random argument", + returnValue: "any", + arguments: ["argument1", "argument2", "..."], + examples: ['choose("Hello", "World", "!")'], + }, +]; diff --git a/backend/src/plugins/Tags/types.ts b/backend/src/plugins/Tags/types.ts index eb61441b..ccce4198 100644 --- a/backend/src/plugins/Tags/types.ts +++ b/backend/src/plugins/Tags/types.ts @@ -61,5 +61,14 @@ export interface TagsPluginType extends BasePluginType { }; } +export interface TemplateFunction { + name: string; + description: string; + arguments: string[]; + returnValue: string; + signature?: string; + examples?: string[]; +} + export const tagsCmd = typedGuildCommand(); export const tagsEvt = typedGuildEventListener(); diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts index 36a664e3..f58f61be 100644 --- a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -1,8 +1,9 @@ -import { MessageEmbedOptions, Role } from "discord.js"; +import { MessageEmbedOptions, Permissions, Role } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import moment from "moment-timezone"; import { EmbedWith, preEmbedPadding, trimLines } from "../../../utils"; +import { PERMISSION_NAMES } from "../../../utils/permissionNames.js"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; @@ -35,14 +36,9 @@ export async function getRoleInfoEmbed( round: true, }); - const rolePerms = Object.keys(role.permissions.toJSON()).map((p) => - p - // Voice channel related permission names start with 'voice' - .replace(/^voice/i, "") - .replace(/([a-z])([A-Z])/g, "$1 $2") - .toLowerCase() - .replace(/(^\w{1})|(\s{1}\w{1})/g, (l) => l.toUpperCase()), - ); + const rolePerms = role.permissions.has(Permissions.FLAGS.ADMINISTRATOR) + ? [PERMISSION_NAMES.ADMINISTRATOR] + : role.permissions.toArray().map((p) => PERMISSION_NAMES[p]); // -1 because of the @everyone role const totalGuildRoles = pluginData.guild.roles.cache.size - 1; diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 39cf4166..e775205c 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -16,6 +16,13 @@ import { import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; +const MAX_ROLES_TO_DISPLAY = 15; + +const trimRoles = (roles: string[]) => + roles.length > MAX_ROLES_TO_DISPLAY + ? roles.slice(0, MAX_ROLES_TO_DISPLAY).join(", ") + `, and ${MAX_ROLES_TO_DISPLAY - roles.length} more roles` + : roles.join(", "); + export async function getUserInfoEmbed( pluginData: GuildPluginData, userId: string, @@ -109,7 +116,7 @@ export async function getUserInfoEmbed( name: preEmbedPadding + "Member information", value: trimLines(` ${user.bot ? "Added" : "Joined"}: **${joinAge} ago** (\`${prettyJoinedAt}\`) - ${roles.length > 0 ? "Roles: " + roles.map((r) => `<@&${r.id}>`).join(", ") : ""} + ${roles.length > 0 ? "Roles: " + trimRoles(roles.map((r) => `<@&${r.id}>`)) : ""} `), }); diff --git a/backend/src/utils/permissionNames.ts b/backend/src/utils/permissionNames.ts new file mode 100644 index 00000000..3bf0c4f1 --- /dev/null +++ b/backend/src/utils/permissionNames.ts @@ -0,0 +1,46 @@ +import { PermissionFlags } from "discord.js"; +import { EMPTY_CHAR } from "../utils"; + +export const PERMISSION_NAMES: Record = { + ADD_REACTIONS: "Add Reactions", + ADMINISTRATOR: "Administrator", + ATTACH_FILES: "Attach Files", + BAN_MEMBERS: "Ban Members", + CHANGE_NICKNAME: "Change Nickname", + CONNECT: "Connect", + CREATE_INSTANT_INVITE: "Create Invite", + CREATE_PRIVATE_THREADS: "Create Private Threads", + CREATE_PUBLIC_THREADS: "Create Public Threads", + DEAFEN_MEMBERS: "Deafen Members", + EMBED_LINKS: "Embed Links", + KICK_MEMBERS: "Kick Members", + MANAGE_CHANNELS: "Manage Channels", + MANAGE_EMOJIS_AND_STICKERS: "Manage Emojis and Stickers", + MANAGE_GUILD: "Manage Server", + MANAGE_MESSAGES: "Manage Messages", + MANAGE_NICKNAMES: "Manage Nicknames", + MANAGE_ROLES: "Manage Roles", + MANAGE_THREADS: "Manage Threads", + MANAGE_WEBHOOKS: "Manage Webhooks", + MENTION_EVERYONE: `Mention @${EMPTY_CHAR}everyone, @${EMPTY_CHAR}here, and All Roles`, + MOVE_MEMBERS: "Move Members", + MUTE_MEMBERS: "Mute Members", + PRIORITY_SPEAKER: "Priority Speaker", + READ_MESSAGE_HISTORY: "Read Message History", + REQUEST_TO_SPEAK: "Request to Speak", + SEND_MESSAGES: "Send Messages", + SEND_MESSAGES_IN_THREADS: "Send Messages in Threads", + SEND_TTS_MESSAGES: "Send Text-To-Speech Messages", + SPEAK: "Speak", + START_EMBEDDED_ACTIVITIES: "Start Embedded Activities", + STREAM: "Video", + USE_APPLICATION_COMMANDS: "Use Application Commands", + USE_EXTERNAL_EMOJIS: "Use External Emoji", + USE_EXTERNAL_STICKERS: "Use External Stickers", + USE_PRIVATE_THREADS: "Use Private Threads", + USE_PUBLIC_THREADS: "Use Public Threads", + USE_VAD: "Use Voice Activity", + VIEW_AUDIT_LOG: "View Audit Log", + VIEW_CHANNEL: "View Channels", + VIEW_GUILD_INSIGHTS: "View Guild Insights", +};