├── zenora ├── api │ ├── __init__.py │ ├── oauthapi.py │ └── userapi.py ├── impl │ ├── __init__.py │ ├── oauthapi.py │ └── userapi.py ├── routes.py ├── models │ ├── oauth.py │ ├── snowflake.py │ ├── connection.py │ ├── channel.py │ ├── integration.py │ ├── user.py │ └── guild.py ├── __init__.py ├── request.py ├── utils.py ├── deserializers.py ├── client.py ├── errors.py └── exceptions.py ├── requirements.txt ├── pytest.ini ├── dev-requirements.txt ├── examples ├── oauth_with_flask │ ├── __main__.py │ ├── config.py │ ├── app.py │ └── templates │ │ └── index.html └── README.md ├── .gitignore ├── pyproject.toml ├── .flake8 ├── mypy.ini ├── LICENSE ├── noxfile.py ├── tests ├── __init__.py └── zenora │ ├── __init__.py │ ├── test_client.py │ ├── test_deserializers.py │ ├── test_request.py │ ├── test_models.py │ ├── impl │ ├── test_oauthapi.py │ └── test_userapi.py │ ├── test_utils.py │ └── test_errors.py ├── CONTRIBUTING.md ├── .gitlab-ci.yml ├── setup.py ├── CODE_OF_CONDUCT.md ├── docs └── index.md └── README.md /zenora/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zenora/impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==20.3.0 2 | requests -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | black==22.3.0 2 | flake8==3.9.0 3 | pytest==6.2.3 4 | pdoc3 5 | nox 6 | mypy 7 | -------------------------------------------------------------------------------- /examples/oauth_with_flask/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import app 2 | from .config import PORT 3 | 4 | if __name__ == "__main__": 5 | app.run(debug=True, host="0.0.0.0", port=PORT) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache/ 2 | __pycache__/ 3 | .nox/ 4 | build/ 5 | dist/ 6 | Zenora.egg-info/ 7 | testing.py 8 | .coverage 9 | venv/ 10 | .idea/ 11 | .vim/ 12 | .vscode/ 13 | html/ -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 80 3 | include = '\.pyi?$' 4 | exclude = ''' 5 | /( 6 | \.git 7 | | \.nox 8 | | venv 9 | | _build 10 | | buck-out 11 | | build 12 | | dist 13 | )/ 14 | ''' 15 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Zenora Examples 2 | 3 | This folder has/will have a few examples of Zenora being used with different frameworks and libraries. 4 | 5 | In order to run one of these examples, make sure you have zenora installed. 6 | 7 | Now you can run `python -m example_folder_name`, for example `python -m oauth_with_flask` 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W605, W291, W503 3 | 4 | exclude = 5 | .git, 6 | __pycache__, 7 | old, 8 | build, 9 | dist, 10 | .nox 11 | testing.py 12 | venv 13 | examples 14 | 15 | max-complexity = 20 16 | max-function-length = 130 17 | docstring-convention = google 18 | max-line-length = 150 19 | 20 | per-file-ignores = 21 | zenora/__init__.py: F403, F401 -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = true 3 | strict = false 4 | warn_redundant_casts = true 5 | warn_return_any = false 6 | warn_unreachable = true 7 | warn_unused_configs = true 8 | warn_unused_ignores = true 9 | check_untyped_defs = true 10 | namespace_packages = true 11 | disallow_incomplete_defs = true 12 | disallow_untyped_defs = true 13 | no_implicit_optional = true 14 | pretty = true 15 | allow_untyped_globals = false 16 | allow_redefinition = true 17 | python_version = 3.8 18 | show_column_numbers = true 19 | show_error_codes = true 20 | show_error_context = true 21 | implicit_reexport = true 22 | disallow_untyped_decorators = true 23 | disable_error_code = attr-defined, name-defined -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DevGuyAhnaf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | import os 3 | 4 | cwd = os.getcwd() 5 | 6 | 7 | @nox.session(reuse_venv=True) 8 | def tests(session): 9 | session.install("pytest") 10 | session.install("attrs") 11 | session.install("requests") 12 | session.run("pytest") 13 | 14 | 15 | @nox.session(reuse_venv=True) 16 | def mypy(session): 17 | session.install("mypy") 18 | session.install("types-attrs") 19 | session.install("types-requests") 20 | session.run("mypy", "zenora") 21 | 22 | 23 | @nox.session(reuse_venv=True) 24 | def formatting(session): 25 | session.install("black") 26 | session.run("black", "./") 27 | 28 | 29 | @nox.session(reuse_venv=True) 30 | def lint(session): 31 | session.install("flake8") 32 | session.run( 33 | "flake8", 34 | "--statistics", 35 | "--show-source", 36 | "--benchmark", 37 | "--tee", 38 | ) 39 | 40 | 41 | @nox.session(reuse_venv=True) 42 | def docs(session): 43 | session.install("attrs") 44 | session.install("requests") 45 | session.install("pdoc3") 46 | 47 | session.run("pdoc", "--html", "zenora", "--force", external=True) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/zenora/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Zenora 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | messaging, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Contributing Process 9 | 10 | We recommend that you make a fork of our GitHub repository in order to contribute. Then you can clone your fork onto your computer 11 | 12 | 1. Run `pip install -r dev-requirements.txt` to install all the development dependencies. 13 | 2. Make your changes. 14 | 3. Ensure any install/build dependencies and test files are in the `.gitignore` file or removed before doing a 15 | pull request. 16 | 4. Run `nox` to run all the pipelines 17 | 5. Update the documentation and changelog in the `docs/` folder with details of changes to the interface, this 18 | includes new environment variables, exposed ports, useful file locations and container parameters. 19 | 6. Increase the version numbers in any examples files, the README.md, setup.py and documentation to the new version that this pull request would represent. 20 | 7. Push to your fork, and make a pull request. 21 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: # List of stages for jobs, and their order of execution 2 | - type-check 3 | - tests 4 | - lint 5 | - docs 6 | 7 | type-check-job: # This job runs in the build stage, which runs first. 8 | stage: type-check 9 | image: python:3.9 10 | before_script: 11 | - pip install mypy types-attrs types-requests 12 | script: 13 | - mypy zenora 14 | 15 | run-tests: 16 | stage: tests 17 | image: python:3.9 18 | before_script: 19 | - pip install -r requirements.txt 20 | - pip install pytest 21 | script: 22 | - pytest 23 | 24 | run-linting: 25 | stage: lint 26 | image: python:3.9 27 | before_script: 28 | - pip install flake8 29 | script: 30 | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | - flake8 . --count --exit-zero --statistics 32 | 33 | build-docs: 34 | stage: docs 35 | image: python:3.9 36 | before_script: 37 | - pip install -r requirements.txt 38 | - pip install pdoc3 39 | script: 40 | - pdoc --html zenora --force 41 | artifacts: 42 | untracked: true 43 | 44 | deploy-docs: 45 | stage: docs 46 | image: node:14-buster 47 | only: 48 | - release 49 | needs: ["build-docs"] 50 | dependencies: 51 | - build-docs 52 | before_script: 53 | - npm install -g netlify-cli --unsafe-perm=true 54 | script: 55 | - netlify deploy --site $NETLIFY_SITE_ID --auth $NETLIFY_AUTH_TOKEN --dir html/zenora --prod 56 | -------------------------------------------------------------------------------- /zenora/routes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | CDN_URL = "https://cdn.discordapp.com" 22 | BASE_URL = "https://discord.com/api/v9" 23 | 24 | # CDN Endpoints 25 | USER_AVATAR = "/avatars" 26 | GUILD_ICON = "/icons" 27 | 28 | # Oauth 29 | OAUTH_TOKEN_URL = "/oauth2/token" 30 | 31 | # Users 32 | GET_CURRENT_USER = "/users/@me" 33 | GET_USER = "/users/{}" 34 | GET_USER_CONNECTIONS = GET_CURRENT_USER + "/connections" 35 | GET_USER_GUILDS = GET_CURRENT_USER + "/guilds" 36 | DM_URL = GET_CURRENT_USER + "/channels" 37 | -------------------------------------------------------------------------------- /tests/zenora/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from unittest import mock 22 | 23 | import zenora 24 | 25 | 26 | def test_APIClient(): 27 | with mock.patch.object(zenora.UserAPIImpl, "get_current_user"): 28 | client = zenora.APIClient("sdadsd") 29 | 30 | assert client._token == "Bot sdadsd" 31 | client.set_token("abcd") 32 | 33 | assert client._token == "Bot abcd" 34 | assert hasattr(client, "users") 35 | assert isinstance(client.users, zenora.UserAPI) 36 | -------------------------------------------------------------------------------- /examples/oauth_with_flask/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from urllib.parse import quote 22 | 23 | PORT = 5000 24 | BOT_TOKEN = "put your bot token here" 25 | CLIENT_SECRET = "put your bot's client secret here" 26 | CLIENT_ID = 123456789 # Enter your bot's client ID 27 | REDIRECT_URI = ( 28 | f"http://localhost:{PORT}/oauth/callback" # Your Oauth redirect URI 29 | ) 30 | OAUTH_URL = f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri={quote(REDIRECT_URI)}&response_type=code&scope=identify" 31 | -------------------------------------------------------------------------------- /zenora/models/oauth.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Final, List 22 | 23 | import attr 24 | 25 | 26 | __all__: Final[List[str]] = ["OauthResponse"] 27 | 28 | 29 | @attr.s(slots=True) 30 | class OauthResponse: 31 | """An object representing an Oauth API response from the Discord API""" 32 | 33 | access_token: str = attr.ib() 34 | """The access token returned by the API""" 35 | 36 | token_type: str = attr.ib() 37 | """Type of the token""" 38 | 39 | scope: str = attr.ib() 40 | """Oauth scopes for which the token has been provided""" 41 | 42 | expires_in: int = attr.ib(default=None) 43 | """Amount of time it expires after""" 44 | 45 | refresh_token: str = attr.ib(default=None) 46 | """A refresh token for getting another token just in case the current one expires""" 47 | -------------------------------------------------------------------------------- /tests/zenora/test_deserializers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.deserializers import deserialize_model 22 | from zenora import OwnUser 23 | 24 | import pytest 25 | 26 | 27 | def test_deserialize_model(): 28 | data = {"id": 12345, "username": "poggere", "discriminator": 1234} 29 | 30 | user = deserialize_model(OwnUser, data) 31 | 32 | assert type(user) == OwnUser 33 | assert user.id == data["id"] 34 | 35 | data = { 36 | "id": 12345, 37 | "username": "poggere", 38 | "discriminator": 1234, 39 | "blah_blah": "blah", 40 | } 41 | 42 | user = deserialize_model( 43 | OwnUser, data 44 | ) # Should not include the blah_blah in the object 45 | assert type(user) == OwnUser 46 | assert not hasattr(user, "blah_blah") 47 | 48 | with pytest.raises(AttributeError): 49 | user.blah_blah 50 | -------------------------------------------------------------------------------- /tests/zenora/test_request.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from unittest import mock 22 | 23 | import requests 24 | import zenora 25 | 26 | 27 | def test_Request(): 28 | with mock.patch.object(requests, "request") as requests_request: 29 | requests_request.return_value.status_code = 200 30 | request = zenora.Request("", "https://website.com", "GET") 31 | 32 | assert request.token == "" 33 | assert request.url == "https://website.com" 34 | assert request.method == "GET" 35 | assert request.json is None 36 | assert hasattr(request, "execute") 37 | assert callable(request.execute) 38 | 39 | request.execute() 40 | 41 | requests_request.assert_called_once() # requests.request should be called once 42 | requests_request().json.assert_called_once() # response.json() should be called once 43 | -------------------------------------------------------------------------------- /zenora/api/oauthapi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from abc import abstractmethod, ABC 22 | from zenora import OauthResponse 23 | from typing import Final, List, Optional 24 | 25 | 26 | __all__: Final[List[str]] = ["OauthAPI"] 27 | 28 | 29 | class OauthAPI(ABC): 30 | """A client for accessing Oauth features of the DIscord API""" 31 | 32 | @abstractmethod 33 | def get_access_token( 34 | self, code: str, redirect_uri: str 35 | ) -> Optional[OauthResponse]: 36 | """Returns an access token using an Oauth code 37 | 38 | Returns: 39 | Optional[OauthReponse]: An object containing Oauth data from the Discord API 40 | """ 41 | 42 | @abstractmethod 43 | def refresh_access_token( 44 | self, refresh_token: str 45 | ) -> Optional[OauthResponse]: 46 | """Refreshes the access token if it expires 47 | 48 | Returns: 49 | Optional[OauthReponse]: An object containing Oauth data from the Discord API 50 | """ 51 | -------------------------------------------------------------------------------- /zenora/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | .. include:: ../docs/index.md 23 | """ 24 | 25 | from datetime import datetime 26 | 27 | from .models.snowflake import * 28 | from .models.user import * 29 | from .models.connection import * 30 | from .models.integration import * 31 | from .models.channel import * 32 | from .models.oauth import * 33 | from .models.guild import * 34 | 35 | from .api.userapi import * 36 | from .api.oauthapi import * 37 | 38 | from .impl.userapi import * 39 | from .impl.oauthapi import * 40 | 41 | from .client import * 42 | from .request import * 43 | from .utils import * 44 | from .exceptions import * 45 | 46 | __version__ = "0.0.3-1" 47 | __author__ = "DevGuyAhnaf" 48 | __copyright__ = f"Copyright (c) {datetime.now().strftime('%Y')} DevGuyAhnaf" 49 | __email__ = "ahnaf@ahnafzamil.com" 50 | __description__ = ( 51 | "A simple to use synchronous Discord REST API wrapper for Python" 52 | ) 53 | __license__ = "MIT" 54 | __github__ = "https://github.com/ahnaf-zamil/zenora" 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from setuptools import setup 22 | 23 | import zenora 24 | 25 | with open("README.md", "r") as fh: 26 | long_description = fh.read() 27 | 28 | setup( 29 | name="Zenora", 30 | author=zenora.__author__, 31 | author_email=zenora.__email__, 32 | version=zenora.__version__, 33 | description=zenora.__description__, 34 | long_description=long_description, 35 | long_description_content_type="text/markdown", 36 | license=zenora.__license__, 37 | url=zenora.__github__, 38 | packages=["zenora", "zenora.api", "zenora.impl", "zenora.models"], 39 | classifiers=[ 40 | "License :: OSI Approved :: MIT License", 41 | "Operating System :: OS Independent", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.8", 44 | "Programming Language :: Python :: 3.9", 45 | ], 46 | python_requires=">=3.8", 47 | include_package_data=True, 48 | exclude=("__pycache__",), 49 | install_requires=["requests", "attrs"], 50 | ) 51 | -------------------------------------------------------------------------------- /examples/oauth_with_flask/app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from flask import Flask, render_template, request, redirect, session 22 | from zenora import APIClient 23 | 24 | from .config import BOT_TOKEN, CLIENT_SECRET, OAUTH_URL, REDIRECT_URI 25 | 26 | app = Flask(__name__) 27 | client = APIClient(BOT_TOKEN, client_secret=CLIENT_SECRET) 28 | 29 | app.config["SECRET_KEY"] = "mysecret" 30 | 31 | 32 | @app.route("/") 33 | def home(): 34 | access_token = session.get("access_token") 35 | 36 | if not access_token: 37 | return render_template("index.html") 38 | 39 | bearer_client = APIClient(access_token, bearer=True) 40 | current_user = bearer_client.users.get_current_user() 41 | 42 | return render_template("index.html", user=current_user) 43 | 44 | 45 | @app.route("/login") 46 | def login(): 47 | return redirect(OAUTH_URL) 48 | 49 | 50 | @app.route("/logout") 51 | def logout(): 52 | session.pop("access_token") 53 | return redirect("/") 54 | 55 | 56 | @app.route("/oauth/callback") 57 | def oauth_callback(): 58 | code = request.args["code"] 59 | access_token = client.oauth.get_access_token( 60 | code, redirect_uri=REDIRECT_URI 61 | ).access_token 62 | session["access_token"] = access_token 63 | 64 | return redirect("/") 65 | -------------------------------------------------------------------------------- /zenora/models/snowflake.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from datetime import datetime 22 | from typing import Final, List, TypeVar, Any, Union 23 | 24 | 25 | __all__: Final[List[str]] = ["Snowflake", "SnowflakeOr"] 26 | 27 | 28 | class Snowflake(int): 29 | """A unique ID for an entity on Discord e.g guild, user, role, channel, etc.""" 30 | 31 | @property 32 | def timestamp(self) -> datetime: 33 | """Milliseconds since Discord Epoch, the first second of 2015 or 1420070400000.""" 34 | return datetime.fromtimestamp( 35 | ((self >> 22) + 1420070400000) / 1000 36 | ).astimezone() 37 | 38 | @property 39 | def internal_worker_id(self) -> int: 40 | """ID of the worker which was responsible for the creation of this snowflake""" 41 | return (self & 0x3E0000) >> 17 42 | 43 | @property 44 | def internal_process_id(self) -> int: 45 | """ID of the process which generated this snowflake""" 46 | return (self & 0x1F000) >> 12 47 | 48 | @property 49 | def increment(self) -> int: 50 | """For every ID that is generated on that process, this number is incremented""" 51 | return self & 0xFFF 52 | 53 | 54 | T = TypeVar("T") 55 | 56 | SnowflakeOr = Union[Snowflake, T] 57 | 58 | 59 | def convert_snowflake(arg: Any) -> Union[Snowflake, Any]: 60 | if not isinstance(arg, (int, str)): 61 | return arg 62 | else: 63 | return Snowflake(arg) 64 | -------------------------------------------------------------------------------- /tests/zenora/test_models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import Connection, User 22 | from datetime import datetime 23 | 24 | import zenora 25 | 26 | 27 | def test_snowflake(): 28 | snowflake_int = 479287754400989217 29 | snowflake = zenora.Snowflake(snowflake_int) 30 | 31 | assert snowflake.internal_worker_id == (snowflake_int & 0x3E0000) >> 17 32 | assert snowflake.internal_process_id == (snowflake_int & 0x1F000) >> 12 33 | assert snowflake.increment == snowflake_int & 0xFFF 34 | assert ( 35 | snowflake.timestamp 36 | == datetime.fromtimestamp( 37 | ((snowflake_int >> 22) + 1420070400000) / 1000 38 | ).astimezone() 39 | ) 40 | 41 | 42 | def test_user(): 43 | user_payload = { 44 | "id": 479287754400989217, 45 | "username": "Ahnaf", 46 | "discriminator": "4346", 47 | "avatar": "abcdefg", 48 | "bio": "I am pog", 49 | } 50 | 51 | user = User(**user_payload) 52 | 53 | assert ( 54 | user.avatar_url 55 | == f"https://cdn.discordapp.com/avatars/{user_payload['id']}/{user_payload['avatar']}.png" 56 | ) 57 | assert user.is_bot is None 58 | assert user.bio == "I am pog" 59 | assert user.avatar_hash == user_payload["avatar"] 60 | 61 | 62 | def test_connection(): 63 | connection_payload = { 64 | "id": "12345", 65 | "name": "DevGuyAhnaf", 66 | "type": "YouTube", 67 | "visibility": 0, 68 | "integrations": [], 69 | } 70 | 71 | connection = Connection(**connection_payload) 72 | 73 | assert connection.id == connection_payload["id"] 74 | assert connection.name == connection_payload["name"] 75 | assert connection.type == connection_payload["type"] 76 | assert connection.visibility == connection_payload["visibility"] 77 | -------------------------------------------------------------------------------- /zenora/request.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.errors import raise_error_or_return 22 | from typing import Final, List, Any, Dict 23 | 24 | import requests 25 | 26 | 27 | __all__: Final[List[str]] = ["Request"] 28 | 29 | 30 | class Request: 31 | """Constructs a request to the Discord API""" 32 | 33 | def __init__( 34 | self, 35 | token: str, 36 | url: str, 37 | method: str, 38 | *, 39 | json_data: Any = None, 40 | headers: Any = None, 41 | form_data: Any = None, 42 | ): 43 | self.token = token 44 | self.url = url 45 | self.method = method 46 | self.json = json_data 47 | self.headers = headers 48 | self.form_data = form_data 49 | 50 | def execute(self) -> Dict[str, Any]: 51 | """Executes the API request""" 52 | headers = { 53 | "User-Agent": "{zenora.__name__} {zenora.__version__}", 54 | "Authorization": f"{self.token}", 55 | } 56 | if self.headers: 57 | headers = self.headers 58 | 59 | if self.json: 60 | r = requests.request( 61 | method=self.method, 62 | url=self.url, 63 | headers=headers, 64 | json=self.json, 65 | ) 66 | else: 67 | r = requests.request( 68 | method=self.method, 69 | url=self.url, 70 | headers=headers, 71 | data=self.form_data, 72 | ) 73 | 74 | return raise_error_or_return(r) # type: ignore[return-value] 75 | 76 | @classmethod 77 | def make_request( 78 | cls, 79 | *args: Any, 80 | **kwargs: Any, 81 | ) -> Dict[Any, Any]: 82 | req = cls(*args, **kwargs) 83 | return req.execute() 84 | -------------------------------------------------------------------------------- /zenora/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.exceptions import BadURLException 22 | from typing import Final, List, Any 23 | 24 | import re 25 | import base64 26 | import mimetypes 27 | import requests 28 | 29 | 30 | __all__: Final[List[str]] = [ 31 | "get__str__", 32 | "is_valid_url", 33 | "convert_image_to_data", 34 | ] 35 | 36 | regex = re.compile( 37 | r"^(?:http)s?://" 38 | r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" 39 | r"localhost|" 40 | r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" 41 | r"(?::\d+)?" 42 | r"(?:/?|[/?]\S+)$", 43 | re.IGNORECASE, 44 | ) 45 | 46 | 47 | def get__str__(obj: object) -> str: 48 | attributes = ", ".join( 49 | [ 50 | f'{atr}="{getattr(obj, atr)}"' 51 | for atr in dir(obj) 52 | if not atr.startswith("_") and not callable(getattr(obj, atr)) 53 | ] 54 | ) 55 | return f"{obj.__class__.__name__}({attributes})" 56 | 57 | 58 | def is_valid_url(url: str) -> bool: 59 | return re.match(regex, url) is not None 60 | 61 | 62 | def convert_image_to_data(path: str) -> str: 63 | if "?" in path: 64 | path = path.split("?")[0] 65 | mime, _ = mimetypes.guess_type(path) 66 | if mime not in ["image/jpeg", "image/png", "image/gif"]: 67 | raise BadURLException( 68 | "Invalid file type. File must be jpeg/jpg, png, or gif" 69 | ) 70 | 71 | if is_valid_url(path): 72 | data = requests.get(path, timeout=5).content 73 | encoded_body = base64.b64encode(data) 74 | return "data:%s;base64,%s" % (mime, encoded_body.decode()) 75 | else: 76 | raise BadURLException(f"Invalid URL: {path}") 77 | 78 | 79 | def extract_snowflake_from_object(obj: Any) -> str: 80 | if isinstance(obj, int): 81 | return str(obj) 82 | else: 83 | return str(int(obj.id)) 84 | -------------------------------------------------------------------------------- /zenora/deserializers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import Snowflake, User 22 | from .models.integration import Integration 23 | from typing import Final, Type, List, Dict, Any 24 | 25 | 26 | __all__: Final[List[str]] = [ 27 | "deserialize_model", 28 | "deserialize_server_integration", 29 | ] 30 | 31 | 32 | def deserialize_model( 33 | cls: Type[Any], 34 | payload: Dict[str, Any], 35 | ) -> Any: 36 | """A deserializer used for most model classes 37 | 38 | By default, attrs throws errors if it gets some properties from the API which do not 39 | exist on the model class. Example, the JSON data contains a property called 'accent color' but the class 40 | does not (maybe because Zenora wasn't updated after the API change). If that happens, attrs will throw an error. 41 | 42 | This deserializer will only map those values, which the class and the JSON data have in common (including private properties). 43 | As a result, that error will not be thrown 44 | """ 45 | try: # If there are no attribute issues by default, no need to loop over properties 46 | return cls(**payload) 47 | except TypeError: 48 | pass 49 | 50 | data = {} 51 | for x in cls.__attrs_attrs__: 52 | if x.name.startswith("_"): 53 | if (prop := x.name[1:]) in payload: 54 | data[prop] = payload[prop] 55 | if x.name in payload: 56 | data[x.name] = payload[x.name] 57 | return cls(**data) 58 | 59 | 60 | def deserialize_server_integration( 61 | payload: List[Dict[str, Any]] 62 | ) -> List[Integration]: 63 | integrations = [] 64 | 65 | for x in payload: 66 | if "user" in x: 67 | x["user"] = deserialize_model(User, x["user"]) 68 | if "role_id" in x: 69 | x["role_id"] = Snowflake(x["role_id"]) 70 | integrations.append(Integration(**x)) 71 | return integrations 72 | -------------------------------------------------------------------------------- /zenora/impl/oauthapi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import OauthAPI, OauthResponse 22 | from zenora.request import Request 23 | from zenora.routes import BASE_URL, OAUTH_TOKEN_URL 24 | from typing import Final, List, Optional, Any 25 | 26 | 27 | __all__: Final[List[str]] = ["OauthAPIImpl"] 28 | 29 | 30 | class OauthAPIImpl(OauthAPI): 31 | def __init__(self, app: Any, client_secret: str) -> None: 32 | # Using Any type on 'app' to not get circular import err 33 | self._app = app 34 | self._token = app._token 35 | self._client_secret = client_secret 36 | 37 | def get_access_token( 38 | self, code: str, redirect_uri: str 39 | ) -> Optional[OauthResponse]: 40 | url = BASE_URL + OAUTH_TOKEN_URL 41 | 42 | current_user = self._app.users.get_current_user() 43 | 44 | data = { 45 | "client_id": current_user.id, 46 | "client_secret": self._client_secret, 47 | "grant_type": "authorization_code", 48 | "redirect_uri": redirect_uri, 49 | "code": code, 50 | } 51 | 52 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 53 | 54 | payload = Request.make_request( 55 | self._token, url, "POST", form_data=data, headers=headers 56 | ) 57 | return OauthResponse(**payload) 58 | 59 | def refresh_access_token( 60 | self, refresh_token: str 61 | ) -> Optional[OauthResponse]: 62 | url = BASE_URL + OAUTH_TOKEN_URL 63 | 64 | current_user = self._app.users.get_current_user() 65 | 66 | data = { 67 | "client_id": current_user.id, 68 | "client_secret": self._client_secret, 69 | "grant_type": "refresh_token", 70 | "refresh_token": refresh_token, 71 | } 72 | 73 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 74 | 75 | payload = Request.make_request( 76 | self._token, url, "POST", form_data=data, headers=headers 77 | ) 78 | return OauthResponse(**payload) 79 | -------------------------------------------------------------------------------- /zenora/models/connection.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.deserializers import deserialize_server_integration 22 | from zenora.utils import get__str__ 23 | from .integration import Integration 24 | from typing import Final, List, Literal, Optional 25 | 26 | import attr 27 | 28 | 29 | __all__: Final[List[str]] = ["Connection"] 30 | 31 | 32 | @attr.s(slots=True) 33 | class Connection: 34 | """An object representing a user's connection on Discord e.g YouTube, Facebook, Steam, etc.""" 35 | 36 | __str__ = get__str__ 37 | 38 | id: str = attr.ib() 39 | """ID of the connection account""" 40 | 41 | name: str = attr.ib() 42 | """The username of the connection account""" 43 | 44 | type: str = attr.ib() 45 | """The service type of the connection e.g YouTube, Facebook, Steam, etc.""" 46 | 47 | visibility: Literal[0, 1] = attr.ib() 48 | """Visibility of the connection. 0 = False, 1 = True""" 49 | 50 | integrations: Optional[List[Integration]] = attr.ib( 51 | default=None, converter=deserialize_server_integration 52 | ) 53 | """Array of partial server integrations""" 54 | 55 | access_token: Optional[str] = attr.ib(default=None) 56 | """Access token for the integration""" 57 | 58 | _verified: Optional[bool] = attr.ib(default=None) 59 | 60 | _revoked: Optional[bool] = attr.ib(default=None) 61 | 62 | _friend_sync: Optional[bool] = attr.ib(default=None) 63 | 64 | _show_activity: Optional[bool] = attr.ib(default=None) 65 | 66 | @property 67 | def is_verified(self) -> Optional[bool]: 68 | """Whether the connection is verified""" 69 | return self._verified 70 | 71 | @property 72 | def is_revoked(self) -> Optional[bool]: 73 | """Whether the connection is revoked""" 74 | return self._revoked 75 | 76 | @property 77 | def has_friend_sync(self) -> Optional[bool]: 78 | """Whether friend sync is enabled for this connection""" 79 | return self._friend_sync 80 | 81 | @property 82 | def shows_activity(self) -> Optional[bool]: 83 | """Whether activities related to this connection will be shown in presence updates""" 84 | return self._show_activity 85 | -------------------------------------------------------------------------------- /zenora/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.exceptions import AuthenticationError, BadTokenError 22 | from zenora import UserAPI, OauthAPI, UserAPIImpl, OauthAPIImpl 23 | from typing import Final, List, Optional 24 | 25 | 26 | __all__: Final[List[str]] = ["APIClient"] 27 | 28 | 29 | class APIClient: 30 | """The API client for accessing the Discord REST API 31 | 32 | Args: 33 | token (str): Token for the bot/user 34 | validate_token (bool, optional): Whether the token should be validated during API instantiation. Defaults to True. 35 | client_secret (str, optional): Client secret for bot, especially if you are using Oauth. Defaults to None. 36 | bearer (bool, optional): Enable bearer tokens, necessary when you are using Oauth. Defaults to False. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | token: str, 42 | *, 43 | validate_token: bool = True, 44 | client_secret: Optional[str] = None, 45 | bearer: Optional[bool] = False, 46 | ) -> None: 47 | self._token_prefix = "Bot" if not bearer else "Bearer" 48 | self._token = f"{self._token_prefix} {token}" 49 | self._client_secret = client_secret 50 | self._user_client = UserAPIImpl(self) 51 | self._oauth_client = None 52 | 53 | if client_secret: 54 | self._oauth_client = OauthAPIImpl(self, client_secret) 55 | 56 | if validate_token: 57 | self._validate_token() 58 | 59 | def _validate_token(self) -> None: 60 | try: 61 | self.users.get_current_user() 62 | except AuthenticationError: 63 | raise BadTokenError("Invalid token has been passed") 64 | 65 | def set_token(self, token: str) -> None: 66 | """Sets the token for the API client instance""" 67 | self._token = f"{self._token_prefix} {token}" 68 | self._validate_token() 69 | 70 | @property 71 | def users(self) -> UserAPI: 72 | """Returns an instance of the UserAPI class""" 73 | return self._user_client 74 | 75 | @property 76 | def oauth(self) -> Optional[OauthAPI]: 77 | """Returns an instance of the OauthAPI class""" 78 | return self._oauth_client 79 | -------------------------------------------------------------------------------- /zenora/api/userapi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import Connection, DMChannel, Guild, SnowflakeOr, OwnUser, User 22 | from abc import ABC, abstractmethod 23 | from typing import Final, List, Optional 24 | 25 | 26 | __all__: Final[List[str]] = ["UserAPI"] 27 | 28 | 29 | class UserAPI(ABC): 30 | """A client for using all the user related API functionality""" 31 | 32 | @abstractmethod 33 | def get_current_user(self) -> OwnUser: 34 | """Returns the currently logged in user 35 | 36 | Returns: 37 | OwnUser: An object representing the current user on Discord 38 | """ 39 | 40 | @abstractmethod 41 | def get_user(self, user_id: SnowflakeOr[str]) -> User: 42 | """Returns a user with the corresponding ID 43 | 44 | Args: 45 | user_id (Union[str, Snowflake]): Snowflake ID of the user 46 | 47 | Returns: 48 | User: An object representing a user on Discord 49 | """ 50 | 51 | @abstractmethod 52 | def modify_current_user( 53 | self, 54 | username: Optional[str] = None, 55 | avatar: Optional[str] = None, 56 | ) -> OwnUser: 57 | """Modify the current user's username or avatar 58 | 59 | Args: 60 | username (str, optional): The user's username. Defaults to None. 61 | avatar (Literal[".png", ".jpg", ".jpeg", ".gif"], optional): The user's new avatar link. Defaults to None. 62 | 63 | Returns: 64 | OwnUser: An object representing the current user on Discord 65 | """ 66 | 67 | @abstractmethod 68 | def get_current_user_connections(self) -> List[Connection]: 69 | """Returns a list of connection objects for current user 70 | 71 | Returns: 72 | List[Connection]: List of connection objects 73 | """ 74 | 75 | @abstractmethod 76 | def create_dm(self, user: SnowflakeOr[User]) -> DMChannel: 77 | """Creates a DM with a user 78 | 79 | Args: 80 | user: Union[SnowflakeOr, User] 81 | Returns: 82 | DMChannel: DM channel dictionary. 83 | """ 84 | 85 | @abstractmethod 86 | def get_my_guilds(self) -> List[Guild]: 87 | """Returns you the user's joined guilds 88 | 89 | Returns: 90 | List[Guild]: List of guild objects 91 | """ 92 | -------------------------------------------------------------------------------- /zenora/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora.exceptions import ( 22 | APIError, 23 | AuthenticationError, 24 | CloudflareException, 25 | RateLimitException, 26 | ) 27 | from zenora.routes import BASE_URL 28 | from typing import Optional, Dict, Any, NoReturn 29 | 30 | import json 31 | import requests 32 | 33 | _401_msg = {"invalid_client": "Invalid Client Secret has been passed"} 34 | 35 | 36 | def _handle_401(data: dict) -> None: 37 | if data.get("message"): 38 | raise AuthenticationError(data["message"]) 39 | else: 40 | msg = _401_msg.get(data["error"]) 41 | raise AuthenticationError( 42 | msg if msg else f"Unknown 401 exception `{data['error']}`" 43 | ) 44 | 45 | 46 | def _handle_other_err(data: dict) -> None: 47 | if "error" in data: 48 | raise APIError(f"{data['error_description']}") 49 | for x in data["errors"]: 50 | err = data["errors"][x] 51 | if "_errors" in err: 52 | msg = err["_errors"][0]["message"] 53 | else: 54 | msg = data["message"] 55 | raise APIError(f"Code {data['code']}. Message: {msg}") 56 | 57 | 58 | def raise_error_or_return( 59 | r: requests.Response, 60 | ) -> Optional[Dict[str, Any]]: 61 | try: 62 | json_data = r.json() 63 | except json.decoder.JSONDecodeError: 64 | raise CloudflareException("Cloudflare blocking API request to Discord") 65 | 66 | if not r.ok: 67 | if "X-RateLimit-Bucket" in r.headers: # Rate limited 68 | return _handle_rate_limit(r) 69 | elif r.status_code == 401: # Unauthorized 70 | _handle_401(json_data) 71 | else: 72 | _handle_other_err(json_data) 73 | return None # Just so that MyPy doesn't yell at me 74 | else: 75 | return json_data 76 | 77 | 78 | def _handle_rate_limit(r: requests.Response) -> NoReturn: 79 | headers = r.headers 80 | 81 | if headers.get("X-RateLimit-Global"): 82 | raise RateLimitException( 83 | f"Being rate limited globally, will reset after {headers['X-RateLimit-Reset-After']}s.", 84 | r.headers, 85 | ) 86 | 87 | raise RateLimitException( 88 | "Being rate limited on {}, will reset after {}s. Bucket: {}".format( 89 | r.url.replace(BASE_URL, ""), 90 | headers["X-RateLimit-Reset-After"], 91 | r.headers["X-RateLimit-Bucket"], 92 | ), 93 | r.headers, 94 | ) 95 | -------------------------------------------------------------------------------- /zenora/models/channel.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | from zenora import Snowflake, User 23 | from zenora.utils import get__str__ 24 | from zenora.deserializers import deserialize_model 25 | from typing import Final, List, Optional 26 | from zenora.models.snowflake import convert_snowflake 27 | 28 | import attr 29 | import enum 30 | 31 | __all__: Final[List[str]] = ["ChannelTypes", "DMChannel"] 32 | 33 | 34 | class ChannelTypes(enum.Enum): 35 | """Enum for all Discord channel types""" 36 | 37 | GUILD_TEXT = 0 38 | """A text channel within a server""" 39 | 40 | DM = 1 41 | """A direct message between users""" 42 | 43 | GUILD_VOICE = 2 44 | """A voice channel within a server""" 45 | 46 | GROUP_DM = 3 47 | """A direct message between multiple users""" 48 | 49 | GUILD_CATEGORY = 4 50 | """An organizational category that contains up to 50 channels""" 51 | 52 | GUILD_NEWS = 5 53 | """A channel that users can follow and crosspost into their own server""" 54 | 55 | GUILD_STORE = 6 56 | """A channel in which game developers can sell their game on Discord""" 57 | 58 | GUILD_NEWS_THREAD = 10 59 | """A temporary sub-channel within a GUILD_NEWS channel""" 60 | 61 | GUILD_PUBLIC_THREAD = 11 62 | """A temporary sub-channel within a GUILD_TEXT channel""" 63 | 64 | GUILD_PRIVATE_THREAD = 12 65 | """A temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the 66 | MANAGE_THREADS permission""" 67 | 68 | GUILD_STAGE_VOICE = 13 69 | """A voice channel for hosting events with an audience""" 70 | 71 | 72 | @attr.s(slots=True) 73 | class BaseChannel: 74 | """Base class for all channel models""" 75 | 76 | __str__ = get__str__ 77 | 78 | id: Snowflake = attr.ib(converter=convert_snowflake) 79 | """A user's unique snowflake ID (in string format)""" 80 | 81 | type: ChannelTypes = attr.ib(converter=ChannelTypes) 82 | """Type of the channel""" 83 | 84 | 85 | @attr.s(slots=True) 86 | class DMChannel(BaseChannel): # type: ignore[no-untyped-def] 87 | """A model representing a DM/private channel on Discord""" 88 | 89 | last_message_id: Optional[Snowflake] = attr.ib() 90 | """Snowflake ID of the last message in the DM channel""" 91 | 92 | recipients: List[User] = attr.ib( # type: ignore[var-annotated] 93 | converter=lambda x: [deserialize_model(User, i) for i in x] 94 | ) 95 | """Recipients in the DM channel""" 96 | -------------------------------------------------------------------------------- /tests/zenora/impl/test_oauthapi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import OauthResponse 22 | from unittest import mock 23 | 24 | import pytest 25 | import requests 26 | import zenora 27 | 28 | 29 | @pytest.fixture() 30 | def api(): 31 | return zenora.APIClient( 32 | "this token is so pog", validate_token=False, client_secret="asdsad" 33 | ).oauth 34 | 35 | 36 | def test_get_access_token(api: zenora.OauthAPI): 37 | with mock.patch.object(zenora.UserAPIImpl, "get_current_user"): 38 | with mock.patch.object(requests, "request") as r: 39 | token_response = { 40 | "access_token": "asdsadsadasds", 41 | "token_type": "Bearer", 42 | "scope": "identify guilds email", 43 | "expires_in": 604800, 44 | "refresh_token": "dsadsadsadas", 45 | } 46 | 47 | r.return_value.status_code = 200 48 | r.return_value.json.return_value = token_response 49 | response = api.get_access_token( 50 | code="dsadasd", redirect_uri="dsadsadsadas" 51 | ) 52 | r.assert_called_once() 53 | 54 | assert type(response) == OauthResponse 55 | assert response.refresh_token == token_response["refresh_token"] 56 | assert response.access_token == token_response["access_token"] 57 | assert response.scope == token_response["scope"] 58 | assert response.token_type == token_response["token_type"] 59 | 60 | 61 | def test_get_refresh_token(api: zenora.OauthAPI): 62 | with mock.patch.object(zenora.UserAPIImpl, "get_current_user"): 63 | with mock.patch.object(requests, "request") as r: 64 | current_refresh_token = "abcdefg" 65 | token_response = { 66 | "access_token": "asdsadsadasds", 67 | "token_type": "Bearer", 68 | "scope": "identify guilds email", 69 | "expires_in": 604800, 70 | "refresh_token": "dsadsadsadas", 71 | } 72 | 73 | r.return_value.status_code = 200 74 | r.return_value.json.return_value = token_response 75 | response = api.refresh_access_token( 76 | refresh_token="current_refresh_token" 77 | ) 78 | r.assert_called_once() 79 | 80 | assert type(response) == OauthResponse 81 | assert response.refresh_token != current_refresh_token 82 | assert response.scope == token_response["scope"] 83 | assert response.token_type == token_response["token_type"] 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | - Using welcoming and inclusive language 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | - The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | - Trolling, insulting or derogatory comments, and personal or political attacks 34 | - Public or private harassment 35 | - Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | - Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Our Responsibilities 41 | 42 | Project maintainers are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Project maintainers have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 55 | project or its community. Examples of representing a project or community include using an official project e-mail 56 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 57 | event. Representation of a project may be further defined and clarified by project maintainers. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project owner, 62 | Ahnaf#4346 on Discord. All complaints will be reviewed and investigated and will result in a response that is deemed 63 | necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to 64 | the reporter of an incident. Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 67 | repercussions as determined by other members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), 72 | version 2.0, available [here](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 73 | -------------------------------------------------------------------------------- /zenora/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | 22 | from typing import Final, List, Any 23 | 24 | 25 | __all__: Final[List[str]] = [ 26 | "ZenoraException", 27 | "APIError", 28 | "CloudflareException", 29 | "RateLimitException", 30 | "AuthenticationError", 31 | "BadTokenError", 32 | ] 33 | 34 | 35 | class ZenoraException(Exception): 36 | """Base exception for all Zenora exceptions""" 37 | 38 | 39 | class APIError(ZenoraException): 40 | """Raised when an API error occurs""" 41 | 42 | 43 | class CloudflareException(ZenoraException): 44 | """Raised when Cloudflare blocks any Zenora API requests (possibly due to rate limits)""" 45 | 46 | 47 | class BadURLException(ZenoraException): 48 | """Raised when an invalid URL has been passed""" 49 | 50 | 51 | class AuthenticationError(ZenoraException): 52 | """Raised when the Discord API responses with 401 status code""" 53 | 54 | 55 | class BadTokenError(ZenoraException): 56 | """Raised when as invalid token is passed to the API constructor""" 57 | 58 | 59 | class RateLimitException(ZenoraException): 60 | """Raised when rate limits are hit occurs""" 61 | 62 | def __init__( 63 | self, 64 | message: str, 65 | payload: Any, 66 | ) -> None: 67 | super().__init__(message) 68 | self._payload = payload 69 | 70 | @property 71 | def ratelimit_limit(self) -> int: 72 | """The number of times a request can be made to this endpoint in a minute 73 | 74 | Returns: 75 | int: Number of times a request can be made to this endpoint in a minute 76 | """ 77 | return int(self._payload["x-ratelimit-limit"]) 78 | 79 | @property 80 | def ratelimit_remaining(self) -> int: 81 | """The number of remaining requests that can be made 82 | 83 | Returns: 84 | int: Number of requests that can be made 85 | """ 86 | return int(self._payload["x-rateLimit-remaining"]) 87 | 88 | @property 89 | def ratelimit_reset_after(self) -> float: 90 | """The total time (in seconds) of when the current rate limit bucket will reset 91 | 92 | Returns: 93 | float: Total time (in seconds) of when the current rate limit bucket will reset 94 | """ 95 | return float(self._payload["x-rateLimit-reset-after"]) 96 | 97 | @property 98 | def ratelimit_bucket(self) -> str: 99 | """A unique string denoting the rate limit being encountered 100 | 101 | Returns: 102 | str: ID of the rate limit bucket 103 | """ 104 | return self._payload["x-rateLimit-bucket"] 105 | -------------------------------------------------------------------------------- /zenora/models/integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 DevGuyAhnaf 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from zenora import Snowflake, User 22 | from typing import Final, List, Optional, Literal 23 | from zenora.models.snowflake import convert_snowflake 24 | 25 | import attr 26 | 27 | __all__: Final[List[str]] = [ 28 | "Integration", 29 | "IntegrationAccount", 30 | ] 31 | 32 | 33 | @attr.s(slots=True) 34 | class IntegrationAccount: 35 | id: str = attr.ib() 36 | name: str = attr.ib() 37 | 38 | 39 | @attr.s(slots=True) 40 | class Integration: 41 | """An object representing a server integration""" 42 | 43 | _enabled: bool = attr.ib() 44 | 45 | id: Snowflake = attr.ib(converter=convert_snowflake) 46 | """ID of the integration""" 47 | 48 | name: str = attr.ib(kw_only=True) 49 | """Name of the integration""" 50 | 51 | type: str = attr.ib() 52 | """Type of the integration""" 53 | 54 | role_id: Snowflake = attr.ib(default=None) 55 | """ID of the role that this integration uses""" 56 | 57 | enable_emoticons: Optional[bool] = attr.ib(default=None) 58 | """Whether emoticons should be synced for this integration (Twitch only currently)""" 59 | 60 | expire_behaviour: Optional[Literal[0, 1]] = attr.ib(default=None) 61 | """The behaviour of expiring subscribers""" 62 | 63 | expire_grace_period: Optional[int] = attr.ib(default=None) 64 | """The grace period (in days) before expiring subscribers""" 65 | 66 | user: Optional[User] = attr.ib(default=None) 67 | """User for this integration""" 68 | 69 | account: Optional[IntegrationAccount] = attr.ib( 70 | default=None, converter=IntegrationAccount # type: ignore[arg-type] 71 | ) 72 | """Integration account information""" 73 | 74 | synced_at: Optional[str] = attr.ib(default=None) 75 | """Last synced at (ISO8601 timestamp)""" 76 | 77 | subscriber_count: Optional[int] = attr.ib(default=None) 78 | """How many subscribers this integration has""" 79 | 80 | application: Optional[dict] = attr.ib(default=None) 81 | """Bot application for Integrations""" 82 | 83 | _syncing: Optional[bool] = attr.ib(default=None) 84 | 85 | _revoked: Optional[bool] = attr.ib(default=None) 86 | 87 | @property 88 | def is_enabled(self) -> bool: 89 | """Whether the integration is enabled""" 90 | return self._enabled 91 | 92 | @property 93 | def is_syncing(self) -> Optional[bool]: 94 | """Whether the integration is syncing""" 95 | return self._syncing 96 | 97 | @property 98 | def is_revoked(self) -> Optional[bool]: 99 | """Has this integration been revoked""" 100 | return self._revoked 101 | -------------------------------------------------------------------------------- /examples/oauth_with_flask/templates/index.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |