discordjs / discord.js

A powerful JavaScript library for interacting with the Discord API
https://discord.js.org
Apache License 2.0
25.45k stars 3.97k forks source link

`GuildMember` and `GuildChannel` permission-based methods do not reflect permission changes on timeouts #9730

Open Bloonatics opened 1 year ago

Bloonatics commented 1 year ago

Which package is this bug report for?

discord.js

Issue description

Description

According to docs, timed out members should have lost all permissions except VIEW_CHANNEL and READ_MESSAGE_HISTORY, if they do not have ADMINISTRATOR permission. However, the methods mentioned below do not reflect the permission changes on timeouts. I understand that we can check if a guild member is timed out with GuildMember.isCommunicationDisabled() and many other ways, and then make a function to obtain more accurate permissions, but having a all-in-one method to check the current permissions accurately is more convenient for us.

image

Impacts

Code reference

GuildChannel.permissionsFor(memberOrRole, [checkAdmin])

https://github.com/discordjs/discord.js/blob/d26e022afcc84448bb91deb4dd621aea20df6617/packages/discord.js/src/structures/GuildChannel.js#L174-L179

https://github.com/discordjs/discord.js/blob/d26e022afcc84448bb91deb4dd621aea20df6617/packages/discord.js/src/structures/GuildChannel.js#L215-L237

GuildMember.permissions

https://github.com/discordjs/discord.js/blob/d26e022afcc84448bb91deb4dd621aea20df6617/packages/discord.js/src/structures/GuildMember.js#L254-L257

GuildMember.permissionsIn(channel)

https://github.com/discordjs/discord.js/blob/d26e022afcc84448bb91deb4dd621aea20df6617/packages/discord.js/src/structures/GuildMember.js#L320-L324

Code sample

No response

Versions

Issue priority

Medium (should be fixed soon)

Which partials do you have configured?

Not applicable

Which gateway intents are you subscribing to?

Not applicable

I have tested this issue on a development release

No response

jaw0r3k commented 1 year ago

Thats just a bitfield, whose behaviour we shouldnt change Better to add methods like TextChannel#sendableFor to allow more complex checks

Jiralite commented 11 months ago

One thing I have noticed is an interaction's appPermissions will change its bit field if timed out. Currently, non-interaction-based permission checks do not change their bitfield.

Bloonatics commented 8 months ago

Suggestion

This is one of the solutions which is a breaking change.

GuildChannel#permissionsFor()

Before:

https://github.com/discordjs/discord.js/blob/a1a3a95c94194a8ab789d567a778b376e13ea973/packages/discord.js/src/structures/GuildChannel.js#L175-L180

After:

permissionsFor(memberOrRole, { checkAdmin = true, checkTimeout = true } = {}) {
   const member = this.guild.members.resolve(memberOrRole);
   if (member) return this.memberPermissions(member, { checkAdmin, checkTimeout });
   const role = this.guild.roles.resolve(memberOrRole);
   return role && this.rolePermissions(role, checkAdmin);
 }

GuildChannel#memberPermissions() Private Method

Before:

https://github.com/discordjs/discord.js/blob/a1a3a95c94194a8ab789d567a778b376e13ea973/packages/discord.js/src/structures/GuildChannel.js#L216-L238

After:

memberPermissions(member, { checkAdmin, checkTimeout }) {
   if (checkAdmin && member.id === this.guild.ownerId) {
     return new PermissionsBitField(PermissionsBitField.All).freeze();
   }

   const roles = member.roles.cache;
   const rolePermissions = new PermissionsBitField(roles.map(role => role.permissions));

   if (checkAdmin && rolePermissions.has(PermissionFlagsBits.Administrator)) {
     return new PermissionsBitField(PermissionsBitField.All).freeze();
   }

   const overwrites = this.overwritesFor(member, true, roles);

   const channelPermissions = rolePermissions
     .remove(overwrites.everyone?.deny ?? PermissionsBitField.DefaultBit)
     .add(overwrites.everyone?.allow ?? PermissionsBitField.DefaultBit)
     .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : PermissionsBitField.DefaultBit)
     .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : PermissionsBitField.DefaultBit)
     .remove(overwrites.member?.deny ?? PermissionsBitField.DefaultBit)
     .add(overwrites.member?.allow ?? PermissionsBitField.DefaultBit);

   if (
     !checkTimeout
     || member.id === this.guild.ownerId
     || channelPermissions.has(PermissionFlagsBits.Administrator)
     || !member.isCommunicationDisabled()
   ) {
     return channelPermissions.freeze();
   }

   return new PermissionsBitField(
     channelPermissions.has(PermissionFlagsBits.ViewChannel) ? PermissionFlagsBits.ViewChannel : PermissionsBitField.DefaultBit |
     channelPermissions.has(PermissionFlagsBits.ReadMessageHistory) ? PermissionFlagsBits.ReadMessageHistory : PermissionsBitField.DefaultBit
   ).freeze();
 }