diff --git a/esm.mjs b/esm.mjs index d52e81806..d3482d4f0 100644 --- a/esm.mjs +++ b/esm.mjs @@ -32,18 +32,25 @@ export const { Member, Message, NewsChannel, + NewsThreadChannel, Permission, PermissionOverwrite, PingInteraction, PrivateChannel, + PrivateThreadChannel, + PublicThreadChannel, Relationship, RequestHandler, Role, SequentialBucket, Shard, SharedStream, + StageChannel, + StageInstance, StoreChannel, TextChannel, + ThreadChannel, + ThreadMember, UnavailableGuild, UnknownInteraction, User, diff --git a/index.d.ts b/index.d.ts index bed991d39..ee7eae48a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,13 +21,15 @@ declare namespace Eris { // Channel type AnyChannel = AnyGuildChannel | PrivateChannel; type AnyGuildChannel = GuildTextableChannel | AnyVoiceChannel | CategoryChannel | StoreChannel; + type AnyThreadChannel = NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | ThreadChannel; type AnyVoiceChannel = VoiceChannel | StageChannel; type ChannelTypes = Constants["ChannelTypes"][keyof Constants["ChannelTypes"]]; type GuildTextableChannel = TextChannel | NewsChannel; - type InviteChannel = InvitePartialChannel | Exclude; + type GuildTextableWithThread = GuildTextableChannel | AnyThreadChannel; + type InviteChannel = InvitePartialChannel | Exclude; type PossiblyUncachedTextable = Textable | Uncached; type PossiblyUncachedTextableChannel = TextableChannel | Uncached; - type TextableChannel = (GuildTextable & GuildTextableChannel) | (Textable & PrivateChannel); + type TextableChannel = (GuildTextable & GuildTextableChannel) | (ThreadTextable & AnyThreadChannel) | (Textable & PrivateChannel); type VideoQualityMode = 1 | 2; // Command @@ -59,6 +61,7 @@ declare namespace Eris { type Component = ActionRow | ActionRowComponents; type ImageFormat = "jpg" | "jpeg" | "png" | "gif" | "webp"; type MessageContent = string | AdvancedMessageContent; + type MessageContentEdit = string | AdvancedMessageContentEdit; type MFALevel = 0 | 1; type PossiblyUncachedMessage = Message | { channel: TextableChannel | { id: string; guild?: Uncached }; guildID?: string; id: string }; @@ -212,8 +215,12 @@ declare namespace Eris { type FriendSuggestionReasons = { name: string; platform_type: string; type: number }[]; type Status = "online" | "idle" | "dnd" | "offline"; + // Thread + type AutoArchiveDuration = 60 | 1440 | 4320 | 10080; + // Voice type ConverterCommand = "./ffmpeg" | "./avconv" | "ffmpeg" | "avconv"; + type StageInstancePrivacyLevel = 1 | 2; // Webhook type MessageWebhookContent = Pick; @@ -252,7 +259,12 @@ declare namespace Eris { userLimit?: number; } interface EditChannelOptions extends Omit { + archived?: boolean; + autoArchiveDuration?: AutoArchiveDuration; + defaultAutoArchiveDuration?: AutoArchiveDuration; icon?: string; + invitable?: boolean; + locked?: boolean; name?: string; ownerID?: string; rtcRegion?: string | null; @@ -275,7 +287,7 @@ declare namespace Eris { createWebhook(options: { name: string; avatar?: string | null }, reason?: string): Promise; deleteMessages(messageIDs: string[], reason?: string): Promise; getWebhooks(): Promise; - purge(limit: number, filter?: (message: Message) => boolean, before?: string, after?: string, reason?: string): Promise; + purge(options: PurgeChannelOptions): Promise; removeMessageReactionEmoji(messageID: string, reaction: string): Promise; removeMessageReactions(messageID: string): Promise; sendTyping(): Promise; @@ -307,7 +319,7 @@ declare namespace Eris { addMessageReaction(messageID: string, reaction: string, userID: string): Promise; createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise; deleteMessage(messageID: string, reason?: string): Promise; - editMessage(messageID: string, content: MessageContent): Promise; + editMessage(messageID: string, content: MessageContentEdit): Promise; getMessage(messageID: string): Promise; getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; /** @deprecated */ @@ -322,6 +334,16 @@ declare namespace Eris { unpinMessage(messageID: string): Promise; unsendMessage(messageID: string): Promise; } + // @ts-ignore ts(2430) - ThreadTextable can't properly extend Textable because of getMessageReaction deprecated overload + interface ThreadTextable extends Textable { + lastPinTimestamp?: number; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + getMessage(messageID: string): Promise>; + getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; + getMessages(options?: GetMessagesOptions): Promise[]>; + getPins(): Promise[]>; + } interface WebhookData { channelID: string; guildID: string; @@ -559,7 +581,6 @@ declare namespace Eris { premiumSubscriptionCount?: number; premiumTier: PremiumTier; publicUpdatesChannelID: string | null; - region: string; rulesChannelID: string | null; splash: string | null; stickers?: Sticker[]; @@ -624,6 +645,19 @@ declare namespace Eris { position: number; unicodeEmoji: string | null; } + interface OldStageInstance { + discoverableDisabled: boolean; + privacyLevel: StageInstancePrivacyLevel; + topic: string; + } + interface OldThread { + name: string; + rateLimitPerUser: number; + threadMetadata: ThreadMetadata; + } + interface OldThreadMember { + flags: number; + } interface OldVoiceState { deaf: boolean; mute: boolean; @@ -686,6 +720,15 @@ declare namespace Eris { relationshipRemove: [relationship: Relationship]; relationshipUpdate: [relationship: Relationship, oldRelationship: { type: number }]; shardPreReady: [id: number]; + stageInstanceCreate: [stageInstance: StageInstance]; + stageInstanceDelete: [stageInstance: StageInstance]; + stageInstanceUpdate: [stageInstance: StageInstance, oldStageInstance: OldStageInstance | null]; + threadCreate: [channel: AnyThreadChannel]; + threadDelete: [channel: AnyThreadChannel]; + threadListSync: [guild: Guild, deletedThreads: (AnyThreadChannel | Uncached)[], activeThreads: AnyThreadChannel[], joinedThreadsMember: ThreadMember[]]; + threadMembersUpdate: [channel: AnyThreadChannel, addedMembers: ThreadMember[], removedMembers: (ThreadMember | Uncached)[]]; + threadMemberUpdate: [channel: AnyThreadChannel, member: ThreadMember, oldMember: OldThreadMember]; + threadUpdate: [channel: AnyThreadChannel, oldChannel: OldThread | null]; typingStart: [channel: GuildTextableChannel | Uncached, user: User | Uncached, member: Member] | [channel: PrivateChannel | Uncached, user: User | Uncached, member: null]; unavailableGuildCreate: [guild: UnavailableGuild]; @@ -770,7 +813,6 @@ declare namespace Eris { defaultNotifications?: DefaultNotifications; explicitContentFilter?: ExplicitContentFilter; icon?: string; - region?: string; roles?: PartialRole[]; systemChannelID: string; verificationLevel?: VerificationLevel; @@ -822,6 +864,7 @@ declare namespace Eris { interface GuildAuditLog { entries: GuildAuditLogEntry[]; integrations: GuildIntegration[]; + threads: AnyThreadChannel[]; users: User[]; webhooks: Webhook[]; } @@ -839,7 +882,6 @@ declare namespace Eris { ownerID?: string; preferredLocale?: string; publicUpdatesChannelID?: string; - region?: string; rulesChannelID?: string; splash?: string; systemChannelFlags?: number; @@ -936,6 +978,12 @@ declare namespace Eris { recipients?: { username: string }[]; type: Exclude; } + interface InviteStageInstance { + members: Member[]; + participantCount: number; + speakerCount: number; + topic: string; + } // Member/User interface FetchMembersOptions { @@ -1002,6 +1050,9 @@ declare namespace Eris { stickerIDs?: string[]; tts?: boolean; } + interface AdvancedMessageContentEdit extends AdvancedMessageContent { + file?: MessageFile | MessageFile[]; + } interface AllowedMentions { everyone?: boolean; repliedUser?: boolean; @@ -1199,6 +1250,36 @@ declare namespace Eris { premium_subscriber?: true; } + // Thread + interface CreateThreadOptions { + autoArchiveDuration: AutoArchiveDuration; + name: string; + } + interface CreateThreadWithoutMessageOptions extends CreateThreadOptions { + invitable: T extends PrivateThreadChannel["type"] ? boolean : never; + type: T; + } + interface GetArchivedThreadsOptions { + before?: Date; + limit?: number; + } + interface ListedChannelThreads extends ListedGuildThreads { + hasMore: boolean; + } + interface ListedGuildThreads { + members: ThreadMember[]; + threads: T[]; + } + interface PrivateThreadMetadata extends ThreadMetadata { + invitable: boolean; + } + interface ThreadMetadata { + archiveTimestamp: number; + archived: boolean; + autoArchiveDuration: AutoArchiveDuration; + locked: boolean; + } + // Voice interface JoinVoiceChannelOptions { opusOnly?: boolean; @@ -1206,6 +1287,10 @@ declare namespace Eris { selfMute?: boolean; shared?: boolean; } + interface StageInstanceOptions { + privacyLevel?: StageInstancePrivacyLevel; + topic?: string; + } interface UncachedMemberVoiceState { id: string; voiceState: OldVoiceState; @@ -1250,13 +1335,18 @@ declare namespace Eris { // Webhook interface Webhook { - avatar?: string; - channel_id: string; - guild_id: string; + application_id: string | null; + avatar: string | null; + channel_id: string | null; + guild_id: string | null; id: string; name: string; - token: string; - user: PartialUser; + source_channel?: { id: string; name: string }; + source_guild: { icon: string | null; id: string; name: string }; + token?: string; + type: 1 | 2 | 3; + url?: string; + user?: PartialUser; } interface WebhookOptions { avatar?: string; @@ -1272,6 +1362,7 @@ declare namespace Eris { embeds?: EmbedOptions[]; file?: FileContent | FileContent[]; flags?: number; + threadID?: string; tts?: boolean; username?: string; wait?: boolean; @@ -1361,6 +1452,10 @@ declare namespace Eris { STICKER_CREATE: 90; STICKER_UPDATE: 91; STICKER_DELETE: 92; + + THREAD_CREATE: 110; + THREAD_UPDATE: 111; + THREAD_DELETE: 112; }; ChannelTypes: { GUILD_TEXT: 0; @@ -1370,9 +1465,12 @@ declare namespace Eris { GUILD_CATEGORY: 4; GUILD_NEWS: 5; GUILD_STORE: 6; + GUILD_NEWS_THREAD: 10; + GUILD_PUBLIC_THREAD: 11; + GUILD_PRIVATE_THREAD: 12; GUILD_STAGE: 13; }; - GATEWAY_VERSION: 8; + GATEWAY_VERSION: 9; GatewayOPCodes: { EVENT: 0; HEARTBEAT: 1; @@ -1430,6 +1528,7 @@ declare namespace Eris { SUPPRESS_EMBEDS: 4; SOURCE_MESSAGE_DELETED: 8; URGENT: 16; + HAS_THREAD: 32; EPHEMERAL: 64; LOADING: 128; }; @@ -1452,9 +1551,10 @@ declare namespace Eris { GUILD_DISCOVERY_REQUALIFIED: 15; GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING: 16; GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: 17; + THREAD_CREATED: 18; REPLY: 19; CHAT_INPUT_COMMAND: 20; - + THREAD_STARTER_MESSAGE: 21; GUILD_INVITE_REMINDER: 22; CONTEXT_MENU_COMMAND: 23; }; @@ -1504,22 +1604,26 @@ declare namespace Eris { /** @deprecated */ useSlashCommands: 2147483648n; voiceRequestToSpeak: 4294967296n; + manageThreads: 17179869184n; + createPublicThreads: 34359738368n; + createPrivateThreads: 68719476736n; useExternalStickers: 137438953472n; + sendMessagesInThreads: 274877906944n; allGuild: 2080899262n; - allText: 140392266833n; + allText: 535529258065n; allVoice: 4629464849n; - all: 146028888063n; - }; - REST_VERSION: 8; - StickerTypes: { - STANDARD: 1; - GUILD: 2; + all: 541165879295n; }; + REST_VERSION: 9; StickerFormats: { PNG: 1; APNG: 2; LOTTIE: 3; }; + StickerTypes: { + STANDARD: 1; + GUILD: 2; + }; SystemJoinMessages: [ "%user% joined the party.", "%user% is here.", @@ -1697,6 +1801,7 @@ declare namespace Eris { id: string; constructor(id: string); static getCreatedAt(id: string): number; + static getDiscordEpoch(id: string): number; inspect(): this; toString(): string; toJSON(props?: string[]): JSONCache; @@ -1784,6 +1889,7 @@ declare namespace Eris { requestHandler: RequestHandler; shards: ShardManager; startTime: number; + threadGuildMap: { [s: string]: string }; unavailableGuilds: Collection; uptime: number; user: ExtendedUser; @@ -1927,6 +2033,9 @@ declare namespace Eris { createInteractionResponse(interactionID: string, interactionToken: string, options: InteractionOptions, file?: FileContent | FileContent[]): Promise; createMessage(channelID: string, content: MessageContent, file?: FileContent | FileContent[]): Promise; createRole(guildID: string, options?: RoleOptions | Role, reason?: string): Promise; + createStageInstance(channelID: string, options: StageInstanceOptions): Promise; + createThreadWithMessage(channelID: string, messageID: string, options: CreateThreadOptions): Promise; + createThreadWithoutMessage(channelID: string, options: CreateThreadWithoutMessageOptions): Promise; crosspostMessage(channelID: string, messageID: string): Promise; deleteChannel(channelID: string, reason?: string): Promise; deleteChannelPermission(channelID: string, overwriteID: string, reason?: string): Promise; @@ -1944,6 +2053,7 @@ declare namespace Eris { deleteRole(guildID: string, roleID: string, reason?: string): Promise; deleteSelfConnection(platform: string, id: string): Promise; deleteSelfPremiumSubscription(): Promise; + deleteStageInstance(channelID: string): Promise; deleteUserNote(userID: string): Promise; deleteWebhook(webhookID: string, token?: string, reason?: string): Promise; deleteWebhookMessage(webhookID: string, token: string, messageID: string): Promise; @@ -1983,7 +2093,7 @@ declare namespace Eris { editGuildVoiceState(guildID: string, options: VoiceStateOptions, userID?: string): Promise; editGuildWelcomeScreen(guildID: string, options: WelcomeScreenOptions): Promise; editGuildWidget(guildID: string, options: Widget): Promise; - editMessage(channelID: string, messageID: string, content: MessageContent): Promise; + editMessage(channelID: string, messageID: string, content: MessageContentEdit): Promise; /** @deprecated */ editNickname(guildID: string, nick: string, reason?: string): Promise; editRole(guildID: string, roleID: string, options: RoleOptions, reason?: string): Promise; // TODO not all options are available? @@ -1995,6 +2105,7 @@ declare namespace Eris { data: { friendSync: boolean; visibility: number } ): Promise; editSelfSettings(data: UserSettings): Promise; + editStageInstance(channelID: string, options: StageInstanceOptions): Promise; editStatus(status: Status, activities?: ActivityPartial[] | ActivityPartial): void; editStatus(activities?: ActivityPartial[] | ActivityPartial): void; editUserNote(userID: string, note: string): Promise; @@ -2016,11 +2127,16 @@ declare namespace Eris { secret: string, code: string ): Promise<{ backup_codes: { code: string; consumed: boolean }[]; token: string }>; - executeSlackWebhook(webhookID: string, token: string, options: Record & { auth?: boolean }): Promise; - executeSlackWebhook(webhookID: string, token: string, options: Record & { auth?: boolean; wait: true }): Promise>; + executeSlackWebhook(webhookID: string, token: string, options: Record & { auth?: boolean; threadID?: string }): Promise; + executeSlackWebhook(webhookID: string, token: string, options: Record & { auth?: boolean; threadID?: string; wait: true }): Promise>; executeWebhook(webhookID: string, token: string, options: WebhookPayload & { wait: true }): Promise>; executeWebhook(webhookID: string, token: string, options: WebhookPayload): Promise; followChannel(channelID: string, webhookChannelID: string): Promise; + getActiveGuildThreads(guildID: string): Promise; + /** @deprecated */ + getActiveThreads(channelID: string): Promise; + getArchivedThreads(channelID: string, type: "private", options?: GetArchivedThreadsOptions): Promise>; + getArchivedThreads(channelID: string, type: "public", options?: GetArchivedThreadsOptions): Promise>; getBotGateway(): Promise<{ session_start_limit: { max_concurrency: number; remaining: number; reset_after: number; total: number }; shards: number; url: string }>; getChannel(channelID: string): AnyChannel; getChannelInvites(channelID: string): Promise; @@ -2055,6 +2171,7 @@ declare namespace Eris { getGuildWidgetSettings(guildID: string): Promise; getInvite(inviteID: string, withCounts?: false): Promise>; getInvite(inviteID: string, withCounts: true): Promise>; + getJoinedPrivateArchivedThreads(channelID: string, options?: GetArchivedThreadsOptions): Promise>; getMessage(channelID: string, messageID: string): Promise; getMessageReaction(channelID: string, messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; /** @deprecated */ @@ -2118,13 +2235,17 @@ declare namespace Eris { status: number; }[]>; getSelfSettings(): Promise; + getStageInstance(channelID: string): Promise; + getThreadMembers(channelID: string): Promise; getUserProfile(userID: string): Promise; getVoiceRegions(guildID?: string): Promise; getWebhook(webhookID: string, token?: string): Promise; getWebhookMessage(webhookID: string, token: string, messageID: string): Promise>; + joinThread(channelID: string, userID?: string): Promise; joinVoiceChannel(channelID: string, options?: JoinVoiceChannelOptions): Promise; kickGuildMember(guildID: string, userID: string, reason?: string): Promise; leaveGuild(guildID: string): Promise; + leaveThread(channelID: string, userID?: string): Promise; leaveVoiceChannel(channelID: string): void; off(event: K, listener: (...args: ClientEvents[K]) => void): this; off(event: string, listener: (...args: any[]) => void): this; @@ -2322,15 +2443,16 @@ declare namespace Eris { primaryCategory?: DiscoveryCategory; primaryCategoryID?: number; publicUpdatesChannelID: string; - region: string; roles: Collection; rulesChannelID: string | null; shard: Shard; splash: string | null; splashURL: string | null; + stageInstances: Collection; stickers?: Sticker[]; systemChannelFlags: number; systemChannelID: string | null; + threads: Collection; unavailable: boolean; vanityURL: string | null; verificationLevel: VerificationLevel; @@ -2400,6 +2522,7 @@ declare namespace Eris { editWidget(options: Widget): Promise; fetchAllMembers(timeout?: number): Promise; fetchMembers(options?: FetchMembersOptions): Promise; + getActiveThreads(): Promise; getAuditLog(options?: GetGuildAuditLogOptions): Promise; /** @deprecated */ getAuditLogs(limit?: number, before?: string, actionType?: number, userID?: string): Promise; @@ -2684,6 +2807,7 @@ declare namespace Eris { maxUses: CT extends "withMetadata" ? number : null; memberCount: CT extends "withMetadata" | "withoutCount" ? null : number; presenceCount: CT extends "withMetadata" | "withoutCount" ? null : number; + stageInstance: CH extends StageChannel ? InviteStageInstance : null; temporary: CT extends "withMetadata" ? boolean : null; uses: CT extends "withMetadata" ? number : null; constructor(data: BaseData, client: Client); @@ -2706,7 +2830,7 @@ declare namespace Eris { game: Activity | null; guild: Guild; id: string; - joinedAt: number; + joinedAt: number | null; mention: string; nick: string | null; pending?: boolean; @@ -2732,6 +2856,7 @@ declare namespace Eris { export class Message extends Base { activity?: MessageActivity; application?: MessageApplication; + applicationID?: string; attachments: Attachment[]; author: User; channel: T; @@ -2745,11 +2870,11 @@ declare namespace Eris { editedTimestamp?: number; embeds: Embed[]; flags: number; - guildID: T extends GuildTextable ? string : undefined; + guildID: T extends GuildTextableWithThread ? string : undefined; id: string; interaction: MessageInteraction | null; jumpLink: string; - member: T extends GuildTextable ? Member : null; + member: T extends GuildTextableWithThread ? Member : null; mentionEveryone: boolean; mentions: User[]; messageReference: MessageReference | null; @@ -2764,11 +2889,12 @@ declare namespace Eris { timestamp: number; tts: boolean; type: number; - webhookID: T extends GuildTextable ? string | undefined : undefined; + webhookID: T extends GuildTextableWithThread ? string | undefined : undefined; constructor(data: BaseData, client: Client); addReaction(reaction: string): Promise; /** @deprecated */ addReaction(reaction: string, userID: string): Promise; + createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; crosspost(): Promise : never>; delete(reason?: string): Promise; deleteWebhook(token: string): Promise; @@ -2789,9 +2915,10 @@ declare namespace Eris { rateLimitPerUser: 0; type: 5; createInvite(options?: CreateInviteOptions, reason?: string): Promise>; - createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; crosspostMessage(messageID: string): Promise>; - editMessage(messageID: string, content: MessageContent): Promise>; + editMessage(messageID: string, content: MessageContentEdit): Promise>; follow(webhookChannelID: string): Promise; getInvites(): Promise<(Invite<"withMetadata", NewsChannel>)[]>; getMessage(messageID: string): Promise>; @@ -2801,6 +2928,16 @@ declare namespace Eris { getPins(): Promise[]>; } + export class NewsThreadChannel extends ThreadChannel { + type: 10; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + edit(options: Pick, reason?: string): Promise; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + getMessage(messageID: string): Promise>; + getMessages(options?: GetMessagesOptions): Promise[]>; + getPins(): Promise[]>; + } + export class Permission extends Base { allow: bigint; deny: bigint; @@ -2843,7 +2980,7 @@ declare namespace Eris { addMessageReaction(messageID: string, reaction: string, userID: string): Promise; createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; deleteMessage(messageID: string, reason?: string): Promise; - editMessage(messageID: string, content: MessageContent): Promise>; + editMessage(messageID: string, content: MessageContentEdit): Promise>; getMessage(messageID: string): Promise>; getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; /** @deprecated */ @@ -2864,6 +3001,27 @@ declare namespace Eris { unsendMessage(messageID: string): Promise; } + export class PrivateThreadChannel extends ThreadChannel { + threadMetadata: PrivateThreadMetadata; + type: 12; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + edit(options: Pick, reason?: string): Promise; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + getMessage(messageID: string): Promise>; + getMessages(options?: GetMessagesOptions): Promise[]>; + getPins(): Promise[]>; + } + + export class PublicThreadChannel extends ThreadChannel { + type: 10 | 11; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + edit(options: Pick, reason?: string): Promise; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + getMessage(messageID: string): Promise>; + getMessages(options?: GetMessagesOptions): Promise[]>; + getPins(): Promise[]>; + } + export class Relationship extends Base implements Omit { activities: Activity[] | null; clientStatus?: ClientStatus; @@ -3034,6 +3192,23 @@ declare namespace Eris { export class StageChannel extends VoiceChannel { topic?: string; type: 13; + createInstance(options: StageInstanceOptions): Promise; + deleteInstance(): Promise; + editInstance(options: StageInstanceOptions): Promise; + getInstance(): Promise; + } + + export class StageInstance extends Base { + channel: StageChannel | Uncached; + client: Client; + discoverableDisabled: boolean; + guild: Guild | Uncached; + privacyLevel: StageInstancePrivacyLevel; + topic: string; + constructor(data: BaseData, client: Client); + delete(): Promise; + edit(options: StageInstanceOptions): Promise; + update(data: BaseData): void; } export class StoreChannel extends GuildChannel { @@ -3042,6 +3217,7 @@ declare namespace Eris { } export class TextChannel extends GuildChannel implements GuildTextable, Invitable { + defaultAutoArchiveDuration: AutoArchiveDuration; lastMessageID: string; lastPinTimestamp: number | null; messages: Collection>; @@ -3053,13 +3229,20 @@ declare namespace Eris { /** @deprecated */ addMessageReaction(messageID: string, reaction: string, userID: string): Promise; createInvite(options?: CreateInviteOptions, reason?: string): Promise>; - createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; + createThreadWithoutMessage(options: CreateThreadWithoutMessageOptions): Promise; createWebhook(options: { name: string; avatar?: string | null }, reason?: string): Promise; deleteMessage(messageID: string, reason?: string): Promise; deleteMessages(messageIDs: string[], reason?: string): Promise; edit(options: Omit, reason?: string): Promise; - editMessage(messageID: string, content: MessageContent): Promise>; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + /** @deprecated */ + getActiveThreads(): Promise; + getArchivedThreads(type: "private", options?: GetArchivedThreadsOptions): Promise>; + getArchivedThreads(type: "public", options?: GetArchivedThreadsOptions): Promise>; getInvites(): Promise<(Invite<"withMetadata", TextChannel>)[]>; + getJoinedPrivateArchivedThreads(options: GetArchivedThreadsOptions): Promise>; getMessage(messageID: string): Promise>; getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; /** @deprecated */ @@ -3081,6 +3264,54 @@ declare namespace Eris { unsendMessage(messageID: string): Promise; } + type A = T; + type B = A; + + export class ThreadChannel extends GuildChannel implements ThreadTextable { + lastMessageID: string; + lastPinTimestamp?: number; + member?: ThreadMember; + memberCount: number; + members: Collection; + messageCount: number; + messages: Collection>; + ownerID: string; + rateLimitPerUser: number; + threadMetadata: ThreadMetadata; + type: 10 | 11 | 12; + constructor(data: BaseData, client: Client, messageLimit?: number); + addMessageReaction(messageID: string, reaction: string): Promise; + createMessage(content: MessageContent, file?: MessageFile | MessageFile[]): Promise>; + deleteMessage(messageID: string, reason?: string): Promise; + deleteMessages(messageIDs: string[], reason?: string): Promise; + edit(options: Pick, reason?: string): Promise; + editMessage(messageID: string, content: MessageContentEdit): Promise>; + getMembers(): Promise; + getMessage(messageID: string): Promise>; + getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; + getMessages(options?: GetMessagesOptions): Promise[]>; + getPins(): Promise[]>; + join(userID?: string): Promise; + leave(userID?: string): Promise; + pinMessage(messageID: string): Promise; + purge(options: PurgeChannelOptions): Promise; + removeMessageReaction(messageID: string, reaction: string, userID?: string): Promise; + removeMessageReactionEmoji(messageID: string, reaction: string): Promise; + removeMessageReactions(messageID: string): Promise; + sendTyping(): Promise; + unpinMessage(messageID: string): Promise; + unsendMessage(messageID: string): Promise; + } + + export class ThreadMember extends Base { + client: Client; + joinTimestamp: number; + threadID: string; + constructor(data: BaseData, client: Client); + leave(): Promise; + update(data: BaseData): void; + } + export class UnavailableGuild extends Base { createdAt: number; id: string; diff --git a/index.js b/index.js index 3329cc4fa..75f8b0ae4 100644 --- a/index.js +++ b/index.js @@ -33,18 +33,25 @@ Eris.Invite = require("./lib/structures/Invite"); Eris.Member = require("./lib/structures/Member"); Eris.Message = require("./lib/structures/Message"); Eris.NewsChannel = require("./lib/structures/NewsChannel"); +Eris.NewsThreadChannel = require("./lib/structures/NewsThreadChannel"); Eris.Permission = require("./lib/structures/Permission"); Eris.PermissionOverwrite = require("./lib/structures/PermissionOverwrite"); Eris.PingInteraction = require("./lib/structures/PingInteraction"); Eris.PrivateChannel = require("./lib/structures/PrivateChannel"); +Eris.PrivateThreadChannel = require("./lib/structures/PrivateThreadChannel"); +Eris.PublicThreadChannel = require("./lib/structures/PublicThreadChannel"); Eris.Relationship = require("./lib/structures/Relationship"); Eris.RequestHandler = require("./lib/rest/RequestHandler"); Eris.Role = require("./lib/structures/Role"); Eris.SequentialBucket = require("./lib/util/SequentialBucket"); Eris.Shard = require("./lib/gateway/Shard"); Eris.SharedStream = require("./lib/voice/SharedStream"); +Eris.StageChannel = require("./lib/structures/StageChannel"); +Eris.StageInstance = require("./lib/structures/StageInstance"); Eris.StoreChannel = require("./lib/structures/StoreChannel"); Eris.TextChannel = require("./lib/structures/TextChannel"); +Eris.ThreadChannel = require("./lib/structures/ThreadChannel"); +Eris.ThreadMember = require("./lib/structures/ThreadMember"); Eris.UnavailableGuild = require("./lib/structures/UnavailableGuild"); Eris.UnknownInteraction = require("./lib/structures/UnknownInteraction"); Eris.User = require("./lib/structures/User"); diff --git a/lib/Client.js b/lib/Client.js index e77cc450c..4e1cdfa3e 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -21,9 +21,11 @@ const Relationship = require("./structures/Relationship"); const RequestHandler = require("./rest/RequestHandler"); const Role = require("./structures/Role"); const ShardManager = require("./gateway/ShardManager"); +const ThreadMember = require("./structures/ThreadMember"); const UnavailableGuild = require("./structures/UnavailableGuild"); const User = require("./structures/User"); const VoiceConnectionManager = require("./voice/VoiceConnectionManager"); +const StageInstance = require("./structures/StageInstance"); let EventEmitter; try { @@ -65,6 +67,7 @@ const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); * @prop {RequestHandler} requestHandler The request handler the client will use * @prop {Collection} shards Collection of shards Eris is using * @prop {Number} startTime Timestamp of bot ready event +* @prop {Object} threadGuildMap Object mapping thread channel IDs to guild IDs * @prop {Collection} unavailableGuilds Collection of unavailable guilds the bot is in * @prop {Number} uptime How long in milliseconds the bot has been up for * @prop {ExtendedUser} user The bot user @@ -207,6 +210,7 @@ class Client extends EventEmitter { this.startTime = 0; this.lastConnect = 0; this.channelGuildMap = {}; + this.threadGuildMap = {}; this.shards = new ShardManager(this); this.groupChannels = new Collection(GroupChannel); this.guilds = new Collection(Guild); @@ -582,7 +586,6 @@ class Client extends EventEmitter { * @arg {Number} [options.defaultNotifications] The default notification settings for the guild. 0 is "All Messages", 1 is "Only @mentions". * @arg {Number} [options.explicitContentFilter] The level of the explicit content filter for messages/images in the guild. 0 disables message scanning, 1 enables scanning the messages of members without roles, 2 enables scanning for all messages. * @arg {String} [options.icon] The guild icon as a base64 data URI. Note: base64 strings alone are not base64 data URI strings - * @arg {String} [options.region] The region of the guild * @arg {Array} [options.roles] The new roles of the guild, the first one is the @everyone role. IDs are placeholders which allow channel overwrites. * @arg {String} [options.systemChannelID] The ID of the system channel * @arg {Number} [options.verificationLevel] The guild verification level @@ -595,7 +598,6 @@ class Client extends EventEmitter { return this.requestHandler.request("POST", Endpoints.GUILDS, true, { name: name, - region: options.region, icon: options.icon, verification_level: options.verificationLevel, default_message_notifications: options.defaultNotifications, @@ -840,6 +842,57 @@ class Client extends EventEmitter { }); } + /** + * Create a stage instance + * @arg {String} channelID The ID of the stage channel to create the instance in + * @arg {Object} options The stage instance options + * @arg {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only + * @arg {String} options.topic The stage instance topic + * @returns {Promise} + */ + createStageInstance(channelID, options) { + return this.requestHandler.request("POST", Endpoints.STAGE_INSTANCES, true, { + channel_id: channelID, + privacy_level: options.privacyLevel, + topic: options.topic + }).then((instance) => new StageInstance(instance, this)); + } + + /** + * Create a thread with an existing message + * @arg {String} channelID The ID of the channel + * @arg {String} messageID The ID of the message to create the thread from + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {String} options.name The thread channel name + * @returns {Promise} + */ + createThreadWithMessage(channelID, messageID, options) { + return this.requestHandler.request("POST", Endpoints.THREAD_WITH_MESSAGE(channelID, messageID), true, { + name: options.name, + auto_archive_duration: options.autoArchiveDuration + }).then((channel) => Channel.from(channel, this)); + } + + /** + * Create a thread without an existing message + * @arg {String} channelID The ID of the channel + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) + * @arg {String} options.name The thread channel name + * @arg {Number} [options.type] The channel type of the thread to create. It is recommended to explicitly set this property as this will be a required property in API v10 + * @returns {Promise} + */ + createThreadWithoutMessage(channelID, options) { + return this.requestHandler.request("POST", Endpoints.THREAD_WITHOUT_MESSAGE(channelID), true, { + auto_archive_duration: options.autoArchiveDuration, + invitable: options.invitable, + name: options.name, + type: options.type + }).then((channel) => Channel.from(channel, this)); + } + /** * Crosspost (publish) a message to subscribed channels * @arg {String} channelID The ID of the NewsChannel @@ -1058,6 +1111,15 @@ class Client extends EventEmitter { return this.requestHandler.request("DELETE", Endpoints.USER_BILLING_PREMIUM_SUBSCRIPTION("@me"), true); } + /** + * Delete a stage instance + * @arg {String} channelID The stage channel associated with the instance + * @returns {Promise} + */ + deleteStageInstance(channelID) { + return this.requestHandler.request("DELETE", Endpoints.STAGE_INSTANCE(channelID), true); + } + /** * [USER ACCOUNT] Delete the current user's note for another user * @returns {Promise} @@ -1134,24 +1196,34 @@ class Client extends EventEmitter { * Edit a channel's properties * @arg {String} channelID The ID of the channel * @arg {Object} options The properties to edit + * @arg {Boolean} [options.archived] The archive status of the channel (thread channels only) + * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) * @arg {Number} [options.bitrate] The bitrate of the channel (guild voice channels only) + * @arg {Number?} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (guild text/news channels only) * @arg {String} [options.icon] The icon of the channel as a base64 data URI (group channels only). Note: base64 strings alone are not base64 data URI strings + * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the channel (private thread channels only) + * @arg {Boolean} [options.locked] The lock status of the channel (thread channels only) * @arg {String} [options.name] The name of the channel * @arg {Boolean} [options.nsfw] The nsfw status of the channel (guild channels only) * @arg {String} [options.ownerID] The ID of the channel owner (group channels only) * @arg {String?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text and thread channels only) * @arg {String?} [options.rtcRegion] The RTC region ID of the channel (automatic if `null`) (guild voice channels only) * @arg {String} [options.topic] The topic of the channel (guild text channels only) * @arg {Number} [options.userLimit] The channel user limit (guild voice channels only) * @arg {Number} [options.videoQualityMode] The camera video quality mode of the channel (guild voice channels only). `1` is auto, `2` is 720p * @arg {String} [reason] The reason to be displayed in audit logs - * @returns {Promise} + * @returns {Promise} */ editChannel(channelID, options, reason) { return this.requestHandler.request("PATCH", Endpoints.CHANNEL(channelID), true, { + archived: options.archived, + auto_archive_duration: options.autoArchiveDuration, bitrate: options.bitrate, + default_auto_archive_duration: options.defaultAutoArchiveDuration, icon: options.icon, + invitable: options.invitable, + locked: options.locked, name: options.name, nsfw: options.nsfw, owner_id: options.ownerID, @@ -1286,7 +1358,6 @@ class Client extends EventEmitter { * @arg {String} [options.ownerID] The ID of the user to transfer server ownership to (bot user must be owner) * @arg {String} [options.preferredLocale] Preferred "COMMUNITY" guild language used in server discovery and notices from Discord * @arg {String} [options.publicUpdatesChannelID] The id of the channel where admins and moderators of "COMMUNITY" guilds receive notices from Discord - * @arg {String} [options.region] The region of the guild * @arg {String} [options.rulesChannelID] The id of the channel where "COMMUNITY" guilds display rules and/or guidelines * @arg {String} [options.splash] The guild splash image as a base64 data URI (VIP only). Note: base64 strings alone are not base64 data URI strings * @arg {Number} [options.systemChannelFlags] The flags for the system channel @@ -1298,7 +1369,6 @@ class Client extends EventEmitter { editGuild(guildID, options, reason) { return this.requestHandler.request("PATCH", Endpoints.GUILD(guildID), true, { name: options.name, - region: options.region, icon: options.icon, verification_level: options.verificationLevel, default_message_notifications: options.defaultNotifications, @@ -1545,6 +1615,9 @@ class Client extends EventEmitter { * @arg {String} content.content A content string * @arg {Object} [content.embed] [DEPRECATED] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure. Use `embeds` instead * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference * @returns {Promise} */ @@ -1564,7 +1637,7 @@ class Client extends EventEmitter { content.allowed_mentions = this._formatAllowedMentions(content.allowedMentions); } } - return this.requestHandler.request("PATCH", Endpoints.CHANNEL_MESSAGE(channelID, messageID), true, content).then((message) => new Message(message, this)); + return this.requestHandler.request("PATCH", Endpoints.CHANNEL_MESSAGE(channelID, messageID), true, content, content.file).then((message) => new Message(message, this)); } /** @@ -1723,6 +1796,18 @@ class Client extends EventEmitter { }); } + /** + * Update a stage instance + * @arg {String} channelID The ID of the stage channel associated with the instance + * @arg {Object} options The properties to edit + * @arg {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only + * @arg {String} [options.topic] The stage instance topic + * @returns {Promise} + */ + editStageInstance(channelID, options) { + return this.requestHandler.request("PATCH", Endpoints.STAGE_INSTANCE(channelID), true, options).then((instance) => new StageInstance(instance, this)); + } + /** * Update the bot's status on all guilds * @arg {String} [status] Sets the bot's status, either "online", "idle", "dnd", or "invisible" @@ -1820,8 +1905,8 @@ class Client extends EventEmitter { * @arg {String} [options.content=""] A content string * @arg {Array} [options.embeds] An array of Discord embeds * @arg {Object | Array} [options.file] A file object (or an Array of them) - * @arg {Buffer} options.file.file A buffer containing file data - * @arg {String} options.file.name What to name the file + * @arg {Buffer} options.file[].file A buffer containing file data + * @arg {String} options.file[].name What to name the file * @returns {Promise} */ editWebhookMessage(webhookID, token, messageID, options) { @@ -1854,6 +1939,7 @@ class Client extends EventEmitter { * @arg {String} token The token of the webhook * @arg {Object} options Slack webhook options * @arg {Boolean} [options.auth=false] Whether or not to authenticate with the bot token. + * @arg {String} [options.threadID] The ID of the thread channel in the webhook's channel to send the message to * @arg {Boolean} [options.wait=false] Whether to wait for the server to confirm the message create or not * @returns {Promise} */ @@ -1862,7 +1948,16 @@ class Client extends EventEmitter { options.wait = undefined; const auth = !!options.auth; options.auth = undefined; - return this.requestHandler.request("POST", Endpoints.WEBHOOK_TOKEN_SLACK(webhookID, token) + (wait ? "?wait=true" : ""), auth, options); + const threadID = options.threadID; + options.threadID = undefined; + let qs = ""; + if(wait) { + qs += "&wait=true"; + } + if(threadID) { + qs += "&thread_id=" + threadID; + } + return this.requestHandler.request("POST", Endpoints.WEBHOOK_TOKEN_SLACK(webhookID, token) + (qs ? "?" + qs : ""), auth, options); } /** @@ -1899,13 +1994,21 @@ class Client extends EventEmitter { * @arg {Buffer} options.file.file A buffer containing file data * @arg {String} options.file.name What to name the file * @arg {Number} [options.flags] Flags to execute the webhook with, 64 for ephemeral (Interaction webhooks only) + * @arg {String} [options.threadID] The ID of the thread channel in the webhook's channel to send the message to * @arg {Boolean} [options.tts=false] Whether the message should be a TTS message or not * @arg {String} [options.username] A custom username, defaults to webhook default username if not specified * @arg {Boolean} [options.wait=false] Whether to wait for the server to confirm the message create or not * @returns {Promise} */ executeWebhook(webhookID, token, options) { - return this.requestHandler.request("POST", Endpoints.WEBHOOK_TOKEN(webhookID, token) + (options.wait ? "?wait=true" : ""), !!options.auth, { + let qs = ""; + if(options.wait) { + qs += "&wait=true"; + } + if(options.threadID) { + qs += "&thread_id=" + options.threadID; + } + return this.requestHandler.request("POST", Endpoints.WEBHOOK_TOKEN(webhookID, token) + (qs ? "?" + qs : ""), !!options.auth, { content: options.content, embeds: options.embeds, username: options.username, @@ -1927,6 +2030,54 @@ class Client extends EventEmitter { return this.requestHandler.request("POST", Endpoints.CHANNEL_FOLLOW(channelID), true, {webhook_channel_id: webhookChannelID}); } + /** + * Get all active threads in a guild + * @arg {String} guildID The ID of the guild + * @returns {Promise} An object containing an array of `threads` and an array of `members` + */ + getActiveGuildThreads(guildID) { + return this.requestHandler.request("GET", Endpoints.THREADS_GUILD_ACTIVE(guildID), true).then((response) => { + return { + members: response.members.map((member) => new ThreadMember(member, this)), + threads: response.threads.map((thread) => Channel.from(thread, this)) + }; + }); + } + + /** + * [DEPRECATED] Get all active threads in a channel. Use getActiveGuildThreads instead + * @arg {String} channelID The ID of the channel + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getActiveThreads(channelID) { + return this.requestHandler.request("GET", Endpoints.THREADS_ACTIVE(channelID), true).then((response) => { + return { + hasMore: response.has_more, + members: response.members.map((member) => new ThreadMember(member, this)), + threads: response.threads.map((thread) => Channel.from(thread, this)) + }; + }); + } + + /** + * Get all archived threads in a channel + * @arg {String} channelID The ID of the channel + * @arg {String} type The type of thread channel, either "public" or "private" + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getArchivedThreads(channelID, type, options = {}) { + return this.requestHandler.request("GET", Endpoints.THREADS_ARCHIVED(channelID, type), true, options).then((response) => { + return { + hasMore: response.has_more, + members: response.members.map((member) => new ThreadMember(member, this)), + threads: response.threads.map((thread) => Channel.from(thread, this)) + }; + }); + } + /** * Get general and bot-specific info on connecting to the Discord gateway (e.g. connection ratelimit) * @returns {Promise} Resolves with an object containing gateway connection info @@ -1941,7 +2092,7 @@ class Client extends EventEmitter { /** * Get a Channel object from a channel ID * @arg {String} channelID The ID of the channel - * @returns {CategoryChannel | GroupChannel | PrivateChannel | TextChannel | VoiceChannel | NewsChannel} + * @returns {CategoryChannel | GroupChannel | PrivateChannel | TextChannel | VoiceChannel | NewsChannel | NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} */ getChannel(channelID) { if(!channelID) { @@ -1951,6 +2102,9 @@ class Client extends EventEmitter { if(this.channelGuildMap[channelID] && this.guilds.get(this.channelGuildMap[channelID])) { return this.guilds.get(this.channelGuildMap[channelID]).channels.get(channelID); } + if(this.threadGuildMap[channelID] && this.guilds.get(this.threadGuildMap[channelID])) { + return this.guilds.get(this.threadGuildMap[channelID]).threads.get(channelID); + } return this.privateChannels.get(channelID) || this.groupChannels.get(channelID); } @@ -2056,7 +2210,7 @@ class Client extends EventEmitter { * @arg {String} [options.before] Get entries before this entry ID * @arg {Number} [options.limit=50] The maximum number of entries to return * @arg {String} [options.userID] Filter entries by the user that performed the action - * @returns {Promise<{users: User[], entries: GuildAuditLogEntry[], integrations: PartialIntegration[], webhooks: Webhook[]}>} + * @returns {Promise<{entries: GuildAuditLogEntry[], integrations: PartialIntegration[], threads: (NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel)[], users: User[], webhooks: Webhook[]}>} */ getGuildAuditLog(guildID, options = {}, before, actionType, userID) { if(!options || typeof options !== "object") { @@ -2085,9 +2239,10 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.GUILD_AUDIT_LOGS(guildID), true, options).then((data) => { const guild = this.guilds.get(guildID); return { - users: data.users.map((user) => this.users.add(user, this)), entries: data.audit_log_entries.map((entry) => new GuildAuditLogEntry(entry, guild)), integrations: data.integrations.map((integration) => new GuildIntegration(integration, guild)), + threads: data.threads.map((thread) => guild.threads.update(thread, this)), + users: data.users.map((user) => this.users.add(user, this)), webhooks: data.webhooks }; }); @@ -2279,6 +2434,24 @@ class Client extends EventEmitter { }).then((invite) => new Invite(invite, this)); } + /** + * Get joined private archived threads in a channel + * @arg {String} channelID The ID of the channel + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getJoinedPrivateArchivedThreads(channelID, options = {}) { + return this.requestHandler.request("GET", Endpoints.THREADS_ARCHIVED_JOINED(channelID), true, options).then((response) => { + return { + hasMore: response.has_more, + members: response.members.map((member) => new ThreadMember(member, this)), + threads: response.threads.map((thread) => Channel.from(thread, this)) + }; + }); + } + /** * Get a previous message in a channel * @arg {String} channelID The ID of the channel @@ -2433,7 +2606,7 @@ class Client extends EventEmitter { /** * Get a channel's data via the REST API. REST mode is required to use this endpoint. * @arg {String} channelID The ID of the channel - * @returns {Promise} + * @returns {Promise} */ getRESTChannel(channelID) { if(!this.options.restMode) { @@ -2676,6 +2849,24 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.USER_SETTINGS("@me"), true); } + /** + * Get the stage instance associated with a stage channel + * @arg {String} channelID The stage channel ID + * @returns {Promise} + */ + getStageInstance(channelID) { + return this.requestHandler.request("GET", Endpoints.STAGE_INSTANCE(channelID), true).then((instance) => new StageInstance(instance, this)); + } + + /** + * Get a list of members that are part of a thread channel + * @arg {String} channelID The ID of the thread channel + * @returns {Promise>} + */ + getThreadMembers(channelID) { + return this.requestHandler.request("GET", Endpoints.THREAD_MEMBERS(channelID), true).then((members) => members.map((member) => new ThreadMember(member, this))); + } + /** * [USER ACCOUNT] Get profile data for a user * @arg {String} userID The ID of the target user @@ -2715,6 +2906,16 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.WEBHOOK_MESSAGE(webhookID, token, messageID)).then((message) => new Message(message, this)); } + /** + * Join a thread + * @arg {String} channelID The ID of the thread channel + * @arg {String} [userID="@me"] The user ID of the user joining + * @returns {Promise} + */ + joinThread(channelID, userID = "@me") { + return this.requestHandler.request("PUT", Endpoints.THREAD_MEMBER(channelID, userID), true); + } + /** * Join a voice channel. If joining a group call, the voice connection ID will be stored in voiceConnections as "call". Otherwise, it will be the guild ID * @arg {String} channelID The ID of the voice channel @@ -2767,6 +2968,16 @@ class Client extends EventEmitter { return this.requestHandler.request("DELETE", Endpoints.USER_GUILD("@me", guildID), true); } + /** + * Leave a thread + * @arg {String} channelID The ID of the thread channel + * @arg {String} [userID="@me"] The user ID of the user leaving + * @returns {Promise} + */ + leaveThread(channelID, userID = "@me") { + return this.requestHandler.request("DELETE", Endpoints.THREAD_MEMBER(channelID, userID), true); + } + /** * Leaves a voice channel * @arg {String} channelID The ID of the voice channel diff --git a/lib/Constants.js b/lib/Constants.js index 8252a959d..ce5460042 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -30,8 +30,8 @@ module.exports.GatewayOPCodes = { SYNC_CALL: 13 }; -module.exports.GATEWAY_VERSION = 8; -module.exports.REST_VERSION = 8; +module.exports.GATEWAY_VERSION = 9; +module.exports.REST_VERSION = 9; const Permissions = { createInstantInvite: 1n, @@ -67,7 +67,12 @@ const Permissions = { manageEmojisAndStickers: 1n << 30n, manageEmojis: 1n << 30n, // [DEPRECATED] useApplicationCommands: 1n << 31n, useSlashCommands: 1n << 31n, // [DEPRECATED] voiceRequestToSpeak: 1n << 32n, - useExternalStickers: 1n << 37n + + manageThreads: 1n << 34n, + createPublicThreads: 1n << 35n, + createPrivateThreads: 1n << 36n, + useExternalStickers: 1n << 37n, + sendMessagesInThreads: 1n << 38n }; Permissions.allGuild = Permissions.kickMembers | Permissions.banMembers @@ -96,7 +101,11 @@ Permissions.allText = Permissions.createInstantInvite | Permissions.manageRoles | Permissions.manageWebhooks | Permissions.useApplicationCommands - | Permissions.useExternalStickers; + | Permissions.manageThreads + | Permissions.createPublicThreads + | Permissions.createPrivateThreads + | Permissions.useExternalStickers + | Permissions.sendMessagesInThreads; Permissions.allVoice = Permissions.createInstantInvite | Permissions.manageChannels | Permissions.voicePrioritySpeaker @@ -193,7 +202,11 @@ module.exports.AuditLogActions = { STICKER_CREATE: 90, STICKER_UPDATE: 91, - STICKER_DELETE: 92 + STICKER_DELETE: 92, + + THREAD_CREATE: 110, + THREAD_UPDATE: 111, + THREAD_DELETE: 112 }; module.exports.MessageActivityFlags = { @@ -214,6 +227,7 @@ module.exports.MessageFlags = { SUPPRESS_EMBEDS: 1 << 2, SOURCE_MESSAGE_DELETED: 1 << 3, URGENT: 1 << 4, + HAS_THREAD: 1 << 5, EPHEMERAL: 1 << 6, LOADING: 1 << 7 }; @@ -237,22 +251,26 @@ module.exports.MessageTypes = { GUILD_DISCOVERY_REQUALIFIED: 15, GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING: 16, GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: 17, + THREAD_CREATED: 18, REPLY: 19, CHAT_INPUT_COMMAND: 20, - + THREAD_STARTER_MESSAGE: 21, GUILD_INVITE_REMINDER: 22, CONTEXT_MENU_COMMAND: 23 }; module.exports.ChannelTypes = { - GUILD_TEXT: 0, - DM: 1, - GUILD_VOICE: 2, - GROUP_DM: 3, - GUILD_CATEGORY: 4, - GUILD_NEWS: 5, - GUILD_STORE: 6, - GUILD_STAGE: 13 + GUILD_TEXT: 0, + DM: 1, + GUILD_VOICE: 2, + GROUP_DM: 3, + GUILD_CATEGORY: 4, + GUILD_NEWS: 5, + GUILD_STORE: 6, + GUILD_NEWS_THREAD: 10, + GUILD_PUBLIC_THREAD: 11, + GUILD_PRIVATE_THREAD: 12, + GUILD_STAGE: 13 }; module.exports.UserFlags = { diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index 6adf9b033..a4c3d92e5 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -15,6 +15,8 @@ const User = require("../structures/User"); const Invite = require("../structures/Invite"); const Interaction = require("../structures/Interaction"); const Constants = require("../Constants"); +const ThreadChannel = require("../structures/ThreadChannel"); +const StageInstance = require("../structures/StageInstance"); const WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws"); @@ -1248,7 +1250,6 @@ class Shard extends EventEmitter { premiumSubscriptionCount: guild.premiumSubscriptionCount, premiumTier: guild.premiumTier, publicUpdatesChannelID: guild.publicUpdatesChannelID, - region: guild.region, rulesChannelID: guild.rulesChannelID, splash: guild.splash, stickers: guild.stickers, @@ -1283,7 +1284,6 @@ class Shard extends EventEmitter { * @prop {Number?} oldGuild.premiumSubscriptionCount The total number of users currently boosting this guild * @prop {Number} oldGuild.premiumTier Nitro boost level of the guild * @prop {String?} oldGuild.publicUpdatesChannelID ID of the guild's updates channel if the guild has "COMMUNITY" features - * @prop {String} oldGuild.region The region of the guild * @prop {String?} oldGuild.rulesChannelID The channel where "COMMUNITY" guilds display rules and/or guidelines * @prop {String?} oldGuild.splash The hash of the guild splash image, or null if no splash (VIP only) * @prop {Array?} stickers An array of guild sticker objects @@ -2098,6 +2098,222 @@ class Shard extends EventEmitter { this.client.userGuildSettings[packet.d.guild_id] = packet.d; break; } + case "THREAD_CREATE": { + const channel = Channel.from(packet.d, this.client); + if(!channel.guild) { + channel.guild = this.client.guilds.get(packet.d.guild_id); + if(!channel.guild) { + this.emit("debug", `Received THREAD_CREATE for channel in missing guild ${packet.d.guild_id}`); + break; + } + } + channel.guild.threads.add(channel, this.client); + this.client.threadGuildMap[packet.d.id] = packet.d.guild_id; + /** + * Fired when a channel is created + * @event Client#threadCreate + * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel + */ + this.emit("threadCreate", channel); + break; + } + case "THREAD_UPDATE": { + const channel = this.client.getChannel(packet.d.id); + if(!channel) { + const thread = Channel.from(packet.d, this.client); + this.emit("threadUpdate", this.client.guilds.get(packet.d.guild_id).threads.add(thread, this.client), null); + this.client.threadGuildMap[packet.d.id] = packet.d.guild_id; + break; + } + if(!(channel instanceof ThreadChannel)) { + this.emit("warn", `Unexpected THREAD_UPDATE for channel ${packet.d.id} with type ${channel.type}`); + break; + } + const oldChannel = { + name: channel.name, + rateLimitPerUser: channel.rateLimitPerUser, + threadMetadata: channel.threadMetadata + }; + channel.update(packet.d); + + /** + * Fired when a thread channel is updated + * @event Client#threadUpdate + * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The updated channel + * @prop {Object?} oldChannel The old thread channel. This will be null if the channel was uncached + * @prop {String} oldChannel.name The name of the channel + * @prop {Number} oldChannel.rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + * @prop {Object} oldChannel.threadMetadata Metadata for the thread + * @prop {Number} oldChannel.threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity + * @prop {Boolean} oldChannel.threadMetadata.archived Whether the thread is archived + * @prop {Number} oldChannel.threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @prop {Boolean?} oldChannel.threadMetadata.locked Whether the thread is locked + */ + this.emit("threadUpdate", channel, oldChannel); + break; + } + case "THREAD_DELETE": { + delete this.client.threadGuildMap[packet.d.id]; + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_DELETE`); + break; + } + const channel = guild.threads.remove(packet.d); + if(!channel) { + break; + } + /** + * Fired when a thread channel is deleted + * @event Client#threadDelete + * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel + */ + this.emit("threadDelete", channel); + break; + } + case "THREAD_LIST_SYNC": { + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_LIST_SYNC`); + break; + } + const deletedThreads = (packet.d.channel_ids || guild.threads.map((c) => c.id)) // REVIEW Is this a good name? + .filter((c) => !packet.d.threads.some((t) => t.id === c)).map((id) => guild.threads.remove({id}) || {id}); + const activeThreads = packet.d.threads.map((t) => guild.threads.update(t, this.client)); + const joinedThreadsMember = packet.d.members.map((m) => guild.threads.get(m.id).members.update(m, this.client)); + /** + * Fired when the current user gains access to a channel + * @event Client#threadListSync + * @prop {Guild} guild The guild where threads are being synced + * @prop {Array} deletedThreads An array of synced threads that the current user no longer has access to. If a thread channel is uncached, it will be an object with an `id` key. No other property is guaranteed + * @prop {Array} activeThreads An array of synced active threads that the current user can access + * @prop {Array} joinedThreadsMember An array of thread member objects where the current user has been added in a synced thread channel + */ + this.emit("threadListSync", guild, deletedThreads, activeThreads, joinedThreadsMember); + break; + } + case "THREAD_MEMBER_UPDATE": { + const channel = this.client.getChannel(packet.d.id); + if(!channel) { + this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBER_UPDATE`); + break; + } + let oldMember = null; + // Thanks Discord + packet.d.thread_id = packet.d.id; + let member = channel.members.get((packet.d.id = packet.d.user_id)); + if(member) { + oldMember = { + flags: member.flags + }; + } + member = channel.members.update(packet.d, this.client); + /** + * Fired when a thread member is updated + * @event Client#threadMemberUpdate + * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel + * @prop {ThreadMember} member The updated thread member + * @prop {Object} oldMember The old thread member data + * @prop {Number} oldMember.flags User thread settings + */ + this.emit("threadMemberUpdate", channel, member, oldMember); + break; + } + case "THREAD_MEMBERS_UPDATE": { + const channel = this.client.getChannel(packet.d.id); + if(!channel) { + this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBERS_UPDATE`); + break; + } + channel.update(packet.d); + let addedMembers; + let removedMembers; + if(packet.d.added_members) { + addedMembers = packet.d.added_members.map((m) => { + if(m.presence) { + m.presence.id = m.presence.user.id; + this.client.users.update(m.presence.user, this.client); + } + + m.thread_id = m.id; + m.id = m.user_id; + m.member.id = m.member.user.id; + const guild = this.client.guilds.get(packet.d.guild_id); + if(guild) { + guild.members.update(m.presence, guild); + guild.members.update(m.member); + } + return channel.members.update(m, this.client); + }); + } + if(packet.d.removed_member_ids) { + removedMembers = packet.d.removed_member_ids.map((id) => channel.members.remove({id}) || {id}); + } + /** + * Fired when anyone is added or removed from a thread. If the `guildMembers` intent is not specified, this will only apply for the current user + * @event Client#threadMembersUpdate + * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The thread channel + * @prop {Array} addedMembers An array of members that were added to the thread channel + * @prop {Array} removedMembers An array of members that were removed from the thread channel. If a member is uncached, it will be an object with an `id` key. No other property is guaranteed + */ + this.emit("threadMembersUpdate", channel, addedMembers || [], removedMembers || []); + break; + } + case "STAGE_INSTANCE_CREATE": { + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("debug", `Missing guild ${packet.d.guild_id} in STAGE_INSTANCE_CREATE`); + break; + } + /** + * Fired when a stage instance is created + * @event Client#stageInstanceCreate + * @prop {StageInstance} stageInstance The stage instance + */ + this.emit("stageInstanceCreate", guild.stageInstances.add(packet.d, this.client)); + break; + } + case "STAGE_INSTANCE_UPDATE": { + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("stageInstanceUpdate", packet.d, null); + break; + } + const stageInstance = guild.stageInstances.get(packet.d.id); + let oldStageInstance = null; + if(stageInstance) { + oldStageInstance = { + discoverableDisabled: stageInstance.discoverableDisabled, + privacyLevel: stageInstance.privacyLevel, + topic: stageInstance.topic + }; + } + /** + * Fired when a stage instance is updated + * @event Client#stageInstanceUpdate + * @prop {StageInstance} stageInstance The stage instance + * @prop {Object?} oldStageInstance The old stage instance. If the stage instance was cached, this will be an object with the properties below. Otherwise, it will be null + * @prop {Boolean} oldStageInstance.discoverableDisabled Whether or not stage discovery was disabled + * @prop {Number} oldStageInstance.privacyLevel The privacy level of the stage instance. 1 is public, 2 is guild only + * @prop {String} oldStageInstance.topic The stage instance topic + */ + this.emit("stageInstanceUpdate", guild.stageInstances.update(packet.d, this.client), oldStageInstance); + break; + } + case "STAGE_INSTANCE_DELETE": { + const guild = this.client.guilds.get(packet.d.guild_id); + if(!guild) { + this.emit("stageInstanceDelete", new StageInstance(packet.d, this.client)); + break; + } + /** + * Fired when a stage instance is deleted + * @event Client#stageInstanceDelete + * @prop {StageInstance} stageInstance The deleted stage instance + */ + this.emit("stageInstanceDelete", guild.stageInstances.remove(packet.d) || new StageInstance(packet.d, this.client)); + break; + } case "MESSAGE_ACK": // Ignore these case "GUILD_INTEGRATIONS_UPDATE": case "USER_SETTINGS_UPDATE": diff --git a/lib/rest/Endpoints.js b/lib/rest/Endpoints.js index a6ff1ca8a..a61140bc7 100644 --- a/lib/rest/Endpoints.js +++ b/lib/rest/Endpoints.js @@ -77,8 +77,18 @@ module.exports.GUILDS = module.exports.INTERACTION_RESPOND = (interactID, interactToken) => `/interactions/${interactID}/${interactToken}/callback`; module.exports.INVITE = (inviteID) => `/invites/${inviteID}`; module.exports.OAUTH2_APPLICATION = (appID) => `/oauth2/applications/${appID}`; +module.exports.STAGE_INSTANCE = (channelID) => `/stage-instances/${channelID}`; +module.exports.STAGE_INSTANCES = "/stage-instances"; module.exports.STICKER = (stickerID) => `/stickers/${stickerID}`; module.exports.STICKER_PACKS = "/sticker-packs"; +module.exports.THREAD_MEMBER = (channelID, userID) => `/channels/${channelID}/thread-members/${userID}`; +module.exports.THREAD_MEMBERS = (channelID) => `/channels/${channelID}/thread-members`; +module.exports.THREAD_WITH_MESSAGE = (channelID, msgID) => `/channels/${channelID}/messages/${msgID}/threads`; +module.exports.THREAD_WITHOUT_MESSAGE = (channelID) => `/channels/${channelID}/threads`; +module.exports.THREADS_ACTIVE = (channelID) => `/channels/${channelID}/threads/active`; +module.exports.THREADS_ARCHIVED = (channelID, type) => `/channels/${channelID}/threads/archived/${type}`; +module.exports.THREADS_ARCHIVED_JOINED = (channelID) => `/channels/${channelID}/users/@me/threads/archived/private`; +module.exports.THREADS_GUILD_ACTIVE = (guildID) => `/guilds/${guildID}/threads/active`; module.exports.USER = (userID) => `/users/${userID}`; module.exports.USER_BILLING = (userID) => `/users/${userID}/billing`; module.exports.USER_BILLING_PAYMENTS = (userID) => `/users/${userID}/billing/payments`; diff --git a/lib/rest/RequestHandler.js b/lib/rest/RequestHandler.js index fe1ac9369..25a1f5165 100644 --- a/lib/rest/RequestHandler.js +++ b/lib/rest/RequestHandler.js @@ -402,6 +402,8 @@ class RequestHandler { const createdAt = Base.getCreatedAt(messageID); if(Date.now() - this.latencyRef.latency - createdAt >= 1000 * 60 * 60 * 24 * 14) { method += "_OLD"; + } else if(Date.now() - this.latencyRef.latency - createdAt <= 1000 * 10) { + method += "_NEW"; } route = method + route; } diff --git a/lib/structures/Base.js b/lib/structures/Base.js index ae16245d9..ed40290c7 100644 --- a/lib/structures/Base.js +++ b/lib/structures/Base.js @@ -14,7 +14,11 @@ class Base { } static getCreatedAt(id) { - return Math.floor(id / 4194304) + 1420070400000; + return Base.getDiscordEpoch(id) + 1420070400000; + } + + static getDiscordEpoch(id) { + return Math.floor(id / 4194304); } [util.inspect.custom]() { diff --git a/lib/structures/Channel.js b/lib/structures/Channel.js index e623cadcd..a2a8c062c 100644 --- a/lib/structures/Channel.js +++ b/lib/structures/Channel.js @@ -45,6 +45,15 @@ class Channel extends Base { case ChannelTypes.GUILD_STORE: { return new StoreChannel(data, client); } + case ChannelTypes.GUILD_NEWS_THREAD: { + return new NewsThreadChannel(data, client); + } + case ChannelTypes.GUILD_PUBLIC_THREAD: { + return new PublicThreadChannel(data, client); + } + case ChannelTypes.GUILD_PRIVATE_THREAD: { + return new PrivateThreadChannel(data, client); + } case ChannelTypes.GUILD_STAGE: { return new StageChannel(data, client); } @@ -76,7 +85,10 @@ const CategoryChannel = require("./CategoryChannel"); const GuildChannel = require("./GuildChannel"); const GroupChannel = require("./GroupChannel"); const NewsChannel = require("./NewsChannel"); +const NewsThreadChannel = require("./NewsThreadChannel"); const PrivateChannel = require("./PrivateChannel"); +const PrivateThreadChannel = require("./PrivateThreadChannel"); +const PublicThreadChannel = require("./PublicThreadChannel"); const StageChannel = require("./StageChannel"); const StoreChannel = require("./StoreChannel"); const TextChannel = require("./TextChannel"); diff --git a/lib/structures/CommandInteraction.js b/lib/structures/CommandInteraction.js index c8e596cc0..51db4c99b 100644 --- a/lib/structures/CommandInteraction.js +++ b/lib/structures/CommandInteraction.js @@ -41,7 +41,7 @@ class CommandInteraction extends Interaction { id: info.channel_id }; - this.data = info.data; + this.data = JSON.parse(JSON.stringify(info.data)); if(info.data.resolved !== undefined) { //Users diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index cdbf59e67..2b0e0bb1a 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -10,6 +10,8 @@ const Role = require("./Role"); const VoiceState = require("./VoiceState"); const Permission = require("./Permission"); const {Permissions} = require("../Constants"); +const StageInstance = require("./StageInstance"); +const ThreadChannel = require("./ThreadChannel"); /** * Represents a guild @@ -55,15 +57,16 @@ const {Permissions} = require("../Constants"); * @prop {Object?} primaryCategory The guild's primary discovery category * @prop {Number?} primaryCategoryID The guild's primary discovery category ID * @prop {String?} publicUpdatesChannelID ID of the guild's updates channel if the guild has "COMMUNITY" features -* @prop {String} region The region of the guild * @prop {Collection} roles Collection of Roles in the guild * @prop {String?} rulesChannelID The channel where "COMMUNITY" guilds display rules and/or guidelines * @prop {Shard} shard The Shard that owns the guild * @prop {String?} splash The hash of the guild splash image, or null if no splash (VIP only) * @prop {String?} splashURL The URL of the guild's splash image +* @prop {Collection} stageInstances Collection of stage instances in the guild * @prop {Array?} stickers An array of guild sticker objects * @prop {Number} systemChannelFlags The flags for the system channel * @prop {String?} systemChannelID The ID of the default channel for system messages (built-in join messages and boost messages) +* @prop {Collection} threads Collection of threads that the current user has permission to view * @prop {Boolean} unavailable Whether the guild is unavailable or not * @prop {String?} vanityURL The vanity URL of the guild (VIP only) * @prop {Number} verificationLevel The guild verification level @@ -78,12 +81,14 @@ class Guild extends Base { constructor(data, client) { super(data.id); this._client = client; - this.shard = client.shards.get(client.guildShardMap[this.id]); + this.shard = client.shards.get(client.guildShardMap[this.id] || (Base.getDiscordEpoch(data.id) % client.options.lastShardID) || 0); this.unavailable = !!data.unavailable; this.joinedAt = Date.parse(data.joined_at); this.voiceStates = new Collection(VoiceState); this.channels = new Collection(GuildChannel); + this.threads = new Collection(ThreadChannel); this.members = new Collection(Member); + this.stageInstances = new Collection(StageInstance); this.memberCount = data.member_count; this.roles = new Collection(Role); this.applicationID = data.application_id; @@ -136,6 +141,15 @@ class Guild extends Base { client.channelGuildMap[channel.id] = this.id; } } + if(data.threads) { + for(const threadData of data.threads) { + threadData.guild_id = this.id; + const channel = Channel.from(threadData, client); + channel.guild = this; + this.threads.add(channel, client); + client.threadGuildMap[channel.id] = this.id; + } + } if(data.members) { for(const member of data.members) { @@ -144,6 +158,13 @@ class Guild extends Base { } } + if(data.stage_instances) { + for(const stageInstance of data.stage_instances) { + stageInstance.guild_id = this.id; + this.stageInstances.add(stageInstance, client); + } + } + if(data.presences) { for(const presence of data.presences) { if(!this.members.get(presence.user.id)) { @@ -203,9 +224,6 @@ class Guild extends Base { if(data.banner !== undefined) { this.banner = data.banner; } - if(data.region !== undefined) { - this.region = data.region; - } if(data.owner_id !== undefined) { this.ownerID = data.owner_id; } @@ -586,7 +604,6 @@ class Guild extends Base { * @arg {String} [options.ownerID] The ID of the member to transfer guild ownership to (bot user must be owner) * @arg {String} [options.preferredLocale] Preferred "COMMUNITY" guild language used in server discovery and notices from Discord * @arg {String} [options.publicUpdatesChannelID] The id of the channel where admins and moderators of "COMMUNITY" guilds receive notices from Discord - * @arg {String} [options.region] The region of the guild * @arg {String} [options.rulesChannelID] The id of the channel where "COMMUNITY" guilds display rules and/or guidelines * @arg {String} [options.splash] The guild splash image as a base64 data URI (VIP only). Note: base64 strings alone are not base64 data URI strings * @arg {Number} [options.systemChannelFlags] The flags for the system channel @@ -804,6 +821,14 @@ class Guild extends Base { return this.shard.requestGuildMembers(this.id, options); } + /** + * Get all active threads in this guild + * @returns {Promise} An object containing an array of `threads` and an array of `members` + */ + getActiveThreads() { + return this.client.getActiveThreads.call(this.client, this.id); + } + /** * Get the audit log for the guild * @arg {Object} [options] Options for the request. If this is a number ([DEPRECATED] behavior), it is treated as `options.limit` @@ -1199,7 +1224,6 @@ class Guild extends Base { "primaryCategory", "primaryCategoryID", "publicUpdatesChannelID", - "region", "roles", "rulesChannelID", "splash", diff --git a/lib/structures/GuildAuditLogEntry.js b/lib/structures/GuildAuditLogEntry.js index 73ce8cf26..6cd985c09 100644 --- a/lib/structures/GuildAuditLogEntry.js +++ b/lib/structures/GuildAuditLogEntry.js @@ -11,7 +11,7 @@ const {AuditLogActions} = require("../Constants"); * For example, if a channel was renamed from #general to #potato, this would be `{name: "potato"}`` * @prop {Object?} before The properties of the targeted object before the action was taken * For example, if a channel was renamed from #general to #potato, this would be `{name: "general"}`` -* @prop {(CategoryChannel | TextChannel | VoiceChannel | StageChannel)?} channel The channel targeted in the entry, action types 26 (MEMBER_MOVE), 72/74/75 (MESSAGE_DELETE/PIN/UNPIN), and 83-85 (STAGE\_INSTANCE\_CREATE/UPDATE/DELETE) only +* @prop {(CategoryChannel | TextChannel | VoiceChannel | NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | StageChannel)?} channel The channel targeted in the entry, action types 26 (MEMBER_MOVE), 72/74/75 (MESSAGE_DELETE/PIN/UNPIN) and 83/84/85 (STAGE_INSTANCE_CREATE/UPDATE/DELETE) only * @prop {Number?} count The number of entities targeted * For example, for action type 26 (MEMBER_MOVE), this is the number of members that were moved/disconnected from the voice channel * @prop {Number?} deleteMemberDays The number of days of inactivity to prune for, action type 21 (MEMBER_PRUNE) only @@ -67,7 +67,11 @@ class GuildAuditLogEntry extends Base { this.count = +data.options.count; } if(data.options.channel_id) { - this.channel = guild.channels.get(data.options.channel_id); + if(this.actionType >= 83) { + this.channel = guild.threads.get(data.options.channel_id); + } else { + this.channel = guild.channels.get(data.options.channel_id); + } if(data.options.message_id) { this.message = this.channel && this.channel.messages.get(data.options.message_id) || {id: data.options.message_id}; } @@ -120,8 +124,10 @@ class GuildAuditLogEntry extends Base { return this.guild && this.guild.emojis.find((emoji) => emoji.id === this.targetID); } else if(this.actionType < 80) { // Message return this.guild && this.guild.shard.client.users.get(this.targetID); - } else if(this.actionType < 90) { // Integrations/Stage instances + } else if(this.actionType < 83) { // Integrations return null; + } else if(this.actionType < 90) { // Stage Instances + return this.guild && this.guild.threads.get(this.targetID); } else if(this.actionType < 100) { // Sticker return this.guild && this.guild.stickers.find((sticker) => sticker.id === this.targetID); } else { diff --git a/lib/structures/GuildChannel.js b/lib/structures/GuildChannel.js index dded7552e..b0f3052d1 100644 --- a/lib/structures/GuildChannel.js +++ b/lib/structures/GuildChannel.js @@ -13,7 +13,7 @@ const PermissionOverwrite = require("./PermissionOverwrite"); * @prop {String} id The ID of the channel * @prop {String} name The name of the channel * @prop {Boolean} nsfw Whether the channel is an NSFW channel or not -* @prop {String?} parentID The ID of the category this channel belongs to +* @prop {String?} parentID The ID of the category this channel belongs to or the channel ID where the thread originated from (thread channels only) * @prop {Collection} permissionOverwrites Collection of PermissionOverwrites in this channel * @prop {Number} position The position of the channel */ @@ -71,17 +71,22 @@ class GuildChannel extends Channel { /** * Edit the channel's properties * @arg {Object} options The properties to edit + * @arg {Boolean} [options.archived] The archive status of the channel (thread channels only) + * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) + * @arg {Number?} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (guild text/news channels only) + * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the channel (private thread channels only) + * @arg {Boolean} [options.locked] The lock status of the channel (thread channels only) * @arg {String} [options.name] The name of the channel * @arg {String} [options.topic] The topic of the channel (guild text channels only) * @arg {Number} [options.bitrate] The bitrate of the channel (guild voice channels only) * @arg {Number} [options.userLimit] The channel user limit (guild voice channels only) * @arg {Number} [options.videoQualityMode] The camera video quality mode of the channel (guild voice channels only). `1` is auto, `2` is 720p - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text and thread channels only) * @arg {String?} [options.rtcRegion] The RTC region ID of the channel (automatic if `null`) (guild voice channels only) * @arg {Boolean} [options.nsfw] The nsfw status of the channel - * @arg {Number?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) + * @arg {Number?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) or the channel ID where the thread originated from (thread channels only) * @arg {String} [reason] The reason to be displayed in audit logs - * @returns {Promise} + * @returns {Promise} */ edit(options, reason) { return this.client.editChannel.call(this.client, this.id, options, reason); @@ -123,20 +128,21 @@ class GuildChannel extends Channel { if(permission & Permissions.administrator) { return new Permission(Permissions.all); } - let overwrite = this.permissionOverwrites.get(this.guild.id); + const channel = this instanceof ThreadChannel ? this.guild.channels.get(this.parentID) : this; + let overwrite = channel && channel.permissionOverwrites.get(this.guild.id); if(overwrite) { permission = (permission & ~overwrite.deny) | overwrite.allow; } let deny = 0n; let allow = 0n; for(const roleID of member.roles) { - if((overwrite = this.permissionOverwrites.get(roleID))) { + if((overwrite = channel && channel.permissionOverwrites.get(roleID))) { deny |= overwrite.deny; allow |= overwrite.allow; } } permission = (permission & ~deny) | allow; - overwrite = this.permissionOverwrites.get(member.id); + overwrite = channel && channel.permissionOverwrites.get(member.id); if(overwrite) { permission = (permission & ~overwrite.deny) | overwrite.allow; } @@ -156,3 +162,5 @@ class GuildChannel extends Channel { } module.exports = GuildChannel; + +const ThreadChannel = require("./ThreadChannel"); diff --git a/lib/structures/Invite.js b/lib/structures/Invite.js index 33c7c4026..db3364bb7 100644 --- a/lib/structures/Invite.js +++ b/lib/structures/Invite.js @@ -5,7 +5,7 @@ const Guild = require("./Guild"); /** * Represents an invite. Some properties are only available when fetching invites from channels, which requires the Manage Channel permission. -* @prop {TextChannel | NewsChannel | VoiceChannel | GroupChannel | Object} channel Info on the invite channel +* @prop {TextChannel | NewsChannel | VoiceChannel | GroupChannel | StageChannel | Object} channel Info on the invite channel * @prop {String} channel.id The ID of the invite's channel * @prop {String?} channel.name The name of the invite's channel * @prop {Number} channel.type The type of the invite's channel @@ -18,6 +18,7 @@ const Guild = require("./Guild"); * @prop {Number?} maxUses The max number of invite uses * @prop {Number?} memberCount The **approximate** member count for the guild * @prop {Number?} presenceCount The **approximate** presence count for the guild +* @prop {Object?} stageInstance The active public stage instance data for the stage channel this invite is for * @prop {Boolean?} temporary Whether the invite grants temporary membership or not * @prop {Number?} uses The number of invite uses */ @@ -48,6 +49,20 @@ class Invite extends Base { this._createdAt = data.created_at !== undefined ? data.created_at : null; this.presenceCount = data.approximate_presence_count !== undefined ? data.approximate_presence_count : null; this.memberCount = data.approximate_member_count !== undefined ? data.approximate_member_count : null; + if(data.stage_instance !== undefined) { + data.stage_instance.members = data.stage_instance.members.map((m) => { + m.id = m.user.id; + return m; + }); + this.stageInstance = { + members: data.stage_instance.members.map((m) => this.guild.members.update(m, this.guild)), + participantCount: data.stage_instance.participant_count, + speakerCount: data.stage_instance.speaker_count, + topic: data.stage_instance.topic + }; + } else { + this.stageInstance = null; + } } get createdAt() { diff --git a/lib/structures/Member.js b/lib/structures/Member.js index a3838db68..4085b36a6 100644 --- a/lib/structures/Member.js +++ b/lib/structures/Member.js @@ -28,7 +28,7 @@ const VoiceState = require("./VoiceState"); * @prop {String?} game.url The url of the active game * @prop {Guild} guild The guild the member is in * @prop {String} id The ID of the member -* @prop {Number} joinedAt Timestamp of when the member joined the guild +* @prop {Number?} joinedAt Timestamp of when the member joined the guild * @prop {String} mention A string that mentions the member * @prop {String?} nick The server nickname of the member * @prop {Boolean?} pending Whether the member has passed the guild's Membership Screening requirements @@ -72,7 +72,7 @@ class Member extends Base { this.status = data.status; } if(data.joined_at !== undefined) { - this.joinedAt = Date.parse(data.joined_at); + this.joinedAt = data.joined_at ? Date.parse(data.joined_at) : null; } if(data.client_status !== undefined) { this.clientStatus = Object.assign({web: "offline", desktop: "offline", mobile: "offline"}, data.client_status); diff --git a/lib/structures/Message.js b/lib/structures/Message.js index 3ebf5e09c..df83ad38a 100644 --- a/lib/structures/Message.js +++ b/lib/structures/Message.js @@ -10,6 +10,7 @@ const User = require("./User"); * Represents a message * @prop {Object?} activity The activity specified in the message * @prop {Object?} application The application of the activity in the message +* @prop {String?} applicationID The ID of the interaction's application * @prop {Array} attachments Array of attachments * @prop {User} author The message author * @prop {PrivateChannel | TextChannel | NewsChannel} channel The channel the message is in. Can be partial with only the id if the channel is not cached. @@ -49,7 +50,6 @@ const User = require("./User"); * @prop {Boolean} tts Whether to play the message using TTS or not * @prop {Number} type The type of the message * @prop {String?} webhookID ID of the webhook that sent the message - */ class Message extends Base { constructor(data, client) { @@ -222,6 +222,9 @@ class Message extends Base { data.content = "This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery."; break; } + case MessageTypes.THREAD_CREATED: { + break; + } case MessageTypes.REPLY: { break; } @@ -231,6 +234,9 @@ class Message extends Base { case MessageTypes.CONTEXT_MENU_COMMAND: { break; } + case MessageTypes.THREAD_STARTER_MESSAGE: { + break; + } case MessageTypes.GUILD_INVITE_REMINDER: { data.content = "Wondering who to invite?\nStart by inviting anyone who can help you build the server!"; break; @@ -288,6 +294,9 @@ class Message extends Base { if(data.application !== undefined) { this.application = data.application; } + if(data.application_id !== undefined) { + this.applicationID = data.application_id; + } if(data.reactions) { data.reactions.forEach((reaction) => { @@ -384,6 +393,17 @@ class Message extends Base { return this._client.addMessageReaction.call(this._client, this.channel.id, this.id, reaction, userID); } + /** + * Create a thread with this message + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {String} options.name The thread channel name + * @returns {Promise} + */ + createThreadWithMessage(options) { + return this.client.createThreadWithMessage.call(this.client, this.channel.id, this.id, options); + } + /** * Crosspost (publish) a message to subscribed channels (NewsChannel only) * @returns {Promise} @@ -425,23 +445,23 @@ class Message extends Base { /** * Edit the message * @arg {String | Array | Object} content A string, array of strings, or object. If an object is passed: - * @arg {Array} [options.components] An array of component objects - * @arg {String} [options.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) - * @arg {Boolean} [options.components[].disabled] Whether the component is disabled (type 2 only) - * @arg {Object} [options.components[].emoji] The emoji to be displayed in the component (type 2) - * @arg {String} [options.components[].label] The label to be displayed in the component (type 2) + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) - * @arg {Array} [options.components[].options] The options for this component (type 3 only) - * @arg {Boolean} [options.components[].options[].default] Whether this option should be the default value selected - * @arg {String} [options.components[].options[].description] The description for this option - * @arg {Object} [options.components[].options[].emoji] The emoji to be displayed in this option - * @arg {String} options.components[].options[].label The label for this option - * @arg {Number | String} options.components[].options[].value The value for this option - * @arg {String} [options.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) - * @arg {Number} [options.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required - * @arg {Number} options.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu - * @arg {String} [options.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) * @arg {String} content.content A content string * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure @@ -450,6 +470,13 @@ class Message extends Base { * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {String} content.content A content string + * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file + * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference * @returns {Promise} */ edit(content) { diff --git a/lib/structures/NewsThreadChannel.js b/lib/structures/NewsThreadChannel.js new file mode 100644 index 000000000..053074517 --- /dev/null +++ b/lib/structures/NewsThreadChannel.js @@ -0,0 +1,15 @@ +"use strict"; + +const ThreadChannel = require("./ThreadChannel"); + +/** +* Represents a news thread channel. See ThreadChannel for extra properties. +* @extends ThreadChannel +*/ +class NewsThreadChannel extends ThreadChannel { + constructor(data, client, messageLimit) { + super(data, client, messageLimit); + } +} + +module.exports = NewsThreadChannel; diff --git a/lib/structures/PrivateChannel.js b/lib/structures/PrivateChannel.js index 54ff072f8..8ae63e89d 100644 --- a/lib/structures/PrivateChannel.js +++ b/lib/structures/PrivateChannel.js @@ -39,7 +39,6 @@ class PrivateChannel extends Channel { /** * Create a message in a text channel - * Note: If you want to DM someone, the user ID is **not** the DM channel ID. use Client.getDMChannel() to get the DM channel ID for a user * @arg {String | Object} content A string or object. If an object is passed: * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. @@ -122,6 +121,9 @@ class PrivateChannel extends Channel { * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) * @arg {Object} [content.embed] [DEPRECATED] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure. Use `embeds` instead * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference * @returns {Promise} */ diff --git a/lib/structures/PrivateThreadChannel.js b/lib/structures/PrivateThreadChannel.js new file mode 100644 index 000000000..69643e6d2 --- /dev/null +++ b/lib/structures/PrivateThreadChannel.js @@ -0,0 +1,34 @@ +"use strict"; + +const ThreadChannel = require("./ThreadChannel"); + +/** +* Represents a private thread channel. See ThreadChannel for extra properties. +* @extends ThreadChannel +* @prop {Object} threadMetadata Metadata for the thread +* @prop {Number} threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity +* @prop {Boolean} threadMetadata.archived Whether the thread is archived +* @prop {Number} threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 +* @prop {Boolean} threadMetadata.invitable Whether non-moderators can add other non-moderators to the thread +* @prop {Boolean} threadMetadata.locked Whether the thread is locked +*/ +class PrivateThreadChannel extends ThreadChannel { + constructor(data, client, messageLimit) { + super(data, client, messageLimit); + this.update(data); + } + + update(data) { + if(data.thread_metadata !== undefined) { + this.threadMetadata = { + archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), + archived: data.thread_metadata.archived, + autoArchiveDuration: data.thread_metadata.auto_archive_duration, + invitable: data.thread_metadata.invitable, + locked: data.thread_metadata.locked + }; + } + } +} + +module.exports = PrivateThreadChannel; diff --git a/lib/structures/PublicThreadChannel.js b/lib/structures/PublicThreadChannel.js new file mode 100644 index 000000000..62cf74a86 --- /dev/null +++ b/lib/structures/PublicThreadChannel.js @@ -0,0 +1,15 @@ +"use strict"; + +const ThreadChannel = require("./ThreadChannel"); + +/** +* Represents a public thread channel. See ThreadChannel for extra properties. +* @extends ThreadChannel +*/ +class PublicThreadChannel extends ThreadChannel { + constructor(data, client, messageLimit) { + super(data, client, messageLimit); + } +} + +module.exports = PublicThreadChannel; diff --git a/lib/structures/StageChannel.js b/lib/structures/StageChannel.js index 68df25ebc..25b5e9d66 100644 --- a/lib/structures/StageChannel.js +++ b/lib/structures/StageChannel.js @@ -15,6 +15,44 @@ class StageChannel extends VoiceChannel { } } + /** + * Create a stage instance + * @arg {Object} options The stage instance options + * @arg {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only + * @arg {String} options.topic The stage instance topic + * @returns {Promise} + */ + createInstance(options) { + return this.client.createStageInstance.call(this.client, this.id, options); + } + + /** + * Delete the stage instance for this channel + * @returns {Promise} + */ + deleteInstance() { + return this.client.deleteStageInstance.call(this.client, this.id); + } + + /** + * Update the stage instance for this channel + * @arg {Object} options The properties to edit + * @arg {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only + * @arg {String} [options.topic] The stage instance topic + * @returns {Promise} + */ + editInstance(options) { + return this.client.editStageInstance.call(this.client, this.id, options); + } + + /** + * Get the stage instance for this channel + * @returns {Promise} + */ + getInstance() { + return this.client.getStageInstance.call(this.client, this.id); + } + toJSON(props = []) { return super.toJSON([ "topic", diff --git a/lib/structures/StageInstance.js b/lib/structures/StageInstance.js new file mode 100644 index 000000000..e13b3e184 --- /dev/null +++ b/lib/structures/StageInstance.js @@ -0,0 +1,55 @@ +"use strict"; + +const Base = require("./Base"); + +/** +* Represents a stage instance +* @prop {StageChannel} channel The associated stage channel +* @prop {Boolean} discoverableDisabled Whether or not stage discovery is disabled +* @prop {Guild} guild The guild of the associated stage channel +* @prop {String} id The ID of the stage instance +* @prop {Number} privacyLevel The privacy level of the stage instance. 1 is public, 2 is guild only +* @prop {String} topic The stage instance topic +*/ +class StageInstance extends Base { + constructor(data, client) { + super(data.id); + this._client = client; + this.channel = client.getChannel(data.channel_id) || {id: data.channel_id}; + this.guild = client.guilds.get(data.guild_id) || {id: data.guild_id}; + this.update(data); + } + + update(data) { + if(data.discoverable_disabled !== undefined) { + this.discoverableDisabled = data.discoverable_disabled; + } + if(data.privacy_level !== undefined) { + this.privacyLevel = data.privacy_level; + } + if(data.topic !== undefined) { + this.topic = data.topic; + } + } + + /** + * Delete this stage instance + * @returns {Promise} + */ + delete() { + return this._client.deleteStageInstance.call(this._client, this.channel.id); + } + + /** + * Update this stage instance + * @arg {Object} options The properties to edit + * @arg {Number} [options.privacyLevel] The privacy level of the stage instance. 1 is public, 2 is guild only + * @arg {String} [options.topic] The stage instance topic + * @returns {Promise} + */ + edit(options) { + return this._client.editStageInstance.call(this._client, this.channel.id, options); + } +} + +module.exports = StageInstance; diff --git a/lib/structures/TextChannel.js b/lib/structures/TextChannel.js index 88c5faf5e..d34a339ab 100644 --- a/lib/structures/TextChannel.js +++ b/lib/structures/TextChannel.js @@ -7,6 +7,7 @@ const Message = require("./Message"); /** * Represents a guild text channel. See GuildChannel for more properties and methods. * @extends GuildChannel +* @prop {Number} defaultAutoArchiveDuration The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) * @prop {String} lastMessageID The ID of the last message in this channel * @prop {Number} lastPinTimestamp The timestamp of the last pinned message * @prop {Collection} messages Collection of Messages in this channel @@ -31,6 +32,9 @@ class TextChannel extends GuildChannel { if(data.topic !== undefined) { this.topic = data.topic; } + if(data.default_auto_archive_duration !== undefined) { + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } } /** @@ -60,7 +64,6 @@ class TextChannel extends GuildChannel { /** * Create a message in the channel - * Note: If you want to DM someone, the user ID is **not** the DM channel ID. use Client.getDMChannel() to get the DM channel ID for a user * @arg {String | Object} content A string or object. If an object is passed: * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. @@ -104,6 +107,31 @@ class TextChannel extends GuildChannel { return this.client.createMessage.call(this.client, this.id, content, file); } + /** + * Create a thread with an existing message + * @arg {String} messageID The ID of the message to create the thread from + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {String} options.name The thread channel name + * @returns {Promise} + */ + createThreadWithMessage(messageID, options) { + return this.client.createThreadWithMessage.call(this.client, this.id, messageID, options); + } + + /** + * Create a thread without an existing message + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) + * @arg {String} options.name The thread channel name + * @arg {Number} [options.type] The channel type of the thread to create. It is recommended to explicitly set this property as this will be a required property in API v10 + * @returns {Promise} + */ + createThreadWithoutMessage(options) { + return this.client.createThreadWithoutMessage.call(this.client, this.id, options); + } + /** * Create a channel webhook * @arg {Object} options Webhook options @@ -165,6 +193,9 @@ class TextChannel extends GuildChannel { * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) * @arg {Object} [content.embed] [DEPRECATED] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure. Use `embeds` instead * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference * @returns {Promise} */ @@ -172,6 +203,26 @@ class TextChannel extends GuildChannel { return this.client.editMessage.call(this.client, this.id, messageID, content); } + /** + * [DEPRECATED] Get all active threads in this channel. Use guild.getActiveThreads instead + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getActiveThreads() { + return this.client.getActiveThreads.call(this.client, this.id); + } + + /** + * Get all archived threads in this channel + * @arg {String} type The type of thread channel, either "public" or "private" + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getArchivedThreads(type, options) { + return this.client.getArchivedThreads.call(this.client, this.id, type, options); + } + /** * Get all invites in the channel * @returns {Promise>} @@ -180,6 +231,17 @@ class TextChannel extends GuildChannel { return this.client.getChannelInvites.call(this.client, this.id); } + /** + * Get joined private archived threads in this channel + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getJoinedPrivateArchivedThreads(options) { + return this.client.getJoinedPrivateArchivedThreads.call(this.client, this.id, options); + } + /** * Get a previous message in the channel * @arg {String} messageID The ID of the message diff --git a/lib/structures/ThreadChannel.js b/lib/structures/ThreadChannel.js new file mode 100644 index 000000000..75c32e212 --- /dev/null +++ b/lib/structures/ThreadChannel.js @@ -0,0 +1,303 @@ +"use strict"; + +const Collection = require("../util/Collection"); +const GuildChannel = require("./GuildChannel"); +const Message = require("./Message"); +const ThreadMember = require("./ThreadMember"); + +/** +* Represents a thread channel. You also probably want to look at NewsThreadChannel, PublicThreadChannel, and PrivateThreadChannel. See GuildChannel for extra properties. +* @extends GuildChannel +* @prop {String} lastMessageID The ID of the last message in this channel +* @prop {Object?} member Thread member for the current user, if they have joined the thread +* @prop {Number} member.flags The user's thread settings +* @prop {String} member.id The ID of the thread +* @prop {Number} member.joinTimestamp The time the user last joined the thread +* @prop {String} member.userID The ID of the user +* @prop {Number} memberCount An approximate number of users in the thread (stops at 50) +* @prop {Collection} members Collection of members in this channel +* @prop {Number} messageCount An approximate number of messages in the thread (stops at 50) +* @prop {Collection} messages Collection of Messages in this channel +* @prop {String} ownerID The ID of the user that created the thread +* @prop {Number} rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled +* @prop {Object} threadMetadata Metadata for the thread +* @prop {Number} threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity +* @prop {Boolean} threadMetadata.archived Whether the thread is archived +* @prop {Number} threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 +* @prop {Boolean} threadMetadata.locked Whether the thread is locked +*/ +class ThreadChannel extends GuildChannel { + constructor(data, client, messageLimit) { + super(data, client); + this.messages = new Collection(Message, messageLimit == null ? client.options.messageLimit : messageLimit); + this.members = new Collection(ThreadMember); + this.lastMessageID = data.last_message_id || null; + this.ownerID = data.ownerID; + this.update(data); + } + + update(data) { + super.update(data); + if(data.member_count !== undefined) { + this.memberCount = data.member_count; + } + if(data.message_count !== undefined) { + this.messageCount = data.message_count; + } + if(data.rate_limit_per_user !== undefined) { + this.rateLimitPerUser = data.rate_limit_per_user; + } + if(data.thread_metadata !== undefined) { + this.threadMetadata = { + archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), + archived: data.thread_metadata.archived, + autoArchiveDuration: data.thread_metadata.auto_archive_duration, + locked: data.thread_metadata.locked + }; + } + if(data.member !== undefined) { + this.member = new ThreadMember(data.member, this.client); + } + } + + /** + * Add a reaction to a message + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @returns {Promise} + */ + addMessageReaction(messageID, reaction) { + return this.client.addMessageReaction.call(this.client, this.id, messageID, reaction); + } + + /** + * Create a message in the channel + * @arg {String | Object} content A string or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Boolean} [options.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to + * @arg {String} content.content A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object} [content.messageReference] The message reference, used when replying to messages + * @arg {String} [content.messageReference.channelID] The channel ID of the referenced message + * @arg {Boolean} [content.messageReference.failIfNotExists=true] Whether to throw an error if the message reference doesn't exist. If false, and the referenced message doesn't exist, the message is created without a referenced message + * @arg {String} [content.messageReference.guildID] The guild ID of the referenced message + * @arg {String} content.messageReference.messageID The message ID of the referenced message. This cannot reference a system message + * @arg {String} [content.messageReferenceID] [DEPRECATED] The ID of the message should be replied to. Use `messageReference` instead + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object} [file] A file object + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + createMessage(content, file) { + return this.client.createMessage.call(this.client, this.id, content, file); + } + + /** + * Delete a message + * @arg {String} messageID The ID of the message + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + deleteMessage(messageID, reason) { + return this.client.deleteMessage.call(this.client, this.id, messageID, reason); + } + + /** + * Bulk delete messages (bot accounts only) + * @arg {Array} messageIDs Array of message IDs to delete + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + deleteMessages(messageIDs, reason) { + return this.client.deleteMessages.call(this.client, this.id, messageIDs, reason); + } + + /** + * Edit a message + * @arg {String} messageID The ID of the message + * @arg {String | Array | Object} content A string, array of strings, or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {String} content.content A content string + * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file + * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference + * @returns {Promise} + */ + editMessage(messageID, content) { + return this.client.editMessage.call(this.client, this.id, messageID, content); + } + + /** + * Get a list of members that are part of this thread channel + * @returns {Promise>} + */ + getMembers() { + return this.client.getThreadMembers.call(this.client, this.id); + } + + /** + * Get a previous message in the channel + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + getMessage(messageID) { + return this.client.getMessage.call(this.client, this.id, messageID); + } + + /** + * Get a list of users who reacted with a specific reaction + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {Object} [options] Options for the request. + * @arg {Number} [options.limit=100] The maximum number of users to get + * @arg {String} [options.after] Get users after this user ID + * @returns {Promise>} + */ + getMessageReaction(messageID, reaction, options) { + return this.client.getMessageReaction.call(this.client, this.id, messageID, reaction, options); + } + + /** + * Get previous messages in the channel + * @arg {Object} [options] Options for the request + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.around] Get messages around this message ID (does not work with limit > 100) + * @arg {String} [options.before] Get messages before this message ID + * @arg {Number} [options.limit=50] The max number of messages to get + * @returns {Promise>} + */ + getMessages(options) { + return this.client.getMessages.call(this.client, this.id, options); + } + + /** + * Get all the pins in the channel + * @returns {Promise>} + */ + getPins() { + return this.client.getPins.call(this.client, this.id); + } + + /** + * Join a thread + * @arg {String} [userID="@me"] The user ID of the user joining + * @returns {Promise} + */ + join(userID) { + return this.client.joinThread.call(this.client, this.id, userID); + } + + /** + * Leave a thread + * @arg {String} [userID="@me"] The user ID of the user leaving + * @returns {Promise} + */ + leave(userID) { + return this.client.leaveThread.call(this.client, this.id, userID); + } + + /** + * Pin a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + pinMessage(messageID) { + return this.client.pinMessage.call(this.client, this.id, messageID); + } + + /** + * Purge previous messages in the channel with an optional filter (bot accounts only) + * @arg {Object} options Options for the request. If this is a number + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.before] Get messages before this message ID + * @arg {Function} [options.filter] Optional filter function that returns a boolean when passed a Message object + * @arg {Number} options.limit The max number of messages to search through, -1 for no limit + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @returns {Promise} Resolves with the number of messages deleted + */ + purge(options) { + return this.client.purgeChannel.call(this.client, this.id, options); + } + + /** + * Remove a reaction from a message + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {String} [userID="@me"] The ID of the user to remove the reaction for + * @returns {Promise} + */ + removeMessageReaction(messageID, reaction, userID) { + return this.client.removeMessageReaction.call(this.client, this.id, messageID, reaction, userID); + } + + /** + * Remove all reactions from a message for a single emoji + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @returns {Promise} + */ + removeMessageReactionEmoji(messageID, reaction) { + return this.client.removeMessageReactionEmoji.call(this.client, this.id, messageID, reaction); + } + + /** + * Remove all reactions from a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + removeMessageReactions(messageID) { + return this.client.removeMessageReactions.call(this.client, this.id, messageID); + } + + /** + * Send typing status in the channel + * @returns {Promise} + */ + sendTyping() { + return this.client.sendChannelTyping.call(this.client, this.id); + } + + /** + * Unpin a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + unpinMessage(messageID) { + return this.client.unpinMessage.call(this.client, this.id, messageID); + } + + /** + * Un-send a message. You're welcome Programmix + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + unsendMessage(messageID) { + return this.client.deleteMessage.call(this.client, this.id, messageID); + } + + toJSON(props = []) { + return super.toJSON([ + "lastMessageID", + "memberCount", + "messageCount", + "messages", + "ownerID", + "rateLimitPerUser", + "threadMetadata", + "member", + ...props + ]); + } +} + +module.exports = ThreadChannel; diff --git a/lib/structures/ThreadMember.js b/lib/structures/ThreadMember.js new file mode 100644 index 000000000..91dbb44c5 --- /dev/null +++ b/lib/structures/ThreadMember.js @@ -0,0 +1,34 @@ +"use strict"; + +const Base = require("./Base"); + +class ThreadMember extends Base { + constructor(data, client) { + super(data.user_id); + this.client = client; + this.threadID = data.thread_id || data.id; // Thanks Discord + this.joinTimestamp = Date.parse(data.join_timestamp); + // this.guildMember FIXME We need to somehow get the guild for this to be possible. Ping me in the Eris server if you have suggestions or if we should just leave this out + this.update(data); + } + + update(data) { + if(data.flags !== undefined) { + this.flags = data.flags; + } + } + + leave() { + return this.client.leaveThread.call(this.client, this.threadID, this.id); + } + + toJSON(props = []) { + return super.toJSON([ + "threadID", + "joinTimestamp", + ...props + ]); + } +} + +module.exports = ThreadMember;