diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdf960..aae741e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## `v1.2.0` - 2026-07-20 +### Added + +- Added bot info and statistics display when the bot is mentioned ([#46](https://github.com/open-devhub/quillbot/issues/46)) + ### Changed - Replaced the previous score/regex-based estimator with a dedicated `analyzeComplexity` utility that properly tracks loop nesting depth, classifies recursion (linear / logarithmic / exponential / factorial / memoized), and produces structured indicators + reasoning. ([#41](https://github.com/open-devhub/quillbot/issues/41)) diff --git a/package.json b/package.json index c2b6074..9eb294a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "quillbot", - "version": "1.1.1", - "description": "Quill is an advanced Discord developer assistant bot built to help programmers code faster, learn better, and debug smarter, directly inside Discord.", + "version": "1.2.0", + "description": "Advanced Discord developer assistant for coding, debugging, AI help, documentation lookup, and developer utilities", "main": "src/index.ts", "type": "module", "scripts": { diff --git a/src/commands/docs/man.ts b/src/commands/docs/man.ts index c66bab6..1acb39e 100644 --- a/src/commands/docs/man.ts +++ b/src/commands/docs/man.ts @@ -47,15 +47,17 @@ export default { content: page.description || "No description available.", }, { type: "separator", spacing: "small" }, + { - type: "section", - components: [{ type: "text", content: "\u200b" }], - accessory: { - type: "button", - style: "link", - label: "Open Man Page", - url: page.url, - }, + type: "actionRow", + components: [ + { + type: "button", + style: "link", + label: "Open Man Page", + url: page.url, + }, + ], }, ], }, diff --git a/src/commands/docs/mdn.ts b/src/commands/docs/mdn.ts index 14748ca..4e3eb4c 100644 --- a/src/commands/docs/mdn.ts +++ b/src/commands/docs/mdn.ts @@ -59,16 +59,15 @@ export default { }, { type: "separator" }, { - type: "section", + type: "actionRow", components: [ - { type: "text", content: "Read the full documentation:" }, + { + type: "button", + style: "link", + label: "Open on MDN", + url, + }, ], - accessory: { - type: "button", - style: "link", - label: "Open on MDN", - url, - }, }, ], }, diff --git a/src/commands/docs/wiki.ts b/src/commands/docs/wiki.ts index 13763e4..4641a8a 100644 --- a/src/commands/docs/wiki.ts +++ b/src/commands/docs/wiki.ts @@ -4,132 +4,167 @@ import type { CommandCallbackOpts } from "../../types/command.ts"; import { buildComponents } from "../../utils/components/buildComponents.ts"; import { buildErrorComponent } from "../../utils/components/buildError.ts"; -async function getMetaDescription(url: string): Promise { - try { - const res = await fetch(url, { - headers: { - "User-Agent": - "Mozilla/5.0 (compatible; QuillBot/1.0; +https://github.com/)", - }, - }); - if (!res.ok) return ""; - - const html = await res.text(); - - const ogMatch = - html.match( - /]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i, - ) || - html.match( - /]+content=["']([^"']+)["'][^>]+property=["']og:description["']/i, - ); - - const metaMatch = - html.match( - /]+name=["']description["'][^>]+content=["']([^"']+)["']/i, - ) || - html.match( - /]+content=["']([^"']+)["'][^>]+name=["']description["']/i, - ); - - const raw = (ogMatch?.[1] || metaMatch?.[1] || "").trim(); - if (!raw) return ""; - - return raw - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\s+/g, " ") - .trim(); - } catch { - return ""; - } -} - export default { - name: "mdn", - description: "Search MDN Web Docs", - aliases: ["mozilla", "mdnsearch"], - usage: "mdn ", + name: "wiki", + description: "Search Wikipedia articles", + usage: "wiki ", + aliases: ["wikipedia", "wikisearch"], + async callback({ message, args }: CommandCallbackOpts) { try { const query = args.join(" "); + if (!query) { return message.reply({ - flags: MessageFlags.IsComponentsV2, components: buildErrorComponent({ - title: "❌ Missing Search Term", - description: "Please provide a search term, e.g. `;mdn WeakMap`", + title: "Missing search term", + description: + "Please provide a search term, e.g. `++wiki JavaScript`", }), + flags: MessageFlags.IsComponentsV2, }); } + const controller1 = new AbortController(); + const timeoutId1 = setTimeout(() => controller1.abort(), 10000); + const res = await fetch( - `https://developer.mozilla.org/api/v1/search?q=${encodeURIComponent(query)}&locale=en-US`, + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent( + query, + )}&utf8=&format=json`, ); + + clearTimeout(timeoutId1); + const data = await res.json(); - if (!data.documents || data.documents.length === 0) { + if (!data.query || !data.query.search || data.query.search.length === 0) { return message.reply({ - flags: MessageFlags.IsComponentsV2, components: buildErrorComponent({ - title: "No Results Found", - description: `¯\\_(ツ)_/¯ No MDN results found for **${query}**`, + title: "No Wikipedia results found", + description: `No articles were found for **${query}**`, }), + flags: MessageFlags.IsComponentsV2, }); } - const doc = data.documents[0]; - const url = `https://developer.mozilla.org${doc.mdn_url}`; + const article = data.query.search[0]; + const title = article.title; + const url = `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`; - let description = await getMetaDescription(url); + const controller2 = new AbortController(); + const timeoutId2 = setTimeout(() => controller2.abort(), 10000); - if (!description && doc.excerpt) { - description = doc.excerpt.replace(/<[^>]+>/g, "").trim(); - } + const pageRes = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro=true&explaintext=true&piprop=thumbnail&pithumbsize=300&format=json&titles=${encodeURIComponent(title)}`, + ); - if (description.length > 500) { - description = description.slice(0, 497) + "..."; - } + clearTimeout(timeoutId2); - return message.reply({ - flags: MessageFlags.IsComponentsV2, - components: buildComponents([ - { - type: "container", - accentColor: 0x83d0f2, - components: [ - { type: "text", content: `### ${doc.title}` }, - { - type: "text", - content: description || "No description available.", - }, - { type: "separator", spacing: "small" }, - { - type: "section", - components: [{ type: "text", content: "\u200b" }], - accessory: { + const pageData = await pageRes.json(); + + const page = Object.values(pageData.query.pages)[0] as { + extract?: string; + thumbnail?: { + source?: string; + }; + }; + + const extract = page?.extract ?? "No extract available."; + const thumbnail = page?.thumbnail?.source; + + const components = buildComponents([ + { + type: "container", + accentColor: "#ffffff", + components: [ + { + type: "section", + components: [ + { + type: "text", + content: `### ${title}\n`, + }, + { + type: "text", + content: + extract.length > 800 + ? extract.slice(0, extract.lastIndexOf(" ", 800)) + " ..." + : extract, + }, + ], + ...(thumbnail + ? { + accessory: { + type: "thumbnail", + url: thumbnail, + description: title, + }, + } + : { + accessory: { + type: "button", + style: "link", + label: "Read on Wikipedia", + url, + }, + }), + }, + + { + type: "separator", + }, + + { + type: "actionRow", + components: [ + { type: "button", style: "link", - label: "Open on MDN", + label: "Read on Wikipedia", url, }, - }, - ], - }, - ]), + ], + }, + + { + type: "text", + content: "-# Source: wikipedia.org", + }, + ], + }, + ]); + + return message.reply({ + components, + flags: MessageFlags.IsComponentsV2, }); - } catch (err) { + } catch (err: unknown) { + const error = err as { + name?: string; + code?: string; + }; + + if (error.name === "AbortError" || error.code === "ETIMEDOUT") { + return message.reply({ + components: buildErrorComponent({ + title: "Wikipedia request timed out", + description: + "The request to Wikipedia took too long and was aborted.", + }), + flags: ["IsComponentsV2"], + }); + } + console.error(err); + return message.reply({ - flags: MessageFlags.IsComponentsV2, components: buildErrorComponent({ - title: "❌ Failed to Fetch MDN Results", + title: "Failed to fetch Wikipedia results", description: - "An error occurred while fetching results from MDN Web Docs.", + "An error occurred while fetching results from Wikipedia.", }), + flags: MessageFlags.IsComponentsV2, }); } }, diff --git a/src/events/messageCreate/info.ts b/src/events/messageCreate/info.ts new file mode 100644 index 0000000..0ee88f2 --- /dev/null +++ b/src/events/messageCreate/info.ts @@ -0,0 +1,151 @@ +import { MessageFlags, type Client, type Message } from "discord.js"; +import pkg from "../../../package.json" with { type: "json" }; +import { buildComponents } from "../../utils/components/buildComponents.ts"; + +export default async (client: Client, message: Message) => { + if ( + message.mentions.users.size !== 1 || + !message.mentions.has(client.user!) || + (message.content.trim() !== `<@${client.user!.id}>` && + message.content.trim() !== `<@!${client.user!.id}>`) + ) { + return; + } + + const uptimeMs = process.uptime() * 1000; + + const days = Math.floor(uptimeMs / 86400000); + const hours = Math.floor((uptimeMs % 86400000) / 3600000); + const minutes = Math.floor((uptimeMs % 3600000) / 60000); + + const uptime = + days > 0 + ? `${days}d ${hours}h ${minutes}m` + : hours > 0 + ? `${hours}h ${minutes}m` + : `${minutes}m`; + + const guilds = client.guilds.cache.size; + const users = client.guilds.cache.reduce( + (n, g) => n + (g.memberCount || 0), + 0, + ); + + return message.reply({ + flags: MessageFlags.IsComponentsV2, + components: buildComponents([ + { + type: "container", + accentColor: 0x5865f2, + components: [ + { + type: "section", + components: [ + { + type: "text", + content: [ + `## ${client.user!.displayName}`, + "", + pkg.description, + "", + "-# Use `;help` to see everything I can do.", + ].join("\n"), + }, + ], + accessory: { + type: "thumbnail", + url: client.user!.displayAvatarURL({ + extension: "png", + size: 512, + }), + }, + }, + + { + type: "separator", + spacing: "small", + }, + + { + type: "text", + content: [ + "### Features", + "", + "⌬ AI-powered code assistance", + "⌬ Sandboxed code execution (60+ languages)", + "⌬ Programming language detection", + "⌬ Code formatting with Prettier", + "⌬ Documentation search (MDN, Wikipedia, man, etc)", + "⌬ Package registry search (npm, PyPI, crates.io, RubyGems, etc)", + "⌬ GitHub repository & profile utilities", + "⌬ URL safety scanning & HTTP inspection", + "⌬ Developer utilities (Color preview, Regex, Base64, IDs, WHOIS, Snowflakes, etc)", + "", + "-# Type in `;changelog` to see the latest changes!", + ].join("\n"), + }, + + { + type: "separator", + spacing: "small", + }, + + { + type: "mediaGallery", + items: [ + { + url: "https://raw.githubusercontent.com/open-devhub/quillbot/refs/heads/main/assets/banner2.png", + }, + ], + }, + + { + type: "separator", + spacing: "small", + }, + + { + type: "text", + content: `-# ${[ + `Quill v${pkg.version}`, + `Latency: ${Math.round(client.ws.ping)}ms`, + `Guilds: ${guilds.toLocaleString()}`, + `Users: ${users.toLocaleString()}`, + ] + .map((i) => `**\` ${i} \`**`) + .join(" ┊ ")}`, + }, + + { + type: "separator", + spacing: "small", + }, + + { + type: "actionRow", + components: [ + { + type: "button", + style: "link", + label: "★ Add to Server", + url: "https://discord.com/oauth2/authorize?client_id=1447982776740610058", + }, + { + type: "button", + style: "link", + label: "★ GitHub Repo", + url: "https://github.com/open-devhub/quillbot", + }, + { + type: "button", + style: "link", + label: "★ Discord", + url: "https://discord.gg/MuZFAeVHgp", + }, + ], + }, + ], + }, + ]), + }); +};