Merge remote-tracking branch 'upstream/main' into main

This commit is contained in:
ITOH
2021-04-08 15:55:23 +02:00
78 changed files with 604 additions and 321 deletions

View File

@@ -1,29 +1,32 @@
// deno-lint-ignore-file require-await no-explicit-any prefer-const
import { Channel, Guild, Member, Message } from "./structures/mod.ts";
import { ChannelStruct } from "./structures/channel.ts";
import { GuildStruct } from "./structures/guild.ts";
import { MemberStruct } from "./structures/member.ts";
import { MessageStruct } from "./structures/message.ts";
import { Emoji } from "./types/emojis/emoji.ts";
import { PresenceUpdate } from "./types/misc/presence_update.ts";
import { Collection } from "./util/collection.ts";
export const cache = {
isReady: false,
/** All of the guild objects the bot has access to, mapped by their Ids */
guilds: new Collection<string, Guild>(),
guilds: new Collection<string, GuildStruct>(),
/** All of the channel objects the bot has access to, mapped by their Ids */
channels: new Collection<string, Channel>(),
channels: new Collection<string, ChannelStruct>(),
/** All of the message objects the bot has cached since the bot acquired `READY` state, mapped by their Ids */
messages: new Collection<string, Message>(),
messages: new Collection<string, MessageStruct>(),
/** All of the member objects that have been cached since the bot acquired `READY` state, mapped by their Ids */
members: new Collection<string, Member>(),
members: new Collection<string, MemberStruct>(),
/** All of the unavailable guilds, mapped by their Ids (id, timestamp) */
unavailableGuilds: new Collection<string, number>(),
/** All of the presence update objects received in PRESENCE_UPDATE gateway event, mapped by their user Id */
presences: new Collection<string, Presence>(),
presences: new Collection<string, PresenceUpdate>(),
fetchAllMembersProcessingRequests: new Collection<
string,
(
value:
| Collection<string, Member>
| PromiseLike<Collection<string, Member>>,
| Collection<string, MemberStruct>
| PromiseLike<Collection<string, MemberStruct>>,
) => void
>(),
executedSlashCommands: new Collection<string, string>(),
@@ -78,28 +81,28 @@ export type TableName =
function set(
table: "guilds",
key: string,
value: Guild,
): Promise<Collection<string, Guild>>;
value: GuildStruct,
): Promise<Collection<string, GuildStruct>>;
function set(
table: "channels",
key: string,
value: Channel,
): Promise<Collection<string, Channel>>;
value: ChannelStruct,
): Promise<Collection<string, ChannelStruct>>;
function set(
table: "messages",
key: string,
value: Message,
): Promise<Collection<string, Message>>;
value: MessageStruct,
): Promise<Collection<string, MessageStruct>>;
function set(
table: "members",
key: string,
value: Member,
): Promise<Collection<string, Member>>;
value: MemberStruct,
): Promise<Collection<string, MemberStruct>>;
function set(
table: "presences",
key: string,
value: PresenceUpdatePayload,
): Promise<Collection<string, PresenceUpdatePayload>>;
value: PresenceUpdate,
): Promise<Collection<string, PresenceUpdate>>;
function set(
table: "unavailableGuilds",
key: string,
@@ -109,41 +112,60 @@ async function set(table: TableName, key: string, value: any) {
return cache[table].set(key, value);
}
function get(table: "guilds", key: string): Promise<Guild | undefined>;
function get(table: "channels", key: string): Promise<Channel | undefined>;
function get(table: "messages", key: string): Promise<Message | undefined>;
function get(table: "members", key: string): Promise<Member | undefined>;
function get(table: "guilds", key: string): Promise<GuildStruct | undefined>;
function get(
table: "channels",
key: string,
): Promise<ChannelStruct | undefined>;
function get(table: "messages", key: string): Promise<MemberStruct | undefined>;
function get(table: "members", key: string): Promise<MemberStruct | undefined>;
function get(
table: "presences",
key: string,
): Promise<PresenceUpdatePayload | undefined>;
): Promise<PresenceUpdate | undefined>;
function get(
table: "unavailableGuilds",
key: string,
): Promise<Guild | undefined>;
): Promise<number | undefined>;
async function get(table: TableName, key: string) {
return cache[table].get(key);
}
function forEach(
table: "guilds",
callback: (value: Guild, key: string, map: Map<string, Guild>) => unknown,
callback: (
value: GuildStruct,
key: string,
map: Map<string, GuildStruct>,
) => unknown,
): void;
function forEach(
table: "unavailableGuilds",
callback: (value: Guild, key: string, map: Map<string, Guild>) => unknown,
callback: (value: number, key: string, map: Map<string, number>) => unknown,
): void;
function forEach(
table: "channels",
callback: (value: Channel, key: string, map: Map<string, Channel>) => unknown,
callback: (
value: ChannelStruct,
key: string,
map: Map<string, ChannelStruct>,
) => unknown,
): void;
function forEach(
table: "messages",
callback: (value: Message, key: string, map: Map<string, Message>) => unknown,
callback: (
value: MemberStruct,
key: string,
map: Map<string, MemberStruct>,
) => unknown,
): void;
function forEach(
table: "members",
callback: (value: Member, key: string, map: Map<string, Member>) => unknown,
callback: (
value: MemberStruct,
key: string,
map: Map<string, MemberStruct>,
) => unknown,
): void;
function forEach(
table: TableName,
@@ -154,24 +176,24 @@ function forEach(
function filter(
table: "guilds",
callback: (value: Guild, key: string) => boolean,
): Promise<Collection<string, Guild>>;
callback: (value: GuildStruct, key: string) => boolean,
): Promise<Collection<string, GuildStruct>>;
function filter(
table: "unavailableGuilds",
callback: (value: Guild, key: string) => boolean,
): Promise<Collection<string, Guild>>;
callback: (value: number, key: string) => boolean,
): Promise<Collection<string, number>>;
function filter(
table: "channels",
callback: (value: Channel, key: string) => boolean,
): Promise<Collection<string, Channel>>;
callback: (value: ChannelStruct, key: string) => boolean,
): Promise<Collection<string, ChannelStruct>>;
function filter(
table: "messages",
callback: (value: Message, key: string) => boolean,
): Promise<Collection<string, Message>>;
callback: (value: MessageStruct, key: string) => boolean,
): Promise<Collection<string, MessageStruct>>;
function filter(
table: "members",
callback: (value: Member, key: string) => boolean,
): Promise<Collection<string, Member>>;
callback: (value: MemberStruct, key: string) => boolean,
): Promise<Collection<string, MemberStruct>>;
async function filter(
table: TableName,
callback: (value: any, key: string) => boolean,

View File

@@ -1,5 +1,6 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordApplicationCommandCreateUpdateDelete } from "../../types/interactions/application_command_create_update_delete.ts";
export function handleApplicationCommandCreate(
data: DiscordGatewayPayload,

View File

@@ -1,5 +1,6 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordApplicationCommandCreateUpdateDelete } from "../../types/interactions/application_command_create_update_delete.ts";
export function handleApplicationCommandDelete(data: DiscordGatewayPayload) {
const {

View File

@@ -1,5 +1,6 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordApplicationCommandCreateUpdateDelete } from "../../types/interactions/application_command_create_update_delete.ts";
export function handleApplicationCommandUpdate(data: DiscordGatewayPayload) {
const {

View File

@@ -1,6 +1,7 @@
import { eventHandlers } from "../../bot.ts";
import { cacheHandlers } from "../../cache.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordGuildBanAddRemove } from "../../types/guilds/guild_ban_add_remove.ts";
export async function handleGuildBanAdd(data: DiscordGatewayPayload) {
const payload = data.d as DiscordGuildBanAddRemove;

View File

@@ -1,6 +1,7 @@
import { eventHandlers } from "../../bot.ts";
import { cacheHandlers } from "../../cache.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordGuildBanAddRemove } from "../../types/guilds/guild_ban_add_remove.ts";
export async function handleGuildBanRemove(data: DiscordGatewayPayload) {
const payload = data.d as DiscordGuildBanAddRemove;

View File

@@ -1,18 +1,19 @@
import { eventHandlers } from "../../bot.ts";
import { cacheHandlers } from "../../cache.ts";
import { GuildUpdateChange } from "../../types/discordeno/guild_update_change.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordGuild } from "../../types/guilds/guild.ts";
export async function handleGuildUpdate(data: DiscordGatewayPayload) {
const payload = data.d as DiscordGuild;
const cachedGuild = await cacheHandlers.get("guilds", payload.id);
if (!cachedGuild) return;
const newGuild = await cacheHandlers.get("guilds", payload.id);
if (!newGuild) return;
const keysToSkip = [
"roles",
"guild_hashes",
"guild_id",
"max_members",
"guildHashes",
"guildId",
"maxMembers",
"emojis",
];
@@ -21,7 +22,7 @@ export async function handleGuildUpdate(data: DiscordGatewayPayload) {
if (keysToSkip.includes(key)) return;
// @ts-ignore index signature
const cachedValue = cachedGuild[key];
const cachedValue = newGuild[key];
if (cachedValue !== value) {
// Guild create sends undefined and update sends false.
if (!cachedValue && !value) return;
@@ -34,12 +35,12 @@ export async function handleGuildUpdate(data: DiscordGatewayPayload) {
}
// @ts-ignore index signature
cachedGuild[key] = value;
newGuild[key] = value;
return { key, oldValue: cachedValue, value };
}
}).filter((change) => change) as GuildUpdateChange[];
await cacheHandlers.set("guilds", payload.id, cachedGuild);
await cacheHandlers.set("guilds", payload.id, newGuild);
eventHandlers.guildUpdate?.(cachedGuild, changes);
eventHandlers.guildUpdate?.(newGuild, changes);
}

View File

@@ -1,28 +1,17 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import {
DiscordIntegrationCreateUpdate,
IntegrationCreateUpdate,
} from "../../types/integration/integration_create_update.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
export function handleIntegrationCreate(
data: DiscordGatewayPayload,
) {
const {
guild_id: guildId,
enable_emoticons: enableEmoticons,
expire_behavior: expireBehavior,
expire_grace_period: expireGracePeriod,
subscriber_count: subscriberCount,
role_id: roleId,
synced_at: syncedAt,
...rest
} = data.d as IntegrationCreateUpdateEvent;
const payload = data.d as DiscordIntegrationCreateUpdate;
eventHandlers.integrationCreate?.({
...rest,
guildId,
enableEmoticons,
expireBehavior,
expireGracePeriod,
syncedAt,
subscriberCount,
roleId,
});
eventHandlers.integrationCreate?.(
snakeKeysToCamelCase(payload) as IntegrationCreateUpdate,
);
}

View File

@@ -1,16 +1,15 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import {
DiscordIntegrationDelete,
IntegrationDelete,
} from "../../types/integration/integration_delete.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
export function handleIntegrationDelete(data: DiscordGatewayPayload) {
const {
guild_id: guildId,
application_id: applicationId,
...rest
} = data.d as IntegrationDeleteEvent;
const payload = data.d as DiscordIntegrationDelete;
eventHandlers.integrationDelete?.({
...rest,
applicationId,
guildId,
});
eventHandlers.integrationDelete?.(
snakeKeysToCamelCase(payload) as IntegrationDelete,
);
}

View File

@@ -1,26 +1,15 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import {
DiscordIntegrationCreateUpdate,
IntegrationCreateUpdate,
} from "../../types/integration/integration_create_update.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
export function handleIntegrationUpdate(data: DiscordGatewayPayload) {
const {
enable_emoticons: enableEmoticons,
expire_behavior: expireBehavior,
expire_grace_period: expireGracePeriod,
role_id: roleId,
subscriber_count: subscriberCount,
synced_at: syncedAt,
guild_id: guildId,
...rest
} = data.d as IntegrationCreateUpdateEvent;
const payload = data.d as DiscordIntegrationCreateUpdate;
eventHandlers.integrationUpdate?.({
...rest,
guildId,
subscriberCount,
enableEmoticons,
expireGracePeriod,
roleId,
expireBehavior,
syncedAt,
});
eventHandlers.integrationUpdate?.(
snakeKeysToCamelCase(payload) as IntegrationCreateUpdate,
);
}

View File

@@ -1,28 +1,13 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import { DiscordInviteCreate } from "../../types/invites/invite_create.ts";
import {
DiscordInviteCreate,
InviteCreate,
} from "../../types/invites/invite_create.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
export function handleInviteCreate(payload: DiscordGatewayPayload) {
// TODO: replace with tocamelcase
const {
channel_id: channelId,
created_at: createdAt,
max_age: maxAge,
guild_id: guildId,
target_user: targetUser,
target_user_type: targetUserType,
max_uses: maxUses,
...rest
} = payload.d as DiscordInviteCreate;
export function handleInviteCreate(data: DiscordGatewayPayload) {
const payload = data.d as DiscordInviteCreate;
eventHandlers.inviteCreate?.({
...rest,
channelId,
guildId,
maxAge,
targetUser,
targetUserType,
maxUses,
createdAt,
});
eventHandlers.inviteCreate?.(snakeKeysToCamelCase(payload) as InviteCreate);
}

View File

@@ -1,17 +1,13 @@
import { eventHandlers } from "../../bot.ts";
import { DiscordInviteDelete } from "../../types/invites/invite_delete.ts";
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
import {
DiscordInviteDelete,
InviteDelete,
} from "../../types/invites/invite_delete.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
export function handleInviteDelete(payload: DiscordGatewayPayload) {
const {
channel_id: channelId,
guild_id: guildId,
...rest
} = payload.d as DiscordInviteDelete;
export function handleInviteDelete(data: DiscordGatewayPayload) {
const payload = data.d as DiscordInviteDelete;
eventHandlers.inviteDelete?.({
...rest,
channelId,
guildId,
});
eventHandlers.inviteDelete?.(snakeKeysToCamelCase(payload) as InviteDelete);
}

View File

@@ -9,7 +9,7 @@ export async function deleteChannel(
guildId: string,
channelId: string,
reason?: string,
) {
): Promise<undefined> {
await requireBotGuildPermissions(guildId, ["MANAGE_CHANNELS"]);
const guild = await cacheHandlers.get("guilds", guildId);

View File

@@ -7,7 +7,7 @@ export async function deleteChannelOverwrite(
guildId: string,
channelId: string,
overwriteId: string,
) {
): Promise<undefined> {
await requireBotGuildPermissions(guildId, ["MANAGE_ROLES"]);
const result = await rest.runMethod(

View File

@@ -1,4 +1,5 @@
import { rest } from "../../rest/rest.ts";
import { ModifyChannel } from "../../types/channels/modify_channel.ts";
import { endpoints } from "../../util/constants.ts";
import {
calculateBits,
@@ -8,7 +9,7 @@ import {
/** Update a channel's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */
export async function editChannel(
channelId: string,
options: ChannelEditOptions,
options: ModifyChannel,
reason?: string,
) {
await requireBotChannelPermissions(channelId, ["MANAGE_CHANNELS"]);
@@ -47,7 +48,7 @@ export async function editChannel(
// deno-lint-ignore camelcase
user_limit: options.userLimit,
// deno-lint-ignore camelcase
permission_overwrites: options.overwrites?.map((overwrite) => {
permission_overwrites: options.permissionOverwrites?.map((overwrite) => {
return {
...overwrite,
allow: calculateBits(overwrite.allow),

View File

@@ -12,7 +12,7 @@ export async function editChannelOverwrite(
channelId: string,
overwriteId: string,
options: Omit<Overwrite, "id">,
) {
): Promise<undefined> {
await requireBotGuildPermissions(guildId, ["MANAGE_ROLES"]);
const result = await rest.runMethod(

View File

@@ -10,7 +10,7 @@ import { botHasChannelPermissions } from "../../util/permissions.ts";
* However, if a bot is responding to a command and expects the computation to take a few seconds,
* this endpoint may be called to let the user know that the bot is processing their message.
*/
export async function startTyping(channelId: string) {
export async function startTyping(channelId: string): Promise<undefined> {
const channel = await cacheHandlers.get("channels", channelId);
// If the channel is cached, we can do extra checks/safety
if (channel) {

View File

@@ -6,7 +6,7 @@ import { endpoints } from "../../util/constants.ts";
export async function swapChannels(
guildId: string,
channelPositions: ModifyGuildChannelPositions[],
) {
): Promise<undefined> {
if (channelPositions.length < 2) {
throw "You must provide at least two channels to be swapped.";
}

View File

@@ -1,7 +1,11 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { CreateGlobalApplicationCommand } from "../../types/interactions/create_global_application_command.ts";
import { endpoints } from "../../util/constants.ts";
import { validateSlashCommands } from "../../util/utils.ts";
import {
camelKeysToSnakeCase,
validateSlashCommands,
} from "../../util/utils.ts";
/**
* There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means:
@@ -14,17 +18,18 @@ import { validateSlashCommands } from "../../util/utils.ts";
* Global commands are cached for **1 hour**. That means that new global commands will fan out slowly across all guilds, and will be guaranteed to be updated in an hour.
* Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use.
*/
export async function createSlashCommand(options: CreateSlashCommandOptions) {
export async function createSlashCommand(
options: CreateGlobalApplicationCommand,
guildId: string,
) {
validateSlashCommands([options], true);
const result = await rest.runMethod(
"post",
options.guildId
? endpoints.COMMANDS_GUILD(applicationId, options.guildId)
guildId
? endpoints.COMMANDS_GUILD(applicationId, guildId)
: endpoints.COMMANDS(applicationId),
{
...options,
},
camelKeysToSnakeCase(options),
);
return result;

View File

@@ -3,12 +3,16 @@ import { rest } from "../../rest/rest.ts";
import { endpoints } from "../../util/constants.ts";
/** Deletes a slash command. */
export function deleteSlashCommand(id: string, guildId?: string) {
if (!guildId) {
return rest.runMethod("delete", endpoints.COMMANDS_ID(applicationId, id));
}
return rest.runMethod(
export async function deleteSlashCommand(
id: string,
guildId?: string,
): Promise<undefined> {
const result = await rest.runMethod(
"delete",
endpoints.COMMANDS_GUILD_ID(applicationId, guildId, id),
guildId
? endpoints.COMMANDS_GUILD_ID(applicationId, guildId, id)
: endpoints.COMMANDS_ID(applicationId, id),
);
return result;
}

View File

@@ -3,7 +3,10 @@ import { rest } from "../../rest/rest.ts";
import { endpoints } from "../../util/constants.ts";
/** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */
export async function deleteSlashResponse(token: string, messageId?: string) {
export async function deleteSlashResponse(
token: string,
messageId?: string,
): Promise<undefined> {
const result = await rest.runMethod(
"delete",
messageId

View File

@@ -1,13 +1,16 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { structures } from "../../structures/mod.ts";
import { DiscordenoEditWebhookMessage } from "../../types/discordeno/edit_webhook_message.ts";
import { DiscordAllowedMentionsTypes } from "../../types/messages/allowed_mentions_types.ts";
import { DiscordMessage } from "../../types/messages/message.ts";
import { Errors } from "../../types/misc/errors.ts";
import { endpoints } from "../../util/constants.ts";
/** To edit your response to a slash command. If a messageId is not provided it will default to editing the original response. */
export async function editSlashResponse(
token: string,
options: EditSlashResponseOptions,
options: DiscordenoEditWebhookMessage,
) {
if (options.content && options.content.length > 2000) {
throw Error(Errors.MESSAGE_MAX_LENGTH);
@@ -17,31 +20,39 @@ export async function editSlashResponse(
options.embeds.splice(10);
}
if (options.allowed_mentions) {
if (options.allowed_mentions.users?.length) {
if (options.allowed_mentions.parse.includes("users")) {
options.allowed_mentions.parse = options.allowed_mentions.parse.filter(
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {
if (
options.allowedMentions.parse.includes(
DiscordAllowedMentionsTypes.UserMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter(
(p) => p !== "users",
);
}
if (options.allowed_mentions.users.length > 100) {
options.allowed_mentions.users = options.allowed_mentions.users.slice(
if (options.allowedMentions.users.length > 100) {
options.allowedMentions.users = options.allowedMentions.users.slice(
0,
100,
);
}
}
if (options.allowed_mentions.roles?.length) {
if (options.allowed_mentions.parse.includes("roles")) {
options.allowed_mentions.parse = options.allowed_mentions.parse.filter(
if (options.allowedMentions.roles?.length) {
if (
options.allowedMentions.parse.includes(
DiscordAllowedMentionsTypes.RoleMentions,
)
) {
options.allowedMentions.parse = options.allowedMentions.parse.filter(
(p) => p !== "roles",
);
}
if (options.allowed_mentions.roles.length > 100) {
options.allowed_mentions.roles = options.allowed_mentions.roles.slice(
if (options.allowedMentions.roles.length > 100) {
options.allowedMentions.roles = options.allowedMentions.roles.slice(
0,
100,
);
@@ -61,7 +72,7 @@ export async function editSlashResponse(
if (!options.messageId) return result;
const message = await structures.createMessageStruct(
result as MessageCreateOptions,
result as DiscordMessage,
);
return message;
}

View File

@@ -1,5 +1,6 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { ApplicationCommand } from "../../types/interactions/application_command.ts";
import { endpoints } from "../../util/constants.ts";
/** Fetchs the global command for the given Id. If a guildId is provided, the guild command will be fetched. */
@@ -11,5 +12,5 @@ export async function getSlashCommand(commandId: string, guildId?: string) {
: endpoints.COMMANDS_ID(applicationId, commandId),
);
return result as SlashCommand;
return result as ApplicationCommand;
}

View File

@@ -1,5 +1,6 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { ApplicationCommand } from "../../types/interactions/application_command.ts";
import { Collection } from "../../util/collection.ts";
import { endpoints } from "../../util/constants.ts";
@@ -10,7 +11,7 @@ export async function getSlashCommands(guildId?: string) {
guildId
? endpoints.COMMANDS_GUILD(applicationId, guildId)
: endpoints.COMMANDS(applicationId),
)) as SlashCommand[];
)) as ApplicationCommand[];
return new Collection(result.map((command) => [command.name, command]));
}

View File

@@ -1,6 +1,7 @@
import { applicationId } from "../../bot.ts";
import { cache } from "../../cache.ts";
import { rest } from "../../rest/rest.ts";
import { DiscordenoInteractionResponse } from "../../types/discordeno/interaction_response.ts";
import { endpoints } from "../../util/constants.ts";
/**
@@ -12,7 +13,7 @@ import { endpoints } from "../../util/constants.ts";
export async function sendInteractionResponse(
id: string,
token: string,
options: SlashCommandResponseOptions,
options: DiscordenoInteractionResponse,
) {
// If its already been executed, we need to send a followup response
if (cache.executedSlashCommands.has(token)) {
@@ -30,12 +31,12 @@ export async function sendInteractionResponse(
// If the user wants this as a private message mark it ephemeral
if (options.private) {
options.data.flags = 64;
options.data = { ...options.data, flags: 64 };
}
// If no mentions are provided, force disable mentions
if (!options.data.allowed_mentions) {
options.data.allowed_mentions = { parse: [] };
if (!options.data?.allowedMentions) {
options.data = { ...options.data, allowedMentions: { parse: [] } };
}
const result = await rest.runMethod(

View File

@@ -1,5 +1,6 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { EditGlobalApplicationCommand } from "../../types/interactions/edit_global_application_command.ts";
import { endpoints } from "../../util/constants.ts";
import { validateSlashCommands } from "../../util/utils.ts";
@@ -8,7 +9,7 @@ import { validateSlashCommands } from "../../util/utils.ts";
*/
export async function upsertSlashCommand(
commandId: string,
options: UpsertSlashCommandOptions,
options: EditGlobalApplicationCommand,
guildId?: string,
) {
validateSlashCommands([options]);

View File

@@ -1,5 +1,6 @@
import { applicationId } from "../../bot.ts";
import { rest } from "../../rest/rest.ts";
import { EditGlobalApplicationCommand } from "../../types/interactions/edit_global_application_command.ts";
import { endpoints } from "../../util/constants.ts";
import { validateSlashCommands } from "../../util/utils.ts";
@@ -9,7 +10,7 @@ import { validateSlashCommands } from "../../util/utils.ts";
* **NOTE:** Any slash commands that are not specified in this function will be **deleted**. If you don't provide the commandId and rename your command, the command gets a new Id.
*/
export async function upsertSlashCommands(
options: UpsertSlashCommandsOptions[],
options: EditGlobalApplicationCommand[],
guildId?: string,
) {
validateSlashCommands(options);

View File

@@ -1,14 +1,16 @@
import { rest } from "../../rest/rest.ts";
import { CreateGuildEmoji } from "../../types/emojis/create_guild_emoji.ts";
import { Emoji } from "../../types/emojis/emoji.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { urlToBase64 } from "../../util/utils.ts";
import { snakeKeysToCamelCase, urlToBase64 } from "../../util/utils.ts";
/** Create an emoji in the server. Emojis and animated emojis have a maximum file size of 256kb. Attempting to upload an emoji larger than this limit will fail and return 400 Bad Request and an error message, but not a JSON status code. If a URL is provided to the image parameter, Discordeno will automatically convert it to a base64 string internally. */
export async function createEmoji(
guildId: string,
name: string,
image: string,
options: CreateEmojisOptions,
options: CreateGuildEmoji,
) {
await requireBotGuildPermissions(guildId, ["MANAGE_EMOJIS"]);
@@ -22,5 +24,5 @@ export async function createEmoji(
image,
});
return result;
return snakeKeysToCamelCase(result) as Emoji;
}

View File

@@ -7,7 +7,7 @@ export async function deleteEmoji(
guildId: string,
id: string,
reason?: string,
) {
): Promise<undefined> {
await requireBotGuildPermissions(guildId, ["MANAGE_EMOJIS"]);
const result = await rest.runMethod(

View File

@@ -1,4 +1,5 @@
import { rest } from "../../rest/rest.ts";
import { ModifyGuildEmoji } from "../../types/emojis/modify_guild_emoji.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
@@ -6,7 +7,7 @@ import { requireBotGuildPermissions } from "../../util/permissions.ts";
export async function editEmoji(
guildId: string,
id: string,
options: EditEmojisOptions,
options: ModifyGuildEmoji,
) {
await requireBotGuildPermissions(guildId, ["MANAGE_EMOJIS"]);

View File

@@ -1,5 +1,6 @@
import { cacheHandlers } from "../../cache.ts";
import { rest } from "../../rest/rest.ts";
import { Emoji } from "../../types/emojis/emoji.ts";
import { Errors } from "../../types/misc/errors.ts";
import { endpoints } from "../../util/constants.ts";
@@ -21,7 +22,7 @@ export async function getEmoji(
if (addToCache) {
const guild = await cacheHandlers.get("guilds", guildId);
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
guild.emojis.set(result.id ?? result.name, result);
guild.emojis.set(emojiId, result);
cacheHandlers.set(
"guilds",
guildId,

View File

@@ -1,5 +1,6 @@
import { cacheHandlers } from "../../cache.ts";
import { rest } from "../../rest/rest.ts";
import { Emoji } from "../../types/emojis/emoji.ts";
import { Errors } from "../../types/misc/errors.ts";
import { endpoints } from "../../util/constants.ts";
@@ -16,7 +17,7 @@ export async function getEmojis(guildId: string, addToCache = true) {
const guild = await cacheHandlers.get("guilds", guildId);
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
result.forEach((emoji) => guild.emojis.set(emoji.id ?? emoji.name, emoji));
result.forEach((emoji) => guild.emojis.set(emoji.id!, emoji));
cacheHandlers.set("guilds", guildId, guild);
}

View File

@@ -1,14 +1,16 @@
import { rest } from "../../rest/rest.ts";
import { structures } from "../../structures/mod.ts";
import { CreateGuild } from "../../types/guilds/create_guild.ts";
import { DiscordGuild } from "../../types/guilds/guild.ts";
import { endpoints } from "../../util/constants.ts";
/** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */
export async function createGuild(options: CreateServerOptions) {
export async function createGuild(options: CreateGuild) {
const guild = (await rest.runMethod(
"post",
endpoints.GUILDS,
options,
)) as CreateGuildPayload;
)) as DiscordGuild;
return structures.createGuildStruct(guild, 0);
}

View File

@@ -3,7 +3,7 @@ import { endpoints } from "../../util/constants.ts";
/** Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event.
*/
export async function deleteServer(guildId: string) {
export async function deleteServer(guildId: string): Promise<undefined> {
const result = await rest.runMethod("delete", endpoints.GUILDS_BASE(guildId));
return result;

View File

@@ -1,10 +1,13 @@
import { rest } from "../../rest/rest.ts";
import { structures } from "../../structures/mod.ts";
import { DiscordGuild } from "../../types/guilds/guild.ts";
import { ModifyGuild } from "../../types/guilds/modify_guild.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { urlToBase64 } from "../../util/utils.ts";
/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */
export async function editGuild(guildId: string, options: GuildEditOptions) {
export async function editGuild(guildId: string, options: ModifyGuild) {
await requireBotGuildPermissions(guildId, ["MANAGE_GUILD"]);
if (options.icon && !options.icon.startsWith("data:image/")) {
@@ -23,7 +26,7 @@ export async function editGuild(guildId: string, options: GuildEditOptions) {
"patch",
endpoints.GUILDS_BASE(guildId),
options,
);
) as DiscordGuild;
return result;
return structures.createGuildStruct(result, -1);
}

View File

@@ -1,6 +1,8 @@
import { rest } from "../../rest/rest.ts";
import { GuildWidget } from "../../types/guilds/guild_widget.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Modify a guild widget object for the guild. Requires the MANAGE_GUILD permission. */
export async function editWidget(
@@ -19,5 +21,5 @@ export async function editWidget(
},
);
return result;
return snakeKeysToCamelCase(result) as GuildWidget;
}

View File

@@ -1,27 +1,30 @@
import { rest } from "../../rest/rest.ts";
import { AuditLog } from "../../types/audit_log/audit_log.ts";
import { GetGuildAuditLog } from "../../types/audit_log/get_guild_audit_log.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import {
camelKeysToSnakeCase,
snakeKeysToCamelCase,
} from "../../util/utils.ts";
/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */
export async function getAuditLogs(
guildId: string,
options: GetAuditLogsOptions,
options: GetGuildAuditLog,
) {
await requireBotGuildPermissions(guildId, ["VIEW_AUDIT_LOG"]);
const result = await rest.runMethod(
"get",
endpoints.GUILD_AUDIT_LOGS(guildId),
{
camelKeysToSnakeCase({
...options,
action_type: options.action_type
? AuditLogs[options.action_type]
: undefined,
limit: options.limit && options.limit >= 1 && options.limit <= 100
? options.limit
: 50,
},
}),
);
return result;
return snakeKeysToCamelCase(result) as AuditLog;
}

View File

@@ -1,9 +1,10 @@
import { rest } from "../../rest/rest.ts";
import { VoiceRegion } from "../../types/voice/voice_region.ts";
import { endpoints } from "../../util/constants.ts";
/** Returns an array of voice regions that can be used when creating servers. */
export async function getAvailableVoiceRegions() {
const result = await rest.runMethod("get", endpoints.VOICE_REGIONS);
return result;
return result as VoiceRegion;
}

View File

@@ -1,6 +1,8 @@
import { rest } from "../../rest/rest.ts";
import { Ban } from "../../types/guilds/ban.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */
export async function getBan(guildId: string, memberId: string) {
@@ -11,5 +13,5 @@ export async function getBan(guildId: string, memberId: string) {
endpoints.GUILD_BAN(guildId, memberId),
);
return result as BannedUser;
return snakeKeysToCamelCase(result) as Ban;
}

View File

@@ -1,7 +1,9 @@
import { rest } from "../../rest/rest.ts";
import { Ban, DiscordBan } from "../../types/guilds/ban.ts";
import { Collection } from "../../util/collection.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */
export async function getBans(guildId: string) {
@@ -10,9 +12,9 @@ export async function getBans(guildId: string) {
const results = (await rest.runMethod(
"get",
endpoints.GUILD_BANS(guildId),
)) as BannedUser[];
)) as DiscordBan[];
return new Collection<string, BannedUser>(
results.map((res) => [res.user.id, res]),
return new Collection<string, Ban>(
results.map((res) => [res.user.id, snakeKeysToCamelCase(res) as Ban]),
);
}

View File

@@ -1,5 +1,7 @@
import { rest } from "../../rest/rest.ts";
import { Guild } from "../../types/guilds/guild.ts";
import { endpoints } from "../../util/constants.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/**
* ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()
@@ -13,5 +15,5 @@ export async function getGuild(guildId: string, counts = true) {
with_counts: counts,
});
return result as UpdateGuildPayload;
return snakeKeysToCamelCase(result) as Guild;
}

View File

@@ -1,9 +1,11 @@
import { rest } from "../../rest/rest.ts";
import { GuildPreview } from "../../types/guilds/guild_preview.ts";
import { endpoints } from "../../util/constants.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Returns the guild preview object for the given id. If the bot is not in the guild, then the guild must be Discoverable. */
export async function getGuildPreview(guildId: string) {
const result = await rest.runMethod("get", endpoints.GUILD_PREVIEW(guildId));
return result;
return snakeKeysToCamelCase(result) as GuildPreview;
}

View File

@@ -1,11 +1,15 @@
import { rest } from "../../rest/rest.ts";
import { GetGuildPruneCountQuery } from "../../types/guilds/get_guild_prune_count.ts";
import { Errors } from "../../types/misc/errors.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { camelKeysToSnakeCase } from "../../util/utils.ts";
/** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */
export async function getPruneCount(guildId: string, options?: PruneOptions) {
export async function getPruneCount(
guildId: string,
options?: GetGuildPruneCountQuery,
) {
if (options?.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS);
if (options?.days && options.days > 30) {
throw new Error(Errors.PRUNE_MAX_DAYS);
@@ -17,7 +21,7 @@ export async function getPruneCount(guildId: string, options?: PruneOptions) {
"get",
endpoints.GUILD_PRUNE(guildId),
camelKeysToSnakeCase(options ?? {}),
) as PrunePayload;
);
return result.pruned;
return result.pruned as number;
}

View File

@@ -1,12 +1,16 @@
import { rest } from "../../rest/rest.ts";
import { InviteMetadata } from "../../types/invites/invite_metadata.ts";
import { endpoints } from "../../util/constants.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Returns the code and uses of the vanity url for this server if it is enabled. Requires the MANAGE_GUILD permission. */
/** Returns the code and uses of the vanity url for this server if it is enabled else `code` will be null. Requires the `MANAGE_GUILD` permission. */
export async function getVanityURL(guildId: string) {
const result = await rest.runMethod(
"get",
endpoints.GUILD_VANITY_URL(guildId),
);
return result;
return snakeKeysToCamelCase(result) as
| (Partial<InviteMetadata> & Pick<InviteMetadata, "uses" | "code">)
| { code: null };
}

View File

@@ -1,9 +1,22 @@
import { rest } from "../../rest/rest.ts";
import {
DiscordVoiceRegion,
VoiceRegion,
} from "../../types/voice/voice_region.ts";
import { Collection } from "../../util/collection.ts";
import { endpoints } from "../../util/constants.ts";
import { snakeKeysToCamelCase } from "../../util/utils.ts";
/** Returns a list of voice region objects for the guild. Unlike the similar /voice route, this returns VIP servers when the guild is VIP-enabled. */
export async function getVoiceRegions(guildId: string) {
const result = await rest.runMethod("get", endpoints.GUILD_REGIONS(guildId));
const result = await rest.runMethod(
"get",
endpoints.GUILD_REGIONS(guildId),
) as DiscordVoiceRegion[];
return result;
const convertedRegions = snakeKeysToCamelCase<VoiceRegion[]>(result);
return new Collection<string, VoiceRegion>(
convertedRegions.map((region) => [region.id, region]),
);
}

View File

@@ -1,14 +1,12 @@
import { cacheHandlers } from "../../cache.ts";
import { GetGuildWidgetImageQuery } from "../../types/guilds/get_guild_widget_image.ts";
import { Errors } from "../../types/misc/errors.ts";
import { endpoints } from "../../util/constants.ts";
/** Returns the widget image URL for the guild. */
export async function getWidgetImageURL(
guildId: string,
options?: {
style?: "shield" | "banner1" | "banner2" | "banner3" | "banner4";
force?: boolean;
},
options?: GetGuildWidgetImageQuery & { force?: boolean },
) {
if (!options?.force) {
const guild = await cacheHandlers.get("guilds", guildId);

View File

@@ -1,4 +1,5 @@
import { rest } from "../../rest/rest.ts";
import { GuildWidget } from "../../types/guilds/guild_widget.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotGuildPermissions } from "../../util/permissions.ts";
@@ -8,5 +9,5 @@ export async function getWidgetSettings(guildId: string) {
const result = await rest.runMethod("get", endpoints.GUILD_WIDGET(guildId));
return result;
return result as GuildWidget;
}

View File

@@ -6,7 +6,7 @@ import { formatImageURL } from "../../util/utils.ts";
/** The full URL of the banner from Discords CDN. Undefined if no banner is set. */
export function guildBannerURL(
id: string,
banner: string,
banner?: string,
size: DiscordImageSize = 128,
format?: DiscordImageFormat,
) {

View File

@@ -6,7 +6,7 @@ import { formatImageURL } from "../../util/utils.ts";
/** The full URL of the icon from Discords CDN. Undefined when no icon is set. */
export function guildIconURL(
id: string,
icon: string,
icon?: string,
size: DiscordImageSize = 128,
format?: DiscordImageFormat,
) {

View File

@@ -6,7 +6,7 @@ import { formatImageURL } from "../../util/utils.ts";
/** The full URL of the splash from Discords CDN. Undefined if no splash is set. */
export function guildSplashURL(
id: string,
splash: string,
splash?: string,
size: DiscordImageSize = 128,
format?: DiscordImageFormat,
) {

View File

@@ -2,7 +2,7 @@ import { rest } from "../../rest/rest.ts";
import { endpoints } from "../../util/constants.ts";
/** Leave a guild */
export async function leaveGuild(guildId: string) {
export async function leaveGuild(guildId: string): Promise<undefined> {
const result = await rest.runMethod("delete", endpoints.GUILD_LEAVE(guildId));
return result;

View File

@@ -2,15 +2,19 @@ import { cacheHandlers } from "../../cache.ts";
import { rest } from "../../rest/rest.ts";
import { structures } from "../../structures/mod.ts";
import { DiscordChannelTypes } from "../../types/channels/channel_types.ts";
import { DiscordenoCreateMessage } from "../../types/discordeno/create_message.ts";
import { DiscordAllowedMentionsTypes } from "../../types/messages/allowed_mentions_types.ts";
import { DiscordMessage } from "../../types/messages/message.ts";
import { Errors } from "../../types/misc/errors.ts";
import { PermissionStrings } from "../../types/permissions/permission_strings.ts";
import { endpoints } from "../../util/constants.ts";
import { requireBotChannelPermissions } from "../../util/permissions.ts";
import { camelKeysToSnakeCase } from "../../util/utils.ts";
/** Send a message to the channel. Requires SEND_MESSAGES permission. */
export async function sendMessage(
channelId: string,
content: string | MessageContent,
content: string | DiscordenoCreateMessage,
) {
if (typeof content === "string") content = { content };
@@ -33,7 +37,10 @@ export async function sendMessage(
if (content.tts) requiredPerms.add("SEND_TTS_MESSAGES");
if (content.embed) requiredPerms.add("EMBED_LINKS");
if (content.replyMessageId || content.mentions?.repliedUser) {
if (
content.messageReference?.messageId ||
content.allowedMentions?.repliedUser
) {
requiredPerms.add("READ_MESSAGE_HISTORY");
}
@@ -45,50 +52,63 @@ export async function sendMessage(
throw new Error(Errors.MESSAGE_MAX_LENGTH);
}
if (content.mentions) {
if (content.mentions.users?.length) {
if (content.mentions.parse?.includes("users")) {
content.mentions.parse = content.mentions.parse.filter(
if (content.allowedMentions) {
if (content.allowedMentions.users?.length) {
if (
content.allowedMentions.parse?.includes(
DiscordAllowedMentionsTypes.UserMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter(
(p) => p !== "users",
);
}
if (content.mentions.users.length > 100) {
content.mentions.users = content.mentions.users.slice(0, 100);
if (content.allowedMentions.users.length > 100) {
content.allowedMentions.users = content.allowedMentions.users.slice(
0,
100,
);
}
}
if (content.mentions.roles?.length) {
if (content.mentions.parse?.includes("roles")) {
content.mentions.parse = content.mentions.parse.filter(
if (content.allowedMentions.roles?.length) {
if (
content.allowedMentions.parse?.includes(
DiscordAllowedMentionsTypes.RoleMentions,
)
) {
content.allowedMentions.parse = content.allowedMentions.parse.filter(
(p) => p !== "roles",
);
}
if (content.mentions.roles.length > 100) {
content.mentions.roles = content.mentions.roles.slice(0, 100);
if (content.allowedMentions.roles.length > 100) {
content.allowedMentions.roles = content.allowedMentions.roles.slice(
0,
100,
);
}
}
}
const result =
(await rest.runMethod("post", endpoints.CHANNEL_MESSAGES(channelId), {
...content,
allowed_mentions: content.mentions
? {
...content.mentions,
replied_user: content.mentions.repliedUser,
}
: undefined,
...(content.replyMessageId
? {
message_reference: {
message_id: content.replyMessageId,
fail_if_not_exists: content.failReplyIfNotExists === true,
},
}
: {}),
})) as MessageCreateOptions;
(await rest.runMethod(
"post",
endpoints.CHANNEL_MESSAGES(channelId),
camelKeysToSnakeCase({
...content,
...(content.messageReference?.messageId
? {
messageReference: {
...content.messageReference,
failIfNotExists:
content.messageReference.failIfNotExists === true,
},
}
: {}),
}),
)) as DiscordMessage;
return structures.createMessageStruct(result);
}

View File

@@ -3,7 +3,10 @@ import { endpoints } from "../../util/constants.ts";
import { requireBotChannelPermissions } from "../../util/permissions.ts";
/** Unpin a message in a channel. Requires MANAGE_MESSAGES. */
export async function unpin(channelId: string, messageId: string) {
export async function unpin(
channelId: string,
messageId: string,
): Promise<undefined> {
await requireBotChannelPermissions(channelId, ["MANAGE_MESSAGES"]);
const result = await rest.runMethod(

View File

@@ -1,16 +1,14 @@
import { Errors } from "../types/misc/errors.ts";
import { IMAGE_BASE_URL } from "../util/constants.ts";
import { API_VERSION } from "../util/constants.ts";
import { BASE_URL } from "../util/constants.ts";
import { API_VERSION, BASE_URL, IMAGE_BASE_URL } from "../util/constants.ts";
import { rest } from "./rest.ts";
export function runMethod(
export function runMethod<T = any>(
method: "get" | "post" | "put" | "delete" | "patch",
url: string,
body?: unknown,
retryCount = 0,
bucketId?: string | null,
) {
): Promise<T | undefined> {
rest.eventHandlers.debug?.("requestCreate", {
method,
url,
@@ -37,7 +35,7 @@ export function runMethod(
.then((res) => {
if (res.status === 204) return undefined;
return res.json();
return res.json() as unknown as T;
})
.catch((error) => {
console.error(error);

View File

@@ -13,17 +13,30 @@ import { getInvites } from "../helpers/invites/get_invites.ts";
import { banMember } from "../helpers/members/ban_member.ts";
import { unbanMember } from "../helpers/members/unban_member.ts";
import { GetGuildAuditLog } from "../types/audit_log/get_guild_audit_log.ts";
import { Emoji } from "../types/emojis/emoji.ts";
import { CreateGuildBan } from "../types/guilds/create_guild_ban.ts";
import { DiscordGuild, Guild } from "../types/guilds/guild.ts";
import { DiscordGuildFeatures } from "../types/guilds/guild_features.ts";
import { GuildMember } from "../types/guilds/guild_member.ts";
import {
DiscordGuildMember,
GuildMember,
} from "../types/guilds/guild_member.ts";
import { ModifyGuild } from "../types/guilds/modify_guild.ts";
import { DiscordImageFormat } from "../types/misc/image_format.ts";
import { DiscordImageSize } from "../types/misc/image_size.ts";
import { PresenceUpdate } from "../types/misc/presence_update.ts";
import { DiscordUser } from "../types/users/user.ts";
import { VoiceState } from "../types/voice/voice_state.ts";
import { Collection } from "../util/collection.ts";
import { createNewProp, snakeKeysToCamelCase } from "../util/utils.ts";
import { RoleStruct, structures } from "./mod.ts";
import {
camelKeysToSnakeCase,
createNewProp,
snakeKeysToCamelCase,
} from "../util/utils.ts";
import { ChannelStruct } from "./channel.ts";
import { MemberStruct } from "./member.ts";
import { structures } from "./mod.ts";
import { RoleStruct } from "./role.ts";
export const initialMemberLoadQueue = new Map<string, GuildMember[]>();
@@ -118,7 +131,9 @@ export async function createGuildStruct(
} = snakeKeysToCamelCase(data) as Guild;
const roles = await Promise.all(
data.roles.map((role) => structures.createRoleStruct(role)),
data.roles.map((role) =>
structures.createRoleStruct({ role, guild_id: rest.id })
),
);
await Promise.all(channels.map(async (channel) => {
@@ -164,7 +179,9 @@ export async function createGuildStruct(
await Promise.allSettled(
members.map(async (member) => {
const memberStruct = await structures.createMemberStruct(
member,
camelKeysToSnakeCase(member) as Omit<DiscordGuildMember, "user"> & {
user: DiscordUser;
},
guild.id,
);
@@ -179,14 +196,25 @@ export async function createGuildStruct(
export interface GuildStruct extends
Omit<
Guild,
"roles" | "presences" | "voiceStates" | "members" | "channels"
| "roles"
| "presences"
| "voiceStates"
| "members"
| "channels"
| "memberCount"
| "owner"
| "emojis"
> {
/** Total number of members in this guild */
memberCount?: number;
/** The roles in the guild */
roles: Collection<string, RoleStruct>;
/** The presences of all the users in the guild. */
presences: Collection<string, PresenceUpdate>;
/** The Voice State data for each user in a voice channel in this server. */
voiceStates: Collection<string, CleanVoiceState>;
voiceStates: Collection<string, VoiceState>;
/** Custom guild emojis */
emojis: Collection<string, Emoji>;
// GETTERS
/** Members in this guild. */
@@ -204,9 +232,12 @@ export interface GuildStruct extends
/** The bot member in this guild if cached */
bot?: MemberStruct;
/** The bot guild member in this guild if cached */
botMember?: GuildMember;
botMember?: Omit<GuildMember, "joinedAt" | "premiumSince"> & {
joinedAt: number;
premiumSince?: number;
};
/** The bots voice state if there is one in this guild */
botVoice?: CleanVoiceState;
botVoice?: VoiceState;
/** The owner member of this guild */
owner?: MemberStruct;
/** Whether or not this guild is partnered */

View File

@@ -6,13 +6,13 @@ import { kickMember } from "../helpers/members/kick_member.ts";
import { sendDirectMessage } from "../helpers/members/send_direct_message.ts";
import { addRole } from "../helpers/roles/add_role.ts";
import { removeRole } from "../helpers/roles/remove_role.ts";
import { CreateMessage } from "../types/channels/create_message.ts";
import { CreateGuildBan } from "../types/guilds/create_guild_ban.ts";
import {
DiscordGuildMember,
GuildMember,
} from "../types/guilds/guild_member.ts";
import { ModifyGuildMember } from "../types/guilds/modify_guild_member.ts";
import { CreateMessage } from "../types/messages/create_message.ts";
import { DiscordImageFormat } from "../types/misc/image_format.ts";
import { DiscordImageSize } from "../types/misc/image_size.ts";
import { DiscordUser, User } from "../types/users/user.ts";
@@ -151,7 +151,14 @@ export interface MemberStruct extends GuildMember, User {
/** Get the nickname or the username if no nickname */
name(guildID: string): string;
/** Get the guild member object for the specified guild */
guildMember(guildID: string): GuildMember | undefined;
guildMember(
guildID: string,
):
| Omit<GuildMember, "joinedAt" | "premiumSince"> & {
joinedAt: number;
premiumSince?: number;
}
| undefined;
/** Send a direct message to the user is possible */
sendDM(
content: string | CreateMessage,

View File

@@ -9,10 +9,18 @@ import { removeAllReactions } from "../helpers/messages/remove_all_reactions.ts"
import { removeReaction } from "../helpers/messages/remove_reaction.ts";
import { removeReactionEmoji } from "../helpers/messages/remove_reaction_emoji.ts";
import { sendMessage } from "../helpers/messages/send_message.ts";
import { DiscordenoCreateMessage } from "../types/discordeno/create_message.ts";
import { GuildMember } from "../types/guilds/guild_member.ts";
import { EditMessage } from "../types/messages/edit_message.ts";
import { DiscordMessage, Message } from "../types/messages/message.ts";
import { CHANNEL_MENTION_REGEX } from "../util/constants.ts";
import { createNewProp } from "../util/utils.ts";
import { createNewProp, snakeKeysToCamelCase } from "../util/utils.ts";
import { ChannelStruct } from "./channel.ts";
import { GuildStruct } from "./guild.ts";
import { MemberStruct } from "./member.ts";
import { RoleStruct } from "./role.ts";
const baseMessage: Partial<Message> = {
const baseMessage: Partial<MessageStruct> = {
get channel() {
if (this.guildId) return cache.channels.get(this.channelId!);
return cache.channels.get(this.author?.id!);
@@ -34,13 +42,13 @@ const baseMessage: Partial<Message> = {
"@me"}/${this.channelId}/${this.id}`;
},
get mentionedRoles() {
return this.mentionRoleIds?.map((id) => this.guild?.roles.get(id)) || [];
return this.mentionedRoleIds?.map((id) => this.guild?.roles.get(id)) || [];
},
get mentionedChannels() {
return this.mentionChannelIds?.map((id) => cache.channels.get(id)) || [];
return this.mentionedChannelIds?.map((id) => cache.channels.get(id)) || [];
},
get mentionedMembers() {
return this.mentions?.map((id) => cache.members.get(id)) || [];
return this.mentionedUserIds?.map((id) => cache.members.get(id)) || [];
},
// METHODS
@@ -69,9 +77,10 @@ const baseMessage: Partial<Message> = {
}
: {
...content,
mentions: { ...(content.mentions || {}), repliedUser: true },
mentions: { ...(content.allowedMentions || {}), repliedUser: true },
replyMessageId: this.id,
failReplyIfNotExists: content.failReplyIfNotExists === true,
failReplyIfNotExists:
content.messageReference?.failIfNotExists === true,
};
if (this.guildId) return sendMessage(this.channelId!, contentWithMention);
@@ -108,58 +117,126 @@ const baseMessage: Partial<Message> = {
},
};
export async function createMessageStruct(data: MessageCreateOptions) {
export async function createMessageStruct(data: DiscordMessage) {
const {
guild_id: guildId = "",
channel_id: channelId,
mentions_everyone: mentionsEveryone,
mention_channels: mentionChannelIds = [],
mention_roles: mentionRoleIds,
webhook_id: webhookId,
message_reference: messageReference,
guildId = "",
channelId,
mentionChannels = [],
mentions,
mentionRoles,
edited_timestamp: editedTimestamp,
referenced_message: referencedMessageId,
member,
...rest
} = data;
} = snakeKeysToCamelCase(data) as Message;
const restProps: Record<string, ReturnType<typeof createNewProp>> = {};
const props: Record<string, ReturnType<typeof createNewProp>> = {};
for (const key of Object.keys(rest)) {
// @ts-ignore index signature
restProps[key] = createNewProp(rest[key]);
props[key] = createNewProp(rest[key]);
}
// Discord doesnt give guild id for getMessage() so this will fill it in
const guildIdFinal = guildId ||
(await cacheHandlers.get("channels", channelId))?.guildId || "";
const message = Object.create(baseMessage, {
...restProps,
const message: MessageStruct = Object.create(baseMessage, {
...props,
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
referencedMessageId: createNewProp(referencedMessageId),
channelId: createNewProp(channelId),
guildId: createNewProp(guildId || guildIdFinal),
mentions: createNewProp(data.mentions.map((m) => m.id)),
mentionsEveryone: createNewProp(mentionsEveryone),
mentionRoleIds: createNewProp(mentionRoleIds),
mentionChannelIds: createNewProp(
guildId: createNewProp(guildIdFinal),
mentionedUserIds: createNewProp(mentions.map((m) => m.id)),
mentionedRoleIds: createNewProp(mentionRoles),
mentionedChannelIds: createNewProp(
[
// Keep any ids that discord sends
...mentionChannelIds,
...mentionChannels.map((m) => m.id),
// Add any other ids that can be validated in a channel mention format
...(rest.content.match(CHANNEL_MENTION_REGEX) || []).map((text) =>
// converts the <#123> into 123
text.substring(2, text.length - 1)
),
].map((m) => m.id),
],
),
webhookId: createNewProp(webhookId),
messageReference: createNewProp(messageReference),
timestamp: createNewProp(Date.parse(data.timestamp)),
editedTimestamp: createNewProp(
editedTimestamp ? Date.parse(editedTimestamp) : undefined,
),
});
return message as Message;
return message;
}
export interface MessageStruct extends Message {
// For better user experience
/** Ids of users specifically mentioned in the message */
mentionedUserIds: string[];
/** Ids of roles specifically mentioned in this message */
mentionedRoleIds: string[];
/** Channels specifically mentioned in this message */
mentionedChannelIds?: string[];
// GETTERS
/** The channel where this message was sent. Can be undefined if uncached. */
channel?: ChannelStruct;
/** The guild of this message. Can be undefined if not in cache or in DM */
guild?: GuildStruct;
/** The member for the user who sent the message. Can be undefined if not in cache or in dm. */
member?: MemberStruct;
/** The guild member details for this guild and member. Can be undefined if not in cache or in dm. */
guildMember?: Omit<GuildMember, "joinedAt" | "premiumSince"> & {
joinedAt: number;
premiumSince?: number;
};
/** The url link to this message */
link: string;
/** The role objects for all the roles that were mentioned in this message */
mentionedRoles: (RoleStruct | undefined)[];
/** The channel objects for all the channels that were mentioned in this message. */
mentionedChannels: (ChannelStruct | undefined)[];
/** The member objects for all the members that were mentioned in this message. */
mentionedMembers: (MemberStruct | undefined)[];
// METHODS
/** Delete the message */
delete(
reason?: string,
delayMilliseconds?: number,
): ReturnType<typeof deleteMessage>;
/** Edit the message */
edit(content: string | EditMessage): ReturnType<typeof editMessage>;
/** Pins the message in the channel */
pin(): ReturnType<typeof pinMessage>;
/** Add a reaction to the message */
addReaction(reaction: string): ReturnType<typeof addReaction>;
/** Add multiple reactions to the message without or without order. */
addReactions(
reactions: string[],
ordered?: boolean,
): ReturnType<typeof addReactions>;
/** Send a inline reply to this message */
reply(
content: string | DiscordenoCreateMessage,
): ReturnType<typeof sendMessage>;
/** Send a message to this channel where this message is */
send(
content: string | DiscordenoCreateMessage,
): ReturnType<typeof sendMessage>;
/** Send a message to this channel and then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */
alert(
content: string | DiscordenoCreateMessage,
timeout?: number,
reason?: string,
): Promise<void>;
/** Send a inline reply to this message but then delete it after a bit. By default it will delete after 10 seconds with no reason provided. */
alertReply(
content: string | DiscordenoCreateMessage,
timeout?: number,
reason?: string,
): Promise<unknown>;
/** Remove all reactions */
removeAllReactions(): ReturnType<typeof removeAllReactions>;
/** Remove all reactions */
removeReactionEmoji(reaction: string): ReturnType<typeof removeReactionEmoji>;
/** Remove all reactions */
removeReaction(reaction: string): ReturnType<typeof removeReaction>;
}

View File

@@ -0,0 +1,7 @@
import { CreateGlobalApplicationCommand } from "../interactions/create_global_application_command.ts";
export interface DiscordenoCreateApplicationCommand
extends CreateGlobalApplicationCommand {
/** Id of the guild to create a guild only application command */
guildId: string;
}

View File

@@ -0,0 +1,6 @@
import { EditWebhookMessage } from "../webhooks/edit_webhook_message.ts";
export interface DiscordenoEditWebhookMessage extends EditWebhookMessage {
/** Id of the message you want to edit */
messageId: string;
}

View File

View File

@@ -0,0 +1,7 @@
import { Guild } from "../guilds/guild.ts";
export interface GuildUpdateChange {
key: keyof Guild;
oldValue?: unknown;
value?: unknown;
}

View File

@@ -0,0 +1,6 @@
import { InteractionResponse } from "../interactions/interaction_response.ts";
export interface DiscordenoInteractionResponse extends InteractionResponse {
/** Set to true if the response should be private */
private?: boolean;
}

View File

@@ -0,0 +1,12 @@
import { SnakeCaseProps } from "../util.ts";
import { Integration } from "./integration.ts";
export interface IntegrationCreateUpdate extends Integration {
/** Id of the guild */
guildId: string;
}
/** https://github.com/discord/discord-api-docs/blob/master/docs/topics/Gateway.md#integration-create-event-additional-fields */
export type DiscordIntegrationCreateUpdate = SnakeCaseProps<
IntegrationCreateUpdate
>;

View File

@@ -0,0 +1,13 @@
import { SnakeCaseProps } from "../util.ts";
export interface IntegrationDelete {
/** Integration id */
id: string;
/** Id of the guild */
guildId: string;
/** Id of the bot/OAuth2 application for this discord integration */
applicationId?: string;
}
/** https://github.com/discord/discord-api-docs/blob/master/docs/topics/Gateway.md#integration-delete-event-fields */
export type DiscordIntegrationDelete = SnakeCaseProps<IntegrationDelete>;

View File

@@ -1,3 +1,4 @@
import { GuildMember } from "../guilds/guild_member.ts";
import { User } from "../users/user.ts";
import { SnakeCaseProps } from "../util.ts";
import { InteractionApplicationCommandCallbackData } from "./application_command_callback_data.ts";

View File

@@ -1,6 +1,7 @@
import { SnakeCaseProps } from "../util.ts";
import { Invite } from "./invite.ts";
export interface InviteMetadata {
export interface InviteMetadata extends Invite {
/** Number of times this invite has been used */
uses: number;
/** Max number of times this invite can be used */

View File

@@ -5,11 +5,11 @@ export interface AllowedMentions {
/** An array of allowed mention types to parse from the content. */
parse: DiscordAllowedMentionsTypes[];
/** Array of role_ids to mention (Max size of 100) */
roles: string[];
roles?: string[];
/** Array of user_ids to mention (Max size of 100) */
users: string[];
users?: string[];
/** For replies, whether to mention the author of the message being replied to (default false) */
repliedUser: boolean;
repliedUser?: boolean;
}
/** https://discord.com/developers/docs/resources/channel#allowed-mentions-object */

View File

@@ -1,6 +1,7 @@
import { Embed } from "../embeds/embed.ts";
import { AllowedMentions } from "../messages/allowed_mentions.ts";
import { MessageReference } from "../messages/message_reference.ts";
import { FileContent } from "../misc/file_content.ts";
import { SnakeCaseProps } from "../util.ts";
export interface CreateMessage {
@@ -16,7 +17,9 @@ export interface CreateMessage {
allowedMentions?: AllowedMentions;
/** Include to make your message a reply */
messageReference?: MessageReference;
/** The contents of the file being sent */
file?: FileContent | FileContent[];
}
/** https://discord.com/developers/docs/resources/channel#create-message */
export type DiscordCreateMessage = SnakeCaseProps<CreateMessage>;
export type DiscordCreateMessage = SnakeCaseProps<Omit<CreateMessage, "file">>;

View File

@@ -0,0 +1,17 @@
import { Embed } from "../embeds/embed.ts";
import { SnakeCaseProps } from "../util.ts";
import { AllowedMentions } from "./allowed_mentions.ts";
export interface EditMessage {
/** The new message contents (up to 2000 characters) */
content?: string | null;
/** Embedded `rich` content */
embed?: Embed | null;
/** Edit the flags of the message (only `SUPRESS_EMBEDS` can currently be set/unset) */
flags?: 4 | null;
/** Allowed mentions for the message */
allowedMentions?: AllowedMentions | null;
}
/** https://discord.com/developers/docs/resources/channel#edit-message-json-params */
export type DiscordEditMessage = SnakeCaseProps<EditMessage>;

View File

@@ -5,15 +5,18 @@ import { Errors } from "../types/misc/errors.ts";
import { DiscordBitwisePermissionFlags } from "../types/permissions/bitwise_permission_flags.ts";
import { PermissionStrings } from "../types/permissions/permission_strings.ts";
async function getCached(table: "guilds", key: string | Guild): Promise<Guild>;
async function getCached(
table: "guilds",
key: string | Guild,
): Promise<Guild | undefined>;
async function getCached(
table: "channels",
key: string | Channel,
): Promise<Channel>;
): Promise<Channel | undefined>;
async function getCached(
table: "members",
key: string | Member,
): Promise<Member>;
): Promise<Member | undefined>;
async function getCached(
table: "guilds" | "channels" | "members",
key: string | Guild | Channel | Member,
@@ -28,7 +31,7 @@ async function getCached(
);
}
return cached;
return typeof cached === "string" ? undefined : cached;
}
/** Calculates the permissions this member has in the given guild */
@@ -39,6 +42,8 @@ export async function calculateBasePermissions(
guild = await getCached("guilds", guild);
member = await getCached("members", member);
if (!guild || !member) return "8";
let permissions = 0n;
// Calculate the role permissions bits, @everyone role is not in memberRoleIds so we need to pass guildId manualy
permissions |= [...(member.guilds.get(guild.id)?.roles || []), guild.id]
@@ -64,10 +69,12 @@ export async function calculateChannelOverwrites(
channel = await getCached("channels", channel);
// This is a DM channel so return ADMINISTRATOR permission
if (!channel.guildId) return "8";
if (!channel?.guildId) return "8";
member = await getCached("members", member);
if (!member) return "8";
// Get all the role permissions this member already has
let permissions = BigInt(
await calculateBasePermissions(channel.guildId, member),
@@ -285,8 +292,10 @@ export async function highestRole(
) {
guild = await getCached("guilds", guild);
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
// Get the roles from the member
const memberRoles = (await getCached("members", member)).guilds.get(guild.id)
const memberRoles = (await getCached("members", member))?.guilds.get(guild.id)
?.roles;
// This member has no roles so the highest one is the @everyone role
if (!memberRoles) return guild.roles.get(guild.id) as Role;
@@ -321,6 +330,8 @@ export async function higherRolePosition(
) {
guild = await getCached("guilds", guild);
if (!guild) return true;
const role = guild.roles.get(roleId);
const otherRole = guild.roles.get(otherRoleId);
if (!role || !otherRole) throw new Error(Errors.ROLE_NOT_FOUND);
@@ -341,7 +352,7 @@ export async function isHigherPosition(
) {
guild = await getCached("guilds", guild);
if (guild.ownerId === memberId) return true;
if (!guild || guild.ownerId === memberId) return true;
const memberHighestRole = await highestRole(guild, memberId);
return higherRolePosition(guild.id, memberHighestRole.id, compareRoleId);

View File

@@ -72,7 +72,9 @@ function isObject(obj: unknown) {
}
// deno-lint-ignore no-explicit-any
export function camelKeysToSnakeCase(obj: Record<string, any>) {
export function camelKeysToSnakeCase<T>(
obj: Record<string, any> | Record<string, any>[],
): T {
if (isObject(obj)) {
// deno-lint-ignore no-explicit-any
const convertedObject: Record<string, any> = {};
@@ -80,20 +82,22 @@ export function camelKeysToSnakeCase(obj: Record<string, any>) {
Object.keys(obj)
.forEach((key) => {
convertedObject[camelToSnakeCase(key)] = camelKeysToSnakeCase(
obj[key],
(obj as Record<string, any>)[key],
);
});
return convertedObject;
return convertedObject as T;
} else if (Array.isArray(obj)) {
obj = obj.map((element) => camelKeysToSnakeCase(element));
}
return obj;
return obj as T;
}
// deno-lint-ignore no-explicit-any
export function snakeKeysToCamelCase(obj: Record<string, any>) {
export function snakeKeysToCamelCase<T>(
obj: Record<string, any> | Record<string, any>[],
): T {
if (isObject(obj)) {
// deno-lint-ignore no-explicit-any
const convertedObject: Record<string, any> = {};
@@ -101,16 +105,16 @@ export function snakeKeysToCamelCase(obj: Record<string, any>) {
Object.keys(obj)
.forEach((key) => {
convertedObject[snakeToCamelCase(key)] = snakeKeysToCamelCase(
obj[key],
(obj as Record<string, any>)[key],
);
});
return convertedObject;
return convertedObject as T;
} else if (Array.isArray(obj)) {
obj = obj.map((element) => snakeKeysToCamelCase(element));
}
return obj;
return obj as T;
}
/** @private */