Implement channel permissions resolution (#11)

* Add channel permission resolution

* Fix target ID handling in permission overwrite retrieval
This commit is contained in:
Slipstream 2025-06-10 15:43:54 -06:00 committed by GitHub
parent c6fb120449
commit 484f091897
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 201 additions and 0 deletions

View File

@ -21,6 +21,7 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
ButtonStyle, # Added for Button
# SelectMenuType will be part of ComponentType or a new enum if needed
)
from .permissions import Permissions
if TYPE_CHECKING:
@ -897,6 +898,78 @@ class Channel:
def __repr__(self) -> str:
return f"<Channel id='{self.id}' name='{self.name}' type='{self.type.name if hasattr(self.type, 'name') else self._type_val}'>"
def permission_overwrite_for(
self, target: Union["Role", "Member", str]
) -> Optional["PermissionOverwrite"]:
"""Return the :class:`PermissionOverwrite` for ``target`` if present."""
if isinstance(target, str):
target_id = int(target)
else:
target_id = target.id
for overwrite in self.permission_overwrites:
if overwrite.id == target_id:
return overwrite
return None
@staticmethod
def _apply_overwrite(
perms: Permissions, overwrite: Optional["PermissionOverwrite"]
) -> Permissions:
if overwrite is None:
return perms
perms &= ~Permissions(int(overwrite.deny))
perms |= Permissions(int(overwrite.allow))
return perms
def permissions_for(self, member: "Member") -> Permissions:
"""Resolve channel permissions for ``member``."""
if self.guild_id is None:
return Permissions(~0)
if not hasattr(self._client, "get_guild"):
return Permissions(0)
guild = self._client.get_guild(self.guild_id)
if guild is None:
return Permissions(0)
base = Permissions(0)
everyone = guild.get_role(guild.id)
if everyone is not None:
base |= Permissions(int(everyone.permissions))
for rid in member.roles:
role = guild.get_role(rid)
if role is not None:
base |= Permissions(int(role.permissions))
if base & Permissions.ADMINISTRATOR:
return Permissions(~0)
# Apply @everyone overwrite
base = self._apply_overwrite(base, self.permission_overwrite_for(guild.id))
# Role overwrites
role_allow = Permissions(0)
role_deny = Permissions(0)
for rid in member.roles:
ow = self.permission_overwrite_for(rid)
if ow is not None:
role_allow |= Permissions(int(ow.allow))
role_deny |= Permissions(int(ow.deny))
base &= ~role_deny
base |= role_allow
# Member overwrite
base = self._apply_overwrite(base, self.permission_overwrite_for(member.id))
return base
class TextChannel(Channel):
"""Represents a guild text channel or announcement channel."""

View File

@ -0,0 +1,128 @@
import pytest # pylint: disable=E0401
from disagreement.models import Guild, Member, Role, TextChannel, PermissionOverwrite
from disagreement.enums import (
ChannelType,
VerificationLevel,
MessageNotificationLevel,
ExplicitContentFilterLevel,
MFALevel,
GuildNSFWLevel,
PremiumTier,
OverwriteType,
)
from disagreement.permissions import Permissions
class DummyClient:
def __init__(self):
self._guilds = {}
def get_guild(self, gid):
return self._guilds.get(gid)
def _base_guild(client):
data = {
"id": "1",
"name": "g",
"owner_id": "1",
"afk_timeout": 60,
"verification_level": VerificationLevel.NONE.value,
"default_message_notifications": MessageNotificationLevel.ALL_MESSAGES.value,
"explicit_content_filter": ExplicitContentFilterLevel.DISABLED.value,
"roles": [],
"emojis": [],
"features": [],
"mfa_level": MFALevel.NONE.value,
"system_channel_flags": 0,
"premium_tier": PremiumTier.NONE.value,
"nsfw_level": GuildNSFWLevel.DEFAULT.value,
}
guild = Guild(data, client_instance=client)
client._guilds[guild.id] = guild
return guild
def _member(guild, *roles):
data = {
"user": {"id": "10", "username": "u", "discriminator": "0001"},
"joined_at": "t",
"roles": [r.id for r in roles] or [guild.id],
}
member = Member(data, client_instance=None)
member.guild_id = guild.id
guild._members[member.id] = member
return member
def _role(guild, rid, perms):
role = Role(
{
"id": rid,
"name": f"r{rid}",
"color": 0,
"hoist": False,
"position": 0,
"permissions": str(int(perms)),
"managed": False,
"mentionable": False,
}
)
guild.roles.append(role)
return role
def _channel(guild, client):
data = {
"id": "100",
"type": ChannelType.GUILD_TEXT.value,
"guild_id": guild.id,
"permission_overwrites": [],
}
channel = TextChannel(data, client_instance=client)
guild._channels[channel.id] = channel
return channel
def test_permissions_for_base_roles():
client = DummyClient()
guild = _base_guild(client)
everyone = _role(
guild, guild.id, Permissions.VIEW_CHANNEL | Permissions.SEND_MESSAGES
)
mod = _role(guild, "2", Permissions.MANAGE_MESSAGES)
member = _member(guild, everyone, mod)
channel = _channel(guild, client)
perms = channel.permissions_for(member)
assert perms & Permissions.MANAGE_MESSAGES
assert perms & Permissions.SEND_MESSAGES
assert perms & Permissions.VIEW_CHANNEL
def test_permissions_for_with_overwrite():
client = DummyClient()
guild = _base_guild(client)
everyone = _role(
guild, guild.id, Permissions.VIEW_CHANNEL | Permissions.SEND_MESSAGES
)
mod = _role(guild, "2", Permissions.MANAGE_MESSAGES)
member = _member(guild, everyone, mod)
channel = _channel(guild, client)
channel.permission_overwrites.append(
PermissionOverwrite(
{
"id": mod.id,
"type": OverwriteType.ROLE.value,
"allow": "0",
"deny": str(int(Permissions.MANAGE_MESSAGES)),
}
)
)
perms = channel.permissions_for(member)
assert not perms & Permissions.MANAGE_MESSAGES
assert perms & Permissions.SEND_MESSAGES
assert perms & Permissions.VIEW_CHANNEL