- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 481
refactor: Rewrite the role tags mess #2708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Draft
      
      
            Paillat-dev
  wants to merge
  12
  commits into
  Pycord-Development:master
  
    
      
        
          
  
    
      Choose a base branch
      
     
    
      
        
      
      
        
          
          
        
        
          
            
              
              
              
  
           
        
        
          
            
              
              
           
        
       
     
  
        
          
            
          
            
          
        
       
    
      
from
Paillat-dev:feat/role-tags-rewrite
  
      
      
   
  
    
  
  
  
 
  
      
    base: master
Could not load branches
            
              
  
    Branch not found: {{ refName }}
  
            
                
      Loading
              
            Could not load tags
            
            
              Nothing to show
            
              
  
            
                
      Loading
              
            Are you sure you want to change the base?
            Some commits from the old base branch may be removed from the timeline,
            and old review comments may become outdated.
          
          
  
     Draft
                    Changes from all commits
      Commits
    
    
            Show all changes
          
          
            12 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      696dcbf
              
                :recycle: Rewrite `RoleTags.bot_id` handling
              
              
                Paillat-dev 54d030d
              
                :recycle: Rewrite this mess
              
              
                Paillat-dev 5810fa3
              
                :sparkles: Things
              
              
                Paillat-dev 6a91875
              
                :bug: Who tf made this messed up system
              
              
                Paillat-dev 54f1baa
              
                :memo: Better docs
              
              
                Paillat-dev 9801d5c
              
                Merge branch 'master' into feat/role-tags-rewrite
              
              
                Paillat-dev 5ac16bb
              
                :adhesive_bandage: Fix error when tags is None
              
              
                Paillat-dev 304d70b
              
                :memo: CHANGELOG.md
              
              
                Paillat-dev 7357716
              
                Merge remote-tracking branch 'origin/master' into feat/role-tags-rewrite
              
              
                Paillat-dev 2dc7e65
              
                Merge remote-tracking branch 'pycord/master' into feat/role-tags-rewrite
              
              
                Paillat-dev 3b34892
              
                :label: Add missing typing
              
              
                Paillat-dev 3273324
              
                Merge remote-tracking branch 'pycord/master' into feat/role-tags-rewrite
              
              
                Paillat-dev File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -25,6 +25,7 @@ | |
|  | ||
| from __future__ import annotations | ||
|  | ||
| from enum import IntEnum | ||
| from typing import TYPE_CHECKING, Any, TypeVar | ||
|  | ||
| from .asset import Asset | ||
|  | @@ -33,7 +34,13 @@ | |
| from .flags import RoleFlags | ||
| from .mixins import Hashable | ||
| from .permissions import Permissions | ||
| from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time | ||
| from .utils import ( | ||
| MISSING, | ||
| _bytes_to_base64_data, | ||
| cached_slot_property, | ||
| deprecated, | ||
| snowflake_time, | ||
| ) | ||
|  | ||
| __all__ = ( | ||
| "RoleTags", | ||
|  | @@ -51,20 +58,117 @@ | |
| from .types.role import RoleTags as RoleTagPayload | ||
|  | ||
|  | ||
| def _parse_tag_bool(data: RoleTagPayload, key: str) -> bool | None: | ||
| """Parse a boolean from a role tag payload. | ||
|  | ||
| None is returned if the key is not present. | ||
| True is returned if the key is present and the value is None. | ||
| False is returned if the key is present and the value is not None. | ||
|  | ||
| Parameters | ||
| ---------- | ||
| data: :class:`RoleTagPayload` | ||
| The role tag payload to parse from. | ||
| key: :class:`str` | ||
| The key to parse from. | ||
|  | ||
| Returns | ||
| ------- | ||
| :class:`bool` | :class:`None` | ||
| The parsed boolean value or None if the key is not present. | ||
| """ | ||
| try: | ||
| # if it is False, False != None -> False | ||
| # if it is None, None == None -> True | ||
| return data[key] is None | ||
| except KeyError: | ||
| # if the key is not present, None | ||
| return None | ||
|  | ||
|  | ||
| def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: | ||
| """Parse an integer from a role tag payload. | ||
|  | ||
| An integer is returned if the key is present and the value is an integer string. | ||
| None is returned if the key is not present or the value is not an integer string. | ||
|  | ||
| Parameters | ||
| ---------- | ||
| data: :class:`RoleTagPayload` | ||
| The role tag payload to parse from. | ||
| key: :class:`str` | ||
| The key to parse from. | ||
|  | ||
| Returns | ||
| ------- | ||
| :class:`int` | :class:`None` | ||
| The parsed integer value or None if the key is not present or the value is not an integer string. | ||
| """ | ||
| try: | ||
| return int(data[key]) # pyright: ignore[reportUnknownArgumentType] | ||
| except (KeyError, ValueError): | ||
| # key error means it's not there | ||
| # value error means it's not an number string (None or "") | ||
| return None | ||
|  | ||
|  | ||
| class RoleType(IntEnum): | ||
| """Represents the type of role. | ||
|  | ||
| This is NOT provided by discord but is rather computed by pycord based on the role tags. | ||
|  | ||
| .. versionadded:: 2.7 | ||
|  | ||
| Attributes | ||
| ---------- | ||
| NORMAL: :class:`int` | ||
| The role is a normal role. | ||
| APPLICATION: :class:`int` | ||
| The role is an application (bot) role. | ||
| BOOSTER: :class:`int` | ||
| The role is a guild's booster role. | ||
| GUILD_PRODUCT: :class:`int` | ||
| The role is a guild product role. | ||
| PREMIUM_SUBSCRIPTION_BASE: :class:`int` | ||
| The role is a base subscription role. This is not possible to determine currently, will be INTEGRATION if it's a base subscription. | ||
| PREMIUM_SUBSCRIPTION_TIER: :class:`int` | ||
| The role is a subscription role. | ||
| DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` | ||
| The role is a draft subscription role. | ||
| INTEGRATION: :class:`int` | ||
| The role is an integration role, such as Twitch or YouTube, or a base subscription role. | ||
| CONNECTION: :class:`int` | ||
| The role is a guild connections role. | ||
| UNKNOWN: :class:`int` | ||
| The role type is unknown. | ||
| """ | ||
|  | ||
| NORMAL = 0 | ||
| APPLICATION = 1 | ||
| BOOSTER = 2 | ||
| GUILD_PRODUCT = 3 | ||
| PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription | ||
| PREMIUM_SUBSCRIPTION_TIER = 5 | ||
| DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 | ||
| INTEGRATION = 7 | ||
| CONNECTION = 8 | ||
| UNKNOWN = 9 | ||
|  | ||
|  | ||
| class RoleTags: | ||
| """Represents tags on a role. | ||
|  | ||
| A role tag is a piece of extra information attached to a managed role | ||
| that gives it context for the reason the role is managed. | ||
|  | ||
| While this can be accessed, a useful interface is also provided in the | ||
| :class:`Role` and :class:`Guild` classes as well. | ||
|  | ||
| Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. | ||
| We aim to improve the documentation / introduce new attributes in future. | ||
| For the meantime read `this <https://discord-lib-devs.notion.site/special-roles-role-tags>`_ if you need detailed information about how role tags work. | ||
| In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. | ||
| If you find an issue, please open an issue on `GitHub <https://github.com/Pycord-Development/pycord/issues/new?template=bug_report.yml>`_. | ||
| Read `this <https://discord-lib-devs.notion.site/special-roles-role-tags>`_ if you need detailed information about how role tags work. | ||
|  | ||
| .. versionadded:: 1.6 | ||
| .. versionchanged:: 2.7 | ||
| The type of the role is now determined by the :attr:`RoleTags.type` attribute. | ||
|  | ||
| Attributes | ||
| ---------- | ||
|  | @@ -79,70 +183,106 @@ class RoleTags: | |
| """ | ||
|  | ||
| __slots__ = ( | ||
| "bot_id", | ||
| "integration_id", | ||
| "subscription_listing_id", | ||
| "_premium_subscriber", | ||
| "_available_for_purchase", | ||
| "_guild_connections", | ||
| "_is_guild_product_role", | ||
| "bot_id", | ||
| "_data", | ||
| "_type", | ||
| ) | ||
|  | ||
| _type: RoleType | ||
|  | ||
| def __init__(self, data: RoleTagPayload): | ||
| self.bot_id: int | None = _get_as_snowflake(data, "bot_id") | ||
| self.integration_id: int | None = _get_as_snowflake(data, "integration_id") | ||
| self.subscription_listing_id: int | None = _get_as_snowflake( | ||
| self._data: RoleTagPayload = data | ||
| self.integration_id: int | None = _parse_tag_int(data, "integration_id") | ||
| self.subscription_listing_id: int | None = _parse_tag_int( | ||
| data, "subscription_listing_id" | ||
| ) | ||
| # NOTE: The API returns "null" for each of the following tags if they are True, and omits them if False. | ||
| # However, "null" corresponds to None. | ||
| # This is different from other fields where "null" means "not there". | ||
| # So in this case, a value of None is the same as True. | ||
| # Which means we would need a different sentinel. | ||
| self._premium_subscriber: Any | None = data.get("premium_subscriber", MISSING) | ||
| self._available_for_purchase: Any | None = data.get( | ||
| "available_for_purchase", MISSING | ||
| self.bot_id: int | None = _parse_tag_int(data, "bot_id") | ||
| self._guild_connections: bool | None = _parse_tag_bool( | ||
| data, "guild_connections" | ||
| ) | ||
| self._guild_connections: Any | None = data.get("guild_connections", MISSING) | ||
|  | ||
| self._premium_subscriber: bool | None = _parse_tag_bool( | ||
| data, "premium_subscriber" | ||
| ) | ||
| self._available_for_purchase: bool | None = _parse_tag_bool( | ||
| data, "available_for_purchase" | ||
| ) | ||
| # here discord did things in a normal and logical way for once | ||
| self._is_guild_product_role: bool | None = data.get("is_guild_product_role") | ||
|  | ||
| @cached_slot_property("_type") | ||
| def type(self) -> RoleType: | ||
| """Determine the role type based on tag flags.""" | ||
| # Bot role | ||
| if self.bot_id is not None: | ||
| return RoleType.APPLICATION | ||
|  | ||
| # Role connection | ||
| if self._guild_connections is True: | ||
| return RoleType.CONNECTION | ||
|  | ||
| # Paid roles | ||
| if self._is_guild_product_role is True: | ||
| return RoleType.GUILD_PRODUCT | ||
|  | ||
| # Booster role | ||
| if self._premium_subscriber is True: | ||
| return RoleType.BOOSTER | ||
|  | ||
| # subscription roles | ||
| if ( | ||
| self.integration_id is not None | ||
| and self._premium_subscriber is None | ||
| and self.subscription_listing_id is not None | ||
| ): | ||
| if self._available_for_purchase is True: | ||
| return RoleType.PREMIUM_SUBSCRIPTION_TIER | ||
| return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER | ||
|  | ||
| # integration role (Twitch/YouTube) | ||
| if self.integration_id is not None: | ||
| return RoleType.INTEGRATION | ||
|  | ||
| # Seeing how messed up this is it wouldn't be a surprise if this happened | ||
| return RoleType.UNKNOWN | ||
|  | ||
| @deprecated("RoleTags.type", "2.7") | ||
| def is_bot_managed(self) -> bool: | ||
| """Whether the role is associated with a bot.""" | ||
| return self.bot_id is not None | ||
|  | ||
| @deprecated("RoleTags.type", "2.7") | ||
| def is_premium_subscriber(self) -> bool: | ||
| """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" | ||
| return self._premium_subscriber is None | ||
|  | ||
| @deprecated("RoleTags.type", "2.7") | ||
| def is_integration(self) -> bool: | ||
| """Whether the guild manages the role through some form of | ||
| integrations such as Twitch or through guild subscriptions. | ||
| """ | ||
| return self.integration_id is not None | ||
|  | ||
| @deprecated("RoleTags.type", "2.7") | ||
| def is_available_for_purchase(self) -> bool: | ||
| """Whether the role is available for purchase. | ||
|  | ||
| Returns ``True`` if the role is available for purchase, and | ||
| ``False`` if it is not available for purchase or if the role | ||
| is not linked to a guild subscription. | ||
|  | ||
| .. versionadded:: 2.7 | ||
| """ | ||
| return self._available_for_purchase is None | ||
| """Whether the role is available for purchase.""" | ||
| return self._available_for_purchase is True | ||
|  | ||
| @deprecated("RoleTags.type", "2.7") | ||
| def is_guild_connections_role(self) -> bool: | ||
| """Whether the role is a guild connections role. | ||
|  | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait this could be removed if this pr is merged before 2.7 since it was added after 2.6.1. Same for  | ||
| .. versionadded:: 2.7 | ||
| """ | ||
| return self._guild_connections is None | ||
| """Whether the role is a guild connections role.""" | ||
| return self._guild_connections is True | ||
|  | ||
| def __repr__(self) -> str: | ||
| return ( | ||
| f"<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} " | ||
| f"subscription_listing_id={self.subscription_listing_id} " | ||
| f"premium_subscriber={self.is_premium_subscriber()} " | ||
| f"available_for_purchase={self.is_available_for_purchase()} " | ||
| f"guild_connections={self.is_guild_connections_role()}>" | ||
| + f"subscription_listing_id={self.subscription_listing_id} " | ||
| + f"type={self.type!r}>" | ||
| ) | ||
|  | ||
|  | ||
|  | @@ -216,7 +356,8 @@ class Role(Hashable): | |
| mentionable: :class:`bool` | ||
| Indicates if the role can be mentioned by users. | ||
| tags: Optional[:class:`RoleTags`] | ||
| The role tags associated with this role. | ||
| The role tags associated with this role. Use the tags to determine additional information about the role, | ||
| like if it's a bot role, a booster role, etc... | ||
| unicode_emoji: Optional[:class:`str`] | ||
| The role's unicode emoji. | ||
| Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. | ||
|  | @@ -316,20 +457,23 @@ def is_default(self) -> bool: | |
| """Checks if the role is the default role.""" | ||
| return self.guild.id == self.id | ||
|  | ||
| @deprecated("Role.type", "2.7") | ||
| def is_bot_managed(self) -> bool: | ||
| """Whether the role is associated with a bot. | ||
|  | ||
| .. versionadded:: 1.6 | ||
| """ | ||
| return self.tags is not None and self.tags.is_bot_managed() | ||
|  | ||
| @deprecated("Role.type", "2.7") | ||
| def is_premium_subscriber(self) -> bool: | ||
| """Whether the role is the premium subscriber, AKA "boost", role for the guild. | ||
|  | ||
| .. versionadded:: 1.6 | ||
| """ | ||
| return self.tags is not None and self.tags.is_premium_subscriber() | ||
|  | ||
| @deprecated("Role.type", "2.7") | ||
| def is_integration(self) -> bool: | ||
| """Whether the guild manages the role through some form of | ||
| integrations such as Twitch or through guild subscriptions. | ||
|  | @@ -350,6 +494,7 @@ def is_assignable(self) -> bool: | |
| and (me.top_role > self or me.id == self.guild.owner_id) | ||
| ) | ||
|  | ||
| @deprecated("Role.type", "2.7") | ||
| def is_available_for_purchase(self) -> bool: | ||
| """Whether the role is available for purchase. | ||
|  | ||
|  | @@ -361,6 +506,7 @@ def is_available_for_purchase(self) -> bool: | |
| """ | ||
| return self.tags is not None and self.tags.is_available_for_purchase() | ||
|  | ||
| @deprecated("Role.type", "2.7") | ||
| def is_guild_connections_role(self) -> bool: | ||
| """Whether the role is a guild connections role. | ||
|  | ||
|  | @@ -414,6 +560,14 @@ def icon(self) -> Asset | None: | |
|  | ||
| return Asset._from_icon(self._state, self.id, self._icon, "role") | ||
|  | ||
| @property | ||
| def type(self) -> RoleType: | ||
| """The type of the role. | ||
|  | ||
| .. versionadded:: 2.7 | ||
| """ | ||
| return self.tags.type if self.tags is not None else RoleType.NORMAL | ||
|  | ||
| async def _move(self, position: int, reason: str | None) -> None: | ||
| if position <= 0: | ||
| raise InvalidArgument("Cannot move role to position 0 or below") | ||
|  | ||
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.