Skip to content

Commit

Permalink
feat: improve the hello-action UI
Browse files Browse the repository at this point in the history
Signed-off-by: Raymond Feng <[email protected]>
  • Loading branch information
raymondfeng committed Feb 18, 2023
1 parent 6bb7dd1 commit a822003
Showing 1 changed file with 121 additions and 50 deletions.
171 changes: 121 additions & 50 deletions src/actions/hello-action.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<APIChatInputApplicationCommandInteraction> {
export class HelloActionController extends BaseDiscordActionController<APIInteraction> {
/**
* 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<DiscordActionMetadata> {
Expand Down Expand Up @@ -69,66 +79,121 @@ export class HelloActionController extends BaseDiscordActionController<APIChatIn
}

/**
* Handle the Discord interaction
* Handle the Discord slash commands
* @param request - Discord interaction with Collab.Land action context
* @returns - Discord interaction response
*/
protected async handleApplicationCommand(
request: DiscordActionRequest<APIChatInputApplicationCommandInteraction>,
): Promise<DiscordActionResponse> {
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<MessageActionRowComponentBuilder>()
.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<APIChatInputApplicationCommandInteraction>,
protected async handleMessageComponent(
request: DiscordActionRequest<APIMessageComponentInteraction>,
): Promise<DiscordActionResponse> {
/**
* 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<APIChatInputApplicationCommandInteraction>,
message: string,
/**
* Run a countdown by updating the original message content
* @param request
*/
private async countDown(
request: DiscordActionRequest<APIMessageComponentInteraction>,
) {
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[] {
Expand All @@ -138,6 +203,12 @@ export class HelloActionController extends BaseDiscordActionController<APIChatIn
type: InteractionType.ApplicationCommand,
names: ['hello-action'],
},
{
// Handle message components such as buttons
type: InteractionType.MessageComponent,
// Use a namespace to catch all buttons with custom id starting with `hello-action:`
ids: ['hello-action:*'],
},
];
}

Expand Down

0 comments on commit a822003

Please sign in to comment.