Buttons
Answer a button click with ButtonHandler, reading the values its custom id carries. Covers rewriting the message the click came from, and one handler serving several buttons.
A click arrives as its own interaction carrying the custom id your component minted. ButtonHandler decodes that id and puts the values on this.params.
import {
ButtonHandler,
ButtonRoute,
CustomId
} from '@seedcord/gateway';
// in your project, write this in the component file that builds the button
const AppealId = new CustomId('appeal').snowflake('userId');
@ButtonRoute(AppealId)
export class Appeal extends ButtonHandler<[typeof AppealId]> {
public async execute(): Promise<void> {
const { userId } = this.params;
await this.reply(`Appeal filed for <@${userId}>.`);
}
}@ButtonRoute takes the CustomId your component used, and the generic lists the same one as a tuple. Passing the decorator one definition and the generic another is a compile error.
A click is an interaction like any other, so every reply method works here.
Gateway only
A gateway ButtonHandler takes a second type argument, discord.js's CacheType, which defaults to 'cached'. That default types this.event.member as a GuildMember, and seedcord doesn't check it at runtime. A button in a DM has no member, so pass 'raw' or CacheType for a handler that can get one. An http ButtonHandler takes only the route tuple.
Rewriting the source message
A reply to a click posts a new message under the old one. The old buttons stay there, so someone can click them again.
this.update() rewrites the message the button came from. It takes the same response shape every reply method takes, so a new components list replaces the old buttons.
const { userId } = this.params;
await this.update({
components: [
new ReviewCard(userId, 'under review').component
]
});
Discord shows "This interaction failed" on a click that nobody answers within three seconds. For a click that changes nothing on the message, this.deferUpdate() answers it and leaves the message unchanged.
Gateway and http differ
update() resolves to a discord.js Message on gateway and an
APIMessage on http.
One handler, several buttons
Approve and reject sit next to each other on the same message, and one handler can serve both. Pass every id to the decorator, list them in the generic, then branch with this.match.
@ButtonRoute<readonly [CustomId<"approve", Record<"userId", CustomIdField<string>>>, CustomId<"reject", Record<"userId", CustomIdField<string>> & Record<"reason", CustomIdField<string>>>]>(defs_0: CustomId<"approve", Record<"userId", CustomIdField<string>>>, defs_1: CustomId<"reject", Record<"userId", CustomIdField<string>> & Record<"reason", CustomIdField<string>>>): <TCtor>(constructor: AssertComponentRoute<...>) => voidButtonRoute(const ApproveId: CustomId<
"approve",
Record<"userId", CustomIdField<string>>
>
ApproveId, const RejectId: CustomId<
"reject",
Record<"userId", CustomIdField<string>> &
Record<"reason", CustomIdField<string>>
>
RejectId)
export class class ReviewReview extends class ButtonHandler<
Defs extends readonly AnyCustomId[],
Cache extends CacheType = "cached"
>
ButtonHandler<
[typeof const ApproveId: CustomId<
"approve",
Record<"userId", CustomIdField<string>>
>
ApproveId, typeof const RejectId: CustomId<
"reject",
Record<"userId", CustomIdField<string>> &
Record<"reason", CustomIdField<string>>
>
RejectId]
> {
public async Review.execute(): Promise<void>execute(): interface Promise<T>Promise<void> {
await this.ComponentHandler<ButtonInteraction<"cached">, [CustomId<"approve", Record<"userId", CustomIdField<string>>>, CustomId<"reject", Record<...> & Record<...>>]>.match<SentMessage>(arms: MatchArms<[CustomId<"approve", Record<"userId", CustomIdField<string>>>, CustomId<"reject", Record<"userId", CustomIdField<string>> & Record<"reason", CustomIdField<string>>>], SentMessage>): Promise<SentMessage>match({
approve: (params: DecodedParams<Record<"userId", CustomIdField<string>>>) => Promisable<SentMessage>approve: ({ userId: stringuserId }) =>
this.ComponentHandler<ButtonInteraction<"cached">, [CustomId<"approve", Record<"userId", CustomIdField<string>>>, CustomId<"reject", Record<...> & Record<...>>]>.update(response: GatewayReplyResponse | string): Promise<SentMessage>update(`Approved <@${userId: stringuserId}>.`),
reject: (params: DecodedParams<Record<"userId", CustomIdField<string>> & Record<"reason", CustomIdField<string>>>) => Promisable<SentMessage>reject: ({ userId: stringuserId, reason: stringreason }) =>
this.ComponentHandler<ButtonInteraction<"cached">, [CustomId<"approve", Record<"userId", CustomIdField<string>>>, CustomId<"reject", Record<...> & Record<...>>]>.update(response: GatewayReplyResponse | string): Promise<SentMessage>update(`Rejected <@${userId: stringuserId}>. ${reason: stringreason}`)
});
}
}You key each arm by its route's prefix, and each one receives that route's own values. reject reads a reason that approve never declared. Both arms pass update a plain string, which seedcord wraps in one text component. Leave an arm out and the build stops, since the compiler requires one per registered prefix.
Keep two buttons in one handler when both arms need the same code first, like a check that the clicker is staff. Two buttons that share nothing belong in two handlers, where each file holds only the code its own button runs.
Warning
this.params is never on a handler registered for several routes, since the decoded shape depends on which button someone clicked. this.match is the only read there.
Decoding runs once, before any arm, so a wire from an older shape throws before your code executes.