feat: api v9 and threads (#5570)

Co-authored-by: Noel <icrawltogo@gmail.com>
Co-authored-by: Amish Shah <dev@shah.gg>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: SynthGhost <60333233+synthghost@users.noreply.github.com>
Co-authored-by: SpaceEEC <24881032+SpaceEEC@users.noreply.github.com>
Co-authored-by: Elliot <elliot@maisl.fr>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
This commit is contained in:
ckohen
2021-06-24 12:48:29 -07:00
committed by GitHub
parent ea49f7ca74
commit 7346621d15
34 changed files with 1461 additions and 24 deletions

View File

@@ -34,6 +34,11 @@ class ActionsManager {
this.register(require('./GuildEmojiDelete'));
this.register(require('./GuildEmojiUpdate'));
this.register(require('./GuildEmojisUpdate'));
this.register(require('./ThreadCreate'));
this.register(require('./ThreadDelete'));
this.register(require('./ThreadListSync'));
this.register(require('./ThreadMemberUpdate'));
this.register(require('./ThreadMembersUpdate'));
this.register(require('./GuildRolesPositionUpdate'));
this.register(require('./GuildChannelsPositionUpdate'));
this.register(require('./GuildIntegrationsUpdate'));

View File

@@ -0,0 +1,23 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class ThreadCreateAction extends Action {
handle(data) {
const client = this.client;
const existing = client.channels.cache.has(data.id);
const thread = client.channels.add(data);
if (!existing && thread) {
/**
* Emitted whenever a thread is created or when the client user is added to a thread.
* @event Client#threadCreate
* @param {ThreadChannel} thread The thread that was created
*/
client.emit(Events.THREAD_CREATE, thread);
}
return { thread };
}
}
module.exports = ThreadCreateAction;

View File

@@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class ThreadDeleteAction extends Action {
handle(data) {
const client = this.client;
const thread = client.channels.cache.get(data.id);
if (thread) {
client.channels.remove(thread.id);
thread.deleted = true;
for (const message of thread.messages.cache.values()) {
message.deleted = true;
}
/**
* Emitted whenever a thread is deleted.
* @event Client#threadDelete
* @param {ThreadChannel} thread The thread that was deleted
*/
client.emit(Events.THREAD_DELETE, thread);
}
return { thread };
}
}
module.exports = ThreadDeleteAction;

View File

@@ -0,0 +1,59 @@
'use strict';
const Action = require('./Action');
const Collection = require('../../util/Collection');
const { Events } = require('../../util/Constants');
class ThreadListSyncAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (!guild) return {};
if (data.channels_ids) {
for (const id of data.channel_ids) {
const channel = client.channels.resolve(id);
if (channel) this.removeStale(channel);
}
} else {
for (const channel of guild.channels.cache.values()) {
this.removeStale(channel);
}
}
const syncedThreads = data.threads.reduce((coll, rawThread) => {
const thread = client.channels.add(rawThread);
return coll.set(thread.id, thread);
}, new Collection());
for (const rawMember of Object.values(data.members)) {
// Discord sends the thread id as id in this object
const thread = client.channels.cache.get(rawMember.id);
if (thread) {
thread.members._add(rawMember);
}
}
/**
* Emitted whenever the client user gains access to a text or news channel that contains threads
* @event Client#threadListSync
* @param {Collection<Snowflake, ThreadChannel>} threads The threads that were synced
*/
client.emit(Events.THREAD_LIST_SYNC, syncedThreads);
return {
syncedThreads,
};
}
removeStale(channel) {
channel.threads?.cache.forEach(thread => {
if (!thread.archived) {
this.client.channels.remove(thread.id);
}
});
}
}
module.exports = ThreadListSyncAction;

View File

@@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class ThreadMemberUpdateAction extends Action {
handle(data) {
const client = this.client;
// Discord sends the thread id as id in this object
const thread = client.channels.cache.get(data.id);
if (thread) {
const member = thread.members.cache.get(data.user_id);
if (!member) {
const newMember = thread.members._add(data);
return { newMember };
}
const old = member._update(data);
/**
* Emitted whenever the client user's thread member is updated.
* @event Client#threadMemberUpdate
* @param {ThreadMember} oldMember The member before the update
* @param {ThreadMember} newMember The member after the update
*/
client.emit(Events.THREAD_MEMBER_UPDATE, old, member);
}
return {};
}
}
module.exports = ThreadMemberUpdateAction;

View File

@@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class ThreadMembersUpdateAction extends Action {
handle(data) {
const client = this.client;
const thread = client.channels.cache.get(data.id);
if (thread) {
const old = thread.members.cache.clone();
thread.memberCount = data.member_count;
data.added_members?.forEach(rawMember => {
thread.members._add(rawMember);
});
data.removed_member_ids?.forEach(memberId => {
thread.members.cache.delete(memberId);
});
/**
* Emitted whenever members are added or removed from a thread. Requires `GUILD_MEMBERS` privileged intent
* @event Client#threadMembersUpdate
* @param {Collection<Snowflake, ThreadMember>} oldMembers The members before the update
* @param {Collection<Snowflake, ThreadMember>} newMembers The members after the update
*/
client.emit(Events.THREAD_MEMBERS_UPDATE, old, thread.members.cache);
}
return {};
}
}
module.exports = ThreadMembersUpdateAction;

View File

@@ -2,7 +2,7 @@
const Action = require('./Action');
const { Events } = require('../../util/Constants');
const textBasedChannelTypes = ['dm', 'text', 'news'];
const textBasedChannelTypes = ['dm', 'text', 'news', 'news_thread', 'public_thread', 'private_thread'];
class TypingStart extends Action {
handle(data) {

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadListSync.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadMembersUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ThreadMemberUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,16 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, packet) => {
const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a thread is updated - e.g. name change, archive state change, locked state change.
* @event Client#threadUpdate
* @param {ThreadChannel} oldThread The thread before the update
* @param {ThreadChannel} newThread The thread after the update
*/
client.emit(Events.THREAD_UPDATE, old, updated);
}
};

View File

@@ -92,6 +92,8 @@ const Messages = {
INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`,
INVALID_ELEMENT: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`,
MESSAGE_THREAD_PARENT: 'The message was not sent in a guild text or news channel',
WEBHOOK_MESSAGE: 'The message was not sent by a webhook.',
WEBHOOK_TOKEN_UNAVAILABLE: 'This action requires a webhook token, but none is available.',
MESSAGE_REFERENCE_MISSING: 'The message does not reference another message',

View File

@@ -26,6 +26,7 @@ module.exports = {
SnowflakeUtil: require('./util/SnowflakeUtil'),
Structures: require('./util/Structures'),
SystemChannelFlags: require('./util/SystemChannelFlags'),
ThreadMemberFlags: require('./util/ThreadMemberFlags'),
UserFlags: require('./util/UserFlags'),
Util: require('./util/Util'),
version: require('../package.json').version,
@@ -47,6 +48,8 @@ module.exports = {
MessageManager: require('./managers/MessageManager'),
PresenceManager: require('./managers/PresenceManager'),
RoleManager: require('./managers/RoleManager'),
ThreadManager: require('./managers/ThreadManager'),
ThreadMemberManager: require('./managers/ThreadMemberManager'),
UserManager: require('./managers/UserManager'),
// Structures
@@ -109,6 +112,8 @@ module.exports = {
Team: require('./structures/Team'),
TeamMember: require('./structures/TeamMember'),
TextChannel: require('./structures/TextChannel'),
ThreadChannel: require('./structures/ThreadChannel'),
ThreadMember: require('./structures/ThreadMember'),
User: require('./structures/User'),
VoiceChannel: require('./structures/VoiceChannel'),
VoiceRegion: require('./structures/VoiceRegion'),

View File

@@ -2,7 +2,7 @@
const BaseManager = require('./BaseManager');
const Channel = require('../structures/Channel');
const { Events } = require('../util/Constants');
const { Events, ThreadChannelTypes } = require('../util/Constants');
/**
* A manager of channels belonging to a client
@@ -24,6 +24,9 @@ class ChannelManager extends BaseManager {
if (existing) {
if (existing._patch && cache) existing._patch(data);
if (guild) guild.channels?.add(existing);
if (ThreadChannelTypes.includes(existing.type) && typeof existing.parent?.threads !== 'undefined') {
existing.parent.threads.add(existing);
}
return existing;
}
@@ -42,6 +45,7 @@ class ChannelManager extends BaseManager {
remove(id) {
const channel = this.cache.get(id);
channel?.guild?.channels.cache.delete(id);
channel?.parent?.threads?.cache.delete(id);
this.cache.delete(id);
}

View File

@@ -4,7 +4,7 @@ const BaseManager = require('./BaseManager');
const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
const Collection = require('../util/Collection');
const { ChannelTypes } = require('../util/Constants');
const { ChannelTypes, ThreadChannelTypes } = require('../util/Constants');
/**
* Manages API methods for GuildChannels and stores their cache.
@@ -21,6 +21,19 @@ class GuildChannelManager extends BaseManager {
this.guild = guild;
}
/**
* The number of channels in this managers cache excluding thread channels
* that do not count towards a guild's maximum channels restriction.
* @type {number}
* @readonly
*/
get channelCountWithoutThreads() {
return this.cache.reduce((acc, channel) => {
if (ThreadChannelTypes.includes(channel.type)) return acc;
return ++acc;
}, 0);
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, GuildChannel>}

View File

@@ -0,0 +1,244 @@
'use strict';
const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');
const Collection = require('../util/Collection');
const { ChannelTypes } = require('../util/Constants');
/**
* Manages API methods for ThreadChannels and stores their cache.
* @extends {BaseManager}
*/
class ThreadManager extends BaseManager {
constructor(channel, iterable) {
super(channel.client, iterable, ThreadChannel);
/**
* The channel this Manager belongs to
* @type {NewsChannel|TextChannel}
*/
this.channel = channel;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadChannel>}
* @name ThreadManager#cache
*/
add(thread) {
const existing = this.cache.get(thread.id);
if (existing) return existing;
this.cache.set(thread.id, thread);
return thread;
}
/**
* Data that can be resolved to give a Thread Channel object. This can be:
* * A ThreadChannel object
* * A Snowflake
* @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable
*/
/**
* Resolves a ThreadChannelResolvable to a Thread Channel object.
* @method resolve
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?ThreadChannel}
*/
/**
* Resolves a ThreadChannelResolvable to a thread channel ID string.
* @method resolveID
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?Snowflake}
*/
/**
* A number that is allowed to be the duration in minutes before a thread is automatically archived. This can be:
* * `60` (1 hour)
* * `1440` (1 day)
* * `4320` (3 days)
* * `10080` (7 days)
* @typedef {number} ThreadAutoArchiveDuration
*/
/**
* Options for creating a thread <warn>Only one of `startMessage` or `type` can be defined.
* If `startMessage` is defined, `type` is automatically defined and cannot be changed.</warn>
* @typedef {Object} ThreadCreateOptions
* @property {string} name The name of the new Thread
* @property {ThreadAutoArchiveDuration} autoArchiveDuration How long before the thread is automatically archived
* @property {MessageResolvable} [startMessage] The message to start a public or news thread from,
* creates a private thread if not provided
* @property {ThreadChannelType|number} [type] The type of thread to create
* <warn>When creating threads in a `news` channel this is always `news_thread`</warn>
* @param {string} [reason] Reason for creating the thread
*/
/**
* Creates a new thread in the channel.
* @param {ThreadCreateOptions} [options] Options
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new public thread
* channel.threads
* .create({
* name: 'food-talk'
* autoArchiveDuration: 60,
* startMessage: channel.lastMessageID,
* reason: 'Needed a separate thread for food',
* })
* .then(console.log)
* .catch(console.error);
* // Create a new private thread
* channel.threads
* .create({ name: 'mod-talk', autoArchiveDuration: 60, reason: 'Needed a separate thread for moderation' })
* .then(console.log)
* .catch(console.error);
*/
async create({ name, autoArchiveDuration, startMessage, type, reason } = {}) {
let path = this.client.api.channels(this.channel.id);
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType = this.channel.type === 'news' ? ChannelTypes.NEWS_THREAD : ChannelTypes.PUBLIC_THREAD;
if (startMessage) {
const startMessageID = this.channel.messages.resolveID(startMessage);
if (!startMessageID) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
path = path.messages(startMessageID);
} else if (this.channel.type !== 'news') {
resolvedType = typeof type === 'string' ? ChannelTypes[type.toUpperCase()] : type;
}
const data = await path.threads.post({
data: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
},
reason,
});
return this.client.actions.ThreadCreate.handle(data).thread;
}
/**
* The options for fetching multiple threads, the properties are mutually exclusive
* @typedef {Object} FetchThreadsOptions
* @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads
* @property {boolean} [active] When true, fetches active threads. <warn>If `archived` is set, this is ignored!</warn>
*/
/**
* Obtains a thread from Discord, or the channel cache if it's already available.
* @param {ThreadChannelResolvable|FetchThreadsOptions} [options] If a ThreadChannelResolvable, the thread to fetch.
* If undefined, fetches all active threads.
* If an Object, fetches the specified threads.
* @param {BaseFetchOptions} [cacheOptions] Additional options for this fetch
* only applies when fetching a single thread
* @returns {Promise<?(ThreadChannel|FetchedThreads)>}
* @example
* // Fetch a thread by its id
* channel.threads.fetch('831955138126104859')
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
fetch(options, { cache = true, force = false } = {}) {
if (!options) return this.fetchActive(cache);
const channel = this.client.channels.resolveID(options);
if (channel) return this.client.channels.fetch(channel, cache, force);
if (options.archived) {
return this.fetchArchived(options.archived, cache);
}
return this.fetchActive(cache);
}
/**
* Data that can be resolved to give a Date object. This can be:
* * A Date object
* * A number representing a timestamp
* * An ISO8601 string
* @typedef {Date|number|string} DateResolvable
*/
/**
* The options used to fetch archived threads.
* @typedef {Object} FetchArchivedThreadOptions
* @property {string} [type='public'] The type of threads to fetch, either `public` or `private`
* @property {boolean} [fetchAll=false] When type is `private` whether to fetch **all** archived threads,
* requires `MANAGE_THREADS` if true
* @property {DateResolvable|ThreadChannelResolvable} [before] Identifier for a Date or Snowflake
* to get threads that were created before it,
* must be a ThreadChannelResolvable when type is `private` and fetchAll is `false`
* @property {number} [limit] Maximum number of threads to return
*/
/**
* The data returned from a thread fetch that returns multiple threads.
* @typedef {FetchedThreads}
* @property {Collection<Snowflake, ThreadChannel>} threads The threads fetched, with any members returned
* @property {?boolean} hasMore Whether there are potentially additional threads that require a subsequent call
*/
/**
* Obtains a set of archived threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
* @param {FetchArchivedThreadOptions} [options] The options to use when fetch archived threads
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* @returns {Promise<FetchedThreads>}
*/
async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) {
let path = this.client.api.channels(this.channel.id);
if (type === 'private' && fetchAll) {
path = path.users('@me');
}
let timestamp;
let id;
if (typeof before !== 'undefined') {
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
id = this.resolveID(before);
timestamp = this.resolve(before)?.archivedAt?.toISOString();
} else {
try {
timestamp = new Date(before).toISOString();
} catch {
throw new TypeError('INVALID_TYPE', 'before', 'DateResolvable or ThreadChannelResolvable');
}
}
}
const raw = await path.threads
.archived(type)
.get({ query: { before: type === 'private' && !fetchAll ? id : timestamp, limit } });
return this._mapThreads(raw, cache);
}
/**
* Obtains the accessible active threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel.
* @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already
* @returns {Promise<FetchedThreads>}
*/
async fetchActive(cache = true) {
const raw = await this.client.api.channels(this.channel.id).threads.active.get();
return this._mapThreads(raw, cache);
}
_mapThreads(rawThreads, cache) {
const threads = rawThreads.threads.reduce((coll, raw) => {
const thread = this.client.channels.add(raw, null, cache);
return coll.set(thread.id, thread);
}, new Collection());
// Discord sends the thread id as id in this object
for (const rawMember of rawThreads.members) threads.get(rawMember.id)?.members._add(rawMember);
return {
threads,
hasMore: rawThreads.has_more,
};
}
}
module.exports = ThreadManager;

View File

@@ -0,0 +1,112 @@
'use strict';
const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
const Collection = require('../util/Collection');
/**
* Manages API methods for GuildMembers and stores their cache.
* @extends {BaseManager}
*/
class ThreadMemberManager extends BaseManager {
constructor(thread, iterable) {
super(thread.client, iterable, ThreadMember);
/**
* The thread this manager belongs to
* @type {ThreadChannel}
*/
this.thread = thread;
}
/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadMember>}
* @name ThreadMemberManager#cache
*/
_add(data, cache = true) {
const existing = this.cache.get(data.user_id);
if (cache) existing?._patch(data);
if (existing) return existing;
const member = new ThreadMember(this.thread, data);
if (cache) this.cache.set(data.user_id, member);
return member;
}
/**
* Data that resolves to give a ThreadMember object. This can be:
* * A ThreadMember object
* * A User resolvable
* @typedef {ThreadMember|UserResolvable} ThreadMemberResolvable
*/
/**
* Resolves a ThreadMemberResolvable to a ThreadMember object.
* @param {ThreadMemberResolvable} member The user that is part of the thread
* @returns {?GuildMember}
*/
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveID(member);
if (userResolvable) return super.resolve(userResolvable);
return null;
}
/**
* Resolves a ThreadMemberResolvable to a thread member ID string.
* @param {ThreadMemberResolvable} member The user that is part of the guild
* @returns {?Snowflake}
*/
resolveID(member) {
const memberResolvable = super.resolveID(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveID(member);
return this.cache.has(userResolvable) ? userResolvable : null;
}
/**
* Adds a member to the thread.
* @param {UserResolvable|'@me'} member The member to add
* @param {string} [reason] The reason for adding this member
* @returns {Promise<Snowflake>}
*/
add(member, reason) {
const id = member === '@me' ? member : this.client.users.resolveID(member);
if (!id) return Promise.reject(new TypeError('INVALID_TYPE', 'member', 'UserResolvable'));
return this.client.api
.channels(this.id, 'thread-members', id)
.put({ reason })
.then(() => id);
}
/**
* Remove a user from the thread.
* @param {Snowflake|'@me'} id The id of the member to remove
* @param {string} [reason] The reason for removing this member from the thread
* @returns {Promise<Snowflake>}
*/
remove(id, reason) {
return this.client.api
.channels(this.thread.id, 'thread-members', id)
.delete({ reason })
.then(() => id);
}
/**
* Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent.
* @param {boolean} [cache=true] Whether or not to cache the fetched members
* @returns {Promise<Collection<Snowflake, ThreadMember>>}
*/
async fetch(cache = true) {
const raw = await this.client.api.channels(this.thread.id, 'thread-members').get();
return raw.reduce((col, rawMember) => {
const member = this.add(rawMember, cache);
return col.set(member.id, member);
}, new Collection());
}
}
module.exports = ThreadMemberManager;

View File

@@ -3,6 +3,7 @@
const BaseManager = require('./BaseManager');
const GuildMember = require('../structures/GuildMember');
const Message = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
/**
@@ -26,7 +27,8 @@ class UserManager extends BaseManager {
* * A Snowflake
* * A Message object (resolves to the message author)
* * A GuildMember object
* @typedef {User|Snowflake|Message|GuildMember} UserResolvable
* * A ThreadMember object
* @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable
*/
/**
@@ -35,7 +37,7 @@ class UserManager extends BaseManager {
* @returns {?User}
*/
resolve(user) {
if (user instanceof GuildMember) return user.user;
if (user instanceof GuildMember || user instanceof ThreadMember) return user.user;
if (user instanceof Message) return user.author;
return super.resolve(user);
}
@@ -46,6 +48,7 @@ class UserManager extends BaseManager {
* @returns {?Snowflake}
*/
resolveID(user) {
if (user instanceof ThreadMember) return user.id;
if (user instanceof GuildMember) return user.user.id;
if (user instanceof Message) return user.author.id;
return super.resolveID(user);

View File

@@ -10,7 +10,7 @@ const SnowflakeUtil = require('../util/SnowflakeUtil');
* @abstract
*/
class Channel extends Base {
constructor(client, data) {
constructor(client, data, immediatePatch = true) {
super(client);
const type = ChannelTypes[data.type];
@@ -22,6 +22,9 @@ class Channel extends Base {
* * `category` - a guild category channel
* * `news` - a guild news channel
* * `store` - a guild store channel
* * 'news_thread` - a guild news channels' public thread channel
* * `public_thread` - a guild text channels' public thread channel
* * `private_thread` - a guild text channels' private thread channel
* * `stage` - a guild stage channel
* * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel
* @type {string}
@@ -34,7 +37,7 @@ class Channel extends Base {
*/
this.deleted = false;
if (data) this._patch(data);
if (data && immediatePatch) this._patch(data);
}
_patch(data) {
@@ -152,6 +155,14 @@ class Channel extends Base {
channel = new StageChannel(guild, data);
break;
}
case ChannelTypes.NEWS_THREAD:
case ChannelTypes.PUBLIC_THREAD:
case ChannelTypes.PRIVATE_THREAD: {
const ThreadChannel = Structures.get('ThreadChannel');
channel = new ThreadChannel(guild, data);
channel.parent?.threads.cache.set(channel.id, channel);
break;
}
}
if (channel) guild.channels?.cache.set(channel.id, channel);
}

View File

@@ -338,6 +338,12 @@ class Guild extends AnonymousGuild {
}
}
if (data.threads) {
for (const rawThread of data.threads) {
this.client.channels.add(rawThread, this);
}
}
if (data.roles) {
this.roles.cache.clear();
for (const role of data.roles) this.roles.add(role);

View File

@@ -368,6 +368,8 @@ class GuildChannel extends Channel {
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
*/
@@ -428,6 +430,7 @@ class GuildChannel extends Channel {
parent_id: data.parentID,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,

View File

@@ -101,6 +101,16 @@ class Message extends Base {
this.pinned = null;
}
if ('thread' in data) {
/**
* The thread started by this message
* @type {?ThreadChannel}
*/
this.thread = this.client.channels.add(data.thread);
} else if (!this.thread) {
this.thread = null;
}
if ('tts' in data) {
/**
* Whether or not the message was Text-To-Speech
@@ -224,7 +234,7 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze();
/**
* Reference data sent in a crossposted message or inline reply.
* Reference data sent in a message that contains IDs identifying the referenced message
* @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was referenced
@@ -294,6 +304,7 @@ class Message extends Base {
if ('content' in data) this.content = data.content;
if ('pinned' in data) this.pinned = data.pinned;
if ('tts' in data) this.tts = data.tts;
if ('thread' in data) this.thread = this.client.channels.add(data.thread);
if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true));
else this.embeds = this.embeds.slice();
if ('components' in data) this.components = data.components.map(c => BaseMessageComponent.create(c, this.client));
@@ -676,6 +687,21 @@ class Message extends Base {
return this.channel.send(data);
}
/**
* Create a new public thread from this message
* @see ThreadManager#create
* @param {string} name The name of the new Thread
* @param {ThreadAutoArchiveDuration} autoArchiveDuration How long before the thread is automatically archived
* @param {string} [reason] Reason for creating the thread
* @returns {Promise<ThreadChannel>}
*/
startThread(name, autoArchiveDuration, reason) {
if (!['text', 'news'].includes(this.channel.type)) {
return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
}
return this.channel.threads.create({ name, autoArchiveDuration, startMessage: this, reason });
}
/**
* Fetch this message.
* @param {boolean} [force=false] Whether to skip the cache check and request the API

View File

@@ -4,6 +4,7 @@ const GuildChannel = require('./GuildChannel');
const Webhook = require('./Webhook');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager');
const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver');
@@ -25,6 +26,12 @@ class TextChannel extends GuildChannel {
*/
this.messages = new MessageManager(this);
/**
* A manager of the threads belonging to this channel
* @type {ThreadManager}
*/
this.threads = new ThreadManager(this);
/**
* If the guild considers this channel NSFW
* @type {boolean}
@@ -73,11 +80,29 @@ class TextChannel extends GuildChannel {
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
if ('default_auto_archive_duration' in data) {
/**
* The default auto archive duration for newly created threads in this channel
* @type {?ThreadAutoArchiveDuration}
*/
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
}
if ('messages' in data) {
for (const message of data.messages) this.messages.add(message);
}
}
/**
* Sets the default auto archive duration for all newly created threads in this channel.
* @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration
* @param {string} [reason] Reason for changing the channel's default auto archive duration
* @returns {Promise<TextChannel>}
*/
setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) {
return this.edit({ defaultAutoArchiveDuration }, reason);
}
/**
* Sets the rate limit per user for this channel.
* <warn>It is not currently possible to set the rate limit per user on a `NewsChannel`.</warn>

View File

@@ -0,0 +1,379 @@
'use strict';
const Channel = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const ThreadMemberManager = require('../managers/ThreadMemberManager');
const Permissions = require('../util/Permissions');
/**
* Represents a thread channel on Discord.
* @extends {Channel}
* @implements {TextBasedChannel}
*/
class ThreadChannel extends Channel {
/**
* @param {Guild} guild The guild the thread channel is part of
* @param {Object} data The data for the thread channel
*/
constructor(guild, data) {
super(guild.client, data, false);
/**
* The guild the thread is in
* @type {Guild}
*/
this.guild = guild;
/**
* A manager of the messages set to this thread
* @type {MessageManager}
*/
this.messages = new MessageManager(this);
/**
* A manager of the members that are part of this thread
* @type {ThreadMemberManager}
*/
this.members = new ThreadMemberManager(this);
this._typing = new Map();
if (data) this._patch(data);
}
_patch(data) {
super._patch(data);
/**
* The name of the thread
* @type {string}
*/
this.name = data.name;
/**
* The ID of the parent channel to this thread
* @type {Snowflake}
*/
this.parentID = data.parent_id;
/**
* Whether the thread is locked
* @type {boolean}
*/
this.locked = data.thread_metadata.locked ?? false;
/**
* Whether the thread is active (false) or archived (true)
* @type {boolean}
*/
this.archived = data.thread_metadata.archived;
/**
* The id of the member that created this thread
* @type {?Snowflake}
*/
this.ownerID = data.owner_id;
/**
* How long in minutes after recent activity before the thread is automatically archived
* @type {number}
*/
this.autoArchiveDuration = data.thread_metadata.auto_archive_duration;
/**
* The ID of the last message sent in this thread, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = data.last_message_id;
/**
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
/**
* The ratelimit per user for this thread in seconds
* @type {number}
*/
this.rateLimitPerUser = data.rate_limit_per_user ?? 0;
/**
* The timestamp the thread was last archived or unarchived at
* @type {?number}
*/
this.archiveTimestamp = data.thread_metadata.archive_timestamp
? new Date(data.thread_metadata.archive_timestamp).getTime()
: null;
/**
* The approximate count of messages in this thread
* <info>This value will not count above 50 even when there are more than 50 messages
* If you need an approximate value higher than this, use ThreadChannel#messages.cache.size</info>
* @type {number}
*/
this.messageCount = data.message_count;
/**
* The approximate count of users in this thread
* <info>This value will not count above 50 even when there are more than 50 members</info>
* @type {number}
*/
this.memberCount = data.member_count;
if (data.member && this.client.user) this.members._add({ user_id: this.client.user.id, ...data.member });
if (data.messages) for (const message of data.messages) this.messages.add(message);
}
/**
* A collection of the guild member objects for each of this thread's members
* @type {Collection<Snowflake, GuildMember>}
* @readonly
*/
get guildMembers() {
return this.members.cache.mapValues(member => member.guildMember);
}
/**
* The time the thread was last archived or unarchived at
* @type {?Date}
* @readonly
*/
get archivedAt() {
return this.archiveTimestamp ? new Date(this.archiveTimestamp) : null;
}
/**
* The parent channel of this thread
* @type {?(NewsChannel|TextChannel)}
* @readonly
*/
get parent() {
return this.guild.channels.resolve(this.parentID);
}
/**
* Makes the client user join the thread.
* @returns {Promise<ThreadChannel>}
*/
join() {
return this.members.add('@me').then(() => this);
}
/**
* Makes the client user leave the thread.
* @returns {Promise<ThreadChannel>}
*/
leave() {
return this.members.remove('@me').then(() => this);
}
/**
* Gets the overall set of permissions for a member or role in this threads' parent, taking into account overwrites.
* @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for
* @returns {?Readonly<Permissions>}
*/
permissionsFor(memberOrRole) {
return this.parent?.permissionsFor(memberOrRole) ?? null;
}
/**
* Edits the thread.
* @param {Object} data The new data for the thread
* @param {string} [data.name] The new name for the trhead
* @param {boolean} [data.archived] Whether the thread is archived
* @param {number} [data.autoArchiveDuration] How long in minutes before the thread is automatically archived,
* one of `60`, `1440`, `4320`, or `10080`
* @param {number} [data.rateLimitPerUser] The ratelimit per user for the thread in seconds
* @param {boolean} [data.locked] Whether the thread is locked
* @param {string} [reason] Reason for editing this thread
* @returns {Promise<ThreadChannel>}
* @example
* // Edit a thread
* thread.edit({ name: 'new-thread' })
* .then(console.log)
* .catch(console.error);
*/
async edit(data, reason) {
const newData = await this.client.api.channels(this.id).patch({
data: {
name: (data.name || this.name).trim(),
archived: data.archived,
auto_archive_duration: data.autoArchiveDuration,
rate_limit_per_user: data.rateLimitPerUser,
locked: data.locked,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
}
/**
* Sets whether the thread is archived.
* @param {boolean} [archived=true] Whether the thread is archived
* @param {string} [reason] Reason for archiving or unarchiving
* @returns {Promise<ThreadChannel>}
* @example
* // Set the thread to archived
* thread.setArchived(true)
* .then(newThread => console.log(`Thread is now ${newThread.archived ? 'archived' : 'active'}`))
* .catch(console.error);
*/
setArchived(archived = true, reason) {
return this.edit({ archived }, reason);
}
/**
* Sets the duration before the channel is automatically archived.
* @param {ThreadAutoArchiveDuration} autoArchiveDuration How long before the thread is automatically archived
* @param {string} [reason] Reason for changing the archive time
* @returns {Promise<ThreadChannel>}
* @example
* // Set the thread auto archive time to 1 hour
* thread.setAutoArchiveDuration(60)
* .then(newThread => {
* console.log(`Thread will now archive after ${newThread.autoArchiveDuration}`);
* });
* .catch(console.error);
*/
setAutoArchiveDuration(autoArchiveDuration, reason) {
return this.edit({ autoArchiveDuration }, reason);
}
/**
* Sets whether the thread can be archived by anyone or just mods.
* @param {boolean} [locked=true] Whether the thread is locked
* @param {string} [reason] Reason for archiving or unarchiving
* @returns {Promise<ThreadChannel>}
* @example
* // Set the thread to locked
* thread.setLocked(true)
* .then(newThread => console.log(`Thread is now ${newThread.locked ? 'locked' : 'unlocked'}`))
* .catch(console.error);
*/
setLocked(locked = true, reason) {
return this.edit({ locked }, reason);
}
/**
* Sets a new name for the thread.
* @param {string} name The new name for the thread
* @param {string} [reason] Reason for changing the thread's name
* @returns {Promise<ThreadChannel>}
* @example
* // Set a new thread name
* thread.setName('not_general')
* .then(newThread => console.log(`Thread's new name is ${newThread.name}`))
* .catch(console.error);
*/
setName(name, reason) {
return this.edit({ name }, reason);
}
/**
* Sets the rate limit per user for this thread.
* @param {number} rateLimitPerUser The new ratelimit in seconds
* @param {string} [reason] Reason for changing the thread's ratelimits
* @returns {Promise<ThreadChannel>}
*/
setRateLimitPerUser(rateLimitPerUser, reason) {
return this.edit({ rateLimitPerUser }, reason);
}
/**
* Whether the thread is editable by the client user (name, archived, autoArchiveDuration)
* @type {boolean}
* @readonly
*/
get editable() {
return this.ownerID === this.client.user.id || this.manageable;
}
/**
* Whether the thread is joinable by the client user
* @type {boolean}
* @readonly
*/
get joinable() {
return (
!this.archived &&
this.permissionsFor(this.client.user)?.has(
this.type === 'private_thread' ? Permissions.FLAGS.MANAGE_THREADS : Permissions.FLAGS.VIEW_CHANNEL,
false,
)
);
}
/**
* Whether the thread is manageable by the client user, for deleting or editing rateLimitPerUser or locked.
* @type {boolean}
* @readonly
*/
get manageable() {
return this.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_THREADS, false);
}
/**
* Whether the client user can send messages in this thread
* @type {boolean}
* @readonly
*/
get sendable() {
return (
!this.archived &&
this.permissionsFor(this.client.user)?.any(
[
Permissions.FLAGS.SEND_MESSAGES,
this.type === 'private_thread' ? Permissions.FLAGS.USE_PRIVATE_THREADS : Permissions.FLAGS.USE_PUBLIC_THREADS,
],
false,
)
);
}
/**
* Whether the thread is unarchivable by the client user
* @type {boolean}
* @readonly
*/
get unarchivable() {
return this.archived && (this.locked ? this.manageable : this.sendable);
}
/**
* Deletes this thread.
* @param {string} [reason] Reason for deleting this thread
* @returns {Promise<ThreadChannel>}
* @example
* // Delete the thread
* thread.delete('cleaning out old threads')
* .then(console.log)
* .catch(console.error);
*/
delete(reason) {
return this.client.api
.channels(this.id)
.delete({ reason })
.then(() => this);
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastMessage() {}
get lastPinAt() {}
send() {}
startTyping() {}
stopTyping() {}
get typing() {}
get typingCount() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentInteractionCollector() {}
awaitMessageComponentInteractions() {}
bulkDelete() {}
}
TextBasedChannel.applyToClass(ThreadChannel, true);
module.exports = ThreadChannel;

View File

@@ -0,0 +1,95 @@
'use strict';
const Base = require('./Base');
const ThreadMemberFlags = require('../util/ThreadMemberFlags');
/**
* Represents a Member for a Thread.
* @extends {Base}
*/
class ThreadMember extends Base {
/**
* @param {ThreadChannel} thread The thread that this member is associated with
* @param {Object} data The data for the thread member
*/
constructor(thread, data) {
super(thread.client);
/**
* The thread that this member is a part of
* @type {ThreadChannel}
*/
this.thread = thread;
/**
* The timestamp the member last joined the thread at
* @type {?number}
*/
this.joinedTimestamp = null;
/**
* The id of the thread member
* @type {Snowflake}
*/
this.id = data.user_id;
this._patch(data);
}
_patch(data) {
this.joinedTimestamp = new Date(data.join_timestamp).getTime();
/**
* The flags for this thread member
* @type {ThreadMemberFlags}
*/
this.flags = new ThreadMemberFlags(data.flags).freeze();
}
/**
* The guild member that this thread member instance represents
* @type {?GuildMember}
* @readonly
*/
get guildMember() {
return this.thread.guild.members.resolve(this.id);
}
/**
* The last time this member joined the thread
* @type {?Date}
* @readonly
*/
get joinedAt() {
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
}
/**
* The user that this thread member instance represents
* @type {?User}
* @readonly
*/
get user() {
return this.client.users.resolve(this.id);
}
/**
* Whether the client user can manage this thread member
* @type {boolean}
* @readonly
*/
get manageable() {
return !this.thread.archived && this.thread.editable;
}
/**
* Remove this member from the thread.
* @param {string} [reason] Reason for removing the member
* @returns {ThreadMember}
*/
remove(reason) {
return this.thread.members.remove(this.id, reason).then(() => this);
}
}
module.exports = ThreadMember;

View File

@@ -92,6 +92,8 @@ class Webhook {
* @typedef {BaseMessageOptions} WebhookMessageOptions
* @property {string} [username=this.name] Username override for the message
* @property {string} [avatarURL] Avatar URL override for the message
* @property {Snowflake} [threadID] The id of the thread in the channel to send to.
* <info>For interaction webhooks, this property is ignored</info>
*/
/**
@@ -115,6 +117,11 @@ class Webhook {
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
* @example
* // Send a basic message in a thread
* webhook.send('hello!', { threadID: '836856309672348295' })
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
* @example
* // Send a remote file
* webhook.send({
* files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048']
@@ -169,7 +176,7 @@ class Webhook {
.post({
data,
files,
query: { wait: true },
query: { thread_id: apiMessage.options.threadID, wait: true },
auth: false,
})
.then(d => {

View File

@@ -91,13 +91,13 @@ exports.DefaultOptions = {
$browser: 'discord.js',
$device: 'discord.js',
},
version: 8,
version: 9,
},
/**
* HTTP options
* @typedef {Object} HTTPOptions
* @property {number} [version=8] API version to use
* @property {number} [version=9] API version to use
* @property {string} [api='https://discord.com/api'] Base url of the API
* @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN
* @property {string} [invite='https://discord.gg'] Base url of invites
@@ -105,7 +105,7 @@ exports.DefaultOptions = {
* @property {Object} [headers] Additional headers to send for all API requests
*/
http: {
version: 8,
version: 9,
api: 'https://discord.com/api',
cdn: 'https://cdn.discordapp.com',
invite: 'https://discord.gg',
@@ -262,6 +262,12 @@ exports.Events = {
MESSAGE_REACTION_REMOVE: 'messageReactionRemove',
MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll',
MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji',
THREAD_CREATE: 'threadCreate',
THREAD_DELETE: 'threadDelete',
THREAD_UPDATE: 'threadUpdate',
THREAD_LIST_SYNC: 'threadListSync',
THREAD_MEMBER_UPDATE: 'threadMemberUpdate',
THREAD_MEMBERS_UPDATE: 'threadMembersUpdate',
USER_UPDATE: 'userUpdate',
PRESENCE_UPDATE: 'presenceUpdate',
VOICE_SERVER_UPDATE: 'voiceServerUpdate',
@@ -341,6 +347,12 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE',
* * MESSAGE_REACTION_REMOVE
* * MESSAGE_REACTION_REMOVE_ALL
* * MESSAGE_REACTION_REMOVE_EMOJI
* * THREAD_CREATE
* * THREAD_UPDATE
* * THREAD_DELETE
* * THREAD_LIST_SYNC
* * THREAD_MEMBER_UPDATE
* * THREAD_MEMBERS_UPDATE
* * USER_UPDATE
* * PRESENCE_UPDATE
* * TYPING_START
@@ -387,6 +399,12 @@ exports.WSEvents = keyMirror([
'MESSAGE_REACTION_REMOVE',
'MESSAGE_REACTION_REMOVE_ALL',
'MESSAGE_REACTION_REMOVE_EMOJI',
'THREAD_CREATE',
'THREAD_UPDATE',
'THREAD_DELETE',
'THREAD_LIST_SYNC',
'THREAD_MEMBER_UPDATE',
'THREAD_MEMBERS_UPDATE',
'USER_UPDATE',
'PRESENCE_UPDATE',
'TYPING_START',
@@ -448,8 +466,11 @@ exports.InviteScopes = [
* * GUILD_DISCOVERY_REQUALIFIED
* * GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING
* * GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING
* * THREAD_CREATED
* * REPLY
* * APPLICATION_COMMAND
* * THREAD_STARTER_MESSAGE
* * GUILD_INVITE_REMINDER
* @typedef {string} MessageType
*/
exports.MessageTypes = [
@@ -471,9 +492,11 @@ exports.MessageTypes = [
'GUILD_DISCOVERY_REQUALIFIED',
'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING',
'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING',
null,
'THREAD_CREATED',
'REPLY',
'APPLICATION_COMMAND',
'THREAD_STARTER_MESSAGE',
'GUILD_INVITE_REMINDER',
];
/**
@@ -509,11 +532,23 @@ exports.ChannelTypes = createEnum([
'NEWS',
// 6
'STORE',
...Array(6).fill(null),
// 13
...Array(3).fill(null),
// 10
'NEWS_THREAD',
'PUBLIC_THREAD',
'PRIVATE_THREAD',
'STAGE',
]);
/**
* The types of channels that are threads. The available types are:
* * news_thread
* * public_thread
* * private_thread
* @typedef {string} ThreadChannelType
*/
exports.ThreadChannelTypes = ['news_thread', 'public_thread', 'private_thread'];
exports.ClientApplicationAssetTypes = {
SMALL: 1,
BIG: 2,
@@ -629,6 +664,7 @@ exports.VerificationLevels = createEnum(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_
* * MAXIMUM_ANIMATED_EMOJIS
* * MAXIMUM_SERVER_MEMBERS
* * GUILD_ALREADY_HAS_TEMPLATE
* * MAXIMUM_THREAD_PARTICIPANTS
* * MAXIMUM_NON_GUILD_MEMBERS_BANS
* * MAXIMUM_BAN_FETCHES
* * UNAUTHORIZED
@@ -672,11 +708,18 @@ exports.VerificationLevels = createEnum(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_
* * PAYMENT_SOURCE_REQUIRED
* * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL
* * INVALID_STICKER_SENT
* * INVALID_OPERATION_ON_ARCHIVED_THREAD
* * INVALID_THREAD_NOTIFICATION_SETTINGS
* * PARAMETER_EARLIER_THAN_CREATION
* * TWO_FACTOR_REQUIRED
* * NO_USERS_WITH_DISCORDTAG_EXIST
* * REACTION_BLOCKED
* * RESOURCE_OVERLOADED
* * STAGE_ALREADY_OPEN
* * MESSAGE_ALREADY_HAS_THREAD
* * THREAD_LOCKED
* * MAXIMUM_ACTIVE_THREADS
* * MAXIMUM_ACTIVE_ANNOUCEMENT_THREAD
* @typedef {string} APIError
*/
exports.APIErrors = {
@@ -735,6 +778,7 @@ exports.APIErrors = {
MAXIMUM_ANIMATED_EMOJIS: 30018,
MAXIMUM_SERVER_MEMBERS: 30019,
GUILD_ALREADY_HAS_TEMPLATE: 30031,
MAXIMUM_THREAD_PARTICIPANTS: 30033,
MAXIMUM_NON_GUILD_MEMBERS_BANS: 30035,
MAXIMUM_BAN_FETCHES: 30037,
UNAUTHORIZED: 40001,
@@ -778,11 +822,18 @@ exports.APIErrors = {
PAYMENT_SOURCE_REQUIRED: 50070,
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074,
INVALID_STICKER_SENT: 50081,
INVALID_OPERATION_ON_ARCHIVED_THREAD: 50083,
INVALID_THREAD_NOTIFICATION_SETTINGS: 50084,
PARAMETER_EARLIER_THAN_CREATION: 50085,
TWO_FACTOR_REQUIRED: 60003,
NO_USERS_WITH_DISCORDTAG_EXIST: 80004,
REACTION_BLOCKED: 90001,
RESOURCE_OVERLOADED: 130000,
STAGE_ALREADY_OPEN: 150006,
MESSAGE_ALREADY_HAS_THREAD: 160004,
THREAD_LOCKED: 160005,
MAXIMUM_ACTIVE_THREADS: 160006,
MAXIMUM_ACTIVE_ANNOUCEMENT_THREAD: 160007,
};
/**

View File

@@ -28,6 +28,7 @@ class MessageFlags extends BitField {}
* * `SUPPRESS_EMBEDS`
* * `SOURCE_MESSAGE_DELETED`
* * `URGENT`
* * `HAS_THREAD`
* * `EPHEMERAL`
* * `LOADING`
* @type {Object}
@@ -39,6 +40,7 @@ MessageFlags.FLAGS = {
SUPPRESS_EMBEDS: 1 << 2,
SOURCE_MESSAGE_DELETED: 1 << 3,
URGENT: 1 << 4,
HAS_THREAD: 1 << 5,
EPHEMERAL: 1 << 6,
LOADING: 1 << 7,
};

View File

@@ -90,6 +90,9 @@ class Permissions extends BitField {
* * `MANAGE_EMOJIS`
* * `USE_APPLICATION_COMMANDS`
* * `REQUEST_TO_SPEAK`
* * `MANAGE_THREADS`
* * `USE_PUBLIC_THREADS`
* * `USE_PRIVATE_THREADS`
* @type {Object<string, bigint>}
* @see {@link https://discord.com/developers/docs/topics/permissions}
*/
@@ -127,6 +130,9 @@ Permissions.FLAGS = {
MANAGE_EMOJIS: 1n << 30n,
USE_APPLICATION_COMMANDS: 1n << 31n,
REQUEST_TO_SPEAK: 1n << 32n,
MANAGE_THREADS: 1n << 34n,
USE_PUBLIC_THREADS: 1n << 35n,
USE_PRIVATE_THREADS: 1n << 36n,
};
/**

View File

@@ -10,7 +10,9 @@
* * **`NewsChannel`**
* * **`StoreChannel`**
* * **`StageChannel`**
* * **`ThreadChannel`**
* * **`GuildMember`**
* * **`ThreadMember`**
* * **`Guild`**
* * **`Message`**
* * **`MessageReaction`**
@@ -103,7 +105,9 @@ const structures = {
NewsChannel: require('../structures/NewsChannel'),
StoreChannel: require('../structures/StoreChannel'),
StageChannel: require('../structures/StageChannel'),
ThreadChannel: require('../structures/ThreadChannel'),
GuildMember: require('../structures/GuildMember'),
ThreadMember: require('../structures/ThreadMember'),
Guild: require('../structures/Guild'),
Message: require('../structures/Message'),
MessageReaction: require('../structures/MessageReaction'),

View File

@@ -0,0 +1,30 @@
'use strict';
const BitField = require('./BitField');
/**
* Data structure that makes it easy to interact with a {@link ThreadMember#flags} bitfield.
* @extends {BitField}
*/
class ThreadMemberFlags extends BitField {}
/**
* @name ThreadMemberFlags
* @kind constructor
* @memberof ThreadMemberFlags
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
*/
/**
* Bitfield of the packed bits
* @type {number}
* @name ThreadMemberFlags#bitfield
*/
/**
* Numeric thread member flags. There are currently no bitflags relevant to bots for this.
* @type {Object<string, number>}
*/
ThreadMemberFlags.FLAGS = {};
module.exports = ThreadMemberFlags;

205
typings/index.d.ts vendored
View File

@@ -16,6 +16,9 @@ declare enum ChannelType {
news = 5,
store = 6,
unknown = 7,
news_thread = 10,
public_thread = 11,
private_thread = 12,
stage = 13,
}
@@ -27,6 +30,9 @@ declare enum ChannelTypes {
CATEGORY = 4,
NEWS = 5,
STORE = 6,
NEWS_THREAD = 10,
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
STAGE = 13,
}
@@ -371,7 +377,7 @@ declare module 'discord.js' {
type CategoryChannelResolvable = Snowflake | CategoryChannel;
export class Channel extends Base {
constructor(client: Client, data?: unknown);
constructor(client: Client, data?: unknown, immediatePatch?: boolean);
public readonly createdAt: Date;
public readonly createdTimestamp: number;
public deleted: boolean;
@@ -379,7 +385,7 @@ declare module 'discord.js' {
public type: keyof typeof ChannelType;
public delete(reason?: string): Promise<Channel>;
public fetch(force?: boolean): Promise<Channel>;
public isText(): this is TextChannel | DMChannel | NewsChannel;
public isText(): this is TextChannel | DMChannel | NewsChannel | ThreadChannel;
public toString(): string;
}
@@ -625,6 +631,12 @@ declare module 'discord.js' {
MESSAGE_REACTION_REMOVE: 'messageReactionRemove';
MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll';
MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji';
THREAD_CREATE: 'threadCreate';
THREAD_DELETE: 'threadDelete';
THREAD_UPDATE: 'threadUpdate';
THREAD_LIST_SYNC: 'threadListSync';
THREAD_MEMBER_UPDATE: 'threadMemberUpdate';
THREAD_MEMBERS_UPDATE: 'threadMembersUpdate';
USER_UPDATE: 'userUpdate';
PRESENCE_UPDATE: 'presenceUpdate';
VOICE_SERVER_UPDATE: 'voiceServerUpdate';
@@ -715,6 +727,7 @@ declare module 'discord.js' {
};
APIErrors: APIErrors;
ChannelTypes: typeof ChannelTypes;
ThreadChannelTypes: ThreadChannelType[];
ClientApplicationAssetTypes: {
SMALL: 1;
BIG: 2;
@@ -1222,14 +1235,14 @@ declare module 'discord.js' {
}
export class Message extends Base {
constructor(client: Client, data: unknown, channel: TextChannel | DMChannel | NewsChannel);
constructor(client: Client, data: unknown, channel: TextChannel | DMChannel | NewsChannel | ThreadChannel);
private patch(data: unknown): Message;
public activity: MessageActivity | null;
public applicationID: Snowflake | null;
public attachments: Collection<Snowflake, MessageAttachment>;
public author: User;
public channel: TextChannel | DMChannel | NewsChannel;
public channel: TextChannel | DMChannel | NewsChannel | ThreadChannel;
public readonly cleanContent: string;
public components: MessageActionRow[];
public content: string;
@@ -1255,6 +1268,7 @@ declare module 'discord.js' {
public reactions: ReactionManager;
public stickers: Collection<Snowflake, Sticker>;
public system: boolean;
public thread: ThreadChannel;
public tts: boolean;
public type: MessageType;
public readonly url: string;
@@ -1289,6 +1303,11 @@ declare module 'discord.js' {
public removeAttachments(): Promise<Message>;
public reply(options: string | APIMessage | (ReplyMessageOptions & { split?: false })): Promise<Message>;
public reply(options: APIMessage | (ReplyMessageOptions & { split: true | SplitOptions })): Promise<Message[]>;
public startThread(
name: string,
autoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
): Promise<ThreadChannel>;
public suppressEmbeds(suppress?: boolean): Promise<Message>;
public toJSON(): unknown;
public toString(): string;
@@ -1514,11 +1533,17 @@ declare module 'discord.js' {
export class NewsChannel extends TextBasedChannel(GuildChannel) {
constructor(guild: Guild, data?: unknown);
public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration;
public messages: MessageManager;
public nsfw: boolean;
public threads: ThreadManager;
public topic: string | null;
public type: 'news';
public createWebhook(name: string, options?: ChannelWebhookCreateOptions): Promise<Webhook>;
public setDefaultAutoArchiveDuration(
defaultAutoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
): Promise<NewsChannel>;
public setNSFW(nsfw: boolean, reason?: string): Promise<NewsChannel>;
public setType(type: Pick<typeof ChannelType, 'text' | 'news'>, reason?: string): Promise<GuildChannel>;
public fetchWebhooks(): Promise<Collection<Snowflake, Webhook>>;
@@ -1846,18 +1871,82 @@ declare module 'discord.js' {
export class TextChannel extends TextBasedChannel(GuildChannel) {
constructor(guild: Guild, data?: unknown);
public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration;
public messages: MessageManager;
public nsfw: boolean;
public type: 'text';
public rateLimitPerUser: number;
public threads: ThreadManager;
public topic: string | null;
public createWebhook(name: string, options?: ChannelWebhookCreateOptions): Promise<Webhook>;
public setDefaultAutoArchiveDuration(
defaultAutoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
): Promise<TextChannel>;
public setNSFW(nsfw: boolean, reason?: string): Promise<TextChannel>;
public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise<TextChannel>;
public setType(type: Pick<typeof ChannelType, 'text' | 'news'>, reason?: string): Promise<GuildChannel>;
public fetchWebhooks(): Promise<Collection<Snowflake, Webhook>>;
}
export class ThreadChannel extends TextBasedChannel(Channel) {
constructor(guild: Guild, data?: object);
public archived: boolean;
public readonly archivedAt: Date | null;
public archiveTimestamp: number | null;
public autoArchiveDuration: ThreadAutoArchiveDuration;
public readonly editable: boolean;
public guild: Guild;
public readonly guildMembers: Collection<Snowflake, GuildMember>;
public readonly joinable: boolean;
public locked: boolean;
public readonly manageable: boolean;
public readonly sendable: boolean;
public memberCount: number | null;
public messageCount: number | null;
public messages: MessageManager;
public members: ThreadMemberManager;
public name: string;
public ownerID: Snowflake;
public readonly parent: TextChannel | NewsChannel | null;
public parentID: Snowflake;
public rateLimitPerUser: number;
public type: ThreadChannelType;
public readonly unarchivable: boolean;
public delete(reason?: string): Promise<ThreadChannel>;
public edit(data: ThreadEditData, reason?: string): Promise<ThreadChannel>;
public join(): Promise<ThreadChannel>;
public leave(): Promise<ThreadChannel>;
public permissionsFor(memberOrRole: GuildMember | Role): Readonly<Permissions>;
public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly<Permissions> | null;
public setArchived(archived: boolean, reason?: string): Promise<ThreadChannel>;
public setAutoArchiveDuration(
autoArchiveDuration: ThreadAutoArchiveDuration,
reason?: string,
): Promise<ThreadChannel>;
public setLocked(locked: boolean, reason?: string): Promise<ThreadChannel>;
public setName(name: string, reason?: string): Promise<ThreadChannel>;
public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise<ThreadChannel>;
}
export class ThreadMember extends Base {
constructor(thread: ThreadChannel, data?: object);
public flags: ThreadMemberFlags;
public readonly guildMember: GuildMember | null;
public id: Snowflake;
public readonly joinedAt: Date | null;
public joinedTimestamp: number | null;
public readonly manageable: boolean;
public thread: ThreadChannel;
public readonly user: User | null;
public remove(reason?: string): Promise<ThreadMember>;
}
export class ThreadMemberFlags extends BitField<ThreadMemberFlagsString> {
public static FLAGS: Record<ThreadMemberFlagsString, number>;
public static resolve(bit?: BitFieldResolvable<ThreadMemberFlagsString, number>): number;
}
export class User extends PartialTextBasedChannel(Base) {
constructor(client: Client, data: unknown);
public avatar: string | null;
@@ -2264,6 +2353,7 @@ declare module 'discord.js' {
export class GuildChannelManager extends BaseManager<Snowflake, GuildChannel, GuildChannelResolvable> {
constructor(guild: Guild, iterable?: Iterable<any>);
public readonly channelCountWithoutThreads: number;
public guild: Guild;
public create(name: string, options: GuildChannelCreateOptions & { type: 'voice' }): Promise<VoiceChannel>;
public create(name: string, options: GuildChannelCreateOptions & { type: 'category' }): Promise<CategoryChannel>;
@@ -2425,6 +2515,33 @@ declare module 'discord.js' {
public delete(channel: StageChannel | Snowflake): Promise<void>;
}
export class ThreadManager extends BaseManager<Snowflake, ThreadChannel, ThreadChannelResolvable> {
constructor(channel: TextChannel | NewsChannel, iterable?: Iterable<any>);
public channel: TextChannel | NewsChannel;
public create(options: {
name: string;
autoArchiveDuration: ThreadAutoArchiveDuration;
startMessage?: MessageResolvable;
reason?: string;
}): Promise<ThreadChannel>;
public fetch(options: ThreadChannelResolvable, cacheOptions?: BaseFetchOptions): Promise<ThreadChannel | null>;
public fetch(
options?: { archived?: FetchArchivedThreadOptions; active?: boolean },
cacheOptions?: { cache?: boolean },
): Promise<FetchedThreads>;
public fetchArchived(options?: FetchArchivedThreadOptions, cache?: boolean): Promise<FetchedThreads>;
public fetchActive(cache?: boolean): Promise<FetchedThreads>;
}
export class ThreadMemberManager extends Omit<BaseManager<Snowflake, ThreadMember, ThreadMemberResolvable>, 'add'> {
constructor(thread: ThreadChannel, iterable?: Iterable<any>);
public thread: ThreadChannel;
public _add(data: any, cache?: boolean): ThreadMember;
public add(member: UserResolvable | '@me', reason?: string): Promise<Snowflake>;
public fetch(cache?: boolean): Promise<Collection<Snowflake, ThreadMember>>;
public remove(id: Snowflake | '@me', reason?: string): Promise<Snowflake>;
}
export class UserManager extends BaseManager<Snowflake, User, UserResolvable> {
constructor(client: Client, iterable?: Iterable<any>);
public fetch(id: Snowflake, options?: BaseFetchOptions): Promise<User>;
@@ -2595,6 +2712,7 @@ declare module 'discord.js' {
MAXIMUM_ANIMATED_EMOJIS: 30018;
MAXIMUM_SERVER_MEMBERS: 30019;
GUILD_ALREADY_HAS_TEMPLATE: 30031;
MAXIMUM_THREAD_PARICIPANTS: 30033;
MAXIMUM_NON_GUILD_MEMBERS_BANS: 30035;
MAXIMUM_BAN_FETCHES: 30037;
UNAUTHORIZED: 40001;
@@ -2638,11 +2756,18 @@ declare module 'discord.js' {
PAYMENT_SOURCE_REQUIRED: 50070;
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074;
INVALID_STICKER_SENT: 50081;
INVALID_THREAD_ARCHIVE_STATE: 50083;
INVALID_THREAD_NOTIFICATION_SETTINGS: 50084;
PARAMETER_EARLIER_THAN_CREATION: 50085;
TWO_FACTOR_REQUIRED: 60003;
NO_USERS_WITH_DISCORDTAG_EXIST: 80004;
REACTION_BLOCKED: 90001;
RESOURCE_OVERLOADED: 130000;
STAGE_ALREADY_OPEN: 150006;
MESSAGE_ALREADY_HAS_THREAD: 160004;
THREAD_LOCKED: 160005;
MAXIMUM_ACTIVE_THREADS: 160006;
MAXIMUM_ACTIVE_ANNOUCEMENT_THREAD: 160007;
}
interface ApplicationAsset {
@@ -2765,6 +2890,7 @@ declare module 'discord.js' {
rateLimitPerUser?: number;
lockPermissions?: boolean;
permissionOverwrites?: readonly OverwriteResolvable[] | Collection<Snowflake, OverwriteResolvable>;
defaultAutoArchiveDuration?: ThreadAutoArchiveDuration;
rtcRegion?: string | null;
}
@@ -2838,7 +2964,16 @@ declare module 'discord.js' {
roleCreate: [role: Role];
roleDelete: [role: Role];
roleUpdate: [oldRole: Role, newRole: Role];
typingStart: [channel: TextChannel | NewsChannel | DMChannel | PartialDMChannel, user: User | PartialUser];
threadCreate: [thread: ThreadChannel];
threadDelete: [thread: ThreadChannel];
threadListSync: [threads: Collection<Snowflake, ThreadChannel>];
threadMemberUpdate: [oldMember: ThreadMember, newMember: ThreadMember];
threadMembersUpdate: [
oldMembers: Collection<Snowflake, ThreadMember>,
mewMembers: Collection<Snowflake, ThreadMember>,
];
threadUpdate: [oldThread: ThreadChannel, newThread: ThreadChannel];
typingStart: [channel: Channel | PartialDMChannel, user: User | PartialUser];
userUpdate: [oldUser: User | PartialUser, newUser: User];
voiceStateUpdate: [oldState: VoiceState, newState: VoiceState];
webhookUpdate: [channel: TextChannel];
@@ -2972,6 +3107,8 @@ declare module 'discord.js' {
name: string;
}
type DateResolvable = Date | number | string;
interface DeconstructedSnowflake {
timestamp: number;
readonly date: Date;
@@ -3033,7 +3170,9 @@ declare module 'discord.js' {
CategoryChannel: typeof CategoryChannel;
NewsChannel: typeof NewsChannel;
StoreChannel: typeof StoreChannel;
ThreadChannel: typeof ThreadChannel;
GuildMember: typeof GuildMember;
ThreadMember: typeof ThreadMember;
Guild: typeof Guild;
Message: typeof Message;
MessageReaction: typeof MessageReaction;
@@ -3067,6 +3206,18 @@ declare module 'discord.js' {
limit?: number;
}
interface FetchArchivedThreadOptions {
type?: 'public' | 'private';
fetchAll?: boolean;
before?: ThreadChannelResolvable | DateResolvable;
limit?: number;
}
interface FetchedThreads {
threads: Collection<Snowflake, ThreadChannel>;
hasMore?: boolean;
}
interface FetchMemberOptions extends BaseFetchOptions {
user: UserResolvable;
}
@@ -3182,7 +3333,16 @@ declare module 'discord.js' {
topic?: string;
type?: Exclude<
keyof typeof ChannelType | ChannelType,
'dm' | 'group' | 'unknown' | ChannelType.dm | ChannelType.group | ChannelType.unknown
| 'dm'
| 'group'
| 'unknown'
| 'public_thread'
| 'private_thread'
| ChannelType.dm
| ChannelType.group
| ChannelType.unknown
| ChannelType.public_thread
| ChannelType.private_thread
>;
nsfw?: boolean;
parent?: ChannelResolvable;
@@ -3535,6 +3695,7 @@ declare module 'discord.js' {
| 'SUPPRESS_EMBEDS'
| 'SOURCE_MESSAGE_DELETED'
| 'URGENT'
| 'HAS_THREAD'
| 'EPHEMERAL'
| 'LOADING';
@@ -3594,6 +3755,7 @@ declare module 'discord.js' {
| InteractionWebhook
| TextChannel
| NewsChannel
| ThreadChannel
| DMChannel
| User
| GuildMember
@@ -3620,8 +3782,11 @@ declare module 'discord.js' {
| 'GUILD_DISCOVERY_REQUALIFIED'
| 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING'
| 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING'
| 'THREAD_CREATED'
| 'REPLY'
| 'APPLICATION_COMMAND';
| 'APPLICATION_COMMAND'
| 'THREAD_STARTER_MESSAGE'
| 'GUILD_INVITE_REMINDER';
type MFALevel = keyof typeof MFALevels;
@@ -3691,7 +3856,10 @@ declare module 'discord.js' {
| 'MANAGE_WEBHOOKS'
| 'MANAGE_EMOJIS'
| 'USE_APPLICATION_COMMANDS'
| 'REQUEST_TO_SPEAK';
| 'REQUEST_TO_SPEAK'
| 'MANAGE_THREADS'
| 'USE_PUBLIC_THREADS'
| 'USE_PRIVATE_THREADS';
interface RecursiveArray<T> extends ReadonlyArray<T | RecursiveArray<T>> {}
@@ -3969,6 +4137,24 @@ declare module 'discord.js' {
privacyLevel?: PrivacyLevel | number;
}
type ThreadAutoArchiveDuration = 60 | 1440 | 4320 | 10080;
type ThreadChannelResolvable = ThreadChannel | Snowflake;
type ThreadChannelType = 'news_thread' | 'public_thread' | 'private_thread';
interface ThreadEditData {
name?: string;
archived?: boolean;
autoArchiveDuration?: ThreadAutoArchiveDuration;
rateLimitPeruser?: number;
locked?: boolean;
}
type ThreadMemberFlagsString = '';
type ThreadMemberResolvable = ThreadMember | UserResolvable;
type UserFlagsString =
| 'DISCORD_EMPLOYEE'
| 'PARTNERED_SERVER_OWNER'
@@ -3984,7 +4170,7 @@ declare module 'discord.js' {
| 'EARLY_VERIFIED_BOT_DEVELOPER'
| 'DISCORD_CERTIFIED_MODERATOR';
type UserResolvable = User | Snowflake | Message | GuildMember;
type UserResolvable = User | Snowflake | Message | GuildMember | ThreadMember;
interface Vanity {
code: string | null;
@@ -4012,6 +4198,7 @@ declare module 'discord.js' {
interface WebhookMessageOptions extends Omit<MessageOptions, 'reply'> {
username?: string;
avatarURL?: string;
threadID?: Snowflake;
}
type WebhookType = keyof typeof WebhookTypes;