Merge branch 'discordeno:main' into main

This commit is contained in:
Tricked
2021-12-22 20:22:40 +01:00
committed by GitHub
44 changed files with 447 additions and 410 deletions

View File

@@ -85,12 +85,13 @@ import { StatusUpdate } from "./types/gateway/statusUpdate.ts";
import { calculateBits, calculatePermissions } from "./util/permissions.ts";
import { transformScheduledEvent } from "./transformers/scheduledEvent.ts";
import { DiscordenoScheduledEvent } from "./transformers/scheduledEvent.ts";
import { transformThreadMember } from "./transformers/threadMember.ts";
import { DiscordenoThreadMember, transformThreadMember } from "./transformers/threadMember.ts";
import { transformApplicationCommandOption } from "./transformers/applicationCommandOption.ts";
import { transformApplicationCommand } from "./transformers/applicationCommand.ts";
import { transformWelcomeScreen } from "./transformers/welcomeScreen.ts";
import { transformVoiceRegion } from "./transformers/voiceRegion.ts";
import { transformWidget } from "./transformers/widget.ts";
import { transformStageInstance } from "./transformers/stageInstance.ts";
export function createBot(options: CreateBotOptions): Bot {
const bot = {
@@ -141,6 +142,10 @@ export function createEventHandlers(events: Partial<EventHandlers>): EventHandle
return {
debug: events.debug ?? ignore,
threadCreate: events.threadCreate ?? ignore,
threadDelete: events.threadDelete ?? ignore,
threadMembersUpdate: events.threadMembersUpdate ?? ignore,
threadUpdate: events.threadUpdate ?? ignore,
scheduledEventCreate: events.scheduledEventCreate ?? ignore,
scheduledEventUpdate: events.scheduledEventUpdate ?? ignore,
scheduledEventDelete: events.scheduledEventDelete ?? ignore,
@@ -484,6 +489,7 @@ export interface Transformers {
welcomeScreen: typeof transformWelcomeScreen;
voiceRegion: typeof transformVoiceRegion;
widget: typeof transformWidget;
stageInstance: typeof transformStageInstance;
}
export function createTransformers(options: Partial<Transformers>) {
@@ -517,6 +523,7 @@ export function createTransformers(options: Partial<Transformers>) {
welcomeScreen: options.welcomeScreen || transformWelcomeScreen,
voiceRegion: options.voiceRegion || transformVoiceRegion,
widget: options.widget || transformWidget,
stageInstance: options.stageInstance || transformStageInstance,
};
}
@@ -627,6 +634,18 @@ export interface GatewayManager {
export interface EventHandlers {
debug: (text: string, ...args: any[]) => unknown;
threadCreate: (bot: Bot, thread: DiscordenoChannel) => unknown;
threadDelete: (bot: Bot, thread: DiscordenoChannel) => unknown;
threadMembersUpdate: (
bot: Bot,
payload: {
id: bigint;
guildId: bigint;
addedMembers?: DiscordenoThreadMember[];
removedMemberIds?: bigint[];
}
) => unknown;
threadUpdate: (bot: Bot, thread: DiscordenoChannel) => unknown;
scheduledEventCreate: (bot: Bot, event: DiscordenoScheduledEvent) => unknown;
scheduledEventUpdate: (bot: Bot, event: DiscordenoScheduledEvent) => unknown;
scheduledEventDelete: (bot: Bot, event: DiscordenoScheduledEvent) => unknown;

View File

@@ -6,5 +6,5 @@ import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
export async function handleThreadCreate(bot: Bot, data: DiscordGatewayPayload) {
const payload = data.d as SnakeCasedPropertiesDeep<Channel>;
// bot.events.threadCreate(bot, payload);
bot.events.threadCreate(bot, bot.transformers.channel(bot, { channel: payload }));
}

View File

@@ -1,12 +1,9 @@
import { Bot } from "../../bot.ts";
import { Channel } from "../../types/channels/channel.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gatewayPayload.ts";
import { snowflakeToBigint } from "../../util/bigint.ts";
import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
export async function handleThreadDelete(data: DiscordGatewayPayload) {
// const payload = data.d as Channel;
// const cachedChannel = await cacheHandlers.get("threads", snowflakeToBigint(payload.id));
// if (!cachedChannel) return;
// await cacheHandlers.delete("threads", snowflakeToBigint(payload.id));
// await cacheHandlers.forEach("DELETE_MESSAGES_FROM_CHANNEL", { channelId: snowflakeToBigint(payload.id) });
// eventHandlers.threadDelete?.(cachedChannel);
export async function handleThreadDelete(bot: Bot, data: DiscordGatewayPayload) {
const payload = data.d as SnakeCasedPropertiesDeep<Channel>;
bot.events.threadDelete(bot, bot.transformers.channel(bot, { channel: payload }));
}

View File

@@ -1,20 +1,21 @@
import { Bot } from "../../bot.ts";
import { ThreadListSync } from "../../types/channels/threads/threadListSync.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gatewayPayload.ts";
import { snowflakeToBigint } from "../../util/bigint.ts";
import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
import { Collection } from "../../util/collection.ts";
export async function handleThreadListSync(data: DiscordGatewayPayload) {
// const payload = data.d as ThreadListSync;
// const threads = await Promise.all(
// payload.threads.map(async (thread) => {
// const threadData = channelToThread(thread);
// await cacheHandlers.set("threads", threadData.id, threadData);
// return threadData;
// })
// );
// eventHandlers.threadListSync?.(
// new Collection(threads.map((t) => [t.id, t])),
// payload.members.map((member) => threadMemberModified(member)),
// snowflakeToBigint(payload.guildId)
// );
export async function handleThreadListSync(bot: Bot, data: DiscordGatewayPayload) {
const payload = data.d as SnakeCasedPropertiesDeep<ThreadListSync>;
const guildId = bot.transformers.snowflake(payload.guild_id);
return {
guildId,
channelIds: payload.channel_ids?.map((id) => bot.transformers.snowflake(id)),
threads: payload.threads.map((thread) => bot.transformers.channel(bot, { channel: thread, guildId })),
members: payload.members.map((member) => ({
id: member.id ? bot.transformers.snowflake(member.id) : undefined,
userId: member.user_id ? bot.transformers.snowflake(member.user_id) : undefined,
joinTimestamp: Date.parse(member.join_timestamp),
})),
};
}

View File

@@ -1,12 +1,14 @@
import { Bot } from "../../bot.ts";
import { ThreadMembersUpdate } from "../../types/channels/threads/threadMembersUpdate.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gatewayPayload.ts";
import { snowflakeToBigint } from "../../util/bigint.ts";
import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
export async function handleThreadMembersUpdate(data: DiscordGatewayPayload) {
// const payload = data.d as ThreadMembersUpdate;
// const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id));
// if (!thread) return;
// thread.memberCount = payload.memberCount;
// await cacheHandlers.set("threads", thread.id, thread);
// eventHandlers.threadMembersUpdate?.(threadMembersUpdateModified(payload));
export async function handleThreadMembersUpdate(bot: Bot, data: DiscordGatewayPayload) {
const payload = data.d as SnakeCasedPropertiesDeep<ThreadMembersUpdate>;
bot.events.threadMembersUpdate(bot, {
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
addedMembers: payload.added_members?.map((member) => bot.transformers.threadMember(bot, member)),
removedMemberIds: payload.removed_member_ids?.map((id) => bot.transformers.snowflake(id)),
});
}

View File

@@ -1,19 +1,7 @@
import { ThreadMember } from "../../types/channels/threads/threadMember.ts";
import { Bot } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gatewayPayload.ts";
import { snowflakeToBigint } from "../../util/bigint.ts";
export async function handleThreadMemberUpdate(data: DiscordGatewayPayload) {
// const payload = data.d as ThreadMember;
// // The id field is omitted from the thread member dispatched within the GUILD_CREATE gateway event.
// const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id!));
// if (!thread) return;
// thread.botIsMember = true;
// await cacheHandlers.set("threads", thread.id, thread);
// const member = {
// ...payload,
// id: snowflakeToBigint(payload.id!),
// userId: snowflakeToBigint(payload.userId!),
// joinTimestamp: Date.parse(payload.joinTimestamp),
// };
// eventHandlers.threadMemberUpdate?.(member, thread);
export async function handleThreadMemberUpdate(bot: Bot, data: DiscordGatewayPayload) {
// This event is documented for completeness, but unlikely to be used by most bots
return;
}

View File

@@ -1,12 +1,10 @@
import { Bot } from "../../bot.ts";
import { Channel } from "../../types/channels/channel.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gatewayPayload.ts";
import { snowflakeToBigint } from "../../util/bigint.ts";
import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
export async function handleThreadUpdate(data: DiscordGatewayPayload) {
// const payload = data.d as Channel;
// const oldThread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id));
// if (!oldThread) return;
// const thread = channelToThread(payload);
// await cacheHandlers.set("threads", thread.id, thread);
// eventHandlers.threadUpdate?.(thread, oldThread);
export async function handleThreadUpdate(bot: Bot, data: DiscordGatewayPayload) {
const payload = data.d as SnakeCasedPropertiesDeep<Channel>;
bot.events.threadUpdate(bot, bot.transformers.channel(bot, { channel: payload }));
}

View File

@@ -10,10 +10,5 @@ export async function createStageInstance(bot: Bot, channelId: bigint, topic: st
privacy_level: privacyLevel || PrivacyLevel.GuildOnly,
});
return {
id: bot.transformers.snowflake(result.id),
guildId: bot.transformers.snowflake(result.guild_id),
channelId: bot.transformers.snowflake(result.channel_id),
topic: result.topic,
};
return bot.transformers.stageInstance(bot, result);
}

View File

@@ -9,10 +9,5 @@ export async function getStageInstance(bot: Bot, channelId: bigint) {
bot.constants.endpoints.STAGE_INSTANCE(channelId)
);
return {
id: bot.transformers.snowflake(result.id),
guildId: bot.transformers.snowflake(result.guild_id),
channelId: bot.transformers.snowflake(result.channel_id),
topic: result.topic,
};
return bot.transformers.stageInstance(bot, result);
}

View File

@@ -13,10 +13,5 @@ export async function updateStageInstance(bot: Bot, channelId: bigint, data: AtL
}
);
return {
id: bot.transformers.snowflake(result.id),
guildId: bot.transformers.snowflake(result.guild_id),
channelId: bot.transformers.snowflake(result.channel_id),
topic: result.topic,
};
return bot.transformers.stageInstance(bot, result);
}

View File

@@ -48,5 +48,7 @@ export async function getAuditLogs(bot: Bot, guildId: bigint, options?: GetGuild
}
: undefined,
})),
threads: auditlog.threads.map((thread) => bot.transformers.channel(bot, { channel: thread, guildId })),
scheduledEvents: auditlog.scheduled_events.map((event) => bot.transformers.scheduledEvent(bot, event)),
};
}

View File

@@ -6,20 +6,20 @@ import type { Bot } from "../../bot.ts";
export function guildIconURL(
bot: Bot,
id: bigint,
options: {
icon?: string | bigint;
icon: bigint | undefined,
options?: {
size?: ImageSize;
format?: ImageFormat;
}
) {
return options.icon
return icon
? bot.utils.formatImageURL(
bot.constants.endpoints.GUILD_ICON(
id,
typeof options.icon === "string" ? options.icon : bot.utils.iconBigintToHash(options.icon)
typeof icon === "string" ? icon : bot.utils.iconBigintToHash(icon)
),
options.size || 128,
options.format
options?.size || 128,
options?.format
)
: undefined;
}

View File

@@ -6,20 +6,20 @@ import type { Bot } from "../../bot.ts";
export function guildSplashURL(
bot: Bot,
id: bigint,
options: {
splash?: string | bigint;
splash: bigint | undefined,
options?: {
size?: ImageSize;
format?: ImageFormat;
}
) {
return options.splash
return splash
? bot.utils.formatImageURL(
bot.constants.endpoints.GUILD_SPLASH(
id,
typeof options.splash === "string" ? options.splash : bot.utils.iconBigintToHash(options.splash)
typeof splash === "string" ? splash : bot.utils.iconBigintToHash(splash)
),
options.size || 128,
options.format
options?.size || 128,
options?.format
)
: undefined;
}

View File

@@ -18,12 +18,22 @@ export async function editInteractionResponse(bot: Bot, token: string, options:
allowed_mentions: options.allowedMentions
? {
parse: options.allowedMentions.parse,
roles: options.allowedMentions.roles,
users: options.allowedMentions.users,
roles: options.allowedMentions.roles?.map((id) => id.toString()),
users: options.allowedMentions.users?.map((id) => id.toString()),
replied_user: options.allowedMentions.repliedUser,
}
: undefined,
attachments: options.attachments,
attachments: options.attachments?.map((attachment) => ({
id: attachment.id.toString(),
filename: attachment.filename,
content_type: attachment.contentType,
size: attachment.size,
url: attachment.url,
proxy_url: attachment.proxyUrl,
height: attachment.height,
width: attachment.width,
ephemeral: attachment.ephemeral,
})),
components: options.components?.map((component) => ({
type: component.type,
components: component.components.map((subcomponent) => {
@@ -79,7 +89,7 @@ export async function editInteractionResponse(bot: Bot, token: string, options:
};
}),
})),
message_id: options.messageId,
message_id: options.messageId?.toString(),
}
);

View File

@@ -22,12 +22,22 @@ export async function editFollowupMessage(
allowed_mentions: options.allowedMentions
? {
parse: options.allowedMentions.parse,
roles: options.allowedMentions.roles,
users: options.allowedMentions.users,
roles: options.allowedMentions.roles?.map((id) => id.toString()),
users: options.allowedMentions.users?.map((id) => id.toString()),
replied_user: options.allowedMentions.repliedUser,
}
: undefined,
attachments: options.attachments,
attachments: options.attachments?.map((attachment) => ({
id: attachment.id.toString(),
filename: attachment.filename,
content_type: attachment.contentType,
size: attachment.size,
url: attachment.url,
proxy_url: attachment.proxyUrl,
height: attachment.height,
width: attachment.width,
ephemeral: attachment.ephemeral,
})),
components: options.components?.map((component) => ({
type: component.type,
components: component.components.map((subcomponent) => {

View File

@@ -7,20 +7,20 @@ export function avatarURL(
bot: Bot,
userId: bigint,
discriminator: number,
options: {
avatar?: string | bigint;
options?: {
avatar: bigint | undefined;
size?: ImageSize;
format?: ImageFormat;
}
) {
return options.avatar
return options?.avatar
? bot.utils.formatImageURL(
bot.constants.endpoints.USER_AVATAR(
userId,
typeof options.avatar === "string" ? options.avatar : bot.utils.iconBigintToHash(options.avatar)
typeof options?.avatar === "string" ? options.avatar : bot.utils.iconBigintToHash(options?.avatar)
),
options.size || 128,
options.format
options?.size || 128,
options?.format
)
: bot.constants.endpoints.USER_DEFAULT_AVATAR(Number(discriminator) % 5);
}

View File

@@ -4,16 +4,8 @@ import type { Bot } from "../../bot.ts";
import { MessageComponentTypes } from "../../types/messages/components/messageComponentTypes.ts";
/** Edit the message. */
export async function editMessage(bot: Bot, channelId: bigint, messageId: bigint, content: string | EditMessage) {
if (typeof content === "string") content = { content };
content.embeds?.splice(10);
if (content.content && content.content.length > 2000) {
throw new Error(bot.constants.Errors.MESSAGE_MAX_LENGTH);
}
const result = await bot.rest.runMethod<Message>(
export async function editMessage(bot: Bot, channelId: bigint, messageId: bigint, content: EditMessage) {
const result = await bot.rest.runMethod<Message>(
bot.rest,
"patch",
bot.constants.endpoints.CHANNEL_MESSAGE(channelId, messageId),

View File

@@ -5,36 +5,7 @@ import type { Bot } from "../../bot.ts";
import { MessageComponentTypes } from "../../types/messages/components/messageComponentTypes.ts";
/** Send a message to the channel. Requires SEND_MESSAGES permission. */
export async function sendMessage(bot: Bot, channelId: bigint, content: string | CreateMessage) {
if (typeof content === "string") content = { content };
// Use ... for content length due to unicode characters and js .length handling
if (content.content && !bot.utils.validateLength(content.content, { max: 2000 })) {
throw new Error(bot.constants.Errors.MESSAGE_MAX_LENGTH);
}
if (content.allowedMentions) {
if (content.allowedMentions.users?.length) {
if (content.allowedMentions.parse?.includes(AllowedMentionsTypes.UserMentions)) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((p) => p !== "users");
}
if (content.allowedMentions.users.length > 100) {
content.allowedMentions.users = content.allowedMentions.users.slice(0, 100);
}
}
if (content.allowedMentions.roles?.length) {
if (content.allowedMentions.parse?.includes(AllowedMentionsTypes.RoleMentions)) {
content.allowedMentions.parse = content.allowedMentions.parse.filter((p) => p !== "roles");
}
if (content.allowedMentions.roles.length > 100) {
content.allowedMentions.roles = content.allowedMentions.roles.slice(0, 100);
}
}
}
export async function sendMessage(bot: Bot, channelId: bigint, content: CreateMessage) {
const result = await bot.rest.runMethod<Message>(
bot.rest,
"post",
@@ -170,3 +141,4 @@ export async function sendMessage(bot: Bot, channelId: bigint, content: string |
return bot.transformers.message(bot, result);
}

View File

@@ -6,24 +6,6 @@ import type { Bot } from "../../bot.ts";
* NOTE: username: if changed may cause the bot's discriminator to be randomized.
*/
export async function editBotProfile(bot: Bot, options: { username?: string; botAvatarURL?: string | null }) {
// Nothing was edited
if (!options.username && options.botAvatarURL === undefined) return;
// Check username requirements if username was provided
if (options.username) {
if (options.username.length > 32) {
throw new Error(Errors.USERNAME_MAX_LENGTH);
}
if (options.username.length < 2) {
throw new Error(Errors.USERNAME_MIN_LENGTH);
}
if (["@", "#", ":", "```"].some((char) => options.username!.includes(char))) {
throw new Error(Errors.USERNAME_INVALID_CHARACTER);
}
if (["discordtag", "everyone", "here"].includes(options.username)) {
throw new Error(Errors.USERNAME_INVALID_USERNAME);
}
}
const avatar = options?.botAvatarURL ? await bot.utils.urlToBase64(options?.botAvatarURL) : options?.botAvatarURL;
const result = await bot.rest.runMethod<User>(bot.rest, "patch", bot.constants.endpoints.USER_BOT, {

View File

@@ -7,7 +7,7 @@ export function editBotStatus(bot: Bot, data: Omit<StatusUpdate, "afk" | "since"
bot.events.debug(`Running forEach loop in editBotStatus function.`);
bot.gateway.sendShardMessage(bot.gateway, shard, {
op: GatewayOpcodes.StatusUpdate,
op: GatewayOpcodes.PresenceUpdate,
d: {
since: null,
afk: false,

View File

@@ -1,6 +1,5 @@
import { Application } from "../../types/applications/application.ts";
import type { Bot } from "../../bot.ts";
import { SnakeCasedPropertiesDeep } from "../../types/util.ts";
/** Get the applications info */
export async function getApplicationInfo(bot: Bot) {

View File

@@ -8,18 +8,15 @@ import type { Webhook } from "../../types/webhooks/webhook.ts";
* Webhook names cannot be: 'clyde'
*/
export async function createWebhook(bot: Bot, channelId: bigint, options: CreateWebhook) {
if (
// Specific usernames that discord does not allow
options.name === "clyde" ||
!bot.utils.validateLength(options.name, { min: 2, max: 32 })
) {
throw new Error(bot.constants.Errors.INVALID_WEBHOOK_NAME);
}
const result = await bot.rest.runMethod<Webhook>(bot.rest, "post", bot.constants.endpoints.CHANNEL_WEBHOOKS(channelId), {
...options,
avatar: options.avatar ? await bot.utils.urlToBase64(options.avatar) : undefined,
});
const result = await bot.rest.runMethod<Webhook>(
bot.rest,
"post",
bot.constants.endpoints.CHANNEL_WEBHOOKS(channelId),
{
...options,
avatar: options.avatar ? await bot.utils.urlToBase64(options.avatar) : undefined,
}
);
return bot.transformers.webhook(bot, result);
}

View File

@@ -1,9 +1,22 @@
import type { Bot } from "../../bot.ts";
export async function deleteWebhookMessage(bot: Bot, webhookId: bigint, webhookToken: string, messageId: bigint) {
await bot.rest.runMethod<undefined>(
bot.rest,
"delete",
bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, messageId)
);
export interface DeleteWebhookMessageOptions {
/** id of the thread the message is in */
threadId: bigint;
}
export async function deleteWebhookMessage(
bot: Bot,
webhookId: bigint,
webhookToken: string,
messageId: bigint,
options?: DeleteWebhookMessageOptions
) {
let url = bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, messageId);
// QUERY PARAMS
if (options?.threadId) {
url += `?threadId=${options.threadId}`;
}
await bot.rest.runMethod<undefined>(bot.rest, "delete", url);
}

View File

@@ -1,124 +1,103 @@
import type { Message } from "../../types/messages/message.ts";
import type { EditWebhookMessage } from "../../types/webhooks/editWebhookMessage.ts";
import type { Bot } from "../../bot.ts";
import { AllowedMentionsTypes } from "../../types/messages/allowedMentionsTypes.ts";
import { MessageComponentTypes } from "../../types/messages/components/messageComponentTypes.ts";
import { hasProperty } from "../../util/utils.ts";
import { ButtonComponent } from "../../types/messages/components/buttonComponent.ts";
export async function editWebhookMessage(
bot: Bot,
webhookId: bigint,
webhookToken: string,
options: EditWebhookMessage & { messageId?: bigint }
options: EditWebhookMessage & { messageId?: bigint; threadId?: bigint }
) {
if (options.content && options.content.length > 2000) {
throw Error(bot.constants.Errors.MESSAGE_MAX_LENGTH);
let url = options.messageId
? bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, options.messageId)
: bot.constants.endpoints.WEBHOOK_MESSAGE_ORIGINAL(webhookId, webhookToken);
// QUERY PARAMS
if (options.threadId) {
url += `?thread_id=${options.threadId}`;
}
if (options.embeds && options.embeds.length > 10) {
options.embeds.splice(10);
}
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (options.allowedMentions.parse?.includes(AllowedMentionsTypes.UserMentions)) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((p) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(0, 100);
}
}
if (options.allowedMentions.roles?.length) {
if (options.allowedMentions.parse?.includes(AllowedMentionsTypes.RoleMentions)) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((p) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(0, 100);
}
}
}
const result = await bot.rest.runMethod<Message>(
bot.rest,
"patch",
options.messageId
? bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, options.messageId)
: bot.constants.endpoints.WEBHOOK_MESSAGE_ORIGINAL(webhookId, webhookToken),
{
content: options.content,
embeds: options.embeds,
file: options.file,
allowed_mentions: options.allowedMentions
? {
parse: options.allowedMentions.parse,
roles: options.allowedMentions.roles,
users: options.allowedMentions.users,
replied_user: options.allowedMentions.repliedUser,
}
: undefined,
attachments: options.attachments,
components: options.components?.map((component) => ({
type: component.type,
components: component.components.map((subcomponent) => {
if (subcomponent.type === MessageComponentTypes.InputText) {
return {
type: subcomponent.type,
style: subcomponent.style,
custom_id: subcomponent.customId,
label: subcomponent.label,
placeholder: subcomponent.placeholder,
min_length: subcomponent.minLength ?? subcomponent.required === false ? 0 : subcomponent.minLength,
max_length: subcomponent.maxLength,
};
}
if (subcomponent.type === MessageComponentTypes.SelectMenu)
return {
type: subcomponent.type,
custom_id: subcomponent.customId,
placeholder: subcomponent.placeholder,
min_values: subcomponent.minValues,
max_values: subcomponent.maxValues,
options: subcomponent.options.map((option) => ({
label: option.label,
value: option.value,
description: option.description,
emoji: option.emoji
? {
id: option.emoji.id?.toString(),
name: option.emoji.name,
animated: option.emoji.animated,
}
: undefined,
default: option.default,
})),
};
const result = await bot.rest.runMethod<Message>(bot.rest, "patch", url, {
content: options.content,
embeds: options.embeds,
file: options.file,
allowed_mentions: options.allowedMentions
? {
parse: options.allowedMentions.parse,
roles: options.allowedMentions.roles?.map((id) => id.toString()),
users: options.allowedMentions.users?.map((id) => id.toString()),
replied_user: options.allowedMentions.repliedUser,
}
: undefined,
attachments: options.attachments?.map((attachment) => ({
id: attachment.id.toString(),
filename: attachment.filename,
content_type: attachment.contentType,
size: attachment.size,
url: attachment.url,
proxy_url: attachment.proxyUrl,
height: attachment.height,
width: attachment.width,
ephemeral: attachment.ephemeral,
})),
components: options.components?.map((component) => ({
type: component.type,
components: component.components.map((subcomponent) => {
if (subcomponent.type === MessageComponentTypes.InputText) {
return {
type: subcomponent.type,
style: subcomponent.style,
custom_id: subcomponent.customId,
label: subcomponent.label,
placeholder: subcomponent.placeholder,
min_length: subcomponent.minLength ?? subcomponent.required === false ? 0 : subcomponent.minLength,
max_length: subcomponent.maxLength,
};
}
if (subcomponent.type === MessageComponentTypes.SelectMenu)
return {
type: subcomponent.type,
custom_id: subcomponent.customId,
label: subcomponent.label,
style: subcomponent.style,
emoji:
"emoji" in subcomponent && subcomponent.emoji
placeholder: subcomponent.placeholder,
min_values: subcomponent.minValues,
max_values: subcomponent.maxValues,
options: subcomponent.options.map((option) => ({
label: option.label,
value: option.value,
description: option.description,
emoji: option.emoji
? {
id: subcomponent.emoji.id?.toString(),
name: subcomponent.emoji.name,
animated: subcomponent.emoji.animated,
id: option.emoji.id?.toString(),
name: option.emoji.name,
animated: option.emoji.animated,
}
: undefined,
url: "url" in subcomponent ? subcomponent.url : undefined,
disabled: "disabled" in subcomponent ? subcomponent.disabled : undefined,
default: option.default,
})),
};
}),
})),
message_id: options.messageId?.toString(),
}
);
return {
type: subcomponent.type,
custom_id: subcomponent.customId,
label: subcomponent.label,
style: subcomponent.style,
emoji:
"emoji" in subcomponent && subcomponent.emoji
? {
id: subcomponent.emoji.id?.toString(),
name: subcomponent.emoji.name,
animated: subcomponent.emoji.animated,
}
: undefined,
url: "url" in subcomponent ? subcomponent.url : undefined,
disabled: "disabled" in subcomponent ? subcomponent.disabled : undefined,
};
}),
})),
message_id: options.messageId?.toString(),
});
return bot.transformers.message(bot, result);
}

View File

@@ -1,14 +1,26 @@
import type { Message } from "../../types/messages/message.ts";
import type { SnakeCasedPropertiesDeep } from "../../types/util.ts";
import type { Bot } from "../../bot.ts";
export interface GetWebhookMessageOptions {
threadId: bigint;
}
/** Returns a previously-sent webhook message from the same token. Returns a message object on success. */
export async function getWebhookMessage(bot: Bot, webhookId: bigint, webhookToken: string, messageId: bigint) {
const result = await bot.rest.runMethod<Message>(
bot.rest,
"get",
bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, messageId)
);
export async function getWebhookMessage(
bot: Bot,
webhookId: bigint,
webhookToken: string,
messageId: bigint,
options?: GetWebhookMessageOptions
) {
let url = bot.constants.endpoints.WEBHOOK_MESSAGE(webhookId, webhookToken, messageId);
// QUERY PARAMS
if (options?.threadId) {
url += `?thread_id=${options.threadId}`;
}
const result = await bot.rest.runMethod<Message>(bot.rest, "get", url);
return bot.transformers.message(bot, result);
}

View File

@@ -5,41 +5,6 @@ import type { ExecuteWebhook } from "../../types/webhooks/executeWebhook.ts";
/** Send a webhook with webhook Id and webhook token */
export async function sendWebhook(bot: Bot, webhookId: bigint, webhookToken: string, options: ExecuteWebhook) {
// DEFAULT TO TRUE
options.wait = options.wait ?? true;
if (!options.content && !options.file && !options.embeds) {
throw new Error(bot.constants.Errors.INVALID_WEBHOOK_OPTIONS);
}
if (options.content && options.content.length > 2000) {
throw Error(bot.constants.Errors.MESSAGE_MAX_LENGTH);
}
options.embeds?.splice(10);
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (options.allowedMentions.parse?.includes(AllowedMentionsTypes.UserMentions)) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((p) => p !== "users");
}
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(0, 100);
}
}
if (options.allowedMentions.roles?.length) {
if (options.allowedMentions.parse?.includes(AllowedMentionsTypes.RoleMentions)) {
options.allowedMentions.parse = options.allowedMentions.parse.filter((p) => p !== "roles");
}
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(0, 100);
}
}
}
const allowedMentions = options.allowedMentions
? {
parse: options.allowedMentions.parse,
@@ -65,6 +30,7 @@ export async function sendWebhook(bot: Bot, webhookId: bigint, webhookToken: str
file: options.file,
embeds: options.embeds,
allowed_mentions: allowedMentions,
component: options.components,
}
);
if (!options.wait) return;

View File

@@ -0,0 +1,27 @@
import { Bot } from "../bot.ts";
import { StageInstance } from "../types/channels/stageInstance.ts";
import { SnakeCasedPropertiesDeep } from "../types/util.ts";
import { PrivacyLevel } from "../types/channels/privacyLevel.ts";
export function transformStageInstance(
bot: Bot,
payload: SnakeCasedPropertiesDeep<StageInstance>
): DiscordenoStageInstance {
return {
id: bot.transformers.snowflake(payload.id),
guildId: bot.transformers.snowflake(payload.guild_id),
channelId: bot.transformers.snowflake(payload.channel_id),
topic: payload.topic,
};
}
export interface DiscordenoStageInstance {
/** The id of this Stage instance */
id: bigint;
/** The guild id of the associated Stage channel */
guildId: bigint;
/** The id of the associated Stage channel */
channelId: bigint;
/** The topic of the Stage instance (1-120 characters) */
topic: string;
}

View File

@@ -3,6 +3,7 @@ import { Integration } from "../integrations/integration.ts";
import { User } from "../users/user.ts";
import { Webhook } from "../webhooks/webhook.ts";
import { AuditLogEntry } from "./auditLogEntry.ts";
import { ScheduledEvent } from "../guilds/scheduledEvents.ts";
/** https://discord.com/developers/docs/resources/audit-log#audit-log-object */
export interface AuditLog {
@@ -16,4 +17,6 @@ export interface AuditLog {
integrations: Partial<Integration>[];
/** List of threads found in the audit log. */
threads: Channel[];
/** List of guild scheduled events found in the audit log */
scheduledEvents: ScheduledEvent[];
}

View File

@@ -10,4 +10,6 @@ export interface ModifyGuildMember {
deaf?: boolean | null;
/** Id of channel to move user to (if they are connected to voice). Requires the `MOVE_MEMBERS` permission */
channelId?: bigint | null;
/** when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future), set to null to remove timeout. Requires the `MODERATE_MEMBERS` permission */
communicationDisabledUntil?: number;
}

View File

@@ -6,6 +6,8 @@ export interface GuildMember {
user?: User;
/** This users guild nickname */
nick?: string | null;
/** The members custom avatar for this server. */
avatar?: string;
/** Array of role object ids */
roles: string[];
/** When the user joined the guild */
@@ -18,10 +20,10 @@ export interface GuildMember {
mute?: boolean;
/** Whether the user has not yet passed the guild's Membership Screening requirements */
pending?: boolean;
/** The members custom avatar for this server. */
avatar?: string;
/** The permissions this member has in the guild. Only present on interaction events. */
permissions?: string;
/** when the user's [timeout](https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ) will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */
communicationDisabledUntil?: number;
}
// We use these types much since user always exists unless its a `CREATE_MESSAGE` or `MESSAGE_UPDATE` event

View File

@@ -10,14 +10,18 @@ export interface GuildMemberUpdate {
user: User;
/** Nickname of the user in the guild */
nick?: string | null;
/** the member's [guild avatar hash](https://discord.com/developers/docs/reference#image-formatting) */
avatar: string;
/** When the user joined the guild */
joinedAt: string;
/** When the user starting boosting the guild */
premiumSince?: string | null;
/** Whether the user has not yet passed the guild's Membership Screening requirements */
pending?: boolean;
/** whether the user is deafened in voice channels */
deaf?: boolean;
/** whether the user is muted in voice channels */
mute?: boolean;
/** Whether the user has not yet passed the guild's Membership Screening requirements */
pending?: boolean;
/** when the user's [timeout](https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ) will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */
communicationDisabledUntil?: number;
}

View File

@@ -73,7 +73,7 @@ export interface Message {
activity?: MessageActivity;
/** Sent with Rich Presence-related chat embeds */
application?: Partial<Application>;
/** If the message is a response to an Interaction, this is the id of the interaction's application */
/** if the message is an Interaction or application-owned webhook, this is the id of the application */
applicationId?: string;
/** Data showing the source of a crossposted channel follow add, pin or reply message */
messageReference?: Omit<MessageReference, "failIfNotExists">;

View File

@@ -1,26 +0,0 @@
import { MessageStickerFormatTypes } from "./messageStickerFormatTypes.ts";
import type { User } from "../users/user.ts";
/** https://discord.com/developers/docs/resources/channel#message-object-message-sticker-structure */
export interface MessageSticker {
/** Id of the sticker */
id: string;
/** Id of the pack the sticker is from */
packId?: string;
/** Name of the sticker */
name: string;
/** Description of the sticker */
description: string;
/** a unicode emoji representing the sticker's expression */
tags: string;
/** Type of sticker format */
formatType: MessageStickerFormatTypes;
/** Whether or not the sticker is available */
available?: boolean;
/** Id of the guild that owns this sticker */
guildId?: string;
/** The user that uploaded the sticker */
user?: User;
/** A sticker's sort order within a pack */
sortValue?: number;
}

View File

@@ -1,6 +0,0 @@
/** https://discord.com/developers/docs/resources/channel#message-object-message-sticker-format-types */
export enum MessageStickerFormatTypes {
Png = 1,
Apng,
Lottie,
}

View File

@@ -1,10 +0,0 @@
import { MessageStickerFormatTypes } from "./messageStickerFormatTypes.ts";
export interface MessageStickerItem {
/** Id of the sticker */
id: string;
/** Name of the sticker */
name: string;
/** Type of sticker format */
formatType: MessageStickerFormatTypes;
}

View File

@@ -1,83 +1,85 @@
/** https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags */
export enum BitwisePermissionFlags {
/** Allows creation of instant invites */
CREATE_INSTANT_INVITE = 0x00000001,
CREATE_INSTANT_INVITE = 0x0000000000000001,
/** Allows kicking members */
KICK_MEMBERS = 0x00000002,
KICK_MEMBERS = 0x0000000000000002,
/** Allows banning members */
BAN_MEMBERS = 0x00000004,
BAN_MEMBERS = 0x0000000000000004,
/** Allows all permissions and bypasses channel permission overwrites */
ADMINISTRATOR = 0x00000008,
ADMINISTRATOR = 0x0000000000000008,
/** Allows management and editing of channels */
MANAGE_CHANNELS = 0x00000010,
MANAGE_CHANNELS = 0x0000000000000010,
/** Allows management and editing of the guild */
MANAGE_GUILD = 0x00000020,
MANAGE_GUILD = 0x0000000000000020,
/** Allows for the addition of reactions to messages */
ADD_REACTIONS = 0x00000040,
ADD_REACTIONS = 0x0000000000000040,
/** Allows for viewing of audit logs */
VIEW_AUDIT_LOG = 0x00000080,
VIEW_AUDIT_LOG = 0x0000000000000080,
/** Allows for using priority speaker in a voice channel */
PRIORITY_SPEAKER = 0x00000100,
PRIORITY_SPEAKER = 0x0000000000000100,
/** Allows the user to go live */
STREAM = 0x00000200,
STREAM = 0x0000000000000200,
/** Allows guild members to view a channel, which includes reading messages in text channels */
VIEW_CHANNEL = 0x00000400,
VIEW_CHANNEL = 0x0000000000000400,
/** Allows for sending messages in a channel. (does not allow sending messages in threads) */
SEND_MESSAGES = 0x00000800,
SEND_MESSAGES = 0x0000000000000800,
/** Allows for sending of /tts messages */
SEND_TTS_MESSAGES = 0x00001000,
SEND_TTS_MESSAGES = 0x0000000000001000,
/** Allows for deletion of other users messages */
MANAGE_MESSAGES = 0x00002000,
MANAGE_MESSAGES = 0x0000000000002000,
/** Links sent by users with this permission will be auto-embedded */
EMBED_LINKS = 0x00004000,
EMBED_LINKS = 0x0000000000004000,
/** Allows for uploading images and files */
ATTACH_FILES = 0x00008000,
ATTACH_FILES = 0x0000000000008000,
/** Allows for reading of message history */
READ_MESSAGE_HISTORY = 0x00010000,
READ_MESSAGE_HISTORY = 0x0000000000010000,
/** Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel */
MENTION_EVERYONE = 0x00020000,
MENTION_EVERYONE = 0x0000000000020000,
/** Allows the usage of custom emojis from other servers */
USE_EXTERNAL_EMOJIS = 0x00040000,
USE_EXTERNAL_EMOJIS = 0x0000000000040000,
/** Allows for viewing guild insights */
VIEW_GUILD_INSIGHTS = 0x00080000,
VIEW_GUILD_INSIGHTS = 0x0000000000080000,
/** Allows for joining of a voice channel */
CONNECT = 0x00100000,
CONNECT = 0x0000000000100000,
/** Allows for speaking in a voice channel */
SPEAK = 0x00200000,
SPEAK = 0x0000000000200000,
/** Allows for muting members in a voice channel */
MUTE_MEMBERS = 0x00400000,
MUTE_MEMBERS = 0x0000000000400000,
/** Allows for deafening of members in a voice channel */
DEAFEN_MEMBERS = 0x00800000,
DEAFEN_MEMBERS = 0x0000000000800000,
/** Allows for moving of members between voice channels */
MOVE_MEMBERS = 0x01000000,
MOVE_MEMBERS = 0x0000000001000000,
/** Allows for using voice-activity-detection in a voice channel */
USE_VAD = 0x02000000,
USE_VAD = 0x0000000002000000,
/** Allows for modification of own nickname */
CHANGE_NICKNAME = 0x04000000,
CHANGE_NICKNAME = 0x0000000004000000,
/** Allows for modification of other users nicknames */
MANAGE_NICKNAMES = 0x08000000,
MANAGE_NICKNAMES = 0x0000000008000000,
/** Allows management and editing of roles */
MANAGE_ROLES = 0x10000000,
MANAGE_ROLES = 0x0000000010000000,
/** Allows management and editing of webhooks */
MANAGE_WEBHOOKS = 0x20000000,
MANAGE_WEBHOOKS = 0x0000000020000000,
/** Allows management and editing of emojis */
MANAGE_EMOJIS = 0x40000000,
MANAGE_EMOJIS = 0x0000000040000000,
/** Allows members to use application commands in text channels */
USE_SLASH_COMMANDS = 0x80000000,
USE_SLASH_COMMANDS = 0x0000000080000000,
/** Allows for requesting to speak in stage channels. */
REQUEST_TO_SPEAK = 0x0100000000,
REQUEST_TO_SPEAK = 0x0000000100000000,
/** Allows for creating, editing, and deleting scheduled events */
MANAGE_EVENTS = 0x0200000000,
MANAGE_EVENTS = 0x0000000200000000,
/** Allows for deleting and archiving threads, and viewing all private threads */
MANAGE_THREADS = 0x0400000000,
MANAGE_THREADS = 0x0000000400000000,
/** Allows for creating threads */
CREATE_PUBLIC_THREADS = 0x0800000000,
CREATE_PUBLIC_THREADS = 0x0000000800000000,
/** Allows for creating private threads */
CREATE_PRIVATE_THREADS = 0x1000000000,
CREATE_PRIVATE_THREADS = 0x0000001000000000,
/** Allows the usage of custom stickers from other servers */
USE_EXTERNAL_STICKERS = 0x2000000000,
USE_EXTERNAL_STICKERS = 0x0000002000000000,
/** Allows for sending messages in threads */
SEND_MESSAGES_IN_THREADS = 0x4000000000,
SEND_MESSAGES_IN_THREADS = 0x0000004000000000,
/** Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. */
START_EMBEDDED_ACTIVITIES = 0x8000000000,
START_EMBEDDED_ACTIVITIES = 0x0000008000000000,
/** Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels */
MODERATE_MEMBERS = 0x0000010000000000,
}

View File

@@ -0,0 +1,29 @@
import type { StickerFormatTypes } from "./stickerFormatTypes.ts";
import type { StickerTypes } from "./stickerTypes.ts";
import type { User } from "../users/user.ts";
/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure */
export interface Sticker {
/** [Id of the sticker](https://discord.com/developers/docs/reference#image-formatting) */
id: string;
/** Id of the pack the sticker is from */
packId?: string;
/** Name of the sticker */
name: string;
/** Description of the sticker */
description: string;
/** a unicode emoji representing the sticker's expression */
tags: string;
/** [type of sticker](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types) */
type: StickerTypes;
/** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */
formatType: StickerFormatTypes;
/** Whether or not the sticker is available */
available?: boolean;
/** Id of the guild that owns this sticker */
guildId?: string;
/** The user that uploaded the sticker */
user?: User;
/** A sticker's sort order within a pack */
sortValue?: number;
}

View File

@@ -0,0 +1,6 @@
/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types */
export enum StickerFormatTypes {
Png = 1,
Apng,
Lottie,
}

View File

@@ -0,0 +1,11 @@
import { StickerFormatTypes } from "./stickerFormatTypes.ts";
/** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */
export interface StickerItem {
/** Id of the sticker */
id: string;
/** Name of the sticker */
name: string;
/** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */
formatType: StickerFormatTypes;
}

View File

@@ -0,0 +1,19 @@
import { Sticker } from "./sticker.ts";
/** https://discord.com/developers/docs/resources/sticker#sticker-pack-object-sticker-pack-structure */
export interface StickerPack {
/** id of the sticker pack */
id: string;
/** the stickers in the pack */
stickers: Sticker[];
/** name of the sticker pack */
name: string;
/** id of the pack's SKU */
sku_id: string;
/** id of a sticker in the pack which is shown as the pack's icon */
cover_sticker_id?: string;
/** description of the sticker pack */
description: string;
/** id of the sticker pack's [banner image](https://discord.com/developers/docs/reference#image-formatting) */
banner_asset_id?: string;
}

View File

@@ -0,0 +1,7 @@
/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types */
export enum StickerTypes {
/** an official sticker in a pack, part of Nitro or in a removed purchasable pack */
Standard = 1,
/** a sticker uploaded to a Boosted guild for the guild's members */
Guild,
}

View File

@@ -13,9 +13,14 @@ export interface EditWebhookMessage {
/** The contents of the file being sent/edited */
file?: FileContent | FileContent[] | null;
/** Allowed mentions for the message */
allowedMentions?: AllowedMentions | null;
allowedMentions?: Omit<AllowedMentions, "users" | "roles"> & {
/** Array of role_ids to mention (Max size of 100) */
roles?: bigint[];
/** Array of user_ids to mention (Max size of 100) */
users?: bigint[];
};
/** Attached files to keep */
attachments?: Attachment | null;
attachments?: (Omit<Attachment, "id"> & { id: bigint })[] | null;
/** The components you would like to have sent in this message */
components?: MessageComponents;
}

View File

@@ -2,6 +2,7 @@ import { Embed } from "../embeds/embed.ts";
import { AllowedMentions } from "../messages/allowedMentions.ts";
import { FileContent } from "../discordeno/fileContent.ts";
import { SnakeCasedPropertiesDeep } from "../util.ts";
import { MessageComponents } from "../messages/components/messageComponents.ts";
/** https://discord.com/developers/docs/resources/webhook#execute-webhook */
export interface ExecuteWebhook {
@@ -28,6 +29,8 @@ export interface ExecuteWebhook {
/** Array of user_ids to mention (Max size of 100) */
users?: bigint[];
};
/** the components to include with the message */
components: MessageComponents;
}
export type DiscordExecuteWebhook = SnakeCasedPropertiesDeep<Omit<ExecuteWebhook, "wait">>;

View File

@@ -0,0 +1,35 @@
import { assertExists, assertEquals } from "../../deps.ts";
import { bot } from "../../mod.ts";
import { CACHED_COMMUNITY_GUILD_ID } from "../../constants.ts";
Deno.test({
name: "[slash] Create a guild slash command",
fn: async (t) => {
let commands = new Map();
await t.step({
name: "[slash] Gets a bot's slash commands in a guild",
fn: async (t) => {
commands = await bot.helpers.getApplicationCommands(CACHED_COMMUNITY_GUILD_ID);
},
});
if (commands.has("test")) {
await t.step({
name: "[slash] Delete a guild slash command",
fn: async (t) => {
await bot.helpers.deleteApplicationCommand(commands.get("test").id, CACHED_COMMUNITY_GUILD_ID);
commands.delete("test");
assertEquals(commands.has("test"), false);
},
});
}
await bot.helpers.createApplicationCommand(
{
name: "test",
description: "Test slash command from the ddeno unit tests",
},
CACHED_COMMUNITY_GUILD_ID
);
},
});