/** * @file Utility functions that are plugin-instance-specific (i.e. use PluginData) */ import { GuildMember, Message, MessageCreateOptions, MessageMentionOptions, PermissionsBitField, TextBasedChannel, } from "discord.js"; import * as t from "io-ts"; import { AnyPluginData, CommandContext, ConfigValidationError, ExtendedMatchParams, GuildPluginData, helpers, PluginOverrideCriteria, } from "knub"; import { logger } from "./logger"; import { isStaff } from "./staff"; import { TZeppelinKnub } from "./types"; import { errorMessage, successMessage, tNullable } from "./utils"; import { Tail } from "./utils/typeUtils"; import { parseIoTsSchema, StrictValidationError } from "./validatorUtils"; const { getMemberLevel } = helpers; export function canActOn( pluginData: GuildPluginData<any>, member1: GuildMember, member2: GuildMember, allowSameLevel = false, allowAdmins = false, ) { if (member2.id === pluginData.client.user!.id) { return false; } const isOwnerOrAdmin = member2.id === member2.guild.ownerId || member2.permissions.has(PermissionsBitField.Flags.Administrator); if (isOwnerOrAdmin && !allowAdmins) { return false; } const ourLevel = getMemberLevel(pluginData, member1); const memberLevel = getMemberLevel(pluginData, member2); return allowSameLevel ? ourLevel >= memberLevel : ourLevel > memberLevel; } export async function hasPermission( pluginData: AnyPluginData<any>, permission: string, matchParams: ExtendedMatchParams, ) { const config = await pluginData.config.getMatchingConfig(matchParams); return helpers.hasPermission(config, permission); } const PluginOverrideCriteriaType: t.Type<PluginOverrideCriteria<unknown>> = t.recursion( "PluginOverrideCriteriaType", () => t.partial({ channel: tNullable(t.union([t.string, t.array(t.string)])), category: tNullable(t.union([t.string, t.array(t.string)])), level: tNullable(t.union([t.string, t.array(t.string)])), user: tNullable(t.union([t.string, t.array(t.string)])), role: tNullable(t.union([t.string, t.array(t.string)])), all: tNullable(t.array(PluginOverrideCriteriaType)), any: tNullable(t.array(PluginOverrideCriteriaType)), not: tNullable(PluginOverrideCriteriaType), extra: t.unknown, }), ); const validTopLevelOverrideKeys = [ "channel", "category", "thread", "is_thread", "level", "user", "role", "all", "any", "not", "extra", "config", ]; const BasicPluginStructureType = t.type({ enabled: tNullable(t.boolean), config: tNullable(t.unknown), overrides: tNullable(t.array(t.union([PluginOverrideCriteriaType, t.type({ config: t.unknown })]))), replaceDefaultOverrides: tNullable(t.boolean), }); export function strictValidationErrorToConfigValidationError(err: StrictValidationError) { return new ConfigValidationError( err .getErrors() .map((e) => e.toString()) .join("\n"), ); } export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema): (input: unknown) => t.TypeOf<Schema> { return (input: unknown) => { try { return parseIoTsSchema(schema, input); } catch (err) { if (err instanceof StrictValidationError) { throw strictValidationErrorToConfigValidationError(err); } throw err; } }; } export async function sendSuccessMessage( pluginData: AnyPluginData<any>, channel: TextBasedChannel, body: string, allowedMentions?: MessageMentionOptions, ): Promise<Message | undefined> { const emoji = pluginData.fullConfig.success_emoji || undefined; const formattedBody = successMessage(body, emoji); const content: MessageCreateOptions = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; return channel .send({ ...content }) // Force line break .catch((err) => { const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id; logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`); return undefined; }); } export async function sendErrorMessage( pluginData: AnyPluginData<any>, channel: TextBasedChannel, body: string, allowedMentions?: MessageMentionOptions, ): Promise<Message | undefined> { const emoji = pluginData.fullConfig.error_emoji || undefined; const formattedBody = errorMessage(body, emoji); const content: MessageCreateOptions = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; return channel .send({ ...content }) // Force line break .catch((err) => { const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id; logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`); return undefined; }); } export function getBaseUrl(pluginData: AnyPluginData<any>) { const knub = pluginData.getKnubInstance() as TZeppelinKnub; // @ts-expect-error return knub.getGlobalConfig().url; } export function isOwner(pluginData: AnyPluginData<any>, userId: string) { const knub = pluginData.getKnubInstance() as TZeppelinKnub; // @ts-expect-error const owners = knub.getGlobalConfig()?.owners; if (!owners) { return false; } return owners.includes(userId); } export const isStaffPreFilter = (_, context: CommandContext<any>) => { return isStaff(context.message.author.id); }; type AnyFn = (...args: any[]) => any; /** * Creates a public plugin function out of a function with pluginData as the first parameter */ export function mapToPublicFn<T extends AnyFn>(inputFn: T) { return (pluginData) => { return (...args: Tail<Parameters<typeof inputFn>>): ReturnType<typeof inputFn> => { return inputFn(pluginData, ...args); }; }; }