26.2 final push
This commit is contained in:
1 parent
2017c33756
commit
a746c468e5
14 files changed
+3048
-163
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "v26.2-pre2"
|
||||
SupportBot_Version: "v26.2"
|
||||
|
||||
# -------------------------
|
||||
# General
|
||||
|
||||
+33
-50
@@ -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,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[2026-08-28T16:37:42.883Z] [Database] Error initializing MySQL tables or state:
|
||||
@@ -0,0 +1 @@
|
||||
[2026-08-28T16:37:42.884Z] [Database] Falling back to SQLite database.
|
||||
@@ -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:
|
||||
|
||||
[]([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.
|
||||
@@ -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 || {};
|
||||
|
||||
Generated
+2625
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in new issue
Block a user