Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
- New `man` command for viewing Unix/Linux manual pages for command documentation
- New `crate` command to search Rust crates
- New `gem` command to search Ruby gems
- New `base64` command encode or decode Base64 strings

### Changed

- Updated the `changelog` command to use a `StringSelectMenuBuilder` instead of just showing the latest version
- Both the command and interaction handler now parse `CHANGELOG.md` dynamically
- Updated `profile` command to include a visual GitHub contribution activity graph in the embed response

## `v1.0.0` - 01/07/2026

Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ All commands use the `;` prefix (e.g., `;run`). Premium features are marked with

### 🐙 GitHub Integration

| Command | Description |
| :--------- | :--------------------------------------------------------------------------- |
| `;profile` | Fetch detailed statistics, repository counts, and bios for a GitHub user. |
| `;repo` | Get overview metrics (stars, forks, open issues) for a specified repository. |
| `;tree` ★ | Generate a structured visual directory tree for a GitHub repository. |
| Command | Description |
| :--------- | :--------------------------------------------------------------------------------------- |
| `;profile` | Fetch detailed statistics, repository counts, and bio, activity graph for a GitHub user. |
| `;repo` | Get overview metrics (stars, forks, open issues) for a specified repository. |
| `;tree` ★ | Generate a structured visual directory tree for a GitHub repository. |

### 🔢 Mathematics

Expand All @@ -71,6 +71,7 @@ All commands use the `;` prefix (e.g., `;run`). Premium features are marked with

| Command | Description |
| :----------- | :------------------------------------------------------------------------------------ |
| `;base64` | Encode or decode Base64 strings |
| `;color` | Preview exact hexadecimal/RGB colors and custom CSS gradients. |
| `;http` | Perform lightweight HTTP requests and inspect responses (headers, payload). |
| `;id` | Generate cryptographic or specific system unique IDs across 60+ formats. |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts",
"format": "prettier --write .",
"check:conflicts": "bun src/scripts/conflicts.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
Expand Down
114 changes: 68 additions & 46 deletions src/commands/github/profile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
ActionRowBuilder,
AttachmentBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
} from "discord.js";
import type { CommandCallbackOpts } from "../../types/command.ts";
import { generateGitHubHeatMap } from "../../utils/githubActivityGraph.ts";

export default {
name: "profile",
Expand All @@ -28,14 +30,11 @@ export default {
});
}

// match URLs like:
// https://github.com/username
// username
// github.com/username
const match =
username.match(
/^(?:https?:\/\/)?(?:www\.)?github\.com\/([^\/]+)(?:\/([^\/]+))?(?:\.git)?$/i,
) || username.match(/^([^\/]+)$/);

if (!match) {
return message.reply({
embeds: [
Expand All @@ -48,58 +47,81 @@ export default {
],
});
}

const user = match[1];
const apiUrl = `https://api.github.com/users/${user}`;

fetch(apiUrl)
.then((res) => {
if (!res.ok) {
throw new Error("User not found");
}
return res.json();
})
.then((data) => {
const name = data.name;
const bio = data.bio;
const repos = data.public_repos || 0;
const followers = data.followers || 0;
const following = data.following || 0;
const res = await fetch(apiUrl);
if (!res.ok) {
return message.reply({
embeds: [
new EmbedBuilder()
.setTitle("❌ GitHub Profile Not Found")
.setDescription(
"Please provide a valid GitHub username or profile URL.\nExample: `;profile octocat` or `;profile https://github.com/octocat`",
)
.setColor(0xd21872),
],
});
}

const data = await res.json();

const embed = new EmbedBuilder()
.setAuthor({
name: data.login,
iconURL: data.avatar_url,
url: data.html_url,
})
.setTitle(name || data.login)
.setDescription(bio || "No bio")
.setThumbnail(data.avatar_url)
.addFields(
{ name: "Public Repos", value: repos.toString(), inline: true },
{ name: "Followers", value: followers.toString(), inline: true },
{ name: "Following", value: following.toString(), inline: true },
)
.setFooter({
text: `Requested by ${message.author.tag}`,
iconURL: message.author.displayAvatarURL(),
});
const name = data.name;
const bio = data.bio;
const repos = data.public_repos || 0;
const followers = data.followers || 0;
const following = data.following || 0;

const button = new ButtonBuilder()
.setLabel("View on GitHub")
.setStyle(ButtonStyle.Link)
.setURL(data.html_url);
const row = new ActionRowBuilder().addComponents(button);
let heatmapAttachment: AttachmentBuilder | null = null;
try {
const heatmapBuffer = await generateGitHubHeatMap(user || "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user variable is used with a fallback to empty string (user || ""), but user is extracted from regex match at line 50 and should always be defined at this point. However, if user is somehow undefined, passing an empty string to generateGitHubHeatMap("") will cause the function to make an API call with an invalid username, potentially causing errors or unexpected behavior. This indicates a logic error in the flow.

Confidence: 5/5

Suggested Fix
Suggested change
const heatmapBuffer = await generateGitHubHeatMap(user || "");
const heatmapBuffer = await generateGitHubHeatMap(user);

Remove the || "" fallback since user is guaranteed to be defined at this point (extracted from regex match at line 50). If there's concern about undefined values, the proper fix is to add validation earlier in the flow, not to pass empty strings to API functions.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/github/profile.ts around line 78, the generateGitHubHeatMap call uses `user || ""` as a fallback, but the user variable is extracted from a regex match at line 50 and should always be defined at this point; remove the `|| ""` fallback and pass `user` directly since it's guaranteed to exist, or if there's genuine concern about undefined values, add proper validation earlier in the flow rather than passing empty strings to API functions which could cause unexpected behavior.

heatmapAttachment = new AttachmentBuilder(heatmapBuffer, {
name: "heatmap.png",
});
} catch (err) {
console.error("Failed to generate heatmap:", err);
}

message.reply({ embeds: [embed], components: [row.toJSON()] });
const embed = new EmbedBuilder()
.setAuthor({
name: data.login,
iconURL: data.avatar_url,
url: data.html_url,
})
.catch((err) => {
console.error(err);
message.reply(
"An error occurred while fetching the user information.",
);
.setTitle(name || data.login)
.setDescription(bio || "No bio")
.setThumbnail(data.avatar_url)
.addFields(
{ name: "Public Repos", value: repos.toString(), inline: true },
{ name: "Followers", value: followers.toString(), inline: true },
{ name: "Following", value: following.toString(), inline: true },
)
// .setImage(graphUrl)
.setFooter({
text: `Requested by ${message.author.tag}`,
iconURL: message.author.displayAvatarURL(),
});

if (heatmapAttachment) {
embed.setImage("attachment://heatmap.png");
}

const button = new ButtonBuilder()
.setLabel("View on GitHub")
.setStyle(ButtonStyle.Link)
.setURL(data.html_url);

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);

await message.reply({
embeds: [embed],
components: [row.toJSON()],
files: heatmapAttachment ? [heatmapAttachment] : [],
});
} catch (err) {
console.error(err);
message.reply("An error occurred while fetching the user information.");
}
},
};
6 changes: 4 additions & 2 deletions src/commands/meta/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ export default {
.setPlaceholder("Select a version to view its changes")
.addOptions(
versions.map((v, i) => ({
label:
v.title.length > 100 ? v.title.slice(0, 97) + "..." : v.title,
label: (v.title.length > 100
? v.title.slice(0, 97) + "..."
: v.title
).replace(/`/g, ""),
value: i.toString(),
...(i === 0 ? { description: "Latest version" } : {}),
})),
Expand Down
91 changes: 91 additions & 0 deletions src/commands/tools/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { EmbedBuilder } from "discord.js";
import type { CommandCallbackOpts } from "../../types/command.ts";

export default {
name: "base64",
description: "Encode or decode Base64 strings.",
usage: "base64 <encode|decode> <text>",
aliases: ["b64"],
callback: {
encode: async ({ message, args }: CommandCallbackOpts) => {
const text = args.join(" ");

if (!text) {
const embed = new EmbedBuilder()
.setTitle("❌ Missing Input")
.setDescription("Please provide some text to encode.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}

const encoded = Buffer.from(text, "utf8").toString("base64");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encode operation lacks input size validation, which could cause memory exhaustion if an attacker provides an extremely large text string. This could lead to service disruption or denial of service.

Confidence: 5/5

Suggested Fix

Add input size validation before attempting to encode:

Suggested change
const encoded = Buffer.from(text, "utf8").toString("base64");
const MAX_INPUT_SIZE = 10 * 1024 * 1024; // 10MB limit
const inputSize = Buffer.byteLength(text, "utf8");
if (inputSize > MAX_INPUT_SIZE) {
const embed = new EmbedBuilder()
.setTitle("❌ Input Too Large")
.setDescription("The text is too large to encode. Maximum size is 10MB.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
const encoded = Buffer.from(text, "utf8").toString("base64");

Add size validation before the encode operation to prevent memory exhaustion. Use Buffer.byteLength() to get the actual byte size of the UTF-8 string, as some characters may be multi-byte.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/tools/base64.ts at line 21, add input size validation before
encoding to prevent memory exhaustion attacks. Check if the input text exceeds
10MB using Buffer.byteLength(text, "utf8") and return an appropriate error
embed if it does, before attempting the Base64 encoding operation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encoding operation lacks input size validation before creating a Buffer. A malicious user could provide extremely large text input (e.g., millions of characters), causing excessive memory allocation and potentially crashing the bot or causing service disruption.

Confidence: 5/5

Suggested Fix

Add input size validation before encoding to prevent memory exhaustion attacks:

Suggested change
const encoded = Buffer.from(text, "utf8").toString("base64");
const MAX_INPUT_SIZE = 100000; // 100KB limit
const text = args.join(" ");
if (!text) {
const embed = new EmbedBuilder()
.setTitle("❌ Missing Input")
.setDescription("Please provide some text to encode.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
if (text.length > MAX_INPUT_SIZE) {
const embed = new EmbedBuilder()
.setTitle("❌ Input Too Large")
.setDescription(`Text is too large to encode. Maximum size: ${MAX_INPUT_SIZE} characters.`)
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
const encoded = Buffer.from(text, "utf8").toString("base64");

Add size validation immediately after checking if text exists to prevent memory exhaustion from oversized inputs.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/tools/base64.ts around line 11-21, the encode function lacks input size validation before creating a Buffer, which could allow malicious users to cause memory exhaustion by providing extremely large text inputs; add a MAX_INPUT_SIZE constant (e.g., 100000 characters) and validate text.length against it after the empty check but before the Buffer.from() operation, returning an appropriate error embed if the limit is exceeded.


const embed = new EmbedBuilder()
.setTitle("🔐 Base64 Encode")
.addFields(
{
name: "Original Text",
value: text.length > 1024 ? text.slice(0, 1021) + "..." : text,
},
{
name: "Encoded (Base64)",
value:
encoded.length > 1024
? "```" + encoded.slice(0, 1021) + "...```"
: "```" + encoded + "```",
},
)
.setColor(0x2ecc71);

await message.reply({ embeds: [embed] });
},

decode: async ({ message, args }: CommandCallbackOpts) => {
const input = args.join(" ");

if (!input) {
const embed = new EmbedBuilder()
.setTitle("❌ Missing Input")
.setDescription("Please provide a Base64 string to decode.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}

try {
const decoded = Buffer.from(input, "base64").toString("utf8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decode operation lacks input size validation, which could cause memory exhaustion if an attacker provides an extremely large Base64 string. This could lead to service disruption or denial of service.

Confidence: 5/5

Suggested Fix

Add input size validation before attempting to decode. Limit the input to a reasonable size (e.g., 10MB when decoded):

Suggested change
const decoded = Buffer.from(input, "base64").toString("utf8");
const MAX_INPUT_SIZE = 10 * 1024 * 1024; // 10MB decoded size limit
const estimatedSize = (input.length * 3) / 4; // Base64 decoding size estimate
if (estimatedSize > MAX_INPUT_SIZE) {
const embed = new EmbedBuilder()
.setTitle("❌ Input Too Large")
.setDescription("The Base64 string is too large to decode. Maximum size is 10MB.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
const decoded = Buffer.from(input, "base64").toString("utf8");

Add size validation before the decode operation to prevent memory exhaustion attacks. The estimated decoded size is calculated as (input.length * 3) / 4 since Base64 encoding increases size by approximately 33%.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/tools/base64.ts at line 55, add input size validation before
decoding Base64 to prevent memory exhaustion attacks. Add a check that estimates
the decoded size using (input.length * 3) / 4 and rejects inputs that would
decode to more than 10MB, returning an appropriate error embed to the user.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decode operation lacks input size validation before creating a Buffer. A malicious user could provide an extremely large Base64 string, causing excessive memory allocation during decoding and potentially crashing the bot or causing service disruption.

Confidence: 5/5

Suggested Fix

Add input size validation before decoding to prevent memory exhaustion attacks:

Suggested change
const decoded = Buffer.from(input, "base64").toString("utf8");
const MAX_INPUT_SIZE = 100000; // 100KB limit
const input = args.join(" ");
if (!input) {
const embed = new EmbedBuilder()
.setTitle("❌ Missing Input")
.setDescription("Please provide a Base64 string to decode.")
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
if (input.length > MAX_INPUT_SIZE) {
const embed = new EmbedBuilder()
.setTitle("❌ Input Too Large")
.setDescription(`Base64 input is too large to decode. Maximum size: ${MAX_INPUT_SIZE} characters.`)
.setColor(0xe74c3c);
return message.reply({ embeds: [embed] });
}
try {
const decoded = Buffer.from(input, "base64").toString("utf8");

Add size validation immediately after checking if input exists to prevent memory exhaustion from oversized Base64 strings.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/tools/base64.ts around line 44-55, the decode function lacks input size validation before creating a Buffer, which could allow malicious users to cause memory exhaustion by providing extremely large Base64 strings; add a MAX_INPUT_SIZE constant (e.g., 100000 characters) and validate input.length against it after the empty check but before the try block, returning an appropriate error embed if the limit is exceeded.


if (!decoded || decoded.includes("\u0000")) {
throw new Error("Invalid Base64");
}
Comment on lines +54 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input length validation before Base64 decoding creates a potential Denial of Service (DoS) vulnerability. An attacker could provide an extremely long Base64 string that decodes to a massive UTF-8 output, causing memory exhaustion. Additionally, the validation logic at line 57 is insufficient - it only checks for null bytes but doesn't validate maximum decoded length or other problematic content.

Confidence: 5/5

Suggested Fix
Suggested change
try {
const decoded = Buffer.from(input, "base64").toString("utf8");
if (!decoded || decoded.includes("\u0000")) {
throw new Error("Invalid Base64");
}
try {
// Validate input length before decoding to prevent DoS
if (input.length > 10000) {
throw new Error("Input too long");
}
const decoded = Buffer.from(input, "base64").toString("utf8");
// Validate decoded output
if (!decoded) {
throw new Error("Empty decode result");
}
if (decoded.includes("\u0000")) {
throw new Error("Invalid Base64");
}
// Validate decoded length to prevent memory issues
if (decoded.length > 50000) {
throw new Error("Decoded output too large");
}

Add input validation before decoding to prevent DoS attacks. Validate both the input Base64 string length (before decoding) and the decoded output length (after decoding). The limits should be adjusted based on your application's requirements, but should prevent processing of maliciously large inputs.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/tools/base64.ts around line 54-59, add input validation before
Base64 decoding to prevent DoS attacks. Validate the input Base64 string length
before decoding (e.g., max 10000 characters) and validate the decoded output
length after decoding (e.g., max 50000 characters) to prevent memory exhaustion
from maliciously large inputs. Also improve the validation logic to distinguish
between empty results, null bytes, and oversized outputs with appropriate error
messages.

📍 This suggestion applies to lines 54-59


const embed = new EmbedBuilder()
.setTitle("🔓 Base64 Decode")
.addFields(
{
name: "Base64 Input",
value:
input.length > 1024
? "```" + input.slice(0, 1021) + "...```"
: "```" + input + "```",
},
{
name: "Decoded Text",
value:
decoded.length > 1024
? decoded.slice(0, 1021) + "..."
: decoded,
},
)
.setColor(0x3498db);

await message.reply({ embeds: [embed] });
} catch {
const embed = new EmbedBuilder()
.setTitle("❌ Invalid Base64")
.setDescription("That doesn't look like valid Base64.")
.setColor(0xe74c3c);
await message.reply({ embeds: [embed] });
}
},
},
};
11 changes: 5 additions & 6 deletions src/interactions/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ export default {
.setPlaceholder("Select a version to view its changes")
.addOptions(
versions.map((v, i) => ({
label: v.title.length > 100 ? v.title.slice(0, 97) + "..." : v.title,
label: (v.title.length > 100
? v.title.slice(0, 97) + "..."
: v.title
).replace(/`/g, ""),
value: i.toString(),
...(i === selectedIndex
? { description: "Currently viewing" }
: i === 0
? { description: "Latest version" }
: {}),
...(i === 0 ? { description: "Latest version" } : {}),
})),
);

Expand Down
Loading
Loading