From 9b821e5dfcfb92a9d23ef96dd947c0bd11ee7b86 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:36:28 +0100 Subject: [PATCH] feat(GuildMemberManager): Add new modify self fields (#11112) * feat(GuildMemberManager): Add new modify self fields (#11089) * fix: use correct route * fix: add deprecation * fix: rewrite message --- .../src/managers/GuildMemberManager.js | 65 +++++++++++++++++-- .../discord.js/src/structures/GuildMember.js | 4 +- packages/discord.js/typings/index.d.ts | 9 +++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index c480e6a55..e37a874d7 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -1,5 +1,6 @@ 'use strict'; +const { process } = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); @@ -10,10 +11,13 @@ const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); const { GuildMember } = require('../structures/GuildMember'); const { Role } = require('../structures/Role'); +const { resolveImage } = require('../util/DataResolver'); const Events = require('../util/Events'); const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); const Partials = require('../util/Partials'); +let deprecatedEmittedForEditSoleNickname = false; + /** * Manages API methods for GuildMembers and stores their cache. * @extends {CachedManager} @@ -336,8 +340,8 @@ class GuildMemberManager extends CachedManager { */ /** - * Edits a member of the guild. - * The user must be a member of the guild + * Edits a member of a guild. + * * @param {UserResolvable} user The member to edit * @param {GuildMemberEditOptions} options The options to provide * @returns {Promise} @@ -372,13 +376,30 @@ class GuildMemberManager extends CachedManager { } let endpoint; + if (id === this.client.user.id) { const keys = Object.keys(options); - if (keys.length === 1 && keys[0] === 'nick') endpoint = Routes.guildMember(this.guild.id); - else endpoint = Routes.guildMember(this.guild.id, id); - } else { - endpoint = Routes.guildMember(this.guild.id, id); + + if (keys.length === 1 && keys[0] === 'nick') { + // For modifying the current application's nickname only, we use the /guilds/{guild.id}/members/@me endpoint. + // This endpoint only requires the CHANGE_NICKNAME permission. + // The other endpoint would require the MANAGE_NICKNAMES permission. + // In v15, this will be split out, so emit a deprecation. + endpoint = Routes.guildMember(this.guild.id, '@me'); + + if (!deprecatedEmittedForEditSoleNickname) { + process.emitWarning( + // eslint-disable-next-line max-len + "You should use GuildMemberManager#editMe() when changing your nickname. Due to Discord's API changes, GuildMemberManager#edit() will end up requiring MANAGE_NICKNAMES in v15.", + 'DeprecationWarning', + ); + + deprecatedEmittedForEditSoleNickname = true; + } + } } + + endpoint ??= Routes.guildMember(this.guild.id, id); const d = await this.client.rest.patch(endpoint, { body: options, reason }); const clone = this.cache.get(id)?._clone(); @@ -386,6 +407,38 @@ class GuildMemberManager extends CachedManager { return clone ?? this._add(d, false); } + /** + * The data for editing the current application's guild member. + * + * @typedef {Object} GuildMemberEditMeOptions + * @property {?string} [nick] The nickname to set + * @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner to set + * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The avatar to set + * @property {?string} [bio] The bio to set + * @property {string} [reason] The reason to use + */ + + /** + * Edits the current application's guild member in a guild. + * + * @param {GuildMemberEditMeOptions} options The options to provide + * @returns {Promise} + */ + async editMe({ reason, ...options }) { + const data = await this.client.rest.patch(Routes.guildMember(this.guild.id, '@me'), { + body: { + ...options, + banner: options.banner && (await resolveImage(options.banner)), + avatar: options.avatar && (await resolveImage(options.avatar)), + }, + reason, + }); + + const clone = this.me?._clone(); + clone?._patch(data); + return clone ?? this._add(data, false); + } + /** * Options used for pruning guild members. * It's recommended to set {@link GuildPruneMembersOptions#count options.count} diff --git a/packages/discord.js/src/structures/GuildMember.js b/packages/discord.js/src/structures/GuildMember.js index 44465ec3c..3abf3f911 100644 --- a/packages/discord.js/src/structures/GuildMember.js +++ b/packages/discord.js/src/structures/GuildMember.js @@ -422,7 +422,9 @@ class GuildMember extends Base { * .catch(console.error); */ setNickname(nick, reason) { - return this.edit({ nick, reason }); + return this.user.id === this.client.user.id + ? this.guild.members.editMe({ nick, reason }) + : this.edit({ nick, reason }); } /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 46da36cc4..7f64a6282 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -5017,6 +5017,7 @@ export class GuildMemberManager extends CachedManager; public edit(user: UserResolvable, options: GuildMemberEditOptions): Promise; + public editMe(options: GuildMemberEditMeOptions): Promise; public fetch( options: UserResolvable | FetchMemberOptions | (FetchMembersOptions & { user: UserResolvable }), ): Promise; @@ -6875,6 +6876,14 @@ export interface GuildMemberEditOptions { export type GuildMemberResolvable = GuildMember | UserResolvable; +export interface GuildMemberEditMeOptions { + avatar?: Base64Resolvable | BufferResolvable | null; + banner?: Base64Resolvable | BufferResolvable | null; + bio?: string | null; + nick?: string | null; + reason?: string; +} + export type GuildResolvable = Guild | NonThreadGuildBasedChannel | GuildMember | GuildEmoji | Invite | Role | Snowflake; export interface GuildPruneMembersOptions {