Browse Source

Implement oauth2 authorization

pull/10109/head
dolfies 2 years ago
parent
commit
d08e22e719
  1. 1
      discord/__init__.py
  2. 32
      discord/channel.py
  3. 164
      discord/client.py
  4. 41
      discord/guild.py
  5. 188
      discord/http.py
  6. 228
      discord/oauth2.py
  7. 68
      discord/types/oauth2.py
  8. 27
      docs/api.rst

1
discord/__init__.py

@ -54,6 +54,7 @@ from .message import *
from .metadata import * from .metadata import *
from .modal import * from .modal import *
from .object import * from .object import *
from .oauth2 import *
from .partial_emoji import * from .partial_emoji import *
from .payments import * from .payments import *
from .permissions import * from .permissions import *

32
discord/channel.py

@ -103,7 +103,7 @@ if TYPE_CHECKING:
ForumChannel as ForumChannelPayload, ForumChannel as ForumChannelPayload,
ForumTag as ForumTagPayload, ForumTag as ForumTagPayload,
) )
from .types.oauth2 import WebhookChannel as WebhookChannelPayload
from .types.snowflake import SnowflakeList from .types.snowflake import SnowflakeList
OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object]) OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object])
@ -3602,8 +3602,6 @@ class PartialMessageable(discord.abc.Messageable, Hashable):
Note that this class is trimmed down and has no rich attributes. Note that this class is trimmed down and has no rich attributes.
.. versionadded:: 2.0
.. container:: operations .. container:: operations
.. describe:: x == y .. describe:: x == y
@ -3618,21 +3616,34 @@ class PartialMessageable(discord.abc.Messageable, Hashable):
Returns the partial messageable's hash. Returns the partial messageable's hash.
.. versionadded:: 2.0
Attributes Attributes
----------- -----------
id: :class:`int` id: :class:`int`
The channel ID associated with this partial messageable. The channel ID associated with this partial messageable.
guild_id: Optional[:class:`int`]
The guild ID associated with this partial messageable.
type: Optional[:class:`ChannelType`] type: Optional[:class:`ChannelType`]
The channel type associated with this partial messageable, if given. The channel type associated with this partial messageable, if given.
name: Optional[:class:`str`]
The channel name associated with this partial messageable, if given.
guild_id: Optional[:class:`int`]
The guild ID associated with this partial messageable.
""" """
def __init__(self, state: ConnectionState, id: int, guild_id: Optional[int] = None, type: Optional[ChannelType] = None): def __init__(
self,
*,
state: ConnectionState,
id: int,
guild_id: Optional[int] = None,
type: Optional[ChannelType] = None,
name: Optional[str] = None,
):
self._state: ConnectionState = state self._state: ConnectionState = state
self.id: int = id self.id: int = id
self.guild_id: Optional[int] = guild_id self.guild_id: Optional[int] = guild_id
self.type: Optional[ChannelType] = type self.type: Optional[ChannelType] = type
self.name: Optional[str] = name
self.last_message_id: Optional[int] = None self.last_message_id: Optional[int] = None
self.last_pin_timestamp: Optional[datetime.datetime] = None self.last_pin_timestamp: Optional[datetime.datetime] = None
@ -3642,6 +3653,15 @@ class PartialMessageable(discord.abc.Messageable, Hashable):
async def _get_channel(self) -> PartialMessageable: async def _get_channel(self) -> PartialMessageable:
return self return self
@classmethod
def _from_webhook_channel(cls, guild: Guild, channel: WebhookChannelPayload) -> Self:
return cls(
state=guild._state,
id=int(channel['id']),
guild_id=guild.id,
name=channel['name'],
)
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild this partial messageable is in.""" """Optional[:class:`Guild`]: The guild this partial messageable is in."""

164
discord/client.py

@ -31,6 +31,7 @@ from typing import (
Any, Any,
AsyncIterator, AsyncIterator,
Callable, Callable,
Collection,
Coroutine, Coroutine,
Dict, Dict,
Generator, Generator,
@ -88,6 +89,7 @@ from .library import LibraryApplication
from .relationship import FriendSuggestion, Relationship from .relationship import FriendSuggestion, Relationship
from .settings import UserSettings, LegacyUserSettings, TrackingSettings, EmailSettings from .settings import UserSettings, LegacyUserSettings, TrackingSettings, EmailSettings
from .affinity import * from .affinity import *
from .oauth2 import OAuth2Authorization, OAuth2Token
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
@ -102,6 +104,7 @@ if TYPE_CHECKING:
from .billing import BillingAddress from .billing import BillingAddress
from .enums import PaymentGateway, RequiredActionType from .enums import PaymentGateway, RequiredActionType
from .metadata import MetadataObject from .metadata import MetadataObject
from .permissions import Permissions
from .types.snowflake import Snowflake as _Snowflake from .types.snowflake import Snowflake as _Snowflake
PrivateChannel = Union[DMChannel, GroupChannel] PrivateChannel = Union[DMChannel, GroupChannel]
@ -3897,6 +3900,167 @@ class Client:
data = await state.http.get_library_entries(state.country_code or 'US') data = await state.http.get_library_entries(state.country_code or 'US')
return [LibraryApplication(state=state, data=d) for d in data] return [LibraryApplication(state=state, data=d) for d in data]
async def authorizations(self) -> List[OAuth2Token]:
"""|coro|
Retrieves the OAuth2 applications authorized on your account.
.. versionadded:: 2.1
Raises
-------
HTTPException
Retrieving the authorized applications failed.
Returns
-------
List[:class:`.OAuth2Token`]
The OAuth2 applications authorized on your account.
"""
state = self._connection
data = await state.http.get_oauth2_tokens()
return [OAuth2Token(state=state, data=d) for d in data]
async def fetch_authorization(
self,
application_id: int,
/,
*,
scopes: Collection[str],
response_type: Optional[str] = None,
redirect_uri: Optional[str] = None,
code_challenge_method: Optional[str] = None,
code_challenge: Optional[str] = None,
state: Optional[str] = None,
) -> OAuth2Authorization:
"""|coro|
Retrieves an OAuth2 authorization for the given application.
This provides information about the application before you authorize it.
.. versionadded:: 2.1
Parameters
-----------
application_id: :class:`int`
The ID of the application to fetch the authorization for.
scopes: List[:class:`str`]
The scopes to request for the authorization.
response_type: Optional[:class:`str`]
The response type that will be used for the authorization, if using the full OAuth2 flow.
redirect_uri: Optional[:class:`str`]
The redirect URI that will be used for the authorization, if using the full OAuth2 flow.
If this isn't provided and ``response_type`` is provided, then the default redirect URI
for the application will be provided in the returned authorization.
code_challenge_method: Optional[:class:`str`]
The code challenge method that will be used for the PKCE authorization, if using the full OAuth2 flow.
code_challenge: Optional[:class:`str`]
The code challenge that will be used for the PKCE authorization, if using the full OAuth2 flow.
state: Optional[:class:`str`]
The state that will be used for authorization security.
Raises
-------
HTTPException
Fetching the authorization failed.
Returns
-------
:class:`.OAuth2Authorization`
The authorization for the application.
"""
_state = self._connection
data = await _state.http.get_oauth2_authorization(
application_id,
list(scopes),
response_type,
redirect_uri,
code_challenge_method,
code_challenge,
state,
)
return OAuth2Authorization(
_state=_state,
data=data,
scopes=list(scopes),
response_type=response_type,
code_challenge_method=code_challenge_method,
code_challenge=code_challenge,
state=state,
)
async def create_authorization(
self,
application_id: int,
/,
*,
scopes: Collection[str],
response_type: Optional[str] = None,
redirect_uri: Optional[str] = None,
code_challenge_method: Optional[str] = None,
code_challenge: Optional[str] = None,
state: Optional[str] = None,
guild: Snowflake = MISSING,
channel: Snowflake = MISSING,
permissions: Permissions = MISSING,
) -> str:
"""|coro|
Creates an OAuth2 authorization for the given application. It is recommended to instead first
fetch the authorization information using :meth:`fetch_authorization` and then call :meth:`.OAuth2Authorization.authorize`.
.. versionadded:: 2.1
Parameters
-----------
application_id: :class:`int`
The ID of the application to create the authorization for.
scopes: List[:class:`str`]
The scopes to request for the authorization.
response_type: Optional[:class:`str`]
The response type to use for the authorization, if using the full OAuth2 flow.
redirect_uri: Optional[:class:`str`]
The redirect URI to use for the authorization, if using the full OAuth2 flow.
If this isn't provided and ``response_type`` is provided, then the default redirect URI
for the application will be used.
code_challenge_method: Optional[:class:`str`]
The code challenge method to use for the PKCE authorization, if using the full OAuth2 flow.
code_challenge: Optional[:class:`str`]
The code challenge to use for the PKCE authorization, if using the full OAuth2 flow.
state: Optional[:class:`str`]
The state to use for authorization security.
guild: :class:`.Guild`
The guild to authorize for, if authorizing with the ``applications.commands`` or ``bot`` scopes.
channel: Union[:class:`.TextChannel`, :class:`.VoiceChannel`, :class:`.StageChannel`]
The channel to authorize for, if authorizing with the ``webhooks.incoming`` scope. See :meth:`.Guild.webhook_channels`.
permissions: :class:`.Permissions`
The permissions to grant, if authorizing with the ``bot`` scope.
Raises
-------
HTTPException
Creating the authorization failed.
Returns
-------
:class:`str`
The URL to redirect the user to for authorization.
"""
_state = self._connection
data = await _state.http.authorize_oauth2(
application_id,
list(scopes),
response_type,
redirect_uri,
code_challenge_method,
code_challenge,
state,
guild_id=guild.id if guild else None,
webhook_channel_id=channel.id if channel else None,
permissions=permissions.value if permissions else None,
)
return data['location']
async def entitlements( async def entitlements(
self, *, with_sku: bool = True, with_application: bool = True, entitlement_type: Optional[EntitlementType] = None self, *, with_sku: bool = True, with_application: bool = True, entitlement_type: Optional[EntitlementType] = None
) -> List[Entitlement]: ) -> List[Entitlement]:

41
discord/guild.py

@ -132,6 +132,7 @@ if TYPE_CHECKING:
from .types.message import MessageSearchAuthorType, MessageSearchHasType from .types.message import MessageSearchAuthorType, MessageSearchHasType
from .types.snowflake import SnowflakeList, Snowflake as _Snowflake from .types.snowflake import SnowflakeList, Snowflake as _Snowflake
from .types.widget import EditWidgetSettings from .types.widget import EditWidgetSettings
from .types.oauth2 import OAuth2Guild as OAuth2GuildPayload
from .message import EmojiInputType, Message from .message import EmojiInputType, Message
VocalGuildChannel = Union[VoiceChannel, StageChannel] VocalGuildChannel = Union[VoiceChannel, StageChannel]
@ -197,15 +198,19 @@ class UserGuild(Hashable):
The guild name. The guild name.
features: List[:class:`str`] features: List[:class:`str`]
A list of features that the guild has. The features that a guild can have are A list of features that the guild has. The features that a guild can have are
subject to arbitrary change by Discord. subject to arbitrary change by Discord. Incomplete when retrieved from :attr:`OAuth2Authorization.guilds`.
owner: :class:`bool` owner: :class:`bool`
Whether the current user is the owner of the guild. Whether the current user is the owner of the guild. Inaccurate when retrieved from :attr:`OAuth2Authorization.guilds`.
mfa_level: :class:`MFALevel`
The guild's Multi-Factor Authentication requirement level. Only available from :attr:`OAuth2Authorization.guilds`.
.. versionadded:: 2.1
approximate_member_count: Optional[:class:`int`] approximate_member_count: Optional[:class:`int`]
The approximate number of members in the guild. This is ``None`` unless the guild is obtained The approximate number of members in the guild. Only available using
using :meth:`Client.fetch_guilds` with ``with_counts=True``. using :meth:`Client.fetch_guilds` with ``with_counts=True``.
approximate_presence_count: Optional[:class:`int`] approximate_presence_count: Optional[:class:`int`]
The approximate number of members currently active in the guild. The approximate number of members currently active in the guild.
Offline members are excluded. This is ``None`` unless the guild is obtained using Offline members are excluded. Only available using
:meth:`Client.fetch_guilds` with ``with_counts=True``. :meth:`Client.fetch_guilds` with ``with_counts=True``.
""" """
@ -215,19 +220,21 @@ class UserGuild(Hashable):
'_icon', '_icon',
'owner', 'owner',
'_permissions', '_permissions',
'mfa_level',
'features', 'features',
'approximate_member_count', 'approximate_member_count',
'approximate_presence_count', 'approximate_presence_count',
'_state', '_state',
) )
def __init__(self, *, state: ConnectionState, data: UserGuildPayload): def __init__(self, *, state: ConnectionState, data: Union[UserGuildPayload, OAuth2GuildPayload]):
self._state: ConnectionState = state self._state: ConnectionState = state
self.id: int = int(data['id']) self.id: int = int(data['id'])
self.name: str = data['name'] self.name: str = data['name']
self._icon: Optional[str] = data.get('icon') self._icon: Optional[str] = data.get('icon')
self.owner: bool = data.get('owner', False) self.owner: bool = data.get('owner', False)
self._permissions: int = int(data.get('permissions', 0)) self._permissions: int = int(data.get('permissions', 0))
self.mfa_level: MFALevel = try_enum(MFALevel, data.get('mfa_level', 0))
self.features: List[str] = data.get('features', []) self.features: List[str] = data.get('features', [])
self.approximate_member_count: Optional[int] = data.get('approximate_member_count') self.approximate_member_count: Optional[int] = data.get('approximate_member_count')
self.approximate_presence_count: Optional[int] = data.get('approximate_presence_count') self.approximate_presence_count: Optional[int] = data.get('approximate_presence_count')
@ -2282,13 +2289,35 @@ class Guild(Hashable):
Returns Returns
-------- --------
List[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`]] List[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`PartialMessageable`]]
The top 10 most read channels. Falls back to :class:`PartialMessageable` if the channel is not found in cache. The top 10 most read channels. Falls back to :class:`PartialMessageable` if the channel is not found in cache.
""" """
state = self._state state = self._state
data = await state.http.get_top_guild_channels(self.id) data = await state.http.get_top_guild_channels(self.id)
return [self.get_channel(int(c)) or PartialMessageable(id=int(c), state=state, guild_id=self.id) for c in data] # type: ignore return [self.get_channel(int(c)) or PartialMessageable(id=int(c), state=state, guild_id=self.id) for c in data] # type: ignore
async def webhook_channels(self) -> List[Union[TextChannel, VoiceChannel, StageChannel, PartialMessageable]]:
"""|coro|
Retrieves the channels that the current user can create webhooks in for the guild.
.. versionadded:: 2.1
Raises
-------
HTTPException
Retrieving the webhook channels failed.
Returns
--------
List[Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`, :class:`PartialMessageable`]]
The channels that the current user can create webhooks in. Falls back to :class:`PartialMessageable` if the channel is not found in cache.
Any :class:`PartialMessageable` will have its :attr:`PartialMessageable.name` filled in.
"""
state = self._state
data = await state.http.get_guild_webhook_channels(self.id)
return [self.get_channel(int(c['id'])) or PartialMessageable._from_webhook_channel(self, c) for c in data] # type: ignore
async def fetch_channels(self) -> Sequence[GuildChannel]: async def fetch_channels(self) -> Sequence[GuildChannel]:
"""|coro| """|coro|

188
discord/http.py

@ -96,6 +96,7 @@ if TYPE_CHECKING:
library, library,
member, member,
message, message,
oauth2,
payments, payments,
profile, profile,
promotions, promotions,
@ -2570,6 +2571,63 @@ class HTTPClient:
params=params, params=params,
) )
def get_auto_moderation_rules(self, guild_id: Snowflake) -> Response[List[automod.AutoModerationRule]]:
return self.request(Route('GET', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id))
def get_auto_moderation_rule(self, guild_id: Snowflake, rule_id: Snowflake) -> Response[automod.AutoModerationRule]:
return self.request(
Route('GET', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id)
)
def create_auto_moderation_rule(
self, guild_id: Snowflake, *, reason: Optional[str], **payload: Any
) -> Response[automod.AutoModerationRule]:
valid_keys = (
'name',
'event_type',
'trigger_type',
'trigger_metadata',
'actions',
'enabled',
'exempt_roles',
'exempt_channels',
)
payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None}
return self.request(
Route('POST', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id), json=payload, reason=reason
)
def edit_auto_moderation_rule(
self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str], **payload: Any
) -> Response[automod.AutoModerationRule]:
valid_keys = (
'name',
'event_type',
'trigger_metadata',
'actions',
'enabled',
'exempt_roles',
'exempt_channels',
)
payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None}
return self.request(
Route('PATCH', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id),
json=payload,
reason=reason,
)
def delete_auto_moderation_rule(
self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str]
) -> Response[None]:
return self.request(
Route('DELETE', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id),
reason=reason,
)
# Relationships # Relationships
def get_relationships(self) -> Response[List[user.Relationship]]: def get_relationships(self) -> Response[List[user.Relationship]]:
@ -4022,72 +4080,88 @@ class HTTPClient:
def get_premium_usage(self) -> Response[billing.PremiumUsage]: def get_premium_usage(self) -> Response[billing.PremiumUsage]:
return self.request(Route('GET', '/users/@me/premium-usage')) return self.request(Route('GET', '/users/@me/premium-usage'))
def enroll_active_developer( # OAuth2
self, application_id: Snowflake, channel_id: Snowflake
) -> Response[application.ActiveDeveloperResponse]:
payload = {'application_id': application_id, 'channel_id': channel_id}
return self.request(Route('POST', '/developers/active-program'), json=payload, super_properties_to_track=True) def get_oauth2_tokens(self) -> Response[List[oauth2.OAuth2Token]]:
return self.request(Route('GET', '/oauth2/tokens'))
def unenroll_active_developer(self) -> Response[None]: def revoke_oauth2_token(self, token_id: Snowflake) -> Response[None]:
return self.request(Route('DELETE', '/developers/active-program'), super_properties_to_track=True) return self.request(Route('DELETE', '/oauth2/tokens/{token_id}', token_id=token_id))
def get_auto_moderation_rules(self, guild_id: Snowflake) -> Response[List[automod.AutoModerationRule]]:
return self.request(Route('GET', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id))
def get_auto_moderation_rule(self, guild_id: Snowflake, rule_id: Snowflake) -> Response[automod.AutoModerationRule]: def get_guild_webhook_channels(self, guild_id: Snowflake) -> Response[List[oauth2.WebhookChannel]]:
return self.request( params = {'guild_id': guild_id}
Route('GET', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id) return self.request(Route('GET', '/oauth2/authorize/webhook-channels'), params=params)
)
def create_auto_moderation_rule( def get_oauth2_authorization(
self, guild_id: Snowflake, *, reason: Optional[str], **payload: Any self,
) -> Response[automod.AutoModerationRule]: application_id: Snowflake,
valid_keys = ( scopes: List[str],
'name', response_type: Optional[str] = None,
'event_type', redirect_uri: Optional[str] = None,
'trigger_type', code_challenge_method: Optional[str] = None,
'trigger_metadata', code_challenge: Optional[str] = None,
'actions', state: Optional[str] = None,
'enabled', ) -> Response[oauth2.OAuth2Authorization]:
'exempt_roles', params = {'client_id': application_id, 'scope': ' '.join(scopes)}
'exempt_channels', if response_type:
) params['response_type'] = response_type
if redirect_uri:
payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} params['redirect_uri'] = redirect_uri
if code_challenge_method:
params['code_challenge_method'] = code_challenge_method
if code_challenge:
params['code_challenge'] = code_challenge
if state:
params['state'] = state
return self.request(Route('GET', '/oauth2/authorize'), params=params)
def authorize_oauth2(
self,
application_id: Snowflake,
scopes: List[str],
response_type: Optional[str] = None,
redirect_uri: Optional[str] = None,
code_challenge_method: Optional[str] = None,
code_challenge: Optional[str] = None,
state: Optional[str] = None,
guild_id: Optional[Snowflake] = None,
webhook_channel_id: Optional[Snowflake] = None,
permissions: Optional[Snowflake] = None,
) -> Response[oauth2.OAuth2Location]:
params = {'client_id': application_id, 'scope': ' '.join(scopes)}
payload: Dict[str, Any] = {'authorize': True}
if response_type:
params['response_type'] = response_type
if redirect_uri:
params['redirect_uri'] = redirect_uri
if code_challenge_method:
params['code_challenge_method'] = code_challenge_method
if code_challenge:
params['code_challenge'] = code_challenge
if state:
params['state'] = state
if guild_id:
payload['guild_id'] = str(guild_id)
payload['permissions'] = '0'
if webhook_channel_id:
payload['webhook_channel_id'] = str(webhook_channel_id)
if permissions:
payload['permissions'] = str(permissions)
return self.request( return self.request(Route('POST', '/oauth2/authorize'), params=params, json=payload)
Route('POST', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id), json=payload, reason=reason
)
def edit_auto_moderation_rule( # Active Developer Program
self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str], **payload: Any
) -> Response[automod.AutoModerationRule]:
valid_keys = (
'name',
'event_type',
'trigger_metadata',
'actions',
'enabled',
'exempt_roles',
'exempt_channels',
)
payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} def enroll_active_developer(
self, application_id: Snowflake, channel_id: Snowflake
) -> Response[application.ActiveDeveloperResponse]:
payload = {'application_id': application_id, 'channel_id': channel_id}
return self.request( return self.request(Route('POST', '/developers/active-program'), json=payload, super_properties_to_track=True)
Route('PATCH', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id),
json=payload,
reason=reason,
)
def delete_auto_moderation_rule( def unenroll_active_developer(self) -> Response[None]:
self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str] return self.request(Route('DELETE', '/developers/active-program'), super_properties_to_track=True)
) -> Response[None]:
return self.request(
Route('DELETE', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id),
reason=reason,
)
# Misc # Misc

228
discord/oauth2.py

@ -0,0 +1,228 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional
from .application import PartialApplication
from .guild import UserGuild
from .mixins import Hashable
from .utils import MISSING
if TYPE_CHECKING:
from .abc import Snowflake
from .permissions import Permissions
from .state import ConnectionState
from .user import User
from .types.oauth2 import OAuth2Authorization as OAuth2AuthorizationPayload, OAuth2Token as OAuth2TokenPayload
__all__ = (
'OAuth2Token',
'OAuth2Authorization',
)
class OAuth2Token(Hashable):
"""Represents an authorized OAuth2 application for a user.
.. container:: operations
.. describe:: x == y
Checks if two authorizations are equal.
.. describe:: x != y
Checks if two authorizations are not equal.
.. describe:: hash(x)
Return the authorizations's hash.
.. describe:: str(x)
Returns the authorizations's name.
.. versionadded:: 2.1
Attributes
-----------
id: :class:`int`
The ID of the authorization.
application: :class:`PartialApplication`
The application that the authorization is for.
scopes: List[:class:`str`]
The scopes that the authorization has.
"""
__slots__ = ('id', 'application', 'scopes', '_state')
def __init__(self, *, state: ConnectionState, data: OAuth2TokenPayload):
self._state = state
self.id: int = int(data['id'])
self.application: PartialApplication = PartialApplication(state=state, data=data['application'])
self.scopes: List[str] = data['scopes']
def __repr__(self):
return f'<OAuth2Token id={self.id} application={self.application!r} scopes={self.scopes!r}>'
def __str__(self):
return self.application.name
@property
def authorized(self) -> bool:
""":class:`bool`: Whether the user has already authorized the application.
This is here for compatibility purposes and is always ``True``.
"""
return True
async def revoke(self):
"""|coro|
Revokes the application's authorization.
Raises
-------
HTTPException
Deauthorizing the application failed.
"""
await self._state.http.revoke_oauth2_token(self.id)
class OAuth2Authorization:
"""Represents a Discord OAuth2 application authorization.
.. versionadded:: 2.1
Attributes
-----------
scopes: List[:class:`str`]
The scopes that the authorization has.
response_type: Optional[:class:`str`]
The response type that will be used for the authorization, if using the full OAuth2 flow.
code_challenge_method: Optional[:class:`str`]
The code challenge method that will be used for the PKCE authorization, if using the full OAuth2 flow.
code_challenge: Optional[:class:`str`]
The code challenge that will be used for the PKCE authorization, if using the full OAuth2 flow.
state: Optional[:class:`str`]
The state that will be used for authorization security.
authorized: :class:`bool`
Whether the user has already authorized the application.
application: :class:`PartialApplication`
The application that the authorization is for.
bot: Optional[:class:`User`]
The bot user associated with the application, provided if authorizing with the ``bot`` scope.
approximate_guild_count: Optional[:class:`int`]
The approximate number of guilds the bot is in, provided if authorizing with the ``bot`` scope.
guilds: List[:class:`UserGuild`]
The guilds the current user is in, provided if authorizing with the ``bot`` scope.
redirect_uri: Optional[:class:`str`]
The redirect URI that will be used for the authorization, if using the full OAuth2 flow and a redirect URI exists.
"""
__slots__ = (
'authorized',
'application',
'bot',
'approximate_guild_count',
'guilds',
'redirect_uri',
'scopes',
'response_type',
'code_challenge_method',
'code_challenge',
'state',
'_state',
)
def __init__(
self,
*,
_state: ConnectionState,
data: OAuth2AuthorizationPayload,
scopes: List[str],
response_type: Optional[str],
code_challenge_method: Optional[str] = None,
code_challenge: Optional[str] = None,
state: Optional[str],
):
self._state = _state
self.scopes: List[str] = scopes
self.response_type: Optional[str] = response_type
self.code_challenge_method: Optional[str] = code_challenge_method
self.code_challenge: Optional[str] = code_challenge
self.state: Optional[str] = state
self.authorized: bool = data['authorized']
self.application: PartialApplication = PartialApplication(state=_state, data=data['application'])
self.bot: Optional[User] = _state.store_user(data['bot']) if 'bot' in data else None
self.approximate_guild_count: Optional[int] = (
data['bot'].get('approximate_guild_count', 0) if 'bot' in data else None
)
self.guilds: List[UserGuild] = [UserGuild(state=_state, data=g) for g in data.get('guilds', [])]
self.redirect_uri: Optional[str] = data.get('redirect_uri')
def __repr__(self):
return f'<OAuth2Authorization authorized={self.authorized} application={self.application!r} scopes={self.scopes!r} response_type={self.response_type!r} redirect_uri={self.redirect_uri}>'
async def authorize(
self, *, guild: Snowflake = MISSING, channel: Snowflake = MISSING, permissions: Permissions = MISSING
) -> str:
"""|coro|
Authorizes the application for the user. A shortcut for :meth:`Client.create_authorization`.
Parameters
-----------
guild: :class:`Guild`
The guild to authorize for, if authorizing with the ``applications.commands`` or ``bot`` scopes.
channel: Union[:class:`TextChannel`, :class:`VoiceChannel`, :class:`StageChannel`]
The channel to authorize for, if authorizing with the ``webhooks.incoming`` scope. See :meth:`Guild.webhook_channels`.
permissions: :class:`Permissions`
The permissions to grant, if authorizing with the ``bot`` scope.
Raises
-------
HTTPException
Authorizing the application failed.
Returns
--------
:class:`str`
The URL to redirect the user to. May be an error page.
"""
data = await self._state.http.authorize_oauth2(
self.application.id,
self.scopes,
self.response_type,
self.redirect_uri,
self.code_challenge_method,
self.code_challenge,
self.state,
guild_id=guild.id if guild else None,
webhook_channel_id=channel.id if channel else None,
permissions=permissions.value if permissions else None,
)
return data['location']

68
discord/types/oauth2.py

@ -0,0 +1,68 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Optional, TypedDict
from typing_extensions import NotRequired
from .application import PartialApplication
from .snowflake import Snowflake
from .user import PartialUser
class OAuth2Token(TypedDict):
id: Snowflake
application: PartialApplication
scopes: List[str]
class BotUser(PartialUser):
approximate_guild_count: int
class OAuth2Guild(TypedDict):
id: Snowflake
name: str
icon: Optional[str]
permissions: str
mfa_level: int
class OAuth2Authorization(TypedDict):
authorized: bool
user: PartialUser
application: PartialApplication
bot: NotRequired[BotUser]
guilds: NotRequired[List[OAuth2Guild]]
redirect_uri: NotRequired[Optional[str]]
class OAuth2Location(TypedDict):
location: str
class WebhookChannel(TypedDict):
id: Snowflake
name: str

27
docs/api.rst

@ -701,9 +701,23 @@ Notes
Called when a :class:`User`\'s note is updated. Called when a :class:`User`\'s note is updated.
.. versionadded:: 2.0
:param note: The note that was updated. :param note: The note that was updated.
:type note: :class:`Note` :type note: :class:`Note`
OAuth2
~~~~~~~
.. function:: on_oauth2_token_revoke(token)
Called when an authorized application is revoked.
.. versionadded:: 2.0
:param token: The token that was revoked.
:type token: :class:`str`
Calls Calls
~~~~~ ~~~~~
@ -6666,6 +6680,19 @@ Library
.. autoclass:: LibrarySKU() .. autoclass:: LibrarySKU()
:members: :members:
OAuth2
~~~~~~
.. attributetable:: OAuth2Token
.. autoclass:: OAuth2Token()
:members:
.. attributetable:: OAuth2Authorization
.. autoclass:: OAuth2Authorization()
:members:
Promotion Promotion
~~~~~~~~~ ~~~~~~~~~

Loading…
Cancel
Save