Merge branch 'main' into more-thread-stuff

This commit is contained in:
Skillz4Killz
2021-06-18 10:37:23 -04:00
committed by GitHub
24 changed files with 140 additions and 27 deletions

View File

@@ -30,7 +30,7 @@ export const cache = {
executedSlashCommands: new Set<string>(),
get emojis() {
return new Collection<bigint, Emoji>(
this.guilds.reduce((a, b) => [...a, ...b.emojis.map((e) => [e.id, e])], [] as any[])
this.guilds.reduce((a, b) => [...a, ...b.emojis.map((e, id) => [id, e])], [] as any[])
);
},
activeGuildIds: new Set<bigint>(),

View File

@@ -6,7 +6,8 @@ import { snowflakeToBigint } from "../../util/bigint.ts";
export async function handleThreadMemberUpdate(data: DiscordGatewayPayload) {
const payload = data.d as ThreadMember;
const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id));
// The id field is omitted from the thread member dispatched within the GUILD_CREATE gateway event.
const thread = await cacheHandlers.get("threads", snowflakeToBigint(payload.id!));
if (!thread) return;
thread.botIsMember = true;

View File

@@ -5,13 +5,18 @@ import type { DiscordenoInteractionResponse } from "../../types/discordeno/inter
import { endpoints } from "../../util/constants.ts";
import { snakelize, validateComponents } from "../../util/utils.ts";
// TODO: v12 remove | string
/**
* Send a response to a users slash command. The command data will have the id and token necessary to respond.
* Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages.
*
* NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object.
*/
export async function sendInteractionResponse(id: bigint, token: string, options: DiscordenoInteractionResponse) {
export async function sendInteractionResponse(
id: bigint | string,
token: string,
options: DiscordenoInteractionResponse
) {
// TODO: add more options validations
if (options.data?.components) validateComponents(options.data?.components);
@@ -37,5 +42,9 @@ export async function sendInteractionResponse(id: bigint, token: string, options
cache.executedSlashCommands.delete(token);
}, 900000);
return await rest.runMethod("post", endpoints.INTERACTION_ID_TOKEN(id, token), snakelize(options));
return await rest.runMethod(
"post",
endpoints.INTERACTION_ID_TOKEN(typeof id === "bigint" ? id : BigInt(id), token),
snakelize(options)
);
}

View File

@@ -29,6 +29,13 @@ 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) {
throw new Error(Errors.MESSAGE_MAX_LENGTH);
}

View File

@@ -34,7 +34,16 @@ 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");
if (content.embed) requiredPerms.add("EMBED_LINKS");
// 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);
}
if (content.messageReference?.messageId || content.allowedMentions?.repliedUser) {
requiredPerms.add("READ_MESSAGE_HISTORY");
}

View File

@@ -7,9 +7,9 @@ import { urlToBase64 } from "../../util/utils.ts";
/** Modifies the bot's username or avatar.
* NOTE: username: if changed may cause the bot's discriminator to be randomized.
*/
export async function editBotProfile(options: { username?: string; botAvatarURL?: string }) {
export async function editBotProfile(options: { username?: string; botAvatarURL?: string | null }) {
// Nothing was edited
if (!options.username && !options.botAvatarURL) return;
if (!options.username && options.botAvatarURL === undefined) return;
// Check username requirements if username was provided
if (options.username) {
if (options.username.length > 32) {
@@ -26,7 +26,7 @@ export async function editBotProfile(options: { username?: string; botAvatarURL?
}
}
const avatar = options?.botAvatarURL ? await urlToBase64(options?.botAvatarURL) : undefined;
const avatar = options?.botAvatarURL ? await urlToBase64(options?.botAvatarURL) : options?.botAvatarURL;
return await rest.runMethod<User>("patch", endpoints.USER_BOT, {
username: options.username?.trim(),

View File

@@ -133,6 +133,8 @@ import { createStageInstance } from "./channels/create_stage_instance.ts";
import { updateStageInstance } from "./channels/update_stage_instance.ts";
import { getStageInstance } from "./channels/get_stage_instance.ts";
import { deleteStageInstance } from "./channels/delete_stage_instance.ts";
import { isSlashCommand } from "./type_guards/is_slash_command.ts";
import { connectToVoiceChannel } from "./voice/connect_to_voice_channel.ts";
export {
addDiscoverySubcategory,
@@ -145,6 +147,7 @@ export {
batchEditSlashCommandPermissions,
categoryChildren,
channelOverwriteHasPermission,
connectToVoiceChannel,
createChannel,
createEmoji,
createGuild,
@@ -242,6 +245,7 @@ export {
guildSplashURL,
isButton,
isSelectMenu,
isSlashCommand,
isChannelSynced,
kick,
kickMember,
@@ -405,6 +409,8 @@ export let helpers = {
getGuildTemplates,
getTemplate,
syncGuildTemplate,
// voice
connectToVoiceChannel,
// webhooks
createWebhook,
deleteWebhookMessage,

View File

@@ -0,0 +1,7 @@
import { Interaction, SlashCommandInteraction } from "../../types/interactions/interaction.ts";
import { DiscordInteractionTypes } from "../../types/interactions/interaction_types.ts";
/** A type guard function to tell if it is a slash command interaction */
export function isSlashCommand(interaction: Interaction): interaction is SlashCommandInteraction {
return interaction.type === DiscordInteractionTypes.ApplicationCommand;
}

View File

@@ -0,0 +1,21 @@
import { DiscordGatewayOpcodes } from "../../types/codes/gateway_opcodes.ts";
import type { UpdateVoiceState } from "../../types/voice/update_voice_state.ts";
import { requireBotChannelPermissions } from "../../util/permissions.ts";
import { calculateShardId } from "../../util/calculate_shard_id.ts";
import { snakelize } from "../../util/utils.ts";
import { ws } from "../../ws/ws.ts";
import type { AtLeastOne } from "../../types/util.ts";
/** Connect or join a voice channel inside a guild. By default, the "selfDeaf" option is true. Requires `CONNECT` and `VIEW_CHANNEL` permissions. */
export async function connectToVoiceChannel(
guildId: bigint,
channelId: bigint,
options?: AtLeastOne<Omit<UpdateVoiceState, "guildId" | "channelId">>
) {
await requireBotChannelPermissions(channelId, ["CONNECT", "VIEW_CHANNEL"]);
ws.sendShardMessage(calculateShardId(guildId), {
op: DiscordGatewayOpcodes.VoiceStateUpdate,
d: snakelize<UpdateVoiceState>({guildId, channelId, selfMute: Boolean(options?.selfMute), selfDeaf: options.selfDeaf ?? true }),
});
}

View File

@@ -17,9 +17,7 @@ export async function sendWebhook(webhookId: bigint, webhookToken: string, optio
throw Error(Errors.MESSAGE_MAX_LENGTH);
}
if (options.embeds && options.embeds.length > 10) {
options.embeds.splice(10);
}
options.embeds?.splice(10);
if (options.allowedMentions) {
if (options.allowedMentions.users?.length) {

View File

@@ -46,7 +46,7 @@ const baseMember: Partial<DiscordenoMember> = {
return `<@!${this.id!}>`;
},
get tag() {
return `${this.username!}#${this.discriminator!}`;
return `${this.username!}#${this.discriminator!.toString().padStart(4, "0")}`;
},
// METHODS
@@ -232,7 +232,9 @@ export interface DiscordenoMember extends Omit<User, "discriminator" | "id" | "a
/** Get the nickname or the username if no nickname */
name(guildId: bigint): string;
/** Get the guild member object for the specified guild */
guildMember(guildId: bigint):
guildMember(
guildId: bigint
):
| (Omit<GuildMember, "joinedAt" | "premiumSince" | "roles"> & {
joinedAt?: number;
premiumSince?: number;

View File

@@ -228,7 +228,7 @@ export async function createDiscordenoMessage(data: Message) {
props.authorId = createNewProp(snowflakeToBigint(author.id));
props.isBot = createNewProp(author.bot || false);
props.tag = createNewProp(`${author.username}#${author.discriminator}`);
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 =

View File

@@ -56,6 +56,6 @@ export interface Channel {
threadMetadata?: ThreadMetadata;
/** Thread member object for the current user, if they have joined the thread, only included on certain API endpoints */
member?: ThreadMember;
/** the default duration for newly created threads in the channel, in minutes, to automatically archive the thread after recent activity */
defaultAutoArchiveDuration: number;
/** Default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */
defaultAutoArchiveDuration?: number;
}

View File

@@ -5,9 +5,9 @@ export interface ThreadMemberBase {
export interface ThreadMember extends ThreadMemberBase {
/** The id of the thread */
id: string;
id?: string;
/** The id of the user */
userId: string;
userId?: string;
/** The time the current user last joined the thread */
joinTimestamp: string;
}

View File

@@ -53,6 +53,8 @@ export enum DiscordJsonErrorCodes {
MaximumNumberOfGuildChannelsReached = 30013,
MaximumNumberOfAttachmentsInAMessageReached = 30015,
MaximumNumberOfInvitesReached,
MaximumNumberOfAnimatedEmojisReached = 30018,
MaximumNumberOfServerMembersReached,
MaximumNumberOfGuildDiscoverySubcategoriesHasBeenReached = 30030,
GuildAlreadyHasTemplate = 30031,
MaximumNumberOfBansForNonGuildMembersHaveBeenExceeded = 30035,
@@ -106,6 +108,10 @@ export enum DiscordJsonErrorCodes {
NoUsersWithDiscordTagExist = 80004,
ReqctionWasBlocked = 90001,
ApiResourceIsCurrentlyOverloadedTryAgainALittleLater = 130000,
AThreadHasAlreadyBeenCreatedForThisMessage = 160004,
ThreadIsLocked = 160005,
MaximumNumberOfActiveThreadsReached = 160006,
MaximumNumberOfActiveAnnouncementThreadsReached = 160007,
}
export type JsonErrrorCodes = DiscordJsonErrorCodes;

View File

@@ -3,6 +3,7 @@ import { Guild } from "../guilds/guild.ts";
import { Application } from "../applications/application.ts";
import { User } from "../users/user.ts";
import { DiscordTargetTypes } from "./target_types.ts";
import { InviteStageInstance } from "./invite_stage_instance.ts";
/** https://discord.com/developers/docs/resources/invite#invite-object */
export interface Invite {
@@ -26,4 +27,6 @@ export interface Invite {
approximateMemberCount?: number;
/** The expiration date of this invite, returned from the `GET /invites/<code>` endpoint when `with_expiration` is `true` */
expiresAt?: string | null;
/** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */
stageInstance?: InviteStageInstance;
}

View File

@@ -0,0 +1,12 @@
import { GuildMember } from "../members/guild_member.ts";
export interface InviteStageInstance {
/** The members speaking in the Stage */
members: Partial<GuildMember>[];
/** The number of users in the Stage */
participantCount: number;
/** The number of users speaking in the Stage */
speakerCount: number;
/** The topic of the Stage instance (1-120 characters) */
topic: string;
}

View File

@@ -10,8 +10,13 @@ export interface CreateMessage {
content?: string;
/** true if this is a TTS message */
tts?: boolean;
/** Embedded `rich` content */
// 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 */
allowedMentions?: AllowedMentions;
/** Include to make your message a reply */

View File

@@ -8,8 +8,13 @@ import { MessageComponents } from "./components/message_components.ts";
export interface EditMessage {
/** The new message contents (up to 2000 characters) */
content?: string | null;
/** Embedded `rich` content */
// 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) */
flags?: 4 | null;
/** The contents of the file being sent/edited */

View File

@@ -78,7 +78,10 @@ export interface Message {
messageReference?: Omit<MessageReference, "failIfNotExists">;
/** Message flags combined as a bitfield */
flags?: number;
/** The stickers sent with the message (bots currently can only receive messages with stickers, not send) */
/**
* The stickers sent with the message (bots currently can only receive messages with stickers, not send)
* @deprecated
*/
stickers?: MessageSticker[];
/**
* The message associated with the `message_reference`

View File

@@ -1,22 +1,32 @@
import { DiscordMessageStickerFormatTypes } from "./message_sticker_format_types.ts";
import type { User } from "../users/user.ts";
/** https://discord.com/developers/docs/resources/channel#message-object-message-sticker-structure */
export interface MessageSticker {
/** id of the sticker */
/** Id of the sticker */
id: string;
/** id of the pack the sticker is from */
packId: string;
/** Id of the pack the sticker is from */
packId?: string;
/** Name of the sticker */
name: string;
/** Description of the sticker */
description: string;
/** A comma-separated list of tags for the sticker */
tags?: string;
/** For guild stickers, a unicode emoji representing the sticker's expression. For Nitro stickers, a comma-separated list of related expressions */
tags: string;
/**
* Sticker asset hash
* Note: The URL for fetching sticker assets is currently private.
* @deprecated the value of the asset field will an empty string.
*/
asset: string;
/** Type of sticker format */
formatType: DiscordMessageStickerFormatTypes;
/** Whether or not the sticker is available */
available?: boolean;
/** Id of the guild that owns this sticker */
guildId?: string;
/** The user that uploaded the sticker */
user?: User;
/** A sticker's sort order within a pack */
sortValue?: number;
}

View File

@@ -149,3 +149,5 @@ export type CamelCasedPropertiesDeep<Value> = Value extends Function
: {
[K in keyof Value as CamelCase<K>]: CamelCasedPropertiesDeep<Value[K]>;
};
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];

View File

@@ -0,0 +1,7 @@
import { ws } from "../ws/ws.ts";
export function calculateShardId(guildId: bigint) {
if (ws.maxShards === 1) return 0;
return Number((guildId >> 22n) % BigInt(ws.maxShards - 1));
}

View File

@@ -9,7 +9,7 @@ export const GATEWAY_VERSION = 9;
// TODO: update this version
/** https://github.com/discordeno/discordeno/releases */
export const DISCORDENO_VERSION = "11.0.3";
export const DISCORDENO_VERSION = "11.2.0";
/** https://discord.com/developers/docs/reference#user-agent */
export const USER_AGENT = `DiscordBot (https://github.com/discordeno/discordeno, v${DISCORDENO_VERSION})`;