diff --git a/.gitignore b/.gitignore index 2b63492..b2109b0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,6 @@ config/dedup-*.json .worktrees/ PRODUCT.md -# Local skills (Claude Code / agent tooling) +# Local editor tooling .agents/ skills-lock.json diff --git a/bot/botUtils.js b/bot/botUtils.js index c557aaf..a28a3c6 100644 --- a/bot/botUtils.js +++ b/bot/botUtils.js @@ -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); diff --git a/bot/interactions.js b/bot/interactions.js index b72523d..3318327 100644 --- a/bot/interactions.js +++ b/bot/interactions.js @@ -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, }); } diff --git a/locales/de.json b/locales/de.json index 53fc41e..9f8844e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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." } } diff --git a/locales/en.json b/locales/en.json index e5efbdc..a1404e6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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." } } diff --git a/locales/template.json b/locales/template.json index c794ed7..1f01875 100644 --- a/locales/template.json +++ b/locales/template.json @@ -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": "" } }