From 4dba90d87e3af93ca838340359ad20c52c33be25 Mon Sep 17 00:00:00 2001 From: Paillat Date: Wed, 3 Jul 2024 15:57:38 +0200 Subject: [PATCH 1/8] :sparkles: Async support :tada: --- discordoauth2/__init__.py | 308 +---------------------- discordoauth2/async_oauth.py | 466 +++++++++++++++++++++++++++++++++++ discordoauth2/sync_oauth.py | 447 +++++++++++++++++++++++++++++++++ setup.py | 3 +- 4 files changed, 927 insertions(+), 297 deletions(-) create mode 100644 discordoauth2/async_oauth.py create mode 100644 discordoauth2/sync_oauth.py diff --git a/discordoauth2/__init__.py b/discordoauth2/__init__.py index 8b50d19..ee027aa 100644 --- a/discordoauth2/__init__.py +++ b/discordoauth2/__init__.py @@ -1,296 +1,12 @@ -import requests -from datetime import datetime -from typing import Optional, Union, Literal -from urllib import parse - -class PartialAccessToken(): - def __init__(self, access_token, client) -> None: - self.client: Client = client - self.token: str = access_token - - def revoke(self): - """Shorthand for `Client.revoke_token` with the `PartialAccessToken`'s access token.""" - return self.client.revoke_token(self.token, token_type="access_token") - - def fetch_identify(self) -> dict: - """Retrieves the user's [user object](https://discord.com/developers/docs/resources/user#user-object). Requires the `identify` scope and the `email` scope for their email address""" - response = requests.get("https://discord.com/api/v10/users/@me", headers={ - "authorization": f"Bearer {self.token}" - }) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def fetch_connections(self) -> list[dict]: - """Retrieves a list of [connection object](https://discord.com/developers/docs/resources/user#connection-object)s the user has linked. Requires the `connections` scope""" - response = requests.get("https://discord.com/api/v10/users/@me/connections", headers={ - "authorization": f"Bearer {self.token}" - }) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def fetch_guilds(self) -> list[dict]: - """Retrieves a list of [partial guild](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)s the user is in. Requires the `guilds` scope""" - - response = requests.get("https://discord.com/api/v10/users/@me/guilds", headers={ - "authorization": f"Bearer {self.token}" - }) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def fetch_guild_member(self, guild_id: int) -> dict: - """Retrieves the user's [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) in a specific guild. Requires the `guilds.members.read` scope - - guild_id: The guild ID to fetch member info for - """ - response = requests.get(f"https://discord.com/api/v10/users/@me/guilds/{guild_id}/member", headers={ - "authorization": f"Bearer {self.token}" - }) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 404: raise exceptions.HTTPException(f"user is not in this guild.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def join_guild(self, guild_id: int, user_id: int, nick: str = None, role_ids: list[int] = None, mute: bool = False, deaf: bool = False) -> dict: - """Adds the user to a guild. Requires the `guilds.join` scope and `Client` must have a bot token, and the bot must have `CREATE_INSTANT_INVITE` in the guild it wants to add the member to. Returns a [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) - - guild_id: The guild ID to add the user to - user_id: The ID of the user. Retrievable with `PartialAccessToken.fetch_identify()['id']` - nick: The nickname to give the user apon joining. Bot must also have `MANAGE_NICKNAMES` - role_ids: A list of role IDs to give the user apon joining (bypasses Membership Screening). Bot must also have `MANAGE_ROLES` - mute: Wether the user is muted in voice channels apon joining. Bot must also have `MUTE_MEMBERS` - deaf: Wether the user is deaf in voice channels apon joining. Bot must also have `DEAFEN_MEMBERS` - """ - response = requests.put(f"https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}", headers={ - "authorization": f"Bot {self.client._Client__bot_token}" - }, json={ - "access_token": self.token, - "nick": nick, - "roles": role_ids, - "mute": mute, - "deaf": deaf, - }) - - if response.status_code == 204: - raise exceptions.HTTPException(f"member is already in the guild.") - elif response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 403: raise exceptions.Forbidden(f"the Bot token must be for a bot in the guild that has permissions to create invites in the target guild and must have any other required permissions.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def fetch_metadata(self): - """Retrieves the user's [metadata](https://discord.com/developers/docs/resources/user#application-role-connection-object) for this application. Requires the `role_connections.write` scope""" - response = requests.get(f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", headers={"authorization": f"Bearer {self.token}"}) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def update_metadata(self, platform_name: str=None, username: str=None, **metadata) -> list[dict]: - """Updates the user's metadata for this application. Requires the `role_connections.write` scope - - platform_name: the platform's name. Appears in full capitals at the top of the application box in the client. - username: the user's name on the platform. Appears below the platform name, - metadata: key and value pairs for metadata. Allows `bool`, `int`, `datetime`, and `str` (only iso timestamps) values. - """ - def metadataTypeHook(item): - if type(item) == bool: - return 1 if item else 0 - if type(item) == datetime: - return item.isoformat() - else: return item - response = requests.put(f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", headers={ - "authorization": f"Bearer {self.token}"}, json={ - "platform_name": platform_name, - "platform_username": username, - "metadata": {key: metadataTypeHook(value) for key, value in metadata.items()} - }) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def clear_metadata(self): - """Clears the user's metadata for this application. Requires the `role_connections.write` scope""" - response = requests.put(f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", headers={"authorization": f"Bearer {self.token}"}, json={}) - - if response.ok: - return response.json() - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - -class AccessToken(PartialAccessToken): - def __init__(self, data, client) -> None: - super().__init__(data["access_token"], client) - - self.expires: int = data.get("expires_in") - self.scope: list[str] = data.get("scope", "").split(" ") - self.refresh_token: str = data.get("refresh_token") - self.webhook: Optional[dict] = data.get("webhook") - self.guild: Optional[dict] = data.get("guild") - - def revoke_refresh_token(self): - """Shorthand for `Client.revoke_token` with the `AccessToken`'s refresh token.""" - return self.client.revoke_token(self.refresh_token, token_type="refresh_token") - -class Client(): - def __init__(self, id: int, secret: str, redirect: str, bot_token: str=None): - """Represents a Discord Application. Create an application on the [Developer Portal](https://discord.com/developers/applications) - - id: The application's ID - secret: The application's Client Secret - redirect: The redirect URL for OAuth2 - bot_token: The token for the application's bot. Only required for joining guilds and updating linked roles metadata. - """ - self.id: int = id - self.redirect_url: str = redirect - self.__secret = secret - self.__bot_token = bot_token - - def update_linked_roles_metadata(self, metadata: list[dict]): - """Updates the application's linked roles metadata. - - metadata: List of [application role connection metadata](https://discord.com/developers/docs/resources/application-role-connection-metadata#application-role-connection-metadata-object) - """ - requests.put(f"https://discord.com/api/v10/applications/{self.id}/role-connections/metadata", headers={ - "authorization": f"Bot {self.__bot_token}"}, json=metadata) - - def from_access_token(self, access_token: str) -> PartialAccessToken: - """Creates a `PartialAccessToken` from a access token string. - - access_token: access token from `PartialAccessToken.token` - """ - return PartialAccessToken(access_token, self) - - def exchange_code(self, code: str) -> AccessToken: - """Converts a code from the redirect url into a `AccessToken` - - code: `code` paramater from OAuth2 redirect URL - """ - response = requests.post("https://discord.com/api/v10/oauth2/token", data={ - "grant_type": "authorization_code", "code": code, - "client_id": self.id, "client_secret": self.__secret, - "redirect_uri": self.redirect_url}) - - if response.ok: - return AccessToken(response.json(), self) - elif response.status_code == 400: raise exceptions.HTTPException("the code, client id, client secret or the redirect uri is invalid/don't match.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def refresh_token(self, refresh_token: str) -> AccessToken: - """Converts a refresh token into a new `AccessToken` - - refresh_token: refresh token from `AccessToken.refresh_token` - """ - response = requests.post("https://discord.com/api/v10/oauth2/token", data={ - "grant_type": "refresh_token", "refresh_token": refresh_token, - "client_id": self.id, "client_secret": self.__secret}) - - if response.ok: - return AccessToken(response.json(), self) - elif response.status_code == 400: raise exceptions.HTTPException("the refresh token, client id or client secret is invalid/don't match.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def client_credentails_grant(self, scope: list[str]) -> AccessToken: - """Creates an `AccessToken` on behalf of the application's owner. If the owner is a team, then only `identify` and `applications.commands.update` are allowed. - - scope: list of string scopes to authorize. - """ - response = requests.post("https://discord.com/api/v10/oauth2/token", data={ - "grant_type": "client_credentials", "scope": " ".join(scope)}, - auth=(self.id, self.__secret)) - if response.ok: - return AccessToken(response.json(), self) - elif response.status_code == 400: raise exceptions.HTTPException("the scope, client id or client secret is invalid/don't match.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def revoke_token(self, token: str, token_type: str=None): - """Revokes a OAuth2 token related to the client. - - token: the token string to be revoked. - token_type: the type of token to be revoked. This may be ignored by Discord. - """ - response = requests.post("https://discord.com/api/oauth2/token/revoke", - data={"token": token, "token_type_hint": token_type}, - auth=(self.id, self.__secret)) - print(response.status_code, response.text) - if response.ok: - return - elif response.status_code == 401: raise exceptions.Forbidden(f"this AccessToken does not have the nessasary scope.") - elif response.status_code == 429: raise exceptions.RateLimited(f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()['retry_after']) - else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") - - def generate_uri(self, scope: Union[str, list[str]], state: Optional[str]=None, skip_prompt: Optional[bool]=False, response_type: Optional[Literal["code", "token"]]="code", guild_id: Optional[Union[int, str]]=None, disable_guild_select: Optional[bool]=None, permissions: Optional[Union[int, str]]=None) -> str: - """Creates an authorization uri with client information prefilled. - - scope: a string, or list of strings for the scope - state: optional state parameter. Optional but recommended. - skip_prompt: doesn't require the end user to reauthorize if they've already authorized you app before. Defaults to `False`. - response_type: either code, or token. token means the server can't access it, but the client can use it without converting. - guild_id: the guild ID to add a bot/webhook. - disable_guild_select: wether to allow the authorizing user to change the selected guild - permissions: the permission bitwise integer for the bot being added. - """ - params = { - "client_id": self.id, - "scope": " ".join(scope) if type(scope) == list else scope, - "state": state, - "redirect_uri": self.redirect_url, - "prompt": "none" if skip_prompt else None, - "response_type": response_type, - "guild_id": guild_id, - "disable_guild_select": disable_guild_select, - "permissions": permissions - } - return f"https://discord.com/oauth2/authorize?{parse.urlencode({key: value for key, value in params.items() if value is not None})}" - -class exceptions(): - class BaseException(Exception): - pass - - class HTTPException(BaseException): - pass - - class RateLimited(HTTPException): - def __init__(self, text, retry_after): - self.retry_after = retry_after - super().__init__(text) - - class Forbidden(HTTPException): - pass +from .async_oauth import AsyncClient, AsyncAccessToken, AsyncPartialAccessToken +from .sync_oauth import Client, AccessToken, PartialAccessToken, exceptions + +__all__ = [ + "Client", + "AccessToken", + "PartialAccessToken", + "AsyncClient", + "AsyncAccessToken", + "AsyncPartialAccessToken", + "exceptions", +] diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py new file mode 100644 index 0000000..649dca4 --- /dev/null +++ b/discordoauth2/async_oauth.py @@ -0,0 +1,466 @@ +import aiohttp +from datetime import datetime +from typing import Optional, Union, Literal +from urllib import parse + + +class AsyncPartialAccessToken: + def __init__(self, access_token, client) -> None: + self.client: Client = client + self.token: str = access_token + + async def revoke(self): + """Shorthand for `Client.revoke_token` with the `PartialAccessToken`'s access token.""" + return await self.client.revoke_token(self.token, token_type="access_token") + + async def fetch_identify(self) -> dict: + """Retrieves the user's [user object](https://discord.com/developers/docs/resources/user#user-object). + Requires the `identify` scope and the `email` scope for their email address + """ + async with aiohttp.ClientSession() as session: + async with session.get( + "https://discord.com/api/v10/users/@me", + headers={"authorization": f"Bearer {self.token}"}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def fetch_connections(self) -> list[dict]: + """Retrieves a list of [connection object](https://discord.com/developers/docs/resources/user#connection-object)s the user has linked. Requires the `connections` scope""" + async with aiohttp.ClientSession() as session: + async with session.get( + "https://discord.com/api/v10/users/@me/connections", + headers={"authorization": f"Bearer {self.token}"}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def fetch_guilds(self) -> list[dict]: + """Retrieves a list of [partial guild](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)s the user is in. Requires the `guilds` scope""" + + async with aiohttp.ClientSession() as session: + async with session.get( + "https://discord.com/api/v10/users/@me/guilds", + headers={"authorization": f"Bearer {self.token}"}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def fetch_guild_member(self, guild_id: int) -> dict: + """Retrieves the user's [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) in a specific guild. Requires the `guilds.members.read` scope + + guild_id: The guild ID to fetch member info for + """ + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://discord.com/api/v10/users/@me/guilds/{guild_id}/member", + headers={"authorization": f"Bearer {self.token}"}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 404: + raise exceptions.HTTPException(f"user is not in this guild.") + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def join_guild( + self, + guild_id: int, + user_id: int, + nick: str = None, + role_ids: list[int] = None, + mute: bool = False, + deaf: bool = False, + ) -> dict: + """Adds the user to a guild. Requires the `guilds.join` scope and `Client` must have a bot token, and the bot must have `CREATE_INSTANT_INVITE` in the guild it wants to add the member to. Returns a [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) + + guild_id: The guild ID to add the user to + user_id: The ID of the user. Retrievable with `PartialAccessToken.fetch_identify()['id']` + nick: The nickname to give the user apon joining. Bot must also have `MANAGE_NICKNAMES` + role_ids: A list of role IDs to give the user apon joining (bypasses Membership Screening). Bot must also have `MANAGE_ROLES` + mute: Wether the user is muted in voice channels apon joining. Bot must also have `MUTE_MEMBERS` + deaf: Wether the user is deaf in voice channels apon joining. Bot must also have `DEAFEN_MEMBERS` + """ + async with aiohttp.ClientSession() as session: + async with session.put( + f"https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}", + headers={"authorization": f"Bot {self.client._Client__bot_token}"}, + json={ + "access_token": self.token, + "nick": nick, + "roles": role_ids, + "mute": mute, + "deaf": deaf, + }, + ) as response: + if response.ok: + return await response.json() + elif response.status == 204: + raise exceptions.HTTPException(f"member is already in the guild.") + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 403: + raise exceptions.Forbidden( + f"the Bot token must be for a bot in the guild that has permissions to create invites in the target guild and must have any other required permissions." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def fetch_metadata(self): + """Retrieves the user's [metadata](https://discord.com/developers/docs/resources/user#application-role-connection-object) for this application. Requires the `role_connections.write` scope""" + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def update_metadata( + self, platform_name: str = None, username: str = None, **metadata + ) -> list[dict]: + """Updates the user's metadata for this application. Requires the `role_connections.write` scope + + platform_name: the platform's name. Appears in full capitals at the top of the application box in the client. + username: the user's name on the platform. Appears below the platform name, + metadata: key and value pairs for metadata. Allows `bool`, `int`, `datetime`, and `str` (only iso timestamps) values. + """ + + def metadataTypeHook(item): + if type(item) == bool: + return 1 if item else 0 + if type(item) == datetime: + return item.isoformat() + else: + return item + + async with aiohttp.ClientSession() as session: + async with session.put( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + json={ + "platform_name": platform_name, + "platform_username": username, + "metadata": { + key: metadataTypeHook(value) for key, value in metadata.items() + }, + }, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def clear_metadata(self): + """Clears the user's metadata for this application. Requires the `role_connections.write` scope""" + async with aiohttp.ClientSession() as session: + async with session.put( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + json={}, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + +class AsyncAccessToken(AsyncPartialAccessToken): + def __init__(self, data, client) -> None: + super().__init__(data["access_token"], client) + + self.expires: int = data.get("expires_in") + self.scope: list[str] = data.get("scope", "").split(" ") + self.refresh_token: str = data.get("refresh_token") + self.webhook: Optional[dict] = data.get("webhook") + self.guild: Optional[dict] = data.get("guild") + + async def revoke_refresh_token(self): + """Shorthand for `Client.revoke_token` with the `AccessToken`'s refresh token.""" + return await self.client.revoke_token( + self.refresh_token, token_type="refresh_token" + ) + + +class AsyncClient: + def __init__(self, id: int, secret: str, redirect: str, bot_token: str = None): + """Represents a Discord Application. Create an application on the [Developer Portal](https://discord.com/developers/applications) + + id: The application's ID + secret: The application's Client Secret + redirect: The redirect URL for OAuth2 + bot_token: The token for the application's bot. Only required for joining guilds and updating linked roles metadata. + """ + self.id: int = id + self.redirect_url: str = redirect + self.__secret = secret + self.__bot_token = bot_token + + async def update_linked_roles_metadata(self, metadata: list[dict]): + """Updates the application's linked roles metadata. + + metadata: List of [application role connection metadata](https://discord.com/developers/docs/resources/application-role-connection-metadata#application-role-connection-metadata-object) + """ + async with aiohttp.ClientSession() as session: + async with session.put( + f"https://discord.com/api/v10/applications/{self.id}/role-connections/metadata", + headers={"authorization": f"Bot {self.__bot_token}"}, + json=metadata, + ) as response: + if response.ok: + return await response.json() + elif response.status == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + def from_access_token(self, access_token: str) -> AsyncPartialAccessToken: + """Creates a `PartialAccessToken` from a access token string. + + access_token: access token from `PartialAccessToken.token` + """ + return AsyncPartialAccessToken(access_token, self) + + async def exchange_code(self, code: str) -> AsyncAccessToken: + """Converts a code from the redirect url into a `AccessToken` + + code: `code` paramater from OAuth2 redirect URL + """ + async with aiohttp.ClientSession() as session: + async with session.post( + "https://discord.com/api/v10/oauth2/token", + data={ + "grant_type": "authorization_code", + "code": code, + "client_id": self.id, + "client_secret": self.__secret, + "redirect_uri": self.redirect_url, + }, + ) as response: + if response.ok: + return AsyncAccessToken(await response.json(), self) + elif response.status == 400: + raise exceptions.HTTPException( + "the code, client id, client secret or the redirect uri is invalid/don't match." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def refresh_token(self, refresh_token: str) -> AsyncAccessToken: + """Converts a refresh token into a new `AccessToken` + + refresh_token: refresh token from `AccessToken.refresh_token` + """ + async with aiohttp.ClientSession() as session: + async with session.post( + "https://discord.com/api/v10/oauth2/token", + data={ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": self.id, + "client_secret": self.__secret, + }, + ) as response: + if response.ok: + return AsyncAccessToken(await response.json(), self) + elif response.status == 400: + raise exceptions.HTTPException( + "the refresh token, client id or client secret is invalid/don't match." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def client_credentails_grant(self, scope: list[str]) -> AsyncAccessToken: + """Creates an `AccessToken` on behalf of the application's owner. If the owner is a team, then only `identify` and `applications.commands.update` are allowed. + + scope: list of string scopes to authorize. + """ + async with aiohttp.ClientSession() as session: + async with session.post( + "https://discord.com/api/v10/oauth2/token", + data={"grant_type": "client_credentials", "scope": " ".join(scope)}, + auth=(self.id, self.__secret), + ) as response: + if response.ok: + return AsyncAccessToken(await response.json(), self) + elif response.status == 400: + raise exceptions.HTTPException( + "the scope, client id or client secret is invalid/don't match." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + async def revoke_token(self, token: str, token_type: str = None): + """Revokes a OAuth2 token related to the client. + + token: the token string to be revoked. + token_type: the type of token to be revoked. This may be ignored by Discord. + """ + async with aiohttp.ClientSession() as session: + async with session.post( + "https://discord.com/api/oauth2/token/revoke", + data={"token": token, "token_type_hint": token_type}, + auth=(self.id, self.__secret), + ) as response: + if response.ok: + return + elif response.status == 400: + raise exceptions.HTTPException( + "the token or token type is invalid." + ) + elif response.status == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + + def generate_uri( + self, + scope: Union[str, list[str]], + state: Optional[str] = None, + skip_prompt: Optional[bool] = False, + response_type: Optional[Literal["code", "token"]] = "code", + guild_id: Optional[Union[int, str]] = None, + disable_guild_select: Optional[bool] = None, + permissions: Optional[Union[int, str]] = None, + ) -> str: + """Creates an authorization uri with client information prefilled. + + scope: a string, or list of strings for the scope + state: optional state parameter. Optional but recommended. + skip_prompt: doesn't require the end user to reauthorize if they've already authorized you app before. Defaults to `False`. + response_type: either code, or token. token means the server can't access it, but the client can use it without converting. + guild_id: the guild ID to add a bot/webhook. + disable_guild_select: wether to allow the authorizing user to change the selected guild + permissions: the permission bitwise integer for the bot being added. + """ + params = { + "client_id": self.id, + "scope": " ".join(scope) if type(scope) == list else scope, + "state": state, + "redirect_uri": self.redirect_url, + "prompt": "none" if skip_prompt else None, + "response_type": response_type, + "guild_id": guild_id, + "disable_guild_select": disable_guild_select, + "permissions": permissions, + } + return f"https://discord.com/oauth2/authorize?{parse.urlencode({key: value for key, value in params.items() if value is not None})}" + + +class exceptions: + class BaseException(Exception): + pass + + class HTTPException(BaseException): + pass + + class RateLimited(HTTPException): + def __init__(self, text, retry_after): + self.retry_after = retry_after + super().__init__(text) + + class Forbidden(HTTPException): + pass diff --git a/discordoauth2/sync_oauth.py b/discordoauth2/sync_oauth.py new file mode 100644 index 0000000..d982153 --- /dev/null +++ b/discordoauth2/sync_oauth.py @@ -0,0 +1,447 @@ +import requests +from datetime import datetime +from typing import Optional, Union, Literal +from urllib import parse + + +class PartialAccessToken: + def __init__(self, access_token, client) -> None: + self.client: Client = client + self.token: str = access_token + + def revoke(self): + """Shorthand for `Client.revoke_token` with the `PartialAccessToken`'s access token.""" + return self.client.revoke_token(self.token, token_type="access_token") + + def fetch_identify(self) -> dict: + """Retrieves the user's [user object](https://discord.com/developers/docs/resources/user#user-object). Requires the `identify` scope and the `email` scope for their email address""" + response = requests.get( + "https://discord.com/api/v10/users/@me", + headers={"authorization": f"Bearer {self.token}"}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def fetch_connections(self) -> list[dict]: + """Retrieves a list of [connection object](https://discord.com/developers/docs/resources/user#connection-object)s the user has linked. Requires the `connections` scope""" + response = requests.get( + "https://discord.com/api/v10/users/@me/connections", + headers={"authorization": f"Bearer {self.token}"}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def fetch_guilds(self) -> list[dict]: + """Retrieves a list of [partial guild](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)s the user is in. Requires the `guilds` scope""" + + response = requests.get( + "https://discord.com/api/v10/users/@me/guilds", + headers={"authorization": f"Bearer {self.token}"}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def fetch_guild_member(self, guild_id: int) -> dict: + """Retrieves the user's [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) in a specific guild. Requires the `guilds.members.read` scope + + guild_id: The guild ID to fetch member info for + """ + response = requests.get( + f"https://discord.com/api/v10/users/@me/guilds/{guild_id}/member", + headers={"authorization": f"Bearer {self.token}"}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 404: + raise exceptions.HTTPException(f"user is not in this guild.") + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def join_guild( + self, + guild_id: int, + user_id: int, + nick: str = None, + role_ids: list[int] = None, + mute: bool = False, + deaf: bool = False, + ) -> dict: + """Adds the user to a guild. Requires the `guilds.join` scope and `Client` must have a bot token, and the bot must have `CREATE_INSTANT_INVITE` in the guild it wants to add the member to. Returns a [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) + + guild_id: The guild ID to add the user to + user_id: The ID of the user. Retrievable with `PartialAccessToken.fetch_identify()['id']` + nick: The nickname to give the user apon joining. Bot must also have `MANAGE_NICKNAMES` + role_ids: A list of role IDs to give the user apon joining (bypasses Membership Screening). Bot must also have `MANAGE_ROLES` + mute: Wether the user is muted in voice channels apon joining. Bot must also have `MUTE_MEMBERS` + deaf: Wether the user is deaf in voice channels apon joining. Bot must also have `DEAFEN_MEMBERS` + """ + response = requests.put( + f"https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}", + headers={"authorization": f"Bot {self.client._Client__bot_token}"}, + json={ + "access_token": self.token, + "nick": nick, + "roles": role_ids, + "mute": mute, + "deaf": deaf, + }, + ) + + if response.status_code == 204: + raise exceptions.HTTPException(f"member is already in the guild.") + elif response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 403: + raise exceptions.Forbidden( + f"the Bot token must be for a bot in the guild that has permissions to create invites in the target guild and must have any other required permissions." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def fetch_metadata(self): + """Retrieves the user's [metadata](https://discord.com/developers/docs/resources/user#application-role-connection-object) for this application. Requires the `role_connections.write` scope""" + response = requests.get( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def update_metadata( + self, platform_name: str = None, username: str = None, **metadata + ) -> list[dict]: + """Updates the user's metadata for this application. Requires the `role_connections.write` scope + + platform_name: the platform's name. Appears in full capitals at the top of the application box in the client. + username: the user's name on the platform. Appears below the platform name, + metadata: key and value pairs for metadata. Allows `bool`, `int`, `datetime`, and `str` (only iso timestamps) values. + """ + + def metadataTypeHook(item): + if type(item) == bool: + return 1 if item else 0 + if type(item) == datetime: + return item.isoformat() + else: + return item + + response = requests.put( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + json={ + "platform_name": platform_name, + "platform_username": username, + "metadata": { + key: metadataTypeHook(value) for key, value in metadata.items() + }, + }, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def clear_metadata(self): + """Clears the user's metadata for this application. Requires the `role_connections.write` scope""" + response = requests.put( + f"https://discord.com/api/v10/users/@me/applications/{self.client.id}/role-connection", + headers={"authorization": f"Bearer {self.token}"}, + json={}, + ) + + if response.ok: + return response.json() + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + +class AccessToken(PartialAccessToken): + def __init__(self, data, client) -> None: + super().__init__(data["access_token"], client) + + self.expires: int = data.get("expires_in") + self.scope: list[str] = data.get("scope", "").split(" ") + self.refresh_token: str = data.get("refresh_token") + self.webhook: Optional[dict] = data.get("webhook") + self.guild: Optional[dict] = data.get("guild") + + def revoke_refresh_token(self): + """Shorthand for `Client.revoke_token` with the `AccessToken`'s refresh token.""" + return self.client.revoke_token(self.refresh_token, token_type="refresh_token") + + +class Client: + def __init__(self, id: int, secret: str, redirect: str, bot_token: str = None): + """Represents a Discord Application. Create an application on the [Developer Portal](https://discord.com/developers/applications) + + id: The application's ID + secret: The application's Client Secret + redirect: The redirect URL for OAuth2 + bot_token: The token for the application's bot. Only required for joining guilds and updating linked roles metadata. + """ + self.id: int = id + self.redirect_url: str = redirect + self.__secret = secret + self.__bot_token = bot_token + + def update_linked_roles_metadata(self, metadata: list[dict]): + """Updates the application's linked roles metadata. + + metadata: List of [application role connection metadata](https://discord.com/developers/docs/resources/application-role-connection-metadata#application-role-connection-metadata-object) + """ + requests.put( + f"https://discord.com/api/v10/applications/{self.id}/role-connections/metadata", + headers={"authorization": f"Bot {self.__bot_token}"}, + json=metadata, + ) + + def from_access_token(self, access_token: str) -> PartialAccessToken: + """Creates a `PartialAccessToken` from a access token string. + + access_token: access token from `PartialAccessToken.token` + """ + return PartialAccessToken(access_token, self) + + def exchange_code(self, code: str) -> AccessToken: + """Converts a code from the redirect url into a `AccessToken` + + code: `code` paramater from OAuth2 redirect URL + """ + response = requests.post( + "https://discord.com/api/v10/oauth2/token", + data={ + "grant_type": "authorization_code", + "code": code, + "client_id": self.id, + "client_secret": self.__secret, + "redirect_uri": self.redirect_url, + }, + ) + + if response.ok: + return AccessToken(response.json(), self) + elif response.status_code == 400: + raise exceptions.HTTPException( + "the code, client id, client secret or the redirect uri is invalid/don't match." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def refresh_token(self, refresh_token: str) -> AccessToken: + """Converts a refresh token into a new `AccessToken` + + refresh_token: refresh token from `AccessToken.refresh_token` + """ + response = requests.post( + "https://discord.com/api/v10/oauth2/token", + data={ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": self.id, + "client_secret": self.__secret, + }, + ) + + if response.ok: + return AccessToken(response.json(), self) + elif response.status_code == 400: + raise exceptions.HTTPException( + "the refresh token, client id or client secret is invalid/don't match." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def client_credentails_grant(self, scope: list[str]) -> AccessToken: + """Creates an `AccessToken` on behalf of the application's owner. If the owner is a team, then only `identify` and `applications.commands.update` are allowed. + + scope: list of string scopes to authorize. + """ + response = requests.post( + "https://discord.com/api/v10/oauth2/token", + data={"grant_type": "client_credentials", "scope": " ".join(scope)}, + auth=(self.id, self.__secret), + ) + if response.ok: + return AccessToken(response.json(), self) + elif response.status_code == 400: + raise exceptions.HTTPException( + "the scope, client id or client secret is invalid/don't match." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def revoke_token(self, token: str, token_type: str = None): + """Revokes a OAuth2 token related to the client. + + token: the token string to be revoked. + token_type: the type of token to be revoked. This may be ignored by Discord. + """ + response = requests.post( + "https://discord.com/api/oauth2/token/revoke", + data={"token": token, "token_type_hint": token_type}, + auth=(self.id, self.__secret), + ) + print(response.status_code, response.text) + if response.ok: + return + elif response.status_code == 401: + raise exceptions.Forbidden( + f"this AccessToken does not have the nessasary scope." + ) + elif response.status_code == 429: + raise exceptions.RateLimited( + f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", + retry_after=response.json()["retry_after"], + ) + else: + raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + + def generate_uri( + self, + scope: Union[str, list[str]], + state: Optional[str] = None, + skip_prompt: Optional[bool] = False, + response_type: Optional[Literal["code", "token"]] = "code", + guild_id: Optional[Union[int, str]] = None, + disable_guild_select: Optional[bool] = None, + permissions: Optional[Union[int, str]] = None, + ) -> str: + """Creates an authorization uri with client information prefilled. + + scope: a string, or list of strings for the scope + state: optional state parameter. Optional but recommended. + skip_prompt: doesn't require the end user to reauthorize if they've already authorized you app before. Defaults to `False`. + response_type: either code, or token. token means the server can't access it, but the client can use it without converting. + guild_id: the guild ID to add a bot/webhook. + disable_guild_select: wether to allow the authorizing user to change the selected guild + permissions: the permission bitwise integer for the bot being added. + """ + params = { + "client_id": self.id, + "scope": " ".join(scope) if type(scope) == list else scope, + "state": state, + "redirect_uri": self.redirect_url, + "prompt": "none" if skip_prompt else None, + "response_type": response_type, + "guild_id": guild_id, + "disable_guild_select": disable_guild_select, + "permissions": permissions, + } + return f"https://discord.com/oauth2/authorize?{parse.urlencode({key: value for key, value in params.items() if value is not None})}" + + +class exceptions: + class BaseException(Exception): + pass + + class HTTPException(BaseException): + pass + + class RateLimited(HTTPException): + def __init__(self, text, retry_after): + self.retry_after = retry_after + super().__init__(text) + + class Forbidden(HTTPException): + pass diff --git a/setup.py b/setup.py index d7e68fd..a8cf19b 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ url='https://github.com/TreeBen77/discordoauth2', keywords='flask, oauth2, discord, discord-api', install_requires=[ - 'requests' + 'aiohttp', + 'requests', ] ) From 5078910ef45a8794180fa58de50a6605b42d66f0 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 4 Jul 2024 18:33:51 +0200 Subject: [PATCH 2/8] :bug: Fix bug because of name mangling --- .gitignore | 1 + discordoauth2/async_oauth.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8937a5a..cef7167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ main.py rewrite.py discordoauth2/__pycache__ +.idea/ \ No newline at end of file diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py index 649dca4..16d307b 100644 --- a/discordoauth2/async_oauth.py +++ b/discordoauth2/async_oauth.py @@ -6,7 +6,7 @@ class AsyncPartialAccessToken: def __init__(self, access_token, client) -> None: - self.client: Client = client + self.client: AsyncClient = client self.token: str = access_token async def revoke(self): @@ -126,7 +126,7 @@ async def join_guild( async with aiohttp.ClientSession() as session: async with session.put( f"https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}", - headers={"authorization": f"Bot {self.client._Client__bot_token}"}, + headers={"authorization": f"Bot {self.client._AsyncClient__bot_token}"}, json={ "access_token": self.token, "nick": nick, From 5e909b90c3f04cfa6fb717db8a572c83390e93a1 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 Jul 2024 15:52:34 +0200 Subject: [PATCH 3/8] :bug: Fix exceptions not shared. --- discordoauth2/__init__.py | 4 +- discordoauth2/async_oauth.py | 103 +++++++++++++++-------------------- discordoauth2/exceptions.py | 18 ++++++ discordoauth2/sync_oauth.py | 97 ++++++++++++++------------------- 4 files changed, 108 insertions(+), 114 deletions(-) create mode 100644 discordoauth2/exceptions.py diff --git a/discordoauth2/__init__.py b/discordoauth2/__init__.py index ee027aa..834d787 100644 --- a/discordoauth2/__init__.py +++ b/discordoauth2/__init__.py @@ -1,5 +1,6 @@ +from .exceptions import exceptions, Exceptions from .async_oauth import AsyncClient, AsyncAccessToken, AsyncPartialAccessToken -from .sync_oauth import Client, AccessToken, PartialAccessToken, exceptions +from .sync_oauth import Client, AccessToken, PartialAccessToken __all__ = [ "Client", @@ -9,4 +10,5 @@ "AsyncAccessToken", "AsyncPartialAccessToken", "exceptions", + "Exceptions", ] diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py index 16d307b..c8e110f 100644 --- a/discordoauth2/async_oauth.py +++ b/discordoauth2/async_oauth.py @@ -1,8 +1,11 @@ import aiohttp + from datetime import datetime from typing import Optional, Union, Literal from urllib import parse +from .exceptions import Exceptions + class AsyncPartialAccessToken: def __init__(self, access_token, client) -> None: @@ -25,16 +28,16 @@ async def fetch_identify(self) -> dict: if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def fetch_connections(self) -> list[dict]: """Retrieves a list of [connection object](https://discord.com/developers/docs/resources/user#connection-object)s the user has linked. Requires the `connections` scope""" @@ -46,16 +49,16 @@ async def fetch_connections(self) -> list[dict]: if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def fetch_guilds(self) -> list[dict]: """Retrieves a list of [partial guild](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)s the user is in. Requires the `guilds` scope""" @@ -68,16 +71,16 @@ async def fetch_guilds(self) -> list[dict]: if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def fetch_guild_member(self, guild_id: int) -> dict: """Retrieves the user's [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) in a specific guild. Requires the `guilds.members.read` scope @@ -92,18 +95,18 @@ async def fetch_guild_member(self, guild_id: int) -> dict: if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 404: - raise exceptions.HTTPException(f"user is not in this guild.") + raise Exceptions.HTTPException(f"user is not in this guild.") elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def join_guild( self, @@ -138,22 +141,22 @@ async def join_guild( if response.ok: return await response.json() elif response.status == 204: - raise exceptions.HTTPException(f"member is already in the guild.") + raise Exceptions.HTTPException(f"member is already in the guild.") elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 403: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"the Bot token must be for a bot in the guild that has permissions to create invites in the target guild and must have any other required permissions." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def fetch_metadata(self): """Retrieves the user's [metadata](https://discord.com/developers/docs/resources/user#application-role-connection-object) for this application. Requires the `role_connections.write` scope""" @@ -165,16 +168,16 @@ async def fetch_metadata(self): if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def update_metadata( self, platform_name: str = None, username: str = None, **metadata @@ -209,16 +212,16 @@ def metadataTypeHook(item): if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def clear_metadata(self): """Clears the user's metadata for this application. Requires the `role_connections.write` scope""" @@ -231,16 +234,16 @@ async def clear_metadata(self): if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") class AsyncAccessToken(AsyncPartialAccessToken): @@ -288,16 +291,16 @@ async def update_linked_roles_metadata(self, metadata: list[dict]): if response.ok: return await response.json() elif response.status == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") def from_access_token(self, access_token: str) -> AsyncPartialAccessToken: """Creates a `PartialAccessToken` from a access token string. @@ -325,16 +328,16 @@ async def exchange_code(self, code: str) -> AsyncAccessToken: if response.ok: return AsyncAccessToken(await response.json(), self) elif response.status == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the code, client id, client secret or the redirect uri is invalid/don't match." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def refresh_token(self, refresh_token: str) -> AsyncAccessToken: """Converts a refresh token into a new `AccessToken` @@ -354,16 +357,16 @@ async def refresh_token(self, refresh_token: str) -> AsyncAccessToken: if response.ok: return AsyncAccessToken(await response.json(), self) elif response.status == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the refresh token, client id or client secret is invalid/don't match." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def client_credentails_grant(self, scope: list[str]) -> AsyncAccessToken: """Creates an `AccessToken` on behalf of the application's owner. If the owner is a team, then only `identify` and `applications.commands.update` are allowed. @@ -379,16 +382,16 @@ async def client_credentails_grant(self, scope: list[str]) -> AsyncAccessToken: if response.ok: return AsyncAccessToken(await response.json(), self) elif response.status == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the scope, client id or client secret is invalid/don't match." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") async def revoke_token(self, token: str, token_type: str = None): """Revokes a OAuth2 token related to the client. @@ -405,16 +408,16 @@ async def revoke_token(self, token: str, token_type: str = None): if response.ok: return elif response.status == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the token or token type is invalid." ) elif response.status == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status}") def generate_uri( self, @@ -448,19 +451,3 @@ def generate_uri( "permissions": permissions, } return f"https://discord.com/oauth2/authorize?{parse.urlencode({key: value for key, value in params.items() if value is not None})}" - - -class exceptions: - class BaseException(Exception): - pass - - class HTTPException(BaseException): - pass - - class RateLimited(HTTPException): - def __init__(self, text, retry_after): - self.retry_after = retry_after - super().__init__(text) - - class Forbidden(HTTPException): - pass diff --git a/discordoauth2/exceptions.py b/discordoauth2/exceptions.py new file mode 100644 index 0000000..f9977d6 --- /dev/null +++ b/discordoauth2/exceptions.py @@ -0,0 +1,18 @@ +class Exceptions: + class BaseException(Exception): + pass + + class HTTPException(BaseException): + pass + + class RateLimited(HTTPException): + def __init__(self, text, retry_after): + self.retry_after = retry_after + super().__init__(text) + + class Forbidden(HTTPException): + pass + + +# Alias for compatibility with the old version, but class names should follow the CapitalizedWords convention +exceptions = Exceptions diff --git a/discordoauth2/sync_oauth.py b/discordoauth2/sync_oauth.py index d982153..e5f9ce9 100644 --- a/discordoauth2/sync_oauth.py +++ b/discordoauth2/sync_oauth.py @@ -1,8 +1,11 @@ import requests + from datetime import datetime from typing import Optional, Union, Literal from urllib import parse +from .exceptions import Exceptions + class PartialAccessToken: def __init__(self, access_token, client) -> None: @@ -23,16 +26,16 @@ def fetch_identify(self) -> dict: if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def fetch_connections(self) -> list[dict]: """Retrieves a list of [connection object](https://discord.com/developers/docs/resources/user#connection-object)s the user has linked. Requires the `connections` scope""" @@ -44,16 +47,16 @@ def fetch_connections(self) -> list[dict]: if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def fetch_guilds(self) -> list[dict]: """Retrieves a list of [partial guild](https://discord.com/developers/docs/resources/user#get-current-user-guilds-example-partial-guild)s the user is in. Requires the `guilds` scope""" @@ -66,16 +69,16 @@ def fetch_guilds(self) -> list[dict]: if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def fetch_guild_member(self, guild_id: int) -> dict: """Retrieves the user's [guild member object](https://discord.com/developers/docs/resources/guild#guild-member-object) in a specific guild. Requires the `guilds.members.read` scope @@ -90,18 +93,18 @@ def fetch_guild_member(self, guild_id: int) -> dict: if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 404: - raise exceptions.HTTPException(f"user is not in this guild.") + raise Exceptions.HTTPException(f"user is not in this guild.") elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def join_guild( self, @@ -134,24 +137,24 @@ def join_guild( ) if response.status_code == 204: - raise exceptions.HTTPException(f"member is already in the guild.") + raise Exceptions.HTTPException(f"member is already in the guild.") elif response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 403: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"the Bot token must be for a bot in the guild that has permissions to create invites in the target guild and must have any other required permissions." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def fetch_metadata(self): """Retrieves the user's [metadata](https://discord.com/developers/docs/resources/user#application-role-connection-object) for this application. Requires the `role_connections.write` scope""" @@ -163,16 +166,16 @@ def fetch_metadata(self): if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def update_metadata( self, platform_name: str = None, username: str = None, **metadata @@ -207,16 +210,16 @@ def metadataTypeHook(item): if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def clear_metadata(self): """Clears the user's metadata for this application. Requires the `role_connections.write` scope""" @@ -229,16 +232,16 @@ def clear_metadata(self): if response.ok: return response.json() elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") class AccessToken(PartialAccessToken): @@ -307,16 +310,16 @@ def exchange_code(self, code: str) -> AccessToken: if response.ok: return AccessToken(response.json(), self) elif response.status_code == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the code, client id, client secret or the redirect uri is invalid/don't match." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def refresh_token(self, refresh_token: str) -> AccessToken: """Converts a refresh token into a new `AccessToken` @@ -336,16 +339,16 @@ def refresh_token(self, refresh_token: str) -> AccessToken: if response.ok: return AccessToken(response.json(), self) elif response.status_code == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the refresh token, client id or client secret is invalid/don't match." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def client_credentails_grant(self, scope: list[str]) -> AccessToken: """Creates an `AccessToken` on behalf of the application's owner. If the owner is a team, then only `identify` and `applications.commands.update` are allowed. @@ -360,16 +363,16 @@ def client_credentails_grant(self, scope: list[str]) -> AccessToken: if response.ok: return AccessToken(response.json(), self) elif response.status_code == 400: - raise exceptions.HTTPException( + raise Exceptions.HTTPException( "the scope, client id or client secret is invalid/don't match." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def revoke_token(self, token: str, token_type: str = None): """Revokes a OAuth2 token related to the client. @@ -386,16 +389,16 @@ def revoke_token(self, token: str, token_type: str = None): if response.ok: return elif response.status_code == 401: - raise exceptions.Forbidden( + raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." ) elif response.status_code == 429: - raise exceptions.RateLimited( + raise Exceptions.RateLimited( f"You are being Rate Limited. Retry after: {response.json()['retry_after']}", retry_after=response.json()["retry_after"], ) else: - raise exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") + raise Exceptions.HTTPException(f"Unexpected HTTP {response.status_code}") def generate_uri( self, @@ -429,19 +432,3 @@ def generate_uri( "permissions": permissions, } return f"https://discord.com/oauth2/authorize?{parse.urlencode({key: value for key, value in params.items() if value is not None})}" - - -class exceptions: - class BaseException(Exception): - pass - - class HTTPException(BaseException): - pass - - class RateLimited(HTTPException): - def __init__(self, text, retry_after): - self.retry_after = retry_after - super().__init__(text) - - class Forbidden(HTTPException): - pass From 928f185b60bfa532d6df945bdffe02abf3de8923 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 Jul 2024 15:52:59 +0200 Subject: [PATCH 4/8] :art: Run linter --- docs/source/conf.py | 32 ++++++++++++++++---------------- setup.py | 22 ++++++++++------------ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index db5e13e..b7bcf11 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,34 +2,34 @@ # -- Project information -project = 'discordoauth2.py' -copyright = '2022, TreeBen77' -author = 'TreeBen77' +project = "discordoauth2.py" +copyright = "2022, TreeBen77" +author = "TreeBen77" -release = '1.1' -version = '1.1.1' +release = "1.1" +version = "1.1.1" # -- General configuration extensions = [ - 'sphinx.ext.duration', - 'sphinx.ext.doctest', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", ] intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), } -intersphinx_disabled_domains = ['std'] +intersphinx_disabled_domains = ["std"] -templates_path = ['_templates'] +templates_path = ["_templates"] # -- Options for HTML output -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # -- Options for EPUB output -epub_show_urls = 'footnote' +epub_show_urls = "footnote" diff --git a/setup.py b/setup.py index a8cf19b..06a84a4 100644 --- a/setup.py +++ b/setup.py @@ -4,21 +4,19 @@ long_description = file.read() setup( - name='discord-oauth2.py', - description='Use Discord\'s OAuth2 effortlessly! Turns the auth code to a access token and the access token into scope infomation. ', + name="discord-oauth2.py", + description="Use Discord's OAuth2 effortlessly! Turns the auth code to a access token and the access token into scope infomation. ", version="1.2.1", long_description=long_description, long_description_content_type="text/markdown", - license='MIT', - python_requires='>=3.8', + license="MIT", + python_requires=">=3.8", author="TreeBen77", - packages=[ - 'discordoauth2' - ], - url='https://github.com/TreeBen77/discordoauth2', - keywords='flask, oauth2, discord, discord-api', + packages=["discordoauth2"], + url="https://github.com/TreeBen77/discordoauth2", + keywords="flask, oauth2, discord, discord-api", install_requires=[ - 'aiohttp', - 'requests', - ] + "aiohttp", + "requests", + ], ) From c6f3d395c1df14a717d52e5e7c459909bec6118b Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 Jul 2024 16:07:49 +0200 Subject: [PATCH 5/8] :bug: Fix 204 is considered ok --- discordoauth2/async_oauth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py index c8e110f..23af7d2 100644 --- a/discordoauth2/async_oauth.py +++ b/discordoauth2/async_oauth.py @@ -138,10 +138,10 @@ async def join_guild( "deaf": deaf, }, ) as response: - if response.ok: - return await response.json() - elif response.status == 204: + if response.status == 204: raise Exceptions.HTTPException(f"member is already in the guild.") + elif response.ok: + return await response.json() elif response.status == 401: raise Exceptions.Forbidden( f"this AccessToken does not have the nessasary scope." From 4f02625ef193f90dac8b5724fb5323795a6d1263 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 Jul 2024 16:09:05 +0200 Subject: [PATCH 6/8] :pencil2: Fix typo --- discordoauth2/async_oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py index 23af7d2..94e51e9 100644 --- a/discordoauth2/async_oauth.py +++ b/discordoauth2/async_oauth.py @@ -144,7 +144,7 @@ async def join_guild( return await response.json() elif response.status == 401: raise Exceptions.Forbidden( - f"this AccessToken does not have the nessasary scope." + f"this AccessToken does not have the necessary scope." ) elif response.status == 403: raise Exceptions.Forbidden( From 0389a334e170d3614dfb9c1980dae26287fd7ad0 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 Jul 2024 22:10:40 +0200 Subject: [PATCH 7/8] :adhesive_bandage: Fix aiohttp wants BasicAuth tuple --- discordoauth2/async_oauth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discordoauth2/async_oauth.py b/discordoauth2/async_oauth.py index 94e51e9..7a29051 100644 --- a/discordoauth2/async_oauth.py +++ b/discordoauth2/async_oauth.py @@ -377,7 +377,7 @@ async def client_credentails_grant(self, scope: list[str]) -> AsyncAccessToken: async with session.post( "https://discord.com/api/v10/oauth2/token", data={"grant_type": "client_credentials", "scope": " ".join(scope)}, - auth=(self.id, self.__secret), + auth=aiohttp.BasicAuth(str(self.id), self.__secret), ) as response: if response.ok: return AsyncAccessToken(await response.json(), self) @@ -403,7 +403,7 @@ async def revoke_token(self, token: str, token_type: str = None): async with session.post( "https://discord.com/api/oauth2/token/revoke", data={"token": token, "token_type_hint": token_type}, - auth=(self.id, self.__secret), + auth=aiohttp.BasicAuth(str(self.id), self.__secret), ) as response: if response.ok: return From 806430496b629a4916b9f4d2090a3b7977619b22 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 6 Jul 2024 22:02:53 +0200 Subject: [PATCH 8/8] :memo: Add Async section in README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7855a17..89ef100 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ I've finally published the library to PyPi! So now you can use pip. ``` pip install discord-oauth2.py ``` + ### Example With Flask Don't forget to replace all the client information with your application's own information. You can leave bot token empty if your not adding members to guilds. ```py @@ -57,3 +58,6 @@ def oauth2(): app.run("0.0.0.0", 8080) ``` + +### Async usage +Asynchronous usage is also supported, you can use the async version of the library by importing `discordoauth2.AsyncClient` instead of `discordoauth2.Client`. The methods are the same, but they’re coroutines.