chore: monorepo setup (#7175)

This commit is contained in:
Noel
2022-01-07 17:18:25 +01:00
committed by GitHub
parent 780b7ed39f
commit 16390efe6e
504 changed files with 25459 additions and 22830 deletions

View File

@@ -0,0 +1,36 @@
import type { APIEmbedField } from 'discord-api-types/v9';
import { z } from 'zod';
export const fieldNamePredicate = z.string().min(1).max(256);
export const fieldValuePredicate = z.string().min(1).max(1024);
export const fieldInlinePredicate = z.boolean().optional();
export const embedFieldPredicate = z.object({
name: fieldNamePredicate,
value: fieldValuePredicate,
inline: fieldInlinePredicate,
});
export const embedFieldsArrayPredicate = embedFieldPredicate.array();
export const fieldLengthPredicate = z.number().lte(25);
export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void {
fieldLengthPredicate.parse(fields.length + amountAdding);
}
export const authorNamePredicate = fieldNamePredicate.nullable();
export const urlPredicate = z.string().url().nullish();
export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
export const footerTextPredicate = z.string().min(1).max(2048).nullable();
export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
export const titlePredicate = fieldNamePredicate.nullable();

View File

@@ -0,0 +1,326 @@
import type {
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedProvider,
APIEmbedThumbnail,
APIEmbedVideo,
} from 'discord-api-types/v9';
import {
authorNamePredicate,
colorPredicate,
descriptionPredicate,
embedFieldsArrayPredicate,
fieldInlinePredicate,
fieldNamePredicate,
fieldValuePredicate,
footerTextPredicate,
timestampPredicate,
titlePredicate,
urlPredicate,
validateFieldLength,
} from './Assertions';
export interface AuthorOptions {
name: string;
url?: string;
iconURL?: string;
}
export interface FooterOptions {
text: string;
iconURL?: string;
}
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
export class Embed implements APIEmbed {
/**
* An array of fields of this embed
*/
public fields: APIEmbedField[];
/**
* The embed title
*/
public title?: string;
/**
* The embed description
*/
public description?: string;
/**
* The embed url
*/
public url?: string;
/**
* The embed color
*/
public color?: number;
/**
* The timestamp of the embed in the ISO format
*/
public timestamp?: string;
/**
* The embed thumbnail data
*/
public thumbnail?: APIEmbedThumbnail;
/**
* The embed image data
*/
public image?: APIEmbedImage;
/**
* Received video data
*/
public video?: APIEmbedVideo;
/**
* The embed author data
*/
public author?: APIEmbedAuthor;
/**
* Received data about the embed provider
*/
public provider?: APIEmbedProvider;
/**
* The embed footer data
*/
public footer?: APIEmbedFooter;
public constructor(data: APIEmbed = {}) {
this.title = data.title;
this.description = data.description;
this.url = data.url;
this.color = data.color;
this.thumbnail = data.thumbnail;
this.image = data.image;
this.video = data.video;
this.author = data.author;
this.provider = data.provider;
this.footer = data.footer;
this.fields = data.fields ?? [];
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
*/
public get length(): number {
return (
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
(this.footer?.text.length ?? 0) +
(this.author?.name.length ?? 0)
);
}
/**
* Adds a field to the embed (max 25)
*
* @param field The field to add.
*/
public addField(field: APIEmbedField): this {
return this.addFields(field);
}
/**
* Adds fields to the embed (max 25)
*
* @param fields The fields to add
*/
public addFields(...fields: APIEmbedField[]): this {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length);
this.fields.push(...Embed.normalizeFields(...fields));
return this;
}
/**
* Removes, replaces, or inserts fields in the embed (max 25)
*
* @param index The index to start at
* @param deleteCount The number of fields to remove
* @param fields The replacing field objects
*/
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length - deleteCount);
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
return this;
}
/**
* Sets the author of this embed
*
* @param options The options for the author
*/
public setAuthor(options: AuthorOptions | null): this {
if (options === null) {
this.author = undefined;
return this;
}
const { name, iconURL, url } = options;
// Data assertions
authorNamePredicate.parse(name);
urlPredicate.parse(iconURL);
urlPredicate.parse(url);
this.author = { name, url, icon_url: iconURL };
return this;
}
/**
* Sets the color of this embed
*
* @param color The color of the embed
*/
public setColor(color: number | null): this {
// Data assertions
colorPredicate.parse(color);
this.color = color ?? undefined;
return this;
}
/**
* Sets the description of this embed
*
* @param description The description
*/
public setDescription(description: string | null): this {
// Data assertions
descriptionPredicate.parse(description);
this.description = description ?? undefined;
return this;
}
/**
* Sets the footer of this embed
*
* @param options The options for the footer
*/
public setFooter(options: FooterOptions | null): this {
if (options === null) {
this.footer = undefined;
return this;
}
const { text, iconURL } = options;
// Data assertions
footerTextPredicate.parse(text);
urlPredicate.parse(iconURL);
this.footer = { text, icon_url: iconURL };
return this;
}
/**
* Sets the image of this embed
*
* @param url The URL of the image
*/
public setImage(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.image = url ? { url } : undefined;
return this;
}
/**
* Sets the thumbnail of this embed
*
* @param url The URL of the thumbnail
*/
public setThumbnail(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.thumbnail = url ? { url } : undefined;
return this;
}
/**
* Sets the timestamp of this embed
*
* @param timestamp The timestamp or date
*/
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
// Data assertions
timestampPredicate.parse(timestamp);
this.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
return this;
}
/**
* Sets the title of this embed
*
* @param title The title
*/
public setTitle(title: string | null): this {
// Data assertions
titlePredicate.parse(title);
this.title = title ?? undefined;
return this;
}
/**
* Sets the URL of this embed
*
* @param url The URL
*/
public setURL(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.url = url ?? undefined;
return this;
}
/**
* Transforms the embed to a plain object
*/
public toJSON(): APIEmbed {
return { ...this };
}
/**
* Normalizes field input and resolves strings
*
* @param fields Fields to normalize
*/
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
return fields.flat(Infinity).map((field) => {
fieldNamePredicate.parse(field.name);
fieldValuePredicate.parse(field.value);
fieldInlinePredicate.parse(field.inline);
return { name: field.name, value: field.value, inline: field.inline ?? undefined };
});
}
}

View File

@@ -0,0 +1,319 @@
import type { Snowflake } from 'discord-api-types/globals';
import type { URL } from 'url';
/**
* Wraps the content inside a codeblock with no language
*
* @param content The content to wrap
*/
export function codeBlock<C extends string>(content: C): `\`\`\`\n${C}\`\`\``;
/**
* Wraps the content inside a codeblock with the specified language
*
* @param language The language for the codeblock
* @param content The content to wrap
*/
export function codeBlock<L extends string, C extends string>(language: L, content: C): `\`\`\`${L}\n${C}\`\`\``;
export function codeBlock(language: string, content?: string): string {
return typeof content === 'undefined' ? `\`\`\`\n${language}\`\`\`` : `\`\`\`${language}\n${content}\`\`\``;
}
/**
* Wraps the content inside \`backticks\`, which formats it as inline code
*
* @param content The content to wrap
*/
export function inlineCode<C extends string>(content: C): `\`${C}\`` {
return `\`${content}\``;
}
/**
* Formats the content into italic text
*
* @param content The content to wrap
*/
export function italic<C extends string>(content: C): `_${C}_` {
return `_${content}_`;
}
/**
* Formats the content into bold text
*
* @param content The content to wrap
*/
export function bold<C extends string>(content: C): `**${C}**` {
return `**${content}**`;
}
/**
* Formats the content into underscored text
*
* @param content The content to wrap
*/
export function underscore<C extends string>(content: C): `__${C}__` {
return `__${content}__`;
}
/**
* Formats the content into strike-through text
*
* @param content The content to wrap
*/
export function strikethrough<C extends string>(content: C): `~~${C}~~` {
return `~~${content}~~`;
}
/**
* Formats the content into a quote. This needs to be at the start of the line for Discord to format it
*
* @param content The content to wrap
*/
export function quote<C extends string>(content: C): `> ${C}` {
return `> ${content}`;
}
/**
* Formats the content into a block quote. This needs to be at the start of the line for Discord to format it
*
* @param content The content to wrap
*/
export function blockQuote<C extends string>(content: C): `>>> ${C}` {
return `>>> ${content}`;
}
/**
* Wraps the URL into `<>`, which stops it from embedding
*
* @param url The URL to wrap
*/
export function hideLinkEmbed<C extends string>(url: C): `<${C}>`;
/**
* Wraps the URL into `<>`, which stops it from embedding
*
* @param url The URL to wrap
*/
export function hideLinkEmbed(url: URL): `<${string}>`;
export function hideLinkEmbed(url: string | URL) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `<${url}>`;
}
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
*/
export function hyperlink<C extends string>(content: C, url: URL): `[${C}](${string})`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
*/
export function hyperlink<C extends string, U extends string>(content: C, url: U): `[${C}](${U})`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
* @param title The title shown when hovering on the masked link
*/
export function hyperlink<C extends string, T extends string>(
content: C,
url: URL,
title: T,
): `[${C}](${string} "${T}")`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
* @param title The title shown when hovering on the masked link
*/
export function hyperlink<C extends string, U extends string, T extends string>(
content: C,
url: U,
title: T,
): `[${C}](${U} "${T}")`;
export function hyperlink(content: string, url: string | URL, title?: string) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return title ? `[${content}](${url} "${title}")` : `[${content}](${url})`;
}
/**
* Wraps the content inside spoiler (hidden text)
*
* @param content The content to wrap
*/
export function spoiler<C extends string>(content: C): `||${C}||` {
return `||${content}||`;
}
/**
* Formats a user ID into a user mention
*
* @param userId The user ID to format
*/
export function userMention<C extends Snowflake>(userId: C): `<@${C}>` {
return `<@${userId}>`;
}
/**
* Formats a user ID into a member-nickname mention
*
* @param memberId The user ID to format
*/
export function memberNicknameMention<C extends Snowflake>(memberId: C): `<@!${C}>` {
return `<@!${memberId}>`;
}
/**
* Formats a channel ID into a channel mention
*
* @param channelId The channel ID to format
*/
export function channelMention<C extends Snowflake>(channelId: C): `<#${C}>` {
return `<#${channelId}>`;
}
/**
* Formats a role ID into a role mention
*
* @param roleId The role ID to format
*/
export function roleMention<C extends Snowflake>(roleId: C): `<@&${C}>` {
return `<@&${roleId}>`;
}
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated?: false): `<:_:${C}>`;
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
* @param animated Whether the emoji is animated or not. Defaults to `false`
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated?: true): `<a:_:${C}>`;
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
* @param animated Whether the emoji is animated or not. Defaults to `false`
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated = false): `<a:_:${C}>` | `<:_:${C}>` {
return `<${animated ? 'a' : ''}:_:${emojiId}>`;
}
/**
* Formats a date into a short date-time string
*
* @param date The date to format, defaults to the current time
*/
export function time(date?: Date): `<t:${bigint}>`;
/**
* Formats a date given a format style
*
* @param date The date to format
* @param style The style to use
*/
export function time<S extends TimestampStylesString>(date: Date, style: S): `<t:${bigint}:${S}>`;
/**
* Formats the given timestamp into a short date-time string
*
* @param seconds The time to format, represents an UNIX timestamp in seconds
*/
export function time<C extends number>(seconds: C): `<t:${C}>`;
/**
* Formats the given timestamp into a short date-time string
*
* @param seconds The time to format, represents an UNIX timestamp in seconds
* @param style The style to use
*/
export function time<C extends number, S extends TimestampStylesString>(seconds: C, style: S): `<t:${C}:${S}>`;
export function time(timeOrSeconds?: number | Date, style?: TimestampStylesString): string {
if (typeof timeOrSeconds !== 'number') {
timeOrSeconds = Math.floor((timeOrSeconds?.getTime() ?? Date.now()) / 1000);
}
return typeof style === 'string' ? `<t:${timeOrSeconds}:${style}>` : `<t:${timeOrSeconds}>`;
}
/**
* The [message formatting timestamp styles](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles) supported by Discord
*/
export const TimestampStyles = {
/**
* Short time format, consisting of hours and minutes, e.g. 16:20
*/
ShortTime: 't',
/**
* Long time format, consisting of hours, minutes, and seconds, e.g. 16:20:30
*/
LongTime: 'T',
/**
* Short date format, consisting of day, month, and year, e.g. 20/04/2021
*/
ShortDate: 'd',
/**
* Long date format, consisting of day, month, and year, e.g. 20 April 2021
*/
LongDate: 'D',
/**
* Short date-time format, consisting of short date and short time formats, e.g. 20 April 2021 16:20
*/
ShortDateTime: 'f',
/**
* Long date-time format, consisting of long date and short time formats, e.g. Tuesday, 20 April 2021 16:20
*/
LongDateTime: 'F',
/**
* Relative time format, consisting of a relative duration format, e.g. 2 months ago
*/
RelativeTime: 'R',
} as const;
/**
* The possible values, see {@link TimestampStyles} for more information
*/
export type TimestampStylesString = typeof TimestampStyles[keyof typeof TimestampStyles];
/**
* An enum with all the available faces from Discord's native slash commands
*/
export enum Faces {
/**
* ¯\\_(ツ)\\_/¯
*/
Shrug = '¯\\_(ツ)\\_/¯',
/**
* (╯°□°)╯︵ ┻━┻
*/
Tableflip = '(╯°□°)╯︵ ┻━┻',
/**
* ┬─┬ ( ゜-゜ノ)
*/
Unflip = '┬─┬ ( ゜-゜ノ)',
}