This commit is contained in:
Elysia
2024-01-14 12:33:11 +07:00
parent e15b9ab7fe
commit 039dd34cf2
27 changed files with 1250 additions and 5297 deletions

View File

@@ -1,46 +1,40 @@
'use strict';
const process = require('node:process');
const { setInterval, setTimeout } = require('node:timers');
const { setInterval } = require('node:timers');
const { setTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { getVoiceConnection } = require('@discordjs/voice');
const chalk = require('chalk');
const fetch = require('node-fetch');
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const { Error, TypeError, RangeError } = require('../errors');
const Discord = require('../index');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const BillingManager = require('../managers/BillingManager');
const ChannelManager = require('../managers/ChannelManager');
const ClientUserSettingManager = require('../managers/ClientUserSettingManager');
const DeveloperPortalManager = require('../managers/DeveloperPortalManager');
const GuildManager = require('../managers/GuildManager');
const PresenceManager = require('../managers/PresenceManager');
const RelationshipManager = require('../managers/RelationshipManager');
const SessionManager = require('../managers/SessionManager');
const UserManager = require('../managers/UserManager');
const UserNoteManager = require('../managers/UserNoteManager');
const VoiceStateManager = require('../managers/VoiceStateManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientPresence = require('../structures/ClientPresence');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const { CustomStatus } = require('../structures/RichPresence');
const { Sticker } = require('../structures/Sticker');
const StickerPack = require('../structures/StickerPack');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Widget = require('../structures/Widget');
const { Events, InviteScopes, Status, captchaServices } = require('../util/Constants');
const { Events, Status } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Intents = require('../util/Intents');
const Options = require('../util/Options');
const Permissions = require('../util/Permissions');
const DiscordAuthWebsocket = require('../util/RemoteAuth');
const Sweepers = require('../util/Sweepers');
const { getProxyObject } = require('../util/Util');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
@@ -50,7 +44,7 @@ class Client extends BaseClient {
/**
* @param {ClientOptions} options Options for the client
*/
constructor(options = {}) {
constructor(options) {
super(options);
const data = require('node:worker_threads').workerData ?? process.env;
@@ -141,17 +135,6 @@ class Client extends BaseClient {
*/
this.users = new UserManager(this);
// Patch
/**
* All of the relationships {@link User}
* @type {RelationshipManager}
*/
this.relationships = new RelationshipManager(this);
/**
* All of the settings {@link Object}
* @type {ClientUserSettingManager}
*/
this.settings = new ClientUserSettingManager(this);
/**
* All of the guilds the client is currently handling, mapped by their ids -
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
@@ -159,18 +142,6 @@ class Client extends BaseClient {
*/
this.guilds = new GuildManager(this);
/**
* Manages the API methods
* @type {BillingManager}
*/
this.billing = new BillingManager(this);
/**
* All of the sessions of the client
* @type {SessionManager}
*/
this.sessions = new SessionManager(this);
/**
* All of the {@link Channel}s that the client is currently handling, mapped by their ids -
* as long as sharding isn't being used, this will be *every* channel in *every* guild the bot
@@ -186,12 +157,6 @@ class Client extends BaseClient {
*/
this.sweepers = new Sweepers(this, this.options.sweepers);
/**
* The developer portal manager of the client
* @type {DeveloperPortalManager}
*/
this.developerPortal = new DeveloperPortalManager(this);
/**
* The presence of the Client
* @private
@@ -199,6 +164,30 @@ class Client extends BaseClient {
*/
this.presence = new ClientPresence(this, this.options.presence);
/**
* A manager of the presences belonging to this client
* @type {PresenceManager}
*/
this.presences = new PresenceManager(this);
/**
* All of the note that have been cached at any point, mapped by their ids
* @type {UserManager}
*/
this.notes = new UserNoteManager(this);
/**
* All of the relationships {@link User}
* @type {RelationshipManager}
*/
this.relationships = new RelationshipManager(this);
/**
* Manages the API methods
* @type {BillingManager}
*/
this.billing = new BillingManager(this);
Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'DISCORD_TOKEN' in process.env) {
/**
@@ -212,20 +201,12 @@ class Client extends BaseClient {
this.token = null;
}
this._interactionCache = new Collection();
/**
* User that the client is logged in as
* @type {?ClientUser}
*/
this.user = null;
/**
* The application of this bot
* @type {?ClientApplication}
*/
this.application = null;
/**
* Time at which the client was last regarded as being in the `READY` state
* (each time the client disconnects and successfully reconnects, this will be overwritten)
@@ -233,12 +214,6 @@ class Client extends BaseClient {
*/
this.readyAt = null;
/**
* Password cache
* @type {?string}
*/
this.password = this.options.password;
if (this.options.messageSweepInterval > 0) {
process.emitWarning(
'The message sweeping client options are deprecated, use the global sweepers instead.',
@@ -251,15 +226,6 @@ class Client extends BaseClient {
}
}
/**
* Session ID
* @type {?string}
* @readonly
*/
get sessionId() {
return this.ws.shards.first()?.sessionId;
}
/**
* All custom emojis that the client has access to, mapped by their ids
* @type {BaseGuildEmojiManager}
@@ -291,19 +257,6 @@ class Client extends BaseClient {
return this.readyAt ? Date.now() - this.readyAt : null;
}
/**
* @external VoiceConnection
* @see {@link https://discord.js.org/#/docs/voice/main/class/VoiceConnection}
*/
/**
* Get connection to current call
* @type {?VoiceConnection}
* @readonly
*/
get callVoice() {
return getVoiceConnection(null);
}
/**
* Logs the client in, establishing a WebSocket connection to Discord.
* @param {string} [token=this.token] Token of the account to log in with
@@ -320,8 +273,7 @@ class Client extends BaseClient {
Logging on with a user token is unfortunately against the Discord
\`Terms of Service\` <https://support.discord.com/hc/en-us/articles/115002192352>
and doing so might potentially get your account banned.
Use this at your own risk.
`,
Use this at your own risk.`,
);
this.emit(
Events.DEBUG,
@@ -346,178 +298,10 @@ class Client extends BaseClient {
}
}
/**
* Login Discord with Username and Password
* @param {string} username Email or Phone Number
* @param {?string} password Password
* @param {?string} mfaCode 2FA Code / Backup Code
* @returns {Promise<string>}
*/
async normalLogin(username, password = this.password, mfaCode) {
if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
throw new Error('NORMAL_LOGIN');
}
this.emit(
Events.DEBUG,
`Connecting to Discord with:
username: ${username}
password: ${password.replace(/./g, '*')}`,
);
const data = await this.api.auth.login.post({
data: {
login: username,
password: password,
undelete: false,
captcha_key: null,
login_source: null,
gift_code_sku_id: null,
},
auth: false,
});
this.password = password;
if (!data.token && data.ticket && data.mfa) {
this.emit(Events.DEBUG, `Using 2FA Code: ${mfaCode}`);
const normal2fa = /(\d{6})/g;
const backupCode = /([a-z0-9]{4})-([a-z0-9]{4})/g;
if (!mfaCode || typeof mfaCode !== 'string') {
throw new Error('LOGIN_FAILED_2FA');
}
if (normal2fa.test(mfaCode) || backupCode.test(mfaCode)) {
const data2 = await this.api.auth.mfa.totp.post({
data: {
code: mfaCode,
ticket: data.ticket,
login_source: null,
gift_code_sku_id: null,
},
auth: false,
});
return this.login(data2.token);
} else {
throw new Error('LOGIN_FAILED_2FA');
}
} else if (data.token) {
return this.login(data.token);
} else {
throw new Error('LOGIN_FAILED_UNKNOWN');
}
}
/**
* Switch the user
* @param {string} token User Token
* @returns {Promise<string>}
*/
switchUser(token) {
this._clearCache(this.emojis.cache);
this._clearCache(this.guilds.cache);
this._clearCache(this.channels.cache);
this._clearCache(this.users.cache);
this._clearCache(this.relationships.cache);
this._clearCache(this.sessions.cache);
this._clearCache(this.voiceStates.cache);
this.ws.status = Status.IDLE;
return this.login(token);
}
/**
* Sign in with the QR code on your phone.
* @param {DiscordAuthWebsocketOptions} options Options
* @returns {DiscordAuthWebsocket}
* @example
* client.QRLogin();
*/
QRLogin(options = {}) {
const QR = new DiscordAuthWebsocket({ ...options, autoLogin: true });
this.emit(Events.DEBUG, `Preparing to connect to the gateway (QR Login)`, QR);
return QR.connect(this);
}
/**
* Implement `remoteAuth`, like using your phone to scan a QR code
* @param {string} url URL from QR code
* @returns {Promise<void>}
*/
async remoteAuth(url) {
if (!this.isReady()) throw new Error('CLIENT_NOT_READY', 'Remote Auth');
// Step 1: Parse URL
url = new URL(url);
if (
!['discordapp.com', 'discord.com'].includes(url.hostname) ||
!url.pathname.startsWith('/ra/') ||
url.pathname.length <= 4
) {
throw new Error('INVALID_REMOTE_AUTH_URL');
}
const hash = url.pathname.replace('/ra/', '');
// Step 2: Post > Get handshake_token
const res = await this.api.users['@me']['remote-auth'].post({
data: {
fingerprint: hash,
},
});
const handshake_token = res.handshake_token;
// Step 3: Post
return this.api.users['@me']['remote-auth'].finish.post({ data: { handshake_token, temporary_token: false } });
// Cancel
// this.api.users['@me']['remote-auth'].cancel.post({ data: { handshake_token } });
}
/**
* Create a new token based on the current token
* @returns {Promise<string>} New Discord Token
*/
createToken() {
return new Promise((resolve, reject) => {
// Step 1: Create DiscordAuthWebsocket
const QR = new DiscordAuthWebsocket({
hiddenLog: true,
generateQR: false,
autoLogin: false,
debug: false,
failIfError: false,
userAgent: this.options.http.headers['User-Agent'],
wsProperties: this.options.ws.properties,
});
// Step 2: Add event
QR.once('ready', async (_, url) => {
try {
await this.remoteAuth(url);
} catch (e) {
reject(e);
}
}).once('finish', (user, token) => {
resolve(token);
});
// Step 3: Connect
QR.connect();
});
}
/**
* Emitted whenever clientOptions.checkUpdate = false
* @event Client#update
* @param {string} oldVersion Current version
* @param {string} newVersion Latest version
*/
/**
* Check for updates
* @returns {Promise<Client>}
*/
async checkUpdate() {
const res_ = await (
await fetch(`https://registry.npmjs.com/${encodeURIComponent('discord.js-selfbot-v13')}`)
).json();
try {
const latest_tag = res_['dist-tags'].latest;
this.emit('update', Discord.version, latest_tag);
this.emit('debug', `${chalk.greenBright('[OK]')} Check Update success`);
} catch {
this.emit('debug', `${chalk.redBright('[Fail]')} Check Update error`);
this.emit('update', Discord.version, false);
}
return this;
QRLogin() {
const ws = new DiscordAuthWebsocket();
ws.once('ready', () => ws.generateQR());
return ws.connect(this);
}
/**
@@ -544,7 +328,6 @@ class Client extends BaseClient {
this.sweepers.destroy();
this.ws.destroy();
this.token = null;
this.password = null;
}
/**
@@ -558,7 +341,7 @@ class Client extends BaseClient {
voip_provider: null,
},
});
await this.destroy();
return this.destroy();
}
/**
@@ -586,51 +369,6 @@ class Client extends BaseClient {
return new Invite(this, data);
}
/**
* Join this Guild using this invite (fast)
* @param {InviteResolvable} invite Invite code or URL
* @returns {Promise<void>}
* @example
* await client.acceptInvite('https://discord.gg/genshinimpact')
*/
async acceptInvite(invite) {
const code = DataResolver.resolveInviteCode(invite);
if (!code) throw new Error('INVITE_RESOLVE_CODE');
if (invite instanceof Invite) {
await invite.acceptInvite();
} else {
await this.api.invites(code).post({
headers: {
'X-Context-Properties': 'eyJsb2NhdGlvbiI6Ik1hcmtkb3duIExpbmsifQ==', // Markdown Link
},
data: {
session_id: this.sessionId,
},
});
}
}
/**
* Redeem nitro from code or url.
* @param {string} nitro Nitro url or code
* @param {TextChannelResolvable} channel Channel that the code was sent in
* @param {Snowflake} [paymentSourceId] Payment source id
* @returns {Promise<any>}
*/
redeemNitro(nitro, channel, paymentSourceId) {
if (typeof nitro !== 'string') throw new Error('INVALID_NITRO');
const nitroCode =
nitro.match(/(discord.gift|discord.com|discordapp.com\/gifts)\/(\w{16,25})/) ||
nitro.match(/(discord\.gift\/|discord\.com\/gifts\/|discordapp\.com\/gifts\/)(\w+)/);
if (!nitroCode) return false;
const code = nitroCode[2];
channel = this.channels.resolveId(channel);
return this.api.entitlements['gift-codes'](code).redeem.post({
auth: true,
data: { channel_id: channel || null, payment_source_id: paymentSourceId || null },
});
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
@@ -721,16 +459,6 @@ class Client extends BaseClient {
}
}
/**
* Clear a cache
* @param {Collection} cache The cache to clear
* @returns {number} The number of removed entries
* @private
*/
_clearCache(cache) {
return cache.sweep(() => true);
}
/**
* Sweeps all text-based channels' messages and removes the ones older than the max message lifetime.
* If the message has been edited, the time of the edit is used rather than the time of the original message.
@@ -791,65 +519,13 @@ class Client extends BaseClient {
*/
/**
* Generates a link that can be used to invite the bot to a guild.
* @param {InviteGenerationOptions} [options={}] Options for the invite
* @returns {string}
* @example
* const link = client.generateInvite({
* scopes: ['applications.commands'],
* });
* console.log(`Generated application invite link: ${link}`);
* @example
* const link = client.generateInvite({
* permissions: [
* Permissions.FLAGS.SEND_MESSAGES,
* Permissions.FLAGS.MANAGE_GUILD,
* Permissions.FLAGS.MENTION_EVERYONE,
* ],
* scopes: ['bot'],
* });
* console.log(`Generated bot invite link: ${link}`);
* The sleep function in JavaScript returns a promise that resolves after a specified timeout.
* @param {number} timeout - The timeout parameter is the amount of time, in milliseconds, that the sleep
* function will wait before resolving the promise and continuing execution.
* @returns {void} The `sleep` function is returning a Promise.
*/
generateInvite(options = {}) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
if (!this.application) throw new Error('CLIENT_NOT_READY', 'generate an invite link');
const query = new URLSearchParams({
client_id: this.application.id,
});
const { scopes } = options;
if (typeof scopes === 'undefined') {
throw new TypeError('INVITE_MISSING_SCOPES');
}
if (!Array.isArray(scopes)) {
throw new TypeError('INVALID_TYPE', 'scopes', 'Array of Invite Scopes', true);
}
if (!scopes.some(scope => ['bot', 'applications.commands'].includes(scope))) {
throw new TypeError('INVITE_MISSING_SCOPES');
}
const invalidScope = scopes.find(scope => !InviteScopes.includes(scope));
if (invalidScope) {
throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope);
}
query.set('scope', scopes.join(' '));
if (options.permissions) {
const permissions = Permissions.resolve(options.permissions);
if (permissions) query.set('permissions', permissions);
}
if (options.disableGuildSelect) {
query.set('disable_guild_select', true);
}
if (options.guild) {
const guildId = this.guilds.resolveId(options.guild);
if (!guildId) throw new TypeError('INVALID_TYPE', 'options.guild', 'GuildResolvable');
query.set('guild_id', guildId);
}
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
sleep(timeout) {
return new Promise(r => setTimeout(r, timeout));
}
toJSON() {
@@ -859,41 +535,46 @@ class Client extends BaseClient {
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
* with the client as `this`.
* @param {string} script Script to eval
* @returns {*}
* @private
* Join this Guild using this invite (fast)
* @param {InviteResolvable} invite Invite code or URL
* @returns {Promise<void>}
* @example
* await client.acceptInvite('https://discord.gg/genshinimpact')
*/
_eval(script) {
return eval(script);
async acceptInvite(invite) {
const code = DataResolver.resolveInviteCode(invite);
if (!code) throw new Error('INVITE_RESOLVE_CODE');
if (invite instanceof Invite) {
await invite.acceptInvite();
} else {
await this.api.invites(code).post({
DiscordContext: { location: 'Markdown Link' },
data: {
session_id: this.ws.shards.first()?.sessionId,
},
});
}
}
/**
* Sets the client's presence. (Sync Setting).
* @param {Client} client Discord Client
* @private
* Redeem nitro from code or url.
* @param {string} nitro Nitro url or code
* @param {TextChannelResolvable} [channel] Channel that the code was sent in
* @param {Snowflake} [paymentSourceId] Payment source id
* @returns {Promise<any>}
*/
customStatusAuto(client) {
client = client ?? this;
if (!client.user) return;
const custom_status = new CustomStatus();
if (!client.settings.rawSetting.custom_status?.text && !client.settings.rawSetting.custom_status?.emoji_name) {
client.user.setPresence({
activities: this.presence.activities.filter(a => a.type !== 'CUSTOM'),
status: client.settings.rawSetting.status ?? 'invisible',
});
} else {
custom_status.setEmoji({
name: client.settings.rawSetting.custom_status?.emoji_name,
id: client.settings.rawSetting.custom_status?.emoji_id,
});
custom_status.setState(client.settings.rawSetting.custom_status?.text);
client.user.setPresence({
activities: [custom_status.toJSON(), ...this.presence.activities.filter(a => a.type !== 'CUSTOM')],
status: client.settings.rawSetting.status ?? 'invisible',
});
}
redeemNitro(nitro, channel, paymentSourceId) {
if (typeof nitro !== 'string') throw new Error('INVALID_NITRO');
const nitroCode =
nitro.match(/(discord.gift|discord.com|discordapp.com\/gifts)\/(\w{16,25})/) ||
nitro.match(/(discord\.gift\/|discord\.com\/gifts\/|discordapp\.com\/gifts\/)(\w+)/);
if (!nitroCode) return false;
const code = nitroCode[2];
channel = this.channels.resolveId(channel);
return this.api.entitlements['gift-codes'](code).redeem.post({
auth: true,
data: { channel_id: channel || null, payment_source_id: paymentSourceId || null },
});
}
/**
@@ -909,7 +590,7 @@ class Client extends BaseClient {
* Authorize an application.
* @param {string} url Discord Auth URL
* @param {OAuth2AuthorizeOptions} options Oauth2 options
* @returns {Promise<Object>}
* @returns {Promise<any>}
* @example
* client.authorizeURL(`https://discord.com/api/oauth2/authorize?client_id=botID&permissions=8&scope=applications.commands%20bot`, {
guild_id: "guildID",
@@ -937,12 +618,14 @@ class Client extends BaseClient {
}
/**
* Makes waiting time for Client.
* @param {number} miliseconds Sleeping time as milliseconds.
* @returns {Promise<void> | null}
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
* with the client as `this`.
* @param {string} script Script to eval
* @returns {*}
* @private
*/
sleep(miliseconds) {
return typeof miliseconds === 'number' ? new Promise(r => setTimeout(r, miliseconds).unref()) : null;
_eval(script) {
return eval(script);
}
/**
@@ -956,73 +639,8 @@ class Client extends BaseClient {
} else {
options.intents = Intents.resolve(options.intents);
}
if (options && typeof options.checkUpdate !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'checkUpdate', 'a boolean');
}
if (options && typeof options.syncStatus !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'syncStatus', 'a boolean');
}
if (options && typeof options.autoRedeemNitro !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'autoRedeemNitro', 'a boolean');
}
if (options && options.captchaService && !captchaServices.includes(options.captchaService)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaService', captchaServices.join(', '));
}
// Parse captcha key
if (options && captchaServices.includes(options.captchaService) && options.captchaService !== 'custom') {
if (typeof options.captchaKey !== 'string') {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaKey', 'a string');
}
switch (options.captchaService) {
case '2captcha':
if (options.captchaKey.length !== 32) {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaKey', 'a 32 character string');
}
break;
case 'capmonster':
if (options.captchaKey.length !== 32) {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaKey', 'a 32 character string');
}
break;
case 'nopecha': {
if (options.captchaKey.length !== 16) {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaKey', 'a 16 character string');
}
break;
}
}
}
if (typeof options.captchaRetryLimit !== 'number' || isNaN(options.captchaRetryLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaRetryLimit', 'a number');
}
if (options && typeof options.captchaSolver !== 'function') {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaSolver', 'a function');
}
if (options && typeof options.captchaWithProxy !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'captchaWithProxy', 'a boolean');
}
if (options && typeof options.DMSync !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'DMSync', 'a boolean');
}
if (options && typeof options.patchVoice !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'patchVoice', 'a boolean');
}
if (options && options.password && typeof options.password !== 'string') {
throw new TypeError('CLIENT_INVALID_OPTION', 'password', 'a string');
}
if (options && options.usingNewAttachmentAPI && typeof options.usingNewAttachmentAPI !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'usingNewAttachmentAPI', 'a boolean');
}
if (options && options.interactionTimeout && typeof options.interactionTimeout !== 'number') {
throw new TypeError('CLIENT_INVALID_OPTION', 'interactionTimeout', 'a number');
}
if (options && typeof options.proxy !== 'string') {
throw new TypeError('CLIENT_INVALID_OPTION', 'proxy', 'a string');
} else if (options && options.proxy && typeof options.proxy === 'string') {
getProxyObject(options.proxy);
}
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1');
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount !== 1) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number equal to 1');
}
if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) {
throw new TypeError('CLIENT_INVALID_OPTION', 'shards', "'auto', a number or array of numbers");
@@ -1046,12 +664,15 @@ class Client extends BaseClient {
if (!Array.isArray(options.partials)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
}
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
}
if (typeof options.messageCreateEventGuildTimeout !== 'number' || isNaN(options.messageCreateEventGuildTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCreateEventGuildTimeout', 'a number');
}
if (typeof options.DMChannelVoiceStatusSync !== 'number' || isNaN(options.DMChannelVoiceStatusSync)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'DMChannelVoiceStatusSync', 'a number');
}
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
}
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number');
}

View File

@@ -1,105 +1,13 @@
'use strict';
const USER_REQUIRED_ACTION = require('./USER_REQUIRED_ACTION_UPDATE');
const { Opcodes } = require('../../../util/Constants');
let ClientUser;
const { VoiceConnection } = require('@discordjs/voice');
const chalk = require('chalk');
const { Events, Opcodes } = require('../../../util/Constants');
const Util = require('../../../util/Util');
const { VoiceConnection: VoiceConnection_patch } = require('../../../util/Voice');
let firstReady = false;
function patchVoice(client) {
try {
/* eslint-disable */
VoiceConnection.prototype.configureNetworking = VoiceConnection_patch.prototype.configureNetworking;
client.emit(
'debug',
`${chalk.greenBright('[OK]')} Patched ${chalk.cyanBright(
'VoiceConnection.prototype.configureNetworking',
)} [${chalk.bgMagentaBright('@discordjs/voice')} - ${chalk.redBright('v0.16.0')}]`,
);
/* eslint-enable */
} catch (e) {
client.emit(
'debug',
`${chalk.redBright('[Fail]')} Patched ${chalk.cyanBright(
'VoiceConnection.prototype.configureNetworking',
)} [${chalk.bgMagentaBright('@discordjs/voice')} - ${chalk.redBright('v0.16.0')}]\n${e.stack}`,
);
client.emit(
Events.ERROR,
`${chalk.redBright('[Fail]')} Patched ${chalk.cyanBright(
'VoiceConnection.prototype.configureNetworking',
)} [${chalk.bgMagentaBright('@discordjs/voice')} - ${chalk.redBright('v0.16.0')}]`,
);
client.emit(
Events.ERROR,
`${chalk.redBright('[Error]')} Please install ${chalk.bgMagentaBright(
'@discordjs/voice',
)} version ${chalk.redBright('v0.16.0')}`,
);
}
}
module.exports = async (client, { d: data }, shard) => {
Util.clientRequiredAction(client, data.required_action);
if (!firstReady) {
if (client.options.checkUpdate) {
client.once('update', (currentVersion, newVersion) => {
if (!newVersion) {
console.log(`
${chalk.redBright('[WARNING]')} Cannot check new Discord.js-selfbot-v13 version.
Current: ${chalk.blueBright(currentVersion)}
If you don't want to show this message, set ${chalk.cyanBright('checkUpdate')} to false
const client = new Client({
checkUpdate: false,
});
and using event update
https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-update\n`);
} else if (currentVersion !== newVersion && !currentVersion.includes('-')) {
console.log(`
${chalk.yellowBright('[WARNING]')} New Discord.js-selfbot-v13 version.
Current: ${chalk.redBright(currentVersion)} => Latest: ${chalk.greenBright(newVersion)}
If you don't want to show this message, set ${chalk.cyanBright('checkUpdate')} to false
const client = new Client({
checkUpdate: false,
});
and using event update
https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-update\n`);
} else {
console.log(
`
${chalk.greenBright('[OK]')} Discord.js-selfbot-v13 is up to date. Current: ${chalk.blueBright(currentVersion)}
If you don't want to show this message, set ${chalk.cyanBright('checkUpdate')} to false
const client = new Client({
checkUpdate: false,
});
and using event update
https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-update\n`,
);
}
});
client.checkUpdate();
}
if (client.options.patchVoice) {
patchVoice(client);
}
if (client.options.syncStatus) {
client.customStatusAuto(client);
}
firstReady = true;
}
module.exports = (client, { d: data }, shard) => {
// Check
USER_REQUIRED_ACTION(client, { d: data });
if (client.user) {
client.user._patch(data.user);
@@ -109,31 +17,8 @@ module.exports = async (client, { d: data }, shard) => {
client.users.cache.set(client.user.id, client.user);
}
client.settings._patch(data.user_settings);
client.user.connectedAccounts = data.connected_accounts ?? [];
client.relationships._setup(data.relationships);
client.user._patchNote(data.notes);
const syncTime = Date.now();
for (const private_channel of data.private_channels) {
const channel = client.channels._add(private_channel);
// Rate limit warning
if (client.options.DMSync) {
client.ws.broadcast({
op: Opcodes.DM_UPDATE,
d: {
channel_id: channel.id,
},
});
}
}
if (client.options.DMSync) {
console.warn(
`Gateway Rate Limit Warning: Sending ${data.private_channels.length} Requests / ${Date.now() - syncTime || 1} ms`,
);
client.channels._add(private_channel);
}
for (const guild of data.guilds) {
@@ -141,31 +26,41 @@ module.exports = async (client, { d: data }, shard) => {
client.guilds._add(guild);
}
for (const gSetting of Array.isArray(data.user_guild_settings) ? data.user_guild_settings : []) {
const guild = client.guilds.cache.get(gSetting.guild_id);
if (guild) guild.settings._patch(gSetting);
}
const largeGuilds = data.guilds.filter(g => g.large);
client.emit('debug', `[READY] Received ${data.guilds.length} guilds, ${largeGuilds.length} large guilds`);
// Receive messages in large guilds
for (const guild of largeGuilds) {
await client.sleep(client.options.messageCreateEventGuildTimeout);
client.ws.broadcast({
op: Opcodes.GUILD_SUBSCRIPTIONS,
d: {
guild_id: guild.id,
typing: true,
threads: true,
activities: true,
thread_member_lists: [],
members: [],
channels: {},
},
});
}
// User Notes
client.notes._reload(data.notes);
shard.checkReady();
// Relationship
client.relationships._setup(data.relationships);
Promise.all(
largeGuilds.map(async (guild, index) => {
client.ws.broadcast({
op: Opcodes.GUILD_SUBSCRIPTIONS,
d: {
guild_id: guild.id,
typing: true,
threads: true,
activities: true,
thread_member_lists: [],
members: [],
channels: {},
},
});
client.emit('debug', `[READY] Register guild ${guild.id}`);
await client.sleep(client.options.messageCreateEventGuildTimeout * index);
}),
data.private_channels.map(async (c, index) => {
if (client.options.DMChannelVoiceStatusSync < 1) return;
client.ws.broadcast({
op: Opcodes.DM_UPDATE,
d: {
channel_id: c.id,
},
});
await client.sleep(client.options.DMChannelVoiceStatusSync * index);
}),
).then(() => shard.checkReady());
};

View File

@@ -1,12 +0,0 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);
guild?.settings._patch(data);
/**
* Emitted whenever guild settings are updated
* @event Client#userGuildSettingsUpdate
* @param {Guild} guild Guild
*/
return client.emit(Events.USER_GUILD_SETTINGS_UPDATE, guild);
};

View File

@@ -1,9 +0,0 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => {
client.settings._patch(data);
if (('status' in data || 'custom_status' in data) && client.options.syncStatus) {
client.customStatusAuto(client);
}
return client.emit(Events.USER_SETTINGS_UPDATE, data);
};

View File

@@ -3,10 +3,6 @@
const handlers = Object.fromEntries([
['READY', require('./READY')],
['RESUMED', require('./RESUMED')],
['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')],
['RELATIONSHIP_REMOVE', require('./RELATIONSHIP_REMOVE')],
['RELATIONSHIP_UPDATE', require('./RELATIONSHIP_UPDATE')],
['APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE', require('./APPLICATION_COMMAND_AUTOCOMPLETE_RESPONSE')],
['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')],
['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')],
['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')],
@@ -15,9 +11,6 @@ const handlers = Object.fromEntries([
['AUTO_MODERATION_RULE_CREATE', require('./AUTO_MODERATION_RULE_CREATE')],
['AUTO_MODERATION_RULE_DELETE', require('./AUTO_MODERATION_RULE_DELETE')],
['AUTO_MODERATION_RULE_UPDATE', require('./AUTO_MODERATION_RULE_UPDATE')],
['CALL_CREATE', require('./CALL_CREATE')],
['CALL_UPDATE', require('./CALL_UPDATE')],
['CALL_DELETE', require('./CALL_DELETE')],
['GUILD_CREATE', require('./GUILD_CREATE')],
['GUILD_DELETE', require('./GUILD_DELETE')],
['GUILD_UPDATE', require('./GUILD_UPDATE')],
@@ -27,8 +20,6 @@ const handlers = Object.fromEntries([
['GUILD_MEMBER_REMOVE', require('./GUILD_MEMBER_REMOVE')],
['GUILD_MEMBER_UPDATE', require('./GUILD_MEMBER_UPDATE')],
['GUILD_MEMBERS_CHUNK', require('./GUILD_MEMBERS_CHUNK')],
['GUILD_MEMBER_LIST_UPDATE', require('./GUILD_MEMBER_LIST_UPDATE.js')],
['GUILD_APPLICATION_COMMANDS_UPDATE', require('./GUILD_APPLICATION_COMMANDS_UPDATE.js')],
['GUILD_INTEGRATIONS_UPDATE', require('./GUILD_INTEGRATIONS_UPDATE')],
['GUILD_ROLE_CREATE', require('./GUILD_ROLE_CREATE')],
['GUILD_ROLE_DELETE', require('./GUILD_ROLE_DELETE')],
@@ -40,9 +31,6 @@ const handlers = Object.fromEntries([
['CHANNEL_DELETE', require('./CHANNEL_DELETE')],
['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')],
['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')],
['CHANNEL_RECIPIENT_ADD', require('./CHANNEL_RECIPIENT_ADD')],
['CHANNEL_RECIPIENT_REMOVE', require('./CHANNEL_RECIPIENT_REMOVE')],
['MESSAGE_ACK', require('./MESSAGE_ACK')],
['MESSAGE_CREATE', require('./MESSAGE_CREATE')],
['MESSAGE_DELETE', require('./MESSAGE_DELETE')],
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')],
@@ -57,21 +45,12 @@ const handlers = Object.fromEntries([
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],
['THREAD_MEMBER_UPDATE', require('./THREAD_MEMBER_UPDATE')],
['THREAD_MEMBERS_UPDATE', require('./THREAD_MEMBERS_UPDATE')],
['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')], // Opcode 0
['USER_GUILD_SETTINGS_UPDATE', require('./USER_GUILD_SETTINGS_UPDATE')],
// USER_SETTINGS_PROTO_UPDATE // opcode 0
['USER_NOTE_UPDATE', require('./USER_NOTE_UPDATE')],
['USER_REQUIRED_ACTION_UPDATE', require('./USER_REQUIRED_ACTION_UPDATE')],
['USER_UPDATE', require('./USER_UPDATE')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
['TYPING_START', require('./TYPING_START')],
['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')],
['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')],
['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')],
['INTERACTION_CREATE', require('./INTERACTION_CREATE')],
['INTERACTION_SUCCESS', require('./INTERACTION_SUCCESS')],
['INTERACTION_MODAL_CREATE', require('./INTERACTION_MODAL_CREATE')],
['INTERACTION_FAILURE', require('./INTERACTION_FAILURE')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
@@ -82,6 +61,18 @@ const handlers = Object.fromEntries([
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')],
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')],
['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')],
// Selfbot
['RELATIONSHIP_ADD', require('./RELATIONSHIP_ADD')],
['RELATIONSHIP_REMOVE', require('./RELATIONSHIP_REMOVE')],
['RELATIONSHIP_UPDATE', require('./RELATIONSHIP_UPDATE')],
['USER_NOTE_UPDATE', require('./USER_NOTE_UPDATE')],
['CHANNEL_RECIPIENT_ADD', require('./CHANNEL_RECIPIENT_ADD')],
['CHANNEL_RECIPIENT_REMOVE', require('./CHANNEL_RECIPIENT_REMOVE')],
['INTERACTION_MODAL_CREATE', require('./INTERACTION_MODAL_CREATE')],
['USER_REQUIRED_ACTION_UPDATE', require('./USER_REQUIRED_ACTION_UPDATE')],
['CALL_CREATE', require('./CALL_CREATE')],
['CALL_UPDATE', require('./CALL_UPDATE')],
['CALL_DELETE', require('./CALL_DELETE')],
]);
module.exports = handlers;

View File

@@ -11,8 +11,6 @@ const Messages = {
TOKEN_INVALID: 'An invalid token was provided.',
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.',
MFA_INVALID: 'An invalid mfa code was provided',
WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.',
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
WS_NOT_OPEN: (data = 'data') => `WebSocket not open to send ${data}`,
@@ -20,8 +18,8 @@ const Messages = {
BITFIELD_INVALID: bit => `Invalid bitfield flag or number: ${bit}.`,
SHARDING_INVALID: 'Invalid shard settings were provided.',
SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.',
SHARDING_INVALID: '[Bot Token] Invalid shard settings were provided.',
SHARDING_REQUIRED: '[Bot Token] This session would have handled too many guilds - Sharding is required.',
INVALID_INTENTS: '[Bot Token] Invalid intent provided for WebSocket intents.',
DISALLOWED_INTENTS: '[Bot Token] Privileged intent provided is not enabled or whitelisted.',
SHARDING_NO_SHARDS: 'No shards have been spawned.',
@@ -49,12 +47,6 @@ const Messages = {
EMBED_FOOTER_TEXT: 'MessageEmbed footer text must be a string.',
EMBED_DESCRIPTION: 'MessageEmbed description must be a string.',
EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.',
/* Add */
MISSING_PERMISSIONS: (...permission) => `You can't do this action [Missing Permission(s): ${permission.join(', ')}]`,
EMBED_PROVIDER_NAME: 'MessageEmbed provider name must be a string.',
INVALID_COMMAND_NAME: allCMD => `Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`,
INVALID_RANGE_QUERY_MEMBER: 'Invalid range query member. (0<x<=100)',
MUST_SPECIFY_BOT: 'You must specify a bot to use this command.',
BUTTON_LABEL: 'MessageButton label must be a string',
BUTTON_URL: 'MessageButton URL must be a string',
@@ -66,16 +58,22 @@ const Messages = {
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',
MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title must be a string',
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
USER_BANNER_NOT_FETCHED: "You must fetch this user's banner before trying to generate its URL!",
USER_NO_DM_CHANNEL: 'No DM Channel exists!',
CLIENT_NO_CALL: 'No call exists!',
VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.',
VOICE_NOT_IN_GUILD: 'You are only allowed to do this in guild channels.',
VOICE_STATE_NOT_OWN:
'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.',
@@ -103,17 +101,11 @@ const Messages = {
GUILD_CHANNEL_UNOWNED: "The fetched channel does not belong to this manager's guild.",
GUILD_OWNED: 'Guild is owned by the client.',
GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.",
GUILD_APPLICATION_COMMANDS_SEARCH_TIMEOUT: "Application commands didn't arrive in time.",
GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.',
CHANNEL_NOT_CACHED: 'Could not find the channel where this message came from in the cache!',
STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.',
GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.',
REQUIRE_PASSWORD: 'You must provide a password.',
INVALIDATE_MEMBER: range => `Invalid member range: [${range[0]}, ${range[1]}]`,
MISSING_VALUE: (where, type) => `Missing value for ${where} (${type})`,
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}`,
@@ -141,10 +133,6 @@ const Messages = {
INVITE_NOT_FOUND: 'Could not find the requested invite.',
NOT_OWNER_GROUP_DM_CHANNEL: "You can't do this action [Missing Permission]",
USER_ALREADY_IN_GROUP_DM_CHANNEL: 'User is already in the channel.',
USER_NOT_IN_GROUP_DM_CHANNEL: 'User is not in the channel.',
DELETE_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot delete them",
FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them",
@@ -169,60 +157,28 @@ const Messages = {
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function',
INVALID_BOT_METHOD: 'Bot accounts cannot use this method',
INVALID_USER_METHOD: 'User accounts cannot use this method',
BOT_ONLY: 'This method only for bots',
USER_ONLY: 'This method only for users',
INTERACTION_SEND_FAILURE: msg => `${msg}`,
INVALID_LOCALE: 'Unable to select this location',
FOLDER_NOT_FOUND: 'Server directory not found',
FOLDER_POSITION_INVALID: 'The server index in the directory is invalid',
APPLICATION_ID_INVALID: "The application isn't BOT",
INVALID_NITRO: 'Invalid Nitro Code',
MESSAGE_ID_NOT_FOUND: 'Message ID not found',
MESSAGE_EMBED_LINK_LENGTH: 'Message content with embed link length is too long',
GUILD_MEMBERS_FETCH: msg => `${msg}`,
USER_NOT_STREAMING: 'User is not streaming',
// Djs v13.7
TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',
MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title must be a string',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVALID_REMOTE_AUTH_URL: 'Invalid remote auth URL (https://discord.com/ra/{hash})',
INVALID_URL: url =>
`Invalid URL: ${url}.\nMake sure you are using a valid URL (https://discord.com/oauth2/authorize?...)`,
NITRO_REQUIRED: 'This feature is only available for Nitro users.',
NITRO_BOOST_REQUIRED: feature => `This feature (${feature}) is only available for Nitro Boost users.`,
ONLY_ME: 'This feature is only available for self.',
MISSING_CAPTCHA_SERVICE: 'This feature is only available for enabled captcha handler.',
GUILD_FORUM_MESSAGE_REQUIRED: 'You must provide a message to create a guild forum thread',
NORMAL_LOGIN: 'Username and password are required for normal login',
LOGIN_FAILED_UNKNOWN: 'Login failed',
LOGIN_FAILED_2FA: 'Login failed, 2FA code is required',
GUILD_IS_LARGE: 'This guild is too large to fetch all members with this method',
TEAM_MEMBER_FORMAT: 'The member provided is either not real or not of the User class',
MISSING_MODULE: (name, installCommand) =>
`The module "${name}" is missing. Please install it with "${installCommand}" and try again.`,
// Selfbot
INVALID_USER_API: 'User accounts cannot use this endpoint',
INVALID_COMMAND_NAME: allCMD => `Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`,
INVALID_SLASH_COMMAND_CHOICES: (parentOptions, value) =>
`${value} is not a valid choice for this option (${parentOptions})`,
SLASH_COMMAND_REQUIRED_OPTIONS_MISSING: (req, opt) => `Value required (${req}) missing (Options: ${opt})`,
SLASH_COMMAND_SUB_COMMAND_GROUP_INVALID: n => `${n} is not a valid sub command group`,
SLASH_COMMAND_SUB_COMMAND_INVALID: n => `${n} is not a valid sub command`,
INTERACTION_FAILED: 'No responsed from Application Command',
USER_NOT_STREAMING: 'User is not streaming',
};
for (const [name, message] of Object.entries(Messages)) register(name, message);

View File

@@ -1,11 +1,5 @@
'use strict';
const tls = require('tls');
// Cipher
tls.DEFAULT_CIPHERS = tls.DEFAULT_CIPHERS.split(':')
.sort(() => Math.random() - 0.5)
.join(':');
// "Root" classes (starting points)
exports.BaseClient = require('./client/BaseClient');
exports.Client = require('./client/Client');
@@ -15,7 +9,6 @@ exports.ShardingManager = require('./sharding/ShardingManager');
exports.WebhookClient = require('./client/WebhookClient');
// Utilities
exports.DiscordRPCServer = require('./util/arRPC/index');
exports.ActivityFlags = require('./util/ActivityFlags');
exports.ApplicationFlags = require('./util/ApplicationFlags');
exports.AttachmentFlags = require('./util/AttachmentFlags');
@@ -42,8 +35,6 @@ exports.ThreadMemberFlags = require('./util/ThreadMemberFlags');
exports.UserFlags = require('./util/UserFlags');
exports.Util = require('./util/Util');
exports.version = require('../package.json').version;
exports.DiscordAuthWebsocket = require('./util/RemoteAuth');
exports.PurchasedFlags = require('./util/PurchasedFlags');
// Managers
exports.ApplicationCommandManager = require('./managers/ApplicationCommandManager');
@@ -71,7 +62,6 @@ exports.PresenceManager = require('./managers/PresenceManager');
exports.ReactionManager = require('./managers/ReactionManager');
exports.ReactionUserManager = require('./managers/ReactionUserManager');
exports.RoleManager = require('./managers/RoleManager');
exports.SessionManager = require('./managers/SessionManager');
exports.StageInstanceManager = require('./managers/StageInstanceManager');
exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
@@ -80,6 +70,7 @@ exports.VoiceStateManager = require('./managers/VoiceStateManager');
exports.WebSocketManager = require('./client/websocket/WebSocketManager');
exports.WebSocketShard = require('./client/websocket/WebSocketShard');
exports.RelationshipManager = require('./managers/RelationshipManager');
exports.UserNoteManager = require('./managers/UserNoteManager');
// Structures
exports.Activity = require('./structures/Presence').Activity;
@@ -88,26 +79,18 @@ exports.Application = require('./structures/interfaces/Application');
exports.ApplicationCommand = require('./structures/ApplicationCommand');
exports.ApplicationRoleConnectionMetadata =
require('./structures/ApplicationRoleConnectionMetadata').ApplicationRoleConnectionMetadata;
exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction');
exports.AutoModerationActionExecution = require('./structures/AutoModerationActionExecution');
exports.AutoModerationRule = require('./structures/AutoModerationRule');
exports.Base = require('./structures/Base');
exports.BaseCommandInteraction = require('./structures/BaseCommandInteraction');
exports.BaseGuild = require('./structures/BaseGuild');
exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji');
exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel');
exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel');
exports.BaseMessageComponent = require('./structures/BaseMessageComponent');
exports.ButtonInteraction = require('./structures/ButtonInteraction');
exports.CategoryChannel = require('./structures/CategoryChannel');
exports.Channel = require('./structures/Channel').Channel;
exports.ClientApplication = require('./structures/ClientApplication');
exports.ClientPresence = require('./structures/ClientPresence');
exports.ClientUser = require('./structures/ClientUser');
exports.Collector = require('./structures/interfaces/Collector');
exports.CommandInteraction = require('./structures/CommandInteraction');
exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
exports.ContextMenuInteraction = require('./structures/ContextMenuInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Guild = require('./structures/Guild').Guild;
@@ -123,49 +106,27 @@ exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildS
exports.GuildTemplate = require('./structures/GuildTemplate');
exports.Integration = require('./structures/Integration');
exports.IntegrationApplication = require('./structures/IntegrationApplication');
exports.Interaction = require('./structures/Interaction');
exports.InteractionCollector = require('./structures/InteractionCollector');
exports.InteractionWebhook = require('./structures/InteractionWebhook');
exports.Invite = require('./structures/Invite');
exports.InviteStageInstance = require('./structures/InviteStageInstance');
exports.InviteGuild = require('./structures/InviteGuild');
exports.Message = require('./structures/Message').Message;
exports.MessageActionRow = require('./structures/MessageActionRow');
exports.MessageAttachment = require('./structures/MessageAttachment');
exports.MessageButton = require('./structures/MessageButton');
exports.MessageCollector = require('./structures/MessageCollector');
exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');
exports.MessageContextMenuInteraction = require('./structures/MessageContextMenuInteraction');
exports.MessageEmbed = require('./structures/MessageEmbed');
exports.WebEmbed = require('./structures/WebEmbed');
exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction');
exports.MessageSelectMenu = require('./structures/MessageSelectMenu');
exports.Modal = require('./structures/Modal');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
exports.GroupDMChannel = require('./structures/GroupDMChannel');
exports.PermissionOverwrites = require('./structures/PermissionOverwrites');
exports.Presence = require('./structures/Presence').Presence;
exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role;
exports.Session = require('./structures/Session');
// RPC
exports.getUUID = require('./structures/RichPresence').getUUID;
exports.CustomStatus = require('./structures/RichPresence').CustomStatus;
exports.RichPresence = require('./structures/RichPresence').RichPresence;
exports.SpotifyRPC = require('./structures/RichPresence').SpotifyRPC;
// SelectMenu
exports.ChannelSelectInteraction = require('./structures/SelectMenuInteraction').ChannelSelectInteraction;
exports.MentionableSelectInteraction = require('./structures/SelectMenuInteraction').MentionableSelectInteraction;
exports.RoleSelectInteraction = require('./structures/SelectMenuInteraction').RoleSelectInteraction;
exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction').SelectMenuInteraction;
exports.UserSelectInteraction = require('./structures/SelectMenuInteraction').UserSelectInteraction;
//
exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance;
exports.Sticker = require('./structures/Sticker').Sticker;
@@ -179,7 +140,6 @@ exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.Typing = require('./structures/Typing');
exports.User = require('./structures/User');
exports.UserContextMenuInteraction = require('./structures/UserContextMenuInteraction');
exports.VoiceChannel = require('./structures/VoiceChannel');
exports.VoiceRegion = require('./structures/VoiceRegion');
exports.VoiceState = require('./structures/VoiceState');
@@ -188,7 +148,12 @@ exports.Widget = require('./structures/Widget');
exports.WidgetMember = require('./structures/WidgetMember');
exports.WelcomeChannel = require('./structures/WelcomeChannel');
exports.WelcomeScreen = require('./structures/WelcomeScreen');
exports.WebSocket = require('./WebSocket');
// DiscordJSVoice Patch
exports.DiscordJSVoice = require('./util/Voice');
exports.CustomStatus = require('./structures/RichPresence').CustomStatus;
exports.RichPresence = require('./structures/RichPresence').RichPresence;
exports.SpotifyRPC = require('./structures/RichPresence').SpotifyRPC;
exports.WebEmbed = require('./structures/WebEmbed');
exports.DiscordAuthWebsocket = require('./util/RemoteAuth');
exports.PurchasedFlags = require('./util/PurchasedFlags');

View File

@@ -1,490 +0,0 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const BaseManager = require('./BaseManager');
const GuildFolderManager = require('./GuildFolderManager');
const { Error, TypeError } = require('../errors/DJSError');
const GuildFolder = require('../structures/GuildFolder');
const { CustomStatus } = require('../structures/RichPresence');
const { localeSetting, DMScanLevel, stickerAnimationMode } = require('../util/Constants');
/**
* Manages API methods for users and stores their cache.
* @extends {BaseManager}
* @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html}
*/
class ClientUserSettingManager extends BaseManager {
constructor(client) {
super(client);
/**
* Raw data
* @type {Object}
*/
this.rawSetting = {};
/**
* Language
* @type {?string}
*/
this.locale = null;
/**
* From: Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message
* @type {?boolean}
*/
this.activityDisplay = null;
/**
* Disable Direct Message from servers
* @type {Collection<Snowflake, boolean>}
*/
this.disableDMfromServer = new Collection();
/**
* Allow direct messages from server members
* @type {?boolean}
*/
this.DMfromServerMode = null;
/**
* Display images
* @type {?boolean}
*/
this.displayImage = null;
/**
* Display linked images
* @type {?boolean}
*/
this.linkedImageDisplay = null;
/**
* From: Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused.
* @type {?boolean}
*/
this.autoplayGIF = null;
/**
* Show embeds and preview website links pasted into chat
* @type {?boolean}
*/
this.previewLink = null;
/**
* From: Setting => APP SETTINGS => Accessibility => Play Animated Emojis
* @type {?boolean}
*/
this.animatedEmojis = null;
/**
* From: Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback
* @type {?boolean}
*/
this.allowTTS = null;
/**
* From: Setting => APP SETTINGS => Appearance => Message Display => Compact Mode
* @type {?boolean}
*/
this.compactMode = null;
/**
* From: Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons
* @type {?boolean}
*/
this.convertEmoticons = null;
/**
* SAFE DIRECT MESSAGING
* @type {?DMScanLevel}
*/
this.DMScanLevel = null;
/**
* From: Setting => APP SETTINGS => Appearance => Theme
* @type {'dark' | 'light' | null}
*/
this.theme = '';
/**
* Developer Mode (Copy ID, etc.)
* @type {?boolean}
*/
this.developerMode = null;
/**
* AFK timeout (receives notifications)
* @type {?number}
*/
this.afkTimeout = null;
/**
* Sticker animation mode
* @type {?stickerAnimationMode}
*/
this.stickerAnimationMode = null;
/**
* WHO CAN ADD YOU AS A FRIEND ?
* @type {?object}
* @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html#friend-source-flags-structure}
*/
this.addFriendFrom = {
all: null,
mutual_friends: null,
mutual_guilds: null,
};
/**
* From: Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions
* @type {?boolean}
*/
this.showEmojiReactions = null;
/**
* Custom Stauts
* @type {?object}
* @see {@link https://luna.gitlab.io/discord-unofficial-docs/custom_status.html#customstatus-structure}
*/
this.customStatus = null;
/**
* Guild folder and position
* @type {GuildFolderManager}
*/
this.guildFolder = new GuildFolderManager(client);
// Todo: add new method from Discum
}
/**
* Patch data file
* https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/user/user.py
* @private
* @param {Object} data Raw Data to patch
*/
_patch(data = {}) {
this.rawSetting = Object.assign(this.rawSetting, data);
if ('locale' in data) {
this.locale = localeSetting[data.locale];
}
if ('show_current_game' in data) {
this.activityDisplay = data.show_current_game;
}
if ('default_guilds_restricted' in data) {
this.DMfromServerMode = data.default_guilds_restricted;
}
if ('inline_attachment_media' in data) {
this.displayImage = data.inline_attachment_media;
}
if ('inline_embed_media' in data) {
this.linkedImageDisplay = data.inline_embed_media;
}
if ('gif_auto_play' in data) {
this.autoplayGIF = data.gif_auto_play;
}
if ('render_embeds' in data) {
this.previewLink = data.render_embeds;
}
if ('animate_emoji' in data) {
this.animatedEmojis = data.animate_emoji;
}
if ('enable_tts_command' in data) {
this.allowTTS = data.enable_tts_command;
}
if ('message_display_compact' in data) {
this.compactMode = data.message_display_compact;
}
if ('convert_emoticons' in data) {
this.convertEmoticons = data.convert_emoticons;
}
if ('explicit_content_filter' in data) {
this.DMScanLevel = DMScanLevel[data.explicit_content_filter];
}
if ('theme' in data) {
this.theme = data.theme;
}
if ('developer_mode' in data) {
this.developerMode = data.developer_mode;
}
if ('afk_timeout' in data) {
this.afkTimeout = data.afk_timeout * 1000; // Second => milisecond
}
if ('animate_stickers' in data) {
this.stickerAnimationMode = stickerAnimationMode[data.animate_stickers];
}
if ('render_reactions' in data) {
this.showEmojiReactions = data.render_reactions;
}
if ('custom_status' in data) {
this.customStatus = data.custom_status || {}; // Thanks PinkDuwc._#3443 reported this issue
this.customStatus.status = data.status;
}
if ('friend_source_flags' in data) {
this.addFriendFrom = {
all: data.friend_source_flags.all || false,
mutual_friends: data.friend_source_flags.all ? true : data.friend_source_flags.mutual_friends,
mutual_guilds: data.friend_source_flags.all ? true : data.friend_source_flags.mutual_guilds,
};
}
if ('guild_folders' in data) {
data.guild_folders.map((folder, index) =>
this.guildFolder.cache.set(index, new GuildFolder(this.client, folder)),
);
}
if ('restricted_guilds' in data) {
this.disableDMfromServer = new Collection(data.restricted_guilds.map(guildId => [guildId, true]));
}
}
async fetch() {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
const data = await this.client.api.users('@me').settings.get();
this._patch(data);
return this;
}
/**
* Edit data
* @param {Object} data Data to edit
* @private
*/
async edit(data) {
if (this.client.bot) throw new Error('INVALID_BOT_METHOD');
const res = await this.client.api.users('@me').settings.patch({ data });
this._patch(res);
return this;
}
/**
* Set compact mode
* @param {boolean | null} value Compact mode enable or disable
* @returns {boolean}
*/
async setDisplayCompactMode(value) {
if (typeof value !== 'boolean' && value !== null) {
throw new TypeError('INVALID_TYPE', 'value', 'boolean | null', true);
}
if (!value) value = !this.compactMode;
if (value !== this.compactMode) {
await this.edit({ message_display_compact: value });
}
return this.compactMode;
}
/**
* Discord Theme
* @param {null |dark |light} value Theme to set
* @returns {theme}
*/
async setTheme(value) {
const validValues = ['dark', 'light'];
if (typeof value !== 'string' && value !== null) {
throw new TypeError('INVALID_TYPE', 'value', 'string | null', true);
}
if (!validValues.includes(value)) {
if (value == validValues[0]) value = validValues[1];
else value = validValues[0];
}
if (value !== this.theme) {
await this.edit({ theme: value });
}
return this.theme;
}
/**
* CustomStatus Object
* @typedef {Object} CustomStatusOption
* @property {string | null} text Text to set
* @property {string | null} status The status to set: 'online', 'idle', 'dnd', 'invisible' or null.
* @property {EmojiResolvable | null} emoji UnicodeEmoji, DiscordEmoji, or null.
* @property {number | null} expires The number of seconds until the status expires, or null.
*/
/**
* Set custom status
* @param {?CustomStatus | CustomStatusOption} options CustomStatus
*/
setCustomStatus(options) {
if (typeof options !== 'object') {
this.edit({ custom_status: null });
} else if (options instanceof CustomStatus) {
options = options.toJSON();
let data = {
emoji_name: null,
expires_at: null,
text: null,
};
if (typeof options.state === 'string') {
data.text = options.state;
}
if (options.emoji) {
if (options.emoji?.id) {
data.emoji_name = options.emoji?.name;
data.emoji_id = options.emoji?.id;
} else {
data.emoji_name = typeof options.emoji?.name === 'string' ? options.emoji?.name : null;
}
}
this.edit({ custom_status: data });
} else {
let data = {
emoji_name: null,
expires_at: null,
text: null,
};
if (typeof options.text === 'string') {
if (options.text.length > 128) {
throw new RangeError('[INVALID_VALUE] Custom status text must be less than 128 characters');
}
data.text = options.text;
}
if (options.emoji) {
const emoji = this.client.emojis.resolve(options.emoji);
if (emoji) {
data.emoji_name = emoji.name;
data.emoji_id = emoji.id;
} else {
data.emoji_name = typeof options.emoji === 'string' ? options.emoji : null;
}
}
if (typeof options.expires === 'number') {
if (options.expires < Date.now()) {
throw new RangeError(`[INVALID_VALUE] Custom status expiration must be greater than ${Date.now()}`);
}
data.expires_at = new Date(options.expires).toISOString();
}
if (['online', 'idle', 'dnd', 'invisible'].includes(options.status)) this.edit({ status: options.status });
this.edit({ custom_status: data });
}
}
/**
* * Locale Setting, must be one of:
* * `DANISH`
* * `GERMAN`
* * `ENGLISH_UK`
* * `ENGLISH_US`
* * `SPANISH`
* * `FRENCH`
* * `CROATIAN`
* * `ITALIAN`
* * `LITHUANIAN`
* * `HUNGARIAN`
* * `DUTCH`
* * `NORWEGIAN`
* * `POLISH`
* * `BRAZILIAN_PORTUGUESE`
* * `ROMANIA_ROMANIAN`
* * `FINNISH`
* * `SWEDISH`
* * `VIETNAMESE`
* * `TURKISH`
* * `CZECH`
* * `GREEK`
* * `BULGARIAN`
* * `RUSSIAN`
* * `UKRAINIAN`
* * `HINDI`
* * `THAI`
* * `CHINA_CHINESE`
* * `JAPANESE`
* * `TAIWAN_CHINESE`
* * `KOREAN`
* @param {localeSetting} value Locale to set
* @returns {locale}
*/
async setLocale(value) {
if (typeof value !== 'string') {
throw new TypeError('INVALID_TYPE', 'value', 'string', true);
}
if (!localeSetting[value]) throw new Error('INVALID_LOCALE');
if (localeSetting[value] !== this.locale) {
await this.edit({ locale: localeSetting[value] });
}
return this.locale;
}
// TODO: Guild positions & folders
// Change Index in Array [Hidden]
/**
*
* @param {Array} array Array
* @param {number} from Index1
* @param {number} to Index2
* @returns {Array}
* @private
*/
_move(array, from, to) {
array.splice(to, 0, array.splice(from, 1)[0]);
return array;
}
// TODO: Move Guild
// folder to folder
// folder to home
// home to home
// home to folder
/**
* Change Guild Position (from * to Folder or Home)
* @param {GuildIDResolve} guildId guild.id
* @param {number} newPosition Guild Position
* * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder.
* @param {number} type Move to folder or home
* * `FOLDER`: 1
* * `HOME`: 2
* @param {FolderID} folderId If you want to move to folder
* @private
*/
guildChangePosition(guildId, newPosition, type, folderId) {
// Get Guild default position
// Escape
const oldGuildFolderPosition = this.rawSetting.guild_folders.findIndex(value => value.guild_ids.includes(guildId));
const newGuildFolderPosition = this.rawSetting.guild_folders.findIndex(value =>
value.guild_ids.includes(this.rawSetting.guild_positions[newPosition]),
);
if (type == 2 || `${type}`.toUpperCase() == 'HOME') {
// Delete GuildID from Folder and create new Folder
// Check it is folder
const folder = this.rawSetting.guild_folders[oldGuildFolderPosition];
if (folder.id) {
this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = this.rawSetting.guild_folders[
oldGuildFolderPosition
].guild_ids.filter(v => v !== guildId);
}
this.rawSetting.guild_folders = this._move(
this.rawSetting.guild_folders,
oldGuildFolderPosition,
newGuildFolderPosition,
);
this.rawSetting.guild_folders[newGuildFolderPosition].id = null;
} else if (type == 1 || `${type}`.toUpperCase() == 'FOLDER') {
// Delete GuildID from oldFolder
this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = this.rawSetting.guild_folders[
oldGuildFolderPosition
].guild_ids.filter(v => v !== guildId);
// Index new Folder
const folderIndex = this.rawSetting.guild_folders.findIndex(value => value.id == folderId);
const folder = this.rawSetting.guild_folders[folderIndex];
folder.guild_ids.push(guildId);
folder.guild_ids = [...new Set(folder.guild_ids)];
folder.guild_ids = this._move(
folder.guild_ids,
folder.guild_ids.findIndex(v => v == guildId),
newPosition,
);
}
this.edit({ guild_folders: this.rawSetting.guild_folders });
}
/**
* Restricted guilds setting
* @param {boolean} status Restricted status
* @returns {Promise}
*/
restrictedGuilds(status) {
if (typeof status !== 'boolean') {
throw new TypeError('INVALID_TYPE', 'status', 'boolean', true);
}
return this.edit({
default_guilds_restricted: status,
restricted_guilds: status ? this.client.guilds.cache.map(v => v.id) : [],
});
}
/**
* Add a guild to the list of restricted guilds.
* @param {GuildIDResolve} guildId The guild to add
* @returns {Promise}
*/
addRestrictedGuild(guildId) {
const temp = Object.assign(
[],
this.disableDMfromServer.map((v, k) => k),
);
if (temp.includes(guildId)) throw new Error('Guild is already restricted');
temp.push(guildId);
return this.edit({ restricted_guilds: temp });
}
/**
* Remove a guild from the list of restricted guilds.
* @param {GuildIDResolve} guildId The guild to remove
* @returns {Promise}
*/
removeRestrictedGuild(guildId) {
if (!this.disableDMfromServer.delete(guildId)) throw new Error('Guild is already restricted');
return this.edit({ restricted_guilds: this.disableDMfromServer.map((v, k) => k) });
}
}
module.exports = ClientUserSettingManager;

View File

@@ -1,104 +0,0 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const BaseManager = require('./BaseManager');
const DeveloperPortalApplication = require('../structures/DeveloperPortalApplication');
const Team = require('../structures/Team');
/**
* Manages API methods for users and stores their cache.
* @extends {BaseManager}
*/
class DeveloperPortalManager extends BaseManager {
constructor(client) {
super(client);
/**
* A collection of all the applications the client has.
* @type {Collection<Snowflake, DeveloperPortalApplication>}
* @readonly
*/
this.applications = new Collection();
/**
* A collection of all the teams the client has.
* @type {Collection<Snowflake, Team>}
* @readonly
*/
this.teams = new Collection(); // Collection<Snowflake, Team>
}
/**
* Fetches all the applications & teams the client has.
* @returns {Promise<DeveloperPortalManager>}
*/
async fetch() {
const promise1 = this.client.api.applications.get({
query: {
with_team_applications: true,
},
});
const promise2 = this.client.api.teams.get();
const [applications, teams] = await Promise.all([promise1, promise2]);
for (const team of teams) {
this.teams.set(team.id, new Team(this.client, team));
}
for (const application of applications) {
this.applications.set(application.id, new DeveloperPortalApplication(this.client, application));
}
return this;
}
/**
* Creates a new Team.
* @param {string} name Name of the team
* @returns {Promise<Team>}
*/
async createTeam(name) {
const team = await this.client.api.teams.post({
data: {
name: name,
},
});
this.teams.set(team.id, new Team(this.client, team));
return this.teams.get(team.id);
}
/**
* Creates a new application.
* @param {string} name Name of the application
* @param {?Snowflake | Team} teamId The team to create the application in
* @returns {Promise<DeveloperPortalApplication>}
*/
async createApplication(name, teamId = null) {
teamId = teamId instanceof Team ? teamId.id : teamId;
const application = await this.client.api.applications.post({
data: {
name,
team_id: teamId,
},
});
this.applications.set(application.id, new DeveloperPortalApplication(this.client, application));
return this.applications.get(application.id);
}
/**
* Deletes an application.
* @param {Snowflake} id Application ID
* @param {?number} MFACode 2FA code (if 2FA is enabled)
* @returns {Promise<void>}
*/
async deleteApplication(id, MFACode) {
if (MFACode) {
await this.client.api.applications[`${id}/delete`].post({
query: {
code: MFACode,
},
});
} else {
await this.client.api.applications[`${id}/delete`].post();
}
this.applications.delete(id);
return undefined;
}
}
module.exports = DeveloperPortalManager;

View File

@@ -1,24 +0,0 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const BaseManager = require('./BaseManager');
/**
* Manages API methods for users and stores their cache.
* @extends {BaseManager}
*/
class GuildFolderManager extends BaseManager {
constructor(client) {
super(client);
/**
* The guild folder cache (Index, GuildFolder)
* @type {Collection<number, GuildFolder>}
*/
this.cache = new Collection();
}
_refresh() {
this.cache.clear();
}
}
module.exports = GuildFolderManager;

View File

@@ -1,148 +0,0 @@
'use strict';
const BaseManager = require('./BaseManager');
/**
* Manages API methods for users and stores their cache.
* @extends {BaseManager}
* @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html}
*/
class GuildSettingManager extends BaseManager {
constructor(client, guildId = null) {
super(client);
/**
* Raw data
* @type {Object}
*/
this.rawSetting = {};
/**
* Guild Id
* @type {?Snowflake}
*/
this.guildId = guildId;
}
/**
* Get the guild
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildId);
}
/**
* Patch data file
* @private
* @param {Object} data Raw Data to patch
*/
_patch(data = {}) {
this.rawSetting = Object.assign(this.rawSetting, data);
if ('suppress_everyone' in data) {
/**
* Notification setting > Suppress `@everyone` and `@here`
* @type {?boolean}
*/
this.suppressEveryone = data.suppress_everyone;
}
if ('suppress_roles' in data) {
/**
* Notification setting > Suppress all role `@mention`
* @type {?boolean}
*/
this.suppressRoles = data.suppress_roles;
}
if ('mute_scheduled_events' in data) {
/**
* Notification setting > Mute new events
* @type {?boolean}
*/
this.muteScheduledEvents = data.mute_scheduled_events;
}
if ('message_notifications' in data) {
/**
* Notification setting > Message notifications
* * `0` = All messages
* * `1` = Only @mentions
* * `2` = Nothing
* @type {?number}
*/
this.messageNotifications = data.message_notifications;
}
if ('flags' in data) {
/**
* Flags (unknown)
* @type {?number}
*/
this.flags = data.flags;
}
if ('mobile_push' in data) {
/**
* Notification setting > Mobile push notifications
* @type {?boolean}
*/
this.mobilePush = data.mobile_push;
}
if ('muted' in data) {
/**
* Mute server
* @type {?boolean}
*/
this.muted = data.muted;
}
if ('mute_config' in data && data.mute_config !== null) {
/**
* Mute config (muted = true)
* * `muteConfig.endTime`: End time (Date)
* * `muteConfig.selectedTimeWindow`: Selected time window (seconds) (number)
* @type {?Object}
*/
this.muteConfig = {
endTime: new Date(data.mute_config.end_time),
selectedTimeWindow: data.mute_config.selected_time_window,
};
} else {
this.muteConfig = null;
}
if ('hide_muted_channels' in data) {
/**
* Hide muted channels
* @type {?boolean}
*/
this.hideMutedChannels = data.hide_muted_channels;
}
if ('channel_overrides' in data) {
/**
* Channel overrides (unknown)
* @type {?Array}
*/
this.channelOverrides = data.channel_overrides;
}
if ('notify_highlights' in data) {
/**
* Notification setting > Suppress highlights
* * `0` = ??? (unknown)
* * `1` = Enable
* * `2` = Disable
* @type {?number}
*/
this.notifyHighlights = data.notify_highlights;
}
if ('version' in data) {
/**
* Version (unknown)
* @type {?number}
*/
this.version = data.version;
}
}
/**
* Edit guild settings
* @param {Object} data Data to edit
* @returns {Promise<GuildSettingManager>}
*/
async edit(data) {
const data_ = await this.client.api.users('@me').settings.patch(data);
this._patch(data_);
return this;
}
}
module.exports = GuildSettingManager;

View File

@@ -1,57 +0,0 @@
'use strict';
const CachedManager = require('./CachedManager');
const { Error } = require('../errors/DJSError');
const Session = require('../structures/Session');
/**
* Manages API methods for users and stores their cache.
* @extends {CachedManager}
*/
class SessionManager extends CachedManager {
constructor(client, iterable) {
super(client, Session, iterable);
}
/**
* The cache of Sessions
* @type {Collection<string, Session>}
* @name SessionManager#cache
*/
/**
* Fetch all sessions of the client.
* @returns {Promise<SessionManager>}
*/
fetch() {
return new Promise((resolve, reject) => {
this.client.api.auth.sessions
.get()
.then(data => {
const allData = data.user_sessions;
this.cache.clear();
for (const session of allData) {
this._add(new Session(this.client, session), true, { id: session.id_hash });
}
resolve(this);
})
.catch(reject);
});
}
/**
* Logout the client (remote).
* @param {string | null} mfaCode MFA code (if 2FA is enabled)
* @returns {Promise<undefined>}
*/
logoutAllDevices(mfaCode) {
if (typeof this.client.password !== 'string') throw new Error('REQUIRE_PASSWORD');
return this.client.api.auth.sessions.logout({
data: {
session_id_hashes: this.cache.map(session => session.id),
password: this.client.password,
code: typeof mfaCode === 'string' ? mfaCode : undefined,
},
});
}
}
module.exports = SessionManager;

View File

@@ -1,520 +0,0 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { ApplicationRoleConnectionMetadata } = require('./ApplicationRoleConnectionMetadata');
const Base = require('./Base');
const ApplicationFlags = require('../util/ApplicationFlags');
const { ClientApplicationAssetTypes, Endpoints, ApplicationRoleConnectionMetadataTypes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
/**
* Represents an OAuth2 Application.
* @extends {Base}
* @abstract
*/
class DeveloperPortalApplication extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
/**
* The application's id
* @type {Snowflake}
*/
this.id = data.id;
if ('name' in data) {
/**
* The name of the application
* @type {?string}
*/
this.name = data.name;
} else {
this.name ??= null;
}
if ('description' in data) {
/**
* The application's description
* @type {?string}
*/
this.description = data.description;
} else {
this.description ??= null;
}
if ('icon' in data) {
/**
* The application's icon hash
* @type {?string}
*/
this.icon = data.icon;
} else {
this.icon ??= null;
}
if ('bot' in data) {
/**
* Bot application
* @type {User}
*/
this.bot = this.client.users._add(data.bot);
}
/**
* The tags this application has (max of 5)
* @type {string[]}
*/
this.tags = data.tags ?? [];
if ('install_params' in data) {
/**
* Settings for this application's default in-app authorization
* @type {?ClientApplicationInstallParams}
*/
this.installParams = {
scopes: data.install_params.scopes,
permissions: new Permissions(data.install_params.permissions).freeze(),
};
} else {
this.installParams ??= null;
}
if ('custom_install_url' in data) {
/**
* This application's custom installation URL
* @type {?string}
*/
this.customInstallURL = data.custom_install_url;
} else {
this.customInstallURL = null;
}
if ('flags' in data) {
/**
* The flags this application has
* @type {ApplicationFlags}
*/
this.flags = new ApplicationFlags(data.flags).freeze();
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('rpc_origins' in data) {
/**
* The application's RPC origins, if enabled
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('bot_require_code_grant' in data) {
/**
* If this application's bot requires a code grant when using the OAuth2 flow
* @type {?boolean}
*/
this.botRequireCodeGrant = data.bot_require_code_grant;
} else {
this.botRequireCodeGrant ??= null;
}
if ('bot_public' in data) {
/**
* If this application's bot is public
* @type {?boolean}
*/
this.botPublic = data.bot_public;
} else {
this.botPublic ??= null;
}
/**
* The owner of this OAuth application
* @type {?(User|Team)}
*/
this.owner = null;
if (data.owner.username == `team${data.owner.id}` && data.owner.discriminator == '0000') {
this.owner = this.client.developerPortal.teams.get(data.owner.id);
} else {
this.owner = data.owner ? this.client.users._add(data.owner) : this.owner ?? null;
}
/**
* Redirect URIs for this application
* @type {Array<string>}
*/
this.redirectURIs = data.redirect_uris ?? [];
/**
* BOT_HTTP_INTERACTIONS feature flag
* @type {?string}
*/
this.interactionEndpointURL = data.interactions_endpoint_url ?? null;
/**
* Public key
* @type {?string}
*/
this.publicKey = data.verify_key ?? null;
/**
* @typedef {Object} Tester
* @property {number} state The state of the tester (2: Accepted, 1: Pending)
* @property {User} user The user that the tester is
*/
/**
* User tester
* @type {Collection<Snowflake, Tester>}
*/
this.testers = new Collection(); // <Snowflake, User>
/**
* Terms of service URL
* @type {?string}
*/
this.TermsOfService = data.terms_of_service_url ?? null;
/**
* Privacy policy URL
* @type {?string}
*/
this.PrivacyPolicy = data.privacy_policy_url ?? null;
if ('role_connections_verification_url' in data) {
/**
* This application's role connection verification entry point URL
* @type {?string}
*/
this.roleConnectionsVerificationURL = data.role_connections_verification_url;
} else {
this.roleConnectionsVerificationURL ??= null;
}
}
/**
* The timestamp the application was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return SnowflakeUtil.timestampFrom(this.id);
}
/**
* The time the application was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* A link to the application's icon.
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
}
/**
* A link to this application's cover image.
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
coverURL({ format, size } = {}) {
if (!this.cover) return null;
return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
}
/**
* Asset data.
* @typedef {Object} ApplicationAsset
* @property {Snowflake} id The asset's id
* @property {string} name The asset's name
* @property {string} type The asset's type
*/
/**
* Gets the application's rich presence assets.
* @returns {Promise<Array<ApplicationAsset>>}
* @deprecated This will be removed in the next major as it is unsupported functionality.
*/
async fetchAssets() {
const assets = await this.client.api.oauth2.applications(this.id).assets.get();
return assets.map(a => ({
id: a.id,
name: a.name,
type: AssetTypes[a.type - 1],
}));
}
/**
* Whether this application is partial
* @type {boolean}
* @readonly
*/
get partial() {
return !this.name;
}
/**
* Obtains this application from Discord.
* @returns {Promise<DeveloperPortalApplication>}
*/
async fetch() {
const app = await this.client.api.applications[this.id].get();
this._patch(app);
return this;
}
/**
* Gets all testers for this application.
* @returns {Promise<DeveloperPortalApplication>}
*/
async fetchTesters() {
const app = await this.client.api.applications[this.id].allowlist.get();
this.testers = new Collection();
for (const tester of app || []) {
this.testers.set(tester.user.id, {
state: tester.state,
user: this.client.users._add(tester.user),
});
}
return this;
}
/**
* Add user to this application's allowlist.
* @param {string} username Username of the user to add
* @param {string} discriminator Discriminator of the user to add
* @returns {Promise<DeveloperPortalApplication>}
*/
async addTester(username, discriminator) {
const app = await this.client.api.applications[this.id].allowlist.post({
data: {
username,
discriminator,
},
});
this.testers.set(app.user.id, {
state: app.state,
user: this.client.users._add(app.user),
});
return this;
}
/**
* Delete user from this application's allowlist.
* @param {UserResolvable} user User
* @returns {Promise<DeveloperPortalApplication>}
*/
async deleteTester(user) {
const userId = this.client.users.resolveId(user);
await this.client.api.applications[this.id].allowlist[userId].delete();
this.testers.delete(userId);
return this;
}
/**
* The data for editing a application.
* @typedef {Object} ApplicationEditData
* @property {string} [name] The name of the app
* @property {string} [description] The description of the app
* @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the app
* @property {?(BufferResolvable|Base64Resolvable)} [cover] The application's default rich presence invite
* @property {boolean} [botPublic] When false only app owner can join the app's bot to guilds
* @property {boolean} [botRequireCodeGrant] When true the app's bot will only join upon completion of the full oauth2 code grant flow
* @property {?string} [TermsOfService] ToS URL
* @property {?string} [PrivacyPolicy] Privacy policy URL
* @property {number} [flags] The application's public flags
* @property {Array<string>} [redirectURIs] Redirect URIs (OAuth2 only)
* @property {Array<string>} [tags] Up to 5 tags describing the content and functionality of the application
*/
/**
* Edits this application.
* @param {ApplicationEditData} data Edit data for the application
* @returns {Promise<DeveloperPortalApplication>}
*/
async edit(data) {
const _data = {};
if (data.name) _data.name = data.name;
if (typeof data.icon !== 'undefined') {
_data.icon = await DataResolver.resolveImage(data.icon);
}
if (data.description) _data.description = data.description;
if (typeof data.cover !== 'undefined') {
_data.cover = await DataResolver.resolveImage(data.cover);
}
if (data.botPublic) _data.bot_public = data.botPublic;
if (data.botRequireCodeGrant) _data.bot_require_code_grant = data.botRequireCodeGrant;
if (data.TermsOfService) _data.terms_of_service_url = data.TermsOfService;
if (data.PrivacyPolicy) _data.privacy_policy_url = data.PrivacyPolicy;
if (data.flags) _data.flags = data.flags;
if (data.redirectURIs) _data.redirect_uris = data.redirectURIs;
if (data.tags) _data.tags = data.tags;
//
const app = await this.client.api.applications[this.id].patch({ data: _data });
this._patch(app);
return this;
}
/**
* Creates a new bot for this application.
* @returns {Promise<DeveloperPortalApplication>}
*/
async createBot() {
if (this.bot) throw new Error('Application already has a bot.');
await this.client.api.applications[this.id].bot.post();
const app = await this.fetch();
return app;
}
/**
* Reset CLient Secret for this application.
* @param {number} MFACode The MFA code (if required)
* @returns {Promise<string>}
*/
async resetClientSecret(MFACode) {
const app = MFACode
? await this.client.api.applications[this.id].reset.post({
data: {
code: MFACode,
},
})
: await this.client.api.applications[this.id].reset.post();
return app.secret;
}
/**
* Reset Bot Token for this application.
* @param {number} MFACode The MFA code (if required)
* @returns {Promise<string>}
*/
async resetBotToken(MFACode) {
const app = MFACode
? await this.client.api.applications[this.id].bot.reset.post({
data: {
code: MFACode,
},
})
: await this.client.api.applications[this.id].bot.reset.post();
return app.token;
}
/**
* Deletes this application.
* @param {number} MFACode The MFA code (if required)
* @returns {Promise<void>}
*/
delete(MFACode) {
return this.client.developerPortal.deleteApplication(this.id, MFACode);
}
/**
* Add new image to this application. (RPC)
* @param {BufferResolvable|Base64Resolvable} image Image Resolvable
* @param {string} name Name of the image
* @returns {ApplicationAsset}
*/
async addAsset(image, name) {
const data = await DataResolver.resolveImage(image);
const asset = await this.client.api.applications[this.id].assets.post({
data: {
type: 1,
name,
image: data,
},
});
return {
id: asset.id,
name: asset.name,
type: AssetTypes[asset.type - 1],
};
}
/**
* Delete an image from this application. (RPC)
* @param {Snowflake} id ID of the image
* @returns {Promise<undefined>}
*/
async deleteAsset(id) {
await this.client.api.applications[this.id].assets[id].delete();
return undefined;
}
/**
* Gets this application's role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async fetchRoleConnectionMetadataRecords() {
const metadata = await this.client.api.applications(this.id)('role-connections').metadata.get();
return metadata.map(data => new ApplicationRoleConnectionMetadata(data));
}
/**
* Data for creating or editing an application role connection metadata.
* @typedef {Object} ApplicationRoleConnectionMetadataEditOptions
* @property {string} name The name of the metadata field
* @property {?Object<Locale, string>} [nameLocalizations] The name localizations for the metadata field
* @property {string} description The description of the metadata field
* @property {?Object<Locale, string>} [descriptionLocalizations] The description localizations for the metadata field
* @property {string} key The dictionary key of the metadata field
* @property {ApplicationRoleConnectionMetadataType} type The type of the metadata field
*/
/**
* Updates this application's role connection metadata records
* @param {ApplicationRoleConnectionMetadataEditOptions[]} records The new role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async editRoleConnectionMetadataRecords(records) {
const newRecords = await this.client.api
.applications(this.client.user.id)('role-connections')
.metadata.put({
data: records.map(record => ({
type: typeof record.type === 'string' ? ApplicationRoleConnectionMetadataTypes[record.type] : record.type,
key: record.key,
name: record.name,
name_localizations: record.nameLocalizations,
description: record.description,
description_localizations: record.descriptionLocalizations,
})),
});
return newRecords.map(data => new ApplicationRoleConnectionMetadata(data));
}
/**
* When concatenated with a string, this automatically returns the application's name instead of the
* Application object.
* @returns {?string}
* @example
* // Logs: Application name: My App
* console.log(`Application name: ${application}`);
*/
toString() {
return this.name;
}
toJSON() {
return super.toJSON({ createdTimestamp: true });
}
}
module.exports = DeveloperPortalApplication;

View File

@@ -1,75 +0,0 @@
'use strict';
const Base = require('./Base');
/**
* Guild Folder.
* @abstract
*/
class GuildFolder extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
if ('id' in data) {
/**
* The guild folder's id
* @type {Snowflake}
*/
this.id = data.id;
}
if ('name' in data) {
/**
* The guild folder's name
* @type {string}
*/
this.name = data.name;
}
if ('color' in data) {
/**
* The base 10 color of the folder
* @type {number}
*/
this.color = data.color;
}
if ('guild_ids' in data) {
/**
* The guild folder's guild ids
* @type {Snowflake[]}
*/
this.guild_ids = data.guild_ids;
}
}
/**
* The hexadecimal version of the folder color, with a leading hashtag
* @type {string}
* @readonly
*/
get hexColor() {
return `#${this.color.toString(16).padStart(6, '0')}`;
}
/**
* Guilds in the folder
* @type {Collection<Snowflake, Guild>}
* @readonly
*/
get guilds() {
return this.client.guilds.cache.filter(guild => this.guild_ids.includes(guild.id));
}
toJSON() {
return {
id: this.id,
name: this.name,
color: this.color,
guild_ids: this.guild_ids,
};
}
}
module.exports = GuildFolder;

View File

@@ -1,81 +0,0 @@
'use strict';
const Base = require('./Base');
/**
* @typedef {Object} SessionClientInfo
* @property {string} location Location of the client (using IP address)
* @property {string} platform Platform of the client
* @property {string} os Operating system of the client
*/
/**
* Represents a Client OAuth2 Application Team.
* @extends {Base}
*/
class Session extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
if ('id_hash' in data) {
/**
* The session hash id
* @type {string}
*/
this.id = data.id_hash;
}
if ('approx_last_used_time' in data) {
this.approxLastUsedTime = data.approx_last_used_time;
}
if ('client_info' in data) {
/**
* The client info
* @type {SessionClientInfo}
*/
this.clientInfo = data.client_info;
}
}
/**
* The timestamp the client was last used at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The time the client was last used at.
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.approxLastUsedTime);
}
/**
* Logout the client (remote).
* @param {string | null} mfaCode MFA code (if 2FA is enabled)
* @returns {Promise<undefined>}
*/
logout(mfaCode) {
if (typeof this.client.password !== 'string') throw new Error('REQUIRE_PASSWORD', 'You must provide a password.');
return this.client.api.auth.sessions.logout({
data: {
session_id_hashes: [this.id],
password: this.client.password,
code: typeof mfaCode === 'string' ? mfaCode : undefined,
},
});
}
toJSON() {
return super.toJSON();
}
}
module.exports = Session;

View File

@@ -1,14 +1,13 @@
'use strict';
const { Buffer } = require('buffer');
const crypto = require('crypto');
const { Buffer } = require('node:buffer');
const crypto = require('node:crypto');
const EventEmitter = require('node:events');
const { StringDecoder } = require('node:string_decoder');
const { setTimeout } = require('node:timers');
const { StringDecoder } = require('string_decoder');
const chalk = require('chalk');
const fetch = require('node-fetch');
const { encode: urlsafe_b64encode } = require('safe-base64');
const WebSocket = require('ws');
const { defaultUA } = require('./Constants');
const { UserAgent } = require('./Constants');
const Options = require('./Options');
const defaultClientOptions = Options.createDefault();
@@ -22,9 +21,9 @@ const receiveEvent = {
NONCE_PROOF: 'nonce_proof',
PENDING_REMOTE_INIT: 'pending_remote_init',
HEARTBEAT_ACK: 'heartbeat_ack',
PENDING_LOGIN: 'pending_ticket',
PENDING_TICKET: 'pending_ticket',
CANCEL: 'cancel',
SUCCESS: 'pending_login',
PENDING_LOGIN: 'pending_login',
};
const sendEvent = {
@@ -37,266 +36,167 @@ const Event = {
READY: 'ready',
ERROR: 'error',
CANCEL: 'cancel',
WAIT: 'pending',
SUCCESS: 'success',
WAIT_SCAN: 'pending',
FINISH: 'finish',
CLOSED: 'closed',
DEBUG: 'debug',
};
/**
* @typedef {Object} DiscordAuthWebsocketOptions
* @property {?boolean} [debug=false] Log debug info
* @property {?boolean} [hiddenLog=false] Hide log ?
* @property {?boolean} [autoLogin=false] Automatically login (DiscordJS.Client Login) ?
* @property {?boolean} [failIfError=true] Throw error ?
* @property {?boolean} [generateQR=true] Create QR Code ?
* @property {?number} [apiVersion=9] API Version
* @property {?string} [userAgent] User Agent
* @property {?Object.<string,string>} [wsProperties] Web Socket Properties
*/
/**
* Discord Auth QR (Discord.RemoteAuth will be removed in the future, v13.9.0 release)
* Discord Auth QR
* @extends {EventEmitter}
* @abstract
*/
class DiscordAuthWebsocket extends EventEmitter {
#ws = null;
#heartbeatInterval = null;
#expire = null;
#publicKey = null;
#privateKey = null;
#ticket = null;
#fingerprint = '';
#userDecryptString = '';
/**
* Creates a new DiscordAuthWebsocket instance.
* @param {?DiscordAuthWebsocketOptions} options Options
*/
constructor(options) {
constructor() {
super();
/**
* WebSocket
* @type {?WebSocket}
*/
this.ws = null;
/**
* Heartbeat Interval
* @type {?number}
*/
this.heartbeatInterval = NaN;
this._expire = NaN;
this.key = null;
/**
* User (Scan QR Code)
* @type {?Object}
*/
this.user = null;
/**
* Temporary Token (Scan QR Code)
* @type {?string}
*/
this.token = undefined;
/**
* Real Token (Login)
* @type {?string}
*/
this.realToken = undefined;
/**
* Fingerprint (QR Code)
* @type {?string}
*/
this.fingerprint = null;
/**
* Captcha Handler
* @type {Function}
* @param {Captcha} data hcaptcha data
* @returns {Promise<string>} Captcha token
*/
// eslint-disable-next-line no-unused-vars
this.captchaSolver = data =>
new Promise((resolve, reject) => {
reject(
new Error(`
Captcha Handler not found - Please set captchaSolver option
Example captchaSolver function:
new DiscordAuthWebsocket({
captchaSolver: async (data) => {
const token = await hcaptchaSolver(data.captcha_sitekey, 'discord.com');
return token;
this.token = '';
}
});
`),
);
});
/**
* Captcha Cache
* @type {?Captcha}
*/
this.captchaCache = null;
this._validateOptions(options);
this.callFindRealTokenCount = 0;
}
/**
* Get expire time
* @type {string} Expire time
* @readonly
* @type {string}
*/
get exprireTime() {
return this._expire.toLocaleString('en-US');
get AuthURL() {
return baseURL + this.#fingerprint;
}
_validateOptions(options = {}) {
/**
* Options
* @type {?DiscordAuthWebsocketOptions}
*/
this.options = {
debug: false,
hiddenLog: false,
autoLogin: false,
failIfError: true,
generateQR: true,
apiVersion: 9,
userAgent: defaultUA,
wsProperties: defaultClientOptions.ws.properties,
captchaSolver: () => new Error('Captcha Handler not found. Please set captchaSolver option.'),
};
if (typeof options == 'object') {
if (typeof options.debug == 'boolean') this.options.debug = options.debug;
if (typeof options.hiddenLog == 'boolean') this.options.hiddenLog = options.hiddenLog;
if (typeof options.autoLogin == 'boolean') this.options.autoLogin = options.autoLogin;
if (typeof options.failIfError == 'boolean') this.options.failIfError = options.failIfError;
if (typeof options.generateQR == 'boolean') this.options.generateQR = options.generateQR;
if (typeof options.apiVersion == 'number') this.options.apiVersion = options.apiVersion;
if (typeof options.userAgent == 'string') this.options.userAgent = options.userAgent;
if (typeof options.wsProperties == 'object') this.options.wsProperties = options.wsProperties;
if (typeof options.captchaSolver == 'function') this.captchaSolver = options.captchaSolver;
}
/**
* @type {Date}
*/
get exprire() {
return this.#expire;
}
_createWebSocket(url) {
this.ws = new WebSocket(url, {
/**
* @type {UserRaw}
*/
get user() {
return DiscordAuthWebsocket.decryptUser(this.#userDecryptString);
}
#createWebSocket(url) {
this.#ws = new WebSocket(url, {
headers: {
Origin: 'https://discord.com',
'User-Agent': this.options.userAgent,
'User-Agent': UserAgent,
},
});
this._handleWebSocket();
this.#handleWebSocket();
}
_handleWebSocket() {
this.ws.on('error', error => {
this._logger('error', error);
#handleWebSocket() {
this.#ws.on('error', error => {
/**
* WS Error
* @event DiscordAuthWebsocket#error
* @param {Error} error Error
*/
this.emit(Event.ERROR, error);
});
this.ws.on('open', () => {
this._logger('debug', 'Client Connected');
this.#ws.on('open', () => {
/**
* Debug Event
* @event DiscordAuthWebsocket#debug
* @param {string} msg Debug msg
*/
this.emit(Event.DEBUG, '[WS] Client Connected');
});
this.ws.on('close', () => {
this._logger('debug', 'Connection closed.');
});
this.ws.on('message', message => {
this._handleMessage(JSON.parse(message));
this.#ws.on('close', () => {
this.emit(Event.DEBUG, '[WS] Connection closed');
});
this.#ws.on('message', this.#handleMessage.bind(this));
}
_handleMessage(message) {
#handleMessage(message) {
message = JSON.parse(message);
switch (message.op) {
case receiveEvent.HELLO: {
this._ready(message);
this.#ready(message);
break;
}
case receiveEvent.NONCE_PROOF: {
this._receiveNonceProof(message);
this.#receiveNonceProof(message);
break;
}
case receiveEvent.PENDING_REMOTE_INIT: {
this._pendingRemoteInit(message);
break;
}
case receiveEvent.HEARTBEAT_ACK: {
this._logger('debug', 'Heartbeat acknowledged.');
this._heartbeatAck();
break;
}
case receiveEvent.PENDING_LOGIN: {
this._pendingLogin(message);
break;
}
case receiveEvent.CANCEL: {
this._logger('debug', 'Cancel login.');
this.#fingerprint = message.fingerprint;
/**
* Emitted whenever a user cancels the login process.
* @event DiscordAuthWebsocket#cancel
* @param {object} user User (Raw)
* Ready Event
* @event DiscordAuthWebsocket#ready
* @param {DiscordAuthWebsocket} client WS
*/
this.emit(Event.CANCEL, this.user);
this.emit(Event.READY, this);
break;
}
case receiveEvent.HEARTBEAT_ACK: {
this.emit(Event.DEBUG, `Heartbeat acknowledged.`);
this.#heartbeatAck();
break;
}
case receiveEvent.PENDING_TICKET: {
this.#pendingLogin(message);
break;
}
case receiveEvent.CANCEL: {
/**
* Cancel
* @event DiscordAuthWebsocket#cancel
* @param {DiscordAuthWebsocket} client WS
*/
this.emit(Event.CANCEL, this);
this.destroy();
break;
}
case receiveEvent.SUCCESS: {
this._logger('debug', 'Receive Token - Login Success.', message.ticket);
/**
* Emitted whenever a token is created. (Fake token)
* @event DiscordAuthWebsocket#success
* @param {object} user Discord User
* @param {string} token Discord Token (Fake)
*/
this.emit(Event.SUCCESS, this.user, message.ticket);
this.token = message.ticket;
this._findRealToken();
this._logger('default', 'Get token success.');
break;
}
default: {
this._logger('debug', `Unknown op: ${message.op}`, message);
}
}
}
_logger(type = 'default', ...message) {
if (this.options.hiddenLog) return;
switch (type.toLowerCase()) {
case 'error': {
// eslint-disable-next-line no-unused-expressions
this.options.failIfError
? this._throwError(new Error(message[0]))
: console.error(chalk.red(`[DiscordRemoteAuth] ERROR`), ...message);
break;
}
case 'default': {
console.log(chalk.green(`[DiscordRemoteAuth]`), ...message);
break;
}
case 'debug': {
if (this.options.debug) console.log(chalk.yellow(`[DiscordRemoteAuth] DEBUG`), ...message);
case receiveEvent.PENDING_LOGIN: {
this.#ticket = message.ticket;
this.#findRealToken();
break;
}
}
}
_throwError(error) {
console.log(chalk.red(`[DiscordRemoteAuth] ERROR`), error);
throw error;
}
_send(op, data) {
if (!this.ws) this._throwError(new Error('WebSocket is not connected.'));
#send(op, data) {
if (!this.#ws) return;
let payload = { op: op };
if (data !== null) payload = { ...payload, ...data };
this._logger('debug', `Send Data:`, payload);
this.ws.send(JSON.stringify(payload));
this.#ws.send(JSON.stringify(payload));
}
_heartbeat() {
this._send(sendEvent.HEARTBEAT);
}
_heartbeatAck() {
#heartbeatAck() {
setTimeout(() => {
this._heartbeat();
}, this.heartbeatInterval).unref();
this.#send(sendEvent.HEARTBEAT);
}, this.#heartbeatInterval).unref();
}
_ready(data) {
this._logger('debug', 'Attempting server handshake...');
this._expire = new Date(Date.now() + data.timeout_ms);
this.heartbeatInterval = data.heartbeat_interval;
this._createKey();
this._heartbeatAck();
this._init();
#ready(data) {
this.emit(Event.DEBUG, 'Attempting server handshake...');
this.#expire = new Date(Date.now() + data.timeout_ms);
this.#heartbeatInterval = data.heartbeat_interval;
this.#createKey();
this.#heartbeatAck();
this.#init();
}
_createKey() {
if (this.key) this._throwError(new Error('Key is already created.'));
this.key = crypto.generateKeyPairSync('rsa', {
#createKey() {
const key = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
@@ -307,35 +207,41 @@ new DiscordAuthWebsocket({
format: 'pem',
},
});
this.#privateKey = key.privateKey;
this.#publicKey = key.publicKey;
}
_createPublicKey() {
if (!this.key) this._throwError(new Error('Key is not created.'));
this._logger('debug', 'Generating public key...');
#encodePublicKey() {
const decoder = new StringDecoder('utf-8');
let pub_key = decoder.write(this.key.publicKey);
let pub_key = decoder.write(this.#publicKey);
pub_key = pub_key.split('\n').slice(1, -2).join('');
this._logger('debug', 'Public key generated.', pub_key);
return pub_key;
}
_init() {
const public_key = this._createPublicKey();
this._send(sendEvent.INIT, { encoded_public_key: public_key });
#init() {
const public_key = this.#encodePublicKey();
this.#send(sendEvent.INIT, { encoded_public_key: public_key });
}
_receiveNonceProof(data) {
#receiveNonceProof(data) {
const nonce = data.encrypted_nonce;
const decrypted_nonce = this._decryptPayload(nonce);
let proof = crypto.createHash('sha256').update(decrypted_nonce).digest();
proof = urlsafe_b64encode(proof);
proof = proof.replace(/\s+$/, '');
this._send(sendEvent.NONCE_PROOF, { proof: proof });
this._logger('debug', `Nonce proof decrypted:`, proof);
const decrypted_nonce = this.#decryptPayload(nonce);
const proof = crypto
.createHash('sha256')
.update(decrypted_nonce)
.digest()
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+/, '')
.replace(/\s+$/, '');
this.#send(sendEvent.NONCE_PROOF, { proof: proof });
}
_decryptPayload(encrypted_payload) {
if (!this.key) this._throwError(new Error('Key is not created.'));
#decryptPayload(encrypted_payload) {
const payload = Buffer.from(encrypted_payload, 'base64');
this._logger('debug', `Encrypted Payload (Buffer):`, payload);
const decoder = new StringDecoder('utf-8');
const private_key = decoder.write(this.key.privateKey);
const private_key = decoder.write(this.#privateKey);
const data = crypto.privateDecrypt(
{
key: private_key,
@@ -344,170 +250,129 @@ new DiscordAuthWebsocket({
},
payload,
);
this._logger('debug', `Decrypted Payload:`, data.toString());
return data;
}
_pendingLogin(data) {
const user_data = this._decryptPayload(data.encrypted_user_payload);
const user = new User(user_data.toString());
this.user = user;
#pendingLogin(data) {
const user_data = this.#decryptPayload(data.encrypted_user_payload);
this.#userDecryptString = user_data.toString();
/**
* @typedef {Object} UserRaw
* @property {Snowflake} id
* @property {string} username
* @property {number} discriminator
* @property {string} avatar
*/
/**
* Emitted whenever a user is scan QR Code.
* @event DiscordAuthWebsocket#pending
* @param {object} user Discord User Raw
* @param {UserRaw} user Discord User Raw
*/
this.emit(Event.WAIT, user);
this._logger('debug', 'Waiting for user to finish login...');
this.user.prettyPrint(this);
this._logger('default', 'Please check your phone again to confirm login.');
this.emit(Event.WAIT_SCAN, this.user);
}
_pendingRemoteInit(data) {
this._logger('debug', `Pending Remote Init:`, data);
/**
* Emitted whenever a url is created.
* @event DiscordAuthWebsocket#ready
* @param {string} fingerprint Fingerprint
* @param {string} url DiscordAuthWebsocket
*/
this.emit(Event.READY, data.fingerprint, `${baseURL}${data.fingerprint}`);
this.fingerprint = data.fingerprint;
if (this.options.generateQR) this.generateQR();
}
_awaitLogin(client) {
this.once(Event.FINISH, (user, token) => {
this._logger('debug', 'Create login state...', user, token);
client.login(token);
#awaitLogin(client) {
return new Promise(r => {
this.once(Event.FINISH, token => {
r(client.login(token));
});
});
}
/**
* Connect to DiscordAuthWebsocket.
* @param {?Client} client Using only for auto login.
* @returns {undefined}
* Connect WS
* @param {Client} [client] DiscordJS Client
* @returns {Promise<void>}
*/
connect(client) {
this._createWebSocket(wsURL);
if (client && this.options.autoLogin) this._awaitLogin(client);
this.#createWebSocket(wsURL);
if (client) {
return this.#awaitLogin(client);
} else {
return Promise.resolve();
}
}
/**
* Disconnect from DiscordAuthWebsocket.
* @returns {undefined}
* Destroy client
* @returns {void}
*/
destroy() {
if (!this.ws) this._throwError(new Error('WebSocket is not connected.'));
if (!this.ws) return;
this.ws.close();
this.emit(Event.DEBUG, 'WebSocket closed.');
/**
* Emitted whenever a connection is closed.
* @event DiscordAuthWebsocket#closed
* @param {boolean} loginState Login state
*/
this.emit(Event.CLOSED, this.token);
this._logger('debug', 'WebSocket closed.');
this.emit(Event.CLOSED);
}
/**
* Generate QR code for user to scan (Terminal)
* @returns {undefined}
* @returns {void}
*/
generateQR() {
if (!this.fingerprint) this._throwError(new Error('Fingerprint is not created.'));
require('@aikochan2k6/qrcode-terminal').generate(`${baseURL}${this.fingerprint}`, {
if (!this.#fingerprint) return;
require('@aikochan2k6/qrcode-terminal').generate(this.AuthURL, {
small: true,
});
this._logger('default', `Please scan the QR code to continue.\nQR Code will expire in ${this.exprireTime}`);
}
async _findRealToken(captchaSolveData) {
this.callFindRealTokenCount++;
if (!this.token) return this._throwError(new Error('Token is not created.'));
if (!captchaSolveData && this.captchaCache) return this._throwError(new Error('Captcha is not solved.'));
if (this.callFindRealTokenCount > 5) {
return this._throwError(
new Error(
`Failed to find real token (${this.callFindRealTokenCount} times) ${this.captchaCache ? '[Captcha]' : ''}`,
),
);
}
this._logger('debug', 'Find real token...');
const res = await (
await fetch(`https://discord.com/api/v${this.options.apiVersion}/users/@me/remote-auth/login`, {
method: 'POST',
headers: {
Accept: '*/*',
'Accept-Language': 'en-US',
'Content-Type': 'application/json',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'X-Debug-Options': 'bugReporterEnabled',
'X-Super-Properties': `${Buffer.from(JSON.stringify(this.options.wsProperties), 'ascii').toString('base64')}`,
'X-Discord-Locale': 'en-US',
'User-Agent': this.options.userAgent,
Referer: 'https://discord.com/channels/@me',
Connection: 'keep-alive',
Origin: 'https://discord.com',
},
body: JSON.stringify(
captchaSolveData
? {
ticket: this.token,
captcha_rqtoken: this.captchaCache.captcha_rqtoken,
captcha_key: captchaSolveData,
}
: {
ticket: this.token,
},
),
#findRealToken() {
return fetch(`https://discord.com/api/v9/users/@me/remote-auth/login`, {
method: 'POST',
headers: {
Accept: '*/*',
'Accept-Language': 'en-US',
'Content-Type': 'application/json',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'X-Debug-Options': 'bugReporterEnabled',
'X-Super-Properties': `${Buffer.from(JSON.stringify(defaultClientOptions.ws.properties), 'ascii').toString(
'base64',
)}`,
'X-Discord-Locale': 'en-US',
'User-Agent': UserAgent,
Referer: 'https://discord.com/channels/@me',
Connection: 'keep-alive',
Origin: 'https://discord.com',
},
body: JSON.stringify({
ticket: this.#ticket,
}),
})
.then(r => r.json())
.then(res => {
if (res.encrypted_token) {
this.token = this.#decryptPayload(res.encrypted_token).toString();
}
/**
* Emitted whenever a real token is found.
* @event DiscordAuthWebsocket#finish
* @param {string} token Discord Token
*/
this.emit(Event.FINISH, this.token);
this.destroy();
})
).json();
if (res?.captcha_key) {
this.captchaCache = res;
} else if (!res.encrypted_token) {
this._throwError(new Error('Request failed. Please try again.', res));
this.captchaCache = null;
}
if (!res && this.captchaCache) {
this._logger('default', 'Captcha is detected. Please solve the captcha to continue.');
this._logger('debug', 'Try call captchaSolver()', this.captchaCache);
const token = await this.options.captchaSolver(this.captchaCache);
return this._findRealToken(token);
}
this.realToken = this._decryptPayload(res.encrypted_token).toString();
/**
* Emitted whenever a real token is found.
* @event DiscordAuthWebsocket#finish
* @param {object} user User
* @param {string} token Real token
*/
this.emit(Event.FINISH, this.user, this.realToken);
return this;
.catch(() => false);
}
}
class User {
constructor(payload) {
static decryptUser(payload) {
const values = payload.split(':');
this.id = values[0];
this.username = values[3];
this.discriminator = values[1];
this.avatar = values[2];
return this;
}
get avatarURL() {
return `https://cdn.discordapp.com/avatars/${this.id}/${this.avatar}.${
this.avatar.startsWith('a_') ? 'gif' : 'png'
}`;
}
get tag() {
return `${this.username}#${this.discriminator}`;
}
prettyPrint(RemoteAuth) {
let string = `\n`;
string += ` ${chalk.bgBlue('User:')} `;
string += `${this.tag} (${this.id})\n`;
string += ` ${chalk.bgGreen('Avatar URL:')} `;
string += chalk.cyan(`${this.avatarURL}\n`);
string += ` ${chalk.bgMagenta('Token:')} `;
string += chalk.red(`${this.token ? this.token : 'Unknown'}`);
RemoteAuth._logger('default', string);
const id = values[0];
const username = values[3];
const discriminator = values[1];
const avatar = values[2];
return {
id,
username,
discriminator,
avatar,
};
}
}

View File

@@ -5,12 +5,13 @@ const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const fetch = require('node-fetch');
const { Colors } = require('./Constants');
const { RangeError, TypeError, Error: DJSError } = require('../errors');
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
const isObject = d => typeof d === 'object' && d !== null;
let deprecationEmittedForSplitMessage = false;
let deprecationEmittedForRemoveMentions = false;
let deprecationEmittedForResolveAutoArchiveMaxLimit = false;
const TextSortableGroupTypes = ['GUILD_TEXT', 'GUILD_ANNOUCMENT', 'GUILD_FORUM'];
const VoiceSortableGroupTypes = ['GUILD_VOICE', 'GUILD_STAGE_VOICE'];
@@ -138,6 +139,7 @@ class Util extends null {
* @property {boolean} [numberedList=false] Whether to escape numbered lists
* @property {boolean} [maskedLink=false] Whether to escape masked links
*/
/**
* Escapes any Discord-flavour markdown in a string.
* @param {string} text Content to escape
@@ -220,6 +222,7 @@ class Util extends null {
if (maskedLink) text = Util.escapeMaskedLink(text);
return text;
}
/**
* Escapes code block markdown in a string.
* @param {string} text Content to escape
@@ -228,6 +231,7 @@ class Util extends null {
static escapeCodeBlock(text) {
return text.replaceAll('```', '\\`\\`\\`');
}
/**
* Escapes inline code markdown in a string.
* @param {string} text Content to escape
@@ -236,6 +240,7 @@ class Util extends null {
static escapeInlineCode(text) {
return text.replace(/(?<=^|[^`])``?(?=[^`]|$)/g, match => (match.length === 2 ? '\\`\\`' : '\\`'));
}
/**
* Escapes italic markdown in a string.
* @param {string} text Content to escape
@@ -253,6 +258,7 @@ class Util extends null {
return `\\_${match}`;
});
}
/**
* Escapes bold markdown in a string.
* @param {string} text Content to escape
@@ -265,6 +271,7 @@ class Util extends null {
return '\\*\\*';
});
}
/**
* Escapes underline markdown in a string.
* @param {string} text Content to escape
@@ -277,6 +284,7 @@ class Util extends null {
return '\\_\\_';
});
}
/**
* Escapes strikethrough markdown in a string.
* @param {string} text Content to escape
@@ -285,6 +293,7 @@ class Util extends null {
static escapeStrikethrough(text) {
return text.replaceAll('~~', '\\~\\~');
}
/**
* Escapes spoiler markdown in a string.
* @param {string} text Content to escape
@@ -293,6 +302,7 @@ class Util extends null {
static escapeSpoiler(text) {
return text.replaceAll('||', '\\|\\|');
}
/**
* Escapes escape characters in a string.
* @param {string} text Content to escape
@@ -301,6 +311,7 @@ class Util extends null {
static escapeEscape(text) {
return text.replaceAll('\\', '\\\\');
}
/**
* Escapes heading characters in a string.
* @param {string} text Content to escape
@@ -309,6 +320,7 @@ class Util extends null {
static escapeHeading(text) {
return text.replaceAll(/^( {0,2}[*-] +)?(#{1,3} )/gm, '$1\\$2');
}
/**
* Escapes bulleted list characters in a string.
* @param {string} text Content to escape
@@ -317,6 +329,7 @@ class Util extends null {
static escapeBulletedList(text) {
return text.replaceAll(/^( *)[*-]( +)/gm, '$1\\-$2');
}
/**
* Escapes numbered list characters in a string.
* @param {string} text Content to escape
@@ -325,6 +338,7 @@ class Util extends null {
static escapeNumberedList(text) {
return text.replaceAll(/^( *\d+)\./gm, '$1\\.');
}
/**
* Escapes masked link characters in a string.
* @param {string} text Content to escape
@@ -334,6 +348,16 @@ class Util extends null {
return text.replaceAll(/\[.+\]\(.+\)/gm, '\\$&');
}
/**
* @typedef {Object} FetchRecommendedShardsOptions
* @property {number} [guildsPerShard=1000] Number of guilds assigned per shard
* @property {number} [multipleOf=1] The multiple the shard count should round up to. (16 for large bot sharding)
*/
static fetchRecommendedShards() {
throw new DiscordError('INVALID_USER_API');
}
/**
* Parses emoji info out of a string. The string must be one of:
* * A UTF-8 emoji (no id)
@@ -623,26 +647,22 @@ class Util extends null {
/**
* Resolves the maximum time a guild's thread channels should automatically archive in case of no recent activity.
* @deprecated
* @param {Guild} guild The guild to resolve this limit from.
* @deprecated This will be removed in the next major version.
* @returns {number}
*/
static resolveAutoArchiveMaxLimit() {
if (!deprecationEmittedForResolveAutoArchiveMaxLimit) {
process.emitWarning(
// eslint-disable-next-line max-len
"The Util.resolveAutoArchiveMaxLimit method and the 'MAX' option are deprecated and will be removed in the next major version.",
'DeprecationWarning',
);
deprecationEmittedForResolveAutoArchiveMaxLimit = true;
}
return 10080;
}
/**
* Lazily evaluates a callback function (yea it's v14 :yay:)
* @param {Function} cb The callback to lazily evaluate
* @returns {Function}
* @example
* const User = lazy(() => require('./User'));
* const user = new (User())(client, data);
*/
static lazy(cb) {
let defaultValue;
return () => (defaultValue ??= cb());
}
/**
* Transforms an API guild forum tag to camel-cased guild forum tag.
* @param {APIGuildForumTag} tag The tag to transform
@@ -708,95 +728,6 @@ class Util extends null {
};
}
static async getAttachments(client, channelId, ...files) {
files = files.flat(2);
if (!files.length) return [];
files = files.map((file, i) => ({
filename: file.name ?? file.attachment?.name ?? file.attachment?.filename ?? 'file.jpg',
// 25MB = 26_214_400bytes
file_size: Math.floor((26_214_400 / 10) * Math.random()),
id: `${i}`,
}));
const { attachments } = await client.api.channels[channelId].attachments.post({
data: {
files,
},
});
return attachments;
}
static uploadFile(data, url) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'PUT',
body: data,
})
.then(res => {
if (res.ok) {
resolve(res);
} else {
reject(res);
}
})
.catch(reject);
});
}
static testImportModule(name) {
try {
require.resolve(name);
return true;
} catch {
return false;
}
}
static getProxyObject(proxy) {
const protocol = new URL(proxy).protocol.slice(0, -1);
const mapObject = {
http: 'https', // Cuz we can't use http for discord
https: 'https',
socks4: 'socks',
socks5: 'socks',
'pac+http': 'pac',
'pac+https': 'pac',
};
const proxyType = mapObject[protocol];
switch (proxyType) {
case 'https': {
if (!Util.testImportModule('https-proxy-agent')) {
throw new DJSError('MISSING_MODULE', 'https-proxy-agent', 'npm install https-proxy-agent');
}
const httpsProxyAgent = require('https-proxy-agent');
return new httpsProxyAgent.HttpsProxyAgent(proxy);
}
case 'socks': {
if (!Util.testImportModule('socks-proxy-agent')) {
throw new DJSError('MISSING_MODULE', 'socks-proxy-agent', 'npm install socks-proxy-agent');
}
const socksProxyAgent = require('socks-proxy-agent');
return new socksProxyAgent.SocksProxyAgent(proxy);
}
case 'pac': {
if (!Util.testImportModule('pac-proxy-agent')) {
throw new DJSError('MISSING_MODULE', 'pac-proxy-agent', 'npm install pac-proxy-agent');
}
const pacProxyAgent = require('pac-proxy-agent');
return new pacProxyAgent.PacProxyAgent(proxy);
}
default: {
if (!Util.testImportModule('proxy-agent')) {
throw new DJSError('MISSING_MODULE', 'proxy-agent', 'npm install proxy-agent@5');
}
const proxyAgent = require('proxy-agent');
return new proxyAgent(proxy);
}
}
}
/**
* Gets an array of the channel types that can be moved in the channel group. For example, a GuildText channel would
* return an array containing the types that can be ordered within the text channels (always at the top), and a voice
@@ -831,94 +762,38 @@ class Util extends null {
return Number(BigInt(userId) >> 22n) % 6;
}
static clientRequiredAction(client, code) {
let msg = '';
let stopClient = false;
switch (code) {
case null: {
msg = 'All required actions have been completed.';
break;
}
case 'AGREEMENTS': {
msg = 'You need to accept the new Terms of Service and Privacy Policy.';
// https://discord.com/api/v9/users/@me/agreements
client.api
.users('@me')
.agreements.patch({
data: {
terms: true,
privacy: true,
},
})
.then(() => {
client.emit(
'debug',
'[USER_REQUIRED_ACTION] Successfully accepted the new Terms of Service and Privacy Policy.',
);
})
.catch(e => {
client.emit(
'debug',
`[USER_REQUIRED_ACTION] Failed to accept the new Terms of Service and Privacy Policy: ${e}`,
);
});
break;
}
case 'REQUIRE_CAPTCHA': {
msg = 'You need to complete a captcha.';
stopClient = true;
break;
}
case 'REQUIRE_VERIFIED_EMAIL': {
msg = 'You need to verify your email.';
stopClient = true;
break;
}
case 'REQUIRE_REVERIFIED_EMAIL': {
msg = 'You need to reverify your email.';
stopClient = true;
break;
}
case 'REQUIRE_VERIFIED_PHONE': {
msg = 'You need to verify your phone number.';
stopClient = true;
break;
}
case 'REQUIRE_REVERIFIED_PHONE': {
msg = 'You need to reverify your phone number.';
stopClient = true;
break;
}
case 'REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE': {
msg = 'You need to verify your email or verify your phone number.';
stopClient = true; // Maybe not
break;
}
case 'REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE': {
msg = 'You need to reverify your email or verify your phone number.';
stopClient = true;
break;
}
case 'REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE': {
msg = 'You need to verify your email or reverify your phone number.';
stopClient = true;
break;
}
case 'REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE': {
msg = 'You need to reverify your email or reverify your phone number.';
stopClient = true;
break;
}
default: {
msg = `Unknown required action: ${code}`;
break;
}
}
if (stopClient) {
client.emit('error', new Error(`[USER_REQUIRED_ACTION] ${msg}`));
} else {
client.emit('debug', `[USER_REQUIRED_ACTION] ${msg}`);
}
static async getUploadURL(client, channelId, files) {
if (!files.length) return [];
files = files.map((file, i) => ({
filename: file.name,
// 25MB = 26_214_400bytes
file_size: Math.floor((26_214_400 / 10) * Math.random()),
id: `${i}`,
}));
const { attachments } = await client.api.channels[channelId].attachments.post({
data: {
files,
},
});
return attachments;
}
static uploadFile(data, url) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'PUT',
body: data,
duplex: 'half', // Node.js v20
})
.then(res => {
if (res.ok) {
resolve(res);
} else {
reject(res);
}
})
.catch(reject);
});
}
}