mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-06-08 00:05:01 +00:00
Merge branch 'master' of github.com:ZeppelinBot/Zeppelin into feat/application-commands
This commit is contained in:
commit
2c0e4b37ca
235 changed files with 3464 additions and 4799 deletions
|
@ -22,31 +22,26 @@ import {
|
|||
PartialChannelData,
|
||||
PartialMessage,
|
||||
RoleResolvable,
|
||||
Snowflake,
|
||||
Sticker,
|
||||
TextBasedChannel,
|
||||
User,
|
||||
} from "discord.js";
|
||||
import emojiRegex from "emoji-regex";
|
||||
import { either } from "fp-ts/lib/Either";
|
||||
import { unsafeCoerce } from "fp-ts/lib/function";
|
||||
import fs from "fs";
|
||||
import https from "https";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import * as t from "io-ts";
|
||||
import { isEqual } from "lodash";
|
||||
import moment from "moment-timezone";
|
||||
import { performance } from "perf_hooks";
|
||||
import tlds from "tlds";
|
||||
import tmp from "tmp";
|
||||
import { URL } from "url";
|
||||
import { z, ZodError } from "zod";
|
||||
import { z, ZodEffects, ZodError, ZodRecord, ZodString } from "zod";
|
||||
import { ISavedMessageAttachmentData, SavedMessage } from "./data/entities/SavedMessage";
|
||||
import { getProfiler } from "./profiler";
|
||||
import { SimpleCache } from "./SimpleCache";
|
||||
import { sendDM } from "./utils/sendDM";
|
||||
import { Brand } from "./utils/typeUtils";
|
||||
import { waitForButtonConfirm } from "./utils/waitForInteraction";
|
||||
import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
|
||||
|
||||
const fsp = fs.promises;
|
||||
|
||||
|
@ -92,71 +87,11 @@ export function isDiscordAPIError(err: Error | string): err is DiscordAPIError {
|
|||
return err instanceof DiscordAPIError;
|
||||
}
|
||||
|
||||
export function tNullable<T extends t.Type<any, any>>(type: T) {
|
||||
return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`);
|
||||
}
|
||||
|
||||
export const tNormalizedNullOrUndefined = new t.Type<undefined, null | undefined>(
|
||||
"tNormalizedNullOrUndefined",
|
||||
(v): v is undefined => typeof v === "undefined",
|
||||
(v, c) => (v == null ? t.success(undefined) : t.failure(v, c, "Value must be null or undefined")),
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* Similar to `tNullable`, but normalizes both `null` and `undefined` to `undefined`.
|
||||
* This allows adding optional config options that can be "removed" by setting the value to `null`.
|
||||
*/
|
||||
export function tNormalizedNullOptional<T extends t.Type<any, any>>(type: T) {
|
||||
return t.union(
|
||||
[type, tNormalizedNullOrUndefined],
|
||||
`Optional<${type.name}>`, // Simplified name for errors and config schema views
|
||||
);
|
||||
}
|
||||
|
||||
export type TDeepPartial<T> = T extends t.InterfaceType<any>
|
||||
? TDeepPartialProps<T["props"]>
|
||||
: T extends t.DictionaryType<any, any>
|
||||
? t.DictionaryType<T["domain"], TDeepPartial<T["codomain"]>>
|
||||
: T extends t.UnionType<any[]>
|
||||
? t.UnionType<Array<TDeepPartial<T["types"][number]>>>
|
||||
: T extends t.IntersectionType<any>
|
||||
? t.IntersectionType<Array<TDeepPartial<T["types"][number]>>>
|
||||
: T extends t.ArrayType<any>
|
||||
? t.ArrayType<TDeepPartial<T["type"]>>
|
||||
: T;
|
||||
|
||||
// Based on t.PartialC
|
||||
export interface TDeepPartialProps<P extends t.Props>
|
||||
extends t.PartialType<
|
||||
P,
|
||||
{
|
||||
[K in keyof P]?: TDeepPartial<t.TypeOf<P[K]>>;
|
||||
},
|
||||
{
|
||||
[K in keyof P]?: TDeepPartial<t.OutputOf<P[K]>>;
|
||||
}
|
||||
> {}
|
||||
|
||||
export function tDeepPartial<T>(type: T): TDeepPartial<T> {
|
||||
if (type instanceof t.InterfaceType || type instanceof t.PartialType) {
|
||||
const newProps = {};
|
||||
for (const [key, prop] of Object.entries(type.props)) {
|
||||
newProps[key] = tDeepPartial(prop);
|
||||
}
|
||||
return t.partial(newProps) as TDeepPartial<T>;
|
||||
} else if (type instanceof t.DictionaryType) {
|
||||
return t.record(type.domain, tDeepPartial(type.codomain)) as TDeepPartial<T>;
|
||||
} else if (type instanceof t.UnionType) {
|
||||
return t.union(type.types.map((unionType) => tDeepPartial(unionType))) as TDeepPartial<T>;
|
||||
} else if (type instanceof t.IntersectionType) {
|
||||
const types = type.types.map((intersectionType) => tDeepPartial(intersectionType));
|
||||
return t.intersection(types as [t.Mixed, t.Mixed]) as unknown as TDeepPartial<T>;
|
||||
} else if (type instanceof t.ArrayType) {
|
||||
return t.array(tDeepPartial(type.type)) as TDeepPartial<T>;
|
||||
} else {
|
||||
return type as TDeepPartial<T>;
|
||||
}
|
||||
// null | undefined -> undefined
|
||||
export function zNullishToUndefined<T extends z.ZodTypeAny>(
|
||||
type: T,
|
||||
): ZodEffects<T, NonNullable<z.output<T>> | undefined> {
|
||||
return type.transform((v) => v ?? undefined);
|
||||
}
|
||||
|
||||
export function getScalarDifference<T extends object>(
|
||||
|
@ -208,29 +143,6 @@ export function differenceToString(diff: Map<string, { was: any; is: any }>): st
|
|||
// https://stackoverflow.com/a/49262929/316944
|
||||
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));
|
||||
};
|
||||
|
||||
export function nonNullish<V>(v: V): v is NonNullable<V> {
|
||||
return v != null;
|
||||
}
|
||||
|
@ -241,70 +153,56 @@ export type GroupDMInvite = Invite & {
|
|||
type: typeof ChannelType.GroupDM;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mirrors EmbedOptions from Eris
|
||||
*/
|
||||
export const tEmbed = t.type({
|
||||
title: tNullable(t.string),
|
||||
description: tNullable(t.string),
|
||||
url: tNullable(t.string),
|
||||
timestamp: tNullable(t.string),
|
||||
color: tNullable(t.number),
|
||||
footer: tNullable(
|
||||
t.type({
|
||||
text: t.string,
|
||||
icon_url: tNullable(t.string),
|
||||
proxy_icon_url: tNullable(t.string),
|
||||
}),
|
||||
),
|
||||
image: tNullable(
|
||||
t.type({
|
||||
url: tNullable(t.string),
|
||||
proxy_url: tNullable(t.string),
|
||||
width: tNullable(t.number),
|
||||
height: tNullable(t.number),
|
||||
}),
|
||||
),
|
||||
thumbnail: tNullable(
|
||||
t.type({
|
||||
url: tNullable(t.string),
|
||||
proxy_url: tNullable(t.string),
|
||||
width: tNullable(t.number),
|
||||
height: tNullable(t.number),
|
||||
}),
|
||||
),
|
||||
video: tNullable(
|
||||
t.type({
|
||||
url: tNullable(t.string),
|
||||
width: tNullable(t.number),
|
||||
height: tNullable(t.number),
|
||||
}),
|
||||
),
|
||||
provider: tNullable(
|
||||
t.type({
|
||||
name: t.string,
|
||||
url: tNullable(t.string),
|
||||
}),
|
||||
),
|
||||
fields: tNullable(
|
||||
t.array(
|
||||
t.type({
|
||||
name: tNullable(t.string),
|
||||
value: tNullable(t.string),
|
||||
inline: tNullable(t.boolean),
|
||||
}),
|
||||
),
|
||||
),
|
||||
author: tNullable(
|
||||
t.type({
|
||||
name: t.string,
|
||||
url: tNullable(t.string),
|
||||
width: tNullable(t.number),
|
||||
height: tNullable(t.number),
|
||||
}),
|
||||
),
|
||||
export function zBoundedCharacters(min: number, max: number) {
|
||||
return z.string().refine(
|
||||
(str) => {
|
||||
const len = [...str].length; // Unicode aware character split
|
||||
return len >= min && len <= max;
|
||||
},
|
||||
{
|
||||
message: `String must be between ${min} and ${max} characters long`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const zSnowflake = z.string().refine((str) => isSnowflake(str), {
|
||||
message: "Invalid snowflake ID",
|
||||
});
|
||||
|
||||
const regexWithFlags = /^\/(.*?)\/([i]*)$/;
|
||||
|
||||
export class InvalidRegexError extends Error {}
|
||||
|
||||
/**
|
||||
* This function supports two input syntaxes for regexes: /<pattern>/<flags> and just <pattern>
|
||||
*/
|
||||
export function inputPatternToRegExp(pattern: string) {
|
||||
const advancedSyntaxMatch = pattern.match(regexWithFlags);
|
||||
const [finalPattern, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [pattern, ""];
|
||||
try {
|
||||
return new RegExp(finalPattern, flags);
|
||||
} catch (e) {
|
||||
throw new InvalidRegexError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export function zRegex<T extends ZodString>(zStr: T) {
|
||||
return zStr.transform((str, ctx) => {
|
||||
try {
|
||||
return inputPatternToRegExp(str);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRegexError) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid regex",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const zEmbedInput = z.object({
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
|
@ -388,15 +286,9 @@ export type StrictMessageContent = {
|
|||
embeds?: APIEmbed[];
|
||||
};
|
||||
|
||||
export const tStrictMessageContent = t.type({
|
||||
content: tNullable(t.string),
|
||||
tts: tNullable(t.boolean),
|
||||
disableEveryone: tNullable(t.boolean),
|
||||
embed: tNullable(tEmbed),
|
||||
embeds: tNullable(t.array(tEmbed)),
|
||||
});
|
||||
export const zMessageContent = z.union([zBoundedCharacters(0, 4000), zStrictMessageContent]);
|
||||
|
||||
export const tMessageContent = t.union([t.string, tStrictMessageContent]);
|
||||
export type MessageContent = string | StrictMessageContent;
|
||||
|
||||
export function validateAndParseMessageContent(input: unknown): StrictMessageContent {
|
||||
if (input == null) {
|
||||
|
@ -455,11 +347,21 @@ function dropNullValuesRecursively(obj: any) {
|
|||
/**
|
||||
* Mirrors AllowedMentions from Eris
|
||||
*/
|
||||
export const tAllowedMentions = t.type({
|
||||
everyone: tNormalizedNullOptional(t.boolean),
|
||||
users: tNormalizedNullOptional(t.union([t.boolean, t.array(t.string)])),
|
||||
roles: tNormalizedNullOptional(t.union([t.boolean, t.array(t.string)])),
|
||||
repliedUser: tNormalizedNullOptional(t.boolean),
|
||||
export const zAllowedMentions = z.strictObject({
|
||||
everyone: zNullishToUndefined(z.boolean().nullable().optional()),
|
||||
users: zNullishToUndefined(
|
||||
z
|
||||
.union([z.boolean(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
),
|
||||
roles: zNullishToUndefined(
|
||||
z
|
||||
.union([z.boolean(), z.array(z.string())])
|
||||
.nullable()
|
||||
.optional(),
|
||||
),
|
||||
replied_user: zNullishToUndefined(z.boolean().nullable().optional()),
|
||||
});
|
||||
|
||||
export function dropPropertiesByName(obj, propName) {
|
||||
|
@ -473,39 +375,28 @@ export function dropPropertiesByName(obj, propName) {
|
|||
}
|
||||
}
|
||||
|
||||
export const tAlphanumeric = new t.Type<string, string>(
|
||||
"tAlphanumeric",
|
||||
(s): s is string => typeof s === "string",
|
||||
(from, to) =>
|
||||
either.chain(t.string.validate(from, to), (s) => {
|
||||
return s.match(/\W/) ? t.failure(from, to, "String must be alphanumeric") : t.success(s);
|
||||
}),
|
||||
(s) => s,
|
||||
);
|
||||
export function zBoundedRecord<TRecord extends ZodRecord<any, any>>(
|
||||
record: TRecord,
|
||||
minKeys: number,
|
||||
maxKeys: number,
|
||||
): ZodEffects<TRecord> {
|
||||
return record.refine(
|
||||
(data) => {
|
||||
const len = Object.keys(data).length;
|
||||
return len >= minKeys && len <= maxKeys;
|
||||
},
|
||||
{
|
||||
message: `Object must have ${minKeys}-${maxKeys} keys`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const tDateTime = new t.Type<string, string>(
|
||||
"tDateTime",
|
||||
(s): s is string => typeof s === "string",
|
||||
(from, to) =>
|
||||
either.chain(t.string.validate(from, to), (s) => {
|
||||
const parsed =
|
||||
s.length === 10 ? moment.utc(s, "YYYY-MM-DD") : s.length === 19 ? moment.utc(s, "YYYY-MM-DD HH:mm:ss") : null;
|
||||
|
||||
return parsed && parsed.isValid() ? t.success(s) : t.failure(from, to, "Invalid datetime");
|
||||
}),
|
||||
(s) => s,
|
||||
);
|
||||
|
||||
export const tDelayString = new t.Type<string, string>(
|
||||
"tDelayString",
|
||||
(s): s is string => typeof s === "string",
|
||||
(from, to) =>
|
||||
either.chain(t.string.validate(from, to), (s) => {
|
||||
const ms = convertDelayStringToMS(s);
|
||||
return ms === null ? t.failure(from, to, "Invalid delay string") : t.success(s);
|
||||
}),
|
||||
(s) => s,
|
||||
);
|
||||
export const zDelayString = z
|
||||
.string()
|
||||
.max(32)
|
||||
.refine((str) => convertDelayStringToMS(str) !== null, {
|
||||
message: "Invalid delay string",
|
||||
});
|
||||
|
||||
// To avoid running into issues with the JS max date vaLue, we cap maximum delay strings *far* below that.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#The_ECMAScript_epoch_and_timestamps
|
||||
|
@ -610,9 +501,11 @@ export function stripObjectToScalars(obj, includedNested: string[] = []) {
|
|||
|
||||
export const snowflakeRegex = /[1-9][0-9]{5,19}/;
|
||||
|
||||
export type Snowflake = Brand<string, "Snowflake">;
|
||||
|
||||
const isSnowflakeRegex = new RegExp(`^${snowflakeRegex.source}$`);
|
||||
export function isSnowflake(v: string): boolean {
|
||||
return isSnowflakeRegex.test(v);
|
||||
export function isSnowflake(v: unknown): v is Snowflake {
|
||||
return typeof v === "string" && isSnowflakeRegex.test(v);
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
|
@ -665,11 +558,12 @@ export function getUrlsInString(str: string, onlyUnique = false): MatchedURL[] {
|
|||
}
|
||||
|
||||
export function parseInviteCodeInput(str: string): string {
|
||||
if (str.match(/^[a-z0-9]{6,}$/i)) {
|
||||
return str;
|
||||
const parsedInviteCodes = getInviteCodesInString(str);
|
||||
if (parsedInviteCodes.length) {
|
||||
return parsedInviteCodes[0];
|
||||
}
|
||||
|
||||
return getInviteCodesInString(str)[0];
|
||||
return str;
|
||||
}
|
||||
|
||||
export function isNotNull<T>(value: T): value is Exclude<T, null | undefined> {
|
||||
|
@ -1228,12 +1122,12 @@ export function resolveUserId(bot: Client, value: string) {
|
|||
return mentionMatch[1];
|
||||
}
|
||||
|
||||
// A non-mention, full username?
|
||||
const usernameMatch = value.match(/^@?([^#]+)#(\d{4})$/);
|
||||
// a username
|
||||
const usernameMatch = value.match(/^@?(\S{3,})$/);
|
||||
if (usernameMatch) {
|
||||
const profiler = getProfiler();
|
||||
const start = performance.now();
|
||||
const user = bot.users.cache.find((u) => u.username === usernameMatch[1] && u.discriminator === usernameMatch[2]);
|
||||
const user = bot.users.cache.find((u) => u.tag === usernameMatch[1]);
|
||||
profiler?.addDataPoint("utils:resolveUserId:usernameMatch", performance.now() - start);
|
||||
if (user) {
|
||||
return user.id;
|
||||
|
@ -1481,8 +1375,7 @@ export function messageLink(guildIdOrMessage: string | Message | null, channelId
|
|||
}
|
||||
|
||||
export function isValidEmbed(embed: any): boolean {
|
||||
const result = decodeAndValidateStrict(tEmbed, embed);
|
||||
return !(result instanceof StrictValidationError);
|
||||
return zEmbedInput.safeParse(embed).success;
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat("en-US");
|
||||
|
@ -1610,7 +1503,11 @@ export function isTruthy<T>(value: T): value is Exclude<T, false | null | undefi
|
|||
|
||||
export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
export function renderUsername(username: string, discriminator: string): string {
|
||||
export function renderUsername(memberOrUser: GuildMember | UnknownUser | User): string;
|
||||
export function renderUsername(username: string, discriminator: string): string;
|
||||
export function renderUsername(username: string | User | GuildMember | UnknownUser, discriminator?: string): string {
|
||||
if (username instanceof GuildMember) return username.user.tag;
|
||||
if (username instanceof User || username instanceof UnknownUser) return username.tag;
|
||||
if (discriminator === "0") {
|
||||
return username;
|
||||
}
|
||||
|
@ -1620,3 +1517,21 @@ export function renderUsername(username: string, discriminator: string): string
|
|||
export function renderUserUsername(user: User | UnknownUser): string {
|
||||
return renderUsername(user.username, user.discriminator);
|
||||
}
|
||||
|
||||
type Entries<T> = Array<
|
||||
{
|
||||
[Key in keyof T]-?: [Key, T[Key]];
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
export function entries<T extends object>(object: T) {
|
||||
return Object.entries(object) as Entries<T>;
|
||||
}
|
||||
|
||||
export function keys<T extends object>(object: T) {
|
||||
return Object.keys(object) as Array<keyof T>;
|
||||
}
|
||||
|
||||
export function values<T extends object>(object: T) {
|
||||
return Object.values(object) as Array<T[keyof T]>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue