refactor(Webhooks)!: remove WebhookClient (#11266)

BREAKING CHANGE: WebhookClient has been removed, use @discordjs/core instead or fetch webhooks. Alternative solutions are in the works
BREAKING CHANGE: `WebhookURLInvalid` is no longer an error code (obsolete).

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
ckohen
2025-11-13 12:57:00 -08:00
committed by GitHub
parent 837af56cf8
commit 9f18cb2129
10 changed files with 32 additions and 189 deletions

View File

@@ -1,119 +0,0 @@
'use strict';
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { Webhook } = require('../structures/Webhook.js');
const { parseWebhookURL } = require('../util/Util.js');
const { BaseClient } = require('./BaseClient.js');
/**
* The webhook client.
*
* @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends BaseClient {
/**
* Represents the credentials used for a webhook in the form of its id and token.
*
* @typedef {Object} WebhookClientDataIdWithToken
* @property {Snowflake} id The webhook's id
* @property {string} token The webhook's token
*/
/**
* Represents the credentials used for a webhook in the form of a URL.
*
* @typedef {Object} WebhookClientDataURL
* @property {string} url The full URL for the webhook
*/
/**
* Represents the credentials used for a webhook.
*
* @typedef {WebhookClientDataIdWithToken|WebhookClientDataURL} WebhookClientData
*/
/**
* Options for a webhook client.
*
* @typedef {Object} WebhookClientOptions
* @property {MessageMentionOptions} [allowedMentions] Default value for {@link BaseMessageOptions#allowedMentions}
* @property {RESTOptions} [rest] Options for the REST manager
*/
/**
* @param {WebhookClientData} data The data of the webhook
* @param {WebhookClientOptions} [options] Options for the webhook client
*/
constructor(data, options) {
super(options);
Object.defineProperty(this, 'client', { value: this });
let { id, token } = data;
if ('url' in data) {
const parsed = parseWebhookURL(data.url);
if (!parsed) {
throw new DiscordjsError(ErrorCodes.WebhookURLInvalid);
}
({ id, token } = parsed);
}
this.id = id;
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
}
/**
* The options the webhook client was instantiated with.
*
* @type {WebhookClientOptions}
* @name WebhookClient#options
*/
// These are here only for documentation purposes - they are implemented by Webhook
/* eslint-disable jsdoc/check-param-names, getter-return */
/**
* Sends a message with this webhook.
*
* @param {string|MessagePayload|WebhookMessageCreateOptions} options The content for the reply
* @returns {Promise<APIMessage>}
*/
async send() {}
/**
* Gets a message that was sent by this webhook.
*
* @param {Snowflake} message The id of the message to fetch
* @param {WebhookFetchMessageOptions} [options] The options to provide to fetch the message.
* @returns {Promise<APIMessage>} Returns the message sent by this webhook
*/
async fetchMessage() {}
/**
* Edits a message that was sent by this webhook.
*
* @param {MessageResolvable} message The message to edit
* @param {string|MessagePayload|WebhookMessageEditOptions} options The options to provide
* @returns {Promise<APIMessage>} Returns the message edited by this webhook
*/
async editMessage() {}
sendSlackMessage() {}
edit() {}
delete() {}
deleteMessage() {}
get createdTimestamp() {}
get createdAt() {}
get url() {}
}
Webhook.applyToClass(WebhookClient);
exports.WebhookClient = WebhookClient;

View File

@@ -77,7 +77,6 @@
* *
* @property {'WebhookMessage'} WebhookMessage * @property {'WebhookMessage'} WebhookMessage
* @property {'WebhookTokenUnavailable'} WebhookTokenUnavailable * @property {'WebhookTokenUnavailable'} WebhookTokenUnavailable
* @property {'WebhookURLInvalid'} WebhookURLInvalid
* @property {'WebhookApplication'} WebhookApplication * @property {'WebhookApplication'} WebhookApplication
* *
* @property {'MessageReferenceMissing'} MessageReferenceMissing * @property {'MessageReferenceMissing'} MessageReferenceMissing
@@ -212,7 +211,6 @@ const keys = [
'WebhookMessage', 'WebhookMessage',
'WebhookTokenUnavailable', 'WebhookTokenUnavailable',
'WebhookURLInvalid',
'WebhookApplication', 'WebhookApplication',
'MessageReferenceMissing', 'MessageReferenceMissing',

View File

@@ -82,7 +82,6 @@ const Messages = {
[ErrorCodes.WebhookMessage]: 'The message was not sent by a webhook.', [ErrorCodes.WebhookMessage]: 'The message was not sent by a webhook.',
[ErrorCodes.WebhookTokenUnavailable]: 'This action requires a webhook token, but none is available.', [ErrorCodes.WebhookTokenUnavailable]: 'This action requires a webhook token, but none is available.',
[ErrorCodes.WebhookURLInvalid]: 'The provided webhook URL is not valid.',
[ErrorCodes.WebhookApplication]: 'This message webhook belongs to an application and cannot be fetched.', [ErrorCodes.WebhookApplication]: 'This message webhook belongs to an application and cannot be fetched.',
[ErrorCodes.MessageReferenceMissing]: 'The message does not reference another message', [ErrorCodes.MessageReferenceMissing]: 'The message does not reference another message',

View File

@@ -8,7 +8,6 @@ exports.Client = require('./client/Client.js').Client;
exports.Shard = require('./sharding/Shard.js').Shard; exports.Shard = require('./sharding/Shard.js').Shard;
exports.ShardClientUtil = require('./sharding/ShardClientUtil.js').ShardClientUtil; exports.ShardClientUtil = require('./sharding/ShardClientUtil.js').ShardClientUtil;
exports.ShardingManager = require('./sharding/ShardingManager.js').ShardingManager; exports.ShardingManager = require('./sharding/ShardingManager.js').ShardingManager;
exports.WebhookClient = require('./client/WebhookClient.js').WebhookClient;
// Errors // Errors
exports.DiscordjsError = require('./errors/DJSError.js').DiscordjsError; exports.DiscordjsError = require('./errors/DJSError.js').DiscordjsError;

View File

@@ -47,15 +47,14 @@ class MessagePayload {
} }
/** /**
* Whether or not the target is a {@link Webhook} or a {@link WebhookClient} * Whether or not the target is a {@link Webhook}
* *
* @type {boolean} * @type {boolean}
* @readonly * @readonly
*/ */
get isWebhook() { get isWebhook() {
const { Webhook } = require('./Webhook.js'); const { Webhook } = require('./Webhook.js');
const { WebhookClient } = require('../client/WebhookClient.js'); return this.target instanceof Webhook;
return this.target instanceof Webhook || this.target instanceof WebhookClient;
} }
/** /**
@@ -302,7 +301,7 @@ exports.MessagePayload = MessagePayload;
/** /**
* A target for a message. * A target for a message.
* *
* @typedef {TextBasedChannels|ChannelManager|Webhook|WebhookClient|BaseInteraction|InteractionWebhook| * @typedef {TextBasedChannels|ChannelManager|Webhook|BaseInteraction|InteractionWebhook|
* Message|MessageManager} MessageTarget * Message|MessageManager} MessageTarget
*/ */

View File

@@ -95,9 +95,9 @@ class Webhook {
/** /**
* The owner of the webhook * The owner of the webhook
* *
* @type {?(User|APIUser)} * @type {?User}
*/ */
this.owner = this.client.users?._add(data.user) ?? data.user; this.owner = this.client.users._add(data.user);
} else { } else {
this.owner ??= null; this.owner ??= null;
} }
@@ -119,7 +119,7 @@ class Webhook {
* *
* @type {?(Guild|APIGuild)} * @type {?(Guild|APIGuild)}
*/ */
this.sourceGuild = this.client.guilds?.cache.get(data.source_guild.id) ?? data.source_guild; this.sourceGuild = this.client.guilds.cache.get(data.source_guild.id) ?? data.source_guild;
} else { } else {
this.sourceGuild ??= null; this.sourceGuild ??= null;
} }
@@ -130,7 +130,7 @@ class Webhook {
* *
* @type {?(AnnouncementChannel|APIChannel)} * @type {?(AnnouncementChannel|APIChannel)}
*/ */
this.sourceChannel = this.client.channels?.cache.get(data.source_channel?.id) ?? data.source_channel; this.sourceChannel = this.client.channels.cache.get(data.source_channel?.id) ?? data.source_channel;
} else { } else {
this.sourceChannel ??= null; this.sourceChannel ??= null;
} }
@@ -248,7 +248,6 @@ class Webhook {
auth: false, auth: false,
}); });
if (!this.client.channels) return data;
return ( return (
this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ?? this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ??
new (getMessage())(this.client, data) new (getMessage())(this.client, data)
@@ -345,7 +344,6 @@ class Webhook {
auth: false, auth: false,
}); });
if (!this.client.channels) return data;
return ( return (
this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ?? this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ??
new (getMessage())(this.client, data) new (getMessage())(this.client, data)
@@ -384,10 +382,7 @@ class Webhook {
}, },
); );
const channelManager = this.client.channels; const messageManager = this.client.channels.cache.get(data.channel_id)?.messages;
if (!channelManager) return data;
const messageManager = channelManager.cache.get(data.channel_id)?.messages;
if (!messageManager) return new (getMessage())(this.client, data); if (!messageManager) return new (getMessage())(this.client, data);
const existing = messageManager.cache.get(data.id); const existing = messageManager.cache.get(data.id);

View File

@@ -469,11 +469,19 @@ function cleanCodeBlockContent(text) {
return text.replaceAll('```', '`\u200B``'); return text.replaceAll('```', '`\u200B``');
} }
/**
* Represents the credentials used for a webhook in the form of its id and token.
*
* @typedef {Object} WebhookDataIdWithToken
* @property {Snowflake} id The webhook's id
* @property {string} token The webhook's token
*/
/** /**
* Parses a webhook URL for the id and token. * Parses a webhook URL for the id and token.
* *
* @param {string} url The URL to parse * @param {string} url The URL to parse
* @returns {?WebhookClientDataIdWithToken} `null` if the URL is invalid, otherwise the id and the token * @returns {?WebhookDataIdWithToken} `null` if the URL is invalid, otherwise the id and the token
*/ */
function parseWebhookURL(url) { function parseWebhookURL(url) {
const matches = const matches =

View File

@@ -7,8 +7,8 @@ const { setTimeout: sleep } = require('node:timers/promises');
const util = require('node:util'); const util = require('node:util');
const { GatewayIntentBits } = require('discord-api-types/v10'); const { GatewayIntentBits } = require('discord-api-types/v10');
const { fetch } = require('undici'); const { fetch } = require('undici');
const { Client, MessageAttachment, Embed } = require('../src/index.js');
const { owner, token, webhookChannel, webhookToken } = require('./auth.js'); const { owner, token, webhookChannel, webhookToken } = require('./auth.js');
const { Client, MessageAttachment, Embed, WebhookClient } = require('../src/index.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] }); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
@@ -84,30 +84,26 @@ client.on('messageCreate', async message => {
if (message.author.id !== owner) return; if (message.author.id !== owner) return;
const match = message.content.match(/^do (.+)$/); const match = message.content.match(/^do (.+)$/);
const hooks = [ const hooks = [
{ type: 'WebhookClient', hook: new WebhookClient({ id: webhookChannel, token: webhookToken }) },
{ type: 'TextChannel#fetchWebhooks', hook: await message.channel.fetchWebhooks().then(x => x.first()) }, { type: 'TextChannel#fetchWebhooks', hook: await message.channel.fetchWebhooks().then(x => x.first()) },
{ type: 'Guild#fetchWebhooks', hook: await message.guild.fetchWebhooks().then(x => x.first()) }, { type: 'Guild#fetchWebhooks', hook: await message.guild.fetchWebhooks().then(x => x.first()) },
]; ];
if (match?.[1] === 'it') { if (match?.[1] === 'it') {
/* eslint-disable no-await-in-loop */
for (const { type, hook } of hooks) { for (const { type, hook } of hooks) {
for (const [i, test] of tests.entries()) { for (const [i, test] of tests.entries()) {
await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``);
await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); await test(message, hook).catch(error => message.channel.send(`Error!\n\`\`\`\n${error}\`\`\``));
await sleep(1_000); await sleep(1_000);
} }
} }
/* eslint-enable no-await-in-loop */
} else if (match) { } else if (match) {
const n = parseInt(match[1]) || 0; const n = Number.parseInt(match[1]) || 0;
const test = tests.slice(n)[0]; const test = tests.slice(n)[0];
const i = tests.indexOf(test); const i = tests.indexOf(test);
/* eslint-disable no-await-in-loop */
for (const { type, hook } of hooks) { for (const { type, hook } of hooks) {
await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``);
await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); await test(message, hook).catch(error => message.channel.send(`Error!\n\`\`\`\n${error}\`\`\``));
} }
/* eslint-enable no-await-in-loop */
} }
}); });

View File

@@ -493,11 +493,11 @@ export abstract class Base {
} }
export class BaseClient<Events extends {}> extends AsyncEventEmitter<Events> implements AsyncDisposable { export class BaseClient<Events extends {}> extends AsyncEventEmitter<Events> implements AsyncDisposable {
public constructor(options?: ClientOptions | WebhookClientOptions); public constructor(options?: ClientOptions);
private decrementMaxListeners(): void; private decrementMaxListeners(): void;
private incrementMaxListeners(): void; private incrementMaxListeners(): void;
public options: ClientOptions | WebhookClientOptions; public options: ClientOptions;
public rest: REST; public rest: REST;
public destroy(): void; public destroy(): void;
public toJSON(...props: Record<string, boolean | string>[]): unknown; public toJSON(...props: Record<string, boolean | string>[]): unknown;
@@ -3742,7 +3742,7 @@ export function fetchRecommendedShardCount(token: string, options?: FetchRecomme
export function flatten(obj: unknown, ...props: Record<string, boolean | string>[]): unknown; export function flatten(obj: unknown, ...props: Record<string, boolean | string>[]): unknown;
export function parseEmoji(text: string): PartialEmoji | null; export function parseEmoji(text: string): PartialEmoji | null;
export function parseWebhookURL(url: string): WebhookClientDataIdWithToken | null; export function parseWebhookURL(url: string): WebhookDataIdWithToken | null;
export function resolveColor(color: ColorResolvable): number; export function resolveColor(color: ColorResolvable): number;
export function resolveSKUId(resolvable: SKUResolvable): Snowflake | null; export function resolveSKUId(resolvable: SKUResolvable): Snowflake | null;
export function verifyString(data: string, error?: typeof Error, errorMessage?: string, allowEmpty?: boolean): string; export function verifyString(data: string, error?: typeof Error, errorMessage?: string, allowEmpty?: boolean): string;
@@ -3828,7 +3828,7 @@ export class Webhook<Type extends WebhookType = WebhookType> {
public readonly client: Client; public readonly client: Client;
public guildId: Snowflake; public guildId: Snowflake;
public name: string; public name: string;
public owner: Type extends WebhookType.Incoming ? APIUser | User | null : APIUser | User; public owner: Type extends WebhookType.Incoming ? User | null : User;
public sourceGuild: Type extends WebhookType.ChannelFollower ? APIPartialGuild | Guild : null; public sourceGuild: Type extends WebhookType.ChannelFollower ? APIPartialGuild | Guild : null;
public sourceChannel: Type extends WebhookType.ChannelFollower ? AnnouncementChannel | APIPartialChannel : null; public sourceChannel: Type extends WebhookType.ChannelFollower ? AnnouncementChannel | APIPartialChannel : null;
public token: Type extends WebhookType.Incoming public token: Type extends WebhookType.Incoming
@@ -3847,7 +3847,7 @@ export class Webhook<Type extends WebhookType = WebhookType> {
| VoiceChannel | VoiceChannel
| null; | null;
public isUserCreated(): this is Webhook<WebhookType.Incoming> & { public isUserCreated(): this is Webhook<WebhookType.Incoming> & {
owner: APIUser | User; owner: User;
}; };
public isApplicationCreated(): this is Webhook<WebhookType.Application>; public isApplicationCreated(): this is Webhook<WebhookType.Application>;
public isIncoming(): this is Webhook<WebhookType.Incoming>; public isIncoming(): this is Webhook<WebhookType.Incoming>;
@@ -3861,20 +3861,6 @@ export class Webhook<Type extends WebhookType = WebhookType> {
public send(options: MessagePayload | WebhookMessageCreateOptions | string): Promise<Message<true>>; public send(options: MessagePayload | WebhookMessageCreateOptions | string): Promise<Message<true>>;
} }
export interface WebhookClient extends WebhookFields, BaseClient<{}> {}
export class WebhookClient extends BaseClient<{}> {
public constructor(data: WebhookClientData, options?: WebhookClientOptions);
public readonly client: this;
public options: WebhookClientOptions;
public token: string;
public editMessage(
message: MessageResolvable,
options: MessagePayload | WebhookMessageEditOptions | string,
): Promise<APIMessage>;
public fetchMessage(message: Snowflake, options?: WebhookFetchMessageOptions): Promise<APIMessage>;
public send(options: MessagePayload | WebhookMessageCreateOptions | string): Promise<APIMessage>;
}
export class Widget extends Base { export class Widget extends Base {
private constructor(client: Client<true>, data: APIGuildWidget); private constructor(client: Client<true>, data: APIGuildWidget);
private _patch(data: APIGuildWidget): void; private _patch(data: APIGuildWidget): void;
@@ -4064,7 +4050,6 @@ export enum DiscordjsErrorCodes {
WebhookMessage = 'WebhookMessage', WebhookMessage = 'WebhookMessage',
WebhookTokenUnavailable = 'WebhookTokenUnavailable', WebhookTokenUnavailable = 'WebhookTokenUnavailable',
WebhookURLInvalid = 'WebhookURLInvalid',
WebhookApplication = 'WebhookApplication', WebhookApplication = 'WebhookApplication',
MessageReferenceMissing = 'MessageReferenceMissing', MessageReferenceMissing = 'MessageReferenceMissing',
@@ -5576,7 +5561,8 @@ export interface ClientFetchInviteOptions {
withCounts?: boolean; withCounts?: boolean;
} }
export interface ClientOptions extends WebhookClientOptions { export interface ClientOptions {
allowedMentions?: MessageMentionOptions;
closeTimeout?: number; closeTimeout?: number;
enforceNonce?: boolean; enforceNonce?: boolean;
failIfNotExists?: boolean; failIfNotExists?: boolean;
@@ -5585,6 +5571,7 @@ export interface ClientOptions extends WebhookClientOptions {
makeCache?: CacheFactory; makeCache?: CacheFactory;
partials?: readonly Partials[]; partials?: readonly Partials[];
presence?: PresenceData; presence?: PresenceData;
rest?: Partial<RESTOptions>;
sweepers?: SweeperOptions; sweepers?: SweeperOptions;
waitGuildTimeout?: number; waitGuildTimeout?: number;
ws?: Partial<WebSocketManagerOptions>; ws?: Partial<WebSocketManagerOptions>;
@@ -6873,8 +6860,7 @@ export type MessageTarget =
| Message | Message
| MessageManager | MessageManager
| TextBasedChannel | TextBasedChannel
| Webhook<WebhookType.Incoming> | Webhook<WebhookType.Incoming>;
| WebhookClient;
export interface MultipleShardRespawnOptions { export interface MultipleShardRespawnOptions {
respawnDelay?: number; respawnDelay?: number;
@@ -7248,22 +7234,11 @@ export interface VoiceStateEditOptions {
suppressed?: boolean; suppressed?: boolean;
} }
export type WebhookClientData = WebhookClientDataIdWithToken | WebhookClientDataURL; export interface WebhookDataIdWithToken {
export interface WebhookClientDataIdWithToken {
id: Snowflake; id: Snowflake;
token: string; token: string;
} }
export interface WebhookClientDataURL {
url: string;
}
export interface WebhookClientOptions {
allowedMentions?: MessageMentionOptions;
rest?: Partial<RESTOptions>;
}
export interface WebhookDeleteOptions { export interface WebhookDeleteOptions {
reason?: string; reason?: string;
token?: string; token?: string;

View File

@@ -10,7 +10,6 @@ import type {
APIInteractionDataResolvedChannel, APIInteractionDataResolvedChannel,
APIInteractionDataResolvedGuildMember, APIInteractionDataResolvedGuildMember,
APIInteractionGuildMember, APIInteractionGuildMember,
APIMessage,
APIPartialChannel, APIPartialChannel,
APIPartialGuild, APIPartialGuild,
APIRole, APIRole,
@@ -231,7 +230,6 @@ import {
UserSelectMenuComponent, UserSelectMenuComponent,
UserSelectMenuInteraction, UserSelectMenuInteraction,
Webhook, Webhook,
WebhookClient,
} from './index.js'; } from './index.js';
// Test type transformation: // Test type transformation:
@@ -2683,7 +2681,6 @@ expectType<UserMention>(user.toString());
expectType<UserMention>(guildMember.toString()); expectType<UserMention>(guildMember.toString());
declare const webhook: Webhook; declare const webhook: Webhook;
declare const webhookClient: WebhookClient;
declare const interactionWebhook: InteractionWebhook; declare const interactionWebhook: InteractionWebhook;
declare const snowflake: Snowflake; declare const snowflake: Snowflake;
@@ -2692,10 +2689,6 @@ expectType<Promise<Message<true>>>(webhook.editMessage(snowflake, 'content'));
expectType<Promise<Message<true>>>(webhook.fetchMessage(snowflake)); expectType<Promise<Message<true>>>(webhook.fetchMessage(snowflake));
expectType<Promise<Webhook>>(webhook.edit({ name: 'name' })); expectType<Promise<Webhook>>(webhook.edit({ name: 'name' }));
expectType<Promise<APIMessage>>(webhookClient.send('content'));
expectType<Promise<APIMessage>>(webhookClient.editMessage(snowflake, 'content'));
expectType<Promise<APIMessage>>(webhookClient.fetchMessage(snowflake));
expectType<Client<true>>(interactionWebhook.client); expectType<Client<true>>(interactionWebhook.client);
expectType<Promise<Message>>(interactionWebhook.send('content')); expectType<Promise<Message>>(interactionWebhook.send('content'));
expectType<Promise<Message>>(interactionWebhook.editMessage(snowflake, 'content')); expectType<Promise<Message>>(interactionWebhook.editMessage(snowflake, 'content'));