diff --git a/src/client/Client.js b/src/client/Client.js index 371b3dc..c94a221 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -15,6 +15,7 @@ const ClientSettingManager = require('../managers/ClientSettingManager'); const DeveloperPortalManager = require('../managers/DeveloperPortalManager'); const GuildManager = require('../managers/GuildManager'); const RelationshipsManager = require('../managers/RelationshipsManager'); +const SessionManager = require('../managers/SessionManager'); const UserManager = require('../managers/UserManager'); const VoiceStateManager = require('../managers/VoiceStateManager'); const ShardClientUtil = require('../sharding/ShardClientUtil'); @@ -156,6 +157,12 @@ class Client extends BaseClient { */ this.guilds = new GuildManager(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 diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 0bb80ce..2a603ec 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -104,6 +104,8 @@ const Messages = { 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.', + MISSING_VALUE: (where, type) => `Missing value for ${where} (${type})`, INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, diff --git a/src/index.js b/src/index.js index d66b7c0..4e9685a 100644 --- a/src/index.js +++ b/src/index.js @@ -60,6 +60,7 @@ 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'); @@ -137,6 +138,7 @@ 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; diff --git a/src/managers/SessionManager.js b/src/managers/SessionManager.js new file mode 100644 index 00000000..ee95717 --- /dev/null +++ b/src/managers/SessionManager.js @@ -0,0 +1,52 @@ +'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); + } + /** + * Fetch all sessions of the client. + * @returns {Promise} + */ + fetch() { + return new Promise((resolve, reject) => { + this.client.api.auth.session + .get() + .then(data => { + const allData = data.user_sessions; + for (const session of allData) { + this._add(new Session(this.client, session), true); + } + resolve(this); + }) + .catch(reject); + }); + } + + /** + * Logout the client (remote). + * @param {string} password User's password + * @param {string | null} mfaCode MFA code (if 2FA is enabled) + * @returns {Promise} + */ + logoutAllDevices(password, mfaCode) { + password = password || this.client.password; + if (!password || typeof 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, + code: typeof mfaCode === 'string' ? mfaCode : undefined, + }, + }); + } +} + +module.exports = SessionManager; diff --git a/src/structures/Session.js b/src/structures/Session.js new file mode 100644 index 00000000..3b6fb92 --- /dev/null +++ b/src/structures/Session.js @@ -0,0 +1,83 @@ +'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} password User's password + * @param {string | null} mfaCode MFA code (if 2FA is enabled) + * @returns {Promise} + */ + logout(password, mfaCode) { + password = password || this.client.password; + if (!password || typeof 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, + code: typeof mfaCode === 'string' ? mfaCode : undefined, + }, + }); + } + + toJSON() { + return super.toJSON(); + } +} + +module.exports = Session; diff --git a/typings/index.d.ts b/typings/index.d.ts index dbc1964..79408d9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -163,6 +163,28 @@ import { } from './rawDataTypes'; // @ts-ignore //#region Classes + +export abstract class SessionManager extends CachedManager { + constructor(client: Client, iterable?: Iterable); + public fetch(): Promise; + public logoutAllDevices(password?: string, mfaCode?: string): Promise; +} + +export abstract class Session extends Base { + constructor(client: Client); + public id?: string; + public clientInfo?: SessionClientInfo; + public readonly createdTimestamp: number; + public readonly createdAt: Date; + public logout(password?: string, mfaCode?: string): Promise; +} + +export interface SessionClientInfo { + location?: string; + platform?: string; + os?: string; +} + export abstract class DiscordAuthWebsocket extends EventEmitter { constructor(options?: DiscordAuthWebsocketOptions); public fingerprint?: string;