26.2 final push

This commit is contained in:
1tzEmerald committed 2026-08-28 17:48:09 +01:00
1 parent 2017c33756
commit a746c468e5
14 files changed
+3048 -163

No files matched your search

-3
View File
@@ -17,8 +17,5 @@ Logs/Warns/*
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
/dashboard/
# Local git hooks + secret-stripping scripts (run npm run git:setup)
scripts/
# Local secrets — use *.example as templates
Configs/api.yml
+37 -61
View File
@@ -177,6 +177,37 @@ function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, a
return container;
}
function getStaffTicketStats(viewingUserId) {
const allTickets = typeof db.getAllTickets === "function" ? db.getAllTickets() : [];
const ticketsClaimed = allTickets.filter(
(t) => t.claimedBy === viewingUserId || t.claimed_by === viewingUserId,
);
const ticketsOpen = ticketsClaimed.filter(
(t) => (t.status || "").toLowerCase() === "open",
);
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
const claimedAt = t.claimedAt || t.claimed_at;
const createdAt = t.createdAt || t.created_at;
if (claimedAt && createdAt) {
const diff = new Date(claimedAt).getTime() - new Date(createdAt).getTime();
if (diff > 0) {
totalResponseTime += diff;
responseCount++;
}
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
return { ticketsClaimed, ticketsOpen, avgMinutes };
}
function buildOpenTicketsContainer(ticketsOpen) {
const container = new ContainerBuilder();
@@ -192,7 +223,9 @@ function buildOpenTicketsContainer(ticketsOpen) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.",
ticketsOpen.length
? ticketsOpen.map((t) => `<#${t.ticket_id || t.id}>`).join("\n")
: "No active tickets.",
),
);
@@ -352,31 +385,7 @@ module.exports = new Command({
}
if (i.customId === "viewTicketStats") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
const totalTickets = ticketsClaimed.length;
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
if (t.claimedAt && t.createdAt) {
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
responseCount++;
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
const statsContainer = buildTicketStatsContainer(
viewingUserId,
@@ -403,17 +412,7 @@ module.exports = new Command({
}
if (i.customId === "showOpenTickets") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsOpen = TicketData.tickets.filter(
(t) => t.claimedBy === viewingUserId && t.open,
);
const { ticketsOpen } = getStaffTicketStats(viewingUserId);
const openContainer = buildOpenTicketsContainer(ticketsOpen);
@@ -431,30 +430,7 @@ module.exports = new Command({
}
if (i.customId === "backToStats") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
if (t.claimedAt && t.createdAt) {
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
responseCount++;
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
const statsContainer = buildTicketStatsContainer(
viewingUserId,
+1 -1
View File
@@ -74,7 +74,7 @@ module.exports = new Command({
}
],
run: async ({ interaction }) => {
async run(interaction) {
const subcommandGroup = interaction.options.getSubcommandGroup();
const subcommand = interaction.options.getSubcommand();
const user = interaction.options.getUser('user');
+5 -22
View File
@@ -1,35 +1,18 @@
const { Command } = require('../../Structures/Addon.js');
const Command = require("../../Structures/Command.js");
const Discord = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const supportbot = require("../../Structures/ConfigStore").supportbot;
const cmdconfig = require("../../Structures/ConfigStore").commands;
async function loadSettings() {
return new Promise((resolve, reject) => {
db.get("SELECT data FROM settings WHERE id = 1", (err, row) => {
if (err) return reject(err);
if (!row) return resolve({});
try {
resolve(JSON.parse(row.data));
} catch (e) {
resolve({});
}
});
});
return typeof db.getSettings === "function" ? db.getSettings() : {};
}
async function saveSettings(settings) {
const data = JSON.stringify(settings);
return new Promise((resolve, reject) => {
db.run(
"INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data",
[data],
(err) => (err ? reject(err) : resolve())
);
});
if (typeof db.saveSettings === "function") {
db.saveSettings(settings);
}
}
module.exports = new Command({
+8 -10
View File
@@ -59,20 +59,18 @@ Memory:
CustomInstructions:
- "You are SupportBot AI made by Emerald Services"
- "You are built with DiscordJS/NodeJS and currently running on dev build 26.0
build 2"
- "You are built with DiscordJS/NodeJS and currently running on version 26.2"
- "You're current build can be found here
https://github.com/Emerald-Services/SupportBot/tree/26.0"
https://github.com/Emerald-Services/SupportBot"
- "You're main build can be found here
https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/
and Polymart at
https://polymart.org/product/518/supportbot-1-discord-ticket-bot which is
version 8.4"
- "dev build 26.0 build 2 includes all-new AI features for SupportBot such as
memory system that allows you to remember facts and summaries of
conversations, which can be used to provide better responses."
- "SupportBot AI also have a new tickets AI Mode which is enabled in
supportbot.ymll, which allows you to assist users in support tickets and
https://polymart.org/product/518/supportbot-1-discord-ticket-bot"
- "SupportBot includes all-new AI features such as a memory system that allows
you to remember facts and summaries of conversations, which can be used to
provide better responses."
- "SupportBot AI also has a tickets AI Mode which is enabled in
supportbot.yml, which allows you to assist users in support tickets and
provide better responses based on the context of the ticket."
- "Your main purpose is to assist users in the support server and provide
helpful responses to their questions and issues. Always be accurate and if
+1 -1
View File
@@ -12,7 +12,7 @@
#
# SupportBot Configuration (supportbot.yml)
SupportBot_Version: "v26.2-pre2"
SupportBot_Version: "v26.2"
# -------------------------
# General
+33 -50
View File
@@ -867,69 +867,52 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
interaction.customId === "downvote" ||
interaction.customId === "removevote"
) {
const suggestion = suggestions[interaction.message.id];
if (!suggestion)
return interaction.reply({
content: "Suggestion not found.",
ephemeral: true,
});
const suggestion = (typeof db.getSuggestion === "function" ? db.getSuggestion(interaction.message.id) : null) || {
id: interaction.message.id,
author_id: "",
text: "",
status: "Created",
upvotes: [],
downvotes: [],
};
const userId = interaction.user.id;
const hasUpvoted = suggestion.upvotes.includes(userId);
const hasDownvoted = suggestion.downvotes.includes(userId);
if (interaction.customId === "upvote") {
if (hasUpvoted) {
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
} else {
suggestion.upvotes.push(userId);
if (hasDownvoted) {
suggestion.downvotes = suggestion.downvotes.filter(
(id) => id !== userId,
);
}
}
} else if (interaction.customId === "downvote") {
if (hasDownvoted) {
suggestion.downvotes = suggestion.downvotes.filter(
(id) => id !== userId,
);
} else {
suggestion.downvotes.push(userId);
if (hasUpvoted) {
suggestion.upvotes = suggestion.upvotes.filter(
(id) => id !== userId,
);
}
}
} else if (interaction.customId === "removevote") {
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
suggestion.downvotes = suggestion.downvotes.filter(
(id) => id !== userId,
);
if (supportbot.Suggestions?.OwnSuggestion === false && suggestion.author_id === userId) {
return interaction.reply({
content: "You cannot vote on your own suggestion.",
flags: Discord.MessageFlags.Ephemeral,
});
}
fs.writeFileSync(
"./Data/SuggestionData.json",
JSON.stringify(suggestions, null, 2),
);
const voteResult = typeof db.voteSuggestion === "function"
? db.voteSuggestion(interaction.message.id, userId, interaction.customId)
: { upvoteCount: 0, downvoteCount: 0 };
const upvoteCount = suggestion.upvotes.length;
const downvoteCount = suggestion.downvotes.length;
const oldEmbed = interaction.message.embeds?.[0];
if (!oldEmbed) {
return interaction.reply({
content: "Suggestion embed not found.",
flags: Discord.MessageFlags.Ephemeral,
});
}
const updatedEmbed = new Discord.EmbedBuilder(
interaction.message.embeds[0].data,
).setFields([
{ name: "Suggestion", value: suggestion.suggestion, inline: true },
{ name: "From", value: `<@${suggestion.author}>` },
const suggestionText = suggestion.text || oldEmbed.fields?.find((f) => f.name === "Suggestion")?.value || "";
const authorText = suggestion.author_id
? `<@${suggestion.author_id}>`
: (oldEmbed.fields?.find((f) => f.name === "From")?.value || `<@${userId}>`);
const updatedEmbed = Discord.EmbedBuilder.from(oldEmbed).setFields([
{ name: "Suggestion", value: suggestionText, inline: true },
{ name: "From", value: authorText },
{
name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`,
value: `${upvoteCount}`,
value: `${voteResult.upvoteCount}`,
inline: true,
},
{
name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`,
value: `${downvoteCount}`,
value: `${voteResult.downvoteCount}`,
inline: true,
},
]);
+1
View File
@@ -0,0 +1 @@
[2026-08-28T16:37:42.883Z] [Database] Error initializing MySQL tables or state:
+1
View File
@@ -0,0 +1 @@
[2026-08-28T16:37:42.884Z] [Database] Falling back to SQLite database.
+6 -14
View File
@@ -28,14 +28,10 @@
| SupportBot | Node.JS | Discord.JS | Supported |
|------------|---------|------------|------------|
| 26.x | 22.x | 14.x. | ✅ Yes
| 8.x | 22.x | 14.x. | ❌ Yes
| 7.7.3 | 16.x | 13.x. | ❌ No
| 7.x | 16.x | 13.x. | ❌ No
| 6.x | 14.x | 13.x. | ❌ No
| 5.x | 12.x | 13.x. | ❌ No
| 4.x | 11.x | 13.x. | ❌ No
| Older | 10.x | 13.x. | ❌ No
| 26.x | 22.x | 14.x | ✅ Yes |
| 8.x | 22.x | 14.x | ❌ No |
| 7.x | 16.x | 13.x | ❌ No |
| Older | 10.x | 13.x | ❌ No |
</div>
@@ -74,7 +70,7 @@ Released under the [MIT](https://opensource.org/licenses/MIT) license.
## Third-Party
- [Verified Addons](https://emeraldsrv.dev)
- [BuiltByBit]([https://builtbybit.com/supportbot](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/))
- [BuiltByBit](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/)
- [Voxel](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
@@ -86,8 +82,4 @@ Released under the [MIT](https://opensource.org/licenses/MIT) license.
## Hosting
You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host it on your own panel. We currently don't offer hosting for you, altough we might add this in the future.
If you don't know how to install pterodactyl, you could always host it on replit:
[![Run on Repl.it](https://repl.it/badge/github/ItsNyoty/IKI-bot)]([https://repl.it/github/ItsNyoty/iki](https://repl.it/github/Emerald-Services/SupportBot
)
You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host SupportBot on your own panel.
+216
View File
@@ -37,6 +37,7 @@ const state = {
profiles: {},
settings: {},
blacklisted_users: {},
suggestions: {},
};
// ----------------------------------------------------
@@ -114,6 +115,16 @@ function initSQLite() {
added_by TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS suggestions (
id TEXT PRIMARY KEY,
author_id TEXT,
text TEXT,
status TEXT DEFAULT 'Created',
upvotes TEXT DEFAULT '[]',
downvotes TEXT DEFAULT '[]',
created_at INTEGER
);
`);
loadSQLiteState();
@@ -132,6 +143,10 @@ function loadSQLiteState() {
department: r.department || "general",
priority: r.priority || "medium",
status: r.status || "open",
claimedBy: r.claimed_by || null,
claimed_by: r.claimed_by || null,
claimedAt: r.claimed_at || null,
claimed_at: r.claimed_at || null,
aiModeEnabled: r.ai_mode_enabled,
aiChannelId: r.ai_channel_id,
aiType: r.ai_type,
@@ -190,6 +205,31 @@ function loadSQLiteState() {
created_at: b.created_at,
};
}
const suggestions = sqliteDb.prepare("SELECT * FROM suggestions").all();
for (const s of suggestions) {
let upvotes = [];
let downvotes = [];
try {
upvotes = JSON.parse(s.upvotes || "[]");
} catch {
upvotes = [];
}
try {
downvotes = JSON.parse(s.downvotes || "[]");
} catch {
downvotes = [];
}
state.suggestions[s.id] = {
id: s.id,
author_id: s.author_id,
text: s.text,
status: s.status || "Created",
upvotes: Array.isArray(upvotes) ? upvotes : [],
downvotes: Array.isArray(downvotes) ? downvotes : [],
created_at: Number(s.created_at),
};
}
}
function initMySQL() {
@@ -275,6 +315,18 @@ function initMySQL() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS suggestions (
id VARCHAR(255) PRIMARY KEY,
author_id VARCHAR(255),
text TEXT,
status VARCHAR(255) DEFAULT 'Created',
upvotes LONGTEXT,
downvotes LONGTEXT,
created_at BIGINT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
// Load initial state from MySQL
const [tickets] = await poolPromise.query("SELECT * FROM tickets");
for (const r of tickets) {
@@ -286,6 +338,10 @@ function initMySQL() {
department: r.department || "general",
priority: r.priority || "medium",
status: r.status || "open",
claimedBy: r.claimed_by || null,
claimed_by: r.claimed_by || null,
claimedAt: r.claimed_at ? Number(r.claimed_at) : null,
claimed_at: r.claimed_at ? Number(r.claimed_at) : null,
aiModeEnabled: r.ai_mode_enabled,
aiChannelId: r.ai_channel_id,
aiType: r.ai_type,
@@ -345,6 +401,31 @@ function initMySQL() {
};
}
const [suggestions] = await poolPromise.query("SELECT * FROM suggestions");
for (const s of suggestions) {
let upvotes = [];
let downvotes = [];
try {
upvotes = JSON.parse(s.upvotes || "[]");
} catch {
upvotes = [];
}
try {
downvotes = JSON.parse(s.downvotes || "[]");
} catch {
downvotes = [];
}
state.suggestions[s.id] = {
id: s.id,
author_id: s.author_id,
text: s.text,
status: s.status || "Created",
upvotes: Array.isArray(upvotes) ? upvotes : [],
downvotes: Array.isArray(downvotes) ? downvotes : [],
created_at: Number(s.created_at),
};
}
mysqlReady = true;
const clr = { reset: "\x1b[0m", gray: "\x1b[90m", green: "\x1b[32m", white: "\x1b[37m" };
console.log(`${clr.green}✓${clr.reset} ${clr.white}Database Connected${clr.reset} ${clr.gray}to MySQL (${mysqlOpts.Host}:${mysqlOpts.Port}/${mysqlOpts.Database})${clr.reset}`);
@@ -639,6 +720,32 @@ function persistBlacklistedUser(userId, action = "add") {
}
}
function persistSuggestion(id) {
const s = state.suggestions[id];
if (!s) return;
const upvotesStr = JSON.stringify(s.upvotes || []);
const downvotesStr = JSON.stringify(s.downvotes || []);
if (isMySQL && mysqlReady && mysqlPool) {
const sql = `
INSERT INTO suggestions (id, author_id, text, status, upvotes, downvotes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE text=VALUES(text), status=VALUES(status), upvotes=VALUES(upvotes), downvotes=VALUES(downvotes);
`;
mysqlPool.query(sql, [s.id, s.author_id || "", s.text || "", s.status || "Created", upvotesStr, downvotesStr, s.created_at || Date.now()], (err) => {
if (err) console.error("[Database] MySQL persistSuggestion error:", err.message);
});
} else if (sqliteDb) {
const stmt = sqliteDb.prepare(`
INSERT INTO suggestions (id, author_id, text, status, upvotes, downvotes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET text=excluded.text, status=excluded.status, upvotes=excluded.upvotes, downvotes=excluded.downvotes;
`);
stmt.run(s.id, s.author_id || "", s.text || "", s.status || "Created", upvotesStr, downvotesStr, s.created_at || Date.now());
}
}
function ensureProfile(userId) {
if (!state.profiles[userId]) {
state.profiles[userId] = { user_id: userId, bio: "", timezone: "", clockedIn: 0 };
@@ -657,8 +764,15 @@ function normaliseTicketRow(row) {
}
}
const claimedBy = row.claimedBy || row.claimed_by || null;
const claimedAt = row.claimedAt || row.claimed_at || null;
return {
...row,
claimedBy,
claimed_by: claimedBy,
claimedAt,
claimed_at: claimedAt,
aiModeEnabled: Boolean(row.aiModeEnabled),
clockedIn: row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
questionAnswers,
@@ -882,6 +996,108 @@ const exportsModule = {
return Object.values(state.blacklisted_users);
},
claimTicket(ticketId, userId) {
const t = state.tickets[ticketId];
if (!t) return;
t.claimedBy = userId;
t.claimed_by = userId;
t.claimedAt = Date.now();
t.claimed_at = Date.now();
t.updated_at = Date.now();
persistTicket(ticketId);
},
addSuggestion(id, authorId, text) {
state.suggestions[id] = {
id,
author_id: authorId,
text,
status: "Created",
upvotes: [],
downvotes: [],
created_at: Date.now(),
};
persistSuggestion(id);
},
add(id, authorId, text) {
this.addSuggestion(id, authorId, text);
},
getSuggestion(id) {
const s = state.suggestions[id];
if (!s) return null;
return {
...s,
upvotes: Array.isArray(s.upvotes) ? s.upvotes : [],
downvotes: Array.isArray(s.downvotes) ? s.downvotes : [],
};
},
setSuggestionStatus(id, status) {
const s = state.suggestions[id];
if (!s) return;
s.status = status;
persistSuggestion(id);
},
setStatus(id, status) {
this.setSuggestionStatus(id, status);
},
voteSuggestion(id, userId, voteType) {
let s = state.suggestions[id];
if (!s) {
s = {
id,
author_id: "",
text: "",
status: "Created",
upvotes: [],
downvotes: [],
created_at: Date.now(),
};
state.suggestions[id] = s;
}
if (!Array.isArray(s.upvotes)) s.upvotes = [];
if (!Array.isArray(s.downvotes)) s.downvotes = [];
const hasUpvoted = s.upvotes.includes(userId);
const hasDownvoted = s.downvotes.includes(userId);
if (voteType === "upvote") {
if (hasUpvoted) {
s.upvotes = s.upvotes.filter((uid) => uid !== userId);
} else {
s.upvotes.push(userId);
if (hasDownvoted) {
s.downvotes = s.downvotes.filter((uid) => uid !== userId);
}
}
} else if (voteType === "downvote") {
if (hasDownvoted) {
s.downvotes = s.downvotes.filter((uid) => uid !== userId);
} else {
s.downvotes.push(userId);
if (hasUpvoted) {
s.upvotes = s.upvotes.filter((uid) => uid !== userId);
}
}
} else if (voteType === "removevote") {
s.upvotes = s.upvotes.filter((uid) => uid !== userId);
s.downvotes = s.downvotes.filter((uid) => uid !== userId);
}
persistSuggestion(id);
return {
suggestion: s,
upvoteCount: s.upvotes.length,
downvoteCount: s.downvotes.length,
};
},
getDatabaseInfo() {
if (isMySQL) {
const mysqlOpts = dbConfig.MySQL || {};
+2625
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "supportbot",
"version": "26.2-pre-2",
"version": "26.2.0",
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
"main": "index.js",
"scripts": {
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* SupportBot | Emerald Services
* Git Security Setup Script
*
* Sets up Git pre-commit hooks and safeguards to prevent sensitive credentials
* (Discord bot tokens, API keys, database credentials) from being accidentally committed.
*/
const fs = require("fs");
const path = require("path");
const ROOT_DIR = path.resolve(__dirname, "..");
const GIT_DIR = path.join(ROOT_DIR, ".git");
const HOOKS_DIR = path.join(GIT_DIR, "hooks");
const PRE_COMMIT_HOOK = path.join(HOOKS_DIR, "pre-commit");
const colors = {
reset: "\x1b[0m",
green: "\x1b[32m",
yellow: "\x1b[33m",
red: "\x1b[31m",
cyan: "\x1b[36m",
bold: "\x1b[1m",
gray: "\x1b[90m",
};
console.log(`\n${colors.bold}${colors.green}SupportBot Git Security Setup${colors.reset}`);
console.log(`${colors.gray}────────────────────────────────────────────────────────${colors.reset}`);
if (!fs.existsSync(GIT_DIR)) {
console.log(`${colors.yellow}[!] Warning: .git directory not found. Are you in a Git repository?${colors.reset}`);
process.exit(0);
}
if (!fs.existsSync(HOOKS_DIR)) {
fs.mkdirSync(HOOKS_DIR, { recursive: true });
}
// Pre-commit hook script
const hookContent = `#!/usr/bin/env node
/**
* SupportBot Git Pre-Commit Security Hook
* Scans staged configuration files for exposed secrets before committing.
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const ROOT = path.resolve(__dirname, "../..");
const SENSITIVE_PATTERNS = [
{ name: "Discord Bot Token", regex: /N[A-Za-z0-9_-]{23,28}\\.[A-Za-z0-9_-]{6}\\.[A-Za-z0-9_-]{27,}/ },
{ name: "Groq API Key", regex: /gsk_[A-Za-z0-9]{40,}/ },
{ name: "OpenAI API Key", regex: /sk-(?:proj-)?[A-Za-z0-9_-]{32,}/ },
{ name: "Anthropic API Key", regex: /sk-ant-[A-Za-z0-9_-]{32,}/ }
];
const CONFIG_FILES = [
"Configs/supportbot.yml",
"Configs/supportbot-ai.yml",
"Configs/api.yml"
];
let hasViolations = false;
try {
const stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf8" })
.split("\\n")
.map((f) => f.trim())
.filter(Boolean);
for (const file of stagedFiles) {
if (CONFIG_FILES.includes(file)) {
const fullPath = path.join(ROOT, file);
if (fs.existsSync(fullPath)) {
const content = fs.readFileSync(fullPath, "utf8");
for (const pattern of SENSITIVE_PATTERNS) {
if (pattern.regex.test(content)) {
console.error("\\x1b[31m\\x1b[1m[GIT SECURITY ALERT]\\x1b[0m Potential active " + pattern.name + " detected in " + file + "!");
console.error("\\x1b[33mPlease replace secrets with placeholders before committing.\\x1b[0m\\n");
hasViolations = true;
}
}
}
}
}
} catch (err) {
// If git command fails, do not block commit
}
if (hasViolations) {
console.error("\\x1b[31mCommit aborted due to sensitive credentials detected in staged files.\\x1b[0m");
process.exit(1);
}
process.exit(0);
`;
try {
fs.writeFileSync(PRE_COMMIT_HOOK, hookContent, { mode: 0o755 });
console.log(`${colors.green}✓${colors.reset} Pre-commit hook installed at ${colors.cyan}.git/hooks/pre-commit${colors.reset}`);
} catch (err) {
console.error(`${colors.red}✗ Failed to write pre-commit hook: ${err.message}${colors.reset}`);
}
console.log(`\n${colors.bold}Recommended local workflow for sensitive configs:${colors.reset}`);
console.log(`${colors.cyan} git update-index --assume-unchanged Configs/supportbot.yml${colors.reset}`);
console.log(`${colors.cyan} git update-index --assume-unchanged Configs/supportbot-ai.yml${colors.reset}`);
console.log(`${colors.cyan} git update-index --assume-unchanged Configs/api.yml${colors.reset}`);
console.log(`\n${colors.green}Security setup completed successfully!${colors.reset}\n`);