diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9736424 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.replit \ No newline at end of file diff --git a/README.md b/README.md index 5c1c2ed..ca72570 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,15 @@ It also makes bot auth url's
User Functions - - [ ] Get User - v2.1 - - [ ] Modify Current User - v2.1 - - [ ] Get Current Guild Members - v2.1 - - [ ] Leave Guild - v2.1 - - [ ] Create DM - v2.1 - - [ ] Create Group DM - v2.1 - - [ ] Get Current User Connections - v2.1 - - [ ] Get Current User Application Role Connection - v2.1 - - [ ] Update Current User Application Role Connection - v2.1 + - [x] Get User - v2.1 + - [x] Modify Current User - v2.1 + - [ ] Get Current Guild Member - v2.3 + - [x] Leave Guild - v2.1 + - [x] Create DM - v2.1 + - [x] Create Group DM - v2.1 + - [x] Get Current User Connections - v2.1 + - [x] Get Current User Application Role Connection - v2.1 + - [x] Update Current User Application Role Connection - v2.1 - [ ] Guild - [ ] Functions
diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 064279d..549684a 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -7,6 +7,25 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `_, and this project adheres to `Semantic Versioning `_. +[`Unreleased`_] +=============== + +Added +----- + +* Get User Function, using a user id, added to the :meth:`discord.user.fetch` method of the :class:`discord.user` class +* Modify Current User +* Get Current Guild Member +* Leave Guild +* Create DM +* Create Guild DM + +Fixed +----- + +* The documentation in the :doc:`permissions` page +* The documentation in the :doc:`migration` page + [`2.0.0`_] - November 13, 2023 ============================== @@ -244,6 +263,7 @@ Added * The AuthUrl class, that would return the auth url * Added a function to make the state for the auth url +.. _Unreleased: https://github.com/disoauth/DiscoAuth/compare/v2.0.0...add/Arcader717-User-functions .. _Diff: https://github.com/disoauth/DiscoAuth/compare/v1.4.0...v2.0.0 .. _2.0.0: https://github.com/disoauth/DiscoAuth/compare/v2.0.0-rc.1...v2.0.0 .. _2.0.0-rc.1: https://github.com/disoauth/DiscoAuth/compare/v1.4.0...v2.0.0-rc.1 diff --git a/docs/api.rst b/docs/api.rst index 4104c80..ec02125 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,7 +1,14 @@ API === +Welcome to the API! +If you are here to check out the general reference head over to :doc:`reference`. +If you want to check out the permissions reference, visit the :doc:`permissions` documentation. + .. toctree:: + :maxdepth: 0 + :hidden: + reference permissions cliAPI diff --git a/docs/conf.py b/docs/conf.py index 6629a0e..58c73b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,8 @@ project = 'DiscoAuth' copyright = '2023, Arcader717' author = 'Arcader717' -version = '2.0' -release = '2.0.0' +version = '2.1' +release = '2.1.0' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary' diff --git a/docs/index.rst b/docs/index.rst index 9a3c38a..3037915 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Welcome to DisOAuth's documentation! =================================== -**DisOAuth** (AKA Discord OAuth) is a python package to help create a oauth process for your Discord Application and/or your Discord Bot, using a *simple* api +**DiscoAuth** (AKA Discord OAuth) is a python package to help create a oauth process for your Discord Application and/or your Discord Bot, using a *simple* api To setup DisOAuth, you'll need to go to the :doc:`quickstart` page to get started, and see how to :ref:`install ` it diff --git a/docs/migration.rst b/docs/migration.rst index 2dbc979..9011f9f 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -24,20 +24,20 @@ We can use :language: python :linenos: - from DisOAuth import auth, discord, bot, permissions + from discoauth import auth, discord, bot, permissions I highly suggest using the :code:`ctrl + F` shortcut, to find the old names and replace it with the new names The following list shows what classes and methods are replaced with what name -* :code:`AuthUrl` --> :code:`auth` -* :code:`discordApi` --> :code:`discord` -* :code:`makeUrl` --> :code:`url` -* :code:`accessToken` --> :code:`token` -* :code:`User` --> :code:`user` -* :code:`get_current_user` --> :code:`fetch` -* :code:`get_user_guilds` --> :code:`guilds` -* :code:`get_guild` --> :code:`fetch` +* :code:`AuthUrl` --> :class:`auth` +* :code:`discordApi` --> :class:`discord` +* :code:`makeUrl` --> :meth:`auth.url` +* :code:`accessToken` --> :meth:`discord.token` +* :code:`User` --> :class:`user` +* :code:`get_current_user` --> :meth:`discord.user.fetch` +* :code:`get_user_guilds` --> :meth:`discord.user.guilds` +* :code:`get_guild` --> :meth:`discord.guild.fetch` Special Cases ============= diff --git a/docs/permissions.rst b/docs/permissions.rst index e5c08af..2779f2b 100644 --- a/docs/permissions.rst +++ b/docs/permissions.rst @@ -12,7 +12,7 @@ The permissions is imported using the :class:`permissions` class :linenos: :caption: perms.py - from DisOAuth import permissions + from discoauth import permissions Permissions ----------- @@ -23,7 +23,7 @@ To set multiple permission with the names when the :class:`permissions` class is :lineno-start: 3 :caption: Multiple perms, init, name - perms = permissions(["kick_members", "ban_members"] + perms = permissions(["kick_members", "ban_members"]) .. code-block:: python :lineno-start: 3 @@ -54,7 +54,7 @@ To set multiple permission with the names when the :class:`permissions` class is +-------------------------------------+--------+ | stream | 9 | +-------------------------------------+--------+ -| view_channe; | 10 | +| view_channel | 10 | +-------------------------------------+--------+ | send_messages | 11 | +-------------------------------------+--------+ diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d54f9c1..0a97bb5 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -58,10 +58,9 @@ Now, we need a new variable, ``client_secret``. Which is your app's client secre using the other three variables and ``client_secret`` we can use :class:`discord`. :: api = discord(client_id, client_secret, scope, redirect_uri) - r = await api.token(code) - token = r['access_token'] + token = await api.token(code) -Now, ``token`` is your access token. Save this somewhere safe. +Now, ``token`` is the response, which can contain a new token. Save this somewhere safe. Remember, :func:`token` returns a dictionary, containing the entire response, so you can still get the ``refresh_token`` diff --git a/docs/reference.rst b/docs/reference.rst index 3473218..add42aa 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -82,6 +82,65 @@ API Reference .. versionchanged:: 2.0 Renamed to :meth:`discord.user.guilds` from the previous :meth:`discordApi.User.get_user_guilds` + .. py:method:: modify(username) + :async: + + Changes the username or avatar url of the user + + :param username: The username of the user that you would like to modify, this is NOT the user's display name. Defaults to none, and uses the access token as the user + :type username: str | None + :return: The json returned by Discord API + + .. versionadded:: 2.1 + + .. py:method:: leaveGuild(guild) + :async: + + Makes a user leave a guild + + :param guild: The id of the guild + :type guild: str | int + :return: Whether the user had left. Returns true if successful, and false if not + :rtype: bool + + .. versionadded:: 2.1 + + .. py:method:: dm(id, tokens, nicks) + :async: + + DM a user or group of users + + :param id: The id of the user you want to DM. This does not apply to group DM's + :param tokens: The access tokens of the users you would like to DM. This does not apply to DMing a single user + :param nicks: The nicks of the users. This does not apply to DMing a single user + :type id: str | None = None + :type tokens: List[str] | None = None + :type nicks: Dict[str, str] | None = None + :return: The json response returned by Discord API + + .. versionadded:: 2.1 + + .. py:method:: connections(id) + :async: + + Can get a user's regular and application connections. Must use the ``connections`` scope for regular connections, and ``role_connections.write`` for application connections + + :param id: The id of the application connected to a user + :type: str | None = None + :return: The json response returned by Discord API + + .. versionadded:: 2.1 + + .. py:method:: modifyConnections(id) + :async: + + Modifies a user's application connections + + :param id: The id of the application + :return: The json response returned by Discord API + + .. versionadded:: 2.1 + .. py:class:: guild(token) Provides access to the guild links in the Discord API diff --git a/pyproject.toml b/pyproject.toml index 011a376..157b5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "discoauth" -version = "2.1.0" +version = "2.1.0-alpha" description = "Discord OAuth, and Discord API, in one package" readme = "README.md" diff --git a/src/discoauth/exceptions.py b/src/discoauth/exceptions.py index 59cb475..07bc31a 100644 --- a/src/discoauth/exceptions.py +++ b/src/discoauth/exceptions.py @@ -10,3 +10,13 @@ def __init__(self, super().__init__("The permission number you passed is invalid") elif permissionInt is not None: super().__init__(f"The permission number you passed, {permissionInt}, is invalid") + +class DiscordException(Exception): + """Raised when something unusual happens with the Discord API. Raise with the error message + + Attributes: + message -- The error message returned by discord api + """ + def __init__(self, message: str): + super().__init__(f"The following error was raised by Discord API\n\n{message}") + diff --git a/src/discoauth/models.py b/src/discoauth/models.py index fdde3b9..68563fa 100644 --- a/src/discoauth/models.py +++ b/src/discoauth/models.py @@ -1,3 +1,5 @@ +from typing import Any, Dict + class UserObj: def __init__(self, response) -> None: @@ -34,150 +36,169 @@ def __init__(self, self.public_flags = r['public_flags'] if 'avatar_decoration' in r: self.avatar_decoration = r['avatar_decoration'] + else: + self.avatar_decoration = None def __getitem__ (self, key): return getattr(self, key) + def __dict__(self) -> Dict[str, Any]: + return { + 'id': self.id, + 'username': self.username, + 'discriminator': self.discriminator, + 'global_name': self.global_name, + 'avatar': self.avatar, + 'bot': self.bot, + 'system': self.system, + 'mfa_enabled': self.mfa_enabled, + 'banner': self.banner, + 'accent_color': self.accent_color, + 'locale': self.locale, + 'verified': self.verified, + 'email': self.email, + 'flags': self.flags, + 'premium_type': self.premium_type, + 'public_flags': self.public_flags, + 'avatar_decoration': self.avatar_decoration + } + class GuildObj: def __init__(self, - response) -> None: - r = response - self.id = r['id'] - self.name = r['name'] - if "icon" in r: - self.icon = r['icon'] + response: dict) -> None: + self.keys = [ + 'id', 'name', 'icon', + 'icon_url', 'splash', 'discovery_splash', + 'owner', 'owner_id', 'permissions', + 'perms', 'region', 'afk_channel_id', + 'afk_timeout', 'widget_enabled', 'widget_channel_id', + 'verification_level', 'default_message_notifications', 'explicit_content_filter', + 'roles', 'emojis', 'features', + 'mfa_level', 'application_id', 'system_channel_id', + 'system_channel_flags', 'rules_channel_id', 'max_presences', + 'max_members', 'vanity_url_code', 'description', + 'banner', 'premium_tier', 'premium_subscription_count', + 'preferred_locale', 'public_updates_channel_id', 'max_video_channel_users', + 'max_stage_video_channel_users', 'approximate_member_count', 'approximate_presence_count', + 'welcome_screen', 'nsfw_level', 'stickers', + 'premium_progress_bar_enabled', 'safety_alert_channel_id' + ] + self.id = response.get('id', None) + self.name = response.get('name', None) + if "icon" in response: + self.icon = response['icon'] self.icon_url = f"https://cdn.discordapp.com/icons/{self.id}/{self.icon}.png" else: self.icon = None self.icon_url = None - if "splash" in r: - self.splash = r['splash'] - else: - self.splash = None - if "discovery_splash" in r: - self.discovery_splash = r['discovery_splash'] - if "owner" in r: - self.owner = r['owner'] - else: - self.owner = None - self.onwer_id = r['owner_id'] - if "permissions" in r: - self.permissions = r['permissions'] - self.perms = r['permissions'] - else: - self.permisssion = None - self.perms = None - if "region" in r: - self.region = r['region'] - else: - self.region = None - if "afk_channel_id" in r: - self.afk_channel_id = r['afk_channel_id'] - else: - self.afk_channel_id = None - if "afk_timeout" in r: - self.afk_timeout = r['afk_timeout'] - else: - self.afk_timeout = None - if "widget_enabled" in r: + self.splash = response.get('splash', None) + self.discovery_splash = response.get('discovery_splash', None) + self.owner = response.get('owner', None) + self.onwer_id = response['owner_id'] + self.permissions = response.get('permissions', None) + self.perms = response.get('permissions', None) + self.region = response.get('region', None) + self.afk_channel_id = response.get('afk_channel_id', None) + self.afk_timeout = response.get('afk_timeout', None) + if "widget_enabled" in response: self.widget_enabled = True else: self.widget_enabled = False - if "widget_channel_id" in r: - self.widget_channel_id = r['widget_channel_id'] + if "widget_channel_id" in response: + self.widget_channel_id = response['widget_channel_id'] else: self.widget_channel_id = None - self.verification_level = r['verification_level'] - self.default_message_notifications = r['default_message_notifications'] - self.explicit_content_filter = r['explicit_content_filter'] - self.roles = r['roles'] - self.emojis = r['emojis'] - self.features = r['features'] - self.mfa_level = r['mfa_level'] - if "application_id" in r: - self.application_id = r['application_id'] + self.verification_level = response['verification_level'] + self.default_message_notifications = response['default_message_notifications'] + self.explicit_content_filter = response['explicit_content_filter'] + self.roles = response['roles'] + self.emojis = response['emojis'] + self.features = response['features'] + self.mfa_level = response['mfa_level'] + if "application_id" in response: + self.application_id = response['application_id'] else: self.application_id = None - if "system_channel_id" in r: - self.system_channel_id = r['system_channel_id'] + if "system_channel_id" in response: + self.system_channel_id = response['system_channel_id'] else: self.system_channel_id = None - if "system_channel_flags" in r: - self.system_channel_flags = r['system_channel_flags'] + if "system_channel_flags" in response: + self.system_channel_flags = response['system_channel_flags'] else: self.system_channel_flags = None - if "rules_channel_id" in r: - self.rules_channel_id = r['rules_channel_id'] + if "rules_channel_id" in response: + self.rules_channel_id = response['rules_channel_id'] else: self.rules_channel_id = None - if "max_presences" in r: - self.max_presences = r['max_presences'] + if "max_presences" in response: + self.max_presences = response['max_presences'] else: self.max_presences = None - if "max_members" in r: - self.max_members = r['max_members'] + if "max_members" in response: + self.max_members = response['max_members'] else: self.max_members = None - if "vanity_url_code" in r: - self.vanity_url_code = r['vanity_url_code'] + if "vanity_url_code" in response: + self.vanity_url_code = response['vanity_url_code'] else: self.vanity_url_code = None - if "description" in r: - self.description = r['description'] + if "description" in response: + self.description = response['description'] else: self.description = None - if "banner" in r: - self.banner = r['banner'] + if "banner" in response: + self.banner = response['banner'] else: self.banner = None - self.premium_tier = r['premium_tier'] - if "premium_subscription_count" in r: - self.premium_subscription_count = r['premium_subscription_count'] + self.premium_tier = response['premium_tier'] + if "premium_subscription_count" in response: + self.premium_subscription_count = response['premium_subscription_count'] else: self.premium_subscription_count = None - if "preferred_locale" in r: - self.preferred_locale = r['preferred_locale'] + if "preferred_locale" in response: + self.preferred_locale = response['preferred_locale'] else: self.preferred_locale = None - if "public_updates_channel_id" in r: - self.public_updates_channel_id = r['public_updates_channel_id'] + if "public_updates_channel_id" in response: + self.public_updates_channel_id = response['public_updates_channel_id'] else: self.public_updates_channel_id = None - if "max_video_channel_users" in r: - self.max_video_channel_users = r['max_video_channel_users'] + if "max_video_channel_users" in response: + self.max_video_channel_users = response['max_video_channel_users'] else: self.max_video_channel_users = None - if "max_stage_video_channel_users" in r: - self.max_stage_video_channel_users = r['max_stage_video_channel_users'] + if "max_stage_video_channel_users" in response: + self.max_stage_video_channel_users = response['max_stage_video_channel_users'] else: self.max_stage_video_channel_users = None - if "approximate_member_count" in r: - self.approximate_member_count = r['approximate_member_count'] + if "approximate_member_count" in response: + self.approximate_member_count = response['approximate_member_count'] else: self.approximate_member_count = None - if "approximate_presence_count" in r: - self.approximate_presence_count = r['approximate_presence_count'] + if "approximate_presence_count" in response: + self.approximate_presence_count = response['approximate_presence_count'] else: self.approximate_presence_count = None - if "welcome_screen" in r: - self.welcome_screen = r['welcome_screen'] + if "welcome_screen" in response: + self.welcome_screen = response['welcome_screen'] else: self.welcome_screen = None - if "nsfw_level" in r: - self.nsfw_level = r['nsfw_level'] + if "nsfw_level" in response: + self.nsfw_level = response['nsfw_level'] else: self.nsfw_level = None - if "stickers" in r: - self.stickers = r['stickers'] + if "stickers" in response: + self.stickers = response['stickers'] else: self.stickers = None - if "premium_progress_bar_enabled" in r: - self.premium_progress_bar_enabled = r['premium_progress_bar_enabled'] + if "premium_progress_bar_enabled" in response: + self.premium_progress_bar_enabled = response['premium_progress_bar_enabled'] else: self.premium_progress_bar_enabled = False - if "safety_alerts_channel_id" in r: - self.safety_alerts_channel_id = r['safety_alerts_channel_id'] + if "safety_alerts_channel_id" in response: + self.safety_alerts_channel_id = response['safety_alerts_channel_id'] else: self.safety_alerts_channel_id = None diff --git a/src/discoauth/url.py b/src/discoauth/url.py index 5a28aeb..a7f79d9 100644 --- a/src/discoauth/url.py +++ b/src/discoauth/url.py @@ -6,6 +6,8 @@ from discoauth.models import UserObj as uObj, GuildObj as gObj +from discoauth.exceptions import DiscordException + apiUrl = "https://discord.com/api" @@ -106,21 +108,35 @@ async def token(self, code) -> dict[str, str]: class user: def __init__(self, token): - self.token = token + if isinstance(token, dict): + try: + self.token = token['access_token'] + except KeyError: + raise DiscordException(token['error']) from KeyError + elif isinstance(token, str): + self.token = token - async def fetch(self) -> uObj: - url = apiUrl + "/users/@me" + async def fetch(self, + id: int | None = None, + *extras) -> uObj: + if isinstance(id, int): + url = apiUrl + f"/users/{id}" + else: + url = apiUrl + "/users/@me" headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer ' + self.token } r = requests.get(url, headers=headers) + if 'raw' in extras: + return r.json() return uObj(r.json()) async def guilds(self, - with_count: bool | None = False) -> List[gObj]: + with_count: bool | None = False, + *extras) -> List[gObj]: url = apiUrl + "/users/@me/guilds" headers = { 'Content-Type': 'application/x-www-form-urlencoded', @@ -133,13 +149,86 @@ async def guilds(self, query['with_counts'] = False r = requests.get(url, headers=headers) guildList = [] + if 'raw' in extras: + return r.json() for guild in r.json(): guildList.append(gObj(guild)) return guildList + async def modify(self, username: str | None = None): + url = apiUrl + "/users/@me" + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Bearer ' + self.token + } + query = {} + if isinstance(username, str): + query['username'] = username + r = requests.patch(url, headers, json=query) + else: + r = requests.patch(url, headers) + return r.json() + + async def leaveGuild(self, guild: str | int): + url = apiUrl + f"/users/@me/guilds/{str(id)}" + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Bearer ' + self.token + } + r = requests.delete(url, headers) + if r.status_code == '204': + return True + else: + return False + + async def dm(self, id: str | None = None, + tokens: List[str] | None = None, + nicks: Dict[str, str] | None = None): + url = apiUrl + "/users/@me/channels" + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + self.token + } + if isinstance(id, str): + query = {'recipient_id': id} + r = requests.post(url, headers, json=query) + if isinstance(access_tokens, list) or isinstance(nicks, dict): + raise ValueError("You can't try to make a group DM and a regular dm at the same time") + elif isinstance(access_tokens, list) and isinstance(nicks, dict): + query = {'access_tokens': tokens, + 'nicks': nicks} + r = requests.post(url, headers, json=query) + return r.json() + + async def connections(self, id: str | None = None): + # Requires connections scope, for basic connections, and role_connections.write for application connections + if isinstance(id, None): + url = baseUrl + "/users/@me/connections" + elif isinstance(id, str): + url = baseUrl + f"/users/@me/applications/{id}/role-connection" + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + self.token + } + r = requests.get(url, headers) + return r.json() + + + async def modifyConnections(self, id: str): + url = baseUrl + f"/users/@me/applications/{id}/role-connection" + headers = { + 'Content-Type': 'applcation/json', + 'Authorization': 'Bearer ' + self.token + } + r = requests.put(url, headers) + return r.json() + class guild: def __init__(self, token): - self.token = token + if isinstance(token, dict): + self.token = token['access_token'] + elif isinstance(token, str): + self.token = token async def fetch(self, id: int, diff --git a/tests/api_test.py b/tests/api_test.py index 85fb625..963820e 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -3,5 +3,12 @@ # if it doesn't make sense, check the workflow for testing. The workflow adds variables, to keep the environment variables secret async def test_make_api(): - api = discord(clid, clsec, ["identify"], "https://tests.com") - token = api.token(code) + api = discord(clid, clsec, ["identify"], "https://www.example.com") + token = await api.token(code) + +async def test_get_user(): + api = discord(clid, clsec, ["identify"], "https://www.example.com") + token = await api.token(code) + user1 = await api.user(token).fetch(clid) + user2 = await api.user(token).fetch() +