zappyzep/backend/src/pluginUtils.ts
2023-04-02 03:18:55 +03:00

198 lines
5.7 KiB
TypeScript

/**
* @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);
};
};
}