Browse Source

Implement joining guilds from id & lurking, bug fixes, doc fixes

pull/10109/head
dolfies 3 years ago
parent
commit
388863e521
  1. 171
      discord/client.py
  2. 9
      discord/gateway.py
  3. 47
      discord/guild.py
  4. 147
      discord/http.py
  5. 4
      discord/invite.py
  6. 2
      discord/team.py
  7. 60
      discord/tracking.py
  8. 28
      discord/types/guild.py
  9. 14
      discord/user.py

171
discord/client.py

@ -232,8 +232,8 @@ class Client:
self._client_status: _ClientStatus = _ClientStatus() self._client_status: _ClientStatus = _ClientStatus()
self._client_activities: Dict[Optional[str], Tuple[ActivityTypes, ...]] = { self._client_activities: Dict[Optional[str], Tuple[ActivityTypes, ...]] = {
None: None, None: tuple(),
'this': None, 'this': tuple(),
} }
self._session_count = 1 self._session_count = 1
@ -275,8 +275,8 @@ class Client:
activities = self.initial_activities activities = self.initial_activities
status = self.initial_status status = self.initial_status
if status is None: if status is None:
status = getattr(state.settings, 'status', None) status = getattr(state.settings, 'status', None) or Status.online
self.loop.create_task(self.change_presence(activities=activities, status=status)) self.loop.create_task(self.change_presence(activities=activities, status=status)) # type: ignore
@property @property
def latency(self) -> float: def latency(self) -> float:
@ -435,7 +435,11 @@ class Client:
if not self._sync_presences: if not self._sync_presences:
return return
if old_settings._status == new_settings._status and old_settings._custom_status == new_settings._custom_status: if (
old_settings is not None
and old_settings._status == new_settings._status
and old_settings._custom_status == new_settings._custom_status
):
return # Nothing changed return # Nothing changed
status = new_settings.status status = new_settings.status
@ -443,7 +447,7 @@ class Client:
if (activity := new_settings.custom_activity) is not None: if (activity := new_settings.custom_activity) is not None:
activities.append(activity) activities.append(activity)
await self.change_presence(status=status, activities=activities, edit_settings=False) await self.change_presence(status=status, activities=activities, edit_settings=False) # type: ignore
# Hooks # Hooks
@ -582,7 +586,7 @@ class Client:
except ReconnectWebSocket as e: except ReconnectWebSocket as e:
_log.info('Got a request to %s the websocket.', e.op) _log.info('Got a request to %s the websocket.', e.op)
self.dispatch('disconnect') self.dispatch('disconnect')
ws_params.update(sequence=self.ws.sequence, resume=e.resume, session=self.ws.session_id) ws_params.update(sequence=self.ws.sequence, resume=e.resume, session=self.ws.session_id) # type: ignore - These are always present at this point
continue continue
except ( except (
OSError, OSError,
@ -606,7 +610,7 @@ class Client:
# If we get connection reset by peer then try to RESUME # If we get connection reset by peer then try to RESUME
if isinstance(exc, OSError) and exc.errno in (54, 10054): if isinstance(exc, OSError) and exc.errno in (54, 10054):
ws_params.update(sequence=self.ws.sequence, initial=False, resume=True, session=self.ws.session_id) ws_params.update(sequence=self.ws.sequence, initial=False, resume=True, session=self.ws.session_id) # type: ignore - These are always present at this point
continue continue
# We should only get this when an unhandled close code happens, # We should only get this when an unhandled close code happens,
@ -624,7 +628,7 @@ class Client:
# Always try to RESUME the connection # Always try to RESUME the connection
# If the connection is not RESUME-able then the gateway will invalidate the session # If the connection is not RESUME-able then the gateway will invalidate the session
# This is apparently what the official Discord client does # This is apparently what the official Discord client does
ws_params.update(sequence=self.ws.sequence, resume=True, session=self.ws.session_id) ws_params.update(sequence=self.ws.sequence, resume=True, session=self.ws.session_id) # type: ignore - These are always present at this point
async def close(self) -> None: async def close(self) -> None:
"""|coro| """|coro|
@ -734,8 +738,7 @@ class Client:
if value is None: if value is None:
self._connection._activities = [] self._connection._activities = []
elif isinstance(value, BaseActivity): elif isinstance(value, BaseActivity):
# ConnectionState._activities is typehinted as List[ActivityPayload], we're passing List[Dict[str, Any]] self._connection._activities = [value.to_dict()]
self._connection._activities = [value.to_dict()] # type: ignore
else: else:
raise TypeError('activity must derive from BaseActivity') raise TypeError('activity must derive from BaseActivity')
@ -750,8 +753,7 @@ class Client:
if not values: if not values:
self._connection._activities = [] self._connection._activities = []
elif all(isinstance(value, BaseActivity) for value in values): elif all(isinstance(value, BaseActivity) for value in values):
# ConnectionState._activities is typehinted as List[ActivityPayload], we're passing List[Dict[str, Any]] self._connection._activities = [value.to_dict() for value in values]
self._connection._activities = [value.to_dict() for value in values] # type: ignore
else: else:
raise TypeError('activity must derive from BaseActivity') raise TypeError('activity must derive from BaseActivity')
@ -854,7 +856,7 @@ class Client:
than 128 characters. See :issue:`1738` for more information. than 128 characters. See :issue:`1738` for more information.
""" """
state = self._connection state = self._connection
activities = tuple(create_activity(d, state) for d in self._client_activities[None]) activities = tuple(create_activity(d, state) for d in self._client_activities[None]) # type: ignore
if activities is None and not self.is_closed(): if activities is None and not self.is_closed():
activities = getattr(state.settings, 'custom_activity', []) activities = getattr(state.settings, 'custom_activity', [])
activities = [activities] if activities else activities activities = [activities] if activities else activities
@ -1363,7 +1365,7 @@ class Client:
payload: Dict[str, Any] = {'status': status} payload: Dict[str, Any] = {'status': status}
payload['custom_activity'] = custom_activity payload['custom_activity'] = custom_activity
await self.user.edit_settings(**payload) await self.user.edit_settings(**payload) # type: ignore - user is always present when logged in
status_str = str(status) status_str = str(status)
activities_tuple = tuple(a.to_dict() for a in activities) activities_tuple = tuple(a.to_dict() for a in activities)
@ -1433,7 +1435,7 @@ class Client:
Parameters Parameters
----------- -----------
with_counts: :class:`bool` with_counts: :class:`bool`
Whether to return approximate :attr:`.Guild.member_count` and :attr:`.Guild.presence_count`. Whether to fill :attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count`.
Defaults to ``True``. Defaults to ``True``.
Raises Raises
@ -1448,7 +1450,10 @@ class Client:
""" """
state = self._connection state = self._connection
guilds = await state.http.get_guilds(with_counts) guilds = await state.http.get_guilds(with_counts)
return [Guild(data=data, state=state) for data in guilds] guilds = [Guild(data=data, state=state) for data in guilds]
for guild in guilds:
guild._cs_joined = True
return guilds
async def fetch_template(self, code: Union[Template, str]) -> Template: async def fetch_template(self, code: Union[Template, str]) -> Template:
"""|coro| """|coro|
@ -1517,6 +1522,28 @@ class Client:
The guild from the ID. The guild from the ID.
""" """
data = await self.http.get_guild(guild_id, with_counts) data = await self.http.get_guild(guild_id, with_counts)
guild = Guild(data=data, state=self._connection)
guild._cs_joined = True
return guild
async def fetch_guild_preview(self, guild_id: int, /) -> Guild:
"""|coro|
Retrieves a public :class:`.Guild` preview from an ID.
Raises
------
NotFound
Guild with given ID does not exist/is not public.
HTTPException
Retrieving the guild failed.
Returns
--------
:class:`.Guild`
The guild from the ID.
"""
data = await self.http.get_guild_preview(guild_id)
return Guild(data=data, state=self._connection) return Guild(data=data, state=self._connection)
async def create_guild( async def create_guild(
@ -1570,7 +1597,69 @@ class Client:
data = await self.http.create_from_template(code, name, icon_base64) data = await self.http.create_from_template(code, name, icon_base64)
else: else:
data = await self.http.create_guild(name, icon_base64) data = await self.http.create_guild(name, icon_base64)
return Guild(data=data, state=self._connection)
guild = Guild(data=data, state=self._connection)
guild._cs_joined = True
return guild
async def join_guild(self, guild_id: int, /, lurking: bool = False) -> Guild:
"""|coro|
Joins a discoverable :class:`.Guild`.
Parameters
-----------
guild_id: :class:`int`
The ID of the guild to join.
lurking: :class:`bool`
Whether to lurk the guild.
Raises
-------
NotFound
Guild with given ID does not exist/have discovery enabled.
HTTPException
Joining the guild failed.
Returns
--------
:class:`.Guild`
The guild that was joined.
"""
state = self._connection
data = await state.http.join_guild(guild_id, lurking, state.session_id)
guild = Guild(data=data, state=state)
guild._cs_joined = not lurking
return guild
async def leave_guild(self, guild: Snowflake, /, lurking: bool = MISSING) -> None:
"""|coro|
Leaves a guild. Equivalent to :meth:`Guild.leave`.
.. versionadded:: 2.0
Parameters
-----------
guild: :class:`abc.Snowflake`
The guild to leave.
lurking: :class:`bool`
Whether you are lurking the guild.
Raises
-------
HTTPException
Leaving the guild failed.
"""
lurking = lurking if lurking is not MISSING else MISSING
if lurking is MISSING:
attr = getattr(guild, 'joined', lurking)
if attr is not MISSING:
lurking = not attr
elif (new_guild := self._connection._get_guild(guild.id)) is not None:
lurking = not new_guild.joined
await self.http.leave_guild(guild.id, lurking=lurking)
async def fetch_stage_instance(self, channel_id: int, /) -> StageInstance: async def fetch_stage_instance(self, channel_id: int, /) -> StageInstance:
"""|coro| """|coro|
@ -1700,7 +1789,6 @@ class Client:
HTTPException HTTPException
Revoking the invite failed. Revoking the invite failed.
""" """
resolved = utils.resolve_invite(invite) resolved = utils.resolve_invite(invite)
await self.http.delete_invite(resolved.code) await self.http.delete_invite(resolved.code)
@ -1728,7 +1816,6 @@ class Client:
The guild joined. This is not the same guild that is The guild joined. This is not the same guild that is
added to cache. added to cache.
""" """
if not isinstance(invite, Invite): if not isinstance(invite, Invite):
invite = await self.fetch_invite(invite, with_counts=False, with_expiration=False) invite = await self.fetch_invite(invite, with_counts=False, with_expiration=False)
@ -1744,7 +1831,9 @@ class Client:
} }
data = await state.http.accept_invite(invite.code, type, **kwargs) data = await state.http.accept_invite(invite.code, type, **kwargs)
if type is InviteType.guild: if type is InviteType.guild:
return Guild(data=data['guild'], state=state) guild = Guild(data=data['guild'], state=state)
guild._cs_joined = True
return guild
elif type is InviteType.group_dm: elif type is InviteType.group_dm:
return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore return GroupChannel(data=data['channel'], state=state, me=state.user) # type: ignore
else: else:
@ -1973,7 +2062,7 @@ class Client:
The sticker you requested. The sticker you requested.
""" """
data = await self.http.get_sticker(sticker_id) data = await self.http.get_sticker(sticker_id)
cls, _ = _sticker_factory(data['type']) # type: ignore cls, _ = _sticker_factory(data['type'])
return cls(state=self._connection, data=data) # type: ignore return cls(state=self._connection, data=data) # type: ignore
async def fetch_sticker_packs( async def fetch_sticker_packs(
@ -2014,6 +2103,8 @@ class Client:
Retrieves a sticker pack with the specified ID. Retrieves a sticker pack with the specified ID.
.. versionadded:: 2.0
Raises Raises
------- -------
NotFound NotFound
@ -2108,7 +2199,7 @@ class Client:
""" """
state = self._connection state = self._connection
channels = await state.http.get_private_channels() channels = await state.http.get_private_channels()
return [_private_channel_factory(data['type'])[0](me=self.user, data=data, state=state) for data in channels] return [_private_channel_factory(data['type'])[0](me=self.user, data=data, state=state) for data in channels] # type: ignore - user is always present when logged in
async def create_dm(self, user: Snowflake) -> DMChannel: async def create_dm(self, user: Snowflake) -> DMChannel:
"""|coro| """|coro|
@ -2161,10 +2252,10 @@ class Client:
:class:`.GroupChannel` :class:`.GroupChannel`
The new group channel. The new group channel.
""" """
users = [str(u.id) for u in recipients] users = [u.id for u in recipients]
state = self._connection state = self._connection
data = await state.http.start_group(users) data = await state.http.start_group(users)
return GroupChannel(me=self.user, data=data, state=state) return GroupChannel(me=self.user, data=data, state=state) # type: ignore - user is always present when logged in
@overload @overload
async def send_friend_request(self, user: BaseUser) -> Relationship: async def send_friend_request(self, user: BaseUser) -> Relationship:
@ -2225,7 +2316,7 @@ class Client:
user = args[0] user = args[0]
if isinstance(user, BaseUser): if isinstance(user, BaseUser):
user = str(user) user = str(user)
username, discrim = user.split('#') # type: ignore username, discrim = user.split('#')
elif len(args) == 2: elif len(args) == 2:
username, discrim = args # type: ignore username, discrim = args # type: ignore
else: else:
@ -2278,6 +2369,10 @@ class Client:
Raises Raises
------- -------
NotFound
The application was not found.
Forbidden
You do not own the application.
HTTPException HTTPException
Retrieving the application failed. Retrieving the application failed.
@ -2295,6 +2390,20 @@ class Client:
Retrieves the partial application with the given ID. Retrieves the partial application with the given ID.
.. versionadded:: 2.0
Parameters
-----------
app_id: :class:`int`
The ID of the partial application to fetch.
Raises
-------
NotFound
The partial application was not found.
HTTPException
Retrieving the partial application failed.
Returns Returns
-------- --------
:class:`.PartialApplication` :class:`.PartialApplication`
@ -2330,6 +2439,8 @@ class Client:
Retrieves the team with the given ID. Retrieves the team with the given ID.
You must be a part of the team.
.. versionadded:: 2.0 .. versionadded:: 2.0
Parameters Parameters
@ -2339,6 +2450,10 @@ class Client:
Raises Raises
------- -------
NotFound
The team was not found.
Forbidden
You are not a part of the team.
HTTPException HTTPException
Retrieving the team failed. Retrieving the team failed.
@ -2356,6 +2471,8 @@ class Client:
Creates an application. Creates an application.
.. versionadded:: 2.0
Parameters Parameters
---------- ----------
name: :class:`str` name: :class:`str`
@ -2380,6 +2497,8 @@ class Client:
Creates a team. Creates a team.
.. versionadded:: 2.0
Parameters Parameters
---------- ----------
name: :class:`str` name: :class:`str`

9
discord/gateway.py

@ -32,7 +32,7 @@ import threading
import traceback import traceback
import zlib import zlib
from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar, Type from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar
import aiohttp import aiohttp
@ -56,6 +56,7 @@ if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .client import Client from .client import Client
from .enums import Status
from .state import ConnectionState from .state import ConnectionState
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
from .voice_client import VoiceClient from .voice_client import VoiceClient
@ -654,7 +655,7 @@ class DiscordWebSocket:
self, self,
*, *,
activities: Optional[List[BaseActivity]] = None, activities: Optional[List[BaseActivity]] = None,
status: Optional[str] = None, status: Optional[Status] = None,
since: float = 0.0, since: float = 0.0,
afk: bool = False, afk: bool = False,
) -> None: ) -> None:
@ -670,7 +671,7 @@ class DiscordWebSocket:
payload = { payload = {
'op': self.PRESENCE, 'op': self.PRESENCE,
'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status)}, 'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status or 'online')},
} }
sent = utils._to_json(payload) sent = utils._to_json(payload)
@ -961,7 +962,7 @@ class DiscordVoiceWebSocket:
async def client_connect(self) -> None: async def client_connect(self) -> None:
payload = { payload = {
'op': self.CLIENT_CONNECT, 'op': self.VIDEO,
'd': { 'd': {
'audio_ssrc': self._connection.ssrc, 'audio_ssrc': self._connection.ssrc,
}, },

47
discord/guild.py

@ -105,10 +105,9 @@ _log = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from .abc import Snowflake, SnowflakeTime from .abc import Snowflake, SnowflakeTime
from .types.guild import ( from .types.guild import (
Ban as BanPayload,
Guild as GuildPayload, Guild as GuildPayload,
GuildPreview as GuildPreviewPayload,
RolePositionUpdate as RolePositionUpdatePayload, RolePositionUpdate as RolePositionUpdatePayload,
GuildFeature,
) )
from .types.threads import ( from .types.threads import (
Thread as ThreadPayload, Thread as ThreadPayload,
@ -266,6 +265,12 @@ class Guild(Hashable):
The notification settings for the guild. The notification settings for the guild.
.. versionadded:: 2.0 .. versionadded:: 2.0
keywords: Optional[:class:`str`]
Discovery search keywords for the guild.
.. versionadded:: 2.0
primary_category_id: Optional[:class:`int`]
The ID of the primary discovery category for the guild.
""" """
__slots__ = ( __slots__ = (
@ -320,6 +325,12 @@ class Guild(Hashable):
'_true_online_count', '_true_online_count',
'_chunked', '_chunked',
'_member_list', '_member_list',
'keywords',
'primary_category_id',
'application_command_count',
'_load_id',
'_joined_at',
'_cs_joined',
) )
_PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = {
@ -330,7 +341,7 @@ class Guild(Hashable):
3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600), 3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600),
} }
def __init__(self, *, data: GuildPayload, state: ConnectionState) -> None: def __init__(self, *, data: Union[GuildPayload, GuildPreviewPayload], state: ConnectionState) -> None:
self._chunked = False self._chunked = False
self._roles: Dict[int, Role] = {} self._roles: Dict[int, Role] = {}
self._channels: Dict[int, GuildChannel] = {} self._channels: Dict[int, GuildChannel] = {}
@ -344,6 +355,7 @@ class Guild(Hashable):
self.notification_settings: Optional[GuildSettings] = None self.notification_settings: Optional[GuildSettings] = None
self.command_counts: Optional[CommandCounts] = None self.command_counts: Optional[CommandCounts] = None
self._member_count: int = 0 self._member_count: int = 0
self._presence_count: Optional[int] = None
self._from_data(data) self._from_data(data)
def _add_channel(self, channel: GuildChannel, /) -> None: def _add_channel(self, channel: GuildChannel, /) -> None:
@ -438,9 +450,9 @@ class Guild(Hashable):
return role return role
def _from_data(self, guild: GuildPayload) -> None: def _from_data(self, guild: Union[GuildPayload, GuildPreviewPayload]) -> None:
try: try:
self._member_count: int = guild['member_count'] self._member_count: int = guild['member_count'] # type: ignore - Handled below
except KeyError: except KeyError:
pass pass
@ -483,7 +495,8 @@ class Guild(Hashable):
self.stickers: Tuple[GuildSticker, ...] = tuple( self.stickers: Tuple[GuildSticker, ...] = tuple(
map(lambda d: state.store_sticker(self, d), guild.get('stickers', [])) map(lambda d: state.store_sticker(self, d), guild.get('stickers', []))
) )
self.features: List[GuildFeature] = guild.get('features', []) self.features: List[str] = guild.get('features', [])
self.keywords: List[str] = guild.get('keywords', [])
self._icon: Optional[str] = guild.get('icon') self._icon: Optional[str] = guild.get('icon')
self._banner: Optional[str] = guild.get('banner') self._banner: Optional[str] = guild.get('banner')
self._splash: Optional[str] = guild.get('splash') self._splash: Optional[str] = guild.get('splash')
@ -506,10 +519,12 @@ class Guild(Hashable):
self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0)) self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0))
self.approximate_presence_count: Optional[int] = guild.get('approximate_presence_count') self.approximate_presence_count: Optional[int] = guild.get('approximate_presence_count')
self.approximate_member_count: Optional[int] = guild.get('approximate_member_count') self.approximate_member_count: Optional[int] = guild.get('approximate_member_count')
self._presence_count: Optional[int] = guild.get('approximate_presence_count')
self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id')
self.owner_application_id: Optional[int] = utils._get_as_snowflake(guild, 'application_id') self.owner_application_id: Optional[int] = utils._get_as_snowflake(guild, 'application_id')
self.premium_progress_bar_enabled: bool = guild.get('premium_progress_bar_enabled', False) self.premium_progress_bar_enabled: bool = guild.get('premium_progress_bar_enabled', False)
self.application_command_count: int = guild.get('application_command_count', 0)
self.primary_category_id: Optional[int] = guild.get('primary_category_id')
self._joined_at = guild.get('joined_at')
large = None if self._member_count is 0 else self._member_count >= 250 large = None if self._member_count is 0 else self._member_count >= 250
self._large: Optional[bool] = guild.get('large', large) self._large: Optional[bool] = guild.get('large', large)
@ -593,9 +608,23 @@ class Guild(Hashable):
""":class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`. """:class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`.
This is essentially used to get the member version of yourself. This is essentially used to get the member version of yourself.
""" """
self_id = self._state.user.id # type: ignore - state.user won't be None if we're logged in self_id = self._state.self_id
return self.get_member(self_id) # type: ignore - The self member is *always* cached return self.get_member(self_id) # type: ignore - The self member is *always* cached
@utils.cached_slot_property('_cs_joined')
def joined(self) -> bool:
""":class:`bool`: Returns whether you are a member of this guild.
May not be accurate for :class:`Guild`s fetched over HTTP.
"""
if self.me or self.joined_at:
return True
return self._state.is_guild_evicted(self)
@property
def joined_at(self) -> Optional[datetime]:
""":class:`datetime.datetime`: Returns when you joined the guild."""
return utils.parse_time(self._joined_at)
@property @property
def voice_client(self) -> Optional[VoiceProtocol]: def voice_client(self) -> Optional[VoiceProtocol]:
"""Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any.""" """Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any."""
@ -1519,7 +1548,7 @@ class Guild(Hashable):
HTTPException HTTPException
Leaving the guild failed. Leaving the guild failed.
""" """
await self._state.http.leave_guild(self.id) await self._state.http.leave_guild(self.id, lurking=not self.joined)
async def delete(self) -> None: async def delete(self) -> None:
"""|coro| """|coro|

147
discord/http.py

@ -1197,6 +1197,33 @@ class HTTPClient:
return self.request(Route('GET', '/users/@me/guilds'), params=params, super_properties_to_track=True) return self.request(Route('GET', '/users/@me/guilds'), params=params, super_properties_to_track=True)
def join_guild(
self,
guild_id: Snowflake,
lurker: bool,
session_id: Optional[str] = MISSING,
load_id: str = MISSING,
location: str = MISSING,
) -> Response[guild.Guild]:
params = {
'lurker': str(lurker).lower(),
}
if lurker:
params['session_id'] = session_id or utils._generate_session_id()
if load_id is not MISSING:
params['recommendation_load_id'] = load_id
params['location'] = 'Guild%20Discovery'
if location is not MISSING:
params['location'] = location
props = ContextProperties._empty() if lurker else ContextProperties._from_lurking()
return self.request(
Route('PUT', '/guilds/{guild_id}/members/@me', guild_id=guild_id),
context_properties=props,
params=params,
json={},
)
def leave_guild(self, guild_id: Snowflake, lurking: bool = False) -> Response[None]: def leave_guild(self, guild_id: Snowflake, lurking: bool = False) -> Response[None]:
r = Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=guild_id) r = Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=guild_id)
payload = {'lurking': lurking} payload = {'lurking': lurking}
@ -2079,64 +2106,7 @@ class HTTPClient:
return self.request(Route('PATCH', '/users/@me/relationships/{user_id}', user_id=user_id), json=payload) return self.request(Route('PATCH', '/users/@me/relationships/{user_id}', user_id=user_id), json=payload)
# Misc # Connections
async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str:
# The gateway URL hasn't changed for over 5 years
# And, the official clients aren't GETting it anymore, sooooo...
self.zlib = zlib
if zlib:
value = 'wss://gateway.discord.gg?encoding={0}&v=9&compress=zlib-stream'
else:
value = 'wss://gateway.discord.gg?encoding={0}&v=9'
return value.format(encoding)
def get_user(self, user_id: Snowflake) -> Response[user.User]:
return self.request(Route('GET', '/users/{user_id}', user_id=user_id))
def get_user_profile(
self, user_id: Snowflake, guild_id: Snowflake = MISSING, *, with_mutual_guilds: bool = True
): # TODO: return type
params: Dict[str, Any] = {'with_mutual_guilds': str(with_mutual_guilds).lower()}
if guild_id is not MISSING:
params['guild_id'] = guild_id
return self.request(Route('GET', '/users/{user_id}/profile', user_id=user_id), params=params)
def get_mutual_friends(self, user_id: Snowflake): # TODO: return type
return self.request(Route('GET', '/users/{user_id}/relationships', user_id=user_id))
def get_notes(self): # TODO: return type
return self.request(Route('GET', '/users/@me/notes'))
def get_note(self, user_id: Snowflake): # TODO: return type
return self.request(Route('GET', '/users/@me/notes/{user_id}', user_id=user_id))
def set_note(self, user_id: Snowflake, *, note: Optional[str] = None) -> Response[None]:
payload = {'note': note or ''}
return self.request(Route('PUT', '/users/@me/notes/{user_id}', user_id=user_id), json=payload)
def change_hypesquad_house(self, house_id: int) -> Response[None]:
payload = {'house_id': house_id}
return self.request(Route('POST', '/hypesquad/online'), json=payload)
def leave_hypesquad_house(self) -> Response[None]:
return self.request(Route('DELETE', '/hypesquad/online'))
def get_settings(self): # TODO: return type
return self.request(Route('GET', '/users/@me/settings'))
def edit_settings(self, **payload): # TODO: return type, is this cheating?
return self.request(Route('PATCH', '/users/@me/settings'), json=payload)
def get_tracking(self): # TODO: return type
return self.request(Route('GET', '/users/@me/consent'))
def edit_tracking(self, payload):
return self.request(Route('POST', '/users/@me/consent'), json=payload)
def get_connections(self): def get_connections(self):
return self.request(Route('GET', '/users/@me/connections')) return self.request(Route('GET', '/users/@me/connections'))
@ -2150,6 +2120,8 @@ class HTTPClient:
def get_connection_token(self, type: str, id: str): def get_connection_token(self, type: str, id: str):
return self.request(Route('GET', '/users/@me/connections/{type}/{id}/access-token', type=type, id=id)) return self.request(Route('GET', '/users/@me/connections/{type}/{id}/access-token', type=type, id=id))
# Applications
def get_my_applications(self, *, with_team_applications: bool = True) -> Response[List[appinfo.AppInfo]]: def get_my_applications(self, *, with_team_applications: bool = True) -> Response[List[appinfo.AppInfo]]:
params = {'with_team_applications': str(with_team_applications).lower()} params = {'with_team_applications': str(with_team_applications).lower()}
@ -2245,6 +2217,65 @@ class HTTPClient:
def reset_token(self, app_id: Snowflake): def reset_token(self, app_id: Snowflake):
return self.request(Route('POST', '/applications/{app_id}/bot/reset', app_id=app_id), super_properties_to_track=True) return self.request(Route('POST', '/applications/{app_id}/bot/reset', app_id=app_id), super_properties_to_track=True)
# Misc
async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str:
# The gateway URL hasn't changed for over 5 years
# And, the official clients aren't GETting it anymore, sooooo...
self.zlib = zlib
if zlib:
value = 'wss://gateway.discord.gg?encoding={0}&v=9&compress=zlib-stream'
else:
value = 'wss://gateway.discord.gg?encoding={0}&v=9'
return value.format(encoding)
def get_user(self, user_id: Snowflake) -> Response[user.User]:
return self.request(Route('GET', '/users/{user_id}', user_id=user_id))
def get_user_profile(
self, user_id: Snowflake, guild_id: Snowflake = MISSING, *, with_mutual_guilds: bool = True
): # TODO: return type
params: Dict[str, Any] = {'with_mutual_guilds': str(with_mutual_guilds).lower()}
if guild_id is not MISSING:
params['guild_id'] = guild_id
return self.request(Route('GET', '/users/{user_id}/profile', user_id=user_id), params=params)
def get_mutual_friends(self, user_id: Snowflake): # TODO: return type
return self.request(Route('GET', '/users/{user_id}/relationships', user_id=user_id))
def get_notes(self): # TODO: return type
return self.request(Route('GET', '/users/@me/notes'))
def get_note(self, user_id: Snowflake): # TODO: return type
return self.request(Route('GET', '/users/@me/notes/{user_id}', user_id=user_id))
def set_note(self, user_id: Snowflake, *, note: Optional[str] = None) -> Response[None]:
payload = {'note': note or ''}
return self.request(Route('PUT', '/users/@me/notes/{user_id}', user_id=user_id), json=payload)
def change_hypesquad_house(self, house_id: int) -> Response[None]:
payload = {'house_id': house_id}
return self.request(Route('POST', '/hypesquad/online'), json=payload)
def leave_hypesquad_house(self) -> Response[None]:
return self.request(Route('DELETE', '/hypesquad/online'))
def get_settings(self): # TODO: return type
return self.request(Route('GET', '/users/@me/settings'))
def edit_settings(self, **payload): # TODO: return type, is this cheating?
return self.request(Route('PATCH', '/users/@me/settings'), json=payload)
def get_tracking(self): # TODO: return type
return self.request(Route('GET', '/users/@me/consent'))
def edit_tracking(self, payload):
return self.request(Route('POST', '/users/@me/consent'), json=payload)
def mobile_report( # Report v1 def mobile_report( # Report v1
self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str
): # TODO: return type ): # TODO: return type

4
discord/invite.py

@ -604,7 +604,9 @@ class Invite(Hashable):
if type is InviteType.guild: if type is InviteType.guild:
from .guild import Guild from .guild import Guild
return Guild(data=data['guild'], state=state) guild = Guild(data=data['guild'], state=state)
guild._cs_joined = True
return guild
elif type is InviteType.group_dm: elif type is InviteType.group_dm:
from .channel import GroupChannel from .channel import GroupChannel

2
discord/team.py

@ -228,7 +228,7 @@ class Team:
user = args[0] user = args[0]
if isinstance(user, BaseUser): if isinstance(user, BaseUser):
user = str(user) user = str(user)
username, discrim = user.split('#') # type: ignore username, discrim = user.split('#')
elif len(args) == 2: elif len(args) == 2:
username, discrim = args # type: ignore username, discrim = args # type: ignore
else: else:

60
discord/tracking.py

@ -26,12 +26,15 @@ from __future__ import annotations
from base64 import b64encode from base64 import b64encode
import json import json
from random import choice
from typing import Any, Dict, overload, Optional, TYPE_CHECKING from typing import Dict, overload, Optional, TYPE_CHECKING
from .utils import MISSING from .utils import MISSING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self
from .enums import ChannelType from .enums import ChannelType
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
from .state import ConnectionState from .state import ConnectionState
@ -56,6 +59,8 @@ class ContextProperties: # Thank you Discord-S.C.U.M
def _encode_data(self, data) -> str: def _encode_data(self, data) -> str:
library = { library = {
'None': 'e30=',
# Locations
'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==', 'Friends': 'eyJsb2NhdGlvbiI6IkZyaWVuZHMifQ==',
'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=', 'ContextMenu': 'eyJsb2NhdGlvbiI6IkNvbnRleHRNZW51In0=',
'User Profile': 'eyJsb2NhdGlvbiI6IlVzZXIgUHJvZmlsZSJ9', 'User Profile': 'eyJsb2NhdGlvbiI6IlVzZXIgUHJvZmlsZSJ9',
@ -69,80 +74,82 @@ class ContextProperties: # Thank you Discord-S.C.U.M
'Verify Email': 'eyJsb2NhdGlvbiI6IlZlcmlmeSBFbWFpbCJ9', 'Verify Email': 'eyJsb2NhdGlvbiI6IlZlcmlmeSBFbWFpbCJ9',
'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9', 'New Group DM': 'eyJsb2NhdGlvbiI6Ik5ldyBHcm91cCBETSJ9',
'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=', 'Add Friends to DM': 'eyJsb2NhdGlvbiI6IkFkZCBGcmllbmRzIHRvIERNIn0=',
'None': 'e30=', # Sources
'Chat Input Blocker - Lurker Mode': 'eyJzb3VyY2UiOiJDaGF0IElucHV0IEJsb2NrZXIgLSBMdXJrZXIgTW9kZSJ9',
'Notice - Lurker Mode': 'eyJzb3VyY2UiOiJOb3RpY2UgLSBMdXJrZXIgTW9kZSJ9',
} }
try: try:
return library[data.get('location', 'None')] return library[self.target or 'None']
except KeyError: except KeyError:
return b64encode(json.dumps(data, separators=(',', ':')).encode()).decode('utf-8') return b64encode(json.dumps(data, separators=(',', ':')).encode()).decode('utf-8')
@classmethod @classmethod
def _empty(cls) -> ContextProperties: def _empty(cls) -> Self:
return cls({}) return cls({})
@classmethod @classmethod
def _from_friends_page(cls) -> ContextProperties: def _from_friends_page(cls) -> Self:
data = {'location': 'Friends'} data = {'location': 'Friends'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_context_menu(cls) -> ContextProperties: def _from_context_menu(cls) -> Self:
data = {'location': 'ContextMenu'} data = {'location': 'ContextMenu'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_user_profile(cls) -> ContextProperties: def _from_user_profile(cls) -> Self:
data = {'location': 'User Profile'} data = {'location': 'User Profile'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_add_friend_page(cls) -> ContextProperties: def _from_add_friend_page(cls) -> Self:
data = {'location': 'Add Friend'} data = {'location': 'Add Friend'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_guild_header_menu(cls) -> ContextProperties: def _from_guild_header_menu(cls) -> Self:
data = {'location': 'Guild Header'} data = {'location': 'Guild Header'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_group_dm(cls) -> ContextProperties: def _from_group_dm(cls) -> Self:
data = {'location': 'Group DM'} data = {'location': 'Group DM'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_new_group_dm(cls) -> ContextProperties: def _from_new_group_dm(cls) -> Self:
data = {'location': 'New Group DM'} data = {'location': 'New Group DM'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_dm_channel(cls) -> ContextProperties: def _from_dm_channel(cls) -> Self:
data = {'location': 'DM Channel'} data = {'location': 'DM Channel'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_add_to_dm(cls) -> ContextProperties: def _from_add_to_dm(cls) -> Self:
data = {'location': 'Add Friends to DM'} data = {'location': 'Add Friends to DM'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_app(cls) -> ContextProperties: def _from_app(cls) -> Self:
data = {'location': '/app'} data = {'location': '/app'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_login(cls) -> ContextProperties: def _from_login(cls) -> Self:
data = {'location': 'Login'} data = {'location': 'Login'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_register(cls) -> ContextProperties: def _from_register(cls) -> Self:
data = {'location': 'Register'} data = {'location': 'Register'}
return cls(data) return cls(data)
@classmethod @classmethod
def _from_verification(cls) -> ContextProperties: def _from_verification(cls) -> Self:
data = {'location': 'Verify Email'} data = {'location': 'Verify Email'}
return cls(data) return cls(data)
@ -153,7 +160,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
guild_id: Snowflake = MISSING, guild_id: Snowflake = MISSING,
channel_id: Snowflake = MISSING, channel_id: Snowflake = MISSING,
channel_type: ChannelType = MISSING, channel_type: ChannelType = MISSING,
) -> ContextProperties: ) -> Self:
data: Dict[str, Snowflake] = { data: Dict[str, Snowflake] = {
'location': 'Accept Invite Page', 'location': 'Accept Invite Page',
} }
@ -172,7 +179,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
guild_id: Snowflake = MISSING, guild_id: Snowflake = MISSING,
channel_id: Snowflake = MISSING, channel_id: Snowflake = MISSING,
channel_type: ChannelType = MISSING, channel_type: ChannelType = MISSING,
) -> ContextProperties: ) -> Self:
data: Dict[str, Snowflake] = { data: Dict[str, Snowflake] = {
'location': 'Join Guild', 'location': 'Join Guild',
} }
@ -192,7 +199,7 @@ class ContextProperties: # Thank you Discord-S.C.U.M
channel_id: Snowflake, channel_id: Snowflake,
message_id: Snowflake, message_id: Snowflake,
channel_type: Optional[ChannelType], channel_type: Optional[ChannelType],
) -> ContextProperties: ) -> Self:
data = { data = {
'location': 'Invite Button Embed', 'location': 'Invite Button Embed',
'location_guild_id': str(guild_id) if guild_id else None, 'location_guild_id': str(guild_id) if guild_id else None,
@ -202,9 +209,14 @@ class ContextProperties: # Thank you Discord-S.C.U.M
} }
return cls(data) return cls(data)
@classmethod
def _from_lurking(cls, source: str = MISSING) -> Self:
data = {'source': source or choice(('Chat Input Blocker - Lurker Mode', 'Notice - Lurker Mode'))}
return cls(data)
@property @property
def location(self) -> Optional[str]: def target(self) -> Optional[str]:
return self._data.get('location') # type: ignore return self._data.get('location', data.get('source')) # type: ignore
@property @property
def guild_id(self) -> Optional[int]: def guild_id(self) -> Optional[int]:
@ -232,10 +244,10 @@ class ContextProperties: # Thank you Discord-S.C.U.M
return self.value is not None return self.value is not None
def __str__(self) -> str: def __str__(self) -> str:
return self._data.get('location', 'None') # type: ignore return self.target or 'None'
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<ContextProperties location={self.location}>' return f'<ContextProperties target={self.target!r}>'
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
return isinstance(other, ContextProperties) and self.value == other.value return isinstance(other, ContextProperties) and self.value == other.value

28
discord/types/guild.py

@ -55,7 +55,6 @@ class UnavailableGuild(_UnavailableGuildOptional):
class _GuildOptional(TypedDict, total=False): class _GuildOptional(TypedDict, total=False):
icon_hash: Optional[str] icon_hash: Optional[str]
owner: bool owner: bool
permissions: str
widget_enabled: bool widget_enabled: bool
widget_channel_id: Optional[Snowflake] widget_channel_id: Optional[Snowflake]
joined_at: Optional[str] joined_at: Optional[str]
@ -78,31 +77,6 @@ MFALevel = Literal[0, 1]
VerificationLevel = Literal[0, 1, 2, 3, 4] VerificationLevel = Literal[0, 1, 2, 3, 4]
NSFWLevel = Literal[0, 1, 2, 3] NSFWLevel = Literal[0, 1, 2, 3]
PremiumTier = Literal[0, 1, 2, 3] PremiumTier = Literal[0, 1, 2, 3]
GuildFeature = Literal[
'ANIMATED_ICON',
'BANNER',
'COMMERCE',
'COMMUNITY',
'DISCOVERABLE',
'FEATURABLE',
'INVITE_SPLASH',
'MEMBER_VERIFICATION_GATE_ENABLED',
'MONETIZATION_ENABLED',
'MORE_EMOJI',
'MORE_STICKERS',
'NEWS',
'PARTNERED',
'PREVIEW_ENABLED',
'PRIVATE_THREADS',
'ROLE_ICONS',
'SEVEN_DAY_THREAD_ARCHIVE',
'THREE_DAY_THREAD_ARCHIVE',
'TICKETED_EVENTS_ENABLED',
'VANITY_URL',
'VERIFIED',
'VIP_REGIONS',
'WELCOME_SCREEN_ENABLED',
]
class _BaseGuildPreview(UnavailableGuild): class _BaseGuildPreview(UnavailableGuild):
@ -112,7 +86,7 @@ class _BaseGuildPreview(UnavailableGuild):
discovery_splash: Optional[str] discovery_splash: Optional[str]
emojis: List[Emoji] emojis: List[Emoji]
stickers: List[GuildSticker] stickers: List[GuildSticker]
features: List[GuildFeature] features: List[str]
description: Optional[str] description: Optional[str]

14
discord/user.py

@ -485,8 +485,6 @@ class ClientUser(BaseUser):
The IETF language tag used to identify the language the user is using. The IETF language tag used to identify the language the user is using.
mfa_enabled: :class:`bool` mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working. Specifies if the user has MFA turned on and working.
premium: :class:`bool`
Specifies if the user is a premium user (i.e. has Discord Nitro).
premium_type: Optional[:class:`PremiumType`] premium_type: Optional[:class:`PremiumType`]
Specifies the type of premium a user has (i.e. Nitro or Nitro Classic). Could be None if the user is not premium. Specifies the type of premium a user has (i.e. Nitro or Nitro Classic). Could be None if the user is not premium.
note: :class:`Note` note: :class:`Note`
@ -507,7 +505,6 @@ class ClientUser(BaseUser):
'phone', 'phone',
'premium_type', 'premium_type',
'note', 'note',
'premium',
'bio', 'bio',
'nsfw_allowed', 'nsfw_allowed',
) )
@ -519,7 +516,6 @@ class ClientUser(BaseUser):
locale: Locale locale: Locale
_flags: int _flags: int
mfa_enabled: bool mfa_enabled: bool
premium: bool
premium_type: Optional[PremiumType] premium_type: Optional[PremiumType]
bio: Optional[str] bio: Optional[str]
nsfw_allowed: bool nsfw_allowed: bool
@ -542,8 +538,9 @@ class ClientUser(BaseUser):
self.locale = try_enum(Locale, data.get('locale', 'en-US')) self.locale = try_enum(Locale, data.get('locale', 'en-US'))
self._flags = data.get('flags', 0) self._flags = data.get('flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False) self.mfa_enabled = data.get('mfa_enabled', False)
self.premium = data.get('premium', False) self.premium_type = try_enum(PremiumType, data['premium_type']) if 'premium_type' in data else None
self.premium_type = try_enum(PremiumType, data.get('premium_type', None)) self.bio = data.get('bio')
self.nsfw_allowed = data.get('nsfw_allowed', False)
self.bio = data.get('bio') or None self.bio = data.get('bio') or None
self.nsfw_allowed = data.get('nsfw_allowed', False) self.nsfw_allowed = data.get('nsfw_allowed', False)
@ -562,6 +559,11 @@ class ClientUser(BaseUser):
""" """
return self._state._relationships.get(user_id) return self._state._relationships.get(user_id)
@property
def premium(self) -> bool:
"""Indicates if the user is a premium user (i.e. has Discord Nitro)."""
return self.premium_type is not None
@property @property
def relationships(self) -> List[Relationship]: def relationships(self) -> List[Relationship]:
"""List[:class:`User`]: Returns all the relationships that the user has.""" """List[:class:`User`]: Returns all the relationships that the user has."""

Loading…
Cancel
Save