'use strict'; const { default: Collection } = require('@discordjs/collection'); const Base = require('./Base'); const ClientApplication = require('./ClientApplication'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Error } = require('../errors'); const { RelationshipTypes } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); const UserFlags = require('../util/UserFlags'); /** * Represents a user on Discord. * @implements {TextBasedChannel} * @extends {Base} */ class User extends Base { constructor(client, data) { super(client); /** * The user's id * @type {Snowflake} */ this.id = data.id; this.bot = null; this.system = null; this.flags = null; /** * An array of object (connected accounts), containing the following properties: * * type: string * * id: string * * name: string * * verified: boolean * @typedef {Object} ConnectionAccount */ /** * Accounts connected to this user * @type {?ConnectionAccount[]} */ this.connectedAccounts = []; /** * Time that User has nitro (Unix Timestamp) * @type {?number} * @readonly */ this.premiumSince = null; /** * Time that User has nitro and boost server (Unix Timestamp) * @type {?number} * @readonly */ this.premiumGuildSince = null; /** * About me (User) * @type {?string} * @readonly */ this.bio = null; /** * This user is on the same servers as Client User * @type {Collection} * @readonly */ this.mutualGuilds = new Collection(); /** * [Bot] Application * @type {?ClientApplication} * @readonly */ this.application = null; this._patch(data); } _patch(data) { if ('username' in data) { /** * The username of the user * @type {?string} */ this.username = data.username; } else { this.username ??= null; } if ('bot' in data) { /** * Whether or not the user is a bot * @type {?boolean} */ this.bot = Boolean(data.bot); if (this.bot === true) { this.application = new ClientApplication(this.client, { id: this.id }, this); this.botInGuildsCount = null; } } else if (!this.partial && typeof this.bot !== 'boolean') { this.bot = false; } if ('discriminator' in data) { /** * A discriminator based on username for the user * @type {?string} */ this.discriminator = data.discriminator; } else { this.discriminator ??= null; } if ('avatar' in data) { /** * The user avatar's hash * @type {?string} */ this.avatar = data.avatar; } else { this.avatar ??= null; } if ('banner' in data) { /** * The user banner's hash * The user must be force fetched for this property to be present or be updated * @type {?string} */ this.banner = data.banner; } else if (this.banner !== null) { this.banner ??= undefined; } if ('accent_color' in data) { /** * The base 10 accent color of the user's banner * The user must be force fetched for this property to be present or be updated * @type {?number} */ this.accentColor = data.accent_color; } else if (this.accentColor !== null) { this.accentColor ??= undefined; } if ('system' in data) { /** * Whether the user is an Official Discord System user (part of the urgent message system) * @type {?boolean} */ this.system = Boolean(data.system); } else if (!this.partial && typeof this.system !== 'boolean') { this.system = false; } if ('public_flags' in data) { /** * The flags for this user * @type {?UserFlags} */ this.flags = new UserFlags(data.public_flags); } if ('approximate_guild_count' in data) { /** * Check how many guilds the bot is in (Probably only approximate) (application.fetch() first) * @type {?number} */ this.botInGuildsCount = data.approximate_guild_count; } } /** * Check relationship status * @type {RelationshipTypes} * @readonly */ get relationships() { const i = this.client.relationships.cache.get(this.id) ?? 0; return RelationshipTypes[parseInt(i)]; } /** * Check note * @type {?string} * @readonly */ get note() { return this.client.user.notes.get(this.id); } _ProfilePatch(data) { if (!data) return; if (data.connected_accounts.length > 0) { this.connectedAccounts = data.connected_accounts; } if ('premium_since' in data) { const date = new Date(data.premium_since); this.premiumSince = date.getTime(); } if ('premium_guild_since' in data) { const date = new Date(data.premium_guild_since); this.premiumGuildSince = date.getTime(); } if ('bio' in data.user) { this.bio = data.user.bio; } this.mutualGuilds = new Collection(data.mutual_guilds.map(obj => [obj.id, obj])); } /** * Get profile from Discord, if client is in a server with the target. * @type {User} * @returns {Promise} */ async getProfile() { if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); const data = await this.client.api.users(this.id).profile.get(); this._ProfilePatch(data); return this; } /** * Friends the user [If incoming request] * @type {boolean} * @returns {Promise} */ setFriend() { return this.client.relationships.addFriend(this); } /** * Send Friend Request to the user * @type {boolean} * @returns {Promise} */ sendFriendRequest() { return this.client.relationships.sendFriendRequest(this.username, this.discriminator); } /** * Blocks the user * @type {boolean} * @returns {Promise} */ setBlock() { return this.client.relationships.addBlocked(this); } /** * Removes the user from your blocks list * @type {boolean} * @returns {Promise} */ unBlock() { return this.client.relationships.deleteBlocked(this); } /** * Removes the user from your friends list * @type {boolean} * @returns {Promise} */ unFriend() { return this.client.relationships.deleteFriend(this); } /** * Whether this User is a partial * @type {boolean} * @readonly */ get partial() { return typeof this.username !== 'string'; } /** * The timestamp the user was created at * @type {number} * @readonly */ get createdTimestamp() { return SnowflakeUtil.timestampFrom(this.id); } /** * The time the user was created at * @type {Date} * @readonly */ get createdAt() { return new Date(this.createdTimestamp); } /** * A link to the user's avatar. * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ avatarURL({ format, size, dynamic } = {}) { if (!this.avatar) return null; return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic); } /** * A link to the user's default avatar * @type {string} * @readonly */ get defaultAvatarURL() { return this.client.rest.cdn.DefaultAvatar(this.discriminator % 5); } /** * A link to the user's avatar if they have one. * Otherwise a link to their default avatar will be returned. * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {string} */ displayAvatarURL(options) { return this.avatarURL(options) ?? this.defaultAvatarURL; } /** * The hexadecimal version of the user accent color, with a leading hash * The user must be force fetched for this property to be present * @type {?string} * @readonly */ get hexAccentColor() { if (typeof this.accentColor !== 'number') return this.accentColor; return `#${this.accentColor.toString(16).padStart(6, '0')}`; } /** * A link to the user's banner. * This method will throw an error if called before the user is force fetched. * See {@link User#banner} for more info * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ bannerURL({ format, size, dynamic } = {}) { if (typeof this.banner === 'undefined') { throw new Error('USER_BANNER_NOT_FETCHED'); } if (!this.banner) return null; return this.client.rest.cdn.Banner(this.id, this.banner, format, size, dynamic); } /** * The Discord "tag" (e.g. `hydrabolt#0001`) for this user * @type {?string} * @readonly */ get tag() { return typeof this.username === 'string' ? `${this.username}#${this.discriminator}` : null; } /** * The DM between the client's user and this user * @type {?DMChannel} * @readonly */ get dmChannel() { return this.client.users.dmChannel(this.id); } /** * Creates a DM channel between the client and the user. * @param {boolean} [force=false] Whether to skip the cache check and request the API * @returns {Promise} */ createDM(force = false) { return this.client.users.createDM(this.id, force); } /** * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. * @returns {Promise} */ deleteDM() { return this.client.users.deleteDM(this.id); } /** * Checks if the user is equal to another. * It compares id, username, discriminator, avatar, banner, accent color, and bot flags. * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. * @param {User} user User to compare with * @returns {boolean} */ equals(user) { return ( user && this.id === user.id && this.username === user.username && this.discriminator === user.discriminator && this.avatar === user.avatar && this.flags?.bitfield === user.flags?.bitfield && this.banner === user.banner && this.accentColor === user.accentColor ); } /** * Compares the user with an API user object * @param {APIUser} user The API user object to compare * @returns {boolean} * @private */ _equals(user) { return ( user && this.id === user.id && this.username === user.username && this.discriminator === user.discriminator && this.avatar === user.avatar && this.flags?.bitfield === user.public_flags && ('banner' in user ? this.banner === user.banner : true) && ('accent_color' in user ? this.accentColor === user.accent_color : true) ); } /** * Fetches this user's flags. * @param {boolean} [force=false] Whether to skip the cache check and request the API * @returns {Promise} */ fetchFlags(force = false) { return this.client.users.fetchFlags(this.id, { force }); } /** * Fetches this user. * @param {boolean} [force=true] Whether to skip the cache check and request the API * @returns {Promise} */ fetch(force = true) { return this.client.users.fetch(this.id, { force }); } /** * When concatenated with a string, this automatically returns the user's mention instead of the User object. * @returns {string} * @example * // Logs: Hello from <@123456789012345678>! * console.log(`Hello from ${user}!`); */ toString() { return `<@${this.id}>`; } toJSON(...props) { const json = super.toJSON( { createdTimestamp: true, defaultAvatarURL: true, hexAccentColor: true, tag: true, }, ...props, ); json.avatarURL = this.avatarURL(); json.displayAvatarURL = this.displayAvatarURL(); json.bannerURL = this.banner ? this.bannerURL() : this.banner; return json; } /** * Set note to user * @param {string} note Note to set * @returns {Promise} */ async setNote(note = null) { await this.client.api.users['@me'].notes(this.id).put({ data: { note } }); return this; } /** * Get presence (~ v12) * @returns {Promise} */ async presenceFetch() { let data = null; await Promise.all( this.client.guilds.cache.map(async guild => { const res_ = await guild.presences.resolve(this.id); if (res_) return (data = res_); return true; }), ); return data; } // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} } TextBasedChannel.applyToClass(User); module.exports = User; /** * @external APIUser * @see {@link https://discord.com/developers/docs/resources/user#user-object} */