feat: auto MFA handling

Co-Authored-By: Yellowy <64450187+TheDevYellowy@users.noreply.github.com>
This commit is contained in:
Elysia
2025-01-20 23:44:36 +07:00
parent 8ba1834e55
commit 15f707b38a
6 changed files with 24 additions and 23 deletions

View File

@@ -15,13 +15,9 @@ const client = new Discord.Client({
}) })
.then(res => res.data); .then(res => res.data);
}, },
TOTPKey: '<string>',
}); });
async function getMFACode() {
// This is just an example, you should implement your own MFA code retrieval
return '123456';
}
client.on('ready', async () => { client.on('ready', async () => {
console.log('Ready!', client.user.tag); console.log('Ready!', client.user.tag);
// Note // Note
@@ -32,7 +28,6 @@ client.on('ready', async () => {
{ {
guild_id: 'guild id', guild_id: 'guild id',
}, },
await getMFACode(), // Optional
); );
}); });

View File

@@ -710,7 +710,6 @@ class Client extends BaseClient {
* Authorize an application. * Authorize an application.
* @param {string} urlOAuth2 Discord Auth URL * @param {string} urlOAuth2 Discord Auth URL
* @param {OAuth2AuthorizeOptions} [options] Oauth2 options * @param {OAuth2AuthorizeOptions} [options] Oauth2 options
* @param {string|number} [mfaCode = null] The mfa code if you have it enabled
* @returns {Promise<{ location: string }>} * @returns {Promise<{ location: string }>}
* @example * @example
* client.authorizeURL(`https://discord.com/api/oauth2/authorize?client_id=botID&permissions=8&scope=applications.commands%20bot`, { * client.authorizeURL(`https://discord.com/api/oauth2/authorize?client_id=botID&permissions=8&scope=applications.commands%20bot`, {
@@ -719,7 +718,7 @@ class Client extends BaseClient {
authorize: true authorize: true
}) })
*/ */
authorizeURL(urlOAuth2, options = {}, mfaCode = null) { authorizeURL(urlOAuth2, options = {}) {
// ! throw new Error('METHOD_WARNING'); // ! throw new Error('METHOD_WARNING');
const url = new URL(urlOAuth2); const url = new URL(urlOAuth2);
if (!/https:\/\/(canary\.|ptb\.)?discord.com\/api(\/v\d{1,2})?\/oauth2\/authorize\?/.test(urlOAuth2)) { if (!/https:\/\/(canary\.|ptb\.)?discord.com\/api(\/v\d{1,2})?\/oauth2\/authorize\?/.test(urlOAuth2)) {
@@ -745,7 +744,6 @@ class Client extends BaseClient {
return this.api.oauth2.authorize.post({ return this.api.oauth2.authorize.post({
query: searchParams, query: searchParams,
data: options, data: options,
mfaCode,
}); });
} }

View File

@@ -9,6 +9,7 @@ const RateLimitError = require('./RateLimitError');
const { const {
Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST }, Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
} = require('../util/Constants'); } = require('../util/Constants');
const TOTP = require('../util/Totp');
const captchaMessage = [ const captchaMessage = [
'incorrect-captcha', 'incorrect-captcha',
@@ -385,22 +386,31 @@ class RequestHandler {
request.retries++; request.retries++;
return this.execute(request, captcha, data.captcha_rqtoken); return this.execute(request, captcha, data.captcha_rqtoken);
} }
// Two factor // Two factor handling
if (data?.code && data.code == 60003 && request.options.mfaCode && request.retries < 1) { if (
data?.code &&
data.code == 60003 && // Two factor is required for this operation
data.mfa.methods.find(o => o.type === 'totp') && // TOTP is available
typeof this.manager.client.options.TOTPKey === 'string' &&
request.options.auth !== false &&
request.retries < 1
) {
// Get mfa code
const { otp } = await TOTP.generate(this.options.TOTPKey);
this.manager.client.emit( this.manager.client.emit(
DEBUG, DEBUG,
`${data.message} `${data.message}
Method : ${request.method} Method : ${request.method}
Path : ${request.path} Path : ${request.path}
Route : ${request.route} Route : ${request.route}
mfaCode : ${request.options.mfaCode}`, mfaCode : ${otp}`,
); );
// Get ticket // Get ticket
const mfaData = data.mfa; const mfaData = data.mfa;
const mfaPost = await this.manager.client.api.mfa.finish.post({ const mfaPost = await this.manager.client.api.mfa.finish.post({
data: { data: {
ticket: mfaData.ticket, ticket: mfaData.ticket,
data: request.options.mfaCode, data: otp,
mfa_type: 'totp', mfa_type: 'totp',
}, },
}); });

View File

@@ -1528,9 +1528,6 @@ class Guild extends AnonymousGuild {
* Set the vanity URL to this guild. * Set the vanity URL to this guild.
* Resolves with an object containing the vanity URL invite code and the use count. * Resolves with an object containing the vanity URL invite code and the use count.
* @param {string} [code=''] Vanity URL code * @param {string} [code=''] Vanity URL code
* @param {string|number} [mfaCode = null] Two-factor authentication code
* After you enter the mfaCode, you will receive a `__Secure-recent_mfa` cookie, which is valid for about
* 5 minutes, during which you won't need to enter MFA again.
* @returns {Promise<Vanity>} * @returns {Promise<Vanity>}
* @example * @example
* // Set invite code * // Set invite code
@@ -1540,11 +1537,10 @@ class Guild extends AnonymousGuild {
* }) * })
* .catch(console.error); * .catch(console.error);
*/ */
async setVanityCode(code = '', mfaCode = null) { async setVanityCode(code = '') {
if (typeof code !== 'string') throw new TypeError('INVALID_VANITY_URL_CODE'); if (typeof code !== 'string') throw new TypeError('INVALID_VANITY_URL_CODE');
const data = await this.client.api.guilds(this.id, 'vanity-url').patch({ const data = await this.client.api.guilds(this.id, 'vanity-url').patch({
data: { code }, data: { code },
mfaCode,
}); });
this.vanityURLCode = data.code; this.vanityURLCode = data.code;
this.vanityURLUses = data.uses; this.vanityURLUses = data.uses;

View File

@@ -52,6 +52,7 @@ const Intents = require('./Intents');
* @property {number} [DMChannelVoiceStatusSync=0] The amount of time in milliseconds that the Client to register the event with each DM channel (0=Disable) * @property {number} [DMChannelVoiceStatusSync=0] The amount of time in milliseconds that the Client to register the event with each DM channel (0=Disable)
* @property {number} [captchaRetryLimit=3] Captcha retry limit * @property {number} [captchaRetryLimit=3] Captcha retry limit
* @property {CaptchaSolver} [captchaSolver] Captcha Solver * @property {CaptchaSolver} [captchaSolver] Captcha Solver
* @property {string} [TOTPKey] TOTP key for two-factor authentication
* @property {number} [closeTimeout=5000] The amount of time in milliseconds to wait for the close frame to be received * @property {number} [closeTimeout=5000] The amount of time in milliseconds to wait for the close frame to be received
* from the WebSocket. * from the WebSocket.
* <info>Don't have this too high/low. It's best to have it between 2000-6000 ms.</info> * <info>Don't have this too high/low. It's best to have it between 2000-6000 ms.</info>
@@ -160,6 +161,7 @@ class Options extends null {
DMChannelVoiceStatusSync: 0, DMChannelVoiceStatusSync: 0,
captchaRetryLimit: 3, captchaRetryLimit: 3,
captchaSolver: () => Promise.reject(new Error('CAPTCHA_SOLVER_NOT_IMPLEMENTED')), captchaSolver: () => Promise.reject(new Error('CAPTCHA_SOLVER_NOT_IMPLEMENTED')),
TOTPKey: null,
closeTimeout: 5_000, closeTimeout: 5_000,
waitGuildTimeout: 15_000, waitGuildTimeout: 15_000,
shardCount: 1, shardCount: 1,

8
typings/index.d.ts vendored
View File

@@ -794,7 +794,7 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
options?: AcceptInviteOptions, options?: AcceptInviteOptions,
): Promise<Guild | DMChannel | GroupDMChannel>; ): Promise<Guild | DMChannel | GroupDMChannel>;
public redeemNitro(nitro: string, channel?: TextChannelResolvable, paymentSourceId?: Snowflake): Promise<any>; public redeemNitro(nitro: string, channel?: TextChannelResolvable, paymentSourceId?: Snowflake): Promise<any>;
public authorizeURL(urlOAuth2: string, options?: OAuth2AuthorizeOptions, mfaCode?: string | number): Promise<any>; public authorizeURL(urlOAuth2: string, options?: OAuth2AuthorizeOptions): Promise<any>;
public installUserApps(applicationId: Snowflake): Promise<void>; public installUserApps(applicationId: Snowflake): Promise<void>;
public deauthorize(applicationId: Snowflake): Promise<void>; public deauthorize(applicationId: Snowflake): Promise<void>;
@@ -1553,7 +1553,7 @@ export class Guild extends AnonymousGuild {
reason?: string, reason?: string,
): Promise<this>; ): Promise<this>;
public topEmojis(): Promise<Collection<number, GuildEmoji>>; public topEmojis(): Promise<Collection<number, GuildEmoji>>;
public setVanityCode(code?: string, mfaCode?: string | number): Promise<this>; public setVanityCode(code?: string): Promise<this>;
} }
export class GuildAuditLogs<T extends GuildAuditLogsResolvable = 'ALL'> { export class GuildAuditLogs<T extends GuildAuditLogsResolvable = 'ALL'> {
@@ -3440,8 +3440,8 @@ export class TOTP {
public static generate(key: string, options?: generateOptions): Promise<{ otp: string; expires: number }>; public static generate(key: string, options?: generateOptions): Promise<{ otp: string; expires: number }>;
} }
export type TOTPAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"; export type TOTPAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
export type TOTPEncoding = "hex" | "ascii"; export type TOTPEncoding = 'hex' | 'ascii';
export interface generateOptions { export interface generateOptions {
digits: number; digits: number;