diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..a55e7a17
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0422079e..90fe8c99 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -6,5 +6,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/royalnet.iml b/.idea/royalnet.iml
index 2b5b5330..f344fe9e 100644
--- a/.idea/royalnet.iml
+++ b/.idea/royalnet.iml
@@ -5,7 +5,7 @@
-
+
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 00000000..71585ea0
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,755 @@
+[[package]]
+category = "main"
+description = "Async http client/server framework (asyncio)"
+name = "aiohttp"
+optional = true
+python-versions = ">=3.5.3"
+version = "3.5.4"
+
+[package.dependencies]
+async-timeout = ">=3.0,<4.0"
+attrs = ">=17.3.0"
+chardet = ">=2.0,<4.0"
+multidict = ">=4.0,<5.0"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["aiodns", "brotlipy", "cchardet"]
+
+[[package]]
+category = "main"
+description = "Timeout context manager for asyncio programs"
+name = "async-timeout"
+optional = true
+python-versions = ">=3.5.3"
+version = "3.0.1"
+
+[[package]]
+category = "main"
+description = "Classes Without Boilerplate"
+name = "attrs"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "19.3.0"
+
+[package.extras]
+azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
+dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
+docs = ["sphinx", "zope.interface"]
+tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+
+[[package]]
+category = "main"
+description = "Python package for providing Mozilla's CA Bundle."
+name = "certifi"
+optional = true
+python-versions = "*"
+version = "2019.9.11"
+
+[[package]]
+category = "main"
+description = "Foreign Function Interface for Python calling C code."
+name = "cffi"
+optional = true
+python-versions = "*"
+version = "1.13.2"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+category = "main"
+description = "Universal encoding detector for Python 2 and 3"
+name = "chardet"
+optional = true
+python-versions = "*"
+version = "3.0.4"
+
+[[package]]
+category = "main"
+description = "Composable command line interface toolkit"
+name = "click"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "7.0"
+
+[[package]]
+category = "main"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+name = "cryptography"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+version = "2.8"
+
+[package.dependencies]
+cffi = ">=1.8,<1.11.3 || >1.11.3"
+six = ">=1.4.1"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
+docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+idna = ["idna (>=2.1)"]
+pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
+test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
+
+[[package]]
+category = "main"
+description = "Date parsing library designed to parse dates from HTML pages"
+name = "dateparser"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "0.7.2"
+
+[package.dependencies]
+python-dateutil = "*"
+pytz = "*"
+regex = "*"
+tzlocal = "*"
+
+[[package]]
+category = "main"
+description = "A python wrapper for the Discord API"
+name = "discord.py"
+optional = true
+python-versions = ">=3.5.3"
+version = "1.3.0a2122+g09a08f9"
+
+[package.dependencies]
+aiohttp = ">=3.3.0,<3.6.0"
+websockets = ">=8.0"
+
+[package.extras]
+docs = ["sphinx (1.8.5)", "sphinxcontrib_trio (1.1.0)", "sphinxcontrib-websupport"]
+voice = ["PyNaCl (1.3.0)"]
+
+[package.source]
+reference = "09a08f9a9f126aa1f55c2444eb70508d1d52f8d9"
+type = "git"
+url = "https://github.com/Steffo99/discord.py"
+[[package]]
+category = "main"
+description = "Python bindings for FFmpeg - with complex filtering support"
+name = "ffmpeg-python"
+optional = true
+python-versions = "*"
+version = "0.2.0"
+
+[package.dependencies]
+future = "*"
+
+[package.extras]
+dev = ["future (0.17.1)", "numpy (1.16.4)", "pytest-mock (1.10.4)", "pytest (4.6.1)", "Sphinx (2.1.0)", "tox (3.12.1)"]
+
+[[package]]
+category = "main"
+description = "Clean single-source support for Python 3 and 2"
+name = "future"
+optional = true
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "0.18.2"
+
+[[package]]
+category = "main"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+name = "h11"
+optional = true
+python-versions = "*"
+version = "0.8.1"
+
+[[package]]
+category = "main"
+description = "A collection of framework independent HTTP protocol utils."
+marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\""
+name = "httptools"
+optional = true
+python-versions = "*"
+version = "0.0.13"
+
+[[package]]
+category = "main"
+description = "Internationalized Domain Names in Applications (IDNA)"
+name = "idna"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.8"
+
+[[package]]
+category = "main"
+description = "multidict implementation"
+name = "multidict"
+optional = true
+python-versions = ">=3.4.1"
+version = "4.5.2"
+
+[[package]]
+category = "main"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+name = "psycopg2"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+version = "2.8.4"
+
+[[package]]
+category = "main"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+name = "psycopg2-binary"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+version = "2.8.4"
+
+[[package]]
+category = "main"
+description = "C parser in Python"
+name = "pycparser"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.19"
+
+[[package]]
+category = "main"
+description = "Python binding to the Networking and Cryptography (NaCl) library"
+name = "pynacl"
+optional = true
+python-versions = "*"
+version = "1.3.0"
+
+[package.dependencies]
+cffi = ">=1.4.1"
+six = "*"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
+tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"]
+
+[[package]]
+category = "main"
+description = "Extensions to the standard Python datetime module"
+name = "python-dateutil"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+version = "2.8.1"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+category = "main"
+description = "We have made you a wrapper you can't refuse"
+name = "python-telegram-bot"
+optional = true
+python-versions = "*"
+version = "12.2.0"
+
+[package.dependencies]
+certifi = "*"
+cryptography = "*"
+future = ">=0.16.0"
+tornado = ">=5.1"
+
+[package.extras]
+json = ["ujson"]
+socks = ["pysocks"]
+
+[[package]]
+category = "main"
+description = "World timezone definitions, modern and historical"
+name = "pytz"
+optional = false
+python-versions = "*"
+version = "2019.3"
+
+[[package]]
+category = "main"
+description = "Alternative regular expression module, to replace re."
+name = "regex"
+optional = false
+python-versions = "*"
+version = "2019.11.1"
+
+[[package]]
+category = "main"
+description = "Python client for Sentry (https://getsentry.com)"
+name = "sentry-sdk"
+optional = true
+python-versions = "*"
+version = "0.13.2"
+
+[package.dependencies]
+certifi = "*"
+urllib3 = ">=1.10.0"
+
+[package.extras]
+bottle = ["bottle (>=0.12.13)"]
+falcon = ["falcon (>=1.4)"]
+flask = ["flask (>=0.8)", "blinker (>=1.1)"]
+
+[[package]]
+category = "main"
+description = "Python 2 and 3 compatibility utilities"
+name = "six"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*"
+version = "1.13.0"
+
+[[package]]
+category = "main"
+description = "Database Abstraction Library"
+name = "sqlalchemy"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.3.11"
+
+[package.extras]
+mssql = ["pyodbc"]
+mssql_pymssql = ["pymssql"]
+mssql_pyodbc = ["pyodbc"]
+mysql = ["mysqlclient"]
+oracle = ["cx-oracle"]
+postgresql = ["psycopg2"]
+postgresql_pg8000 = ["pg8000"]
+postgresql_psycopg2binary = ["psycopg2-binary"]
+postgresql_psycopg2cffi = ["psycopg2cffi"]
+pymysql = ["pymysql"]
+
+[[package]]
+category = "main"
+description = "The little ASGI library that shines."
+name = "starlette"
+optional = true
+python-versions = ">=3.6"
+version = "0.12.13"
+
+[package.extras]
+full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
+
+[[package]]
+category = "main"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+name = "tornado"
+optional = true
+python-versions = ">= 3.5"
+version = "6.0.3"
+
+[[package]]
+category = "main"
+description = "tzinfo object for the local timezone"
+name = "tzlocal"
+optional = false
+python-versions = "*"
+version = "2.0.0"
+
+[package.dependencies]
+pytz = "*"
+
+[[package]]
+category = "main"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+name = "urllib3"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
+version = "1.25.7"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
+
+[[package]]
+category = "main"
+description = "The lightning-fast ASGI server."
+name = "uvicorn"
+optional = true
+python-versions = "*"
+version = "0.10.8"
+
+[package.dependencies]
+click = ">=7.0.0,<8.0.0"
+h11 = ">=0.8.0,<0.9.0"
+httptools = "0.0.13"
+uvloop = ">=0.14.0"
+websockets = ">=8.0.0,<9.0.0"
+
+[[package]]
+category = "main"
+description = "Fast implementation of asyncio event loop on top of libuv"
+marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\""
+name = "uvloop"
+optional = true
+python-versions = "*"
+version = "0.14.0"
+
+[[package]]
+category = "main"
+description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
+name = "websockets"
+optional = true
+python-versions = ">=3.6.1"
+version = "8.1"
+
+[[package]]
+category = "main"
+description = "Yet another URL library"
+name = "yarl"
+optional = true
+python-versions = ">=3.5.3"
+version = "1.3.0"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
+[[package]]
+category = "main"
+description = "YouTube video downloader"
+name = "youtube-dl"
+optional = true
+python-versions = "*"
+version = "2019.11.5"
+
+[extras]
+alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
+alchemy_hard = ["sqlalchemy", "psycopg2"]
+bard = ["ffmpeg_python", "youtube_dl"]
+constellation = ["starlette", "uvicorn"]
+discord = ["discord.py", "pynacl"]
+sentry = ["sentry_sdk"]
+telegram = ["python_telegram_bot"]
+
+[metadata]
+content-hash = "d101c51ae28aea2b4692767328e42026950bd3f920fdf6f6afca76bac40e41df"
+ python-versions = "^3.8"
+
+[metadata.files]
+aiohttp = [
+ {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-win32.whl", hash = "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4"},
+ {file = "aiohttp-3.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-win32.whl", hash = "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1"},
+ {file = "aiohttp-3.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-win32.whl", hash = "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc"},
+ {file = "aiohttp-3.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939"},
+ {file = "aiohttp-3.5.4.tar.gz", hash = "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf"},
+]
+async-timeout = [
+ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
+ {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
+]
+attrs = [
+ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
+ {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
+]
+certifi = [
+ {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"},
+ {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"},
+]
+cffi = [
+ {file = "cffi-1.13.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43"},
+ {file = "cffi-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396"},
+ {file = "cffi-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54"},
+ {file = "cffi-1.13.2-cp27-cp27m-win32.whl", hash = "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159"},
+ {file = "cffi-1.13.2-cp27-cp27m-win_amd64.whl", hash = "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97"},
+ {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579"},
+ {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc"},
+ {file = "cffi-1.13.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f"},
+ {file = "cffi-1.13.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858"},
+ {file = "cffi-1.13.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42"},
+ {file = "cffi-1.13.2-cp34-cp34m-win32.whl", hash = "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b"},
+ {file = "cffi-1.13.2-cp34-cp34m-win_amd64.whl", hash = "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20"},
+ {file = "cffi-1.13.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3"},
+ {file = "cffi-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25"},
+ {file = "cffi-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5"},
+ {file = "cffi-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c"},
+ {file = "cffi-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b"},
+ {file = "cffi-1.13.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04"},
+ {file = "cffi-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652"},
+ {file = "cffi-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57"},
+ {file = "cffi-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e"},
+ {file = "cffi-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"},
+ {file = "cffi-1.13.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410"},
+ {file = "cffi-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a"},
+ {file = "cffi-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12"},
+ {file = "cffi-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e"},
+ {file = "cffi-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a"},
+ {file = "cffi-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d"},
+ {file = "cffi-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3"},
+ {file = "cffi-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db"},
+ {file = "cffi-1.13.2-cp38-cp38-win32.whl", hash = "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506"},
+ {file = "cffi-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba"},
+ {file = "cffi-1.13.2.tar.gz", hash = "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346"},
+]
+chardet = [
+ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
+ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+]
+click = [
+ {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
+ {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
+]
+cryptography = [
+ {file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"},
+ {file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"},
+ {file = "cryptography-2.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad"},
+ {file = "cryptography-2.8-cp27-cp27m-win32.whl", hash = "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2"},
+ {file = "cryptography-2.8-cp27-cp27m-win_amd64.whl", hash = "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912"},
+ {file = "cryptography-2.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d"},
+ {file = "cryptography-2.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42"},
+ {file = "cryptography-2.8-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879"},
+ {file = "cryptography-2.8-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d"},
+ {file = "cryptography-2.8-cp34-abi3-manylinux2010_x86_64.whl", hash = "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9"},
+ {file = "cryptography-2.8-cp34-cp34m-win32.whl", hash = "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c"},
+ {file = "cryptography-2.8-cp34-cp34m-win_amd64.whl", hash = "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0"},
+ {file = "cryptography-2.8-cp35-cp35m-win32.whl", hash = "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf"},
+ {file = "cryptography-2.8-cp35-cp35m-win_amd64.whl", hash = "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793"},
+ {file = "cryptography-2.8-cp36-cp36m-win32.whl", hash = "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595"},
+ {file = "cryptography-2.8-cp36-cp36m-win_amd64.whl", hash = "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7"},
+ {file = "cryptography-2.8-cp37-cp37m-win32.whl", hash = "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff"},
+ {file = "cryptography-2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f"},
+ {file = "cryptography-2.8-cp38-cp38-win32.whl", hash = "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e"},
+ {file = "cryptography-2.8-cp38-cp38-win_amd64.whl", hash = "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13"},
+ {file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"},
+]
+dateparser = [
+ {file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"},
+ {file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"},
+]
+"discord.py" = []
+ffmpeg-python = [
+ {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
+ {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
+]
+future = [
+ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+h11 = [
+ {file = "h11-0.8.1-py2.py3-none-any.whl", hash = "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"},
+ {file = "h11-0.8.1.tar.gz", hash = "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208"},
+]
+httptools = [
+ {file = "httptools-0.0.13.tar.gz", hash = "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"},
+]
+idna = [
+ {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
+ {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
+]
+multidict = [
+ {file = "multidict-4.5.2-cp34-cp34m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73"},
+ {file = "multidict-4.5.2-cp34-cp34m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5"},
+ {file = "multidict-4.5.2-cp34-cp34m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d"},
+ {file = "multidict-4.5.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0"},
+ {file = "multidict-4.5.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351"},
+ {file = "multidict-4.5.2-cp34-cp34m-win32.whl", hash = "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a"},
+ {file = "multidict-4.5.2-cp34-cp34m-win_amd64.whl", hash = "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9"},
+ {file = "multidict-4.5.2-cp35-cp35m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3"},
+ {file = "multidict-4.5.2-cp35-cp35m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d"},
+ {file = "multidict-4.5.2-cp35-cp35m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036"},
+ {file = "multidict-4.5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3"},
+ {file = "multidict-4.5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1"},
+ {file = "multidict-4.5.2-cp35-cp35m-win32.whl", hash = "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941"},
+ {file = "multidict-4.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3"},
+ {file = "multidict-4.5.2-cp36-cp36m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"},
+ {file = "multidict-4.5.2-cp36-cp36m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a"},
+ {file = "multidict-4.5.2-cp36-cp36m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a"},
+ {file = "multidict-4.5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b"},
+ {file = "multidict-4.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc"},
+ {file = "multidict-4.5.2-cp36-cp36m-win32.whl", hash = "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b"},
+ {file = "multidict-4.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7"},
+ {file = "multidict-4.5.2-cp37-cp37m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd"},
+ {file = "multidict-4.5.2-cp37-cp37m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7"},
+ {file = "multidict-4.5.2-cp37-cp37m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0"},
+ {file = "multidict-4.5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014"},
+ {file = "multidict-4.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce"},
+ {file = "multidict-4.5.2-cp37-cp37m-win32.whl", hash = "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef"},
+ {file = "multidict-4.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1"},
+ {file = "multidict-4.5.2.tar.gz", hash = "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f"},
+]
+psycopg2 = [
+ {file = "psycopg2-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b"},
+ {file = "psycopg2-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0"},
+ {file = "psycopg2-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38"},
+ {file = "psycopg2-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6"},
+ {file = "psycopg2-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a"},
+ {file = "psycopg2-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4"},
+ {file = "psycopg2-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151"},
+ {file = "psycopg2-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b"},
+ {file = "psycopg2-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c"},
+ {file = "psycopg2-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d"},
+ {file = "psycopg2-2.8.4-cp38-cp38-win32.whl", hash = "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677"},
+ {file = "psycopg2-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f"},
+ {file = "psycopg2-2.8.4.tar.gz", hash = "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6"},
+]
+psycopg2-binary = [
+ {file = "psycopg2-binary-2.8.4.tar.gz", hash = "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9"},
+ {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b"},
+ {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309"},
+ {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e"},
+ {file = "psycopg2_binary-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29"},
+ {file = "psycopg2_binary-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49"},
+ {file = "psycopg2_binary-2.8.4-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881"},
+ {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e"},
+ {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"},
+ {file = "psycopg2_binary-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03"},
+ {file = "psycopg2_binary-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e"},
+ {file = "psycopg2_binary-2.8.4-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3"},
+ {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d"},
+ {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd"},
+ {file = "psycopg2_binary-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f"},
+ {file = "psycopg2_binary-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7"},
+ {file = "psycopg2_binary-2.8.4-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b"},
+ {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964"},
+ {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70"},
+ {file = "psycopg2_binary-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03"},
+ {file = "psycopg2_binary-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8"},
+ {file = "psycopg2_binary-2.8.4-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b"},
+ {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039"},
+ {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103"},
+ {file = "psycopg2_binary-2.8.4-cp38-cp38-win32.whl", hash = "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1"},
+ {file = "psycopg2_binary-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f"},
+]
+pycparser = [
+ {file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"},
+]
+pynacl = [
+ {file = "PyNaCl-1.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621"},
+ {file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39"},
+ {file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255"},
+ {file = "PyNaCl-1.3.0-cp27-cp27m-win32.whl", hash = "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f"},
+ {file = "PyNaCl-1.3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"},
+ {file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1"},
+ {file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e"},
+ {file = "PyNaCl-1.3.0-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1"},
+ {file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_i686.whl", hash = "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786"},
+ {file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415"},
+ {file = "PyNaCl-1.3.0-cp34-cp34m-win32.whl", hash = "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b"},
+ {file = "PyNaCl-1.3.0-cp34-cp34m-win_amd64.whl", hash = "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae"},
+ {file = "PyNaCl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310"},
+ {file = "PyNaCl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a"},
+ {file = "PyNaCl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20"},
+ {file = "PyNaCl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b"},
+ {file = "PyNaCl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56"},
+ {file = "PyNaCl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715"},
+ {file = "PyNaCl-1.3.0-cp38-cp38-win32.whl", hash = "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5"},
+ {file = "PyNaCl-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92"},
+ {file = "PyNaCl-1.3.0.tar.gz", hash = "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c"},
+]
+python-dateutil = [
+ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
+ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
+]
+python-telegram-bot = [
+ {file = "python-telegram-bot-12.2.0.tar.gz", hash = "sha256:346d42771c2b23384c59f5f41e05bd7e801a0ce118d8dcb95209bb73d5f694c5"},
+ {file = "python_telegram_bot-12.2.0-py2.py3-none-any.whl", hash = "sha256:3beee89cba3bc3217566c96199f04776dd25f541ac8992da27fd247b2d208a14"},
+]
+pytz = [
+ {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
+ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
+]
+regex = [
+ {file = "regex-2019.11.1-cp27-none-win32.whl", hash = "sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1"},
+ {file = "regex-2019.11.1-cp27-none-win_amd64.whl", hash = "sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96"},
+ {file = "regex-2019.11.1-cp35-none-win32.whl", hash = "sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7"},
+ {file = "regex-2019.11.1-cp35-none-win_amd64.whl", hash = "sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6"},
+ {file = "regex-2019.11.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59"},
+ {file = "regex-2019.11.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143"},
+ {file = "regex-2019.11.1-cp36-none-win32.whl", hash = "sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910"},
+ {file = "regex-2019.11.1-cp36-none-win_amd64.whl", hash = "sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a"},
+ {file = "regex-2019.11.1-cp37-none-win32.whl", hash = "sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2"},
+ {file = "regex-2019.11.1-cp37-none-win_amd64.whl", hash = "sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7"},
+ {file = "regex-2019.11.1-cp38-none-win32.whl", hash = "sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"},
+ {file = "regex-2019.11.1-cp38-none-win_amd64.whl", hash = "sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66"},
+ {file = "regex-2019.11.1.tar.gz", hash = "sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69"},
+]
+sentry-sdk = [
+ {file = "sentry-sdk-0.13.2.tar.gz", hash = "sha256:ff1fa7fb85703ae9414c8b427ee73f8363232767c9cd19158f08f6e4f0b58fc7"},
+ {file = "sentry_sdk-0.13.2-py2.py3-none-any.whl", hash = "sha256:09e1e8f00f22ea580348f83bbbd880adf40b29f1dec494a8e4b33e22f77184fb"},
+]
+six = [
+ {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"},
+ {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"},
+]
+sqlalchemy = [
+ {file = "SQLAlchemy-1.3.11.tar.gz", hash = "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"},
+]
+starlette = [
+ {file = "starlette-0.12.13.tar.gz", hash = "sha256:9597bc28e3c4659107c1c4a45ec32dc45e947d78fe56230222be673b2c36454a"},
+]
+tornado = [
+ {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"},
+ {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"},
+ {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"},
+ {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"},
+ {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"},
+ {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"},
+ {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"},
+]
+tzlocal = [
+ {file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"},
+ {file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"},
+]
+urllib3 = [
+ {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
+ {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
+]
+uvicorn = [
+ {file = "uvicorn-0.10.8.tar.gz", hash = "sha256:f4c34642618449f55e2bab8c6b22ff7615b520d2e7e23275be2ca894254327a3"},
+]
+uvloop = [
+ {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"},
+ {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"},
+ {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"},
+ {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"},
+ {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"},
+ {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"},
+ {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"},
+ {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
+ {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
+]
+websockets = [
+ {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
+ {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
+ {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
+ {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
+ {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
+ {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
+ {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
+ {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
+ {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
+ {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
+ {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
+]
+yarl = [
+ {file = "yarl-1.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320"},
+ {file = "yarl-1.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb"},
+ {file = "yarl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829"},
+ {file = "yarl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310"},
+ {file = "yarl-1.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f"},
+ {file = "yarl-1.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842"},
+ {file = "yarl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8"},
+ {file = "yarl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4"},
+ {file = "yarl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"},
+ {file = "yarl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0"},
+ {file = "yarl-1.3.0.tar.gz", hash = "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9"},
+]
+youtube-dl = [
+ {file = "youtube_dl-2019.11.5-py2.py3-none-any.whl", hash = "sha256:1314de17f0d41c0f1062c4942406b8e0558d14d44b32f9fce00272760a06455b"},
+ {file = "youtube_dl-2019.11.5.tar.gz", hash = "sha256:25324aab78df9a09b2ee34f642f116933134bc66ea629a778c1fffe05b66f733"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index bf573702..642497b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,5 @@
+# Remember to run `poetry update` after you edit this file!
+
[tool.poetry]
name = "royalnet"
version = "5.1a1"
@@ -18,30 +20,41 @@
[tool.poetry.dependencies]
python = "^3.8"
dateparser = "^0.7.2"
+ # telegram
python_telegram_bot = {version="^12.2.0", optional=true}
- discord_py = {git = "https://github.com/Rapptz/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods
+ # discord
+ "discord.py" = {git = "https://github.com/Steffo99/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods
pynacl = {version="^1.3.0", optional=true} # This requires libffi-dev and python3.*-dev to be installed on Linux systems
+ # bard
ffmpeg_python = {version="~0.2.0", optional=true}
youtube_dl = {version="*", optional=true}
+ # alchemy
sqlalchemy = {version="^1.3.10", optional=true}
psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
+ # constellation
starlette = {version="^0.12.13", optional=true}
+ uvicorn = {version="^0.10.7", optional=true}
+ # sentry
sentry_sdk = {version="~0.13.2", optional=true}
+ # herald
+ websockets = {version="^8.1", optional=true}
+
+# Development dependencies
+[tool.poetry.dev-dependencies]
+ pytest = "^5.2.1"
# Optional dependencies
[tool.poetry.extras]
telegram = ["python_telegram_bot"]
- discord = ["discord_py", "pynacl"]
+ discord = ["discord.py", "pynacl"]
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
alchemy_hard = ["sqlalchemy", "psycopg2"]
bard = ["ffmpeg_python", "youtube_dl"]
- constellation = ["starlette"]
+ constellation = ["starlette", "uvicorn"]
sentry = ["sentry_sdk"]
+ herald = ["websockets"]
-# Development dependencies
-[tool.poetry.dev-dependencies]
- # There are none
[build-system]
requires = ["poetry>=0.12"]
diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py
index 7d7d16bc..7c5a064b 100644
--- a/royalnet/alchemy/alchemy.py
+++ b/royalnet/alchemy/alchemy.py
@@ -1,4 +1,4 @@
-from typing import Set, Dict, Union, Optional
+from typing import Set, Dict, Union, Type
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.schema import Table
@@ -28,15 +28,17 @@ class Alchemy:
self._engine: Engine = create_engine(database_uri)
self._Base: DeclarativeMeta = declarative_base(bind=self._engine)
self._Session: sessionmaker = sessionmaker(bind=self._engine)
- self._tables: Dict[str, Table] = {}
+ self._tables: Dict[str, Type[Table]] = {}
for table in tables:
name = table.__name__
assert self._tables.get(name) is None
assert isinstance(name, str)
- self._tables[name] = type(name, (self._Base, table), {})
+ # noinspection PyTypeChecker
+ bound_table: Type[Table] = type(name, (self._Base, table), {})
+ self._tables[name] = bound_table
self._Base.metadata.create_all()
- def get(self, table: Union[str, type]) -> Optional[Table]:
+ def get(self, table: Union[str, type]) -> Type[Table]:
"""Get the table with a specified name or class.
Args:
diff --git a/royalnet/alchemy/errors.py b/royalnet/alchemy/errors.py
index 860900c9..dd96c310 100644
--- a/royalnet/alchemy/errors.py
+++ b/royalnet/alchemy/errors.py
@@ -1,4 +1,3 @@
-
class AlchemyException(Exception):
"""Base class for Alchemy exceptions."""
diff --git a/royalnet/alchemy/table_dfs.py b/royalnet/alchemy/table_dfs.py
index 99376d24..74d850e9 100644
--- a/royalnet/alchemy/table_dfs.py
+++ b/royalnet/alchemy/table_dfs.py
@@ -1,9 +1,9 @@
-from typing import Optional
+from typing import Type
from sqlalchemy.inspection import inspect
from sqlalchemy.schema import Table
-def table_dfs(starting_table: Table, ending_table: Table) -> tuple:
+def table_dfs(starting_table: Type[Table], ending_table: Type[Table]) -> tuple:
"""Depth-first-search for the path from the starting table to the ending table.
Returns:
diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py
index b4fc1cc3..6cbc9c77 100644
--- a/royalnet/commands/__init__.py
+++ b/royalnet/commands/__init__.py
@@ -2,7 +2,7 @@ from .commandinterface import CommandInterface
from .command import Command
from .commanddata import CommandData
from .commandargs import CommandArgs
-from .commanderrors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError
+from .errors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError
__all__ = [
"CommandInterface",
diff --git a/royalnet/commands/commandargs.py b/royalnet/commands/commandargs.py
index 69d7ef54..ccaaa904 100644
--- a/royalnet/commands/commandargs.py
+++ b/royalnet/commands/commandargs.py
@@ -1,16 +1,33 @@
import re
from typing import Pattern, AnyStr, Optional, Sequence, Union
-from .commanderrors import InvalidInputError
+from .errors import InvalidInputError
class CommandArgs(list):
- """An interface to access the arguments of a command with ease."""
+ """An interface to easily access the arguments of a command.
+
+ Inherits from :class:`list`."""
def __getitem__(self, item):
- """Arguments can be accessed with an array notation, such as ``args[0]``.
+ """Access arguments as if they were a :class:`list`.
Raises:
- royalnet.error.InvalidInputError: if the requested argument does not exist."""
+ royalnet.error.InvalidInputError: if the requested argument does not exist.
+
+ Examples:
+ ::
+
+ # /pasta spaghetti aldente
+ >>> self[0]
+ "spaghetti"
+ >>> self[1]
+ "aldente"
+ >>> self[2]
+ # InvalidInputError: Missing argument #3.
+ >>> self[0:2]
+ ["spaghetti", "aldente"]
+
+ """
if isinstance(item, int):
try:
return super().__getitem__(item)
@@ -33,7 +50,18 @@ class CommandArgs(list):
royalnet.error.InvalidInputError: if there are less than ``require_at_least`` arguments.
Returns:
- The space-joined string."""
+ The space-joined string.
+
+ Examples:
+ ::
+
+ # /pasta spaghetti aldente
+ >>> self.joined()
+ "spaghetti aldente"
+ >>> self.joined(require_at_least=3)
+ # InvalidInputError: Not enough arguments specified (minimum is 3).
+
+ """
if len(self) < require_at_least:
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
return " ".join(self)
@@ -63,7 +91,20 @@ class CommandArgs(list):
default: The value returned if the argument is missing.
Returns:
- Either the argument or the ``default`` value, defaulting to ``None``."""
+ Either the argument or the ``default`` value, defaulting to ``None``.
+
+ Examples:
+ ::
+
+ # /pasta spaghetti aldente
+ >>> self.optional(0)
+ "spaghetti"
+ >>> self.optional(2)
+ None
+ >>> self.optional(2, default="carbonara")
+ "carbonara"
+
+ """
try:
return self[index]
except InvalidInputError:
diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py
index 60a1eb5e..c46997e3 100644
--- a/royalnet/commands/commanddata.py
+++ b/royalnet/commands/commanddata.py
@@ -1,6 +1,6 @@
from typing import Dict, Callable
import warnings
-from .commanderrors import UnsupportedError
+from .errors import UnsupportedError
from .commandinterface import CommandInterface
from ..utils import asyncify
diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py
index 3ad62e03..9a539ecc 100644
--- a/royalnet/commands/commandinterface.py
+++ b/royalnet/commands/commandinterface.py
@@ -1,10 +1,10 @@
import typing
import asyncio
-from .commanderrors import UnsupportedError
+from .errors import UnsupportedError
if typing.TYPE_CHECKING:
from .command import Command
from ..alchemy import Alchemy
- from ..interfaces import GenericBot
+ from ..serf import GenericBot
class CommandInterface:
diff --git a/royalnet/commands/commanderrors.py b/royalnet/commands/errors.py
similarity index 100%
rename from royalnet/commands/commanderrors.py
rename to royalnet/commands/errors.py
diff --git a/royalnet/herald/__init__.py b/royalnet/herald/__init__.py
new file mode 100644
index 00000000..291c7088
--- /dev/null
+++ b/royalnet/herald/__init__.py
@@ -0,0 +1,26 @@
+from .config import Config
+from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError
+from .link import Link
+from .package import Package
+from .request import Request
+from .response import Response, ResponseSuccess, ResponseFailure
+from .server import Server
+from .broadcast import Broadcast
+
+
+__all__ = [
+ "Config",
+ "HeraldError",
+ "ConnectionClosedError",
+ "LinkError",
+ "InvalidServerResponseError",
+ "ServerError",
+ "Link",
+ "Package",
+ "Request",
+ "Response",
+ "ResponseSuccess",
+ "ResponseFailure",
+ "Server",
+ "Broadcast",
+]
diff --git a/royalnet/herald/broadcast.py b/royalnet/herald/broadcast.py
new file mode 100644
index 00000000..848aff9b
--- /dev/null
+++ b/royalnet/herald/broadcast.py
@@ -0,0 +1,26 @@
+import typing
+
+
+class Broadcast:
+ def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None):
+ super().__init__()
+ if msg_type is not None:
+ assert msg_type == self.__class__.__name__
+ self.msg_type = self.__class__.__name__
+ self.handler: str = handler
+ self.data: dict = data
+
+ def to_dict(self):
+ return self.__dict__
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ return cls(**d)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.handler == other.handler and self.data == other.data
+ return False
+
+ def __repr__(self):
+ return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})"
diff --git a/royalnet/herald/config.py b/royalnet/herald/config.py
new file mode 100644
index 00000000..1b9b1a44
--- /dev/null
+++ b/royalnet/herald/config.py
@@ -0,0 +1,34 @@
+class Config:
+ def __init__(self,
+ name: str,
+ address: str,
+ port: int,
+ secret: str,
+ secure: bool = False,
+ path: str = "/"):
+ if ":" in name:
+ raise ValueError("Herald names cannot contain colons (:)")
+ self.name = name
+
+ self.address = address
+
+ if port < 0 or port > 65535:
+ raise ValueError("No such port")
+ self.port = port
+
+ self.secure = secure
+
+ if ":" in secret:
+ raise ValueError("Herald secrets cannot contain colons (:)")
+ self.secret = secret
+
+ if not path.startswith("/"):
+ raise ValueError("Herald paths must start with a slash (/)")
+ self.path = path
+
+ @property
+ def url(self):
+ return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}"
+
+ def __repr__(self):
+ return f""
diff --git a/royalnet/herald/errors.py b/royalnet/herald/errors.py
new file mode 100644
index 00000000..268eb85a
--- /dev/null
+++ b/royalnet/herald/errors.py
@@ -0,0 +1,18 @@
+class HeraldError(Exception):
+ """A generic :py:mod:`royalherald` error."""
+
+
+class LinkError(HeraldError):
+ """An error for something that happened in a :py:class:`Link`."""
+
+
+class ServerError(HeraldError):
+ """An error for something that happened in a :py:class:`Server`."""
+
+
+class ConnectionClosedError(LinkError):
+ """The :py:class:`Link`'s connection was closed unexpectedly. The link can't be used anymore."""
+
+
+class InvalidServerResponseError(LinkError):
+ """The :py:class:`Server` sent invalid data to the :py:class:`Link`."""
diff --git a/royalnet/herald/link.py b/royalnet/herald/link.py
new file mode 100644
index 00000000..ce234282
--- /dev/null
+++ b/royalnet/herald/link.py
@@ -0,0 +1,179 @@
+import asyncio
+import websockets
+import uuid
+import functools
+import logging as _logging
+import typing
+from .package import Package
+from .request import Request
+from .response import Response, ResponseSuccess, ResponseFailure
+from .broadcast import Broadcast
+from .errors import ConnectionClosedError, InvalidServerResponseError
+from .config import Config
+
+
+log = _logging.getLogger(__name__)
+
+
+class PendingRequest:
+ def __init__(self, *, loop: asyncio.AbstractEventLoop = None):
+ if loop is None:
+ self.loop = asyncio.get_event_loop()
+ else:
+ self.loop = loop
+ self.event: asyncio.Event = asyncio.Event(loop=loop)
+ self.data: typing.Optional[dict] = None
+
+ def __repr__(self):
+ if self.event.is_set():
+ return f"<{self.__class__.__qualname__}: {self.data.__class__.__name__}>"
+ return f"<{self.__class__.__qualname__}>"
+
+ def set(self, data):
+ self.data = data
+ self.event.set()
+
+
+def requires_connection(func):
+ @functools.wraps(func)
+ async def new_func(self, *args, **kwargs):
+ await self.connect_event.wait()
+ return await func(self, *args, **kwargs)
+ return new_func
+
+
+def requires_identification(func):
+ @functools.wraps(func)
+ async def new_func(self, *args, **kwargs):
+ await self.identify_event.wait()
+ return await func(self, *args, **kwargs)
+ return new_func
+
+
+class Link:
+ def __init__(self, config: Config, request_handler, *,
+ loop: asyncio.AbstractEventLoop = None):
+ self.config: Config = config
+ self.nid: str = str(uuid.uuid4())
+ self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None
+ self.request_handler: typing.Callable[[typing.Union[Request, Broadcast]],
+ typing.Awaitable[Response]] = request_handler
+ self._pending_requests: typing.Dict[str, PendingRequest] = {}
+ if loop is None:
+ self._loop = asyncio.get_event_loop()
+ else:
+ self._loop = loop
+ self.error_event: asyncio.Event = asyncio.Event(loop=self._loop)
+ self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop)
+ self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop)
+
+ def __repr__(self):
+ if self.identify_event.is_set():
+ return f"<{self.__class__.__qualname__} (identified)>"
+ elif self.connect_event.is_set():
+ return f"<{self.__class__.__qualname__} (connected)>"
+ elif self.error_event.is_set():
+ return f"<{self.__class__.__qualname__} (error)>"
+ else:
+ return f"<{self.__class__.__qualname__} (disconnected)>"
+
+ async def connect(self):
+ """Connect to the :py:class:`royalnet.network.NetworkServer` at ``self.master_uri``."""
+ log.info(f"Connecting to {self.config.url}...")
+ self.websocket = await websockets.connect(self.config.url, loop=self._loop)
+ self.connect_event.set()
+ log.info(f"Connected!")
+
+ @requires_connection
+ async def receive(self) -> Package:
+ """Recieve a :py:class:`Package` from the :py:class:`Server`.
+
+ Raises:
+ :py:exc:`royalnet.network.royalnetlink.ConnectionClosedError` if the connection closes."""
+ try:
+ jbytes: bytes = await self.websocket.recv()
+ package: Package = Package.from_json_bytes(jbytes)
+ except websockets.ConnectionClosed:
+ self.error_event.set()
+ self.connect_event.clear()
+ self.identify_event.clear()
+ log.info(f"Connection to {self.config.url} was closed.")
+ # What to do now? Let's just reraise.
+ raise ConnectionClosedError()
+ if self.identify_event.is_set() and package.destination != self.nid:
+ raise InvalidServerResponseError("Package is not addressed to this NetworkLink.")
+ log.debug(f"Received package: {package}")
+ return package
+
+ @requires_connection
+ async def identify(self) -> None:
+ log.info(f"Identifying...")
+ await self.websocket.send(f"Identify {self.nid}:{self.config.name}:{self.config.secret}")
+ response: Package = await self.receive()
+ if not response.source == "":
+ raise InvalidServerResponseError("Received a non-service package before identification.")
+ if "type" not in response.data:
+ raise InvalidServerResponseError("Missing 'type' in response data")
+ if response.data["type"] == "error":
+ raise ConnectionClosedError(f"Identification error: {response.data['type']}")
+ assert response.data["type"] == "success"
+ self.identify_event.set()
+ log.info(f"Identified successfully!")
+
+ @requires_identification
+ async def send(self, package: Package):
+ await self.websocket.send(package.to_json_bytes())
+ log.debug(f"Sent package: {package}")
+
+ @requires_identification
+ async def broadcast(self, destination: str, broadcast: Broadcast) -> None:
+ package = Package(broadcast.to_dict(), source=self.nid, destination=destination)
+ await self.send(package)
+ log.debug(f"Sent broadcast: {broadcast}")
+
+ @requires_identification
+ async def request(self, destination: str, request: Request) -> Response:
+ if destination.startswith("*"):
+ raise ValueError("requests cannot have multiple destinations")
+ package = Package(request.to_dict(), source=self.nid, destination=destination)
+ request = PendingRequest(loop=self._loop)
+ self._pending_requests[package.source_conv_id] = request
+ await self.send(package)
+ log.debug(f"Sent request to {destination}: {request}")
+ await request.event.wait()
+ if request.data["type"] == "ResponseSuccess":
+ response: Response = ResponseSuccess.from_dict(request.data)
+ elif request.data["type"] == "ResponseFailure":
+ response: Response = ResponseFailure.from_dict(request.data)
+ else:
+ raise TypeError("Unknown response type")
+ log.debug(f"Received from {destination}: {request} -> {response}")
+ return response
+
+ async def run(self):
+ """Blockingly run the Link."""
+ log.debug(f"Running main client loop for {self.nid}.")
+ if self.error_event.is_set():
+ raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.")
+ while True:
+ if not self.connect_event.is_set():
+ await self.connect()
+ if not self.identify_event.is_set():
+ await self.identify()
+ package: Package = await self.receive()
+ # Package is a response
+ if package.destination_conv_id in self._pending_requests:
+ request = self._pending_requests[package.destination_conv_id]
+ request.set(package.data)
+ continue
+ # Package is a request
+ elif package.data["msg_type"] == "Request":
+ log.debug(f"Received request {package.source_conv_id}: {package}")
+ response: Response = await self.request_handler(Request.from_dict(package.data))
+ response_package: Package = package.reply(response.to_dict())
+ await self.send(response_package)
+ log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}")
+ # Package is a broadcast
+ elif package.data["msg_type"] == "Broadcast":
+ log.debug(f"Received broadcast {package.source_conv_id}: {package}")
+ await self.request_handler(Broadcast.from_dict(package.data))
diff --git a/royalnet/herald/package.py b/royalnet/herald/package.py
new file mode 100644
index 00000000..4f162233
--- /dev/null
+++ b/royalnet/herald/package.py
@@ -0,0 +1,113 @@
+import json
+import uuid
+import typing
+
+
+class Package:
+ """A ``royalherald`` package, the data type with which a :py:class:`Link` communicates with a :py:class:`Server` or
+ another Link.
+
+ Contains info about the source and the destination."""
+
+ def __init__(self,
+ data: dict,
+ *,
+ source: str,
+ destination: str,
+ source_conv_id: typing.Optional[str] = None,
+ destination_conv_id: typing.Optional[str] = None):
+ """Create a Package.
+
+ Parameters:
+ data: The data that should be sent.
+ source: The ``nid`` of the node that created this Package.
+ destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody.
+ source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
+ destination_conv_id: The conversation id of the node that this Package is a reply to."""
+ # TODO: something is not right in these type hints. Check them.
+ self.data: dict = data
+ self.source: str = source
+ self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
+ self.destination: str = destination
+ self.destination_conv_id: typing.Optional[str] = destination_conv_id
+
+ def __repr__(self):
+ return f"<{self.__class__.__qualname__} {self.source} ({self.source_conv_id}) to {self.destination} ({self.destination_conv_id}>"
+
+ def __eq__(self, other):
+ if isinstance(other, Package):
+ return (self.data == other.data) and \
+ (self.source == other.source) and \
+ (self.destination == other.destination) and \
+ (self.source_conv_id == other.source_conv_id) and \
+ (self.destination_conv_id == other.destination_conv_id)
+ return False
+
+ def reply(self, data) -> "Package":
+ """Reply to this Package with another Package.
+
+ Parameters:
+ data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
+
+ Returns:
+ The reply Package."""
+ return Package(data,
+ source=self.destination,
+ destination=self.source,
+ source_conv_id=self.destination_conv_id or str(uuid.uuid4()),
+ destination_conv_id=self.source_conv_id)
+
+ @staticmethod
+ def from_dict(d) -> "Package":
+ """Create a Package from a dictionary."""
+ if "source" not in d:
+ raise ValueError("Missing source field")
+ if "nid" not in d["source"]:
+ raise ValueError("Missing source.nid field")
+ if "conv_id" not in d["source"]:
+ raise ValueError("Missing source.conv_id field")
+ if "destination" not in d:
+ raise ValueError("Missing destination field")
+ if "nid" not in d["destination"]:
+ raise ValueError("Missing destination.nid field")
+ if "conv_id" not in d["destination"]:
+ raise ValueError("Missing destination.conv_id field")
+ if "data" not in d:
+ raise ValueError("Missing data field")
+ return Package(d["data"],
+ source=d["source"]["nid"],
+ destination=d["destination"]["nid"],
+ source_conv_id=d["source"]["conv_id"],
+ destination_conv_id=d["destination"]["conv_id"])
+
+ def to_dict(self) -> dict:
+ """Convert the Package into a dictionary."""
+ return {
+ "source": {
+ "nid": self.source,
+ "conv_id": self.source_conv_id
+ },
+ "destination": {
+ "nid": self.destination,
+ "conv_id": self.destination_conv_id
+ },
+ "data": self.data
+ }
+
+ @staticmethod
+ def from_json_string(string: str) -> "Package":
+ """Create a Package from a JSON string."""
+ return Package.from_dict(json.loads(string))
+
+ def to_json_string(self) -> str:
+ """Convert the Package into a JSON string."""
+ return json.dumps(self.to_dict())
+
+ @staticmethod
+ def from_json_bytes(b: bytes) -> "Package":
+ """Create a Package from UTF8-encoded JSON bytes."""
+ return Package.from_json_string(str(b, encoding="utf8"))
+
+ def to_json_bytes(self) -> bytes:
+ """Convert the Package into UTF8-encoded JSON bytes."""
+ return bytes(self.to_json_string(), encoding="utf8")
diff --git a/royalnet/herald/request.py b/royalnet/herald/request.py
new file mode 100644
index 00000000..9e760e67
--- /dev/null
+++ b/royalnet/herald/request.py
@@ -0,0 +1,30 @@
+import typing
+
+
+class Request:
+ """A request sent from a :py:class:`Link` to another.
+
+ It contains the name of the requested handler, in addition to the data."""
+
+ def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None):
+ super().__init__()
+ if msg_type is not None:
+ assert msg_type == self.__class__.__name__
+ self.msg_type = self.__class__.__name__
+ self.handler: str = handler
+ self.data: dict = data
+
+ def to_dict(self):
+ return self.__dict__
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ return cls(**d)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.handler == other.handler and self.data == other.data
+ return False
+
+ def __repr__(self):
+ return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})"
diff --git a/royalnet/herald/response.py b/royalnet/herald/response.py
new file mode 100644
index 00000000..73d830bb
--- /dev/null
+++ b/royalnet/herald/response.py
@@ -0,0 +1,50 @@
+import typing
+
+
+class Response:
+ """A base class to be inherited by all other response types."""
+
+ def to_dict(self) -> dict:
+ """Prepare the Response to be sent by converting it to a JSONable :py:class:`dict`."""
+ return {
+ "type": self.__class__.__name__,
+ **self.__dict__
+ }
+
+ def __eq__(self, other):
+ if isinstance(other, Response):
+ return self.to_dict() == other.to_dict()
+ return False
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "Response":
+ """Recreate the response from a received :py:class:`dict`."""
+ # Ignore type in dict
+ del d["type"]
+ # noinspection PyArgumentList
+ return cls(**d)
+
+
+class ResponseSuccess(Response):
+ """A response to a successful :py:class:`Request`."""
+
+ def __init__(self, data: typing.Optional[dict] = None):
+ if data is None:
+ self.data = {}
+ else:
+ self.data = data
+
+ def __repr__(self):
+ return f"{self.__class__.__qualname__}(data={self.data})"
+
+
+class ResponseFailure(Response):
+ """A response to a invalid :py:class:`Request`."""
+
+ def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None):
+ self.name: str = name
+ self.description: str = description
+ self.extra_info: typing.Optional[dict] = extra_info
+
+ def __repr__(self):
+ return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})"
diff --git a/royalnet/herald/server.py b/royalnet/herald/server.py
new file mode 100644
index 00000000..dff0bd29
--- /dev/null
+++ b/royalnet/herald/server.py
@@ -0,0 +1,153 @@
+import typing
+import websockets
+import re
+import datetime
+import uuid
+import asyncio
+import logging as _logging
+from .package import Package
+from .config import Config
+
+
+log = _logging.getLogger(__name__)
+
+
+class ConnectedClient:
+ """The :py:class:`Server`-side representation of a connected :py:class:`Link`."""
+ def __init__(self, socket: websockets.WebSocketServerProtocol):
+ self.socket: websockets.WebSocketServerProtocol = socket
+ self.nid: typing.Optional[str] = None
+ self.link_type: typing.Optional[str] = None
+ self.connection_datetime: datetime.datetime = datetime.datetime.now()
+
+ def __repr__(self):
+ return f"<{self.__class__.__qualname__} {self.nid}>"
+
+ @property
+ def is_identified(self) -> bool:
+ """Has the client sent a valid identification package?"""
+ return bool(self.nid)
+
+ async def send_service(self, msg_type: str, message: str):
+ await self.send(Package({"type": msg_type, "service": message},
+ source="",
+ destination=self.nid))
+
+ async def send(self, package: Package):
+ """Send a :py:class:`Package` to the :py:class:`Link`."""
+ await self.socket.send(package.to_json_bytes())
+
+
+class Server:
+ def __init__(self, config: Config, *, loop: asyncio.AbstractEventLoop = None):
+ self.config: Config = config
+ self.identified_clients: typing.List[ConnectedClient] = []
+ self.loop = loop
+
+ def __repr__(self):
+ return f"<{self.__class__.__qualname__}>"
+
+ def find_client(self, *, nid: str = None, link_type: str = None) -> typing.List[ConnectedClient]:
+ assert not (nid and link_type)
+ if nid:
+ matching = [client for client in self.identified_clients if client.nid == nid]
+ assert len(matching) <= 1
+ return matching
+ if link_type:
+ matching = [client for client in self.identified_clients if client.link_type == link_type]
+ return matching or []
+
+ async def listener(self, websocket: websockets.server.WebSocketServerProtocol, path):
+ log.info(f"{websocket.remote_address} connected to the server.")
+ connected_client = ConnectedClient(websocket)
+ # Wait for identification
+ identify_msg = await websocket.recv()
+ log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
+ if not isinstance(identify_msg, str):
+ await connected_client.send_service("error", "Invalid identification message (not a str)")
+ return
+ identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
+ if identification is None:
+ await connected_client.send_service("error", "Invalid identification message (regex failed)")
+ return
+ secret = identification.group(3)
+ if secret != self.config.secret:
+ await connected_client.send_service("error", "Invalid secret")
+ return
+ # Identification successful
+ connected_client.nid = identification.group(1)
+ connected_client.link_type = identification.group(2)
+ self.identified_clients.append(connected_client)
+ log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid}"
+ f" ({connected_client.link_type}).")
+ await connected_client.send_service("success", "Identification successful!")
+ log.debug(f"{connected_client.nid}'s identification confirmed.")
+ # Main loop
+ while True:
+ # Receive packages
+ raw_bytes = await websocket.recv()
+ package: Package = Package.from_json_bytes(raw_bytes)
+ log.debug(f"Received package: {package}")
+ # Check if the package destination is the server itself.
+ if package.destination == "":
+ # TODO: do stuff
+ pass
+ # Otherwise, route the package to its destination
+ # noinspection PyAsyncCall
+ self.loop.create_task(self.route_package(package))
+
+ def find_destination(self, package: Package) -> typing.List[ConnectedClient]:
+ """Find a list of destinations for the package.
+
+ Parameters:
+ package: The package to find the destination of.
+
+ Returns:
+ A :py:class:`list` of :py:class:`ConnectedClient` to send the package to."""
+ # Parse destination
+ # Is it nothing?
+ if package.destination == "":
+ return []
+ # Is it all possible destinations?
+ if package.destination == "*":
+ return self.identified_clients
+ # Is it a valid nid?
+ try:
+ destination = str(uuid.UUID(package.destination))
+ except ValueError:
+ pass
+ else:
+ return self.find_client(nid=destination)
+ # Is it a link_type?
+ return self.find_client(link_type=package.destination)
+
+ async def route_package(self, package: Package) -> None:
+ """Executed every time a package is received and must be routed somewhere."""
+ destinations = self.find_destination(package)
+ log.debug(f"Routing package: {package} -> {destinations}")
+ for destination in destinations:
+ # This may have some consequences
+ specific_package = Package(package.data,
+ source=package.source,
+ destination=destination.nid,
+ source_conv_id=package.source_conv_id,
+ destination_conv_id=package.destination_conv_id)
+ await destination.send(specific_package)
+
+ def serve(self):
+ if self.config.secure:
+ raise Exception("Secure servers aren't supported yet")
+ log.debug(f"Serving on {self.config.url}")
+ self.loop.run_until_complete(self.run())
+ self.loop.run_forever()
+
+ async def run(self):
+ await websockets.serve(self.listener,
+ host=self.config.address,
+ port=self.config.port,
+ loop=self.loop)
+
+ def run_blocking(self):
+ if self.loop is None:
+ self.loop = asyncio.get_event_loop()
+ self.serve()
diff --git a/royalnet/interfaces/__init__.py b/royalnet/serf/__init__.py
similarity index 84%
rename from royalnet/interfaces/__init__.py
rename to royalnet/serf/__init__.py
index fdedb173..b34fa4f4 100644
--- a/royalnet/interfaces/__init__.py
+++ b/royalnet/serf/__init__.py
@@ -1,6 +1,6 @@
"""Various bot interfaces, and a common class to create new ones."""
-from .interface import GenericBot
+from .serf import GenericBot
from .telegram import TelegramBot
from .discord import DiscordBot
diff --git a/royalnet/serf/alchemyconfig.py b/royalnet/serf/alchemyconfig.py
new file mode 100644
index 00000000..ba6d7b88
--- /dev/null
+++ b/royalnet/serf/alchemyconfig.py
@@ -0,0 +1,12 @@
+from typing import Type
+from sqlalchemy.schema import Table
+
+
+class AlchemyConfig:
+ """A helper class to configure :class:`Alchemy` in a :class:`Serf`."""
+ def __init__(self,
+ master_table: Type[Table],
+ identity_table: Type[Table],
+ ):
+ self.master_table: Type[Table] = master_table
+ self.identity_table: Type[Table] = identity_table
\ No newline at end of file
diff --git a/royalnet/interfaces/discord/__init__.py b/royalnet/serf/discord/__init__.py
similarity index 100%
rename from royalnet/interfaces/discord/__init__.py
rename to royalnet/serf/discord/__init__.py
diff --git a/royalnet/interfaces/discord/create_rich_embed.py b/royalnet/serf/discord/create_rich_embed.py
similarity index 100%
rename from royalnet/interfaces/discord/create_rich_embed.py
rename to royalnet/serf/discord/create_rich_embed.py
diff --git a/royalnet/interfaces/discord/discord.py b/royalnet/serf/discord/discord.py
similarity index 99%
rename from royalnet/interfaces/discord/discord.py
rename to royalnet/serf/discord/discord.py
index 313fe622..24df3140 100644
--- a/royalnet/interfaces/discord/discord.py
+++ b/royalnet/serf/discord/discord.py
@@ -31,7 +31,7 @@ class DiscordBot(GenericBot):
def _interface_factory(self) -> typing.Type[CommandInterface]:
# noinspection PyPep8Naming
- GenericInterface = super()._interface_factory()
+ GenericInterface = super().interface_factory()
# noinspection PyMethodParameters,PyAbstractClass
class DiscordInterface(GenericInterface):
diff --git a/royalnet/interfaces/discord/escape.py b/royalnet/serf/discord/escape.py
similarity index 100%
rename from royalnet/interfaces/discord/escape.py
rename to royalnet/serf/discord/escape.py
diff --git a/royalnet/interfaces/discord/fileaudiosource.py b/royalnet/serf/discord/fileaudiosource.py
similarity index 100%
rename from royalnet/interfaces/discord/fileaudiosource.py
rename to royalnet/serf/discord/fileaudiosource.py
diff --git a/royalnet/interfaces/discord/playmodes.py b/royalnet/serf/discord/playmodes.py
similarity index 100%
rename from royalnet/interfaces/discord/playmodes.py
rename to royalnet/serf/discord/playmodes.py
diff --git a/royalnet/interfaces/discord/ytdldiscord.py b/royalnet/serf/discord/ytdldiscord.py
similarity index 100%
rename from royalnet/interfaces/discord/ytdldiscord.py
rename to royalnet/serf/discord/ytdldiscord.py
diff --git a/royalnet/interfaces/interface.py b/royalnet/serf/serf.py
similarity index 50%
rename from royalnet/interfaces/interface.py
rename to royalnet/serf/serf.py
index 73d38cf7..204a8c6f 100644
--- a/royalnet/interfaces/interface.py
+++ b/royalnet/serf/serf.py
@@ -1,86 +1,101 @@
-import sys
import asyncio
import logging
+from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union
import sentry_sdk
import keyring
-import royalnet.version
-import royalherald as rh
+from royalnet.herald import Response, ResponseSuccess, Broadcast, ResponseFailure, Request, Link
+from royalnet.herald import Config as HeraldConfig
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
-from ..utils import *
-from ..alchemy import *
-from ..commands import *
-from ..error import *
+from royalnet.commands import Command, CommandInterface, CommandData, CommandError, UnsupportedError
+from royalnet.alchemy import Alchemy
+from .alchemyconfig import AlchemyConfig
log = logging.getLogger(__name__)
-class Interface:
- """A common bot class, to be used as base for the other more specific classes, such as
- :py:class:`royalnet.bots.TelegramBot` and :py:class:`royalnet.bots.DiscordBot`. """
+class Serf:
+ """An abstract class, to be used as base to implement Royalnet bots on multiple interfaces (such as Telegram or
+ Discord)."""
interface_name = NotImplemented
- def _init_commands(self) -> None:
- """Generate the ``packs`` dictionary required to handle incoming messages, and the ``network_handlers``
- dictionary required to handle incoming requests. """
- log.info(f"Registering packs...")
- self._Interface = self._interface_factory()
- self._Data = self._data_factory()
- self.commands = {}
- self.network_handlers: typing.Dict[str, typing.Callable[["Interface", typing.Any],
- typing.Awaitable[typing.Optional[typing.Dict]]]] = {}
- for SelectedCommand in self.uninitialized_commands:
- interface = self._Interface()
- try:
- command = SelectedCommand(interface)
- except Exception as e:
- log.error(f"{e.__class__.__qualname__} during the registration of {SelectedCommand.__qualname__}")
- sentry_sdk.capture_exception(e)
- continue
- # Linking the command to the interface
- interface.command = command
- # Override the main command name, but warn if it's overriding something
- if f"{interface.prefix}{SelectedCommand.name}" in self.commands:
- log.warning(f"Overriding (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}")
- else:
- log.debug(f"Registering: {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}")
- self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command
- # Register aliases, but don't override anything
- for alias in SelectedCommand.aliases:
- if f"{interface.prefix}{alias}" not in self.commands:
- log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
- self.commands[f"{interface.prefix}{alias}"] = self.commands[f"{interface.prefix}{SelectedCommand.name}"]
- else:
- log.info(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
+ def __init__(self, *,
+ alchemy_config: Optional[AlchemyConfig] = None,
+ commands: List[Type[Command]] = None,
+ network_config: Optional[HeraldConfig] = None,
+ sentry_dsn: Optional[str] = None,
+ loop: asyncio.AbstractEventLoop = None,
+ secrets_name: str = "__default__"):
+ self.secrets_name = secrets_name
- def _interface_factory(self) -> typing.Type[CommandInterface]:
- # noinspection PyAbstractClass,PyMethodParameters
+ if alchemy_config is not None:
+ self.init_alchemy(alchemy_config)
+
+ self.Interface: Type[CommandInterface] = self.interface_factory()
+ """The :class:`CommandInterface` class of this Serf."""
+
+ self.Data: Type[CommandData] = self.data_factory()
+ """The :class:`CommandData` class of this Serf."""
+
+ self.commands: Dict[str, Command] = {}
+ """The :class:`dict` connecting each command name to its :class:`Command` object."""
+
+ if commands is None:
+ commands = []
+ self.register_commands(commands)
+
+ self.herald_handlers: Dict[str, Callable[["Serf", Any], Awaitable[Optional[dict]]]] = {}
+ """A :class:`dict` linking :class:`Request` event names to coroutines returning a :class:`dict` that will be
+ sent as :class:`Response` to the event."""
+
+ self.herald: Optional[Link] = None
+ """The :class:`Link` object connecting the Serf to the rest of the herald network."""
+
+ self.herald_task: Optional[asyncio.Task] = None
+ """A reference to the :class:`asyncio.Task` that runs the :class:`Link`."""
+
+ if network_config is not None:
+ self.init_network(network_config)
+
+ def interface_factory(self) -> Type[CommandInterface]:
+ """Create the :class:`CommandInterface` class for the Serf."""
+ # noinspection PyMethodParameters
class GenericInterface(CommandInterface):
- alchemy = self.alchemy
- bot = self
- loop = self.loop
+ alchemy: Alchemy = self.alchemy
+ bot: "Serf" = self
+ loop: asyncio.AbstractEventLoop = self.loop
def register_herald_action(ci,
event_name: str,
- coroutine: typing.Callable[[typing.Any], typing.Awaitable[typing.Dict]]) -> None:
- self.network_handlers[event_name] = coroutine
+ coroutine: Callable[[Any], Awaitable[Dict]]) -> None:
+ """Allow a coroutine to be called when a :class:`royalherald.Request` is received."""
+ if self.herald is None:
+ raise UnsupportedError("`royalherald` is not enabled on this bot.")
+ self.herald_handlers[event_name] = coroutine
def unregister_herald_action(ci, event_name: str):
- del self.network_handlers[event_name]
+ """Disable a previously registered coroutine from being called on reception of a
+ :class:`royalherald.Request`."""
+ if self.herald is None:
+ raise UnsupportedError("`royalherald` is not enabled on this bot.")
+ del self.herald_handlers[event_name]
- async def call_herald_action(ci, destination: str, event_name: str, args: typing.Dict) -> typing.Dict:
- if self.network is None:
- raise UnsupportedError("Herald is not enabled on this bot")
- request: rh.Request = rh.Request(handler=event_name, data=args)
- response: rh.Response = await self.network.request(destination=destination, request=request)
- if isinstance(response, rh.ResponseFailure):
+ async def call_herald_action(ci, destination: str, event_name: str, args: Dict) -> Dict:
+ """Send a :class:`royalherald.Request` to a specific destination, and wait for a
+ :class:`royalherald.Response`."""
+ if self.herald is None:
+ raise UnsupportedError("`royalherald` is not enabled on this bot.")
+ request: Request = Request(handler=event_name, data=args)
+ response: Response = await self.herald.request(destination=destination, request=request)
+ if isinstance(response, ResponseFailure):
if response.extra_info["type"] == "CommandError":
raise CommandError(response.extra_info["message"])
- raise CommandError(f"Herald action call failed:\n"
- f"[p]{response}[/p]")
- elif isinstance(response, rh.ResponseSuccess):
+ # TODO: change exception type
+ raise Exception(f"Herald action call failed:\n"
+ f"[p]{response}[/p]")
+ elif isinstance(response, ResponseSuccess):
return response.data
else:
raise TypeError(f"Other Herald Link returned unknown response:\n"
@@ -88,41 +103,78 @@ class Interface:
return GenericInterface
- def _data_factory(self) -> typing.Type[CommandData]:
+ def data_factory(self) -> Type[CommandData]:
+ """Create the :class:`CommandData` for the Serf."""
raise NotImplementedError()
- def _init_network(self):
- """Create a :py:class:`royalherald.Link`, and run it as a :py:class:`asyncio.Task`."""
- if self.uninitialized_network_config is not None:
- self.network: rh.Link = rh.Link(self.uninitialized_network_config.master_uri,
- self.uninitialized_network_config.master_secret,
- self.interface_name,
- self._network_handler)
- log.debug(f"Running NetworkLink {self.network}")
- self.loop.create_task(self.network.run())
+ def register_commands(self, commands: List[Type[Command]]) -> None:
+ """Initialize and register all commands passed as argument.
- async def _network_handler(self, message: typing.Union[rh.Request, rh.Broadcast]) -> rh.Response:
+ If called again during the execution of the bot, all current commands will be replaced with the new ones.
+
+ Warning:
+ Hot-replacing commands was never tested and probably doesn't work."""
+ log.info(f"Registering {len(commands)} commands...")
+ # Instantiate the Commands
+ for SelectedCommand in commands:
+ # Warn if the command would be overriding something
+ if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands:
+ log.warning(f"Overriding (already defined): "
+ f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
+ else:
+ log.debug(f"Registering: "
+ f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
+ # Create a new interface
+ interface = self.Interface()
+ # Try to instantiate the command
+ try:
+ command = SelectedCommand(interface)
+ except Exception as e:
+ log.error(f"Skipping: "
+ f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.")
+ sentry_sdk.capture_exception(e)
+ continue
+ # Link the interface to the command
+ interface.command = command
+ # Register the command in the commands dict
+ self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command
+ # Register aliases, but don't override anything
+ for alias in SelectedCommand.aliases:
+ if f"{interface.prefix}{alias}" not in self.commands:
+ log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
+ self.commands[f"{interface.prefix}{alias}"] = \
+ self.commands[f"{interface.prefix}{SelectedCommand.name}"]
+ else:
+ log.info(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
+
+ def init_network(self, config: HeraldConfig):
+ """Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`."""
+ log.debug(f"Initializing herald...")
+ self.herald: Link = Link(config, self._network_handler)
+ self.herald_task = self.loop.create_task(self.herald.run())
+
+ async def _network_handler(self, message: Union[Request, Broadcast]) -> Response:
try:
- network_handler = self.network_handlers[message.handler]
+ network_handler = self.herald_handlers[message.handler]
except KeyError:
log.warning(f"Missing network_handler for {message.handler}")
- return rh.ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.")
+ return ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.")
else:
log.debug(f"Using {network_handler} as handler for {message.handler}")
- if isinstance(message, rh.Request):
+ if isinstance(message, Request):
try:
response_data = await network_handler(self, **message.data)
- return rh.ResponseSuccess(data=response_data)
+ return ResponseSuccess(data=response_data)
except Exception as e:
sentry_sdk.capture_exception(e)
log.error(f"Exception {e} in {network_handler}")
- return rh.ResponseFailure("exception_in_handler",
- f"An exception was raised in {network_handler} for {message.handler}.",
- extra_info={
- "type": e.__class__.__name__,
- "message": str(e)
- })
- elif isinstance(message, rh.Broadcast):
+ return ResponseFailure("exception_in_handler",
+ f"An exception was raised in {network_handler} for {message.handler}.",
+ extra_info={
+ "type": e.__class__.__name__,
+ "message": str(e)
+ })
+ elif isinstance(message, Broadcast):
await network_handler(self, **message.data)
def _init_database(self):
@@ -173,21 +225,6 @@ class Interface:
else:
self.loop = self.uninitialized_loop
- def __init__(self, *,
- network_config: typing.Optional[rh.Config] = None,
- database_config: typing.Optional[DatabaseConfig] = None,
- commands: typing.List[typing.Type[Command]] = None,
- sentry_dsn: typing.Optional[str] = None,
- loop: asyncio.AbstractEventLoop = None,
- secrets_name: str = "__default__"):
- self.initialized = False
- self.uninitialized_network_config = network_config
- self.uninitialized_database_config = database_config
- self.uninitialized_commands = commands
- self.uninitialized_sentry_dsn = sentry_dsn
- self.uninitialized_loop = loop
- self.secrets_name = secrets_name
-
def get_secret(self, username: str):
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
@@ -200,7 +237,7 @@ class Interface:
self._init_loop()
self._init_database()
self._init_commands()
- self._init_network()
+ self.init_network()
self.initialized = True
def run(self):
diff --git a/royalnet/interfaces/telegram/__init__.py b/royalnet/serf/telegram/__init__.py
similarity index 100%
rename from royalnet/interfaces/telegram/__init__.py
rename to royalnet/serf/telegram/__init__.py
diff --git a/royalnet/interfaces/telegram/escape.py b/royalnet/serf/telegram/escape.py
similarity index 100%
rename from royalnet/interfaces/telegram/escape.py
rename to royalnet/serf/telegram/escape.py
diff --git a/royalnet/interfaces/telegram/telegram.py b/royalnet/serf/telegram/telegram.py
similarity index 99%
rename from royalnet/interfaces/telegram/telegram.py
rename to royalnet/serf/telegram/telegram.py
index 03dd4638..e5d683b4 100644
--- a/royalnet/interfaces/telegram/telegram.py
+++ b/royalnet/serf/telegram/telegram.py
@@ -29,7 +29,7 @@ class TelegramBot(GenericBot):
def _interface_factory(self) -> typing.Type[CommandInterface]:
# noinspection PyPep8Naming
- GenericInterface = super()._interface_factory()
+ GenericInterface = super().interface_factory()
# noinspection PyMethodParameters,PyAbstractClass
class TelegramInterface(GenericInterface):