mirror of
https://github.com/xHyroM/roles-bot.git
synced 2024-11-24 01:01:06 +01:00
feat(bot): finish setup, select
This commit is contained in:
parent
d1b58b4496
commit
aff5c3227d
15 changed files with 762 additions and 73 deletions
61
packages/bot/src/components/select.ts
Normal file
61
packages/bot/src/components/select.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
ComponentType,
|
||||
MessageFlags,
|
||||
RouteBases,
|
||||
Routes,
|
||||
} from "discord-api-types/v10";
|
||||
import { Component } from "../structs/Component";
|
||||
|
||||
new Component({
|
||||
id: "select:role",
|
||||
default: true,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
run: async (ctx) => {
|
||||
if (!ctx.guildId)
|
||||
return ctx.editReply({
|
||||
content: "Guild not found.",
|
||||
});
|
||||
|
||||
if (!ctx.interaction.member)
|
||||
return ctx.editReply({
|
||||
content: "Member not found.",
|
||||
});
|
||||
|
||||
const roleId =
|
||||
ctx.interaction.data.component_type === ComponentType.StringSelect
|
||||
? ctx.interaction.data.values[0].startsWith("role:")
|
||||
? ctx.interaction.data.values[0].split(":")[1]
|
||||
: // support for legacy select menus
|
||||
ctx.interaction.data.values[0]
|
||||
: ctx.interaction.data.custom_id.startsWith("role:")
|
||||
? ctx.interaction.data.custom_id.split(":")[1]
|
||||
: // support for legacy select menus
|
||||
ctx.interaction.data.custom_id;
|
||||
|
||||
const content = !ctx.interaction.member?.roles.includes(roleId)
|
||||
? `Gave the <@&${roleId}> role!`
|
||||
: `Removed the <@&${roleId}> role!`;
|
||||
|
||||
const method = !ctx.interaction.member?.roles.includes(roleId)
|
||||
? "PUT"
|
||||
: "DELETE";
|
||||
|
||||
await fetch(
|
||||
`${RouteBases.api}${Routes.guildMemberRole(
|
||||
ctx.guildId,
|
||||
ctx.interaction.member.user.id,
|
||||
roleId,
|
||||
)}`,
|
||||
{
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bot ${ctx.env.token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await ctx.editReply({
|
||||
content,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -15,6 +15,7 @@ import {
|
|||
import returnRoleLpe from "../utils/returnRoleLpe";
|
||||
import { REDIS } from "../things";
|
||||
import { encodeToHex, decodeFromString } from "serialize";
|
||||
import sendFinal from "../utils/sendFinal";
|
||||
|
||||
// Part 2 Channels ## select button/dropdowns
|
||||
new Component({
|
||||
|
@ -137,7 +138,7 @@ new Component({
|
|||
);
|
||||
|
||||
return rawRoleIds.length > 0
|
||||
? returnRoleLpe(ctx, rawRoleIds[0])
|
||||
? returnRoleLpe(data, ctx, rawRoleIds[0])
|
||||
: ctx.returnModal({
|
||||
title: "Message Preview",
|
||||
custom_id: "setup:part-messageContent",
|
||||
|
@ -191,17 +192,15 @@ new Component({
|
|||
},
|
||||
});
|
||||
|
||||
// Part 7 Send As ## finish
|
||||
// Part 7 Send As ## finish or open modal for webhook preview
|
||||
new Component({
|
||||
id: "setup:part-sendAs",
|
||||
acknowledge: false,
|
||||
run: async (ctx) => {
|
||||
if (!ctx.interaction.guild_id)
|
||||
if (!ctx.guildId)
|
||||
return await ctx.editReply({ content: "Guild not found." });
|
||||
|
||||
const rawData = await REDIS.get(
|
||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||
);
|
||||
const rawData = await REDIS.get(`roles-bot-setup:${ctx.guildId}`);
|
||||
if (!rawData)
|
||||
return ctx.respond({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
|
@ -212,29 +211,58 @@ new Component({
|
|||
});
|
||||
|
||||
const data = decodeFromString(rawData);
|
||||
console.log(data);
|
||||
const sendAs = ctx.interaction.data.custom_id.split(":")[2];
|
||||
data.sendAs = sendAs;
|
||||
|
||||
// delete data
|
||||
await REDIS.del(`roles-bot-setup:${ctx.guildId}`);
|
||||
await REDIS.setex(
|
||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||
encodeToHex(data),
|
||||
600,
|
||||
);
|
||||
|
||||
// TODO: finish sending
|
||||
const actionRow = new ActionRowBuilder();
|
||||
|
||||
/*switch (selecting) {
|
||||
case "buttons": {
|
||||
// TOOD: finish
|
||||
switch (sendAs) {
|
||||
case "webhook": {
|
||||
return ctx.returnModal({
|
||||
title: "Webhook Preview",
|
||||
custom_id: "setup:part-webhook",
|
||||
components: [
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setLabel("Webhook Name")
|
||||
.setCustomId("name")
|
||||
.setPlaceholder("Roles Bot")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMaxLength(80)
|
||||
.setRequired(true),
|
||||
)
|
||||
.toJSON(),
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setLabel("Webhook Avatar URL")
|
||||
.setCustomId("avatarUrl")
|
||||
.setPlaceholder(
|
||||
"https://raw.githubusercontent.com/Hyro-Blobs/blobs/main/base/hyro_blob-upscaled.png",
|
||||
)
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
],
|
||||
});
|
||||
}
|
||||
case "dropdowns": {
|
||||
// TODO: finish
|
||||
}
|
||||
}*/
|
||||
case "bot": {
|
||||
sendFinal(ctx, data);
|
||||
|
||||
return ctx.respond({
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "Done!",
|
||||
content: "Setup completed!",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import "./commands/setup";
|
||||
import "./components/setup";
|
||||
import "./components/select";
|
||||
import "./modals/setup";
|
||||
|
||||
import {
|
||||
|
@ -92,9 +93,10 @@ export default {
|
|||
}
|
||||
case InteractionType.MessageComponent: {
|
||||
const context = new ComponentContext(interaction, env);
|
||||
const component = COMPONENTS.find((cmp) =>
|
||||
const component =
|
||||
COMPONENTS.find((cmp) =>
|
||||
context.interaction.data.custom_id.startsWith(cmp.id),
|
||||
);
|
||||
) ?? COMPONENTS.find((cmp) => cmp.default);
|
||||
|
||||
if (!component)
|
||||
return new Response("Unknown component", { status: 404 });
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import {
|
||||
APIWebhook,
|
||||
ButtonStyle,
|
||||
InteractionResponseType,
|
||||
MessageFlags,
|
||||
RouteBases,
|
||||
Routes,
|
||||
} from "discord-api-types/v10";
|
||||
import { Modal } from "../structs/Modal";
|
||||
import { ActionRowBuilder, ButtonBuilder } from "builders";
|
||||
import { encodeToHex, decodeFromString } from "serialize";
|
||||
import { REDIS } from "../things";
|
||||
import sendFinal from "../utils/sendFinal";
|
||||
import { RoleId } from "../types";
|
||||
|
||||
// Part 5 Roles ## add label, placeholder, emoji OR message content
|
||||
new Modal({
|
||||
id: "setup:part-roles-lpe",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
run: async (ctx) => {
|
||||
const rawData = await REDIS.get(
|
||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||
|
@ -22,17 +27,28 @@ new Modal({
|
|||
|
||||
const data = decodeFromString(rawData);
|
||||
const rawRoleIds = data.rawRoleIds as string[];
|
||||
const roleIds = (data.roleIds ?? []) as {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
emoji: string;
|
||||
}[];
|
||||
const roleIds = (data.roleIds ?? []) as RoleId[];
|
||||
|
||||
const label = ctx.interaction.data.components[0].components[0].value;
|
||||
const placeholder = ctx.interaction.data.components[1].components[0].value;
|
||||
const emoji = ctx.interaction.data.components[2].components[0].value;
|
||||
const emoji = ctx.interaction.data.components[1].components[0].value;
|
||||
|
||||
roleIds.push({ label, placeholder, emoji });
|
||||
switch (data.selecting) {
|
||||
case "buttons": {
|
||||
const style = ctx.interaction.data.components[2].components[0].value;
|
||||
|
||||
roleIds.push({ label, style, emoji, id: rawRoleIds[0] });
|
||||
|
||||
break;
|
||||
}
|
||||
case "dropdowns": {
|
||||
const description =
|
||||
ctx.interaction.data.components[2].components[0].value;
|
||||
|
||||
roleIds.push({ label, description, emoji, id: rawRoleIds[0] });
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rawRoleIds.shift();
|
||||
data.rawRoleIds = rawRoleIds;
|
||||
|
@ -121,3 +137,53 @@ new Modal({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Part 8 (ONLY IF WEBHOOK) Webhook ## send webhook
|
||||
new Modal({
|
||||
id: "setup:part-webhook",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
run: async (ctx) => {
|
||||
const rawData = await REDIS.get(
|
||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||
);
|
||||
if (!rawData)
|
||||
return await ctx.editReply({
|
||||
content: "Data not found. Try running setup again.",
|
||||
});
|
||||
|
||||
const data = decodeFromString(rawData);
|
||||
|
||||
const webhookName = ctx.interaction.data.components[0].components[0].value;
|
||||
const webhookAvatarUrl =
|
||||
ctx.interaction.data.components[1].components[0].value;
|
||||
|
||||
const webhook = (await (
|
||||
await fetch(
|
||||
`${RouteBases.api}${Routes.channelWebhooks(data.channelId)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bot ${ctx.env.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Roles Bot Webhook",
|
||||
}),
|
||||
},
|
||||
)
|
||||
).json()) as APIWebhook;
|
||||
|
||||
data.webhook = {
|
||||
name: webhookName,
|
||||
avatarUrl: webhookAvatarUrl,
|
||||
id: webhook.id,
|
||||
token: webhook.token,
|
||||
};
|
||||
|
||||
sendFinal(ctx, data);
|
||||
|
||||
await ctx.editReply({
|
||||
content: "Setup completed!",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ComponentContext } from "./contexts/ComponentContext";
|
|||
interface ComponentOptions {
|
||||
id: string;
|
||||
acknowledge?: boolean;
|
||||
default?: boolean;
|
||||
flags?: MessageFlags;
|
||||
run: (interaction: ComponentContext) => void;
|
||||
}
|
||||
|
@ -12,12 +13,14 @@ interface ComponentOptions {
|
|||
export class Component {
|
||||
public id: string;
|
||||
public acknowledge: boolean;
|
||||
public default: boolean;
|
||||
public flags: MessageFlags | undefined;
|
||||
public run: (interaction: ComponentContext) => void | Response;
|
||||
|
||||
constructor(options: ComponentOptions) {
|
||||
this.id = options.id;
|
||||
this.acknowledge = options.acknowledge ?? true;
|
||||
this.default = options.default ?? false;
|
||||
this.flags = options.flags;
|
||||
this.run = options.run;
|
||||
|
||||
|
|
20
packages/bot/src/types.d.ts
vendored
20
packages/bot/src/types.d.ts
vendored
|
@ -21,3 +21,23 @@ declare interface Env {
|
|||
redisApiClientKey: string;
|
||||
redisApiClientHost: string;
|
||||
}
|
||||
|
||||
declare interface RoleId {
|
||||
label: string;
|
||||
description?: string;
|
||||
emoji: string;
|
||||
id: string;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
declare interface BasicData {
|
||||
channelId: string;
|
||||
selecting: "buttons" | "dropdowns";
|
||||
roleIds: RoleId[];
|
||||
message: {
|
||||
content: string;
|
||||
embedTitle: string;
|
||||
embedDescription: string;
|
||||
embedColor: string;
|
||||
};
|
||||
}
|
||||
|
|
12
packages/bot/src/utils/hexToRGB.ts
Normal file
12
packages/bot/src/utils/hexToRGB.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { RGBTuple } from "builders";
|
||||
|
||||
export default function (hex: string): RGBTuple | null {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? [
|
||||
parseInt(result[1], 16),
|
||||
parseInt(result[2], 16),
|
||||
parseInt(result[3], 16),
|
||||
]
|
||||
: null;
|
||||
}
|
16
packages/bot/src/utils/resolveButtonStyle.ts
Normal file
16
packages/bot/src/utils/resolveButtonStyle.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { ButtonStyle } from "discord-api-types/v10";
|
||||
|
||||
export default function (style: string) {
|
||||
switch (style.toLowerCase()) {
|
||||
case "primary":
|
||||
return ButtonStyle.Primary;
|
||||
case "secondary":
|
||||
return ButtonStyle.Secondary;
|
||||
case "success":
|
||||
return ButtonStyle.Success;
|
||||
case "danger":
|
||||
return ButtonStyle.Danger;
|
||||
default:
|
||||
return ButtonStyle.Primary;
|
||||
}
|
||||
}
|
18
packages/bot/src/utils/resolveEmoji.ts
Normal file
18
packages/bot/src/utils/resolveEmoji.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// https://github.com/discordjs/discord.js/blob/6412da4921a5fd9ed0987205508bacd2b4868fd6/packages/discord.js/src/util/Util.js#L90-L109
|
||||
|
||||
export function parseEmoji(text: string) {
|
||||
if (text.includes("%")) text = decodeURIComponent(text);
|
||||
if (!text.includes(":"))
|
||||
return { animated: false, name: text, id: undefined };
|
||||
const match = text.match(/<?(?:(a):)?(\w{2,32}):(\d{17,19})?>?/);
|
||||
return match && { animated: Boolean(match[1]), name: match[2], id: match[3] };
|
||||
}
|
||||
|
||||
export function resolvePartialEmoji(emoji: string) {
|
||||
if (!emoji) return null;
|
||||
if (typeof emoji === "string")
|
||||
return /^\d{17,19}$/.test(emoji) ? { id: emoji } : parseEmoji(emoji);
|
||||
const { id, name, animated } = emoji;
|
||||
if (!id && !name) return null;
|
||||
return { id, name, animated: Boolean(animated) };
|
||||
}
|
|
@ -8,8 +8,9 @@ import {
|
|||
} from "discord-api-types/v10";
|
||||
import { REDIS } from "../things";
|
||||
import { decodeFromString } from "serialize";
|
||||
import { BasicData } from "../types";
|
||||
|
||||
export default async function (ctx: Context, rawRole: string) {
|
||||
export default async function (data: BasicData, ctx: Context, rawRole: string) {
|
||||
const rolesRaw = await REDIS.get(`roles-bot-setup-roles:${ctx.guildId}`);
|
||||
if (!rolesRaw)
|
||||
return ctx.respond({
|
||||
|
@ -25,10 +26,7 @@ export default async function (ctx: Context, rawRole: string) {
|
|||
|
||||
const roleName = roles?.find((r) => r.id === rawRole)?.name;
|
||||
|
||||
return ctx.returnModal({
|
||||
title: `${roleName?.slice(0, 39)} Role`,
|
||||
custom_id: "setup:part-roles-lpe",
|
||||
components: [
|
||||
const components = [
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
|
@ -36,17 +34,7 @@ export default async function (ctx: Context, rawRole: string) {
|
|||
.setCustomId("label")
|
||||
.setPlaceholder("Ping")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setLabel("Placeholder")
|
||||
.setCustomId("placeholder")
|
||||
.setPlaceholder("pingping pong pong")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false),
|
||||
.setRequired(true),
|
||||
)
|
||||
.toJSON(),
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
|
@ -59,6 +47,44 @@ export default async function (ctx: Context, rawRole: string) {
|
|||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
],
|
||||
];
|
||||
|
||||
switch (data.selecting) {
|
||||
case "buttons": {
|
||||
components.push(
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setLabel("Style")
|
||||
.setCustomId("style")
|
||||
.setPlaceholder("Primary, Secondary, Success or Danger")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(true),
|
||||
)
|
||||
.toJSON(),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "dropdowns": {
|
||||
components.push(
|
||||
new ActionRowBuilder<TextInputBuilder>()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setLabel("Description")
|
||||
.setCustomId("description")
|
||||
.setPlaceholder("pingping pong pong")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.returnModal({
|
||||
title: `${roleName?.slice(0, 39)} Role`,
|
||||
custom_id: "setup:part-roles-lpe",
|
||||
components,
|
||||
});
|
||||
}
|
||||
|
|
164
packages/bot/src/utils/sendFinal.ts
Normal file
164
packages/bot/src/utils/sendFinal.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
import {
|
||||
APIActionRowComponent,
|
||||
APIEmbed,
|
||||
APIMessageActionRowComponent,
|
||||
ButtonStyle,
|
||||
RouteBases,
|
||||
Routes,
|
||||
} from "discord-api-types/v10";
|
||||
import { Context } from "../structs/contexts/Context";
|
||||
import { REDIS } from "../things";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
} from "builders";
|
||||
import hexToRGB from "./hexToRGB";
|
||||
import splitArray from "./splitArray";
|
||||
import { resolvePartialEmoji } from "./resolveEmoji";
|
||||
import { BasicData } from "../types";
|
||||
import resolveButtonStyle from "./resolveButtonStyle";
|
||||
|
||||
type DataBot = BasicData & {
|
||||
sendAs: "bot";
|
||||
};
|
||||
|
||||
type DataWebhook = BasicData & {
|
||||
sendAs: "webhook";
|
||||
webhook: {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
id: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Data = DataBot | DataWebhook;
|
||||
|
||||
export default async function (ctx: Context, data: Data) {
|
||||
await REDIS.del(`roles-bot-setup:${ctx.guildId}`);
|
||||
await REDIS.del(`roles-bot-setup-roles:${ctx.guildId}`);
|
||||
|
||||
const payload: {
|
||||
content?: string;
|
||||
embeds?: APIEmbed[];
|
||||
components: APIActionRowComponent<APIMessageActionRowComponent>[];
|
||||
} = {
|
||||
components: [],
|
||||
};
|
||||
|
||||
if (data.message.content) payload.content = data.message.content;
|
||||
if (data.message.embedTitle || data.message.embedDescription) {
|
||||
payload.embeds = [
|
||||
new EmbedBuilder()
|
||||
.setTitle(data.message.embedTitle)
|
||||
.setDescription(data.message.embedDescription)
|
||||
.setColor(
|
||||
data.message.embedColor ? hexToRGB(data.message.embedColor) : null,
|
||||
)
|
||||
.toJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
const components: APIActionRowComponent<APIMessageActionRowComponent>[] = [];
|
||||
const array = splitArray(data.roleIds, 25);
|
||||
for (const items of array) {
|
||||
const actionRow = new ActionRowBuilder();
|
||||
|
||||
const selectMenu = new StringSelectMenuBuilder().setCustomId("select:role");
|
||||
|
||||
for (const item of items) {
|
||||
switch (data.selecting) {
|
||||
case "buttons": {
|
||||
const button = new ButtonBuilder()
|
||||
.setLabel(item.label)
|
||||
.setCustomId(`role:${item.id}`)
|
||||
// rome-ignore lint/style/noNonNullAssertion: defined
|
||||
.setStyle(resolveButtonStyle(item.style!));
|
||||
|
||||
if (item.emoji && resolvePartialEmoji(item.emoji))
|
||||
// rome-ignore lint/style/noNonNullAssertion: its fine
|
||||
button.setEmoji(resolvePartialEmoji(item.emoji)!);
|
||||
|
||||
actionRow.addComponents(button);
|
||||
|
||||
break;
|
||||
}
|
||||
case "dropdowns": {
|
||||
const option = new StringSelectMenuOptionBuilder()
|
||||
.setLabel(item.label)
|
||||
// rome-ignore lint/style/noNonNullAssertion: defined
|
||||
.setDescription(item.description!)
|
||||
.setValue(`role:${item.id}`);
|
||||
|
||||
if (item.emoji && resolvePartialEmoji(item.emoji))
|
||||
// rome-ignore lint/style/noNonNullAssertion: its fine
|
||||
option.setEmoji(resolvePartialEmoji(item.emoji)!);
|
||||
|
||||
option.setValue(item.id);
|
||||
selectMenu.addOptions(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.selecting === "dropdowns") actionRow.addComponents(selectMenu);
|
||||
|
||||
// @ts-expect-error i know i know
|
||||
components.push(actionRow);
|
||||
}
|
||||
|
||||
payload.components = components;
|
||||
|
||||
switch (data.sendAs) {
|
||||
case "bot": {
|
||||
const res = await fetch(
|
||||
`${RouteBases.api}${Routes.channelMessages(data.channelId)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bot ${ctx.env.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const json: { message: string; code: string } = await res.json();
|
||||
await ctx.editReply({
|
||||
content: `Error: ${json.message} (${json.code})`,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "webhook": {
|
||||
const res = await fetch(
|
||||
`${RouteBases.api}${Routes.webhook(
|
||||
data.webhook.id,
|
||||
data.webhook.token,
|
||||
)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
username: data.webhook.name,
|
||||
avatar_url: data.webhook.avatarUrl,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const json: { message: string; code: string } = await res.json();
|
||||
await ctx.editReply({
|
||||
content: `Error: ${json.message} (${json.code})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
packages/bot/src/utils/splitArray.ts
Normal file
7
packages/bot/src/utils/splitArray.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function <T>(array: T[], x: number): T[][] {
|
||||
const result: T[][] = [];
|
||||
for (let i = 0; i < array.length; i += x) {
|
||||
result.push(array.slice(i, i + x));
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -192,6 +192,8 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from "./messages/embed/Embed.js";
|
||||
|
||||
export * from "./components/ActionRow";
|
||||
export * from "./components/button/Button";
|
||||
export * from "./components/Component";
|
||||
|
|
262
packages/builders/src/messages/embed/Embed.ts
Normal file
262
packages/builders/src/messages/embed/Embed.ts
Normal file
|
@ -0,0 +1,262 @@
|
|||
import type {
|
||||
APIEmbed,
|
||||
APIEmbedAuthor,
|
||||
APIEmbedField,
|
||||
APIEmbedFooter,
|
||||
APIEmbedImage,
|
||||
} from "discord-api-types/v10";
|
||||
import { normalizeArray, type RestOrArray } from "../../util/normalizeArray.js";
|
||||
|
||||
export type RGBTuple = [red: number, green: number, blue: number];
|
||||
|
||||
export interface IconData {
|
||||
/**
|
||||
* The URL of the icon
|
||||
*/
|
||||
iconURL?: string;
|
||||
/**
|
||||
* The proxy URL of the icon
|
||||
*/
|
||||
proxyIconURL?: string;
|
||||
}
|
||||
|
||||
export type EmbedAuthorData = IconData &
|
||||
Omit<APIEmbedAuthor, "icon_url" | "proxy_icon_url">;
|
||||
|
||||
export type EmbedAuthorOptions = Omit<EmbedAuthorData, "proxyIconURL">;
|
||||
|
||||
export type EmbedFooterData = IconData &
|
||||
Omit<APIEmbedFooter, "icon_url" | "proxy_icon_url">;
|
||||
|
||||
export type EmbedFooterOptions = Omit<EmbedFooterData, "proxyIconURL">;
|
||||
|
||||
export interface EmbedImageData extends Omit<APIEmbedImage, "proxy_url"> {
|
||||
/**
|
||||
* The proxy URL for the image
|
||||
*/
|
||||
proxyURL?: string;
|
||||
}
|
||||
/**
|
||||
* Represents a embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class EmbedBuilder {
|
||||
public readonly data: APIEmbed;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
this.data = { ...data };
|
||||
if (data.timestamp)
|
||||
this.data.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends fields to the embed
|
||||
*
|
||||
* @remarks
|
||||
* This method accepts either an array of fields or a variable number of field parameters.
|
||||
* The maximum amount of fields that can be added is 25.
|
||||
* @example
|
||||
* Using an array
|
||||
* ```ts
|
||||
* const fields: APIEmbedField[] = ...;
|
||||
* const embed = new EmbedBuilder()
|
||||
* .addFields(fields);
|
||||
* ```
|
||||
* @example
|
||||
* Using rest parameters (variadic)
|
||||
* ```ts
|
||||
* const embed = new EmbedBuilder()
|
||||
* .addFields(
|
||||
* { name: 'Field 1', value: 'Value 1' },
|
||||
* { name: 'Field 2', value: 'Value 2' },
|
||||
* );
|
||||
* ```
|
||||
* @param fields - The fields to add
|
||||
*/
|
||||
public addFields(...fields: RestOrArray<APIEmbedField>): this {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
fields = normalizeArray(fields);
|
||||
|
||||
if (this.data.fields) this.data.fields.push(...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts fields in the embed.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice}.
|
||||
* The maximum amount of fields that can be added is 25.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing fields of an embed.
|
||||
* @example
|
||||
* Remove the first field
|
||||
* ```ts
|
||||
* embed.spliceFields(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n fields
|
||||
* ```ts
|
||||
* const n = 4
|
||||
* embed.spliceFields(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last field
|
||||
* ```ts
|
||||
* embed.spliceFields(-1, 1);
|
||||
* ```
|
||||
* @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 {
|
||||
if (this.data.fields)
|
||||
this.data.fields.splice(index, deleteCount, ...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the embed's fields
|
||||
*
|
||||
* @remarks
|
||||
* This method is an alias for {@link EmbedBuilder.spliceFields}. More specifically,
|
||||
* it splices the entire array of fields, replacing them with the provided fields.
|
||||
*
|
||||
* You can set a maximum of 25 fields.
|
||||
* @param fields - The fields to set
|
||||
*/
|
||||
public setFields(...fields: RestOrArray<APIEmbedField>) {
|
||||
this.spliceFields(
|
||||
0,
|
||||
this.data.fields?.length ?? 0,
|
||||
...normalizeArray(fields),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed
|
||||
*
|
||||
* @param options - The options for the author
|
||||
*/
|
||||
|
||||
public setAuthor(options: EmbedAuthorOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.author = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.author = {
|
||||
name: options.name,
|
||||
url: options.url,
|
||||
icon_url: options.iconURL,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of this embed
|
||||
*
|
||||
* @param color - The color of the embed
|
||||
*/
|
||||
public setColor(color: RGBTuple | number | null): this {
|
||||
if (Array.isArray(color)) {
|
||||
const [red, green, blue] = color;
|
||||
this.data.color = (red << 16) + (green << 8) + blue;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.color = color ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this embed
|
||||
*
|
||||
* @param description - The description
|
||||
*/
|
||||
public setDescription(description: string | null): this {
|
||||
this.data.description = description ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed
|
||||
*
|
||||
* @param options - The options for the footer
|
||||
*/
|
||||
public setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.footer = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.footer = { text: options.text, icon_url: options.iconURL };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image of this embed
|
||||
*
|
||||
* @param url - The URL of the image
|
||||
*/
|
||||
public setImage(url: string | null): this {
|
||||
this.data.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 {
|
||||
this.data.thumbnail = url ? { url } : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of this embed
|
||||
*
|
||||
* @param timestamp - The timestamp or date
|
||||
*/
|
||||
public setTimestamp(timestamp: number | null = Date.now()): this {
|
||||
this.data.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 {
|
||||
this.data.title = title ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this embed
|
||||
*
|
||||
* @param url - The URL
|
||||
*/
|
||||
public setURL(url: string | null): this {
|
||||
this.data.url = url ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the embed to a plain object
|
||||
*/
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this.data };
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ export class RedisAPIClient {
|
|||
this.host = host;
|
||||
}
|
||||
|
||||
public async get(key: string): Promise<string> {
|
||||
public async get(key: string): Promise<string | null> {
|
||||
const url = `${this.host}/get?key=${key}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
|
@ -15,7 +15,8 @@ export class RedisAPIClient {
|
|||
},
|
||||
});
|
||||
|
||||
return response.text();
|
||||
const text = await response.text();
|
||||
return text === "null" ? null : text;
|
||||
}
|
||||
|
||||
public async set(key: string, value: string): Promise<string> {
|
||||
|
@ -60,6 +61,7 @@ export class RedisAPIClient {
|
|||
public async del(key: string): Promise<string> {
|
||||
const url = `${this.host}/del?key=${key}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: this.apiKey,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue