discord.js机器人分配用户角色时报50013 Missing Permissions错误
基于discord.js开发的Discord服务器自动分配角色机器人,运行时抛出权限不足错误,相同代码1年前可正常运行,近期无改动突然失效。
已知前置条件:
- 机器人已被授予业务逻辑所需的全部权限
- 代码逻辑未尝试修改服务器所有者、角色层级高于机器人的用户的角色
/app/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js:85 new DiscordAPIError(res.request.path, res.body, res.request.method) : err); ^ DiscordAPIError: Missing Permissions at /app/node_modules/discord.js/src/client/rest/RequestHandlers/Sequential.js:85:15 at /app/node_modules/snekfetch/src/index.js:215:21 at processTicksAndRejections (node:internal/process/task_queues:96:5) { path: '/api/v7/guilds/715245634218885131/members/235148962103951360', code: 50013, method: 'PATCH' }
//basic constants and functions var checked; var checking = []; const Discord = require("discord.js"); const {usernameorid, token,usernamesp, roles, apiKey, spreadsheetId, range, unremoveableroles, refreshrate} = require("./config/discord.json"); const { google } = require("googleapis"); const connection = google.sheets({ version: "v4", auth: apiKey }); const client = new Discord.Client(); let oldRows = []; //checking for errors const logError = error => { console.error("error log " + new Date()); console.error(error); }; //the spreadsheet fetching const fetchRows = async (spreadsheetId, range, sheetsConnection) => { try { const response = await sheetsConnection.spreadsheets.values.get({ spreadsheetId: spreadsheetId, range: range }); //fetching all data const rows = response.data.values; //dividing the data between different array using while loop var i = 0; var k=0; while (i < rows.length) { const checkrow=""; const rows123= rows[i] //finally putting all roles together const therolenames = rows123[1]+","+rows123[2]+","+rows123[3]+","+rows123[4]; assignRoles(rows123[0].trim(), therolenames.trim()); rows123[0]==null; rows123[1]==null; rows123[2]==null; rows123[3]==null; rows123[4]==null; rows123[1]==null; roles[1]==null; roles[2]==null; roles[3]==null; roles[4]==null; therolenames==null; roles.length=0; rows123.length=0; i++; k++; } return rows; } catch (error) { logError(error); } }; const assignRoles = async (usernames, roleNames) => { var rolenames1 = roleNames.split(","); try { const guildMembers = client.guilds.array()[0].members.array(); //number of all members in the group const guildRoles = client.guilds.array()[0].roles.array(); // deleting unremovable roles from the list const Roles2 = guildRoles.filter(Role => !unremoveableroles.includes(Role.name)); // * getting Role instances of role names const removedRoles = guildRoles.filter(Role => unremoveableroles.includes(Role.name)); const Roles = guildRoles.filter(Role => roleNames.includes(Role.name)); //role number of all members in the group roleNames.length=0; // * getting GuildMember instances of usernames and setting roles guildMembers.forEach(async member => { const username= member.user.username + "#" + member.user.discriminator; //check if the first column of spreadsheet matches the userid/username of discord var UNcheck; if (usernameorid=="UN"){ UNcheck=username }else{ UNcheck=member.id } if (usernames.includes(UNcheck)) { const notAssignedRoles = []; Roles.forEach(role => { //if (!member.roles.array().includes(Roles)) { //if (!guildRoles.includes(Roles)) { checked="no" //Check whether the roles (discord) are in roles (spreadsheet) if (Roles.includes(member.roles.array()[1])|| removedRoles.includes(member.roles.array()[1])|| member.roles.array()[1]==null){ }else{checked="yes" } if (Roles.includes(member.roles.array()[2]) || removedRoles.includes(member.roles.array()[2]) || member.roles.array()[2]==null){ }else{checked="yes"; } if (Roles.includes(member.roles.array()[3]) || removedRoles.includes(member.roles.array()[3])|| member.roles.array()[3]==null){ }else{checked="yes"; } if (Roles.includes(member.roles.array()[4])|| removedRoles.includes(member.roles.array()[4]) || member.roles.array()[4]==null){ }else{checked="yes"; } //Check whether the roles (Spreadsheet) are in roles (Discord) if (member.roles.array().includes(Roles[0])|| removedRoles.includes(Roles[0]) || Roles[0]==null){ }else{checked="yes"; } if (member.roles.array().includes(Roles[1]) || removedRoles.includes(Roles[1])|| Roles[1]==null){ }else{checked="yes"; } if (member.roles.array().includes(Roles[2]) || removedRoles.includes(Roles[2]) || Roles[2]==null){ }else{checked="yes"; } if (member.roles.array().includes(Roles[3])|| removedRoles.includes(Roles[3]) || Roles[3]==null){ }else{checked="yes"; } if (checked=="yes"){ notAssignedRoles.push(role); } }); if (notAssignedRoles.length > 0) { await member.removeRoles(Roles2); member.addRoles(Roles); console.log( "Assigned " + notAssignedRoles.map(role => role.name) + " to " + username + " ["+member.id+"] on " + new Date().toString() ); } else { if (Roles[0]==null && Roles[1]==null && Roles[2]==null && Roles[3]==null) { member.removeRoles(Roles2); console.log(username + " ["+member.id+"] has all roles removed on"+ new Date().toString()); }else{ console.log(username + " ["+member.id+"] already has all the roles assigned, checked on "+ new Date().toString()); } } } }); } catch (err) { logError(err); } }; const extractNewEntries = (oldRows, rows) => { let newRows = []; if (rows.length > oldRows.length) { newRows = rows.slice(oldRows.length); } newRows.forEach(row => { oldRows.push(row); }); return newRows; }; const extractDiscordIDs = rows => { return rows.map(user => user[0]); console.log(rows.map(user => user[0])); }; client.once( "ready", () => { console.log("Bot started with these settings:"); console.log("• Spreadsheet ID: "); console.log("• Range: " + range); console.log("• Roles: " + roles.join(", ")); setInterval(async () => { console.log("\nChecked for new entries on " + new Date().toString()); let rows; try { rows = await fetchRows(spreadsheetId, range, connection); const newEntries = extractNewEntries(oldRows, rows); if (newEntries.length > 0) { console.log(`Found ${newEntries.length} new entries`); const usernames = extractDiscordIDs(newEntries); } else { console.log("No new entries"); } } catch (err) { if (rows === undefined) console.error("Google Sheet is empty"); logError(err); rows = []; } }, refreshrate); // refresh rate: 60000 milliseconds == 1 minute }, logError ); client.on("error", logError); const start = async () => { try { await client.login(token); } catch (err) { logError(err); setTimeout(start, 30000); } }; start(); client.on("disconnect", start); module.exports = { fetchRows, assignRoles, extractNewEntries, extractDiscordIDs }; const express = require("express"); const fs = require("fs"); const app = express(); app.use(express.static("public")); const listener = app.listen(process.env.PORT, function() { console.log("Your app is listening on port " + listener.address().port); });
依赖版本与API版本过时
项目使用的discord.js为v11版本,调用的是Discord API v7接口,该版本API早已停止维护并下线,Discord侧对旧接口的权限校验规则、数据返回逻辑均已变更,旧接口调用会出现非预期的权限拒绝。从报错路径/api/v7/可以直接确认该问题。缺少特权网关意图配置
Discord从2021年开始强制要求所有操作服务器成员数据的机器人开启「服务器成员意图」特权开关,代码初始化Client时既没有声明需要的intents参数,也没有在Discord开发者后台开启对应特权开关,导致机器人能获取到的成员缓存、角色数据不完整,操作不存在/不匹配的对象时触发权限错误。角色层级校验缺失
代码中Roles2集合包含了服务器内所有不在不可移除列表里的角色,没有校验这些角色的排序位置是否低于机器人自身的最高角色。Discord权限规则明确要求:机器人只能操作层级低于自身最高角色的其他角色,只要待移除/添加的角色列表里有任意一个角色层级高于机器人,哪怕操作对象是普通用户,接口也会直接返回50013权限不足错误。目标服务器选择逻辑bug
代码通过client.guilds.array()[0]取缓存中第一个服务器作为操作目标,如果机器人加入了多个服务器,会直接操作非目标服务器,在非目标服务器内机器人没有配置对应角色和权限,自然会抛出权限错误。
- 升级discord.js到v14稳定版本,适配当前官方支持的Discord API v10接口,替换旧版本废弃的
array()、removeRoles()、addRoles()等方法,改用新版的cache、member.roles.set()等标准接口。 - 初始化Client时传入需要的intents配置,同时到Discord开发者后台的机器人设置页,开启特权网关意图分类下的服务器成员意图、消息内容意图开关。
- 不要用数组下标取服务器,直接通过配置的目标服务器ID获取Guild实例,避免操作错服务器。
- 操作角色前先过滤掉所有层级大于等于机器人最高角色的角色,不要尝试操作这类无权限的角色。
- 替换原有先全量移除角色再添加角色的逻辑,直接用
member.roles.set()方法一次性设置用户最终的角色列表,减少接口调用次数和权限触发概率。
内容的提问来源于stack exchange,提问作者Suprin Aziz Talpur

