mirror of
https://github.com/xHyroM/roles-bot.git
synced 2024-11-24 09:11:05 +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 returnRoleLpe from "../utils/returnRoleLpe";
|
||||||
import { REDIS } from "../things";
|
import { REDIS } from "../things";
|
||||||
import { encodeToHex, decodeFromString } from "serialize";
|
import { encodeToHex, decodeFromString } from "serialize";
|
||||||
|
import sendFinal from "../utils/sendFinal";
|
||||||
|
|
||||||
// Part 2 Channels ## select button/dropdowns
|
// Part 2 Channels ## select button/dropdowns
|
||||||
new Component({
|
new Component({
|
||||||
|
@ -137,7 +138,7 @@ new Component({
|
||||||
);
|
);
|
||||||
|
|
||||||
return rawRoleIds.length > 0
|
return rawRoleIds.length > 0
|
||||||
? returnRoleLpe(ctx, rawRoleIds[0])
|
? returnRoleLpe(data, ctx, rawRoleIds[0])
|
||||||
: ctx.returnModal({
|
: ctx.returnModal({
|
||||||
title: "Message Preview",
|
title: "Message Preview",
|
||||||
custom_id: "setup:part-messageContent",
|
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({
|
new Component({
|
||||||
id: "setup:part-sendAs",
|
id: "setup:part-sendAs",
|
||||||
acknowledge: false,
|
acknowledge: false,
|
||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
if (!ctx.interaction.guild_id)
|
if (!ctx.guildId)
|
||||||
return await ctx.editReply({ content: "Guild not found." });
|
return await ctx.editReply({ content: "Guild not found." });
|
||||||
|
|
||||||
const rawData = await REDIS.get(
|
const rawData = await REDIS.get(`roles-bot-setup:${ctx.guildId}`);
|
||||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
|
||||||
);
|
|
||||||
if (!rawData)
|
if (!rawData)
|
||||||
return ctx.respond({
|
return ctx.respond({
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
type: InteractionResponseType.ChannelMessageWithSource,
|
||||||
|
@ -212,29 +211,58 @@ new Component({
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = decodeFromString(rawData);
|
const data = decodeFromString(rawData);
|
||||||
console.log(data);
|
const sendAs = ctx.interaction.data.custom_id.split(":")[2];
|
||||||
|
data.sendAs = sendAs;
|
||||||
|
|
||||||
// delete data
|
await REDIS.setex(
|
||||||
await REDIS.del(`roles-bot-setup:${ctx.guildId}`);
|
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||||
|
encodeToHex(data),
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: finish sending
|
switch (sendAs) {
|
||||||
const actionRow = new ActionRowBuilder();
|
case "webhook": {
|
||||||
|
return ctx.returnModal({
|
||||||
/*switch (selecting) {
|
title: "Webhook Preview",
|
||||||
case "buttons": {
|
custom_id: "setup:part-webhook",
|
||||||
// TOOD: finish
|
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": {
|
case "bot": {
|
||||||
// TODO: finish
|
sendFinal(ctx, data);
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return ctx.respond({
|
return ctx.respond({
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
type: InteractionResponseType.ChannelMessageWithSource,
|
||||||
data: {
|
data: {
|
||||||
content: "Done!",
|
content: "Setup completed!",
|
||||||
flags: MessageFlags.Ephemeral,
|
flags: MessageFlags.Ephemeral,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import "./commands/setup";
|
import "./commands/setup";
|
||||||
import "./components/setup";
|
import "./components/setup";
|
||||||
|
import "./components/select";
|
||||||
import "./modals/setup";
|
import "./modals/setup";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -92,9 +93,10 @@ export default {
|
||||||
}
|
}
|
||||||
case InteractionType.MessageComponent: {
|
case InteractionType.MessageComponent: {
|
||||||
const context = new ComponentContext(interaction, env);
|
const context = new ComponentContext(interaction, env);
|
||||||
const component = COMPONENTS.find((cmp) =>
|
const component =
|
||||||
context.interaction.data.custom_id.startsWith(cmp.id),
|
COMPONENTS.find((cmp) =>
|
||||||
);
|
context.interaction.data.custom_id.startsWith(cmp.id),
|
||||||
|
) ?? COMPONENTS.find((cmp) => cmp.default);
|
||||||
|
|
||||||
if (!component)
|
if (!component)
|
||||||
return new Response("Unknown component", { status: 404 });
|
return new Response("Unknown component", { status: 404 });
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import {
|
import {
|
||||||
|
APIWebhook,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
InteractionResponseType,
|
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
|
RouteBases,
|
||||||
|
Routes,
|
||||||
} from "discord-api-types/v10";
|
} from "discord-api-types/v10";
|
||||||
import { Modal } from "../structs/Modal";
|
import { Modal } from "../structs/Modal";
|
||||||
import { ActionRowBuilder, ButtonBuilder } from "builders";
|
import { ActionRowBuilder, ButtonBuilder } from "builders";
|
||||||
import { encodeToHex, decodeFromString } from "serialize";
|
import { encodeToHex, decodeFromString } from "serialize";
|
||||||
import { REDIS } from "../things";
|
import { REDIS } from "../things";
|
||||||
|
import sendFinal from "../utils/sendFinal";
|
||||||
|
import { RoleId } from "../types";
|
||||||
|
|
||||||
// Part 5 Roles ## add label, placeholder, emoji OR message content
|
// Part 5 Roles ## add label, placeholder, emoji OR message content
|
||||||
new Modal({
|
new Modal({
|
||||||
id: "setup:part-roles-lpe",
|
id: "setup:part-roles-lpe",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
const rawData = await REDIS.get(
|
const rawData = await REDIS.get(
|
||||||
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
`roles-bot-setup:${ctx.interaction.guild_id}`,
|
||||||
|
@ -22,17 +27,28 @@ new Modal({
|
||||||
|
|
||||||
const data = decodeFromString(rawData);
|
const data = decodeFromString(rawData);
|
||||||
const rawRoleIds = data.rawRoleIds as string[];
|
const rawRoleIds = data.rawRoleIds as string[];
|
||||||
const roleIds = (data.roleIds ?? []) as {
|
const roleIds = (data.roleIds ?? []) as RoleId[];
|
||||||
label: string;
|
|
||||||
placeholder: string;
|
|
||||||
emoji: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
const label = ctx.interaction.data.components[0].components[0].value;
|
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[1].components[0].value;
|
||||||
const emoji = ctx.interaction.data.components[2].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();
|
rawRoleIds.shift();
|
||||||
data.rawRoleIds = rawRoleIds;
|
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 {
|
interface ComponentOptions {
|
||||||
id: string;
|
id: string;
|
||||||
acknowledge?: boolean;
|
acknowledge?: boolean;
|
||||||
|
default?: boolean;
|
||||||
flags?: MessageFlags;
|
flags?: MessageFlags;
|
||||||
run: (interaction: ComponentContext) => void;
|
run: (interaction: ComponentContext) => void;
|
||||||
}
|
}
|
||||||
|
@ -12,12 +13,14 @@ interface ComponentOptions {
|
||||||
export class Component {
|
export class Component {
|
||||||
public id: string;
|
public id: string;
|
||||||
public acknowledge: boolean;
|
public acknowledge: boolean;
|
||||||
|
public default: boolean;
|
||||||
public flags: MessageFlags | undefined;
|
public flags: MessageFlags | undefined;
|
||||||
public run: (interaction: ComponentContext) => void | Response;
|
public run: (interaction: ComponentContext) => void | Response;
|
||||||
|
|
||||||
constructor(options: ComponentOptions) {
|
constructor(options: ComponentOptions) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.acknowledge = options.acknowledge ?? true;
|
this.acknowledge = options.acknowledge ?? true;
|
||||||
|
this.default = options.default ?? false;
|
||||||
this.flags = options.flags;
|
this.flags = options.flags;
|
||||||
this.run = options.run;
|
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;
|
redisApiClientKey: string;
|
||||||
redisApiClientHost: 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";
|
} from "discord-api-types/v10";
|
||||||
import { REDIS } from "../things";
|
import { REDIS } from "../things";
|
||||||
import { decodeFromString } from "serialize";
|
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}`);
|
const rolesRaw = await REDIS.get(`roles-bot-setup-roles:${ctx.guildId}`);
|
||||||
if (!rolesRaw)
|
if (!rolesRaw)
|
||||||
return ctx.respond({
|
return ctx.respond({
|
||||||
|
@ -25,40 +26,65 @@ export default async function (ctx: Context, rawRole: string) {
|
||||||
|
|
||||||
const roleName = roles?.find((r) => r.id === rawRole)?.name;
|
const roleName = roles?.find((r) => r.id === rawRole)?.name;
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
new ActionRowBuilder<TextInputBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setLabel("Label")
|
||||||
|
.setCustomId("label")
|
||||||
|
.setPlaceholder("Ping")
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
new ActionRowBuilder<TextInputBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setLabel("Emoji")
|
||||||
|
.setCustomId("emoji")
|
||||||
|
.setPlaceholder("emoji 💡")
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.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({
|
return ctx.returnModal({
|
||||||
title: `${roleName?.slice(0, 39)} Role`,
|
title: `${roleName?.slice(0, 39)} Role`,
|
||||||
custom_id: "setup:part-roles-lpe",
|
custom_id: "setup:part-roles-lpe",
|
||||||
components: [
|
components,
|
||||||
new ActionRowBuilder<TextInputBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new TextInputBuilder()
|
|
||||||
.setLabel("Label")
|
|
||||||
.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),
|
|
||||||
)
|
|
||||||
.toJSON(),
|
|
||||||
new ActionRowBuilder<TextInputBuilder>()
|
|
||||||
.addComponents(
|
|
||||||
new TextInputBuilder()
|
|
||||||
.setLabel("Emoji")
|
|
||||||
.setCustomId("emoji")
|
|
||||||
.setPlaceholder("emoji 💡")
|
|
||||||
.setStyle(TextInputStyle.Short)
|
|
||||||
.setRequired(false),
|
|
||||||
)
|
|
||||||
.toJSON(),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./messages/embed/Embed.js";
|
||||||
|
|
||||||
export * from "./components/ActionRow";
|
export * from "./components/ActionRow";
|
||||||
export * from "./components/button/Button";
|
export * from "./components/button/Button";
|
||||||
export * from "./components/Component";
|
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;
|
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 url = `${this.host}/get?key=${key}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
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> {
|
public async set(key: string, value: string): Promise<string> {
|
||||||
|
@ -60,6 +61,7 @@ export class RedisAPIClient {
|
||||||
public async del(key: string): Promise<string> {
|
public async del(key: string): Promise<string> {
|
||||||
const url = `${this.host}/del?key=${key}`;
|
const url = `${this.host}/del?key=${key}`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: this.apiKey,
|
Authorization: this.apiKey,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue