Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

69 the user should be able to trigger a search query on the mdn via a command #110

7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN yarn install --frozen-lockfile \
&& yarn env-gen \
&& yarn build \
&& yarn install --production \
&& zip -r app.zip ./node_modules ./build ./yarn.lock ./.env ./entrypoint.sh
&& zip -r app.zip ./node_modules ./build ./yarn.lock ./.env

# ------------------------------------------------------------
FROM node:lts-alpine AS app
Expand All @@ -22,7 +22,4 @@ COPY --from=build /app/app.zip .
RUN unzip app.zip \
&& rm app.zip \
&& mv ./build/* . \
&& rm -rf ./build \
&& chmod +x ./entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]
&& rm -rf ./build
4 changes: 3 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ services:
build:
context: .
dockerfile: ./Dockerfile
command: node ./index.js | tee -a /var/log/datadrop/console.log
volumes:
- bot_logs:/var/log/datadrop/
depends_on:
- database
database:
condition: service_healthy

volumes:
postgres:
Expand Down
12 changes: 0 additions & 12 deletions entrypoint.sh

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "datadrop",
"version": "2.0.0",
"version": "2.1.0",
"type": "module",
"main": "./build/index.js",
"scripts": {
Expand Down
184 changes: 184 additions & 0 deletions src/commands/utility/mdn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright https://github.com/discordjs/discord-utils-bot
import {
ActionRowBuilder,
type AutocompleteInteraction,
ButtonBuilder,
ButtonStyle,
type ChatInputCommandInteraction,
Colors,
EmbedBuilder,
SlashCommandBuilder,
bold,
hideLinkEmbed,
hyperlink,
inlineCode,
} from "discord.js";

import type { DatadropClient } from "../../datadrop.js";
import { getErrorMessage } from "../../helpers.js";
import type { Command } from "../../models/Command.js";

type MDNCandidate = {
entry: MDNIndexEntry;
matches: string[];
};

type MDNIndexEntry = {
title: string;
url: string;
};

type APIResult = {
doc: Document;
};

type Document = {
mdn_url: string;
summary: string;
title: string;
};

const MDN_URL = "https://developer.mozilla.org/" as const;
const searchCache = new Map<string, Document>();
const indexCache: MDNIndexEntry[] = [];

async function getMDNIndex() {
const response = await fetch(`${MDN_URL}/en-US/search-index.json`);
if (!response.ok) throw new Error("Failed to fetch MDN index.");

const data = await response.json();
indexCache.push(
...data.map((entry) => ({ title: entry.title, url: entry.url })),
);
}

function sanitize(str: string): string {
return str
.replaceAll("||", "|\u200B|") // avoid spoiler
.replaceAll("*", "\\*") // avoid bold/italic
.replaceAll(/\s+/g, " ") // remove duplicate spaces
.replaceAll(
/\[(.+?)]\((.+?)\)/g,
hyperlink("$1", hideLinkEmbed(`${MDN_URL}$2`)),
) // handle links
.replaceAll(/`\*\*(.*)\*\*`/g, bold(inlineCode("$1"))); // handle code blocks
}

export default {
data: new SlashCommandBuilder()
.setName("mdn")
.setDescription(
"Lance une recherche dans la documentation de Mozila Developer Network!",
)
.addStringOption((option) =>
option
.setName("query")
.setDescription(
"La classe, méthode, propriété ou autre à rechercher.",
)
.setRequired(true)
.setAutocomplete(true),
),

async autocomplete(
client: DatadropClient,
interaction: AutocompleteInteraction,
) {
if (indexCache.length === 0) {
client.logger.verbose("Fetching MDN index...");
try {
await getMDNIndex();
} catch (err) {
client.logger.error(
`Failed to fetch MDN index: ${getErrorMessage(err)}`,
);
}
}

const focusedOption = interaction.options.getFocused(true);
const parts = focusedOption.value
.split(/[.#]/)
.map((part) => part.toLowerCase());

const candidates: MDNCandidate[] = [];
for (const entry of indexCache) {
const lowerTitle = entry.title.toLowerCase();
const matches = parts.filter((phrase) =>
lowerTitle.includes(phrase),
);
if (matches.length) {
candidates.push({ entry, matches });
}
}

await interaction.respond(
candidates
.toSorted((one, other) => {
if (one.matches.length !== other.matches.length) {
return other.matches.length - one.matches.length;
}

const aMatches = one.matches.join("").length;
const bMatches = other.matches.join("").length;
return bMatches - aMatches;
})
.map((candidate) => ({
name: candidate.entry.title,
value: candidate.entry.url,
}))
.slice(0, 24), // 25 is the limit of choices for an autocomplete
);
},

async execute(
client: DatadropClient,
interaction: ChatInputCommandInteraction,
) {
await interaction.deferReply({ ephemeral: false });

const cleanQuery = interaction.options.getString("query", true).trim();
const searchUrl = `${MDN_URL}${cleanQuery}/index.json`;

try {
let hit = searchCache.get(searchUrl);
if (!hit) {
client.logger.debug(`Fetching MDN search results for ${cleanQuery} hitting on ${searchUrl}...`);
const response = await fetch(searchUrl);
if (!response.ok)
throw new Error(
"Erreur lors de la recherche dans la documentation de MDN.",
);
const result = (await response.json()) as APIResult;
hit = result.doc;
searchCache.set(searchUrl, hit);
}

const url = `${MDN_URL}${hit.mdn_url}`;
const embed = new EmbedBuilder()
.setColor(Colors.Purple)
.setURL(url)
.setTitle(sanitize(hit.title))
.setDescription(sanitize(hit.summary))
.setFooter({ text: 'MDN Web Docs', iconURL: 'https://developer.mozilla.org/favicon-48x48.png' })
.setTimestamp();
const button = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setLabel("Voir plus")
.setURL(url);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
await interaction.editReply({
content: "✅ Voici le résultat de votre recherche!",
embeds: [embed],
components: [row]
});
} catch (err) {
client.logger.error(
`Une erreur est survenue lors de la recherche de la documentation MDN : ${getErrorMessage(err)}`,
);
await interaction.editReply({
content:
"❌ **Oups!** - Une erreur est survenue lors de la recherche dans la documentation de MDN... La ressource n'existe probablement pas.",
});
}
},
} as Command;
5 changes: 3 additions & 2 deletions src/events/guildMemberAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "discord.js";

import type { DatadropClient } from "../datadrop.js";
import { getErrorMessage } from "../helpers.js";
import type { AnnounceConfiguration } from "../models/Configuration.js";
import type { Event } from "../models/Event.js";

Expand Down Expand Up @@ -64,8 +65,8 @@ async function guildMemberAdd(client: DatadropClient, member: GuildMember) {
client.logger.info(
`Un DM a été envoyé à <${member.user.tag}> à son entrée dans la guilde`,
);
} catch (err: unknown) {
client.logger.error((<Error>err).message);
} catch (err) {
client.logger.error(getErrorMessage(err));
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/events/guildMemberRemove.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Events, type GuildMember } from "discord.js";

import type { DatadropClient } from "../datadrop.js";
import { getErrorMessage } from "../helpers.js";
import type { Event } from "../models/Event.js";

export default {
Expand All @@ -17,7 +18,7 @@ async function guildMemberRemove(client: DatadropClient, member: GuildMember) {

try {
await client.database.delete(member.id);
} catch (err: unknown) {
client.logger.error((<Error>err).message);
} catch (err) {
client.logger.error(getErrorMessage(err));
}
}
7 changes: 5 additions & 2 deletions src/events/interactionCreate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ActionRowBuilder,
type AutocompleteInteraction,
ButtonBuilder,
type ButtonInteraction,
ButtonStyle,
Expand Down Expand Up @@ -30,7 +31,8 @@ async function interactionCreate(
) {
if (
interaction.isChatInputCommand() ||
interaction.isMessageContextMenuCommand()
interaction.isMessageContextMenuCommand() ||
interaction.isAutocomplete()
) {
await handleCommandInteraction(client, interaction);
} else if (isVerificationButton(interaction)) {
Expand All @@ -52,7 +54,8 @@ async function handleCommandInteraction(
client: DatadropClient,
interaction:
| ChatInputCommandInteraction
| MessageContextMenuCommandInteraction,
| MessageContextMenuCommandInteraction
| AutocompleteInteraction,
) {
const commandHandler = new CommandHandler(client);
if (commandHandler.shouldExecute(interaction)) {
Expand Down
5 changes: 5 additions & 0 deletions src/models/Command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AutocompleteInteraction,
ChatInputCommandInteraction,
ContextMenuCommandBuilder,
MessageContextMenuCommandInteraction,
Expand All @@ -10,6 +11,10 @@ import type { DatadropClient } from "../datadrop.js";
export interface Command {
data: SlashCommandBuilder | ContextMenuCommandBuilder;
ownerOnly?: boolean;
autocomplete?(
client: DatadropClient,
interaction: AutocompleteInteraction,
): Promise<void>;
execute(
client: DatadropClient,
interaction:
Expand Down
Loading
Loading