Update
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
259
src/util/Util.js
259
src/util/Util.js
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user