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: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ config/dedup-*.json
.worktrees/
PRODUCT.md

# Local skills (Claude Code / agent tooling)
# Local editor tooling
.agents/
skills-lock.json
55 changes: 44 additions & 11 deletions bot/botUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,54 @@ export function parseQualityAndServerOptions(options, mediaType, isAnime = false
return { profileId, serverId };
}

export function checkRolePermission(member) {
if (!member || !member.roles) return true;
// Config problems are logged when they appear, not on every interaction
const activeRoleConfigErrors = new Map();
function reportRoleConfigError(name, value, message) {
if (activeRoleConfigErrors.get(name) === value) return;
activeRoleConfigErrors.set(name, value);
logger.error(message);
}

let allowlist = [];
let blocklist = [];
try {
allowlist = process.env.ROLE_ALLOWLIST ? JSON.parse(process.env.ROLE_ALLOWLIST) : [];
} catch (e) {
logger.error(`Invalid JSON in ROLE_ALLOWLIST — allowlist disabled, all users permitted: ${e.message}`);
// Returns the parsed role ID list, or null if the value is set but invalid
function parseRoleList(name) {
const raw = process.env[name];
if (!raw) {
activeRoleConfigErrors.delete(name);
return [];
}
let parsed;
try {
blocklist = process.env.ROLE_BLOCKLIST ? JSON.parse(process.env.ROLE_BLOCKLIST) : [];
} catch (e) {
logger.error(`Invalid JSON in ROLE_BLOCKLIST — blocklist disabled, no users blocked: ${e.message}`);
parsed = JSON.parse(raw);
} catch {
reportRoleConfigError(name, raw, `Invalid JSON in ${name}: denying role-restricted interactions`);
return null;
}
if (Array.isArray(parsed) && parsed.every((id) => typeof id === "string")) {
activeRoleConfigErrors.delete(name);
return parsed;
}
reportRoleConfigError(name, raw, `${name} is not an array of role IDs: denying role-restricted interactions`);
return null;
}

export function checkRolePermission(member, guildId) {
const allowlist = parseRoleList("ROLE_ALLOWLIST");
const blocklist = parseRoleList("ROLE_BLOCKLIST");
if (allowlist === null || blocklist === null) return false;

if (allowlist.length === 0 && blocklist.length === 0) {
activeRoleConfigErrors.delete("GUILD_ID");
return true;
}

// Roles are only meaningful in the configured guild; DMs and other guilds are denied
if (!member?.roles?.cache) return false;
if (!process.env.GUILD_ID) {
reportRoleConfigError("GUILD_ID", "", "Role lists are set but GUILD_ID is empty: denying role-restricted interactions");
return false;
}
activeRoleConfigErrors.delete("GUILD_ID");
if (guildId !== process.env.GUILD_ID) return false;

const userRoles = member.roles.cache.map((r) => r.id);

Expand Down
21 changes: 14 additions & 7 deletions bot/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,17 +317,24 @@ async function handleSearchOrRequest(
export function registerInteractions(client) {
client.on("interactionCreate", async (interaction) => {
try {
// Check role permissions for all commands, buttons, and select menus
// Check role permissions for all commands, buttons, select menus, and autocomplete
if (
interaction.isCommand() ||
interaction.isButton() ||
(interaction.isStringSelectMenu() &&
!interaction.customId.startsWith("request_seasons|") &&
!interaction.customId.startsWith("request_with_tags|"))
interaction.isStringSelectMenu() ||
interaction.isAutocomplete()
) {
if (!checkRolePermission(interaction.member)) {
return interaction.reply({
content: "❌ You don't have permission to use this command.",
if (!checkRolePermission(interaction.member, interaction.guildId)) {
if (interaction.isAutocomplete()) return await interaction.respond([]);
logger.info(
`[PERMISSION] Denied interaction from user ${interaction.user.id} (${interaction.guildId ? `guild ${interaction.guildId}` : "DM"})`
);
return await interaction.reply({
content: t(
interaction.guildId
? "permission_errors.denied"
: "permission_errors.dm_denied"
),
flags: 64,
});
}
Expand Down
4 changes: 4 additions & 0 deletions locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,5 +492,9 @@
"generic": "⚠️ Seerr-Fehler: {msg}",
"unreachable": "⚠️ Seerr konnte nicht erreicht werden. Prüfe, ob die Seerr-URL korrekt und erreichbar ist.",
"unknown": "⚠️ Ein Fehler ist aufgetreten: {msg}"
},
"permission_errors": {
"denied": "❌ Du hast keine Berechtigung, diesen Befehl zu verwenden.",
"dm_denied": "❌ Dieser Bot ist auf Server-Rollen beschränkt. Bitte nutze den Befehl im Server."
}
}
4 changes: 4 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,5 +425,9 @@
"generic": "⚠️ Seerr error: {msg}",
"unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.",
"unknown": "⚠️ An error occurred: {msg}"
},
"permission_errors": {
"denied": "❌ You don't have permission to use this command.",
"dm_denied": "❌ This bot is restricted to server roles. Please use this command in the server."
}
}
4 changes: 4 additions & 0 deletions locales/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,5 +425,9 @@
"generic": "⚠️ Seerr error: {msg}",
"unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.",
"unknown": "⚠️ An error occurred: {msg}"
},
"permission_errors": {
"denied": "",
"dm_denied": ""
}
}
Loading