diff --git a/src/client/Client.js b/src/client/Client.js index feac80fd8..51a4e9eeb 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -486,7 +486,7 @@ class Client extends EventEmitter { this.presences.get(id).update(presence); return; } - this.presences.set(id, new Presence(presence)); + this.presences.set(id, new Presence(presence, this)); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 85ddbfc95..bfff04fb8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1208,7 +1208,7 @@ class Guild { this.presences.get(id).update(presence); return; } - this.presences.set(id, new Presence(presence)); + this.presences.set(id, new Presence(presence, this.client)); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index b551cede8..5c3785e85 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -123,7 +123,7 @@ class GuildMember { * @readonly */ get presence() { - return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(); + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(undefined, this.client); } /** diff --git a/src/structures/Presence.js b/src/structures/Presence.js index a7c6600f9..38403862b 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,8 +1,12 @@ +const { ActivityFlags, Endpoints } = require('../util/Constants'); + /** * Represents a user's presence. */ class Presence { - constructor(data = {}) { + constructor(data = {}, client) { + Object.defineProperty(this, 'client', { value: client }); + /** * The status of the presence: * @@ -18,12 +22,12 @@ class Presence { * The game that the user is playing * @type {?Game} */ - this.game = data.game ? new Game(data.game) : null; + this.game = data.game ? new Game(data.game, this) : null; } update(data) { this.status = data.status || this.status; - this.game = data.game ? new Game(data.game) : null; + this.game = data.game ? new Game(data.game, this) : null; } /** @@ -44,7 +48,9 @@ class Presence { * Represents a game that is part of a user's presence. */ class Game { - constructor(data) { + constructor(data, presence) { + Object.defineProperty(this, 'presence', { value: presence }); + /** * The name of the game being played * @type {string} @@ -62,6 +68,60 @@ class Game { * @type {?string} */ this.url = data.url || null; + + /** + * Details about the activity + * @type {?string} + */ + this.details = data.details || null; + + /** + * State of the activity + * @type {?string} + */ + this.state = data.state || null; + + /** + * Application ID associated with this activity + * @type {?Snowflake} + */ + this.applicationID = data.application_id || null; + + /** + * Timestamps for the activity + * @type {?Object} + * @prop {?Date} start When the activity started + * @prop {?Date} end When the activity will end + */ + this.timestamps = data.timestamps ? { + start: data.timestamps.start ? new Date(Number(data.timestamps.start)) : null, + end: data.timestamps.end ? new Date(Number(data.timestamps.end)) : null, + } : null; + + /** + * Party of the activity + * @type {?Object} + * @prop {?string} id ID of the party + * @prop {number[]} size Size of the party as `[current, max]` + */ + this.party = data.party || null; + + /** + * Assets for rich presence + * @type {?RichPresenceAssets} + */ + this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; + + this.syncID = data.sync_id; + this._flags = data.flags; + } + + get flags() { + const flags = []; + for (const [name, flag] of Object.entries(ActivityFlags)) { + if ((this._flags & flag) === flag) flags.push(name); + } + return flags; } /** @@ -96,5 +156,64 @@ class Game { } } +/** + * Assets for a rich presence + */ +class RichPresenceAssets { + constructor(game, assets) { + Object.defineProperty(this, 'game', { value: game }); + + /** + * Hover text for the large image + * @type {?string} + */ + this.largeText = assets.large_text || null; + + /** + * Hover text for the small image + * @type {?string} + */ + this.smallText = assets.small_text || null; + + /** + * ID of the large image asset + * @type {?Snowflake} + */ + this.largeImage = assets.large_image || null; + + /** + * ID of the small image asset + * @type {?Snowflake} + */ + this.smallImage = assets.small_image || null; + } + + /** + * The URL of the small image asset + * @type {?string} + * @readonly + */ + get smallImageURL() { + if (!this.smallImage) return null; + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.smallImage); + } + + /** + * The URL of the large image asset + * @type {?string} + * @readonly + */ + get largeImageURL() { + if (!this.largeImage) return null; + if (/^spotify:/.test(this.largeImage)) { + return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.largeImage); + } +} + exports.Presence = Presence; exports.Game = Game; +exports.RichPresenceAssets = RichPresenceAssets; diff --git a/src/structures/User.js b/src/structures/User.js index 027eedbb6..23ef38f0c 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -99,7 +99,7 @@ class User { for (const guild of this.client.guilds.values()) { if (guild.presences.has(this.id)) return guild.presences.get(this.id); } - return new Presence(); + return new Presence(undefined, this.client); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 25531a6d0..48001df86 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -206,6 +206,8 @@ const Endpoints = exports.Endpoints = { Asset: name => `${root}/assets/${name}`, Avatar: (userID, hash) => `${root}/avatars/${userID}/${hash}.${hash.startsWith('a_') ? 'gif' : 'png?size=2048'}`, Icon: (guildID, hash) => `${root}/icons/${guildID}/${hash}.jpg`, + AppIcon: (clientID, hash) => `${root}/app-icons/${clientID}/${hash}.png`, + AppAsset: (clientID, hash) => `${root}/app-assets/${clientID}/${hash}.png`, GDMIcon: (channelID, hash) => `${root}/channel-icons/${channelID}/${hash}.jpg?size=2048`, Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, }; @@ -366,6 +368,15 @@ exports.ActivityTypes = [ 'WATCHING', ]; +exports.ActivityFlags = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, +}; + /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: * * READY