mirror of
				https://github.com/Rapptz/discord.py.git
				synced 2025-10-25 02:23:04 +00:00 
			
		
		
		
	Fix potential conflicts in snowflake keys
This can happen on really old channels with the same ID as the guild ID and having a command with both a role and a channel.
This commit is contained in:
		| @@ -50,7 +50,7 @@ import re | |||||||
| from .enums import AppCommandOptionType, AppCommandType | from .enums import AppCommandOptionType, AppCommandType | ||||||
| from ..interactions import Interaction | from ..interactions import Interaction | ||||||
| from ..enums import ChannelType, try_enum | from ..enums import ChannelType, try_enum | ||||||
| from .models import Choice | from .models import AppCommandChannel, AppCommandThread, Choice | ||||||
| from .errors import CommandSignatureMismatch, CommandAlreadyRegistered | from .errors import CommandSignatureMismatch, CommandAlreadyRegistered | ||||||
| from ..utils import resolve_annotation, MISSING, is_inside_class | from ..utils import resolve_annotation, MISSING, is_inside_class | ||||||
| from ..user import User | from ..user import User | ||||||
| @@ -195,6 +195,8 @@ annotation_to_option_type: Dict[Any, AppCommandOptionType] = { | |||||||
|     User: AppCommandOptionType.user, |     User: AppCommandOptionType.user, | ||||||
|     Member: AppCommandOptionType.user, |     Member: AppCommandOptionType.user, | ||||||
|     Role: AppCommandOptionType.role, |     Role: AppCommandOptionType.role, | ||||||
|  |     AppCommandChannel: AppCommandOptionType.channel, | ||||||
|  |     AppCommandThread: AppCommandOptionType.channel, | ||||||
|     # StageChannel: AppCommandOptionType.channel, |     # StageChannel: AppCommandOptionType.channel, | ||||||
|     # StoreChannel: AppCommandOptionType.channel, |     # StoreChannel: AppCommandOptionType.channel, | ||||||
|     # VoiceChannel: AppCommandOptionType.channel, |     # VoiceChannel: AppCommandOptionType.channel, | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. | |||||||
|  |  | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Tuple | from typing import TYPE_CHECKING, Any, Dict, Iterable, List, NamedTuple, Tuple | ||||||
| from ..interactions import Interaction | from ..interactions import Interaction | ||||||
| from ..member import Member | from ..member import Member | ||||||
| from ..object import Object | from ..object import Object | ||||||
| @@ -32,11 +32,35 @@ from ..role import Role | |||||||
| from ..message import Message, Attachment | from ..message import Message, Attachment | ||||||
| from ..channel import PartialMessageable | from ..channel import PartialMessageable | ||||||
| from .models import AppCommandChannel, AppCommandThread | from .models import AppCommandChannel, AppCommandThread | ||||||
|  | from .enums import AppCommandOptionType | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption |     from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ResolveKey(NamedTuple): | ||||||
|  |     id: str | ||||||
|  |     # CommandOptionType does not use 0 or negative numbers so those can be safe for library | ||||||
|  |     # internal use, if necessary. Likewise, only 6, 7, 8, and 11 are actually in use. | ||||||
|  |     type: int | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def any_with(cls, id: str) -> ResolveKey: | ||||||
|  |         return ResolveKey(id=id, type=-1) | ||||||
|  |  | ||||||
|  |     def __eq__(self, o: object) -> bool: | ||||||
|  |         if not isinstance(o, ResolveKey): | ||||||
|  |             return NotImplemented | ||||||
|  |         if self.type == -1 or o.type == -1: | ||||||
|  |             return self.id == o.id | ||||||
|  |         return (self.id, self.type) == (o.id, o.type) | ||||||
|  |  | ||||||
|  |     def __hash__(self) -> int: | ||||||
|  |         # Most of the time an ID lookup is all that is necessary | ||||||
|  |         # In case of collision then we look up both the ID and the type. | ||||||
|  |         return hash(self.id) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Namespace: | class Namespace: | ||||||
|     """An object that holds the parameters being passed to a command in a mostly raw state. |     """An object that holds the parameters being passed to a command in a mostly raw state. | ||||||
|  |  | ||||||
| @@ -103,47 +127,62 @@ class Namespace: | |||||||
|             elif opt_type in (6, 7, 8, 9, 11): |             elif opt_type in (6, 7, 8, 9, 11): | ||||||
|                 # Remaining ones should be snowflake based ones with resolved data |                 # Remaining ones should be snowflake based ones with resolved data | ||||||
|                 snowflake: str = option['value']  # type: ignore -- Key is there |                 snowflake: str = option['value']  # type: ignore -- Key is there | ||||||
|                 value = completed.get(snowflake) |                 if opt_type == 9:  # Mentionable | ||||||
|  |                     # Mentionable is User | Role, these do not cause any conflict | ||||||
|  |                     key = ResolveKey.any_with(snowflake) | ||||||
|  |                 else: | ||||||
|  |                     # The remaining keys can conflict, for example, a role and a channel | ||||||
|  |                     # could end up with the same ID in very old guilds since they used to default | ||||||
|  |                     # to sharing the guild ID. Old general channels no longer exist, but some old | ||||||
|  |                     # servers will still have them so this needs to be handled. | ||||||
|  |                     key = ResolveKey(id=snowflake, type=opt_type) | ||||||
|  |  | ||||||
|  |                 value = completed.get(key) | ||||||
|                 self.__dict__[name] = value |                 self.__dict__[name] = value | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) -> Dict[str, Any]: |     def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) -> Dict[ResolveKey, Any]: | ||||||
|         completed: Dict[str, Any] = {} |         completed: Dict[ResolveKey, Any] = {} | ||||||
|         state = interaction._state |         state = interaction._state | ||||||
|         members = resolved.get('members', {}) |         members = resolved.get('members', {}) | ||||||
|         guild_id = interaction.guild_id |         guild_id = interaction.guild_id | ||||||
|         guild = (state._get_guild(guild_id) or Object(id=guild_id)) if guild_id is not None else None |         guild = (state._get_guild(guild_id) or Object(id=guild_id)) if guild_id is not None else None | ||||||
|  |         type = AppCommandOptionType.user.value | ||||||
|         for (user_id, user_data) in resolved.get('users', {}).items(): |         for (user_id, user_data) in resolved.get('users', {}).items(): | ||||||
|             try: |             try: | ||||||
|                 member_data = members[user_id] |                 member_data = members[user_id] | ||||||
|             except KeyError: |             except KeyError: | ||||||
|                 completed[user_id] = state.create_user(user_data) |                 completed[ResolveKey(id=user_id, type=type)] = state.create_user(user_data) | ||||||
|             else: |             else: | ||||||
|                 member_data['user'] = user_data |                 member_data['user'] = user_data | ||||||
|                 # Guild ID can't be None in this case. |                 # Guild ID can't be None in this case. | ||||||
|                 # There's a type mismatch here that I don't actually care about |                 # There's a type mismatch here that I don't actually care about | ||||||
|                 member = Member(state=state, guild=guild, data=member_data)  # type: ignore |                 member = Member(state=state, guild=guild, data=member_data)  # type: ignore | ||||||
|                 completed[user_id] = member |                 completed[ResolveKey(id=user_id, type=type)] = member | ||||||
|  |  | ||||||
|  |         type = AppCommandOptionType.role.value | ||||||
|         completed.update( |         completed.update( | ||||||
|             { |             { | ||||||
|                 # The guild ID can't be None in this case. |                 # The guild ID can't be None in this case. | ||||||
|                 role_id: Role(guild=guild, state=state, data=role_data)  # type: ignore |                 ResolveKey(id=role_id, type=type): Role(guild=guild, state=state, data=role_data)  # type: ignore | ||||||
|                 for role_id, role_data in resolved.get('roles', {}).items() |                 for role_id, role_data in resolved.get('roles', {}).items() | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         type = AppCommandOptionType.channel.value | ||||||
|         for (channel_id, channel_data) in resolved.get('channels', {}).items(): |         for (channel_id, channel_data) in resolved.get('channels', {}).items(): | ||||||
|  |             key = ResolveKey(id=channel_id, type=type) | ||||||
|             if channel_data['type'] in (10, 11, 12): |             if channel_data['type'] in (10, 11, 12): | ||||||
|                 # The guild ID can't be none in this case |                 # The guild ID can't be none in this case | ||||||
|                 completed[channel_id] = AppCommandThread(state=state, data=channel_data, guild_id=guild_id)  # type: ignore |                 completed[key] = AppCommandThread(state=state, data=channel_data, guild_id=guild_id)  # type: ignore | ||||||
|             else: |             else: | ||||||
|                 # The guild ID can't be none in this case |                 # The guild ID can't be none in this case | ||||||
|                 completed[channel_id] = AppCommandChannel(state=state, data=channel_data, guild_id=guild_id)  # type: ignore |                 completed[key] = AppCommandChannel(state=state, data=channel_data, guild_id=guild_id)  # type: ignore | ||||||
|  |  | ||||||
|  |         type = AppCommandOptionType.attachment.value | ||||||
|         completed.update( |         completed.update( | ||||||
|             { |             { | ||||||
|                 attachment_id: Attachment(data=attachment_data, state=state) |                 ResolveKey(id=attachment_id, type=type): Attachment(data=attachment_data, state=state) | ||||||
|                 for attachment_id, attachment_data in resolved.get('attachments', {}).items() |                 for attachment_id, attachment_data in resolved.get('attachments', {}).items() | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| @@ -157,7 +196,9 @@ class Namespace: | |||||||
|                 channel = guild.get_channel_or_thread(channel_id) or PartialMessageable(state=state, id=channel_id) |                 channel = guild.get_channel_or_thread(channel_id) or PartialMessageable(state=state, id=channel_id) | ||||||
|  |  | ||||||
|             # Type checker doesn't understand this due to failure to narrow |             # Type checker doesn't understand this due to failure to narrow | ||||||
|             completed[message_id] = Message(state=state, channel=channel, data=message_data)  # type: ignore |             message = Message(state=state, channel=channel, data=message_data)  # type: ignore | ||||||
|  |             key = ResolveKey(id=message_id, type=-1) | ||||||
|  |             completed[key] = message | ||||||
|  |  | ||||||
|         return completed |         return completed | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ import inspect | |||||||
| from typing import Callable, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, Union, overload | from typing import Callable, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, Union, overload | ||||||
|  |  | ||||||
|  |  | ||||||
| from .namespace import Namespace | from .namespace import Namespace, ResolveKey | ||||||
| from .models import AppCommand | from .models import AppCommand | ||||||
| from .commands import Command, ContextMenu, Group, _shorten | from .commands import Command, ContextMenu, Group, _shorten | ||||||
| from .enums import AppCommandType | from .enums import AppCommandType | ||||||
| @@ -532,8 +532,14 @@ class CommandTree: | |||||||
|             raise CommandNotFound(name, [], AppCommandType(type)) |             raise CommandNotFound(name, [], AppCommandType(type)) | ||||||
|  |  | ||||||
|         resolved = Namespace._get_resolved_items(interaction, data.get('resolved', {})) |         resolved = Namespace._get_resolved_items(interaction, data.get('resolved', {})) | ||||||
|  |  | ||||||
|  |         target_id = data.get('target_id') | ||||||
|  |         # Right now, the only types are message and user | ||||||
|  |         # Therefore, there's no conflict with snowflakes | ||||||
|  |  | ||||||
|         # This will always work at runtime |         # This will always work at runtime | ||||||
|         value = resolved.get(data.get('target_id'))  # type: ignore |         key = ResolveKey.any_with(target_id)  # type: ignore | ||||||
|  |         value = resolved.get(key) | ||||||
|         if ctx_menu.type.value != type: |         if ctx_menu.type.value != type: | ||||||
|             raise CommandSignatureMismatch(ctx_menu) |             raise CommandSignatureMismatch(ctx_menu) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user