Merge branch 'main' into v12

This commit is contained in:
ITOH
2021-07-09 15:57:41 +01:00
committed by GitHub
43 changed files with 355 additions and 248 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @Skillz4Killz @itohatweb
* @Skillz4Killz @itohatweb @ayntee

View File

@@ -65,7 +65,7 @@ unofficial templates:
- [Discordeno Template (official)](https://github.com/discordeno/template)
- [Serverless Slash Commands Template
(official)](https://github.com/discordeno/slash-commands-template)
(official)](https://github.com/discordeno/serverless-deno-deploy-template)
- [Add Your Own!](https://github.com/discordeno/discordeno/pulls)
## Links

View File

@@ -8,6 +8,12 @@ import type { PresenceUpdate } from "./types/activity/presence_update.ts";
import type { Emoji } from "./types/emojis/emoji.ts";
import { DiscordenoThread } from "./util/transformers/channel_to_thread.ts";
import { Collection } from "./util/collection.ts";
import { Channel } from "./types/channels/channel.ts";
import { Guild } from "./types/guilds/guild.ts";
import { GuildMemberWithUser } from "./types/members/guild_member.ts";
import { Message } from "./types/messages/message.ts";
import { Role } from "./types/permissions/role.ts";
import { VoiceState } from "./types/voice/voice_state.ts";
export const cache = {
isReady: false,
@@ -37,6 +43,21 @@ export const cache = {
dispatchedGuildIds: new Set<bigint>(),
dispatchedChannelIds: new Set<bigint>(),
threads: new Collection<bigint, DiscordenoThread>(),
/** ADVANCED USER ONLY: Please ask for help before modifying these. The properties that you want to use for your bot's structures. If you do not set any properties, all properties will be used by default. */
requiredStructureProperties: {
/** Only these properties will be added to memory for your channels. */
channels: new Set<keyof Channel>(),
/** Only these properties will be added to memory for your guilds. */
guilds: new Set<keyof Guild>(),
/** Only these properties will be added to memory for your members. */
members: new Set<keyof GuildMemberWithUser>(),
/** Only these properties will be added to memory for your messages. */
messages: new Set<keyof Message>(),
/** Only these properties will be added to memory for your roles. */
roles: new Set<keyof Role>(),
/** Only these properties will be added to memory for your voice states. */
voiceStates: new Set<keyof VoiceState>(),
},
};
function messageSweeper(message: DiscordenoMessage) {
@@ -149,58 +170,80 @@ async function get(table: TableName, key: bigint) {
return cache[table].get(key);
}
function forEach(
table: "threads",
callback: (value: DiscordenoThread, key: bigint, map: Map<bigint, DiscordenoThread>) => unknown
): void;
function forEach(
table: "guilds",
callback: (value: DiscordenoGuild, key: bigint, map: Map<bigint, DiscordenoGuild>) => unknown
): void;
function forEach(
table: "unavailableGuilds",
callback: (value: number, key: bigint, map: Map<bigint, number>) => unknown
): void;
function forEach(
table: "channels",
callback: (value: DiscordenoChannel, key: bigint, map: Map<bigint, DiscordenoChannel>) => unknown
): void;
function forEach(
table: "messages",
callback: (value: DiscordenoMessage, key: bigint, map: Map<bigint, DiscordenoMessage>) => unknown
): void;
function forEach(
table: "members",
callback: (value: DiscordenoMember, key: bigint, map: Map<bigint, DiscordenoMember>) => unknown
): void;
function forEach(table: TableName, callback: (value: any, key: bigint, map: Map<bigint, any>) => unknown) {
return cache[table].forEach(callback);
// callback: (value: DiscordenoThread, key: bigint, map: Map<bigint, DiscordenoThread>) => void
async function forEach(type: "DELETE_MESSAGES_FROM_CHANNEL", options: { channelId: bigint }): Promise<void>;
async function forEach(type: "DELETE_MESSAGES_FROM_GUILD", options: { guildId: bigint }): Promise<void>;
async function forEach(type: "DELETE_CHANNELS_FROM_GUILD", options: { guildId: bigint }): Promise<void>;
async function forEach(type: "DELETE_GUILD_FROM_MEMBER", options: { guildId: bigint }): Promise<void>;
async function forEach(type: "DELETE_ROLE_FROM_MEMBER", options: { guildId: bigint; roleId: bigint }): Promise<void>;
async function forEach(
type:
| "DELETE_MESSAGES_FROM_CHANNEL"
| "DELETE_MESSAGES_FROM_GUILD"
| "DELETE_CHANNELS_FROM_GUILD"
| "DELETE_GUILD_FROM_MEMBER"
| "DELETE_ROLE_FROM_MEMBER",
options?: Record<string, unknown>
) {
if (type === "DELETE_MESSAGES_FROM_CHANNEL") {
cache.messages.forEach((message) => {
if (message.channelId === options?.channelId) cache.messages.delete(message.id);
});
return;
}
if (type === "DELETE_MESSAGES_FROM_GUILD") {
cache.messages.forEach((message) => {
if (message.guildId === options?.guildId) cache.messages.delete(message.id);
});
return;
}
if (type === "DELETE_CHANNELS_FROM_GUILD") {
cache.channels.forEach((channel) => {
if (channel.guildId === options?.guildId) cache.channels.delete(channel.id);
});
return;
}
if (type === "DELETE_GUILD_FROM_MEMBER") {
cache.members.forEach((member) => {
if (!member.guilds.has(options?.guildId as bigint)) return;
member.guilds.delete(options?.guildId as bigint);
if (!member.guilds.size) {
return cache.members.delete(member.id);
}
cache.members.set(member.id, member);
});
return;
}
if (type === "DELETE_ROLE_FROM_MEMBER") {
cache.members.forEach((member) => {
// Not in the relevant guild so just skip
if (!member.guilds.has(options?.guildId as bigint)) return;
const guildMember = member.guilds.get(options?.guildId as bigint)!;
guildMember.roles = guildMember.roles.filter((id) => id !== (options?.roleId as bigint));
cache.members.set(member.id, member);
});
return;
}
}
async function filter(
table: "threads",
callback: (value: DiscordenoThread, key: bigint) => boolean
): Promise<Collection<bigint, DiscordenoThread>>;
async function filter(
table: "guilds",
callback: (value: DiscordenoGuild, key: bigint) => boolean
): Promise<Collection<bigint, DiscordenoGuild>>;
async function filter(
table: "unavailableGuilds",
callback: (value: number, key: bigint) => boolean
): Promise<Collection<bigint, number>>;
async function filter(
table: "channels",
callback: (value: DiscordenoChannel, key: bigint) => boolean
): Promise<Collection<bigint, DiscordenoChannel>>;
async function filter(
table: "messages",
callback: (value: DiscordenoMessage, key: bigint) => boolean
): Promise<Collection<bigint, DiscordenoMessage>>;
async function filter(
table: "members",
callback: (value: DiscordenoMember, key: bigint) => boolean
type: "GET_MEMBERS_IN_GUILD",
options: { guildId: bigint }
): Promise<Collection<bigint, DiscordenoMember>>;
async function filter(table: TableName, callback: (value: any, key: bigint) => boolean) {
return cache[table].filter(callback);
async function filter(
type: "GET_MEMBERS_IN_GUILD",
options?: Record<string, unknown>
): Promise<Collection<bigint, DiscordenoMember> | undefined> {
if (type === "GET_MEMBERS_IN_GUILD") {
return cache.members.filter((member) => member.guilds.has(options?.guildId as bigint));
}
}

View File

@@ -40,12 +40,7 @@ export async function handleChannelDelete(data: DiscordGatewayPayload) {
].includes(payload.type)
) {
await cacheHandlers.delete("channels", snowflakeToBigint(payload.id));
cacheHandlers.forEach("messages", (message) => {
eventHandlers.debug?.("loop", `Running forEach messages loop in CHANNEL_DELTE file.`);
if (message.channelId === snowflakeToBigint(payload.id)) {
cacheHandlers.delete("messages", message.id);
}
});
await cacheHandlers.forEach("DELETE_MESSAGES_FROM_CHANNEL", { channelId: snowflakeToBigint(payload.id) });
}
await cacheHandlers.delete("channels", snowflakeToBigint(payload.id));

View File

@@ -11,12 +11,7 @@ export async function handleThreadDelete(data: DiscordGatewayPayload) {
if (!cachedChannel) return;
await cacheHandlers.delete("threads", snowflakeToBigint(payload.id));
cacheHandlers.forEach("messages", (message) => {
eventHandlers.debug?.("loop", `Running forEach messages loop in THREAD_DELETE file.`);
if (message.channelId === snowflakeToBigint(payload.id)) {
cacheHandlers.delete("messages", message.id);
}
});
await cacheHandlers.forEach("DELETE_MESSAGES_FROM_CHANNEL", { channelId: snowflakeToBigint(payload.id) });
eventHandlers.threadDelete?.(cachedChannel);
}

View File

@@ -23,30 +23,9 @@ export async function handleGuildDelete(data: DiscordGatewayPayload, shardId: nu
eventHandlers.guildDelete?.(guild);
}
cacheHandlers.forEach("messages", (message) => {
eventHandlers.debug?.("loop", `1. Running forEach messages loop in CHANNEL_DELTE file.`);
if (message.guildId === guild.id) {
cacheHandlers.delete("messages", message.id);
}
});
cacheHandlers.forEach("channels", (channel) => {
eventHandlers.debug?.("loop", `2. Running forEach channels loop in CHANNEL_DELTE file.`);
if (channel.guildId === guild.id) {
cacheHandlers.delete("channels", channel.id);
}
});
cacheHandlers.forEach("members", (member) => {
eventHandlers.debug?.("loop", `3. Running forEach members loop in CHANNEL_DELTE file.`);
if (!member.guilds.has(guild.id)) return;
member.guilds.delete(guild.id);
if (!member.guilds.size) {
return cacheHandlers.delete("members", member.id);
}
cacheHandlers.set("members", member.id, member);
});
await Promise.all([
cacheHandlers.forEach("DELETE_MESSAGES_FROM_GUILD", { guildId: guild.id }),
cacheHandlers.forEach("DELETE_CHANNELS_FROM_GUILD", { guildId: guild.id }),
cacheHandlers.forEach("DELETE_GUILD_FROM_MEMBER", { guildId: guild.id }),
]);
}

View File

@@ -31,7 +31,7 @@ export async function handleGuildMembersChunk(data: DiscordGatewayPayload) {
return resolve(new Collection(members.map((m) => [m.id, m])));
}
return resolve(await cacheHandlers.filter("members", (m) => m.guilds.has(guildId)));
return resolve(await cacheHandlers.filter("GET_MEMBERS_IN_GUILD", { guildId }));
}
}
}

View File

@@ -17,18 +17,5 @@ export async function handleGuildRoleDelete(data: DiscordGatewayPayload) {
if (cachedRole) eventHandlers.roleDelete?.(guild, cachedRole);
// For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking.
cacheHandlers.forEach("members", (member) => {
eventHandlers.debug?.("loop", `1. Running forEach members loop in GUILD_ROLE_DELETE file.`);
// Not in the relevant guild so just skip.
if (!member.guilds.has(guild.id)) return;
member.guilds.forEach((g) => {
eventHandlers.debug?.("loop", `2. Running forEach loop in CHANNEL_DELTE file.`);
// Member does not have this role
if (!g.roles.includes(roleId)) return;
// Remove this role from the members cache
g.roles = g.roles.filter((id) => id !== roleId);
cacheHandlers.set("members", member.id, member);
});
});
await cacheHandlers.forEach("DELETE_ROLE_FROM_MEMBER", { guildId: guild.id, roleId });
}

View File

@@ -1,6 +1,8 @@
import { cacheHandlers } from "../../cache.ts";
import { cache } from "../../cache.ts";
/** Gets an array of all the channels ids that are the children of this category. */
export async function categoryChildren(id: bigint) {
return await cacheHandlers.filter("channels", (channel) => channel.parentId === id);
/** Gets an array of all the channels ids that are the children of this category.
* ⚠️ This does not work for custom cache users!
*/
export function categoryChildren(parentChannelId: bigint) {
return cache.channels.filter((channel) => channel.parentId === parentChannelId);
}

View File

@@ -20,7 +20,6 @@ export async function deleteChannel(channelId: bigint, reason?: string) {
throw new Error(Errors.UPDATES_CHANNEL_CANNOT_BE_DELETED);
}
// TODO(threads): check if this requires guild perms or channel is enough
await requireBotGuildPermissions(guild, ["MANAGE_CHANNELS"]);
}

View File

@@ -9,7 +9,6 @@ import { endpoints } from "../../util/constants.ts";
import { calculateBits, requireOverwritePermissions } from "../../util/permissions.ts";
import { snakelize } from "../../util/utils.ts";
//TODO: implement DM group channel edit
/** Update a channel's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */
export async function editChannel(channelId: bigint, options: ModifyChannel, reason?: string) {
const channel = await cacheHandlers.get("channels", channelId);

View File

@@ -4,8 +4,7 @@ import { Errors } from "../../../types/discordeno/errors.ts";
import { endpoints } from "../../../util/constants.ts";
import { requireBotChannelPermissions } from "../../../util/permissions.ts";
/** Adds a user to a thread. Requires the ability to send messages in the thread. Requires the thread is not archived.
*/
/** Adds a user to a thread. Requires the ability to send messages in the thread. Requires the thread is not archived. */
export async function addToThread(threadId: bigint, userId: bigint) {
const thread = await cacheHandlers.get("threads", threadId);
if (thread) {
@@ -19,5 +18,5 @@ export async function addToThread(threadId: bigint, userId: bigint) {
if (channel) await requireBotChannelPermissions(channel, ["SEND_MESSAGES"]);
}
return await rest.runMethod("put", endpoints.THREAD_USER(threadId, userId));
return await rest.runMethod<undefined>("put", endpoints.THREAD_USER(threadId, userId));
}

View File

@@ -1,6 +1,6 @@
import { editThread } from "./edit_thread.ts";
/** Sets a thread channel to be archived. */
export function archiveThread(threadId: bigint) {
return editThread(threadId, { archived: true });
export async function archiveThread(threadId: bigint) {
return await editThread(threadId, { archived: true });
}

View File

@@ -11,5 +11,5 @@ export async function deleteThread(threadId: bigint, reason?: string) {
if (channel?.guildId) await requireBotGuildPermissions(channel.guildId, ["MANAGE_THREADS"]);
}
return await rest.runMethod("delete", endpoints.CHANNEL_BASE(threadId), { reason });
return await rest.runMethod<undefined>("delete", endpoints.CHANNEL_BASE(threadId), { reason });
}

View File

@@ -1,9 +1,12 @@
import { cacheHandlers } from "../../../cache.ts";
import { rest } from "../../../rest/rest.ts";
import { ThreadMember } from "../../../types/channels/threads/thread_member.ts";
import { Errors } from "../../../types/discordeno/errors.ts";
import { DiscordGatewayIntents } from "../../../types/gateway/gateway_intents.ts";
import { Collection } from "../../../util/collection.ts";
import { endpoints } from "../../../util/constants.ts";
import { botHasChannelPermissions } from "../../../util/permissions.ts";
import { threadMemberModified } from "../../../util/transformers/thread_member_modified.ts";
import { ws } from "../../../ws/ws.ts";
/** Returns thread members objects that are members of the thread. */
@@ -20,5 +23,9 @@ export async function getThreadMembers(threadId: bigint) {
throw new Error(Errors.CANNOT_GET_MEMBERS_OF_AN_UNJOINED_PRIVATE_THREAD);
}
return await rest.runMethod("get", endpoints.THREAD_MEMBERS(threadId));
const result = await rest.runMethod<ThreadMember[]>("get", endpoints.THREAD_MEMBERS(threadId));
const members = result.map((member) => threadMemberModified(member));
return new Collection(members.map((member) => [member.id, member]));
}

View File

@@ -10,5 +10,5 @@ export async function joinThread(threadId: bigint) {
throw new Error(Errors.CANNOT_ADD_USER_TO_ARCHIVED_THREADS);
}
return await rest.runMethod("put", endpoints.THREAD_ME(threadId));
return await rest.runMethod<undefined>("put", endpoints.THREAD_ME(threadId));
}

View File

@@ -8,5 +8,5 @@ export async function leaveThread(threadId: bigint) {
const thread = await cacheHandlers.get("threads", threadId);
if (thread?.archived) throw new Error(Errors.CANNOT_LEAVE_ARCHIVED_THREAD);
return await rest.runMethod("delete", endpoints.THREAD_ME(threadId));
return await rest.runMethod<undefined>("delete", endpoints.THREAD_ME(threadId));
}

View File

@@ -1,6 +1,6 @@
import { editThread } from "./edit_thread.ts";
/** Sets a thread channel to be locked. */
export function lockThread(threadId: bigint) {
return editThread(threadId, { locked: true });
export async function lockThread(threadId: bigint) {
return await editThread(threadId, { locked: true });
}

View File

@@ -17,5 +17,5 @@ export async function removeThreadMember(threadId: bigint, userId: bigint) {
}
}
return await rest.runMethod("delete", endpoints.THREAD_USER(threadId, userId));
return await rest.runMethod<undefined>("delete", endpoints.THREAD_USER(threadId, userId));
}

View File

@@ -1,5 +1,6 @@
import { cacheHandlers } from "../../../cache.ts";
import { rest } from "../../../rest/rest.ts";
import { Channel } from "../../../types/channels/channel.ts";
import { StartThread } from "../../../types/channels/threads/start_thread.ts";
import { Errors } from "../../../types/discordeno/errors.ts";
import { endpoints } from "../../../util/constants.ts";
@@ -18,5 +19,7 @@ export async function startPrivateThread(channelId: bigint, options: StartThread
await requireBotChannelPermissions(channel, ["SEND_MESSAGES", "USE_PRIVATE_THREADS"]);
}
return channelToThread(await rest.runMethod("post", endpoints.THREAD_START_PRIVATE(channelId), snakelize(options)));
return channelToThread(
await rest.runMethod<Channel>("post", endpoints.THREAD_START_PRIVATE(channelId), snakelize(options))
);
}

View File

@@ -1,13 +1,15 @@
import { cacheHandlers } from "../../../cache.ts";
import { rest } from "../../../rest/rest.ts";
import { Channel } from "../../../types/channels/channel.ts";
import { StartThread } from "../../../types/channels/threads/start_thread.ts";
import { Errors } from "../../../types/discordeno/errors.ts";
import { endpoints } from "../../../util/constants.ts";
import { requireBotChannelPermissions } from "../../../util/permissions.ts";
import { channelToThread } from "../../../util/transformers/channel_to_thread.ts";
import { snakelize } from "../../../util/utils.ts";
/** Creates a new public thread from an existing message. Returns a thread channel. */
export async function startThread(channelId: bigint, options: StartThread & { messageId: bigint }) {
export async function startThread(channelId: bigint, messageId: bigint, options: StartThread) {
const channel = await cacheHandlers.get("channels", channelId);
if (channel) {
if (!channel.isGuildTextBasedChannel) {
@@ -17,9 +19,7 @@ export async function startThread(channelId: bigint, options: StartThread & { me
await requireBotChannelPermissions(channel, ["SEND_MESSAGES", "USE_PUBLIC_THREADS"]);
}
return await rest.runMethod(
"post",
endpoints.THREAD_START_PUBLIC(channelId, options.messageId),
snakelize(options)
return channelToThread(
await rest.runMethod<Channel>("post", endpoints.THREAD_START_PUBLIC(channelId, messageId), snakelize(options))
);
}

View File

@@ -1,6 +1,6 @@
import { editThread } from "./edit_thread.ts";
/** Sets a thread channel to be unarchived. */
export function unarchiveThread(threadId: bigint) {
return editThread(threadId, { archived: false });
export async function unarchiveThread(threadId: bigint) {
return await editThread(threadId, { archived: false });
}

View File

@@ -1,6 +1,6 @@
import { editThread } from "./edit_thread.ts";
/** Sets a thread channel to be unlocked. */
export function unlockThread(threadId: bigint) {
return editThread(threadId, { locked: false });
export async function unlockThread(threadId: bigint) {
return await editThread(threadId, { locked: false });
}

View File

@@ -5,10 +5,10 @@ import { requireBotGuildPermissions } from "../../util/permissions.ts";
import { snakelize } from "../../util/utils.ts";
/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */
export async function ban(guildId: bigint, id: bigint, options: CreateGuildBan) {
export async function ban(guildId: bigint, id: bigint, options?: CreateGuildBan) {
await requireBotGuildPermissions(guildId, ["BAN_MEMBERS"]);
return await rest.runMethod<undefined>("put", endpoints.GUILD_BAN(guildId, id), snakelize(options));
return await rest.runMethod<undefined>("put", endpoints.GUILD_BAN(guildId, id), snakelize(options ?? {}));
}
// aliases

View File

@@ -29,11 +29,6 @@ export async function editMessage(channelId: bigint, messageId: bigint, content:
validateComponents(content.components);
}
// TODO: v12 remove
if (content.embed) {
content.embeds = [content.embed, ...(content.embeds || [])];
content.embed = undefined;
}
content.embeds?.splice(10);
if (content.content && content.content.length > 2000) {

View File

@@ -34,11 +34,6 @@ export async function sendMessage(channelId: bigint, content: string | CreateMes
const requiredPerms: Set<PermissionStrings> = new Set(["SEND_MESSAGES", "VIEW_CHANNEL"]);
if (content.tts) requiredPerms.add("SEND_TTS_MESSAGES");
// TODO: v12 remove
if (content.embed) {
content.embeds = [content.embed, ...(content.embeds || [])];
content.embed = undefined;
}
if (content.embeds?.length) {
requiredPerms.add("EMBED_LINKS");
content.embeds?.splice(10);

View File

@@ -100,6 +100,7 @@ import { editBotProfile } from "./misc/edit_bot_profile.ts";
import { editBotStatus } from "./misc/edit_bot_status.ts";
import { getGatewayBot } from "./misc/get_gateway_bot.ts";
import { getUser } from "./misc/get_user.ts";
import { getApplicationInfo } from "./oauth/get_application.ts";
import { addRole } from "./roles/add_role.ts";
import { createRole } from "./roles/create_role.ts";
import { deleteRole } from "./roles/delete_role.ts";
@@ -246,6 +247,7 @@ export {
getStageInstance,
getTemplate,
getUser,
getApplicationInfo,
getVanityURL,
getVoiceRegions,
getWebhook,

View File

@@ -0,0 +1,8 @@
import { rest } from "../../rest/rest.ts";
import { endpoints } from "../../util/constants.ts";
import { Application } from "../../types/applications/application.ts";
/** Get the applications info */
export async function getApplicationInfo() {
return await rest.runMethod<Omit<Application, "flags">>("get", endpoints.OAUTH2_APPLICATION);
}

View File

@@ -109,32 +109,36 @@ const baseChannel: Partial<DiscordenoChannel> = {
export async function createDiscordenoChannel(data: Channel, guildId?: bigint) {
const { lastPinTimestamp, permissionOverwrites = [], ...rest } = data;
const requiredPropsSize = cache.requiredStructureProperties.channels.size;
const props: Record<string, PropertyDescriptor> = {};
(Object.keys(rest) as (keyof typeof rest)[]).forEach((key) => {
for (const key of Object.keys(rest) as (keyof typeof rest)[]) {
eventHandlers.debug?.("loop", `Running forEach loop in createDiscordenoChannel function.`);
// If empty then support all, otherwise we only allow the ones user added
if (requiredPropsSize && !cache.requiredStructureProperties.channels.has(key)) continue;
props[key] = createNewProp(
CHANNEL_SNOWFLAKES.includes(key) ? (rest[key] ? snowflakeToBigint(rest[key] as string) : undefined) : rest[key]
);
});
}
// Set the guildId seperately because sometimes guildId is not included
props.guildId = createNewProp(snowflakeToBigint(guildId?.toString() || data.guildId || ""));
if (!requiredPropsSize || cache.requiredStructureProperties.channels.has("guildId"))
props.guildId = createNewProp(snowflakeToBigint(guildId?.toString() || data.guildId || ""));
const channel: DiscordenoChannel = Object.create(baseChannel, {
...props,
lastPinTimestamp: createNewProp(lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined),
permissionOverwrites: createNewProp(
if (!requiredPropsSize || cache.requiredStructureProperties.channels.has("lastPinTimestamp"))
props.lastPinTimestamp = createNewProp(lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined);
if (!requiredPropsSize || cache.requiredStructureProperties.channels.has("permissionOverwrites"))
props.permissionOverwrites = createNewProp(
permissionOverwrites.map((o) => ({
...o,
id: snowflakeToBigint(o.id),
allow: snowflakeToBigint(o.allow),
deny: snowflakeToBigint(o.deny),
}))
),
});
);
return channel;
return Object.create(baseChannel, props) as DiscordenoChannel;
}
export interface DiscordenoChannel

View File

@@ -264,7 +264,11 @@ export async function createDiscordenoGuild(data: Guild, shardId: number) {
});
}
await Promise.all(promises);
await Promise.all(
promises.map(async (promise) => {
return await promise();
})
);
const roles = await Promise.all(
(data.roles || []).map((role) =>
@@ -283,27 +287,37 @@ export async function createDiscordenoGuild(data: Guild, shardId: number) {
);
const props: Record<string, ReturnType<typeof createNewProp>> = {};
(Object.keys(rest) as (keyof typeof rest)[]).forEach((key) => {
for (const key of Object.keys(rest) as (keyof typeof rest)[]) {
eventHandlers.debug?.("loop", `Running for of loop in createDiscordenoGuild function.`);
// If its empty default allows all, otherwise only allow those users required.
if (cache.requiredStructureProperties.guilds.size && !cache.requiredStructureProperties.guilds.has(key)) {
continue;
}
const toggleBits = guildToggles[key as keyof typeof guildToggles];
if (toggleBits) {
bitfield |= rest[key] ? toggleBits : 0n;
return;
continue;
}
props[key] = createNewProp(
GUILD_SNOWFLAKES.includes(key) ? (rest[key] ? snowflakeToBigint(rest[key] as string) : undefined) : rest[key]
);
});
}
const hashes = [
{ name: "icon", toggle: guildToggles.animatedIcon, value: icon },
{ name: "banner", toggle: guildToggles.animatedBanner, value: banner },
{ name: "splash", toggle: guildToggles.animatedSplash, value: splash },
];
] as const;
for (const hash of hashes) {
// If its empty default allows all, otherwise only allow those users required.
if (cache.requiredStructureProperties.guilds.size && !cache.requiredStructureProperties.guilds.has(hash.name)) {
continue;
}
const transformed = hash.value ? iconHashToBigInt(hash.value) : undefined;
if (transformed) {
props[hash.name] = createNewProp(hash.value);
@@ -311,20 +325,35 @@ export async function createDiscordenoGuild(data: Guild, shardId: number) {
}
}
const guild: DiscordenoGuild = Object.create(baseGuild, {
...props,
shardId: createNewProp(shardId),
roles: createNewProp(new Collection(roles.map((r: DiscordenoRole) => [r.id, r]))),
joinedAt: createNewProp(Date.parse(joinedAt)),
presences: createNewProp(new Collection(presences.map((p) => [snowflakeToBigint(p.user!.id), p]))),
memberCount: createNewProp(memberCount),
emojis: createNewProp(new Collection(emojis.map((emoji) => [snowflakeToBigint(emoji.id!), emoji]))),
voiceStates: createNewProp(new Collection(voiceStateStructs.map((vs) => [vs.userId, vs]))),
bitfield: createNewProp(bitfield),
});
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("roles")) {
props.roles = createNewProp(new Collection(roles.map((r: DiscordenoRole) => [r.id, r])));
}
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("joinedAt")) {
props.joinedAt = createNewProp(Date.parse(joinedAt));
}
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("presences")) {
props.presences = createNewProp(new Collection(presences.map((p) => [snowflakeToBigint(p.user!.id), p])));
}
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("memberCount")) {
props.memberCount = createNewProp(memberCount);
}
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("emojis")) {
props.emojis = createNewProp(new Collection(emojis.map((emoji) => [snowflakeToBigint(emoji.id!), emoji])));
}
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("voiceStates")) {
props.voiceStates = createNewProp(new Collection(voiceStateStructs.map((vs) => [vs.userId, vs])));
}
// @ts-ignore allow using these props
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("shardId")) {
props.shardId = createNewProp(shardId);
}
// @ts-ignore allow using these props
if (!cache.requiredStructureProperties.guilds.size || cache.requiredStructureProperties.guilds.has("bitfield")) {
props.bitfield = createNewProp(bitfield);
}
const guild: DiscordenoGuild = Object.create(baseGuild, props);
await cacheMembers(guild.id, members as GuildMemberWithUser[]);
return guild;
}
@@ -441,7 +470,7 @@ export interface DiscordenoGuild
/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */
bans(): ReturnType<typeof getBans>;
/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */
ban(memberId: bigint, options: CreateGuildBan): ReturnType<typeof banMember>;
ban(memberId: bigint, options?: CreateGuildBan): ReturnType<typeof banMember>;
/** Remove the ban for a user. Requires BAN_MEMBERS permission */
unban(memberId: bigint): ReturnType<typeof unbanMember>;
/** Get all the invites for this guild. Requires MANAGE_GUILD permission */

View File

@@ -137,39 +137,48 @@ export async function createDiscordenoMember(
let bitfield = 0n;
const props: Record<string, ReturnType<typeof createNewProp>> = {};
(Object.keys(user) as (keyof typeof user)[]).forEach((key) => {
for (const key of Object.keys(user) as (keyof typeof user)[]) {
eventHandlers.debug?.("loop", `Running for of for Object.keys(user) loop in DiscordenoMember function.`);
// @ts-ignore allow user prop args
if (cache.requiredStructureProperties.members.size && !cache.requiredStructureProperties.members.has(key)) continue;
const toggleBits = memberToggles[key as keyof typeof memberToggles];
if (toggleBits) {
bitfield |= user[key] ? toggleBits : 0n;
return;
continue;
}
if (key === "avatar") {
const transformed = user[key] ? iconHashToBigInt(user[key] as string) : undefined;
if (transformed?.animated) bitfield |= memberToggles.animatedAvatar;
props.avatar = createNewProp(transformed?.bigint);
return;
continue;
}
if (key === "discriminator") {
props.discriminator = createNewProp(Number(user[key]));
return;
continue;
}
props[key] = createNewProp(
MEMBER_SNOWFLAKES.includes(key) ? (user[key] ? snowflakeToBigint(user[key] as string) : undefined) : user[key]
);
});
}
const member: DiscordenoMember = Object.create(baseMember, {
...props,
/** The guild related data mapped by guild id */
guilds: createNewProp(new Collection<bigint, GuildMember>()),
bitfield: createNewProp(bitfield),
cachedAt: createNewProp(Date.now()),
});
/** The guild related data mapped by guild id */
// @ts-ignore allow this prop to be required
if (!cache.requiredStructureProperties.members.size || cache.requiredStructureProperties.members.has("guilds"))
props.guilds = createNewProp(new Collection<bigint, GuildMember>());
// @ts-ignore allow this prop to be required
if (!cache.requiredStructureProperties.members.size || cache.requiredStructureProperties.members.has("bitfield"))
props.bitfield = createNewProp(bitfield);
// @ts-ignore allow this prop to be required
if (!cache.requiredStructureProperties.members.size || cache.requiredStructureProperties.members.has("cachedAt"))
props.cachedAt = createNewProp(Date.now());
const member: DiscordenoMember = Object.create(baseMember, props);
const cached = await cacheHandlers.get("members", snowflakeToBigint(user.id));
if (cached) {
@@ -251,7 +260,7 @@ export interface DiscordenoMember extends Omit<User, "discriminator" | "id" | "a
}
): ReturnType<typeof editMember>;
/** Ban a member in a guild */
ban(guildId: bigint, options: CreateGuildBan): ReturnType<typeof banMember>;
ban(guildId: bigint, options?: CreateGuildBan): ReturnType<typeof banMember>;
/** Add a role to the member */
addRole(guildId: bigint, roleId: bigint, reason?: string): ReturnType<typeof addRole>;
/** Remove a role from the member */

View File

@@ -116,16 +116,16 @@ const baseMessage: Partial<DiscordenoMessage> = {
async alert(content, timeout = 10, reason = "") {
if (this.guildId) {
return await sendMessage(this.channelId!, content).then((response) => {
response.delete(reason, timeout * 1000).catch(console.error);
response?.delete(reason, timeout * 1000).catch(console.error);
});
}
return await sendDirectMessage(this.authorId!, content).then((response) => {
response.delete(reason, timeout * 1000).catch(console.error);
response?.delete(reason, timeout * 1000).catch(console.error);
});
},
async alertReply(content, timeout = 10, reason = "") {
return await this.reply!(content).then((response) => response.delete(reason, timeout * 1000).catch(console.error));
return await this.reply!(content).then((response) => response?.delete(reason, timeout * 1000).catch(console.error));
},
removeAllReactions() {
return removeAllReactions(this.channelId!, this.id!);
@@ -213,29 +213,41 @@ export async function createDiscordenoMessage(data: Message) {
let bitfield = 0n;
const props: Record<string, ReturnType<typeof createNewProp>> = {};
(Object.keys(rest) as (keyof typeof rest)[]).forEach((key) => {
const requiredPropsSize = cache.requiredStructureProperties.messages.size;
for (const key of Object.keys(rest) as (keyof typeof rest)[]) {
eventHandlers.debug?.("loop", `Running for of loop in createDiscordenoMessage function.`);
// If empty all are allowed, otherwise check if this prop is allowed
if (requiredPropsSize && !cache.requiredStructureProperties.messages.has(key)) continue;
const toggleBits = messageToggles[key as keyof typeof messageToggles];
if (toggleBits) {
bitfield |= rest[key] ? toggleBits : 0n;
return;
continue;
}
// Don't add member to props since it would overwrite the message.member getter
// thread should not be cached on a message
if (["member", "thread"].includes(key)) return;
if (["member", "thread"].includes(key)) continue;
props[key] = createNewProp(
MESSAGE_SNOWFLAKES.includes(key) ? (rest[key] ? snowflakeToBigint(rest[key] as string) : undefined) : rest[key]
);
});
}
if (rest.thread) await cacheHandlers.set("threads", snowflakeToBigint(data.id), channelToThread(rest.thread));
props.authorId = createNewProp(snowflakeToBigint(author.id));
props.isBot = createNewProp(author.bot || false);
props.tag = createNewProp(`${author.username}#${author.discriminator.toString().padStart(4, "0")}`);
// @ts-ignore allow this prop
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("authorId"))
props.authorId = createNewProp(snowflakeToBigint(author.id));
// @ts-ignore allow this prop
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("isBot"))
props.isBot = createNewProp(author.bot || false);
// @ts-ignore allow this prop
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("tag"))
props.tag = createNewProp(`${author.username}#${author.discriminator.toString().padStart(4, "0")}`);
// Discord doesnt give guild id for getMessage() so this will fill it in
const guildIdFinal =
@@ -243,13 +255,28 @@ export async function createDiscordenoMessage(data: Message) {
(await cacheHandlers.get("channels", snowflakeToBigint(data.channelId)))?.guildId ||
0n;
const message: DiscordenoMessage = Object.create(baseMessage, {
...props,
content: createNewProp(data.content || ""),
guildId: createNewProp(guildIdFinal),
mentionedUserIds: createNewProp(mentions.map((m) => snowflakeToBigint(m.id))),
mentionedRoleIds: createNewProp(mentionRoles.map((id) => snowflakeToBigint(id))),
mentionedChannelIds: createNewProp([
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("content"))
props.content = createNewProp(data.content || "");
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("guildId"))
props.guildId = createNewProp(guildIdFinal);
if (
!requiredPropsSize ||
// @ts-ignore allow this prop
cache.requiredStructureProperties.messages.has("mentionedUserIds")
)
props.mentionedUserIds = createNewProp(mentions.map((m) => snowflakeToBigint(m.id)));
if (
!requiredPropsSize ||
// @ts-ignore allow this prop
cache.requiredStructureProperties.messages.has("mentionedRoleIds")
)
props.mentionedRoleIds = createNewProp(mentionRoles.map((id) => snowflakeToBigint(id)));
if (
!requiredPropsSize ||
// @ts-ignore allow this prop
cache.requiredStructureProperties.messages.has("mentionedChannelIds")
)
props.mentionedChannelIds = createNewProp([
// Keep any ids that discord sends
...mentionChannels.map((m) => snowflakeToBigint(m.id)),
// Add any other ids that can be validated in a channel mention format
@@ -257,10 +284,13 @@ export async function createDiscordenoMessage(data: Message) {
// converts the <#123> into 123
snowflakeToBigint(text.substring(2, text.length - 1))
),
]),
timestamp: createNewProp(Date.parse(data.timestamp)),
editedTimestamp: createNewProp(editedTimestamp ? Date.parse(editedTimestamp) : undefined),
messageReference: createNewProp(
]);
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("timestamp"))
props.timestamp = createNewProp(Date.parse(data.timestamp));
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("editedTimestamp"))
props.editedTimestamp = createNewProp(editedTimestamp ? Date.parse(editedTimestamp) : undefined);
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("messageReference"))
props.messageReference = createNewProp(
messageReference
? {
messageId: messageReference.messageId ? snowflakeToBigint(messageReference.messageId) : undefined,
@@ -268,11 +298,12 @@ export async function createDiscordenoMessage(data: Message) {
guildId: messageReference.guildId ? snowflakeToBigint(messageReference.guildId) : undefined,
}
: undefined
),
bitfield: createNewProp(bitfield),
});
);
// @ts-ignore allow this prop
if (!requiredPropsSize || cache.requiredStructureProperties.messages.has("bitfield"))
props.bitfield = createNewProp(bitfield);
return message;
return Object.create(baseMessage, props) as DiscordenoMessage;
}
export interface DiscordenoMessage

View File

@@ -115,30 +115,36 @@ export async function createDiscordenoRole(
let bitfield = 0n;
const props: Record<string, ReturnType<typeof createNewProp>> = {};
(Object.keys(rest) as (keyof typeof rest)[]).forEach((key) => {
for (const key of Object.keys(rest) as (keyof typeof rest)[]) {
eventHandlers.debug?.("loop", `Running for of loop in createDiscordenoRole function.`);
const toggleBits = roleToggles[key as keyof typeof roleToggles];
if (toggleBits) {
bitfield |= rest[key] ? toggleBits : 0n;
return;
continue;
}
props[key] = createNewProp(
ROLE_SNOWFLAKES.includes(key) ? (rest[key] ? snowflakeToBigint(rest[key] as string) : undefined) : rest[key]
);
});
}
const role: DiscordenoRole = Object.create(baseRole, {
...props,
permissions: createNewProp(BigInt(rest.permissions)),
botId: createNewProp(tags.botId ? snowflakeToBigint(tags.botId) : undefined),
isNitroBoostRole: createNewProp("premiumSubscriber" in tags),
integrationId: createNewProp(tags.integrationId ? snowflakeToBigint(tags.integrationId) : undefined),
bitfield: createNewProp(bitfield),
});
if (!cache.requiredStructureProperties.roles.size || cache.requiredStructureProperties.roles.has("permissions"))
props.permissions = createNewProp(BigInt(rest.permissions));
// @ts-ignore allow this prop
if (!cache.requiredStructureProperties.roles.size || cache.requiredStructureProperties.roles.has("botId"))
props.botId = createNewProp(tags.botId ? snowflakeToBigint(tags.botId) : undefined);
// @ts-ignore allow this prop
if (!cache.requiredStructureProperties.roles.size || cache.requiredStructureProperties.roles.has("isNitroBoostRole"))
props.isNitroBoostRole = createNewProp("premiumSubscriber" in tags);
// @ts-ignore allow this prop
if (!cache.requiredStructureProperties.roles.size || cache.requiredStructureProperties.roles.has("integrationId"))
props.integrationId = createNewProp(tags.integrationId ? snowflakeToBigint(tags.integrationId) : undefined);
// @ts-ignore allow this prop
if (!cache.requiredStructureProperties.roles.size || cache.requiredStructureProperties.roles.has("bitfield"))
props.bitfield = createNewProp(bitfield);
return role;
return Object.create(baseRole, props) as DiscordenoRole;
}
export interface DiscordenoRole extends Omit<Role, "tags" | "id" | "permissions"> {

View File

@@ -81,16 +81,20 @@ export async function createDiscordenoVoiceState(guildId: bigint, data: VoiceSta
let bitfield = 0n;
const props: Record<string, ReturnType<typeof createNewProp>> = {};
(Object.keys(data) as (keyof typeof data)[]).forEach((key) => {
for (const key of Object.keys(data) as (keyof typeof data)[]) {
eventHandlers.debug?.("loop", `Running for of loop in createDiscordenoVoiceState function.`);
// if is empty allow all, otherwise check if prop is required
if (cache.requiredStructureProperties.voiceStates.size && !cache.requiredStructureProperties.voiceStates.has(key))
continue;
// We don't need to cache member twice. It will be in cache.members
if (key === "member") return;
if (key === "member") continue;
const toggleBits = voiceStateToggles[key as keyof typeof voiceStateToggles];
if (toggleBits) {
bitfield |= data[key] ? toggleBits : 0n;
return;
continue;
}
props[key] = createNewProp(
@@ -100,15 +104,21 @@ export async function createDiscordenoVoiceState(guildId: bigint, data: VoiceSta
: undefined
: data[key]
);
});
}
const voiceState: DiscordenoVoiceState = Object.create(baseRole, {
...props,
guildId: createNewProp(guildId),
bitfield: createNewProp(bitfield),
});
if (
!cache.requiredStructureProperties.voiceStates.size ||
cache.requiredStructureProperties.voiceStates.has("guildId")
)
props.guildId = createNewProp(guildId);
if (
!cache.requiredStructureProperties.voiceStates.size ||
// @ts-ignore allow this prop
cache.requiredStructureProperties.voiceStates.has("bitfield")
)
props.bitfield = createNewProp(bitfield);
return voiceState;
return Object.create(baseRole, props) as DiscordenoVoiceState;
}
export interface DiscordenoVoiceState extends Omit<VoiceState, "channelId" | "guildId" | "userId" | "member"> {

View File

@@ -33,6 +33,7 @@ export enum DiscordJsonErrorCodes {
UnknownInteraction = 10062,
UnknownApplicationCommand = 10063,
UnknownApplicationCommandPermissions = 10066,
UnknownGuildMemberVerificationForm = 10068,
BotsCannotUseThisEndpoint = 20001,
OnlyBotsCanUseThisEndpoint,
ExplicitContentCannotBeSentToTheDesiredRecipient = 20009,
@@ -42,6 +43,7 @@ export enum DiscordJsonErrorCodes {
ThisMessageCannotBeEditedDueToAnnouncementRateLimits = 20022,
TheChannelYouAreWritingHasHitTheWriteRateLimit = 20028,
YourStageTopicContainsWordsThatAreNotAllowedForPublicStages = 20031,
GuildPremiumSubscriptionLevelTooLow = 20035,
MaximumNumberOfGuildsReached = 30001,
MaximumNumberOfFriendsReached,
MaximumNumberOfPinsReachedForTheChannel,

View File

@@ -4,9 +4,9 @@ import { ApplicationCommandInteractionDataResolved } from "./application_command
/** https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata */
export interface ApplicationCommandInteractionData {
/** The Id of the invoked command */
id?: string;
id: string;
/** The name of the invoked command */
name?: string;
name: string;
/** Converted users + roles + channels */
resolved?: ApplicationCommandInteractionDataResolved;
/** The params + values from the user */

View File

@@ -10,11 +10,6 @@ export interface CreateMessage {
content?: string;
/** true if this is a TTS message */
tts?: boolean;
// TODO: v12 remove
/** Embedded `rich` content
* @deprecated will be removed in Discordeno v12 use embeds
*/
embed?: Embed;
/** Embedded `rich` content (up to 6000 characters) */
embeds?: Embed[];
/** Allowed mentions for the message */

View File

@@ -8,11 +8,6 @@ import { MessageComponents } from "./components/message_components.ts";
export interface EditMessage {
/** The new message contents (up to 2000 characters) */
content?: string | null;
// TODO: v12 remove
/** Embedded `rich` content
* @deprecated will be removed in Discordeno v12 use embeds
*/
embed?: Embed | null;
/** Embedded `rich` content (up to 6000 characters) */
embeds?: Embed[] | null;
/** Edit the flags of the message (only `SUPRESS_EMBEDS` can currently be set/unset) */

View File

@@ -13,6 +13,7 @@ import { MessageReference } from "./message_reference.ts";
import { MessageSticker } from "./message_sticker.ts";
import { DiscordMessageTypes } from "./message_types.ts";
import { Reaction } from "./reaction.ts";
import { MessageStickerItem } from "./message_sticker_item.ts";
/** https://discord.com/developers/docs/resources/channel#message-object */
export interface Message {
@@ -93,5 +94,7 @@ export interface Message {
/** The thread that was started from this message, includes thread member object */
thread?: Omit<Channel, "member"> & { member: ThreadMember };
/** The components related to this message */
components: MessageComponents;
components?: MessageComponents;
/** Sent if the message contains stickers */
stickerItems?: MessageStickerItem[];
}

View File

@@ -5,7 +5,7 @@ export enum DiscordMessageFlags {
/** This message originated from a message in another channel (via Channel Following) */
IsCrosspost = 1 << 1,
/** Do not include any embeds when serializing this message */
SuppressEmbeeds = 1 << 2,
SuppressEmbeds = 1 << 2,
/** The source message for this crosspost has been deleted (via Channel Following) */
SourceMessageDeleted = 1 << 3,
/** This message came from the urgent message system */

View File

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

View File

@@ -1,3 +1,4 @@
import { cache } from "../../cache.ts";
import { Channel } from "../../types/channels/channel.ts";
import { DiscordChannelTypes } from "../../types/channels/channel_types.ts";
import { ThreadMemberModified } from "../../types/channels/threads/thread_member.ts";
@@ -25,6 +26,9 @@ const baseThread: Partial<DiscordenoThread> = {
get isPublic() {
return !this.isPrivate;
},
get guildId() {
return cache.channels.get(this.parentId!)!.guildId;
},
toJSON() {
return {
id: this.id?.toString(),
@@ -98,6 +102,7 @@ export interface DiscordenoThread {
isPrivate: boolean;
isPublic: boolean;
botIsMember: boolean;
guildId: bigint;
members: Collection<bigint, Omit<ThreadMemberModified, "id">>;
toJSON(): Thread;
}

View File

@@ -1,3 +1,3 @@
export * from "./channel_to_thread.ts";
export * from "./thread_member_modified.ts";
export * from "./thread_members_update_modified.ts"
export * from "./thread_members_update_modified.ts";