From 2f5351b323be3a0f72a502d94450f923bfa7ebf0 Mon Sep 17 00:00:00 2001 From: DysektAI <33468668+DysektAI@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:49:04 -0400 Subject: [PATCH 1/2] fix: whitelist admins from honeypot and make warning more prominent Reuse the shared ADMIN_ROLE_ID config (via hasAdminRole) so members with an admin role are never banned/kicked when posting in the honeypot channel. Also update the warning message to a large heading so users clearly understand the consequence of posting. --- src/handlers/honeypotHandler.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/handlers/honeypotHandler.js b/src/handlers/honeypotHandler.js index 3c0c6c7..b271b47 100644 --- a/src/handlers/honeypotHandler.js +++ b/src/handlers/honeypotHandler.js @@ -1,4 +1,5 @@ import { Events } from "discord.js"; +import { hasAdminRole } from "../config/constants.js"; // Honeypot channel: a hidden channel that real users should never post in. // Bots/spam accounts that find it and send a message are banned on sight. @@ -10,7 +11,7 @@ const HONEYPOT_CHANNEL_ID = process.env.HONEYPOT_CHANNEL_ID; // across restarts without persisting state. const HONEYPOT_MARKER = ""; -const HONEYPOT_WARNING = `${HONEYPOT_MARKER}\n**⚠️ Honeypot channel** — do not post here.`; +const HONEYPOT_WARNING = `${HONEYPOT_MARKER}\n# ⛔️⚠️ DO NOT SEND MESSAGES HERE, YOU WILL BE BANNED INSTANTLY ⚠️⛔️`; const SIXTEEN_HOURS_MS = 16 * 60 * 60 * 1000; const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // hourly @@ -51,6 +52,10 @@ export function setupHoneypot(client) { if (message.channelId !== HONEYPOT_CHANNEL_ID) return; if (message.author.bot) return; + // Admins are whitelisted so the honeypot can never ban/kick them, even if + // they post here intentionally. Uses the shared ADMIN_ROLE_ID config. + if (message.member && hasAdminRole(message.member.roles.cache)) return; + try { await message.delete(); } catch (err) { From 9aa72949db15cd523384daee5fa838714804fbe2 Mon Sep 17 00:00:00 2001 From: DysektAI <33468668+DysektAI@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:56:45 -0400 Subject: [PATCH 2/2] fix: fail safe when resolving honeypot members --- src/handlers/honeypotHandler.js | 23 ++++++---- test/honeypot.test.js | 80 ++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/handlers/honeypotHandler.js b/src/handlers/honeypotHandler.js index b271b47..482d112 100644 --- a/src/handlers/honeypotHandler.js +++ b/src/handlers/honeypotHandler.js @@ -52,17 +52,24 @@ export function setupHoneypot(client) { if (message.channelId !== HONEYPOT_CHANNEL_ID) return; if (message.author.bot) return; - // Admins are whitelisted so the honeypot can never ban/kick them, even if - // they post here intentionally. Uses the shared ADMIN_ROLE_ID config. - if (message.member && hasAdminRole(message.member.roles.cache)) return; - try { - await message.delete(); + // Fetch the member when Discord did not hydrate it in the event so the + // admin exemption cannot be bypassed by a missing message.member value. + const member = message.member ?? await message.guild.members.fetch(message.author.id); + if (hasAdminRole(member.roles.cache)) return; + + try { + await message.delete(); + } catch (err) { + console.error("Failed to delete honeypot trigger message:", err); + } + + await punishAuthor(message); } catch (err) { - console.error("Failed to delete honeypot trigger message:", err); + // Fail safe: if member resolution or role inspection fails, do not risk + // punishing an administrator. + console.error("Failed to handle honeypot message:", err); } - - await punishAuthor(message); }); } diff --git a/test/honeypot.test.js b/test/honeypot.test.js index 3f3c84f..b003d1a 100644 --- a/test/honeypot.test.js +++ b/test/honeypot.test.js @@ -9,6 +9,19 @@ async function importHoneypot() { return import(url); } +function createFakeClient() { + const onEvents = {}; + const onceEvents = {}; + return { + onEvents, + onceEvents, + client: { + on: (evt, fn) => { onEvents[evt] = fn; }, + once: (evt, fn) => { onceEvents[evt] = fn; } + } + }; +} + test("setupHoneypot is a no-op when HONEYPOT_CHANNEL_ID is unset", async () => { delete process.env.HONEYPOT_CHANNEL_ID; const { setupHoneypot } = await importHoneypot(); @@ -29,14 +42,9 @@ test("setupHoneypot registers listeners when enabled", async () => { process.env.HONEYPOT_CHANNEL_ID = "123"; const { setupHoneypot } = await importHoneypot(); - const onEvents = {}; - const onceEvents = {}; - const fakeClient = { - on: (evt, fn) => { onEvents[evt] = fn; }, - once: (evt, fn) => { onceEvents[evt] = fn; } - }; + const { client, onEvents, onceEvents } = createFakeClient(); - setupHoneypot(fakeClient); + setupHoneypot(client); assert.ok(onEvents.messageCreate, "MessageCreate listener should be registered via .on"); // ClientReady must be registered via .once (not .on) to avoid timer leaks on reconnect. assert.ok(onceEvents.clientReady, "ClientReady handler should be registered via .once"); @@ -44,3 +52,61 @@ test("setupHoneypot registers listeners when enabled", async () => { delete process.env.HONEYPOT_CHANNEL_ID; }); + +test("honeypot resolves a missing member before enforcing the admin exemption", async () => { + process.env.HONEYPOT_CHANNEL_ID = "123"; + const { adminRoles } = await import("../src/config/constants.js"); + adminRoles.add("admin"); + const { setupHoneypot } = await importHoneypot(); + const { client, onEvents } = createFakeClient(); + setupHoneypot(client); + + let deleted = false; + let banned = false; + const adminMember = { roles: { cache: [{ id: "admin" }] } }; + await onEvents.messageCreate({ + channelId: "123", + author: { id: "user", bot: false }, + member: null, + guild: { + members: { fetch: async () => adminMember }, + bans: { create: async () => { banned = true; } } + }, + delete: async () => { deleted = true; } + }); + + assert.equal(deleted, false, "admin message should not be deleted"); + assert.equal(banned, false, "admin should not be banned"); + adminRoles.delete("admin"); + delete process.env.HONEYPOT_CHANNEL_ID; +}); + +test("honeypot fails safe when a missing member cannot be resolved", async () => { + process.env.HONEYPOT_CHANNEL_ID = "123"; + const { setupHoneypot } = await importHoneypot(); + const { client, onEvents } = createFakeClient(); + setupHoneypot(client); + + let deleted = false; + let banned = false; + const originalConsoleError = console.error; + console.error = () => {}; + try { + await onEvents.messageCreate({ + channelId: "123", + author: { id: "user", bot: false }, + member: null, + guild: { + members: { fetch: async () => { throw new Error("unavailable"); } }, + bans: { create: async () => { banned = true; } } + }, + delete: async () => { deleted = true; } + }); + } finally { + console.error = originalConsoleError; + } + + assert.equal(deleted, false, "message should remain when roles cannot be checked"); + assert.equal(banned, false, "author should not be punished when roles cannot be checked"); + delete process.env.HONEYPOT_CHANNEL_ID; +});