|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright 2022 Google LLC |
| 4 | + * SPDX-License-Identifier: BSD-3-Clause |
| 5 | + */ |
| 6 | + |
| 7 | +import { |
| 8 | + ChatInputCommandInteraction, |
| 9 | + Client, |
| 10 | + GatewayIntentBits, |
| 11 | + InteractionType, |
| 12 | + REST, |
| 13 | + Routes, |
| 14 | + SlashCommandBuilder, |
| 15 | +} from 'discord.js'; |
| 16 | +import algolia from 'algoliasearch/lite.js'; |
| 17 | +import {publicVars} from 'lit-dev-tools-esm/lib/configs.js'; |
| 18 | + |
| 19 | +// set up algolia search |
| 20 | +const algClient = algolia( |
| 21 | + publicVars.algolia.appId, |
| 22 | + publicVars.algolia.searchOnlyKey |
| 23 | +); |
| 24 | +const index = algClient.initIndex(publicVars.algolia.index); |
| 25 | + |
| 26 | +// The is the GH action secret under the name LIT_DEV_DISCORD_BOT_CLIENT_TOKEN |
| 27 | +const BOT_CLIENT_SECRET = process.env.BOT_CLIENT_SECRET; |
| 28 | + |
| 29 | +if (!BOT_CLIENT_SECRET) { |
| 30 | + throw new Error('Missing BOT_CLIENT_SECRET'); |
| 31 | +} |
| 32 | + |
| 33 | +interface Suggestion { |
| 34 | + id: number; |
| 35 | + relativeUrl: string; |
| 36 | + heading: string; |
| 37 | + isSubsection: boolean; |
| 38 | + title: string; |
| 39 | +} |
| 40 | + |
| 41 | +// Build the UI for the slash command. |
| 42 | +const command = new SlashCommandBuilder() |
| 43 | + .setName('docs') |
| 44 | + .setDescription('Will search lit.dev.') |
| 45 | + .addStringOption((option) => |
| 46 | + option |
| 47 | + .setName('query') |
| 48 | + .setDescription('The query to search for.') |
| 49 | + .setRequired(true) |
| 50 | + .setAutocomplete(true) |
| 51 | + ); |
| 52 | + |
| 53 | +const rest = new REST({version: '10'}).setToken(BOT_CLIENT_SECRET); |
| 54 | + |
| 55 | +(async () => { |
| 56 | + try { |
| 57 | + console.log('Started refreshing application (/) commands.'); |
| 58 | + |
| 59 | + // Tell Discord that we publish the following slash commands. |
| 60 | + await rest.put(Routes.applicationCommands(publicVars.discord.clientId), { |
| 61 | + body: [command], |
| 62 | + }); |
| 63 | + |
| 64 | + console.log('Successfully reloaded application (/) commands.'); |
| 65 | + } catch (error) { |
| 66 | + console.error(error); |
| 67 | + } |
| 68 | +})(); |
| 69 | + |
| 70 | +const client = new Client({intents: [GatewayIntentBits.Guilds]}); |
| 71 | + |
| 72 | +client.on('ready', () => { |
| 73 | + console.log(`Logged in as ${client.user?.tag}!`); |
| 74 | +}); |
| 75 | + |
| 76 | +client.on('interactionCreate', async (interaction) => { |
| 77 | + if ((interaction as ChatInputCommandInteraction).commandName !== 'docs') { |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + // This happens as the user is typing. Enabled by the SlashCommandBuilder's |
| 82 | + // .setAutocomplete(true) option. |
| 83 | + if (interaction.type === InteractionType.ApplicationCommandAutocomplete) { |
| 84 | + const focusedValue = interaction.options.getFocused(); |
| 85 | + // Do not waste a query if the user has fewer than 3 chars. |
| 86 | + if (focusedValue.length < 3) { |
| 87 | + await interaction.respond([]); |
| 88 | + return; |
| 89 | + } |
| 90 | + |
| 91 | + // Search algolia for the query. |
| 92 | + const searchRes = await index.search<Suggestion>(focusedValue, { |
| 93 | + page: 0, |
| 94 | + hitsPerPage: 5, |
| 95 | + }); |
| 96 | + |
| 97 | + // Transform the hits' relative URL to objects that are readable and |
| 98 | + // linkable outside of lit.dev. |
| 99 | + const results = searchRes.hits.map((hit) => { |
| 100 | + const readableText = hit.isSubsection |
| 101 | + ? `${hit.title} - ${hit.heading}` |
| 102 | + : hit.title; |
| 103 | + |
| 104 | + // autocomplete requires a `name` and a `value` property like a <select> |
| 105 | + // The name is what is shown to the user, but the `value` is what is |
| 106 | + // actually sent to the bot in the `.isChatInputCommand()` event. |
| 107 | + return { |
| 108 | + name: readableText, |
| 109 | + value: `https://lit.dev${hit.relativeUrl}`, |
| 110 | + }; |
| 111 | + }); |
| 112 | + |
| 113 | + await interaction.respond(results); |
| 114 | + } |
| 115 | + |
| 116 | + // This is true when the user finally returns a command. (a lit.dev url) |
| 117 | + if (interaction.isChatInputCommand()) { |
| 118 | + const value = interaction.options.data[0].value as string; |
| 119 | + |
| 120 | + // If the response is a lit.dev URL then tell the bot to post it. |
| 121 | + if (value.startsWith('https://lit.dev')) { |
| 122 | + await interaction.reply({content: value}); |
| 123 | + } else { |
| 124 | + // If the response is not a lit.dev url, then bot responds with an |
| 125 | + // ephemeral message that is only visible to the user. This happens when |
| 126 | + // there are no results, or if the user hits enter before results show up. |
| 127 | + interaction.reply({ |
| 128 | + ephemeral: true, |
| 129 | + content: `value: "${value}" is not a valid lit.dev url. Please select from the autocomplete list.`, |
| 130 | + }); |
| 131 | + } |
| 132 | + } |
| 133 | +}); |
| 134 | + |
| 135 | +// Start the web socket connection to the Bot. |
| 136 | +client.login(BOT_CLIENT_SECRET); |
0 commit comments