diff --git a/src/api/Router.ts b/src/api/Router.ts index 6c6eb40..430c446 100644 --- a/src/api/Router.ts +++ b/src/api/Router.ts @@ -1,83 +1,83 @@ -import { CDN_URL } from '../common'; -import type { APIRoutes, ApiHandler, CDNRoute } from './index'; -import type { HttpMethods, ImageExtension, ImageSize, StickerExtension } from './shared'; - -export enum ProxyRequestMethod { - Delete = 'delete', - Get = 'get', - Patch = 'patch', - Post = 'post', - Put = 'put', -} - -const ArrRequestsMethods = Object.freeze(Object.values(ProxyRequestMethod)) as string[]; - -export class Router { - noop = () => { - return; - }; - - constructor(private rest: ApiHandler) {} - - createProxy(route = [] as string[]): APIRoutes { - return new Proxy(this.noop, { - get: (_, key: string) => { - if (ArrRequestsMethods.includes(key)) { - return (...options: any[]) => - this.rest.request(key.toUpperCase() as HttpMethods, `/${route.join('/')}`, ...options); - } - return this.createProxy([...route, key]); - }, - apply: (...[, _, args]) => { - return this.createProxy([...route, ...args]); - }, - }) as unknown as APIRoutes; - } -} - -export const CDNRouter = { - createProxy(route = [] as string[]): CDNRoute { - const noop = () => { - return; - }; - return new Proxy(noop, { - get: (_, key: string) => { - if (key === 'get') { - return (value: string | CDNUrlOptions | undefined, options?: CDNUrlOptions) => { - const lastRoute = `${CDN_URL}/${route.join('/')}`; - let routeResult = lastRoute; - if (typeof value === 'string') { - routeResult = `${lastRoute}${value ? `/${value}` : ''}`; - return parseCDNURL(routeResult, options); - } - return parseCDNURL(routeResult, value); - }; - } - return this.createProxy([...route, key]); - }, - apply: (...[, _, args]) => { - return this.createProxy([...route, ...args]); - }, - }) as unknown as CDNRoute; - }, -}; - -export interface BaseCDNUrlOptions { - extension?: ImageExtension | StickerExtension | undefined; - size?: ImageSize; -} - -export interface CDNUrlOptions extends BaseCDNUrlOptions { - forceStatic?: boolean; -} - -export function parseCDNURL(route: string, options: CDNUrlOptions = {}) { - if (options.forceStatic && route.includes('a_')) options.extension = 'png'; - if (!options.extension && route.includes('a_')) options.extension = 'gif'; - - const url = new URL(`${route}.${options.extension || 'png'}`); - - if (options.size) url.searchParams.set('size', `${options.size}`); - - return url.toString(); -} +import { CDN_URL } from '../common'; +import type { APIRoutes, ApiHandler, CDNRoute } from './index'; +import type { HttpMethods, ImageExtension, ImageSize, StickerExtension } from './shared'; + +export enum ProxyRequestMethod { + Delete = 'delete', + Get = 'get', + Patch = 'patch', + Post = 'post', + Put = 'put', +} + +const ArrRequestsMethods = Object.freeze(Object.values(ProxyRequestMethod)) as string[]; + +export class Router { + noop = () => { + return; + }; + + constructor(private rest: ApiHandler) {} + + createProxy(route = [] as string[]): APIRoutes { + return new Proxy(this.noop, { + get: (_, key: string) => { + if (ArrRequestsMethods.includes(key)) { + return (...options: any[]) => + this.rest.request(key.toUpperCase() as HttpMethods, `/${route.join('/')}`, ...options); + } + return this.createProxy([...route, key]); + }, + apply: (...[, _, args]) => { + return this.createProxy([...route, ...args]); + }, + }) as unknown as APIRoutes; + } +} + +export const CDNRouter = { + createProxy(route = [] as string[]): CDNRoute { + const noop = () => { + return; + }; + return new Proxy(noop, { + get: (_, key: string) => { + if (key === 'get') { + return (value: string | CDNUrlOptions | undefined, options?: CDNUrlOptions) => { + const lastRoute = `${CDN_URL}/${route.join('/')}`; + let routeResult = lastRoute; + if (typeof value === 'string' || typeof value === 'number') { + routeResult = `${lastRoute}${value ? `/${value}` : ''}`; + return parseCDNURL(routeResult, options); + } + return parseCDNURL(routeResult, value); + }; + } + return this.createProxy([...route, key]); + }, + apply: (...[, _, args]) => { + return this.createProxy([...route, ...args]); + }, + }) as unknown as CDNRoute; + }, +}; + +export interface BaseCDNUrlOptions { + extension?: ImageExtension | StickerExtension | undefined; + size?: ImageSize; +} + +export interface CDNUrlOptions extends BaseCDNUrlOptions { + forceStatic?: boolean; +} + +export function parseCDNURL(route: string, options: CDNUrlOptions = {}) { + if (options.forceStatic && route.includes('a_')) options.extension = 'png'; + if (!options.extension && route.includes('a_')) options.extension = 'gif'; + + const url = new URL(`${route}.${options.extension || 'png'}`); + + if (options.size) url.searchParams.set('size', `${options.size}`); + + return url.toString(); +} diff --git a/src/commands/handler.ts b/src/commands/handler.ts index 5cd3d27..c3e88ca 100644 --- a/src/commands/handler.ts +++ b/src/commands/handler.ts @@ -266,6 +266,8 @@ export class CommandHandler extends BaseHandler { this.client.options?.commands?.defaults?.onPermissionsFail; option.botPermissions ??= commandInstance.botPermissions; option.defaultMemberPermissions ??= commandInstance.defaultMemberPermissions; + option.contexts ??= commandInstance.contexts; + option.integrationTypes ??= commandInstance.integrationTypes; } } diff --git a/src/structures/GuildMember.ts b/src/structures/GuildMember.ts index 47547d9..902cb80 100644 --- a/src/structures/GuildMember.ts +++ b/src/structures/GuildMember.ts @@ -1,257 +1,255 @@ -import { DiscordBase } from './extra/DiscordBase'; - -export type GuildMemberData = - | APIGuildMember - | GatewayGuildMemberUpdateDispatchData - | GatewayGuildMemberAddDispatchData - | APIInteractionDataResolvedGuildMember; - -import type { - APIGuildMember, - APIInteractionDataResolvedGuildMember, - APIUser, - GatewayGuildMemberAddDispatchData, - GatewayGuildMemberUpdateDispatchData, - RESTGetAPIGuildMembersQuery, - RESTGetAPIGuildMembersSearchQuery, - RESTPatchAPIGuildMemberJSONBody, - RESTPutAPIGuildBanJSONBody, - RESTPutAPIGuildMemberJSONBody, -} from 'discord-api-types/v10'; -import type { UsingClient } from '../commands'; -import type { MakeRequired, MessageCreateBodyRequest, ObjectToLower, ToClass } from '../common'; -import type { ImageOptions, MethodContext } from '../common/types/options'; -import type { GuildMemberResolvable } from '../common/types/resolvables'; -import { User } from './User'; -import { PermissionsBitField } from './extra/Permissions'; - -export type GatewayGuildMemberAddDispatchDataFixed = Pending extends true - ? Omit & { id: string } - : MakeRequired; - -export interface BaseGuildMember extends DiscordBase, ObjectToLower> {} -export class BaseGuildMember extends DiscordBase { - private _roles: string[]; - joinedTimestamp?: number; - communicationDisabledUntilTimestamp?: number | null; - constructor( - client: UsingClient, - data: GuildMemberData, - id: string, - /** the choosen guild id */ - readonly guildId: string, - ) { - const { roles, ...dataN } = data; - super(client, { ...dataN, id }); - this._roles = data.roles; - this.patch(data); - } - - guild(force = false) { - return this.client.guilds.fetch(this.id, force); - } - - fetch(force = false) { - return this.client.members.fetch(this.guildId, this.id, force); - } - - ban(body?: RESTPutAPIGuildBanJSONBody, reason?: string) { - return this.client.members.ban(this.guildId, this.id, body, reason); - } - - kick(reason?: string) { - return this.client.members.kick(this.guildId, this.id, reason); - } - - edit(body: RESTPatchAPIGuildMemberJSONBody, reason?: string) { - return this.client.members.edit(this.guildId, this.id, body, reason); - } - - presence() { - return this.client.members.presence(this.id); - } - - voice() { - return this.client.members.voice(this.guildId, this.id); - } - - toString() { - return `<@${this.id}>`; - } - - private patch(data: GuildMemberData) { - if ('joined_at' in data && data.joined_at) { - this.joinedTimestamp = Date.parse(data.joined_at); - } - if ('communication_disabled_until' in data) { - this.communicationDisabledUntilTimestamp = data.communication_disabled_until?.length - ? Date.parse(data.communication_disabled_until) - : null; - } - } - - get roles() { - return { - keys: Object.freeze(this._roles.concat(this.guildId)) as string[], - list: (force = false) => - this.client.roles - .list(this.guildId, force) - .then(roles => roles.filter(role => this.roles.keys.includes(role.id))), - add: (id: string) => this.client.members.addRole(this.guildId, this.id, id), - remove: (id: string) => this.client.members.removeRole(this.guildId, this.id, id), - permissions: (force = false) => - this.roles.list(force).then(roles => new PermissionsBitField(roles.map(x => BigInt(x.permissions.bits)))), - sorted: (force = false) => this.roles.list(force).then(roles => roles.sort((a, b) => b.position - a.position)), - highest: (force = false) => this.roles.sorted(force).then(roles => roles[0]), - }; - } - - static methods({ client, guildId }: MethodContext<{ guildId: string }>) { - return { - resolve: (resolve: GuildMemberResolvable) => client.members.resolve(guildId, resolve), - search: (query?: RESTGetAPIGuildMembersSearchQuery) => client.members.search(guildId, query), - unban: (id: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) => - client.members.unban(guildId, id, body, reason), - ban: (id: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) => - client.members.ban(guildId, id, body, reason), - kick: (id: string, reason?: string) => client.members.kick(guildId, id, reason), - edit: (id: string, body: RESTPatchAPIGuildMemberJSONBody, reason?: string) => - client.members.edit(guildId, id, body, reason), - add: (id: string, body: RESTPutAPIGuildMemberJSONBody) => client.members.add(guildId, id, body), - fetch: (memberId: string, force = false) => client.members.fetch(guildId, memberId, force), - list: (query?: RESTGetAPIGuildMembersQuery, force = false) => client.members.list(guildId, query, force), - }; - } -} - -export interface GuildMember extends ObjectToLower> {} -/** - * Represents a guild member - * @link https://discord.com/developers/docs/resources/guild#guild-member-object - */ -export class GuildMember extends BaseGuildMember { - user: User; - private __me?: GuildMember; - constructor( - client: UsingClient, - data: GuildMemberData, - user: APIUser | User, - /** the choosen guild id */ - readonly guildId: string, - ) { - super(client, data, user.id, guildId); - this.user = user instanceof User ? user : new User(client, user); - } - - get tag() { - return this.user.tag; - } - - get bot() { - return this.user.bot; - } - - get name() { - return this.user.name; - } - - get username() { - return this.user.username; - } - - get globalName() { - return this.user.globalName; - } - - /** gets the nickname or the username */ - get displayName() { - return this.nick ?? this.globalName ?? this.username; - } - - dm(force = false) { - return this.user.dm(force); - } - - write(body: MessageCreateBodyRequest) { - return this.user.write(body); - } - - avatarURL(options?: ImageOptions) { - if (!this.avatar) { - return null; - } - - return this.rest.cdn.guilds(this.guildId).users(this.id).avatars(this.avatar).get(options); - } - - dynamicAvatarURL(options?: ImageOptions) { - if (!this.avatar) { - return this.user.avatarURL(options); - } - - return this.rest.cdn.guilds(this.guildId).users(this.id).avatars(this.avatar).get(options); - } - - bannerURL(options?: ImageOptions) { - return this.user.bannerURL(options); - } - - async fetchPermissions(force = false) { - if ('permissions' in this) return this.permissions as PermissionsBitField; - return this.roles.permissions(force); - } - - async manageable(force = false) { - this.__me = await this.client.guilds.fetchSelf(this.guildId, force); - const ownerId = (await this.client.guilds.fetch(this.guildId, force)).ownerId; - if (this.user.id === ownerId) return false; - if (this.user.id === this.client.botId) return false; - if (this.client.botId === ownerId) return true; - return (await this.__me!.roles.highest()).position > (await this.roles.highest(force)).position; - } - - async bannable(force = false) { - return (await this.manageable(force)) && (await this.__me!.fetchPermissions(force)).has('BanMembers'); - } - - async kickable(force = false) { - return (await this.manageable(force)) && (await this.__me!.fetchPermissions(force)).has('KickMembers'); - } - - async moderatable(force = false) { - return ( - !(await this.roles.permissions(force)).has('Administrator') && - (await this.manageable(force)) && - (await this.__me!.fetchPermissions(force)).has('KickMembers') - ); - } -} - -export interface UnavailableMember { - pending: true; -} - -export class UnavailableMember extends BaseGuildMember {} - -export interface InteractionGuildMember - extends ObjectToLower> {} -/** - * Represents a guild member - * @link https://discord.com/developers/docs/resources/guild#guild-member-object - */ -export class InteractionGuildMember extends (GuildMember as unknown as ToClass< - Omit, - InteractionGuildMember ->) { - permissions: PermissionsBitField; - constructor( - client: UsingClient, - data: APIInteractionDataResolvedGuildMember, - user: APIUser | User, - /** the choosen guild id */ - guildId: string, - ) { - super(client, data, user, guildId); - this.permissions = new PermissionsBitField(Number(data.permissions)); - } -} +import { DiscordBase } from './extra/DiscordBase'; + +export type GuildMemberData = + | APIGuildMember + | GatewayGuildMemberUpdateDispatchData + | GatewayGuildMemberAddDispatchData + | APIInteractionDataResolvedGuildMember; + +import type { + APIGuildMember, + APIInteractionDataResolvedGuildMember, + APIUser, + GatewayGuildMemberAddDispatchData, + GatewayGuildMemberUpdateDispatchData, + RESTGetAPIGuildMembersQuery, + RESTGetAPIGuildMembersSearchQuery, + RESTPatchAPIGuildMemberJSONBody, + RESTPutAPIGuildBanJSONBody, + RESTPutAPIGuildMemberJSONBody, +} from 'discord-api-types/v10'; +import type { UsingClient } from '../commands'; +import type { MakeRequired, MessageCreateBodyRequest, ObjectToLower, ToClass } from '../common'; +import type { ImageOptions, MethodContext } from '../common/types/options'; +import type { GuildMemberResolvable } from '../common/types/resolvables'; +import { User } from './User'; +import { PermissionsBitField } from './extra/Permissions'; + +export type GatewayGuildMemberAddDispatchDataFixed = Pending extends true + ? Omit & { id: string } + : MakeRequired; + +export interface BaseGuildMember extends DiscordBase, ObjectToLower> {} +export class BaseGuildMember extends DiscordBase { + private _roles: string[]; + joinedTimestamp?: number; + communicationDisabledUntilTimestamp?: number | null; + constructor( + client: UsingClient, + data: GuildMemberData, + id: string, + /** the choosen guild id */ + readonly guildId: string, + ) { + const { roles, ...dataN } = data; + super(client, { ...dataN, id }); + this._roles = data.roles; + this.patch(data); + } + + guild(force = false) { + return this.client.guilds.fetch(this.id, force); + } + + fetch(force = false) { + return this.client.members.fetch(this.guildId, this.id, force); + } + + ban(body?: RESTPutAPIGuildBanJSONBody, reason?: string) { + return this.client.members.ban(this.guildId, this.id, body, reason); + } + + kick(reason?: string) { + return this.client.members.kick(this.guildId, this.id, reason); + } + + edit(body: RESTPatchAPIGuildMemberJSONBody, reason?: string) { + return this.client.members.edit(this.guildId, this.id, body, reason); + } + + presence() { + return this.client.members.presence(this.id); + } + + voice() { + return this.client.members.voice(this.guildId, this.id); + } + + toString() { + return `<@${this.id}>`; + } + + private patch(data: GuildMemberData) { + if ('joined_at' in data && data.joined_at) { + this.joinedTimestamp = Date.parse(data.joined_at); + } + if ('communication_disabled_until' in data) { + this.communicationDisabledUntilTimestamp = data.communication_disabled_until?.length + ? Date.parse(data.communication_disabled_until) + : null; + } + } + + get roles() { + return { + keys: Object.freeze(this._roles.concat(this.guildId)) as string[], + list: (force = false) => + this.client.roles + .list(this.guildId, force) + .then(roles => roles.filter(role => this.roles.keys.includes(role.id))), + add: (id: string) => this.client.members.addRole(this.guildId, this.id, id), + remove: (id: string) => this.client.members.removeRole(this.guildId, this.id, id), + permissions: (force = false) => + this.roles.list(force).then(roles => new PermissionsBitField(roles.map(x => BigInt(x.permissions.bits)))), + sorted: (force = false) => this.roles.list(force).then(roles => roles.sort((a, b) => b.position - a.position)), + highest: (force = false) => this.roles.sorted(force).then(roles => roles[0]), + }; + } + + static methods({ client, guildId }: MethodContext<{ guildId: string }>) { + return { + resolve: (resolve: GuildMemberResolvable) => client.members.resolve(guildId, resolve), + search: (query?: RESTGetAPIGuildMembersSearchQuery) => client.members.search(guildId, query), + unban: (id: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) => + client.members.unban(guildId, id, body, reason), + ban: (id: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) => + client.members.ban(guildId, id, body, reason), + kick: (id: string, reason?: string) => client.members.kick(guildId, id, reason), + edit: (id: string, body: RESTPatchAPIGuildMemberJSONBody, reason?: string) => + client.members.edit(guildId, id, body, reason), + add: (id: string, body: RESTPutAPIGuildMemberJSONBody) => client.members.add(guildId, id, body), + fetch: (memberId: string, force = false) => client.members.fetch(guildId, memberId, force), + list: (query?: RESTGetAPIGuildMembersQuery, force = false) => client.members.list(guildId, query, force), + }; + } +} + +export interface GuildMember extends ObjectToLower> {} +/** + * Represents a guild member + * @link https://discord.com/developers/docs/resources/guild#guild-member-object + */ +export class GuildMember extends BaseGuildMember { + user: User; + private __me?: GuildMember; + constructor( + client: UsingClient, + data: GuildMemberData, + user: APIUser | User, + /** the choosen guild id */ + readonly guildId: string, + ) { + super(client, data, user.id, guildId); + this.user = user instanceof User ? user : new User(client, user); + } + + get tag() { + return this.user.tag; + } + + get bot() { + return this.user.bot; + } + + get name() { + return this.user.name; + } + + get username() { + return this.user.username; + } + + get globalName() { + return this.user.globalName; + } + + /** gets the nickname or the username */ + get displayName() { + return this.nick ?? this.globalName ?? this.username; + } + + dm(force = false) { + return this.user.dm(force); + } + + write(body: MessageCreateBodyRequest) { + return this.user.write(body); + } + + defaultAvatarURL() { + return this.user.defaultAvatarURL(); + } + + avatarURL(options: ImageOptions & { exclude: true }): string | null; + avatarURL(options?: ImageOptions & { exclude?: false }): string; + avatarURL(options?: ImageOptions & { exclude?: boolean }): string | null { + if (!this.avatar) { + return options?.exclude ? null : this.user.avatarURL(); + } + + return this.rest.cdn.guilds(this.guildId).users(this.id).avatars(this.avatar).get(options); + } + + bannerURL(options?: ImageOptions) { + return this.user.bannerURL(options); + } + + async fetchPermissions(force = false) { + if ('permissions' in this) return this.permissions as PermissionsBitField; + return this.roles.permissions(force); + } + + async manageable(force = false) { + this.__me = await this.client.guilds.fetchSelf(this.guildId, force); + const ownerId = (await this.client.guilds.fetch(this.guildId, force)).ownerId; + if (this.user.id === ownerId) return false; + if (this.user.id === this.client.botId) return false; + if (this.client.botId === ownerId) return true; + return (await this.__me!.roles.highest()).position > (await this.roles.highest(force)).position; + } + + async bannable(force = false) { + return (await this.manageable(force)) && (await this.__me!.fetchPermissions(force)).has('BanMembers'); + } + + async kickable(force = false) { + return (await this.manageable(force)) && (await this.__me!.fetchPermissions(force)).has('KickMembers'); + } + + async moderatable(force = false) { + return ( + !(await this.roles.permissions(force)).has('Administrator') && + (await this.manageable(force)) && + (await this.__me!.fetchPermissions(force)).has('KickMembers') + ); + } +} + +export interface UnavailableMember { + pending: true; +} + +export class UnavailableMember extends BaseGuildMember {} + +export interface InteractionGuildMember + extends ObjectToLower> {} +/** + * Represents a guild member + * @link https://discord.com/developers/docs/resources/guild#guild-member-object + */ +export class InteractionGuildMember extends (GuildMember as unknown as ToClass< + Omit, + InteractionGuildMember +>) { + permissions: PermissionsBitField; + constructor( + client: UsingClient, + data: APIInteractionDataResolvedGuildMember, + user: APIUser | User, + /** the choosen guild id */ + guildId: string, + ) { + super(client, data, user, guildId); + this.permissions = new PermissionsBitField(Number(data.permissions)); + } +} diff --git a/src/structures/User.ts b/src/structures/User.ts index 8ce155f..a0067f8 100644 --- a/src/structures/User.ts +++ b/src/structures/User.ts @@ -1,61 +1,65 @@ -import type { APIUser } from 'discord-api-types/v10'; -import { calculateUserDefaultAvatarIndex } from '../api'; -import type { MessageCreateBodyRequest, ObjectToLower } from '../common'; -import type { ImageOptions } from '../common/types/options'; -import { DiscordBase } from './extra/DiscordBase'; - -export interface User extends ObjectToLower {} - -export class User extends DiscordBase { - get tag() { - return this.globalName ?? `${this.username}#${this.discriminator}`; - } - - get name() { - return this.globalName ?? this.username; - } - - /** - * Fetch user - */ - fetch(force = false) { - return this.client.users.fetch(this.id, force); - } - - /** - * Open a DM with the user - */ - dm(force = false) { - return this.client.users.createDM(this.id, force); - } - - write(body: MessageCreateBodyRequest) { - return this.client.users.write(this.id, body); - } - - avatarURL(options?: ImageOptions) { - if (!this.avatar) { - return this.rest.cdn.embed.avatars.get(calculateUserDefaultAvatarIndex(this.id, this.discriminator)); - } - - return this.rest.cdn.avatars(this.id).get(this.avatar, options); - } - - avatarDecorationURL(options?: ImageOptions) { - if (!this.avatarDecoration) return; - return this.rest.cdn['avatar-decorations'](this.id).get(this.avatarDecoration, options); - } - - bannerURL(options?: ImageOptions) { - if (!this.banner) return; - return this.rest.cdn.banners(this.id).get(this.banner, options); - } - - presence() { - return this.client.members.presence(this.id); - } - - toString() { - return `<@${this.id}>`; - } -} +import type { APIUser } from 'discord-api-types/v10'; +import { calculateUserDefaultAvatarIndex } from '../api'; +import type { MessageCreateBodyRequest, ObjectToLower } from '../common'; +import type { ImageOptions } from '../common/types/options'; +import { DiscordBase } from './extra/DiscordBase'; + +export interface User extends ObjectToLower {} + +export class User extends DiscordBase { + get tag() { + return this.globalName ?? `${this.username}#${this.discriminator}`; + } + + get name() { + return this.globalName ?? this.username; + } + + /** + * Fetch user + */ + fetch(force = false) { + return this.client.users.fetch(this.id, force); + } + + /** + * Open a DM with the user + */ + dm(force = false) { + return this.client.users.createDM(this.id, force); + } + + write(body: MessageCreateBodyRequest) { + return this.client.users.write(this.id, body); + } + + defaultAvatarURL() { + return this.rest.cdn.embed.avatars.get(calculateUserDefaultAvatarIndex(this.id, this.discriminator)); + } + + avatarURL(options?: ImageOptions) { + if (!this.avatar) { + return this.defaultAvatarURL(); + } + + return this.rest.cdn.avatars(this.id).get(this.avatar, options); + } + + avatarDecorationURL(options?: ImageOptions) { + if (!this.avatarDecoration) return; + return this.rest.cdn['avatar-decorations'](this.id).get(this.avatarDecoration, options); + } + + bannerURL(options?: ImageOptions) { + if (!this.banner) return; + return this.rest.cdn.banners(this.id).get(this.banner, options); + } + + presence() { + return this.client.members.presence(this.id); + } + + toString() { + return `<@${this.id}>`; + } +}