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 {'WebhookTokenUnavailable'} WebhookTokenUnavailable
* @property {'WebhookURLInvalid'} WebhookURLInvalid
* @property {'WebhookApplication'} WebhookApplication
*
* @property {'MessageReferenceMissing'} MessageReferenceMissing
@@ -212,7 +211,6 @@ const keys = [
'WebhookMessage',
'WebhookTokenUnavailable',
'WebhookURLInvalid',
'WebhookApplication',
'MessageReferenceMissing',

View File

@@ -82,7 +82,6 @@ const Messages = {
[ErrorCodes.WebhookMessage]: 'The message was not sent by a webhook.',
[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.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.ShardClientUtil = require('./sharding/ShardClientUtil.js').ShardClientUtil;
exports.ShardingManager = require('./sharding/ShardingManager.js').ShardingManager;
exports.WebhookClient = require('./client/WebhookClient.js').WebhookClient;
// Errors
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}
* @readonly
*/
get isWebhook() {
const { Webhook } = require('./Webhook.js');
const { WebhookClient } = require('../client/WebhookClient.js');
return this.target instanceof Webhook || this.target instanceof WebhookClient;
return this.target instanceof Webhook;
}
/**
@@ -302,7 +301,7 @@ exports.MessagePayload = MessagePayload;
/**
* A target for a message.
*
* @typedef {TextBasedChannels|ChannelManager|Webhook|WebhookClient|BaseInteraction|InteractionWebhook|
* @typedef {TextBasedChannels|ChannelManager|Webhook|BaseInteraction|InteractionWebhook|
* Message|MessageManager} MessageTarget
*/

View File

@@ -95,9 +95,9 @@ class 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 {
this.owner ??= null;
}
@@ -119,7 +119,7 @@ class Webhook {
*
* @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 {
this.sourceGuild ??= null;
}
@@ -130,7 +130,7 @@ class Webhook {
*
* @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 {
this.sourceChannel ??= null;
}
@@ -248,7 +248,6 @@ class Webhook {
auth: false,
});
if (!this.client.channels) return data;
return (
this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ??
new (getMessage())(this.client, data)
@@ -345,7 +344,6 @@ class Webhook {
auth: false,
});
if (!this.client.channels) return data;
return (
this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ??
new (getMessage())(this.client, data)
@@ -384,10 +382,7 @@ class Webhook {
},
);
const channelManager = this.client.channels;
if (!channelManager) return data;
const messageManager = channelManager.cache.get(data.channel_id)?.messages;
const messageManager = this.client.channels.cache.get(data.channel_id)?.messages;
if (!messageManager) return new (getMessage())(this.client, data);
const existing = messageManager.cache.get(data.id);

View File

@@ -469,11 +469,19 @@ function cleanCodeBlockContent(text) {
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.
*
* @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) {
const matches =

View File

@@ -7,8 +7,8 @@ const { setTimeout: sleep } = require('node:timers/promises');
const util = require('node:util');
const { GatewayIntentBits } = require('discord-api-types/v10');
const { fetch } = require('undici');
const { Client, MessageAttachment, Embed } = require('../src/index.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] });
@@ -84,30 +84,26 @@ client.on('messageCreate', async message => {
if (message.author.id !== owner) return;
const match = message.content.match(/^do (.+)$/);
const hooks = [
{ type: 'WebhookClient', hook: new WebhookClient({ id: webhookChannel, token: webhookToken }) },
{ type: 'TextChannel#fetchWebhooks', hook: await message.channel.fetchWebhooks().then(x => x.first()) },
{ type: 'Guild#fetchWebhooks', hook: await message.guild.fetchWebhooks().then(x => x.first()) },
];
if (match?.[1] === 'it') {
/* eslint-disable no-await-in-loop */
for (const { type, hook } of hooks) {
for (const [i, test] of tests.entries()) {
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);
}
}
/* eslint-enable no-await-in-loop */
} else if (match) {
const n = parseInt(match[1]) || 0;
const n = Number.parseInt(match[1]) || 0;
const test = tests.slice(n)[0];
const i = tests.indexOf(test);
/* eslint-disable no-await-in-loop */
for (const { type, hook } of hooks) {
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 {
public constructor(options?: ClientOptions | WebhookClientOptions);
public constructor(options?: ClientOptions);
private decrementMaxListeners(): void;
private incrementMaxListeners(): void;
public options: ClientOptions | WebhookClientOptions;
public options: ClientOptions;
public rest: REST;
public destroy(): void;
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 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 resolveSKUId(resolvable: SKUResolvable): Snowflake | null;
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 guildId: Snowflake;
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 sourceChannel: Type extends WebhookType.ChannelFollower ? AnnouncementChannel | APIPartialChannel : null;
public token: Type extends WebhookType.Incoming
@@ -3847,7 +3847,7 @@ export class Webhook<Type extends WebhookType = WebhookType> {
| VoiceChannel
| null;
public isUserCreated(): this is Webhook<WebhookType.Incoming> & {
owner: APIUser | User;
owner: User;
};
public isApplicationCreated(): this is Webhook<WebhookType.Application>;
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>>;
}
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 {
private constructor(client: Client<true>, data: APIGuildWidget);
private _patch(data: APIGuildWidget): void;
@@ -4064,7 +4050,6 @@ export enum DiscordjsErrorCodes {
WebhookMessage = 'WebhookMessage',
WebhookTokenUnavailable = 'WebhookTokenUnavailable',
WebhookURLInvalid = 'WebhookURLInvalid',
WebhookApplication = 'WebhookApplication',
MessageReferenceMissing = 'MessageReferenceMissing',
@@ -5576,7 +5561,8 @@ export interface ClientFetchInviteOptions {
withCounts?: boolean;
}
export interface ClientOptions extends WebhookClientOptions {
export interface ClientOptions {
allowedMentions?: MessageMentionOptions;
closeTimeout?: number;
enforceNonce?: boolean;
failIfNotExists?: boolean;
@@ -5585,6 +5571,7 @@ export interface ClientOptions extends WebhookClientOptions {
makeCache?: CacheFactory;
partials?: readonly Partials[];
presence?: PresenceData;
rest?: Partial<RESTOptions>;
sweepers?: SweeperOptions;
waitGuildTimeout?: number;
ws?: Partial<WebSocketManagerOptions>;
@@ -6873,8 +6860,7 @@ export type MessageTarget =
| Message
| MessageManager
| TextBasedChannel
| Webhook<WebhookType.Incoming>
| WebhookClient;
| Webhook<WebhookType.Incoming>;
export interface MultipleShardRespawnOptions {
respawnDelay?: number;
@@ -7248,22 +7234,11 @@ export interface VoiceStateEditOptions {
suppressed?: boolean;
}
export type WebhookClientData = WebhookClientDataIdWithToken | WebhookClientDataURL;
export interface WebhookClientDataIdWithToken {
export interface WebhookDataIdWithToken {
id: Snowflake;
token: string;
}
export interface WebhookClientDataURL {
url: string;
}
export interface WebhookClientOptions {
allowedMentions?: MessageMentionOptions;
rest?: Partial<RESTOptions>;
}
export interface WebhookDeleteOptions {
reason?: string;
token?: string;

View File

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