diff --git a/.changeset/gateway-crosspost-create-message.md b/.changeset/gateway-crosspost-create-message.md new file mode 100644 index 00000000..3745fb68 --- /dev/null +++ b/.changeset/gateway-crosspost-create-message.md @@ -0,0 +1,5 @@ +--- +"@wolfstar/plugin-gateway": minor +--- + +`channel.messages.crosspost` takes a message or its ID, and `client.channels.createMessage(channel, options)` sends a message to a channel or its ID, like discord.js. diff --git a/packages/plugin-gateway/README.md b/packages/plugin-gateway/README.md index 32d8e023..aada338e 100644 --- a/packages/plugin-gateway/README.md +++ b/packages/plugin-gateway/README.md @@ -1233,9 +1233,9 @@ with `Partials.Message`, else `undefined`. Narrow a `Message | PartialMessage` w `GuildMessageManager`, `DMMessageManager`, and `PartialGroupDMMessageManager`. Its guild-only methods (`crosspost`, `bulkDelete`, `startThread`) throw `ChannelMessagesGuildOnly` before any request in a direct message or group direct message; the rest (`fetch`, `send`, `edit`, `delete`, `pin`, `react`, ...) is the -same everywhere. `crosspost` takes a message ID, not a `MessageResolvable`, like the other methods of the -manager. discord.js's `client.channels.createMessage(channel, options)` is `client.messages.send(channelId, -options)` (or `channel.send(options)`) here. +same everywhere. `crosspost` takes a message or its ID, and `client.channels.createMessage(channel, options)` sends +a message to a channel or its ID, like discord.js (`client.messages.send(channelId, options)` and +`channel.send(options)` do the same). `Message` is generic like discord.js's: `Message` has a `guildId` string and guild-text-based `channel`s, and `inGuild()` narrows to it. `reply`, `edit`, and the other actions resolve to messages whose `channel` is never a group diff --git a/packages/plugin-gateway/src/managers/ChannelManager.ts b/packages/plugin-gateway/src/managers/ChannelManager.ts index 7d691992..60feea45 100644 --- a/packages/plugin-gateway/src/managers/ChannelManager.ts +++ b/packages/plugin-gateway/src/managers/ChannelManager.ts @@ -30,6 +30,7 @@ import { StageChannel } from "../structures/channels/StageChannel.js"; import { TextChannel } from "../structures/channels/TextChannel.js"; import { VoiceChannel } from "../structures/channels/VoiceChannel.js"; import type { Guild } from "../structures/guilds/Guild.js"; +import type { Message } from "../structures/messages/Message.js"; import { StageInstance } from "../structures/stageInstances/StageInstance.js"; import { bindClient } from "../structures/Structure.js"; import { whenAll, type Cache } from "../util/cache.js"; @@ -38,6 +39,8 @@ import { CachedManager, type AddOptions } from "./CachedManager.js"; import { PermissionOverwriteManager } from "./PermissionOverwriteManager.js"; import { GatewayRangeError, GatewayTypeError } from "../errors/GatewayError.js"; import { GuildChannelInfoTimeoutError } from "../util/errors.js"; +import type { MessageCreateOptions, MessagePayloadResolvable } from "../util/messages.js"; +import type { TextBasedChannelResolvable } from "../types.js"; import { shardIdOf } from "../util/shards.js"; /** @@ -566,6 +569,21 @@ export class ChannelManager extends CachedManager<"channels", AnyChannel, [chann } as GatewayDispatchPayload); } + /** + * Sends a message to a channel, like discord.js's `ChannelManager#createMessage`. The same as `client.messages.send` + * and `channel.send`. + * + * @param channel The channel, or its ID. + * @param options The message, or its content. + * @throws {@link GatewayTypeError} `IdUnresolvable` when `channel` holds no ID. + */ + public createMessage( + channel: TextBasedChannelResolvable, + options: MessagePayloadResolvable, + ): Promise { + return this.client.messages.send(resolveId(channel), options); + } + /** * Gets the permission overwrites of a channel, cache first. * diff --git a/packages/plugin-gateway/src/managers/ChannelMessageManager.ts b/packages/plugin-gateway/src/managers/ChannelMessageManager.ts index a579a3a9..f5e2b6df 100644 --- a/packages/plugin-gateway/src/managers/ChannelMessageManager.ts +++ b/packages/plugin-gateway/src/managers/ChannelMessageManager.ts @@ -19,6 +19,7 @@ import type { } from "../util/messages.js"; import { GatewayError } from "../errors/GatewayError.js"; import { scopeCache, type ScopedCache } from "../util/cache.js"; +import { resolveId } from "../util/channels.js"; import { BaseManager } from "./BaseManager.js"; /** @@ -119,12 +120,13 @@ export class ChannelMessageManager extends BaseManager { * Publishes a message of an announcement channel to the channels following it, like discord.js's * `GuildMessageManager#crosspost`. * - * @param messageId The ID of the message. + * @param message The message, or its ID. * @throws {@link GatewayError} `ChannelMessagesGuildOnly` in a direct message. + * @throws {@link GatewayTypeError} `IdUnresolvable` when `message` holds no ID. */ - public async crosspost(messageId: string): Promise { + public async crosspost(message: MessageResolvable): Promise { this.assertGuild("crosspost"); - return this.client.messages.crosspost(this.channelId, messageId); + return this.client.messages.crosspost(this.channelId, resolveId(message)); } /** diff --git a/packages/plugin-gateway/tests/dm-messages.test.ts b/packages/plugin-gateway/tests/dm-messages.test.ts index a423b59a..7de14b77 100644 --- a/packages/plugin-gateway/tests/dm-messages.test.ts +++ b/packages/plugin-gateway/tests/dm-messages.test.ts @@ -206,6 +206,36 @@ describe("guild-only paths of the messages of a direct message", () => { expect(message.guildId).toBe(guildId); }); + test("GIVEN a message structure THEN crosspost publishes it like its ID", async () => { + createClient(); + const post = vi + .spyOn(container.rest, "post") + .mockResolvedValue(dmMessage({ channel_id: guildChannelId, guild_id: guildId })); + const message = await guildChannel().messages.crosspost(messageId); + + await guildChannel().messages.crosspost(message); + + expect(post).toHaveBeenNthCalledWith( + 2, + Routes.channelMessageCrosspost(guildChannelId, messageId), + expect.anything(), + ); + }); + + test("GIVEN a channel or its ID THEN createMessage sends the message to it", async () => { + const client = createClient(); + const post = vi.spyOn(container.rest, "post").mockResolvedValue(dmMessage()); + + const byId = await client.channels.createMessage(dmId, "hi"); + await client.channels.createMessage(dmChannel(), { content: "hi" }); + + expect(byId.channelId).toBe(dmId); + expect(post.mock.calls.map(([route]) => route)).toEqual([ + Routes.channelMessages(dmId), + Routes.channelMessages(dmId), + ]); + }); + test("GIVEN a guild channel THEN bulkDelete and startThread are not refused", async () => { createClient(); vi.spyOn(container.rest, "post").mockResolvedValue({}); diff --git a/packages/plugin-gateway/tests/types/thread-and-message-managers.ts b/packages/plugin-gateway/tests/types/thread-and-message-managers.ts index 4d856ef9..93b88848 100644 --- a/packages/plugin-gateway/tests/types/thread-and-message-managers.ts +++ b/packages/plugin-gateway/tests/types/thread-and-message-managers.ts @@ -61,15 +61,27 @@ if (options.message === undefined) { void type; } -// `crosspost` takes the message's ID and resolves to the published message, like `GuildMessageManager#crosspost` -// (which takes a `MessageResolvable`). +// `crosspost` takes a message or its ID and resolves to the published message, like `GuildMessageManager#crosspost`. declare const messages: ChannelMessageManager; +declare const message: Message; export const published: Promise = messages.crosspost("1"); +export const publishedByStructure: Promise = messages.crosspost(message); export const publishedByClient: Promise = client.messages.crosspost("1", "2"); -// @ts-expect-error the ID is a string +// @ts-expect-error a number is not a message void messages.crosspost(1); -// `client.messages.send` stands in for `ChannelManager#createMessage`: a string, options, or a payload. +// `ChannelManager#createMessage` takes a channel or its ID, and a string, options, or a payload. +export const created: Promise = client.channels.createMessage("1", "hi"); +export const createdByStructure: Promise = client.channels.createMessage(text, { + content: "hi", +}); +export const createdPayload: Promise = client.channels.createMessage("1", payload); +// @ts-expect-error a number is not a message +void client.channels.createMessage("1", 1); +// @ts-expect-error a number is not a channel +void client.channels.createMessage(1, "hi"); + +// `client.messages.send` does the same with an ID. export const sent: Promise = client.messages.send("1", "hi"); export const sentOptions: Promise = client.messages.send("1", { content: "hi" }); export const sentPayload: Promise = client.messages.send("1", payload);