aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Amrou Bellalouna <[email protected]>2022-12-29 00:58:15 +0100
committerGravatar GitHub <[email protected]>2022-12-29 00:58:15 +0100
commitab1b5460f4f089283ccd43f9bb19ddcc4e600a3a (patch)
tree609ebd37878fb5ba5532048d40236fa3c4a53736
parentremove useless blank constraint (diff)
parentUpdate help channels guide (#814) (diff)
Merge branch 'main' into 2304-link-previous-nomination-threads
-rwxr-xr-xmanage.py2
-rw-r--r--poetry.lock284
-rw-r--r--pydis_site/apps/api/models/bot/metricity.py28
-rw-r--r--pydis_site/apps/api/tests/test_users.py84
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py57
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md (renamed from pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/commit-messages.md)0
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md79
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md4
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/app-commands.md418
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md323
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md23
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md29
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md70
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md48
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/vps-services.md41
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/vps_services.md58
-rw-r--r--pydis_site/static/images/content/fix-ssl-certificate/pem.pngbin0 -> 11619 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/available_channels.pngbin6556 -> 0 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/available_message.pngbin89386 -> 0 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/claimed_channel.pngbin26100 -> 0 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/dormant_channels.pngbin22386 -> 0 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/help-system-category.pngbin0 -> 7425 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/new-post-button.pngbin0 -> 15804 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/new-post-form.pngbin0 -> 35364 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/newly-created-thread-example.pngbin0 -> 149793 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/occupied_channels.pngbin10950 -> 0 bytes
-rw-r--r--pydis_site/static/images/content/help_channels/question-example.pngbin0 -> 45436 bytes
-rw-r--r--pydis_site/static/images/content/regenerating_token.jpgbin0 -> 180570 bytes
-rw-r--r--pyproject.toml22
32 files changed, 1312 insertions, 264 deletions
diff --git a/manage.py b/manage.py
index 37fb141f..afca6121 100755
--- a/manage.py
+++ b/manage.py
@@ -195,7 +195,7 @@ def main() -> None:
# Pass any others directly to standard management commands
else:
- _static_build = "distill" in sys.argv[1]
+ _static_build = len(sys.argv) > 1 and "distill" in sys.argv[1]
if _static_build:
# Build a static version of the site with no databases and API support
diff --git a/poetry.lock b/poetry.lock
index 5f068515..3486e75d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -38,7 +38,7 @@ python-versions = ">=3.5"
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "bandit"
@@ -61,7 +61,7 @@ yaml = ["PyYAML"]
[[package]]
name = "certifi"
-version = "2022.9.24"
+version = "2022.12.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -95,7 +95,7 @@ optional = false
python-versions = ">=3.6.0"
[package.extras]
-unicode_backport = ["unicodedata2"]
+unicode-backport = ["unicodedata2"]
[[package]]
name = "colorama"
@@ -107,7 +107,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7
[[package]]
name = "coverage"
-version = "6.5.0"
+version = "7.0.1"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
@@ -118,7 +118,7 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
-version = "38.0.1"
+version = "38.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@@ -144,8 +144,8 @@ optional = false
python-versions = "*"
[[package]]
-name = "Django"
-version = "4.1.2"
+name = "django"
+version = "4.1.4"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
@@ -162,7 +162,7 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-distill"
-version = "3.0.1"
+version = "3.0.2"
description = "Static site renderer and publisher for Django."
category = "main"
optional = false
@@ -248,16 +248,16 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pyt
[[package]]
name = "flake8"
-version = "5.0.4"
+version = "6.0.0"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.8.1"
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
-pycodestyle = ">=2.9.0,<2.10.0"
-pyflakes = ">=2.5.0,<2.6.0"
+pycodestyle = ">=2.10.0,<2.11.0"
+pyflakes = ">=3.0.0,<3.1.0"
[[package]]
name = "flake8-annotations"
@@ -285,7 +285,7 @@ flake8 = ">=5.0.0"
[[package]]
name = "flake8-bugbear"
-version = "22.10.27"
+version = "22.12.6"
description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
category = "dev"
optional = false
@@ -312,7 +312,7 @@ pydocstyle = ">=2.1"
[[package]]
name = "flake8-import-order"
-version = "0.18.1"
+version = "0.18.2"
description = "Flake8 and pylama plugin that checks the ordering of import statements."
category = "dev"
optional = false
@@ -422,7 +422,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
-version = "0.23.0"
+version = "0.23.1"
description = "The next generation HTTP client."
category = "main"
optional = false
@@ -430,7 +430,7 @@ python-versions = ">=3.7"
[package.dependencies]
certifi = "*"
-httpcore = ">=0.15.0,<0.16.0"
+httpcore = ">=0.15.0,<0.17.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*"
@@ -518,14 +518,14 @@ python-versions = ">=2.6"
[[package]]
name = "pep8-naming"
-version = "0.13.2"
+version = "0.13.3"
description = "Check PEP-8 naming conventions, plugin for flake8"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
-flake8 = ">=3.9.1"
+flake8 = ">=5.0.0"
[[package]]
name = "platformdirs"
@@ -541,7 +541,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock
[[package]]
name = "pre-commit"
-version = "2.20.0"
+version = "2.21.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@@ -552,8 +552,7 @@ cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
-toml = "*"
-virtualenv = ">=20.0.8"
+virtualenv = ">=20.10.0"
[[package]]
name = "prometheus-client"
@@ -587,7 +586,7 @@ python-versions = ">=3.6"
[[package]]
name = "pycodestyle"
-version = "2.9.1"
+version = "2.10.0"
description = "Python style guide checker"
category = "dev"
optional = false
@@ -625,7 +624,7 @@ python-versions = ">=3.7"
[[package]]
name = "pyflakes"
-version = "2.5.0"
+version = "3.0.1"
description = "passive checker of Python programs"
category = "dev"
optional = false
@@ -650,7 +649,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pymdown-extensions"
-version = "9.7"
+version = "9.9"
description = "Extension pack for Python Markdown."
category = "main"
optional = false
@@ -717,7 +716,7 @@ urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rfc3986"
@@ -735,7 +734,7 @@ idna2008 = ["idna"]
[[package]]
name = "sentry-sdk"
-version = "1.10.1"
+version = "1.12.1"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
@@ -756,7 +755,9 @@ falcon = ["falcon (>=1.4)"]
fastapi = ["fastapi (>=0.79.0)"]
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
httpx = ["httpx (>=0.16.0)"]
-pure_eval = ["asttokens", "executing", "pure-eval"]
+opentelemetry = ["opentelemetry-distro (>=0.350b0)"]
+pure-eval = ["asttokens", "executing", "pure-eval"]
+pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
rq = ["rq (>=0.6)"]
@@ -767,7 +768,7 @@ tornado = ["tornado (>=5)"]
[[package]]
name = "setuptools"
-version = "65.5.0"
+version = "65.5.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
@@ -775,7 +776,7 @@ python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
@@ -844,14 +845,6 @@ psutil = ">=5.7.2,<6.0.0"
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
[[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
@@ -911,7 +904,7 @@ brotli = ["Brotli"]
[metadata]
lock-version = "1.1"
python-versions = "3.10.*"
-content-hash = "2b62b82ad01ceeafd68405adffdc364d8594f25697b4f51c96da357b0e4762dc"
+content-hash = "00b17753b3593eaf5c8217ae2bd194b1389df0d97b49fcfaac93a592e5fc3c9f"
[metadata.files]
anyio = [
@@ -931,8 +924,8 @@ bandit = [
{file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"},
]
certifi = [
- {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
- {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
+ {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -1013,95 +1006,96 @@ colorama = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
coverage = [
- {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
- {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
- {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
- {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
- {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
- {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
- {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
- {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
- {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
- {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
- {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
- {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
- {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
- {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
- {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
- {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
- {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
- {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
- {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
- {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
- {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
- {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
- {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
- {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
- {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
- {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
- {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
- {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
- {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
- {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
+ {file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"},
+ {file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"},
+ {file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"},
+ {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"},
+ {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"},
+ {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"},
+ {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"},
+ {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"},
+ {file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"},
+ {file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"},
+ {file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"},
+ {file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"},
+ {file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"},
+ {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"},
+ {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"},
+ {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"},
+ {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"},
+ {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"},
+ {file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"},
+ {file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"},
+ {file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"},
+ {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"},
+ {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"},
+ {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"},
+ {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"},
+ {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"},
+ {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"},
+ {file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"},
+ {file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"},
+ {file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"},
+ {file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"},
+ {file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"},
+ {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"},
+ {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"},
+ {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"},
+ {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"},
+ {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"},
+ {file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"},
+ {file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"},
+ {file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"},
+ {file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"},
+ {file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"},
+ {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"},
+ {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"},
+ {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"},
+ {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"},
+ {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"},
+ {file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"},
+ {file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"},
+ {file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"},
+ {file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
]
cryptography = [
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"},
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"},
- {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"},
- {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"},
- {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"},
+ {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"},
+ {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"},
+ {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"},
+ {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"},
+ {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"},
+ {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"},
+ {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"},
]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
-Django = [
- {file = "Django-4.1.2-py3-none-any.whl", hash = "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793"},
- {file = "Django-4.1.2.tar.gz", hash = "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f"},
+django = [
+ {file = "Django-4.1.4-py3-none-any.whl", hash = "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148"},
+ {file = "Django-4.1.4.tar.gz", hash = "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b"},
]
django-distill = [
- {file = "django-distill-3.0.1.tar.gz", hash = "sha256:8bbac5e45d2afc61cc718d587c6026267c985305f5e599465f2ebc4b0cba9ebf"},
+ {file = "django-distill-3.0.2.tar.gz", hash = "sha256:01df70e595c6ba4fee4baa5511b0bdea42388bfec409aeb70000315f109a993f"},
]
django-environ = [
{file = "django-environ-0.9.0.tar.gz", hash = "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21"},
@@ -1128,8 +1122,8 @@ filelock = [
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
flake8 = [
- {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
- {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
+ {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
+ {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
]
flake8-annotations = [
{file = "flake8-annotations-2.9.1.tar.gz", hash = "sha256:11f09efb99ae63c8f9d6b492b75fe147fbc323179fddfe00b2e56eefeca42f57"},
@@ -1140,16 +1134,16 @@ flake8-bandit = [
{file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"},
]
flake8-bugbear = [
- {file = "flake8-bugbear-22.10.27.tar.gz", hash = "sha256:a6708608965c9e0de5fff13904fed82e0ba21ac929fe4896459226a797e11cd5"},
- {file = "flake8_bugbear-22.10.27-py3-none-any.whl", hash = "sha256:6ad0ab754507319060695e2f2be80e6d8977cfcea082293089a9226276bd825d"},
+ {file = "flake8-bugbear-22.12.6.tar.gz", hash = "sha256:4cdb2c06e229971104443ae293e75e64c6107798229202fbe4f4091427a30ac0"},
+ {file = "flake8_bugbear-22.12.6-py3-none-any.whl", hash = "sha256:b69a510634f8a9c298dfda2b18a8036455e6b19ecac4fe582e4d7a0abfa50a30"},
]
flake8-docstrings = [
{file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
{file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"},
]
flake8-import-order = [
- {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"},
- {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"},
+ {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"},
+ {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"},
]
flake8-string-format = [
{file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"},
@@ -1183,8 +1177,8 @@ httpcore = [
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
]
httpx = [
- {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
- {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
+ {file = "httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"},
+ {file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"},
]
identify = [
{file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"},
@@ -1227,16 +1221,16 @@ pbr = [
{file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"},
]
pep8-naming = [
- {file = "pep8-naming-0.13.2.tar.gz", hash = "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48"},
- {file = "pep8_naming-0.13.2-py3-none-any.whl", hash = "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23"},
+ {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"},
+ {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pre-commit = [
- {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
- {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
+ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
+ {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
]
prometheus-client = [
{file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"},
@@ -1304,6 +1298,8 @@ psycopg2-binary = [
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"},
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"},
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"},
+ {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"},
+ {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"},
{file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"},
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"},
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"},
@@ -1352,8 +1348,8 @@ psycopg2-binary = [
{file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"},
]
pycodestyle = [
- {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
- {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
+ {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
+ {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
]
pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
@@ -1368,16 +1364,16 @@ pyfakefs = [
{file = "pyfakefs-5.0.0.tar.gz", hash = "sha256:19d1d8f1ee520891d78b6ed05c2078e0792d545f83dee33461fbaa5cc72e187d"},
]
pyflakes = [
- {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
- {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
+ {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
+ {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
]
PyJWT = [
{file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
]
pymdown-extensions = [
- {file = "pymdown_extensions-9.7-py3-none-any.whl", hash = "sha256:767d07d9dead0f52f5135545c01f4ed627f9a7918ee86c646d893e24c59db87d"},
- {file = "pymdown_extensions-9.7.tar.gz", hash = "sha256:651b0107bc9ee790aedea3673cb88832c0af27d2569cf45c2de06f1d65292e96"},
+ {file = "pymdown_extensions-9.9-py3-none-any.whl", hash = "sha256:ac698c15265680db5eb13cd4342abfcde2079ac01e5486028f47a1b41547b859"},
+ {file = "pymdown_extensions-9.9.tar.gz", hash = "sha256:0f8fb7b74a37a61cc34e90b2c91865458b713ec774894ffad64353a5fce85cfc"},
]
python-dotenv = [
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
@@ -1442,12 +1438,12 @@ rfc3986 = [
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
sentry-sdk = [
- {file = "sentry-sdk-1.10.1.tar.gz", hash = "sha256:105faf7bd7b7fa25653404619ee261527266b14103fe1389e0ce077bd23a9691"},
- {file = "sentry_sdk-1.10.1-py2.py3-none-any.whl", hash = "sha256:06c0fa9ccfdc80d7e3b5d2021978d6eb9351fa49db9b5847cf4d1f2a473414ad"},
+ {file = "sentry-sdk-1.12.1.tar.gz", hash = "sha256:5bbe4b72de22f9ac1e67f2a4e6efe8fbd595bb59b7b223443f50fe5802a5551c"},
+ {file = "sentry_sdk-1.12.1-py2.py3-none-any.whl", hash = "sha256:9f0b960694e2d8bb04db4ba6ac2a645040caef4e762c65937998ff06064f10d6"},
]
setuptools = [
- {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
- {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
+ {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
+ {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1477,10 +1473,6 @@ taskipy = [
{file = "taskipy-1.10.3-py3-none-any.whl", hash = "sha256:4c0070ca53868d97989f7ab5c6f237525d52ee184f9b967576e8fe427ed9d0b8"},
{file = "taskipy-1.10.3.tar.gz", hash = "sha256:112beaf21e3d5569950b99162a1de003fa885fabee9e450757a6b874be914877"},
]
-toml = [
- {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
- {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
-]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index abd25ef0..f53dd33c 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -130,3 +130,31 @@ class Metricity:
raise NotFoundError()
return values
+
+ def total_messages_in_past_n_days(
+ self,
+ user_ids: list[str],
+ days: int
+ ) -> list[tuple[str, int]]:
+ """
+ Query activity by a list of users in the past `days` days.
+
+ Returns a list of (user_id, message_count) tuples.
+ """
+ self.cursor.execute(
+ """
+ SELECT
+ author_id, COUNT(*)
+ FROM messages
+ WHERE
+ author_id IN %s
+ AND NOT is_deleted
+ AND channel_id NOT IN %s
+ AND created_at > now() - interval '%s days'
+ GROUP BY author_id
+ """,
+ [tuple(user_ids), EXCLUDE_CHANNELS, days]
+ )
+ values = self.cursor.fetchall()
+
+ return values
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 5d10069d..d86e80bb 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -502,6 +502,90 @@ class UserMetricityTests(AuthenticatedAPITestCase):
"total_messages": total_messages
})
+ def test_metricity_activity_data(self):
+ # Given
+ self.mock_no_metricity_user() # Other functions shouldn't be used.
+ self.metricity.total_messages_in_past_n_days.return_value = [(0, 10)]
+
+ # When
+ url = reverse("api:bot:user-metricity-activity-data")
+ response = self.client.post(
+ url,
+ data=[0, 1],
+ QUERY_STRING="days=10",
+ )
+
+ # Then
+ self.assertEqual(response.status_code, 200)
+ self.metricity.total_messages_in_past_n_days.assert_called_once_with(["0", "1"], 10)
+ self.assertEqual(response.json(), {"0": 10, "1": 0})
+
+ def test_metricity_activity_data_invalid_days(self):
+ # Given
+ self.mock_no_metricity_user() # Other functions shouldn't be used.
+
+ # When
+ url = reverse("api:bot:user-metricity-activity-data")
+ response = self.client.post(
+ url,
+ data=[0, 1],
+ QUERY_STRING="days=fifty",
+ )
+
+ # Then
+ self.assertEqual(response.status_code, 400)
+ self.metricity.total_messages_in_past_n_days.assert_not_called()
+ self.assertEqual(response.json(), {"days": ["This query parameter must be an integer."]})
+
+ def test_metricity_activity_data_no_days(self):
+ # Given
+ self.mock_no_metricity_user() # Other functions shouldn't be used.
+
+ # When
+ url = reverse('api:bot:user-metricity-activity-data')
+ response = self.client.post(
+ url,
+ data=[0, 1],
+ )
+
+ # Then
+ self.assertEqual(response.status_code, 400)
+ self.metricity.total_messages_in_past_n_days.assert_not_called()
+ self.assertEqual(response.json(), {'days': ["This query parameter is required."]})
+
+ def test_metricity_activity_data_no_users(self):
+ # Given
+ self.mock_no_metricity_user() # Other functions shouldn't be used.
+
+ # When
+ url = reverse('api:bot:user-metricity-activity-data')
+ response = self.client.post(
+ url,
+ QUERY_STRING="days=10",
+ )
+
+ # Then
+ self.assertEqual(response.status_code, 400)
+ self.metricity.total_messages_in_past_n_days.assert_not_called()
+ self.assertEqual(response.json(), ['Expected a list of items but got type "dict".'])
+
+ def test_metricity_activity_data_invalid_users(self):
+ # Given
+ self.mock_no_metricity_user() # Other functions shouldn't be used.
+
+ # When
+ url = reverse('api:bot:user-metricity-activity-data')
+ response = self.client.post(
+ url,
+ data=[123, 'username'],
+ QUERY_STRING="days=10",
+ )
+
+ # Then
+ self.assertEqual(response.status_code, 400)
+ self.metricity.total_messages_in_past_n_days.assert_not_called()
+ self.assertEqual(response.json(), {'1': ['A valid integer is required.']})
+
def mock_metricity_user(self, joined_at, total_messages, total_blocks, top_channel_activity):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index ba1bcd9d..db73a83c 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -3,8 +3,9 @@ from collections import OrderedDict
from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework import status
+from rest_framework import fields, status
from rest_framework.decorators import action
+from rest_framework.exceptions import ParseError
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
@@ -138,6 +139,29 @@ class UserViewSet(ModelViewSet):
- 200: returned on success
- 404: if a user with the given `snowflake` could not be found
+ ### POST /bot/users/metricity_activity_data
+ Returns a mapping of user ID to message count in a given period for
+ the given user IDs.
+
+ #### Required Query Parameters
+ - days: how many days into the past to count message from.
+
+ #### Request Format
+ >>> [
+ ... 409107086526644234,
+ ... 493839819168808962
+ ... ]
+
+ #### Response format
+ >>> {
+ ... "409107086526644234": 54,
+ ... "493839819168808962": 0
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if request body or query parameters were missing or invalid
+
### POST /bot/users
Adds a single or multiple new users.
The roles attached to the user(s) must be roles known by the site.
@@ -298,3 +322,34 @@ class UserViewSet(ModelViewSet):
except NotFoundError:
return Response(dict(detail="User not found in metricity"),
status=status.HTTP_404_NOT_FOUND)
+
+ @action(detail=False, methods=["POST"])
+ def metricity_activity_data(self, request: Request) -> Response:
+ """Request handler for metricity_activity_data endpoint."""
+ if "days" in request.query_params:
+ try:
+ days = int(request.query_params["days"])
+ except ValueError:
+ raise ParseError(detail={
+ "days": ["This query parameter must be an integer."]
+ })
+ else:
+ raise ParseError(detail={
+ "days": ["This query parameter is required."]
+ })
+
+ user_id_list_validator = fields.ListField(
+ child=fields.IntegerField(min_value=0),
+ allow_empty=False
+ )
+ user_ids = [
+ str(user_id) for user_id in
+ user_id_list_validator.run_validation(request.data)
+ ]
+
+ with Metricity() as metricity:
+ data = metricity.total_messages_in_past_n_days(user_ids, days)
+
+ default_data = {user_id: 0 for user_id in user_ids}
+ response_data = default_data | dict(data)
+ return Response(response_data, status=status.HTTP_200_OK)
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md
index 971989a9..b08ba7c6 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md
@@ -26,7 +26,7 @@ If none of the above steps help you or you're not sure how to do some of the abo
# A Good Question
-When you're ready to ask a question, there's a few things you should have to hand before forming a query.
+When you're ready to ask a question, there are a few things you should have to hand before forming a query.
* A code example that illustrates your problem
* If possible, make this a minimal example rather than an entire application
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/commit-messages.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md
index ba476b65..ba476b65 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/commit-messages.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md
index 73c5dcab..d1e4250d 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md
@@ -12,7 +12,7 @@ We have simple but strict style rules that are enforced through linting.
Not all of the style rules are enforced by linting, so make sure to read the [style guide](../style-guide/) as well.
2. **Make great commits.**
Great commits should be atomic, with a commit message explaining what and why.
-Check out [Writing Good Commit Messages](./commit-messages) for details.
+Check out [Writing Good Commit Messages](../commit-messages/) for details.
3. **Do not open a pull request if you aren't assigned to the issue.**
If someone is already working on it, consider offering to collaborate with that person.
4. **Use assets licensed for public use.**
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml
deleted file mode 100644
index 80c8e772..00000000
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/_info.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-title: Contributing Guidelines
-description: Guidelines to adhere to when contributing to our projects.
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
index 2be845d3..bef2df9b 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
@@ -9,7 +9,7 @@ relevant_links:
toc: 3
---
-At Python Discord we have two different kinds of help channels: **topical help channels** and **general help channels**.
+At Python Discord we have two different kinds of help channels: **topical help channels** and **help forum posts**.
# Topical Help Channels
@@ -24,71 +24,82 @@ For example, `#data-science-and-ai` covers scientific Python, statistics, and ma
Each channel on the server has a channel description which briefly describes the topics covered by that channel. If you're not sure where to post, feel free to ask us which channel is appropriate in `#community-meta`.
-# General Help Channels
+# Help Forum Posts
-General help channels can be used for all Python-related help, and have the advantage of attracting a more diverse spectrum of helpers. There is also the added benefit of receiving individual focus and attention on your question. These channels are a great choice for generic Python help, but can be used for domain-specific Python help as well.
+Help forum posts can be used for all Python-related help, and have the advantage of attracting a more diverse spectrum of helpers. There is also the added benefit of receiving individual focus and attention on your question. These posts are a great choice for generic Python help, but can be used for domain-specific Python help as well.
-## How to Claim a Channel
+## How to Create A New Post
-There are always three help channels waiting to be claimed in the **Available Help Channels** category.
+There are 4 easy needed steps to make this happen
-![Available help channels](/static/images/content/help_channels/available_channels.png)
-*The Available Help Channels category is always at the top of the server's channel list.*
+1. Navigate to the **Python Help System** category.<br>
+![Python help system category](/static/images/content/help_channels/help-system-category.png)
+2. Open the **python-help** forum channel.
+3. Click on the **New Post** button in the top-right corner.<br>
+![New post button](/static/images/content/help_channels/new-post-button.png)
+4. Choose a brief title that best describes your issue, along with a message explaining it more in details, and **post** it.
+Note that you can also choose one or more tags which can help attract experts of that tag easily.<br>
+![New post form](/static/images/content/help_channels/new-post-form.png)
-![Available message](/static/images/content/help_channels/available_message.png)
-*This message indicates that a channel is available.*
+Be sure to [ask questions with enough information](../asking-good-questions) in order to give yourself the best chances of getting help!
-In order to claim one, simply ask your question in one of the available channels. Be sure to [ask questions with enough information](../asking-good-questions) in order to give yourself the best chances of getting help!
+At this point you will have the **Help Cooldown** role which will remain on your profile until you close your newly created post. This ensures that users can only have one post at any given time, giving everyone a chance to have their question seen.
-![Channel claimed embed](/static/images/content/help_channels/claimed_channel.png)
-*This messages indicates that you've claimed the channel.*
+# Frequently Asked Questions
-At this point you will have the **Help Cooldown** role which will remain on your profile until you close your help channel. This ensures that users can claim only one help channel at any given time, giving everyone a chance to have their question seen.
+### I created a new help post, what happens now?
+Once you click on `Post`, these events take place:<br>
+1. A new channel will be created for you, and you'll have an `OP` next to you username, which tells people you're the `Original Poster`, or in other words, the owner of the help topic in that channel.<br>
+2. Your original question/message will always be the first one in that channel.<br>
+3. Our Python bot will send a message reminding you of what you should include in your question/message in case you could have missed anything.<br>
+4. People will be able to jump on that channel, and you can have a discussion with anyone who's volunteering to help you by asking as many followup questions as you want.<br>
-# Frequently Asked Questions
+#### Example
+Suppose we're trying to find the minimum value in a list of integers.
+Once we've chosen our title and message content, we are ready to make a new post.<br><br>
+![Filled form example](/static/images/content/help_channels/question-example.png)<br><br>
+Note how we've checked the **Algos & data structs** tag here, whose circumference is highlighted in blue, since this is a question about an algorithm to find the minimum.<br>
+This will greatly help others pinpoint where they can help you best based on a combination of your title and tag from a first glance.<br><br>
+Once you click on post, a new channel is created, and you can see the original message on top along with the `OP` tag next to the poster's avatar.<br>
+You will also see the message that our Python bot sends instantly right after yours.<br><br>
+![Newly created thread example](/static/images/content/help_channels/newly-created-thread-example.png)
-### How long does my help channel stay active?
+### How long does my help post stay active?
-The channel remains open for **30 minutes** after your last message, or 10 minutes after the last message sent by another user (whichever time comes later).
+The post remains open for **30 minutes** after your last message, or 10 minutes after the last message sent by another user (whichever time comes later).
![Channel dormant message](/static/images/content/help_channels/dormant_message.png)
-*You'll see this message in your channel once it goes dormant.*
+*You'll see this message in your post once it goes dormant.*
+
### No one answered my question. How come?
-The server has users active all over the world and all hours of the day, but some time periods are less active than others. It's also possible that the users that read your question didn't have the knowledge required to help you. If no one responded, feel free to claim another help channel a little later, or try an appropriate topical channel.
+The server has users active all over the world and all hours of the day, but some time periods are less active than others. It's also possible that the users that read your question didn't have the knowledge required to help you. If no one responded, feel free to open another post a little later, or try an appropriate topical channel.
If you feel like your question is continuously being overlooked, read our guide on [asking good questions](../asking-good-questions) to increase your chances of getting a response.
### My question was answered. What do I do?
-Go ahead use the `!close` command if you've satisfactorily solved your problem. You will only be able to run this command in your own help channel, and no one (outside of staff) will be able to close your channel for you.
+Go ahead and use one of the `!close` or `!solved` commands if you've satisfactorily solved your problem. You will only be able to run this command in your own post, and no one (outside of staff) will be able to close your post for you.
-Closing your help channel once you are finished leads to less occupied channels, which means more attention can be given to other users that still need help.
+Closing your post once you are finished leads to less occupied ones, which means more attention can be given to other users that still need help.
### Can only Helpers answer help questions?
-Definitely not! We encourage all members of the community to participate in giving help. If you'd like to help answer some questions, head over to the **Occupied Help Channels** or **Topical Chat/Help** categories.
+Definitely not! We encourage all members of the community to participate in giving help. If you'd like to help answer some questions, you can either browse all posts in the **python-help** forum channel or head over to the **Topical Chat/Help** category.
Before jumping in, please read our guide on [helping others](../helping-others) which explains our expectations for the culture and quailty of help that we aim for on the server.
-Tip: run the `!helpdm on` command in `#bot-commands` to get notified via DM with jumplinks to help channels you're participating in.
-
-### What are the available, occupied, and dormant categories?
-
-The three help channels under **Available Help Channels** are free for anyone to claim. Claimed channels are then moved to **Occupied Help Channels**. Once they close, they are moved to the **Python Help: Dormant** category until they are needed again for **Available Help Channels**.
+Tip: run the `!helpdm on` command in the `#bot-commands` channel to get notified via DM with jumplinks to help posts you're participating in.
### Can I save my help session for future reference?
-Yes! Because the help channels are continuously cycled in and out without being deleted, this means you can always refer to a previous help session if you found one particularly helpful.
+Yes! Because the help posts are only closed without being deleted, this means you can always refer to a previous help session if you found one particularly helpful.
Tip: reply to a message and run the `.bm` command to get bookmarks sent to you via DM for future reference.
-### I lost my help channel!
+### I lost my help post!
-No need to panic. Your channel was probably just closed due to inactivity.
-All the dormant help channels are still available at the bottom of the channel list, in the **Python Help: Dormant** category, and also through search.
-If you're not sure what the name of your help channel was, you can easily find it by using the Discord Search feature.
+No need to panic. Your post was probably just closed due to inactivity.
+All the dormant help posts are still available at the bottom of the **python-help** forum channel and also through search in the **Python Help System** category.
+If you're not sure what the title of your help post was, you can easily find it by using the Discord Search feature.
Try searching for `from:<your nickname>` to find the last messages sent by yourself, and from there you will be able to jump directly into the channel by pressing the Jump button on your message.
-
-![Dormant help channels](/static/images/content/help_channels/dormant_channels.png)
-*The dormant help channels can be found at the bottom of the channel list.*
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md
index a7f1ce1d..9f0d947f 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md
@@ -9,7 +9,7 @@ relevant_links:
toc: 2
---
-Python Discord has a lot of people asking questions, be it in the help channels, topical channels, or any other part of the server.
+Python Discord has a lot of people asking questions, be it in the help forum, topical channels, or any other part of the server.
Therefore, you might sometimes want to give people the answers you have in mind.
But you might not be sure how best to approach the issue, or maybe you'd like to see how others handle it.
This article aims to present a few of the general principles which guide the staff on a day-to-day basis on the server.
@@ -64,7 +64,7 @@ At other times, it might not be as obvious, and it might be a good idea to kindl
The path is often more important than the answer.
Your goal should primarily be to allow the helpee to apply, at least to a degree, the concepts you introduce in your answer.
Otherwise, they might keep struggling with the same problem over and over again.
-That means that simply showing your answer might close the help channel for the moment, but won't be very helpful in the long-term.
+That means that simply showing your answer might close the help post for the moment, but won't be very helpful in the long-term.
A common approach is to walk the helpee through to an answer:
diff --git a/pydis_site/apps/content/resources/guides/python-guides/app-commands.md b/pydis_site/apps/content/resources/guides/python-guides/app-commands.md
new file mode 100644
index 00000000..713cd650
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/app-commands.md
@@ -0,0 +1,418 @@
+---
+title: Discord.py 2.0 changes
+description: Changes and new features in version 2.0 of discord.py
+---
+
+Upon the return of the most popular discord API wrapper library for Python, `discord.py`, while catching on to the latest features of the discord API, there have been numerous changes with additions of features to the library. Additions to the library include support for Buttons, Select Menus, Forms (AKA Modals), Slash Commands (AKA Application Commands) and a bunch more handy features! All the changes can be found [here](https://discordpy.readthedocs.io/en/latest/migrating.html). Original discord.py Gist regarding resumption can be found [here](https://gist.github.com/Rapptz/c4324f17a80c94776832430007ad40e6).
+
+
+# Install the latest version of discord.py
+
+Before you can make use of any of the new 2.0 features, you need to install the latest version of discord.py. Make sure that the version is 2.0 or above!
+Also, make sure to uninstall any third party libraries intended to add slash-command support to pre-2.0 discord.py, as they are no longer necessary and will likely cause issues.
+
+The latest and most up-to-date stable discord.py version can be installed using `pip install -U discord.py`.
+
+**Before migrating to discord.py 2.0, make sure you read the migration guide [here](https://discordpy.readthedocs.io/en/latest/migrating.html) as there are lots of breaking changes.**.
+{: .notification .is-warning }
+
+# What are Slash Commands?
+
+Slash Commands are an exciting new way to build and interact with bots on Discord. As soon as you type "/", you can easily see all the commands a bot has. It also comes with autocomplete, validation and error handling, which will all help users of your bot get the command right the first time.
+
+# Basic structure for discord.py Slash Commands!
+
+### Note that Slash Commands in discord.py are also referred to as **Application Commmands** and **App Commands** and every *interaction* is a *webhook*.
+Slash commands in discord.py are held by a container, [CommandTree](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=commandtree#discord.app_commands.CommandTree). A command tree is required to create Slash Commands in discord.py. This command tree provides a `command` method which decorates an asynchronous function indicating to discord.py that the decorated function is intended to be a slash command. This asynchronous function expects a default argument which acts as the interaction which took place that invoked the slash command. This default argument is an instance of the **Interaction** class from discord.py. Further up, the command logic takes over the behaviour of the slash command.
+
+# Fundamentals for this Gist!
+
+One new feature added in discord.py v2 is `setup_hook`. `setup_hook` is a special asynchronous method of the Client and Bot classes which can be overwritten to perform numerous tasks. This method is safe to use as it is always triggered before any events are dispatched, i.e. this method is triggered before the *IDENTIFY* payload is sent to the discord gateway.
+Note that methods of the Bot class such as `change_presence` will not work in setup_hook as the current application does not have an active connection to the gateway at this point.
+A full list of commands you can't use in setup_hook can be found [here](https://discord.com/developers/docs/topics/gateway-events#send-events).
+
+__**THE FOLLOWING ARE EXAMPLES OF HOW A `SETUP_HOOK` FUNCTION CAN BE DEFINED**__
+
+Note that the default intents are defined [here](https://discordpy.readthedocs.io/en/stable/api.html?highlight=discord%20intents%20default#discord.Intents.default) to have all intents enabled except presences, members, and message_content.
+
+```python
+import discord
+
+# You can create the setup_hook directly in the class definition
+
+class SlashClient(discord.Client):
+ def __init__(self) -> None:
+ super().__init__(intents=discord.Intents.default())
+
+ async def setup_hook(self) -> None:
+ ...
+
+# Or add it to the client after creating it
+
+client = discord.Client(intents=discord.Intents.default())
+async def my_setup_hook() -> None:
+ ...
+
+client.setup_hook = my_setup_hook
+```
+
+# Basic Slash Command application using discord.py.
+
+#### The `CommandTree` class resides within the `app_commands` of the discord.py package.
+
+## Slash Command Application with a Client
+
+```python
+import discord
+
+class SlashClient(discord.Client):
+ def __init__(self) -> None:
+ super().__init__(intents=discord.Intents.default())
+ self.tree = discord.app_commands.CommandTree(self)
+
+ async def setup_hook(self) -> None:
+ self.tree.copy_global_to(guild=discord.Object(id=12345678900987654))
+ await self.tree.sync()
+
+client = SlashClient()
+
[email protected](name="ping", description="...")
+async def _ping(interaction: discord.Interaction) -> None:
+ await interaction.response.send_message("pong")
+
+client.run("token")
+```
+
+
+__**EXPLANATION**__
+
+- `import discord` imports the **discord.py** package.
+- `class SlashClient(discord.Client)` is a class subclassing **Client**. Though there is no particular reason except readability to subclass the **Client** class, using the `Client.setup_hook = my_func` is equally valid.
+- Next up `super().__init__(...)` runs the `__init__` function of the **Client** class, this is equivalent to `discord.Client(...)`. Then, `self.tree = discord.app_commands.CommandTree(self)` creates a CommandTree which acts as the container for slash commands.
+- Then in the `setup_hook`, `self.tree.copy_global_to(...)` adds the slash command to the guild of which the ID is provided as a `discord.Object` object. **Essential to creation of commands** Further up, `self.tree.sync()` updates the API with any changes to the Slash Commands.
+- Finishing up with the **Client** subclass, we create an instance of the subclassed Client class which here has been named as `SlashClient` with `client = SlashClient()`.
+- Then using the `command` method of the `CommandTree` we decorate a function with it as `client.tree` is an instance of `CommandTree` for the current application. The command function takes a default argument as said, which acts as the interaction that took place. Catching up is `await interaction.response.send_message("pong")` which sends back a message to the slash command invoker.
+- And the classic old `client.run("token")` is used to connect the client to the discord gateway.
+- Note that the `send_message` is a method of the `InteractionResponse` class and `interaction.response` in this case is an instance of the `InteractionResponse` object. The `send_message` method will not function if the response is not sent within 3 seconds of command invocation. We will discuss how to handle this issue later following the Gist.
+
+## Slash Command application with the Bot class
+
+```python
+import discord
+
+class SlashBot(commands.Bot):
+ def __init__(self) -> None:
+ super().__init__(command_prefix=".", intents=discord.Intents.default())
+
+ async def setup_hook(self) -> None:
+ self.tree.copy_global_to(guild=discord.Object(id=12345678900987654))
+ await self.tree.sync()
+
+bot = SlashBot()
+
[email protected](name="ping", description="...")
+async def _ping(interaction: discord.Interaction) -> None:
+ await interaction.response.send_message("pong")
+
+bot.run("token")
+```
+
+The above example shows a basic Slash Commands within discord.py using the Bot class.
+
+__**EXPLANATION**__
+
+Most of the explanation is the same as the prior example that featured `SlashClient` which was a subclass of **discord.Client**. Though some minor changes are discussed below.
+
+- The `SlashBot` class now subclasses `discord.ext.commands.Bot` following the passing in of the required arguments to its `__init__` method.
+- `discord.ext.commands.Bot` already consists of an instance of the `CommandTree` class which can be accessed using the `tree` property.
+
+# Slash Commands within a Cog!
+
+A cog is a collection of commands, listeners, and optional state to help group commands together. More information on them can be found on the [Cogs](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html#ext-commands-cogs) page.
+
+## An Example to using cogs with discord.py for Slash Commands!
+
+```python
+import discord
+from discord.ext import commands
+from discord import app_commands
+
+class MySlashCog(commands.Cog):
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+
+ @app_commands.command(name="ping", description="...")
+ async def _ping(self, interaction: discord.Interaction):
+ await interaction.response.send_message("pong!")
+
+class MySlashBot(commands.Bot):
+ def __init__(self) -> None:
+ super().__init__(command_prefix="!", intents=discord.Intents.default())
+
+ async def setup_hook(self) -> None:
+ await self.add_cog(MySlashCog(self))
+ await self.tree.copy_global_to(discord.Object(id=123456789098765432))
+ await self.tree.sync()
+
+bot = MySlashBot()
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+
+- Firstly, `import discord` imports the **discord.py** package. `from discord import app_commands` imports the `app_commands` module from the **discord.py** root module. `from discord.ext import commands` imports the commands extension.
+- Further up, `class MySlashCog(commands.Cog)` is a class subclassing the `Cog` class. You can read more about this [here](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html#ext-commands-cogs).
+- `def __init__(self, bot: commands.Bot): self.bot = bot` is the constructor method of the class that is always run when the class is instantiated and that is why we pass in a **Bot** object whenever we create an instance of the cog class.
+- Following up is the `@app_commands.command(name="ping", description="...")` decorator. This decorator basically functions the same as a `bot.tree.command` but since the cog currently does not have a **bot**, the `app_commands.command` decorator is used instead. The next two lines follow the same structure for Slash Commands with **self** added as the first parameter to the function as it is a method of a class.
+- The next up lines are mostly the same.
+- Talking about the first line inside the `setup_hook` is the `add_cog` method of the **Bot** class. And since **self** acts as the **instance** of the current class, we use **self** to use the `add_cog` method of the **Bot** class as we are inside a subclassed class of the **Bot** class. Then we pass in **self** to the `add_cog` method as the `__init__` function of the **MySlashCog** cog accepts a `Bot` object.
+- After that we instantiate the `MySlashBot` class and run the bot using the **run** method which executes our setup_hook function and our commands get loaded and synced. The bot is now ready to use!
+
+# An Example to using groups with discord.py for Slash Commands!
+
+## An example with optional group!
+
+```python
+import discord
+from discord.ext import commands
+from discord import app_commands
+
+class MySlashGroupCog(commands.Cog):
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+
+ #--------------------------------------------------------
+ group = app_commands.Group(name="uwu", description="...")
+ #--------------------------------------------------------
+
+ @app_commands.command(name="ping", description="...")
+ async def _ping(self, interaction: discord.) -> None:
+ await interaction.response.send_message("pong!")
+
+ @group.command(name="command", description="...")
+ async def _cmd(self, interaction: discord.Interaction) -> None:
+ await interaction.response.send_message("uwu")
+
+class MySlashBot(commands.Bot):
+ def __init__(self) -> None:
+ super().__init__(command_prefix="!", intents=discord.Intents.default())
+
+ async def setup_hook(self) -> None:
+ await self.add_cog(MySlashGroupCog(self))
+ await self.tree.copy_global_to(discord.Object(id=123456789098765432))
+ await self.tree.sync()
+
+bot = MySlashBot()
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+- The only difference used here is `group = app_commands.Group(name="uwu", description="...")` and `group.command`. `app_commands.Group` is used to initiate a group while `group.command` registers a command under a group. For example, the ping command can be run using **/ping** but this is not the case for group commands. They are registered with the format of `group_name command_name`. So here, the **command** command of the **uwu** group would be run using **/uwu command**. Note that only group commands can have a single space between them.
+
+## An example with a **Group** subclass!
+
+```python
+import discord
+from discord.ext import commands
+from discord import app_commands
+
+class MySlashGroup(app_commands.Group, name="uwu"):
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+ super().__init__()
+
+ @app_commands.command(name="ping", description="...")
+ async def _ping(self, interaction: discord.) -> None:
+ await interaction.response.send_message("pong!")
+
+ @app_commands.command(name="command", description="...")
+ async def _cmd(self, interaction: discord.Interaction) -> None:
+ await interaction.response.send_message("uwu")
+
+class MySlashBot(commands.Bot):
+ def __init__(self) -> None:
+ super().__init__(command_prefix="!", intents=discord.Intents.default())
+
+ async def setup_hook(self) -> None:
+ await self.add_cog(MySlashGroup(self))
+ await self.tree.copy_global_to(discord.Object(id=123456789098765432))
+ await self.tree.sync()
+
+bot = MySlashBot()
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+- The only difference here too is that the `MySlashGroup` class directly subclasses the **Group** class from discord.app_commands which automatically registers all the methods within the group class to be commands of that specific group. So now, the commands such as `ping` can be run using **/uwu ping** and `command` using **/uwu command**.
+
+# Some common methods and features used for Slash Commands.
+
+### A common function used for Slash Commands is the `describe` function. This is used to add descriptions to the arguments of a slash command. The command function can decorated with this function. It goes by the following syntax as shown below.
+
+```python
+from discord.ext import commands
+from discord import app_commands
+import discord
+
+bot = commands.Bot(command_prefix=".", intents=discord.Intents.default())
+#sync the commands
+
[email protected](name="echo", description="...")
+@app_commands.describe(text="The text to send!", channel="The channel to send the message in!")
+async def _echo(interaction: discord.Interaction, text: str, channel: discord.TextChannel=None):
+ channel = channel or interaction.channel
+ await channel.send(text)
+```
+
+### Another common issue that most people come across is the time duration of sending a message with `send_message`. This issue can be tackled by deferring the interaction response using the `defer` method of the `InteractionResponse` class. An example for fixing this issue is shown below.
+
+```python
+import discord
+from discord.ext import commands
+import asyncio
+
+bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
+#sync the commands
+
[email protected](name="time", description="...")
+async def _time(interaction: discord.Interaction, time_to_wait: int):
+ # -------------------------------------------------------------
+ await interaction.response.defer(ephemeral=True)
+ # -------------------------------------------------------------
+ await interaction.edit_original_response(content=f"I will notify you after {time_to_wait} seconds have passed!")
+ await asyncio.sleep(time_to_wait)
+ await interaction.edit_original_response(content=f"{interaction.user.mention}, {time_to_wait} seconds have already passed!")
+```
+
+# Checking for Permissions and Roles!
+
+To add a permissions check to a command, the methods are imported through `discord.app_commands.checks`. To check for a member's permissions, the function can be decorated with the [discord.app_commands.checks.has_permissions](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=has_permissions#discord.app_commands.checks.has_permissions) method. An example to this as follows.
+
+```py
+from discord import app_commands
+from discord.ext import commands
+import discord
+
+bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
+#sync commands
+
[email protected](name="ping")
+@app_commands.checks.has_permissions(manage_messages=True, manage_channels=True) #example permissions
+async def _ping(interaction: discord.Interaction):
+ await interaction.response.send_message("pong!")
+
+```
+
+If the check fails, it will raise a `MissingPermissions` error which can be handled within an app commands error handler! We will discuss making an error handler later in the Gist. All the permissions can be found [here](https://discordpy.readthedocs.io/en/latest/api.html?highlight=discord%20permissions#discord.Permissions).
+
+Other methods that you can decorate the commands with are -
+- `bot_has_permissions` | This checks if the bot has the required permissions for executing the slash command. This raises a [BotMissingPermissions](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.BotMissingPermissions) exception.
+- `has_role` | This checks if the slash command user has the required role or not. Only **ONE** role name or role ID can be passed to this. If the name is being passed, make sure to have the exact same name as the role name. This raises a [MissingRole](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.MissingRole) exception.
+- To pass in several role names or role IDs, `has_any_role` can be used to decorate a command. This raises two exceptions -> [MissingAnyRole](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.MissingAnyRole) and [NoPrivateMessage](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.NoPrivateMessage)
+
+
+# Adding cooldowns to Slash Commands!
+
+Slash Commands within discord.py can be applied cooldowns to in order to prevent spamming of the commands. This can be done through the `discord.app_commands.checks.cooldown` method which can be used to decorate a slash command function and register a cooldown to the function. This raises a [CommandOnCooldown](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=checks%20cooldown#discord.app_commands.CommandOnCooldown) exception if the command is currently on cooldown.
+An example is as follows.
+
+```python
+from discord.ext import commands
+import discord
+
+class Bot(commands.Bot):
+ def __init__(self):
+ super().__init__(command_prefix="uwu", intents=discord.Intents.all())
+
+ async def setup_hook(self):
+ self.tree.copy_global_to(guild=discord.Object(id=12345678909876543))
+ await self.tree.sync()
+
+
+bot = Bot()
+
[email protected](name="ping")
+# -----------------------------------------
[email protected]_commands.checks.cooldown(1, 30)
+# -----------------------------------------
+async def ping(interaction: discord.Interaction):
+ await interaction.response.send_message("pong!")
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+- The first argument is the number of times this command can be invoked before the cooldown is triggered.
+- The second argument it takes is the period of time in which the command can be run the specified number of times.
+- The `CommandOnCooldown` exception can be handled using an error handler. We will discuss making an error handler for Slash Commands later in the Gist.
+
+
+# Handling errors for Slash Commands!
+
+The Slash Commands exceptions can be handled by overwriting the `on_error` method of the `CommandTree`. The error handler takes two arguments. The first argument is the `Interaction` that took place when the error occurred and the second argument is the error that occurred when the Slash Commands was invoked. The error is an instance of [discord.app_commands.AppCommandError](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=appcommanderror#discord.app_commands.AppCommandError) which is a subclass of [DiscordException](https://discordpy.readthedocs.io/en/latest/api.html?highlight=discordexception#discord.DiscordException).
+An example to creating an error handler for Slash Commands is as follows.
+
+```python
+from discord.ext import commands
+from discord import app_commands
+import discord
+
+bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
+#sync commands
+
[email protected](name="ping")
+@app_commands.checks.cooldown(1, 30)
+async def ping(interaction: discord.Interaction):
+ await interaction.response.send_message("pong!")
+
+async def on_tree_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
+ if isinstance(error, app_commands.CommandOnCooldown):
+ return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!")
+ elif isinstance(error, ...):
+ ...
+ else:
+ raise error
+
+bot.tree.on_error = on_tree_error
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+
+First we create a simple asynchronous function named `on_tree_error` here. To which the first two required arguments are passed, `Interaction` which is named as `interaction` here and `AppCommandError` which is named as `error` here. Then using simple functions and keywords, we make an error handler like above. Here we have used the `isinstance` function which takes in an object and a base class as the second argument, this function returns a bool value. The `raise error` is just for displaying unhandled errors, i.e. the ones which have not been handled manually. If this is **removed**, you will not be able to see any exceptions raised by Slash Commands and makes debugging the code harder.
+After creating the error handler function, we set the function as the error handler for the Slash Commands. Here, `bot.tree.on_error = on_tree_error` overwrites the default `on_error` method of the **CommandTree** class with our custom error handler which has been named as `on_tree_error` here.
+
+### Creating an error handler for a specific error!
+
+```python
+from discord.ext import commands
+from discord import app_commands
+import discord
+
+bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
+#sync commands
+
[email protected](name="ping")
+@app_commands.checks.cooldown(1, 30)
+async def ping(interaction: discord.Interaction):
+ await interaction.response.send_message("pong!")
+
+async def ping_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
+ if isinstance(error, app_commands.CommandOnCooldown):
+ return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!")
+ elif isinstance(error, ...):
+ ...
+ else:
+ raise error
+
+bot.run("token")
+```
+
+__**EXPLANATION**__
+
+Here the command name is simply used to access the `error` method to decorate a function which acts as the `on_error` but for a specific command. You should not need to call the `error` method manually.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md b/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md
new file mode 100644
index 00000000..57d86e99
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md
@@ -0,0 +1,323 @@
+---
+title: How to host a bot with Docker and GitHub Actions on Ubuntu VPS
+description: This guide shows how to host a bot with Docker and GitHub Actions on Ubuntu VPS
+---
+
+## Contents
+
+1. [You will learn](#you-will-learn)
+2. [Introduction](#introduction)
+3. [Installing Docker](#installing-docker)
+4. [Creating Dockerfile](#creating-dockerfile)
+5. [Building Image and Running Container](#building-image-and-running-container)
+6. [Creating Volumes](#creating-volumes)
+7. [Using GitHub Actions for full automation](#using-github-actions-for-full-automation)
+
+## You will learn how to
+
+- write Dockerfile
+- build Docker image and run the container
+- use Docker Compose
+- make docker keep the files throughout the container's runs
+- parse environment variables into container
+- use GitHub Actions for automation
+- set up self-hosted runner
+- use runner secrets
+
+## Introduction
+
+Let's say you have got a nice discord bot written in python and you have a VPS to host it on. Now the only question is
+how to run it 24/7. You might have been suggested to use *screen multiplexer*, but it has some disadvantages:
+
+1. Every time you update the bot you have to SSH to your server, attach to screen, shutdown the bot, run `git pull` and
+ run the bot again. You might have good extensions management that allows you to update the bot without restarting it,
+ but there are some other cons as well
+2. If you update some dependencies, you have to update them manually
+3. The bot doesn't run in an isolated environment, which is not good for security.
+
+But there's a nice and easy solution to these problems - **Docker**! Docker is a containerization utility that automates
+some stuff like dependencies update and running the application in the background. So let's get started.
+
+## Installing Docker
+
+The best way to install Docker is to use
+the [convenience script](https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script) provided
+by Docker developers themselves. You just need 2 lines:
+
+```shell
+$ curl -fsSL https://get.docker.com -o get-docker.sh
+$ sudo sh get-docker.sh
+```
+
+## Creating Dockerfile
+
+To tell Docker what it has to do to run the application, we need to create a file named `Dockerfile` in our project's
+root.
+
+1. First we need to specify the *base image*, which is the OS that the docker container will be running. Doing that will
+ make Docker install some apps we need to run our bot, for
+ example the Python interpreter
+
+```dockerfile
+FROM python:3.10-bullseye
+```
+
+2. Next, we need to copy our Python project's external dependencies to some directory *inside the container*. Let's call
+ it `/app`
+
+```dockerfile
+COPY requirements.txt /app/
+```
+
+3. Now we need to set the directory as working and install the requirements
+
+```dockerfile
+WORKDIR /app
+RUN pip install -r requirements.txt
+```
+
+4. The only thing that is left to do is to copy the rest of project's files and run the main executable
+
+```dockerfile
+COPY . .
+CMD ["python3", "main.py"]
+```
+
+The final version of Dockerfile looks like this:
+
+```dockerfile
+FROM python:3.10-bullseye
+COPY requirements.txt /app/
+WORKDIR /app
+RUN pip install -r requirements.txt
+COPY . .
+CMD ["python3", "main.py"]
+```
+
+## Building Image and Running Container
+
+Now update the project on your VPS, so we can run the bot with Docker.
+
+1. Build the image (dot at the end is very important)
+
+```shell
+$ docker build -t mybot .
+```
+
+- the `-t` flag specifies a **tag** that will be assigned to the image. With it, we can easily run the image that the
+ tag was assigned to.
+- the dot at the end is basically the path to search for Dockerfile. The dot means current directory (`./`)
+
+2. Run the container
+
+```shell
+$ docker run -d --name mybot mybot:latest
+```
+
+- `-d` flag tells Docker to run the container in detached mode, meaning it will run the container in the background of
+ your terminal and not give us
+ any output from it. If we don't
+ provide it, the `run` will be giving us the output until the application exits. Discord bots aren't supposed to exit
+ after certain time, so we do need this flag
+- `--name` assigns a name to the container. By default, container is identified by id that is not human-readable. To
+ conveniently refer to container when needed,
+ we can assign it a name
+- `mybot:latest` means "latest version of `mybot` image"
+
+3. Read bot logs (keep in mind that this utility only allows to read STDERR)
+
+```shell
+$ docker logs -f mybot
+```
+
+- `-f` flag tells the docker to keep reading logs as they appear in container and is called "follow mode". To exit
+ press `CTRL + C`.
+
+If everything went successfully, your bot will go online and will keep running!
+
+## Using Docker Compose
+
+Just 2 commands to run a container is cool, but we can shorten it down to just 1 simple command. For that, create
+a `docker-compose.yml` file in project's root and fill it with the following contents:
+
+```yml
+version: "3.8"
+services:
+ main:
+ build: .
+ container_name: mybot
+```
+
+- `version` tells Docker what version of Compose to use. You may check all the
+ versions [here](https://docs.docker.com/compose/compose-file/compose-versioning/)
+- `services` contains services to build and run. Read more about
+ services [here](https://docs.docker.com/compose/compose-file/#services-top-level-element)
+- `main` is a service. We can call it whatever we would like to, not necessarily `main`
+- `build: .` is a path to search for Dockerfile, just like `docker build` command's dot
+- `container_name: mybot` is a container name to use for a bot, just like `docker run --name mybot`
+
+Update the project on VPS, remove the previous container with `docker rm -f mybot` and run this command
+
+```shell
+docker compose up -d --build
+```
+
+Now the docker will automatically build the image for you and run the container.
+
+### Why docker-compose
+
+The main purpose of Compose is to run several services at once. Mostly we
+don't need this in discord bots, however.
+For us, it has the following benefits:
+
+- we can build and run the container with just one command
+- if we need to parse some environment variables or volumes (more about them further in tutorial) our run command would
+ look like this
+
+```shell
+$ docker run -d --name mybot -e TOKEN=... -e WEATHER_API_APIKEY=... -e SOME_USEFUL_ENVIRONMENT_VARIABLE=... --net=host -v /home/exenifix/bot-data/data:/app/data -v /home/exenifix/bot-data/images:/app/data/images
+```
+
+This is pretty long and unreadable. Compose allows us to transfer those flags into single config file and still
+use just one short command to run the container.
+
+## Creating Volumes
+
+The files creating during container run are destroyed after its recreation. To prevent some files from getting
+destroyed, we need to use *volumes* that basically save the files from directory inside of container somewhere on drive.
+
+1. Create a new directory somewhere and copy path to it
+
+```shell
+$ mkdir mybot-data
+$ echo $(pwd)/mybot-data
+```
+
+My path is `/home/exenifix/mybot-data`, yours is most likely **different**!
+
+2. In your project, store the files that need to be persistent in a separate directory (eg. `data`)
+3. Add `volumes` to `docker-compose.yaml` so it looks like this:
+
+```yml
+version: "3.8"
+services:
+ main:
+ build: .
+ container_name: mybot
+ volumes:
+ - /home/exenifix/mybot-data:/app/data
+```
+
+The path before the colon `:` is the directory *on server's drive, outside of container*, and the second path is the
+directory *inside of container*.
+All the files saved in container in that directory will be saved on drive's directory as well and Docker will be
+accessing them *from drive*.
+
+## Using GitHub Actions for full automation
+
+Now it's time to fully automate the process and make Docker update the bot automatically on every commit or release. For
+that, we will use a **GitHub Actions workflow**, which basically runs some commands when we need to. You may read more
+about them [here](https://docs.github.com/en/actions/using-workflows).
+
+### Create repository secret
+
+We will not have the ability to use `.env` files with the workflow, so it's better to store the environment variables
+as **actions secrets**. Let's add your discord bot's token as a secret
+
+1. Head to your repository page -> Settings -> Secrets -> Actions
+2. Press `New repository secret`
+3. Give it a name like `TOKEN` and paste the token.
+ Now we will be able to access its value in workflow like `${{ secrets.TOKEN }}`. However, we also need to parse the
+ variable into container now. Edit `docker-compose` so it looks like this:
+
+```yml
+version: "3.8"
+services:
+ main:
+ build: .
+ container_name: mybot
+ volumes:
+ - /home/exenifix/mybot-data:/app/data
+ environment:
+ - TOKEN
+```
+
+### Setup self-hosted runner
+
+To run the workflow on our VPS, we will need to register it as *self-hosted runner*.
+
+1. Head to Settings -> Actions -> Runners
+2. Press `New self-hosted runner`
+3. Select runner image and architecture
+4. Follow the instructions but don't run the runner
+5. Instead, create a service
+
+```shell
+$ sudo ./svc.sh install
+$ sudo ./svc.sh start
+```
+
+Now we have registered our VPS as a self-hosted runner and we can run the workflow on it now.
+
+### Write a workflow
+
+Create a new file `.github/workflows/runner.yml` and paste the following content into it. Please pay attention to
+the `branches` instruction.
+The GitHub's standard main branch name is `main`, however it may be named `master` or something else if you edited its
+name. Make sure to put
+the correct branch name, otherwise it won't work. More about GitHub workflows
+syntax [here](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
+
+```yml
+name: Docker Runner
+
+on:
+ push:
+ branches: [ main ]
+
+jobs:
+ run:
+ runs-on: self-hosted
+ environment: production
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Run Container
+ run: docker compose up -d --build
+ env:
+ TOKEN: ${{ secrets.TOKEN }}
+
+ - name: Cleanup Unused Images
+ run: docker image prune -f
+```
+
+Run `docker rm -f mybot` (it only needs to be done once) and push to GitHub. Now if you open `Actions` tab on your
+repository, you should see a workflow running your bot. Congratulations!
+
+### Displaying logs in actions terminal
+
+There's a nice utility for reading docker container's logs and stopping upon meeting a certain phrase and it might be
+useful for you as well.
+
+1. Install the utility on your VPS with
+
+```shell
+$ pip install exendlr
+```
+
+2. Add a step to your workflow that would show the logs until it meets `"ready"` phrase. I recommend putting it before
+ the cleanup.
+
+```yml
+- name: Display Logs
+ run: python3 -m exendlr mybot "ready"
+```
+
+Now you should see the logs of your bot until the stop phrase is met.
+
+**WARNING**
+> The utility only reads from STDERR and redirects to STDERR, if you are using STDOUT for logs, it will not work and
+> will be waiting for stop phrase forever. The utility automatically exits if bot's container is stopped (e.g. error
+> occurred during starting) or if a log line contains a stop phrase. Make sure that your bot 100% displays a stop phrase
+> when it's ready otherwise your workflow will get stuck.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md b/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md
new file mode 100644
index 00000000..096e3a90
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md
@@ -0,0 +1,23 @@
+---
+title: Fixing an SSL Certificate Verification Error
+description: A guide on fixing verification of an SSL certificate.
+---
+
+We're fixing the error Python specifies as [ssl.SSLCertVerificationError](https://docs.python.org/3/library/ssl.html#ssl.SSLCertVerificationError).
+
+# How to fix SSL Certificate issue on Windows
+
+Firstly, try updating your OS, wouldn't hurt to try.
+
+Now, if you're still having an issue, you would need to download the certificate for the SSL.
+
+The SSL Certificate, Sectigo (cert vendor) provides a download link of an [SSL certificate](https://crt.sh/?id=2835394). You should find it in the bottom left corner, shown below:
+
+A picture where to find the certificate in the website is:
+![location of certificate](/static/images/content/fix-ssl-certificate/pem.png)
+
+You have to setup the certificate yourself. To do that you can just click on it, or if that doesn't work, refer to [this link](https://portal.threatpulse.com/docs/sol/Solutions/ManagePolicy/SSL/ssl_chrome_cert_ta.htm)
+
+# How to fix SSL Certificate issue on Mac
+
+Navigate to your `Applications/Python 3.x/` folder and double-click the `Install Certificates.command` to fix this.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md b/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md
new file mode 100644
index 00000000..9d523b4b
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md
@@ -0,0 +1,29 @@
+---
+title: Keeping Discord Bot Tokens Safe
+description: How to keep your bot tokens safe and safety measures you can take.
+---
+It's **very** important to keep a bot token safe,
+primarily because anyone who has the bot token can do whatever they want with the bot --
+such as destroying servers your bot has been added to and getting your bot banned from the API.
+
+# How to Avoid Leaking your Token
+To help prevent leaking your token,
+you should ensure that you don't upload it to an open source program/website,
+such as replit and github, as they show your code publicly.
+The best practice for storing tokens is generally utilising .env files
+([click here](https://vcokltfre.dev/tips/tokens/.) for more information on storing tokens safely).
+
+# What should I do if my token does get leaked?
+
+If for whatever reason your token gets leaked, you should immediately follow these steps:
+- Go to the list of [Discord Bot Applications](https://discord.com/developers/applications) you have and select the bot application that had the token leaked.
+- Select the Bot (1) tab on the left-hand side, next to a small image of a puzzle piece. After doing so you should see a small section named TOKEN (under your bot USERNAME and next to his avatar image)
+- Press the Regenerate button to regenerate your bot token and invalidate the old one.
+
+![Steps to Take to Reset your Discord Bot](/static/images/content/regenerating_token.jpg)
+
+Following these steps will create a new token for your bot, making it secure again and terminating any connections from the leaked token.
+The old token will stop working though, so make sure to replace the old token with the new one in your code if you haven't already.
+
+# Summary
+Make sure you keep your token secure by storing it safely, not sending it to anyone you don't trust, and regenerating your token if it does get leaked.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md b/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md
new file mode 100644
index 00000000..74b0f59b
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md
@@ -0,0 +1,70 @@
+---
+title: Proper error handling in discord.py
+description: Are you not getting any errors? This might be why!
+---
+If you're not recieving any errors in your console, even though you know you should be, try this:
+
+# With bot subclass:
+```py
+import discord
+from discord.ext import commands
+
+import traceback
+import sys
+
+class MyBot(commands.Bot):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ async def on_command_error(self, ctx: commands.Context, error):
+ # Handle your errors here
+ if isinstance(error, commands.MemberNotFound):
+ await ctx.send("I could not find member '{error.argument}'. Please try again")
+
+ elif isinstance(error, commands.MissingRequiredArgument):
+ await ctx.send(f"'{error.param.name}' is a required argument.")
+ else:
+ # All unhandled errors will print their original traceback
+ print(f'Ignoring exception in command {ctx.command}:', file=sys.stderr)
+ traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
+
+bot = MyBot(command_prefix="!", intents=discord.Intents.default())
+
+bot.run("token")
+```
+
+# Without bot subclass
+```py
+import discord
+from discord.ext import commands
+
+import traceback
+import sys
+
+async def on_command_error(self, ctx: commands.Context, error):
+ # Handle your errors here
+ if isinstance(error, commands.MemberNotFound):
+ await ctx.send("I could not find member '{error.argument}'. Please try again")
+
+ elif isinstance(error, commands.MissingRequiredArgument):
+ await ctx.send(f"'{error.param.name}' is a required argument.")
+ else:
+ # All unhandled errors will print their original traceback
+ print(f'Ignoring exception in command {ctx.command}:', file=sys.stderr)
+ traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
+
+bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
+bot.on_command_error = on_command_error
+
+bot.run("token")
+```
+
+
+Make sure to import `traceback` and `sys`!
+
+-------------------------------------------------------------------------------------------------------------
+
+Useful Links:
+- [FAQ](https://discordpy.readthedocs.io/en/latest/faq.html)
+- [Simple Error Handling](https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612)
diff --git a/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md b/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md
new file mode 100644
index 00000000..45c7b37c
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md
@@ -0,0 +1,48 @@
+---
+title: Setting Different Statuses on Your Bot
+description: How to personalize your Discord bot status
+---
+
+You've probably seen a bot or two have a status message under their username in the member bar set to something such as `Playing Commands: .help`.
+
+This guide shows how to set such a status, so your bot can have one as well.
+
+**Please note:**
+
+If you want to change the bot status, it is suggested to not do so during the `on_ready` event, since it would be called many times and making an API call on that event has a chance to disconnect the bot.
+
+The status should not have a problem being set during runtime with `change_presence`, in the examples shown below.
+
+Instead, set the desired status using the activity / status kwarg of commands.Bot, for example:
+```python
+bot = commands.Bot(command_prefix="!", activity=..., status=...)
+```
+
+The following are examples of what you can put into the `activity` keyword argument.
+
+#### Setting 'Playing' Status
+```python
+await client.change_presence(activity=discord.Game(name="a game"))
+```
+
+#### Setting 'Streaming' Status
+```python
+await client.change_presence(activity=discord.Streaming(name="My Stream", url=my_twitch_url))
+```
+
+#### Setting 'Listening' Status
+```python
+await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="a song"))
+```
+
+#### Setting 'Watching' Status
+```python
+await client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="a movie"))
+```
+
+### Add Optional Status as Well:
+
+* `discord.Status.online` (default, green icon)
+* `discord.Status.idle` (yellow icon)
+* `discord.Status.do_not_disturb` (red icon)
+* `discord.Status.offline` (gray icon)
diff --git a/pydis_site/apps/content/resources/guides/python-guides/vps-services.md b/pydis_site/apps/content/resources/guides/python-guides/vps-services.md
index 0acd3e55..710fd914 100644
--- a/pydis_site/apps/content/resources/guides/python-guides/vps-services.md
+++ b/pydis_site/apps/content/resources/guides/python-guides/vps-services.md
@@ -1,9 +1,12 @@
---
-title: VPS Services
-description: On different VPS services
+title: VPS and Free Hosting Service for Discord bots
+description: This article lists recommended VPS services and covers the disasdvantages of utilising a free hosting service to run a discord bot.
+toc: 2
---
-If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). This is a list of VPS services that are sufficient for running Discord bots.
+## Recommended VPS services
+
+If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). Here is a list of VPS services that are sufficient for running Discord bots.
* Europe
* [netcup](https://www.netcup.eu/)
@@ -25,7 +28,31 @@ If you need to run your bot 24/7 (with no downtime), you should consider using a
* [OVHcloud](https://www.ovhcloud.com/)
* [Vultr](https://www.vultr.com/)
----
-# Free hosts
-There are no reliable free options for VPS hosting. If you would rather not pay for a hosting service, you can consider self-hosting.
-Any modern hardware should be sufficient for running a bot. An old computer with a few GB of ram could be suitable, or a Raspberry Pi.
+
+## Why not to use free hosting services for bots?
+While these may seem like nice and free services, it has a lot more caveats than you may think. For example, the drawbacks of using common free hosting services to host a discord bot are discussed below.
+
+### Replit
+
+- The machines are super underpowered, resulting in your bot lagging a lot as it gets bigger.
+
+- You need to run a webserver alongside your bot to prevent it from being shut off. This uses extra machine power.
+
+- Repl.it uses an ephemeral file system. This means any file you saved through your bot will be overwritten when you next launch.
+
+- They use a shared IP for everything running on the service.
+This one is important - if someone is running a user bot on their service and gets banned, everyone on that IP will be banned. Including you.
+
+### Heroku
+- Bots are not what the platform is designed for. Heroku is designed to provide web servers (like Django, Flask, etc). This is why they give you a domain name and open a port on their local emulator.
+
+- Heroku's environment is heavily containerized, making it significantly underpowered for a standard use case.
+
+- Heroku's environment is volatile. In order to handle the insane amount of users trying to use it for their own applications, Heroku will dispose your environment every time your application dies unless you pay.
+
+- Heroku has minimal system dependency control. If any of your Python requirements need C bindings (such as PyNaCl
+ binding to libsodium, or lxml binding to libxml), they are unlikely to function properly, if at all, in a native
+ environment. As such, you often need to resort to adding third-party buildpacks to facilitate otherwise normal
+ CPython extension functionality. (This is the reason why voice doesn't work natively on heroku)
+
+- Heroku only offers a limited amount of time on their free programme for your applications. If you exceed this limit, which you probably will, they'll shut down your application until your free credit resets.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/vps_services.md b/pydis_site/apps/content/resources/guides/python-guides/vps_services.md
deleted file mode 100644
index 710fd914..00000000
--- a/pydis_site/apps/content/resources/guides/python-guides/vps_services.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-title: VPS and Free Hosting Service for Discord bots
-description: This article lists recommended VPS services and covers the disasdvantages of utilising a free hosting service to run a discord bot.
-toc: 2
----
-
-## Recommended VPS services
-
-If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). Here is a list of VPS services that are sufficient for running Discord bots.
-
-* Europe
- * [netcup](https://www.netcup.eu/)
- * Germany & Austria data centres.
- * Great affiliate program.
- * [Yandex Cloud](https://cloud.yandex.ru/)
- * Vladimir, Ryazan, and Moscow region data centres.
- * [Scaleway](https://www.scaleway.com/)
- * France data centre.
- * [Time 4 VPS](https://www.time4vps.eu/)
- * Lithuania data centre.
-* US
- * [GalaxyGate](https://galaxygate.net/)
- * New York data centre.
- * Great affiliate program.
-* Global
- * [Linode](https://www.linode.com/)
- * [Digital Ocean](https://www.digitalocean.com/)
- * [OVHcloud](https://www.ovhcloud.com/)
- * [Vultr](https://www.vultr.com/)
-
-
-## Why not to use free hosting services for bots?
-While these may seem like nice and free services, it has a lot more caveats than you may think. For example, the drawbacks of using common free hosting services to host a discord bot are discussed below.
-
-### Replit
-
-- The machines are super underpowered, resulting in your bot lagging a lot as it gets bigger.
-
-- You need to run a webserver alongside your bot to prevent it from being shut off. This uses extra machine power.
-
-- Repl.it uses an ephemeral file system. This means any file you saved through your bot will be overwritten when you next launch.
-
-- They use a shared IP for everything running on the service.
-This one is important - if someone is running a user bot on their service and gets banned, everyone on that IP will be banned. Including you.
-
-### Heroku
-- Bots are not what the platform is designed for. Heroku is designed to provide web servers (like Django, Flask, etc). This is why they give you a domain name and open a port on their local emulator.
-
-- Heroku's environment is heavily containerized, making it significantly underpowered for a standard use case.
-
-- Heroku's environment is volatile. In order to handle the insane amount of users trying to use it for their own applications, Heroku will dispose your environment every time your application dies unless you pay.
-
-- Heroku has minimal system dependency control. If any of your Python requirements need C bindings (such as PyNaCl
- binding to libsodium, or lxml binding to libxml), they are unlikely to function properly, if at all, in a native
- environment. As such, you often need to resort to adding third-party buildpacks to facilitate otherwise normal
- CPython extension functionality. (This is the reason why voice doesn't work natively on heroku)
-
-- Heroku only offers a limited amount of time on their free programme for your applications. If you exceed this limit, which you probably will, they'll shut down your application until your free credit resets.
diff --git a/pydis_site/static/images/content/fix-ssl-certificate/pem.png b/pydis_site/static/images/content/fix-ssl-certificate/pem.png
new file mode 100644
index 00000000..face520f
--- /dev/null
+++ b/pydis_site/static/images/content/fix-ssl-certificate/pem.png
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/available_channels.png b/pydis_site/static/images/content/help_channels/available_channels.png
deleted file mode 100644
index 0b9cfd03..00000000
--- a/pydis_site/static/images/content/help_channels/available_channels.png
+++ /dev/null
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/available_message.png b/pydis_site/static/images/content/help_channels/available_message.png
deleted file mode 100644
index 09668c9b..00000000
--- a/pydis_site/static/images/content/help_channels/available_message.png
+++ /dev/null
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/claimed_channel.png b/pydis_site/static/images/content/help_channels/claimed_channel.png
deleted file mode 100644
index 777e31ea..00000000
--- a/pydis_site/static/images/content/help_channels/claimed_channel.png
+++ /dev/null
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/dormant_channels.png b/pydis_site/static/images/content/help_channels/dormant_channels.png
deleted file mode 100644
index 7c9ba61e..00000000
--- a/pydis_site/static/images/content/help_channels/dormant_channels.png
+++ /dev/null
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/help-system-category.png b/pydis_site/static/images/content/help_channels/help-system-category.png
new file mode 100644
index 00000000..bea5a92c
--- /dev/null
+++ b/pydis_site/static/images/content/help_channels/help-system-category.png
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/new-post-button.png b/pydis_site/static/images/content/help_channels/new-post-button.png
new file mode 100644
index 00000000..4ceabf0f
--- /dev/null
+++ b/pydis_site/static/images/content/help_channels/new-post-button.png
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/new-post-form.png b/pydis_site/static/images/content/help_channels/new-post-form.png
new file mode 100644
index 00000000..3e90bf7d
--- /dev/null
+++ b/pydis_site/static/images/content/help_channels/new-post-form.png
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/newly-created-thread-example.png b/pydis_site/static/images/content/help_channels/newly-created-thread-example.png
new file mode 100644
index 00000000..d7b1eed4
--- /dev/null
+++ b/pydis_site/static/images/content/help_channels/newly-created-thread-example.png
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/occupied_channels.png b/pydis_site/static/images/content/help_channels/occupied_channels.png
deleted file mode 100644
index 6ccb4ed6..00000000
--- a/pydis_site/static/images/content/help_channels/occupied_channels.png
+++ /dev/null
Binary files differ
diff --git a/pydis_site/static/images/content/help_channels/question-example.png b/pydis_site/static/images/content/help_channels/question-example.png
new file mode 100644
index 00000000..da181351
--- /dev/null
+++ b/pydis_site/static/images/content/help_channels/question-example.png
Binary files differ
diff --git a/pydis_site/static/images/content/regenerating_token.jpg b/pydis_site/static/images/content/regenerating_token.jpg
new file mode 100644
index 00000000..7b2588dc
--- /dev/null
+++ b/pydis_site/static/images/content/regenerating_token.jpg
Binary files differ
diff --git a/pyproject.toml b/pyproject.toml
index 7aff4491..ef3c6f5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,37 +7,37 @@ license = "MIT"
[tool.poetry.dependencies]
python = "3.10.*"
-django = "4.1.2"
+django = "4.1.4"
django-environ = "0.9.0"
django-filter = "22.1"
djangorestframework = "3.14.0"
psycopg2-binary = "2.9.5"
django-simple-bulma = "2.5.0"
whitenoise = "6.2.0"
-httpx = "0.23.0"
+httpx = "0.23.1"
pyyaml = "6.0"
gunicorn = "20.1.0"
-sentry-sdk = "1.10.1"
+sentry-sdk = "1.12.1"
markdown = "3.4.1"
python-frontmatter = "1.0.0"
django-prometheus = "2.2.0"
-django-distill = "3.0.1"
+django-distill = "3.0.2"
PyJWT = {version = "2.6.0", extras = ["crypto"]}
-pymdown-extensions = "9.7"
+pymdown-extensions = "9.9"
[tool.poetry.dev-dependencies]
-coverage = "6.5.0"
-flake8 = "5.0.4"
+coverage = "7.0.1"
+flake8 = "6.0.0"
flake8-annotations = "2.9.1"
flake8-bandit = "4.1.1"
-flake8-bugbear = "22.10.27"
+flake8-bugbear = "22.12.6"
flake8-docstrings = "1.6.0"
-flake8-import-order = "0.18.1"
+flake8-import-order = "0.18.2"
flake8-tidy-imports = "4.8.0"
flake8-string-format = "0.3.0"
flake8-todo = "0.7"
-pep8-naming = "0.13.2"
-pre-commit = "2.20.0"
+pep8-naming = "0.13.3"
+pre-commit = "2.21.0"
pyfakefs = "5.0.0"
taskipy = "1.10.3"
python-dotenv = "0.21.0"