diff --git a/.gitignore b/.gitignore index 2748024..b2629ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ main.py -rewrite.py \ No newline at end of file +rewrite.py +discordoauth2/__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index b0e9d78..553e0cb 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,38 @@ 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 on line 20 and 21 with your application's own information. You can leave bot token empty if your not adding members to guilds. +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 import discordoauth2 from flask import Flask, request client = discordoauth2.Client(849930878276993044, secret="very-secret-code", -redirect="https://findingfakeurlsisprettyhard.tv/oauth2", bot_token="bot-token-only-required-for-guild-joining") +redirect="https://findingfakeurlsisprettyhard.tv/oauth2", bot_token="bot-token-only-required-for-guild-joining-or-updating-linked-roles-metadata") app = Flask(__name__) +client.update_linked_roles_metadata([ + { + "type": 2, + "key": "level", + "name": "Level", + "description": "The level the user is on" + }, + { + "type": 7, + "key": "supporter", + "name": "Supporter", + "description": "Spent money to help the game" + } +]) + @app.route("/oauth2") def oauth2(): code = request.args.get("code") access = client.exchange_code(code) + access.update_metadata("Platform Name", "Username", level=69, supporter=True) + identify = access.fetch_identify() connections = access.fetch_connections() guilds = access.fetch_guilds() diff --git a/discordoauth2/__init__.py b/discordoauth2/__init__.py index 5405de9..afc350f 100644 --- a/discordoauth2/__init__.py +++ b/discordoauth2/__init__.py @@ -1,9 +1,13 @@ import requests +from datetime import datetime class PartialAccessToken(): def __init__(self, access_token, client) -> None: - self.client: Client = client + self.client = client self.token = access_token + + def revoke(self): + return self.client.revoke_token(self.token, token_type="access_token") def fetch_identify(self): response = requests.get("https://discord.com/api/v10/users/@me", headers={ @@ -74,9 +78,31 @@ def join_guild(self, guild_id, user_id, nick = None, role_ids = None, mute = Fal 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=None, username=None, **metadata): + def metadataTypeHook(item): + print(item, type(item), type(type(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}") class AccessToken(PartialAccessToken): - def __init__(self, data: dict, client) -> None: + def __init__(self, data, client) -> None: super().__init__(data["access_token"], client) self.expires = data.get("expires_in") @@ -84,6 +110,9 @@ def __init__(self, data: dict, client) -> None: self.refresh_token = data.get("refresh_token") self.webhook = data.get("webhook") self.guild = data.get("guild") + + def revoke_refresh_token(self): + return self.client.revoke_token(self.refresh_token, token_type="refresh_token") class Client(): def __init__(self, id, secret, redirect, bot_token=None): @@ -91,6 +120,10 @@ def __init__(self, id, secret, redirect, bot_token=None): self.redirect_url = redirect self.__secret = secret self.__bot_token = bot_token + + def update_linked_roles_metadata(self, metadata): + 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): return PartialAccessToken(access_token, self) @@ -124,13 +157,24 @@ def client_credentails_grant(self, scope): 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, token_type=None): + 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}") class exceptions(): class BaseException(Exception): diff --git a/example.py b/example.py index 3de3b35..ad26e67 100644 --- a/example.py +++ b/example.py @@ -1,12 +1,33 @@ from flask import Flask, redirect, request -from discordoauth2 import discordOauth2 +import discordoauth2 import os app = Flask('Discord OAuth2 Example') -client = discordOauth2(client=your-client-id, secret="your-client-secret", +client = discordoauth2.Client(client=your-client-id, secret="your-client-secret", redirect="your-redirect-url", token="your-bot-token (optional)") # Replace your-client-id with your application's client id, replace your-client-secret with your client secret and replace your-redirect-url with the url that discord will redirect users to once they complete OAuth2. -# If you want to add users to a guild, insert a bot token with CREATE_INSTANT_INVITE permissions in the guilds you want to add users to. +# If you want to updated linked roles metadata or add users to a guild, insert a bot token with CREATE_INSTANT_INVITE permissions in the guilds you want to add users to. + +client.update_linked_roles_metadata([ + { + "type": 2, + "key": "level", + "name": "Level", + "description": "The level the user is on" + }, + { + "type": 2, + "key": "wins", + "name": "Wins", + "description": "The number of times the user has won" + }, + { + "type": 7, + "key": "supporter", + "name": "Supporter", + "description": "Spent money to help the game" + } +]) @app.route('/') def main(): @@ -15,19 +36,23 @@ def main(): @app.route('/oauth2') def oauth(): - tokenObject = client.exchange_code(token=request.args.get('code')) - print("refresh token: "+tokenObject.refresh_token) + access_token = client.exchange_code(token=request.args.get('code')) + print("refresh token: "+access_token.refresh) # returns basic data about the user, including username, avatar and badges, if the email scope was parsed, it will also return their email. - identify = tokenObject.access.identify() + identify = access_token.fetch_identify() # returns visible and hidden connections such as GitHub, YouTube or Twitter. - connections = tokenObject.access.connections() + connections = access_token.fetch_connections() # returns a list of guilds that the user is in - guilds = tokenObject.access.guilds() + guilds = access_token.fetch_guilds() # returns a member object for the provided guild - guilds_member = tokenObject.access.guilds_member(guilds[0]["id"]) + guilds_member = access_token.fetch_guild_member(guilds[0]["id"]) # makes a user join a guild, bot token provided must have CREATE_INSTANT_INVITE in that guild - tokenObject.access.guilds_join(guild-id-here) + access_token.join_guild(guild-id-here, identify["id"]) + # this update's the user's metadata for linked roles. + access_token.update_metadata("Platform Name", "Username", level=69, wins=420, supporter=True) + # when you're done with the token, you can revoke it: note this revokes both the access token and refresh token. + access_token.revoke() return f"{identify}

{connections}

{guilds}

guild data for the first guild: {guilds_member}" diff --git a/setup.py b/setup.py index abbe7ca..43fb5fc 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ 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. ', - version="1.0.1", + version="1.1.0", long_description=long_description, long_description_content_type="text/markdown", license='MIT',