Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gateway-crosspost-create-message.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions packages/plugin-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<true>` 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
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-gateway/src/managers/ChannelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

/**
Expand Down Expand Up @@ -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<MessageCreateOptions>,
): Promise<Message> {
return this.client.messages.send(resolveId(channel), options);
}

/**
* Gets the permission overwrites of a channel, cache first.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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<Message> {
public async crosspost(message: MessageResolvable): Promise<Message> {
this.assertGuild("crosspost");
return this.client.messages.crosspost(this.channelId, messageId);
return this.client.messages.crosspost(this.channelId, resolveId(message));
}

/**
Expand Down
30 changes: 30 additions & 0 deletions packages/plugin-gateway/tests/dm-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message> = messages.crosspost("1");
export const publishedByStructure: Promise<Message> = messages.crosspost(message);
export const publishedByClient: Promise<Message> = 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<Message> = client.channels.createMessage("1", "hi");
export const createdByStructure: Promise<Message> = client.channels.createMessage(text, {
content: "hi",
});
export const createdPayload: Promise<Message> = 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<Message> = client.messages.send("1", "hi");
export const sentOptions: Promise<Message> = client.messages.send("1", { content: "hi" });
export const sentPayload: Promise<Message> = client.messages.send("1", payload);
Expand Down
Loading