diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abaa851..6317b8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,11 @@ jobs: - name: Run JQ lint uses: home-assistant/actions/helpers/jq@master + - name: Setup YQ + uses: chrisdickinson/setup-yq@latest + with: + yq-version: v4.25.3 + - name: Use Node.js ${{ env.node-NODE_VERSION }} uses: actions/setup-node@v3.4.1 with: @@ -40,6 +45,9 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- + - name: Lint YAML files + run: yq '.' ./data/discord_messages.yaml + - name: Install dependencies run: yarn install --immutable diff --git a/data/discord_messages.yaml b/data/discord_messages.yaml new file mode 100644 index 0000000..0fb078d --- /dev/null +++ b/data/discord_messages.yaml @@ -0,0 +1,3 @@ +hello_world: + description: "Print hello world message" + content: "Hello world :wave:" \ No newline at end of file diff --git a/package.json b/package.json index fc94d7c..6d4aa4f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "discord.js": "^14.3.0", "find-up": "^4.0.0", "graphql": "^16.6.0", + "js-yaml": "^4.1.0", "nestjs-pino": "^2.5.2", "node-fetch": "2", "pino": "^8.4.2", @@ -64,6 +65,7 @@ "@nestjs/cli": "^8.2.5", "@nestjs/schematics": "^8.0.10", "@nestjs/testing": "^8.4.4", + "@types/js-yaml": "^4", "@typescript-eslint/eslint-plugin": "5.21.0", "@typescript-eslint/parser": "^5.21.0", "eslint": "8.14.0", diff --git a/services/bots/src/discord/commands/message.ts b/services/bots/src/discord/commands/message.ts new file mode 100644 index 0000000..50e3b1e --- /dev/null +++ b/services/bots/src/discord/commands/message.ts @@ -0,0 +1,124 @@ +import fetch from 'node-fetch'; +import yaml from 'js-yaml'; + +import { TransformPipe } from '@discord-nestjs/common'; +import { + DiscordTransformedCommand, + Payload, + TransformedCommandExecutionContext, + Param, + UsePipes, + On, +} from '@discord-nestjs/core'; +import { CommandHandler, DiscordCommandClass } from '../discord.decorator'; +import { AutocompleteInteraction, EmbedBuilder } from 'discord.js'; +import { reportException } from '@lib/sentry/reporting'; + +const DATA_FILE_URL = + 'https://raw.githubusercontent.com/home-assistant/service-hub/main/data/discord_messages.yaml'; + +interface MessageData { + [key: string]: { description: string; content: string }; +} + +class MessageDto { + @Param({ + name: 'message', + description: 'What message do you want to post?', + required: true, + autocomplete: true, + }) + messageKey: string; +} + +@DiscordCommandClass({ + name: 'message', + description: 'Returns a predefined message', +}) +@UsePipes(TransformPipe) +export class MessageCommand implements DiscordTransformedCommand { + private messageData: MessageData; + + async reloadMessageData(force?: boolean): Promise { + if (force || !this.messageData) { + this.messageData = yaml.load(await (await fetch(DATA_FILE_URL)).text(), { + json: true, + }) as MessageData; + } + } + @CommandHandler() + async handler( + @Payload() handlerDto: MessageDto, + context: TransformedCommandExecutionContext, + ): Promise { + const { messageKey } = handlerDto; + const { interaction } = context; + if (messageKey === 'reload') { + await this.reloadMessageData(true); + + await interaction.reply({ + content: 'Message list reloaded', + ephemeral: true, + }); + return; + } + + if (!this.messageData[messageKey]) { + await interaction.reply({ + content: 'Could not find information', + ephemeral: true, + }); + return; + } + + await this.reloadMessageData(); + + await interaction.reply({ + embeds: [ + new EmbedBuilder({ + description: this.messageData[messageKey].content, + }), + ], + }); + } + + // This is the autocomplete handler for the /message command + @On('interactionCreate') + async onInteractionCreate(interaction: AutocompleteInteraction): Promise { + if (!interaction.isAutocomplete() || interaction.commandName !== 'message') { + return; + } + try { + await this.reloadMessageData(); + const focusedValue = interaction.options.getFocused()?.toLowerCase(); + + await interaction.respond( + focusedValue.length !== 0 + ? Object.entries(this.messageData) + .map(([key, data]) => ({ + name: data.description, + value: key, + })) + .filter( + (choice) => + choice.value.toLowerCase().includes(focusedValue) || + choice.name.toLowerCase().includes(focusedValue), + ) + // The API only allow max 25 sugestions + .slice(0, 25) + : [], + ); + } catch (err) { + reportException(err, { + cause: err, + data: { + interaction: interaction.toJSON(), + user: interaction.user.toJSON(), + channel: interaction.channel.toJSON(), + command: interaction.command.toJSON(), + }, + }); + await interaction.respond([]); + } + } +} diff --git a/services/bots/src/discord/discord.module.ts b/services/bots/src/discord/discord.module.ts index 6785d0a..b070ba2 100644 --- a/services/bots/src/discord/discord.module.ts +++ b/services/bots/src/discord/discord.module.ts @@ -3,9 +3,10 @@ import { DiscordModule } from '@discord-nestjs/core'; import { PingCommand } from './commands/ping'; import { VersionsCommand } from './commands/versions'; import { IntegrationCommand } from './commands/integration'; +import { MessageCommand } from './commands/message'; @Module({ imports: [DiscordModule.forFeature()], - providers: [PingCommand, VersionsCommand, IntegrationCommand], + providers: [PingCommand, VersionsCommand, IntegrationCommand, MessageCommand], }) export class DiscordBotModule {} diff --git a/yarn.lock b/yarn.lock index ab9199a..4023b42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1475,6 +1475,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4": + version: 4.0.5 + resolution: "@types/js-yaml@npm:4.0.5" + checksum: 7dcac8c50fec31643cc9d6444b5503239a861414cdfaa7ae9a38bc22597c4d850c4b8cec3d82d73b3fbca408348ce223b0408d598b32e094470dfffc6d486b4d + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" @@ -6511,6 +6518,7 @@ __metadata: "@octokit/webhooks-types": ^6.3.6 "@sentry/integrations": ^6.19.7 "@sentry/node": ^6.19.7 + "@types/js-yaml": ^4 "@typescript-eslint/eslint-plugin": 5.21.0 "@typescript-eslint/parser": ^5.21.0 apollo-server-express: ^3.6.7 @@ -6523,6 +6531,7 @@ __metadata: eslint-plugin-import: ^2.26.0 find-up: ^4.0.0 graphql: ^16.6.0 + js-yaml: ^4.1.0 mocha: ^10.0.0 nestjs-pino: ^2.5.2 node-fetch: 2