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,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);
});
}
}