From a8220036c0f20a4af87bfc86ceec0ba88f85ecf5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 18 Feb 2023 07:48:04 -0800 Subject: [PATCH] feat: improve the hello-action UI Signed-off-by: Raymond Feng --- src/actions/hello-action.controller.ts | 171 +++++++++++++++++-------- 1 file changed, 121 insertions(+), 50 deletions(-) diff --git a/src/actions/hello-action.controller.ts b/src/actions/hello-action.controller.ts index e11a754..11df541 100644 --- a/src/actions/hello-action.controller.ts +++ b/src/actions/hello-action.controller.ts @@ -20,11 +20,20 @@ import { InteractionType, MessageFlags, RESTPatchAPIWebhookWithTokenMessageJSONBody, - RESTPostAPIWebhookWithTokenJSONBody, } from '@collabland/discord'; import {MiniAppManifest} from '@collabland/models'; import {BindingScope, injectable} from '@loopback/core'; import {api} from '@loopback/rest'; +import { + ActionRowBuilder, + APIInteraction, + APIMessageComponentInteraction, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + InteractionResponseType, + MessageActionRowComponentBuilder, +} from 'discord.js'; /** * HelloActionController is a LoopBack REST API controller that exposes endpoints @@ -34,9 +43,10 @@ import {api} from '@loopback/rest'; scope: BindingScope.SINGLETON, }) @api({basePath: '/hello-action'}) // Set the base path to `/hello-action` -export class HelloActionController extends BaseDiscordActionController { +export class HelloActionController extends BaseDiscordActionController { /** - * Expose metadata for the action + * Expose metadata for the action. The return value is used by Collab.Land `/test-flight` command + * or marketplace to list this action as a miniapp. * @returns */ async getMetadata(): Promise { @@ -69,66 +79,121 @@ export class HelloActionController extends BaseDiscordActionController, + ): Promise { + switch (request.data.name) { + case 'hello-action': { + /** + * Get the value of `your-name` argument for `/hello-action` + */ + const yourName = getCommandOptionValue(request, 'your-name'); + const message = `Hello, ${ + yourName ?? request.user?.username ?? 'World' + }!`; + + const appId = request.application_id; + const response: APIInteractionResponse = { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: message, + embeds: [ + new EmbedBuilder() + .setTitle('Hello Action') + .setColor('#f5c248') + .setAuthor({ + name: 'Collab.Land', + url: 'https://collab.land', + iconURL: `https://cdn.discordapp.com/app-icons/${appId}/8a814f663844a69d22344dc8f4983de6.png`, + }) + .setDescription( + 'This is demo Collab.Land action that adds `/hello-action` ' + + 'command to your Discord server. Please click the `Count down` button below to proceed.', + ) + .setURL('https://github.com/abridged/collabland-hello-action/') + .toJSON(), + ], + components: [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(`Count down`) + .setStyle(ButtonStyle.Primary) + // Set the custom id to start with `hello-action:` + .setCustomId('hello-action:count-button'), + ) + .toJSON(), + ], + flags: MessageFlags.Ephemeral, + }, + }; + + // Return the 1st response to Discord + return response; + } + default: { + return buildSimpleResponse( + `Slash command ${request.data.name} is not implemented.`, + ); + } + } + } + + /** + * Handle the Discord message components including buttons * @param interaction - Discord interaction with Collab.Land action context * @returns - Discord interaction response */ - protected async handle( - interaction: DiscordActionRequest, + protected async handleMessageComponent( + request: DiscordActionRequest, ): Promise { - /** - * Get the value of `your-name` argument for `/hello-action` - */ - const yourName = getCommandOptionValue(interaction, 'your-name'); - const message = `Hello, ${ - yourName ?? interaction.user?.username ?? 'World' - }!`; - /** - * Build a simple Discord message private to the user - */ - const response: APIInteractionResponse = buildSimpleResponse(message, true); - /** - * Allow advanced followup messages - */ - this.followup(interaction, message).catch(err => { - console.error( - 'Fail to send followup message to interaction %s: %O', - interaction.id, - err, - ); - }); - // Return the 1st response to Discord - return response; + switch (request.data.custom_id) { + case 'hello-action:count-button': { + // Run count down in the background after 1 second + this.countDown(request).catch(err => { + console.error( + 'Fail to send followup message to interaction %s: %O', + request.id, + err, + ); + }); + } + } + // Instruct Discord that we'll edit the original message later on + return { + type: InteractionResponseType.DeferredMessageUpdate, + }; } - private async followup( - request: DiscordActionRequest, - message: string, + /** + * Run a countdown by updating the original message content + * @param request + */ + private async countDown( + request: DiscordActionRequest, ) { - const callback = request.actionContext?.callbackUrl; - if (callback != null) { - const followupMsg: RESTPostAPIWebhookWithTokenJSONBody = { - content: `Follow-up: **${message}**`, - flags: MessageFlags.Ephemeral, + await sleep(1000); + const message = request.message.content; + // 5 seconds count down + for (let i = 5; i > 0; i--) { + const updated: RESTPatchAPIWebhookWithTokenMessageJSONBody = { + content: `[${i}s]: **${message}**`, + components: [], // Remove the `Count down` button }; + await this.editMessage(request, updated, request.message.id); await sleep(1000); - let msg = await this.followupMessage(request, followupMsg); - await sleep(1000); - // 5 seconds count down - for (let i = 5; i > 0; i--) { - const updated: RESTPatchAPIWebhookWithTokenMessageJSONBody = { - content: `[${i}s]: **${message}**`, - }; - msg = await this.editMessage(request, updated, msg?.id); - await sleep(1000); - } - // Delete the follow-up message - await this.deleteMessage(request, msg?.id); } + // Delete the follow-up message + await this.deleteMessage(request, request.message.id); } /** - * Build a list of supported Discord interactions + * Build a list of supported Discord interactions. The return value is used as filter so that + * Collab.Land can route the corresponding interactions to this action. * @returns */ private getSupportedInteractions(): DiscordInteractionPattern[] { @@ -138,6 +203,12 @@ export class HelloActionController extends BaseDiscordActionController