diff --git a/.gitignore b/.gitignore index 01ed3d1f..cad48a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Royalnet ignores config*.toml downloads/ - +markov/ # Python ignores **/__pycache__/ @@ -11,4 +11,3 @@ dist/ # PyCharm ignores .idea/ - diff --git a/README.md b/README.md index 3ce0b202..bb419be5 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,84 @@ - - # `royalpack` -## Commands +## Configuration -### `ciaoruozi` +```toml +[Packs."royalpack"] -Saluta Ruozi, un leggendario essere che una volta era in User Games. +# The main Telegram group +Telegram.main_group_id = -1001153723135 -### `color` +# The main Discord channel +Discord.main_channel_id = 566023556618518538 -Invia un colore in chat...? +# A Imgur API token (https://apidocs.imgur.com/?version=latest) +Imgur.token = "1234567890abcde" -### `cv` +# A Steam Web API key (https://steamcommunity.com/dev/apikey) +Steam.web_api_key = "123567890ABCDEF123567890ABCDEF12" -Elenca le persone attualmente connesse alla chat vocale. +# The Peertube instance you want to use for new video notifications +Peertube.instance_url = "https://pt.steffo.eu" -### `diario` +# The delay in seconds between two new video checks +Peertube.feed_update_timeout = 300 -Aggiungi una citazione al Diario. +# The Funkwhale instance you want to use for the fw commands +Funkwhale.instance_url = "https://fw.steffo.eu" -### `rage` +# The id of the role that users should have to be displayed by default in cv +Cv.displayed_role_id = 424549048561958912 -Arrabbiati per qualcosa, come una software house californiana. +# The max duration of a song downloaded with the play commands +Play.max_song_duration = 7230 -> Aliases: `balurage` `madden` +# The Telegram channel where matchmaking messages should be sent in +Matchmaking.mm_chat_id = -1001204402796 -### `reminder` +[Packs."royalpack"."steampowered"] +token = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" -Ti ricorda di fare qualcosa dopo un po' di tempo. +[Packs."royalpack"."steampowered".updater] +enabled = false +period = 86400 +delay = 1 +target = -1001153723135 -> Aliases: `calendar` +[Packs."royalpack"."dota".updater] +enabled = true +period = 86400 +delay = 1 +target = -1001153723135 -### `ship` +[Packs."royalpack"."brawlhalla"] +token = "1234567890ABCDEFGHJKLMNOPQRST" -Crea una ship tra due nomi. +[Packs."royalpack"."brawlhalla".updater] +enabled = true +period = 86400 +delay = 1 +target = -1001153723135 -### `smecds` +[Packs."royalpack"."leagueoflegends"] +token = "RGAPI-AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" +region = "euw1" -Secondo me, è colpa dello stagista... +[Packs."royalpack"."leagueoflegends".updater] +enabled = true +period = 86400 +delay = 1 +target = -1001153723135 -> Aliases: `secondomeecolpadellostagista` +[Packs."royalpack"."osu"] +client_id = 123456789 +client_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" -### `videochannel` - -Converti il canale vocale in un canale video. - -> Aliases: `golive` `live` `video` - -### `pause` - -Metti in pausa o riprendi la riproduzione di un file. - -> Aliases: `resume` - -### `play` - -Aggiunge un url alla coda della chat vocale. - -> Aliases: `p` - -### `queue` - -Visualizza la coda di riproduzione attuale.. - -> Aliases: `q` - -### `skip` - -Salta il file attualmente in riproduzione. - -> Aliases: `s` - -### `summon` - -Evoca il bot in un canale vocale. - -> Aliases: `cv` - -### `youtube` - -Cerca un video su YouTube e lo aggiunge alla coda della chat vocale. - -> Aliases: `yt` - -### `soundcloud` - -Cerca un video su SoundCloud e lo aggiunge alla coda della chat vocale. - -> Aliases: `sc` - -### `emojify` - -Converti un messaggio in emoji. - -### `leagueoflegends` - -Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche. - -> Aliases: `lol` `league` - -### `diarioquote` - -Cita una riga del diario. - -> Aliases: `dq` `quote` `dquote` - -### `peertube` - -Guarda quando è uscito l'ultimo video su RoyalTube. - -### `googlevideo` - -Cerca un video su Google Video e lo aggiunge alla coda della chat vocale. - -> Aliases: `gv` - -### `yahoovideo` - -Cerca un video su Yahoo Video e lo aggiunge alla coda della chat vocale. - -> Aliases: `yv` - -### `userinfo` - -Visualizza informazioni su un utente. - -> Aliases: `uinfo` `ui` `useri` - -### `spell` - -Genera casualmente una spell! - -### `ahnonlosoio` - -Ah, non lo so io! - -### `eat` - -Mangia qualcosa! - -### `pmots` - -Confondi Proto! - -## Events - -### `discord_cv` - -### `discord_summon` - -### `discord_play` - -### `discord_skip` - -### `discord_queue` - -### `discord_pause` - -## Page Stars - -### `/api/user/list` - -### `/api/user/get/{uid_str}` - -### `/api/diario/list` - -### `/api/diario/get/{diario_id}` - -## Exception Stars - -## Tables - -### `diario` - -### `aliases` - -### `wikipages` - -Wiki page properties. - - Warning: - Requires PostgreSQL! - -### `wikirevisions` - -A wiki page revision. - - Warning: - Requires PostgreSQL! - -### `bios` - -### `reminder` - -### `triviascores` - -### `mmevents` - -### `mmresponse` - -### `leagueoflegends` +[Packs."royalpack"."osu".login] +enabled = false +[Packs."royalpack"."osu".updater] +enabled = true +period = 86400 +delay = 5 +target = -1001153723135 +``` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 14d8f8eb..378d7827 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,13 +4,13 @@ description = "Async http client/server framework (asyncio)" name = "aiohttp" optional = false python-versions = ">=3.5.3" -version = "3.5.4" +version = "3.6.2" [package.dependencies] async-timeout = ">=3.0,<4.0" attrs = ">=17.3.0" chardet = ">=2.0,<4.0" -multidict = ">=4.0,<5.0" +multidict = ">=4.5,<5.0" yarl = ">=1.0,<2.0" [package.extras] @@ -38,13 +38,36 @@ dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.int docs = ["sphinx", "zope.interface"] tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +[[package]] +category = "main" +description = "Modern password hashing for your software and your servers" +name = "bcrypt" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.1.7" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"] + +[[package]] +category = "main" +description = "Extensible memoizing collections and decorators" +name = "cachetools" +optional = false +python-versions = "~=3.5" +version = "4.1.1" + [[package]] category = "main" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.11.28" +version = "2020.6.20" [[package]] category = "main" @@ -52,7 +75,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = false python-versions = "*" -version = "1.13.2" +version = "1.14.0" [package.dependencies] pycparser = "*" @@ -70,8 +93,8 @@ category = "main" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" [[package]] category = "main" @@ -102,18 +125,19 @@ category = "main" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." name = "cryptography" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.8" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.0" [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"] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "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"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +ssh = ["bcrypt (>=3.1.5)"] 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]] @@ -122,14 +146,66 @@ 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" +version = "0.7.6" [package.dependencies] python-dateutil = "*" pytz = "*" -regex = "*" +regex = "!=2019.02.19" tzlocal = "*" +[[package]] +category = "main" +description = "Decorators for Humans" +name = "decorator" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.2" + +[[package]] +category = "main" +description = "A library to handle automated deprecations" +name = "deprecation" +optional = false +python-versions = "*" +version = "2.1.0" + +[package.dependencies] +packaging = "*" + +[[package]] +category = "main" +description = "A Python wrapper for the Discord API" +name = "discord.py" +optional = false +python-versions = ">=3.5.3" +version = "1.3.4" + +[package.dependencies] +aiohttp = ">=3.6.0,<3.7.0" +websockets = ">=6.0,<7.0 || >7.0,<8.0 || >8.0,<8.0.1 || >8.0.1,<9.0" + +[package.extras] +docs = ["sphinx (1.8.5)", "sphinxcontrib-trio (1.1.1)", "sphinxcontrib-websupport"] +voice = ["PyNaCl (1.3.0)"] + +[[package]] +category = "main" +description = "Python audio data toolkit (ID3 and MP3)" +name = "eyed3" +optional = false +python-versions = "*" +version = "0.9.5" + +[package.dependencies] +deprecation = "*" +filetype = "*" + +[package.extras] +art-plugin = ["pylast", "requests", "pillow"] +display-plugin = ["grako"] +yaml-plugin = ["ruamel.yaml"] + [[package]] category = "main" description = "Python bindings for FFmpeg - with complex filtering support" @@ -144,6 +220,14 @@ 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 = "Infer file type and MIME type of any file/buffer. No external dependencies." +name = "filetype" +optional = false +python-versions = "*" +version = "1.0.7" + [[package]] category = "main" description = "Clean single-source support for Python 3 and 2" @@ -158,7 +242,7 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" name = "h11" optional = false python-versions = "*" -version = "0.8.1" +version = "0.9.0" [[package]] category = "main" @@ -174,8 +258,8 @@ category = "main" description = "Human friendly output for text interfaces using Python" name = "humanfriendly" optional = false -python-versions = "*" -version = "4.18" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "8.2" [package.dependencies] pyreadline = "*" @@ -186,7 +270,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.10" [[package]] category = "main" @@ -194,7 +278,19 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.6.1" +version = "4.7.6" + +[[package]] +category = "main" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" [[package]] category = "main" @@ -202,7 +298,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" name = "psycopg2-binary" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.8.4" +version = "2.8.5" [[package]] category = "main" @@ -210,15 +306,23 @@ description = "C parser in Python" name = "pycparser" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.19" +version = "2.20" + +[[package]] +category = "main" +description = "Cryptographic library for Python" +name = "pycryptodomex" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.9.8" [[package]] category = "main" description = "Python binding to the Networking and Cryptography (NaCl) library" name = "pynacl" optional = false -python-versions = "*" -version = "1.3.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" [package.dependencies] cffi = ">=1.4.1" @@ -228,6 +332,14 @@ six = "*" 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 = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + [[package]] category = "main" description = "A python implmementation of GNU readline." @@ -265,12 +377,12 @@ description = "We have made you a wrapper you can't refuse" name = "python-telegram-bot" optional = false python-versions = "*" -version = "12.2.0" +version = "12.8" [package.dependencies] certifi = "*" cryptography = "*" -future = ">=0.16.0" +decorator = ">=4.4.0" tornado = ">=5.1" [package.extras] @@ -283,7 +395,7 @@ description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.3" +version = "2020.1" [[package]] category = "main" @@ -291,7 +403,7 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2019.12.9" +version = "2020.7.14" [[package]] category = "main" @@ -299,16 +411,16 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] @@ -317,7 +429,7 @@ description = "RiotWatcher is a thin wrapper on top of the Riot Games API for Le name = "riotwatcher" optional = false python-versions = "*" -version = "2.7.1" +version = "3.0.0" [package.dependencies] requests = "*" @@ -331,16 +443,28 @@ description = "A multipurpose bot and web framework" name = "royalnet" optional = false python-versions = ">=3.8,<4.0" -version = "5.1.6" +version = "5.10.4" [package.dependencies] dateparser = ">=0.7.2,<0.8.0" toml = ">=0.10.0,<0.11.0" +[package.dependencies.bcrypt] +optional = true +version = ">=3.1.7,<4.0.0" + [package.dependencies.coloredlogs] optional = true version = ">=10.0,<11.0" +[package.dependencies."discord.py"] +optional = true +version = ">=1.3.1,<2.0.0" + +[package.dependencies.eyed3] +optional = true +version = ">=0.9,<0.10" + [package.dependencies.ffmpeg_python] optional = true version = ">=0.2.0,<0.3.0" @@ -367,16 +491,12 @@ version = ">=0.13.2,<0.14.0" [package.dependencies.sqlalchemy] optional = true -version = ">=1.3.10,<2.0.0" +version = ">=1.3.18,<2.0.0" [package.dependencies.starlette] optional = true version = ">=0.12.13,<0.13.0" -[package.dependencies.temp_discordpy_without_websockets_requirement] -optional = true -version = "0.1" - [package.dependencies.uvicorn] optional = true version = ">=0.10.7,<0.11.0" @@ -390,13 +510,14 @@ optional = true version = "*" [package.extras] -alchemy_easy = ["sqlalchemy (>=1.3.10,<2.0.0)", "psycopg2_binary (>=2.8.4,<3.0.0)"] -alchemy_hard = ["sqlalchemy (>=1.3.10,<2.0.0)", "psycopg2 (>=2.8.4,<3.0.0)"] -bard = ["ffmpeg_python (>=0.2.0,<0.3.0)", "youtube-dl"] +alchemy_easy = ["sqlalchemy (>=1.3.18,<2.0.0)", "psycopg2_binary (>=2.8.4,<3.0.0)", "bcrypt (>=3.1.7,<4.0.0)"] +alchemy_hard = ["sqlalchemy (>=1.3.18,<2.0.0)", "psycopg2 (>=2.8.4,<3.0.0)", "bcrypt (>=3.1.7,<4.0.0)"] +bard = ["ffmpeg_python (>=0.2.0,<0.3.0)", "youtube-dl", "eyed3 (>=0.9,<0.10)"] coloredlogs = ["coloredlogs (>=10.0,<11.0)"] constellation = ["starlette (>=0.12.13,<0.13.0)", "uvicorn (>=0.10.7,<0.11.0)", "python-multipart (>=0.0.5,<0.0.6)"] -discord = ["temp_discordpy_without_websockets_requirement (0.1)", "pynacl (>=1.3.0,<2.0.0)"] +discord = ["discord.py (>=1.3.1,<2.0.0)", "pynacl (>=1.3.0,<2.0.0)"] herald = ["websockets (>=8.1,<9.0)"] +matrix = ["matrix-nio (>=0.6,<0.7)"] sentry = ["sentry_sdk (>=0.13.2,<0.14.0)"] telegram = ["python_telegram_bot (>=12.2.0,<13.0.0)"] @@ -439,8 +560,8 @@ 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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" [[package]] category = "main" @@ -448,7 +569,7 @@ description = "Database Abstraction Library" name = "sqlalchemy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.11" +version = "1.3.18" [package.extras] mssql = ["pyodbc"] @@ -475,19 +596,21 @@ full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "p [[package]] category = "main" -description = "A python wrapper for the Discord API" -name = "temp-discordpy-without-websockets-requirement" +description = "Module for interacting with various Steam features" +name = "steam" optional = false -python-versions = ">=3.5.3" -version = "0.1" +python-versions = "*" +version = "1.0.2" [package.dependencies] -aiohttp = ">=3.3.0,<3.6.0" -websockets = ">=8.0" +cachetools = ">=3.0.0" +pycryptodomex = ">=3.7.0" +requests = ">=2.9.1" +six = ">=1.10" +vdf = ">=3.3" [package.extras] -docs = ["sphinx (1.8.5)", "sphinxcontrib-trio (1.1.0)", "sphinxcontrib-websupport"] -voice = ["PyNaCl (1.3.0)"] +client = ["gevent (>=1.3.0)", "protobuf (>=3.0.0)", "gevent-eventemitter (>=2.1)"] [[package]] category = "main" @@ -495,7 +618,7 @@ description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false python-versions = "*" -version = "0.10.0" +version = "0.10.1" [[package]] category = "main" @@ -503,7 +626,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib name = "tornado" optional = false python-versions = ">= 3.5" -version = "6.0.3" +version = "6.0.4" [[package]] category = "main" @@ -511,7 +634,7 @@ description = "tzinfo object for the local timezone" name = "tzlocal" optional = false python-versions = "*" -version = "2.0.0" +version = "2.1" [package.dependencies] pytz = "*" @@ -521,12 +644,12 @@ category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" [package.extras] brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] @@ -535,11 +658,11 @@ description = "The lightning-fast ASGI server." name = "uvicorn" optional = false python-versions = "*" -version = "0.10.8" +version = "0.10.9" [package.dependencies] click = ">=7.0.0,<8.0.0" -h11 = ">=0.8.0,<0.9.0" +h11 = ">=0.9.0,<0.10.0" httptools = "0.0.13" uvloop = ">=0.14.0" websockets = ">=8.0.0,<9.0.0" @@ -553,6 +676,14 @@ optional = false python-versions = "*" version = "0.14.0" +[[package]] +category = "main" +description = "Library for working with Valve's VDF text format" +name = "vdf" +optional = false +python-versions = "*" +version = "3.3" + [[package]] category = "main" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" @@ -579,36 +710,27 @@ description = "YouTube video downloader" name = "youtube-dl" optional = false python-versions = "*" -version = "2019.11.28" +version = "2020.6.16.1" [metadata] -content-hash = "131063cd0294061fef5c403e5c71281f0ca7109576fedfcbbc15540b6a3ac1f9" +content-hash = "6186dd82fb955af7343f43eaddc27c2eb6b9abe24b53e34bc41891e264658a89" +lock-version = "1.0" 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"}, + {file = "aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e"}, + {file = "aiohttp-3.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec"}, + {file = "aiohttp-3.6.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48"}, + {file = "aiohttp-3.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win32.whl", hash = "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17"}, + {file = "aiohttp-3.6.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a"}, + {file = "aiohttp-3.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win32.whl", hash = "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654"}, + {file = "aiohttp-3.6.2-py3-none-any.whl", hash = "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4"}, + {file = "aiohttp-3.6.2.tar.gz", hash = "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326"}, ] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, @@ -618,52 +740,71 @@ attrs = [ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, ] +bcrypt = [ + {file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"}, + {file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"}, + {file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"}, + {file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"}, + {file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"}, + {file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"}, + {file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"}, + {file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"}, +] +cachetools = [ + {file = "cachetools-4.1.1-py3-none-any.whl", hash = "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98"}, + {file = "cachetools-4.1.1.tar.gz", hash = "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20"}, +] certifi = [ - {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, - {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] 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"}, + {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"}, + {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"}, + {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"}, + {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"}, + {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"}, + {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"}, + {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"}, + {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"}, + {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"}, + {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"}, + {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"}, + {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"}, + {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"}, + {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"}, + {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"}, + {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, ] 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"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, @@ -674,132 +815,185 @@ coloredlogs = [ {file = "coloredlogs-10.0.tar.gz", hash = "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36"}, ] 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"}, + {file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, + {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, + {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, + {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, + {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, + {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, + {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, + {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, + {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, + {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, + {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, + {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, + {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, + {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, + {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, + {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, + {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, + {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, + {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, ] dateparser = [ - {file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"}, - {file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"}, + {file = "dateparser-0.7.6-py2.py3-none-any.whl", hash = "sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b"}, + {file = "dateparser-0.7.6.tar.gz", hash = "sha256:e875efd8c57c85c2d02b238239878db59ff1971f5a823457fcc69e493bf6ebfa"}, +] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +deprecation = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] +"discord.py" = [ + {file = "discord.py-1.3.4-py3-none-any.whl", hash = "sha256:8ef58d6fc1e66903bc00ae79c4c09a38aa71043e88a83da4d2e8b9b1c9f9b9e2"}, + {file = "discord.py-1.3.4.tar.gz", hash = "sha256:1b546a32c0cd83d949392a71e5b06e30e19d1067246e3826d32ae9b8b3d06c1e"}, +] +eyed3 = [ + {file = "eyeD3-0.9.5-py2.py3-none-any.whl", hash = "sha256:94d475c0b55d9227a7f885f0be0f8433da9de6e5037e5164a524b042e78a2b62"}, + {file = "eyeD3-0.9.5-py3.8.egg", hash = "sha256:5e517b8c0eb36d5225e9bc3e85ab340e211a0887dfb83507a1dd8f586bf55df9"}, + {file = "eyeD3-0.9.5.tar.gz", hash = "sha256:faf5806197f2093e82c2830d41f2378f07b3a9da07a16fafb14fc6fbdebac50a"}, ] 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"}, ] +filetype = [ + {file = "filetype-1.0.7-py2.py3-none-any.whl", hash = "sha256:353369948bb1c09b8b3ea3d78390b5586e9399bff9aab894a1dff954e31a66f6"}, + {file = "filetype-1.0.7.tar.gz", hash = "sha256:da393ece8d98b47edf2dd5a85a2c8733e44b769e32c71af4cd96ed8d38d96aa7"}, +] 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"}, + {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, + {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, ] httptools = [ {file = "httptools-0.0.13.tar.gz", hash = "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"}, ] humanfriendly = [ - {file = "humanfriendly-4.18-py2.py3-none-any.whl", hash = "sha256:23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d"}, - {file = "humanfriendly-4.18.tar.gz", hash = "sha256:33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"}, + {file = "humanfriendly-8.2-py2.py3-none-any.whl", hash = "sha256:e78960b31198511f45fd455534ae7645a6207d33e512d2e842c766d15d9c8080"}, + {file = "humanfriendly-8.2.tar.gz", hash = "sha256:bf52ec91244819c780341a3438d5d7b09f431d3f113a475147ac9b7b167a3d12"}, ] idna = [ - {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, - {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] multidict = [ - {file = "multidict-4.6.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:318aadf1cfb6741c555c7dd83d94f746dc95989f4f106b25b8a83dfb547f2756"}, - {file = "multidict-4.6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c890978e2b37dd0dc1bd952da9a5d9f245d4807bee33e3517e4119c48d66f8c"}, - {file = "multidict-4.6.1-cp35-cp35m-win32.whl", hash = "sha256:efaf1b18ea6c1f577b1371c0159edbe4749558bfe983e13aa24d0a0c01e1ad7b"}, - {file = "multidict-4.6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:07f9a6bf75ad675d53956b2c6a2d4ef2fa63132f33ecc99e9c24cf93beb0d10b"}, - {file = "multidict-4.6.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:42cdd649741a14b0602bf15985cad0dd4696a380081a3319cd1ead46fd0f0fab"}, - {file = "multidict-4.6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:205a011e636d885af6dd0029e41e3514a46e05bb2a43251a619a6e8348b96fc0"}, - {file = "multidict-4.6.1-cp36-cp36m-win32.whl", hash = "sha256:cfec9d001a83dc73580143f3c77e898cf7ad78b27bb5e64dbe9652668fcafec7"}, - {file = "multidict-4.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8d919034420378132d074bf89df148d0193e9780c9fe7c0e495e895b8af4d8a2"}, - {file = "multidict-4.6.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a37433ce8cdb35fc9e6e47e1606fa1bfd6d70440879038dca7d8dd023197eaa9"}, - {file = "multidict-4.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b605272c558e4c659dbaf0fb32a53bfede44121bcf77b356e6e906867b958b7"}, - {file = "multidict-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:891b7e142885e17a894d9d22b0349b92bb2da4769b4e675665d0331c08719be5"}, - {file = "multidict-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:250632316295f2311e1ed43e6b26a63b0216b866b45c11441886ac1543ca96e1"}, - {file = "multidict-4.6.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:2bc9c2579312c68a3552ee816311c8da76412e6f6a9cf33b15152e385a572d2a"}, - {file = "multidict-4.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0ffe4d4d28cbe9801952bfb52a8095dd9ffecebd93f84bdf973c76300de783c5"}, - {file = "multidict-4.6.1-cp38-cp38-win32.whl", hash = "sha256:87e26d8b89127c25659e962c61a4c655ec7445d19150daea0759516884ecb8b4"}, - {file = "multidict-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:c626029841ada34c030b94a00c573a0c7575fe66489cde148785b6535397d675"}, - {file = "multidict-4.6.1.tar.gz", hash = "sha256:5159c4975931a1a78bf6602bbebaa366747fce0a56cb2111f44789d2c45e379f"}, + {file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"}, + {file = "multidict-4.7.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a"}, + {file = "multidict-4.7.6-cp35-cp35m-win32.whl", hash = "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5"}, + {file = "multidict-4.7.6-cp35-cp35m-win_amd64.whl", hash = "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3"}, + {file = "multidict-4.7.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87"}, + {file = "multidict-4.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2"}, + {file = "multidict-4.7.6-cp36-cp36m-win32.whl", hash = "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7"}, + {file = "multidict-4.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463"}, + {file = "multidict-4.7.6-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"}, + {file = "multidict-4.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255"}, + {file = "multidict-4.7.6-cp37-cp37m-win32.whl", hash = "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507"}, + {file = "multidict-4.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c"}, + {file = "multidict-4.7.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b"}, + {file = "multidict-4.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7"}, + {file = "multidict-4.7.6-cp38-cp38-win32.whl", hash = "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d"}, + {file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"}, + {file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] 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"}, + {file = "psycopg2-binary-2.8.5.tar.gz", hash = "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6"}, + {file = "psycopg2_binary-2.8.5-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:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38"}, + {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389"}, + {file = "psycopg2_binary-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9"}, + {file = "psycopg2_binary-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04"}, + {file = "psycopg2_binary-2.8.5-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:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4"}, + {file = "psycopg2_binary-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb"}, + {file = "psycopg2_binary-2.8.5-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:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1"}, + {file = "psycopg2_binary-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162"}, + {file = "psycopg2_binary-2.8.5-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:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3"}, + {file = "psycopg2_binary-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-win32.whl", hash = "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa"}, + {file = "psycopg2_binary-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"}, ] pycparser = [ - {file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"}, + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pycryptodomex = [ + {file = "pycryptodomex-3.9.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761"}, + {file = "pycryptodomex-3.9.8-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1714675fb4ac29a26ced38ca22eb8ffd923ac851b7a6140563863194d7158422"}, + {file = "pycryptodomex-3.9.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00"}, + {file = "pycryptodomex-3.9.8-cp27-cp27m-win32.whl", hash = "sha256:9fd758e5e2fe02d57860b85da34a1a1e7037155c4eadc2326fc7af02f9cae214"}, + {file = "pycryptodomex-3.9.8-cp27-cp27m-win_amd64.whl", hash = "sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c"}, + {file = "pycryptodomex-3.9.8-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32"}, + {file = "pycryptodomex-3.9.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2199708ebeed4b82eb45b10e1754292677f5a0df7d627ee91ea01290b9bab7e6"}, + {file = "pycryptodomex-3.9.8-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68"}, + {file = "pycryptodomex-3.9.8-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:06f5a458624c9b0e04c0086c7f84bcc578567dab0ddc816e0476b3057b18339f"}, + {file = "pycryptodomex-3.9.8-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34"}, + {file = "pycryptodomex-3.9.8-cp35-cp35m-win32.whl", hash = "sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677"}, + {file = "pycryptodomex-3.9.8-cp35-cp35m-win_amd64.whl", hash = "sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda"}, + {file = "pycryptodomex-3.9.8-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:93a75d1acd54efed314b82c952b39eac96ce98d241ad7431547442e5c56138aa"}, + {file = "pycryptodomex-3.9.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594"}, + {file = "pycryptodomex-3.9.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"}, + {file = "pycryptodomex-3.9.8-cp36-cp36m-win32.whl", hash = "sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71"}, + {file = "pycryptodomex-3.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9"}, + {file = "pycryptodomex-3.9.8-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:a2bc4e1a2e6ca3a18b2e0be6131a23af76fecb37990c159df6edc7da6df913e3"}, + {file = "pycryptodomex-3.9.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be"}, + {file = "pycryptodomex-3.9.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f"}, + {file = "pycryptodomex-3.9.8-cp37-cp37m-win32.whl", hash = "sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259"}, + {file = "pycryptodomex-3.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:17272d06e4b2f6455ee2cbe93e8eb50d9450a5dc6223d06862ee1ea5d1235861"}, + {file = "pycryptodomex-3.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46"}, + {file = "pycryptodomex-3.9.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48"}, + {file = "pycryptodomex-3.9.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e"}, + {file = "pycryptodomex-3.9.8-cp38-cp38-win32.whl", hash = "sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489"}, + {file = "pycryptodomex-3.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2"}, + {file = "pycryptodomex-3.9.8-cp39-cp39-manylinux1_i686.whl", hash = "sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11"}, + {file = "pycryptodomex-3.9.8-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060"}, + {file = "pycryptodomex-3.9.8.tar.gz", hash = "sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9"}, ] 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"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pyreadline = [ {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, @@ -814,37 +1008,47 @@ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] 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"}, + {file = "python-telegram-bot-12.8.tar.gz", hash = "sha256:327186c56469216207dcdf8706892e58e0a62e51ef46f5143268e387bbb4edc3"}, + {file = "python_telegram_bot-12.8-py2.py3-none-any.whl", hash = "sha256:7eebed539ccacf77896cff9e41d1f68746b8ff3ca4da1e2e59285e9c749cb050"}, ] pytz = [ - {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, - {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, ] regex = [ - {file = "regex-2019.12.9-cp27-none-win32.whl", hash = "sha256:40b7d1291a56897927e08bb973f8c186c2feb14c7f708bfe7aaee09483e85a20"}, - {file = "regex-2019.12.9-cp27-none-win_amd64.whl", hash = "sha256:c203c9ee755e9656d0af8fab82754d5a664ebaf707b3f883c7eff6a3dd5151cf"}, - {file = "regex-2019.12.9-cp35-none-win32.whl", hash = "sha256:719978a9145d59fc78509ea1d1bb74243f93583ef2a34dcc5623cf8118ae9726"}, - {file = "regex-2019.12.9-cp35-none-win_amd64.whl", hash = "sha256:75cf3796f89f75f83207a5c6a6e14eaf57e0369ef0ffff8e22bf36bbcfa0f1de"}, - {file = "regex-2019.12.9-cp36-none-win32.whl", hash = "sha256:3dbd8333fd2ebd50977ac8747385a73aa1f546eb6b16fcd83d274470fe11f243"}, - {file = "regex-2019.12.9-cp36-none-win_amd64.whl", hash = "sha256:ad9e3c7260809c0d1ded100269f78ea0217c0704f1eaaf40a382008461848b45"}, - {file = "regex-2019.12.9-cp37-none-win32.whl", hash = "sha256:91235c98283d2bddf1a588f0fbc2da8afa37959294bbd18b76297bdf316ba4d6"}, - {file = "regex-2019.12.9-cp37-none-win_amd64.whl", hash = "sha256:aaffd68c4c1ed891366d5c390081f4bf6337595e76a157baf453603d8e53fbcb"}, - {file = "regex-2019.12.9-cp38-none-win32.whl", hash = "sha256:e865bc508e316a3a09d36c8621596e6599a203bc54f1cd41020a127ccdac468a"}, - {file = "regex-2019.12.9-cp38-none-win_amd64.whl", hash = "sha256:77396cf80be8b2a35db863cca4c1a902d88ceeb183adab328b81184e71a5eafe"}, - {file = "regex-2019.12.9.tar.gz", hash = "sha256:77a3799152951d6d14ae5720ca162c97c64f85d4755da585418eac216b736cad"}, + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, ] requests = [ - {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, - {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] riotwatcher = [ - {file = "riotwatcher-2.7.1-py2.py3-none-any.whl", hash = "sha256:3fb03b20f768cea7830d54c6d301a8341c67223625ac8536d26d1c140af3dcb0"}, - {file = "riotwatcher-2.7.1.tar.gz", hash = "sha256:5349271c7e00637b7619491a6070e66603705db60558ea2a690e7016f6e6d9a4"}, + {file = "riotwatcher-3.0.0-py2.py3-none-any.whl", hash = "sha256:f00346692cd05c82450d7cfbdbf94e0de9553179daf612d08d1ffb163a091945"}, + {file = "riotwatcher-3.0.0.tar.gz", hash = "sha256:bf8b8eb7e13c794730c18cd7846513cf22295a068204433117ce4d83a25b0fd2"}, ] royalnet = [ - {file = "royalnet-5.1.6-py3-none-any.whl", hash = "sha256:4d64607fafbd94b82e8de47f9e0c83ae60d0558c682850d05a4e90ce0f9e9081"}, - {file = "royalnet-5.1.6.tar.gz", hash = "sha256:bfdbf55895162dd0ef7cd18f4260e1574f0e5569cef9fc3d069338c71f52e18d"}, + {file = "royalnet-5.10.4-py3-none-any.whl", hash = "sha256:664b81400d58078d6759d23f0c9a6e231f6499ddeeb93e19a7f1043d6892fb17"}, + {file = "royalnet-5.10.4.tar.gz", hash = "sha256:4b28bc90bcae0257dd48386fd637d83c953a5bb8ef785c0a39c126ac6e560c28"}, ] royalspells = [ {file = "royalspells-3.2.tar.gz", hash = "sha256:2bd4a9a66514532e35c02c3907425af48c7cb292364c4843c795719a82b25dfe"}, @@ -854,43 +1058,71 @@ sentry-sdk = [ {file = "sentry_sdk-0.13.5-py2.py3-none-any.whl", hash = "sha256:05285942901d38c7ce2498aba50d8e87b361fc603281a5902dda98f3f8c5e145"}, ] six = [ - {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, - {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.3.11.tar.gz", hash = "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-win32.whl", hash = "sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-win_amd64.whl", hash = "sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-win32.whl", hash = "sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-win_amd64.whl", hash = "sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-win32.whl", hash = "sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-win_amd64.whl", hash = "sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-win32.whl", hash = "sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-win_amd64.whl", hash = "sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-win32.whl", hash = "sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl", hash = "sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8"}, + {file = "SQLAlchemy-1.3.18.tar.gz", hash = "sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7"}, ] starlette = [ {file = "starlette-0.12.13.tar.gz", hash = "sha256:9597bc28e3c4659107c1c4a45ec32dc45e947d78fe56230222be673b2c36454a"}, ] -temp-discordpy-without-websockets-requirement = [ - {file = "temp_discordpy_without_websockets_requirement-0.1-py3-none-any.whl", hash = "sha256:361ddcc6fb2ec469706fde56c46a3e14f02671cf0e27204ef6c3c4d89c7227ab"}, - {file = "temp_discordpy_without_websockets_requirement-0.1.tar.gz", hash = "sha256:7db02603cde084c7d579d872389f60ffd3f34197d885e4ca722a92bc302cb33c"}, +steam = [ + {file = "steam-1.0.2.tar.gz", hash = "sha256:ca7c026848338ca2a004c0cb73d362c94e5c1a74815c8605d34fce3a6662f9c0"}, ] toml = [ - {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, - {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, - {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] 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"}, + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] tzlocal = [ - {file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"}, - {file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"}, + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, ] urllib3 = [ - {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, - {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, ] uvicorn = [ - {file = "uvicorn-0.10.8.tar.gz", hash = "sha256:f4c34642618449f55e2bab8c6b22ff7615b520d2e7e23275be2ca894254327a3"}, + {file = "uvicorn-0.10.9-py3-none-any.whl", hash = "sha256:dc7119b28e15c4c737315c5a570081b0a5a7d8d5c1e8a70a7be70043d88b23a7"}, + {file = "uvicorn-0.10.9.tar.gz", hash = "sha256:c010df69d16e27f1a18481316325b4fd23f562c1fac050915fc03a397d0f6b64"}, ] uvloop = [ {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, @@ -903,6 +1135,10 @@ uvloop = [ {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, ] +vdf = [ + {file = "vdf-3.3-py2.py3-none-any.whl", hash = "sha256:f88db3a3e66e7264da7fdacf0ffa1d99be52dd30510b2c1a1340171b227472e4"}, + {file = "vdf-3.3.tar.gz", hash = "sha256:9193901ce20ee08391c1d5044234b5da021d939b00ce879f5aae0a1a759577bc"}, +] 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"}, @@ -947,6 +1183,6 @@ yarl = [ {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, ] youtube-dl = [ - {file = "youtube_dl-2019.11.28-py2.py3-none-any.whl", hash = "sha256:43d6b991a34934fec2be20b93d4774ccc8642db1e5f0b8f1569f53b8d769af48"}, - {file = "youtube_dl-2019.11.28.tar.gz", hash = "sha256:de2254dc38472b0e0e3ac4d1e95a9e636ec790af83e7b055e1e09f03eb61bb5c"}, + {file = "youtube_dl-2020.6.16.1-py2.py3-none-any.whl", hash = "sha256:e54b307048bb18164729fb278013af6d5477c69c3d995147205a16f22a61296b"}, + {file = "youtube_dl-2020.6.16.1.tar.gz", hash = "sha256:9fc0389a1bbbeb609a5bb4ad5630dea107a9d1a24c73721c611a78c234309a75"}, ] diff --git a/publish.bat b/publish.bat new file mode 100644 index 00000000..f0813a03 --- /dev/null +++ b/publish.bat @@ -0,0 +1,2 @@ +git commit -am "publish: %1" +git push && poetry build && poetry publish && hub release create "%1" -m "Royalnet %1" diff --git a/pyproject.toml b/pyproject.toml index 01b5f50b..9dbe6bf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.poetry] name = "royalpack" - version = "5.1.9" + version = "5.13.4" description = "A Royalnet command pack for the Royal Games community" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" @@ -20,11 +20,14 @@ [tool.poetry.dependencies] python = "^3.8" - riotwatcher = "^2.7.1" + riotwatcher = "^3.0.0" royalspells = "^3.2" + steam = "*" + sqlalchemy = "^1.3.18" + bcrypt = "^3.1.7" [tool.poetry.dependencies.royalnet] - version = "^5.1.6" + version = "~5.10.4" # Maybe... there is a way to make these selectable? extras = [ "telegram", @@ -34,7 +37,7 @@ "constellation", "sentry", "herald", - "coloredlogs" + "coloredlogs", ] # Development dependencies diff --git a/royalpack/__init__.py b/royalpack/__init__.py deleted file mode 100644 index 23e63a6f..00000000 --- a/royalpack/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This is a template Pack __init__. You can use this without changing anything in other packages too! - -from . import commands, tables, stars, events -from .commands import available_commands -from .tables import available_tables -from .stars import available_page_stars, available_exception_stars -from .events import available_events - -from .version import semantic as __version__ - -__all__ = [ - "commands", - "tables", - "stars", - "events", - "available_commands", - "available_tables", - "available_page_stars", - "available_exception_stars", - "available_events", -] diff --git a/royalpack/commands/__init__.py b/royalpack/commands/__init__.py index 6921a8e5..eb4d5a1b 100644 --- a/royalpack/commands/__init__.py +++ b/royalpack/commands/__init__.py @@ -1,67 +1,87 @@ # Imports go here! +from .ahnonlosoio import AhnonlosoioCommand +from .answer import AnswerCommand +from .brawlhalla import BrawlhallaCommand +from .cat import CatCommand from .ciaoruozi import CiaoruoziCommand from .color import ColorCommand from .cv import CvCommand +from .cvstats import CvstatsCommand from .diario import DiarioCommand -from .rage import RageCommand -from .reminder import ReminderCommand -from .ship import ShipCommand -from .smecds import SmecdsCommand -from .videochannel import VideochannelCommand -from .pause import PauseCommand -from .play import PlayCommand -from .queue import QueueCommand -from .skip import SkipCommand -from .summon import SummonCommand -from .youtube import YoutubeCommand -from .soundcloud import SoundcloudCommand -from .emojify import EmojifyCommand -from .leagueoflegends import LeagueoflegendsCommand from .diarioquote import DiarioquoteCommand -from .peertubeupdates import PeertubeUpdatesCommand -from .googlevideo import GooglevideoCommand -from .yahoovideo import YahoovideoCommand -from .userinfo import UserinfoCommand -from .spell import SpellCommand -from .ahnonlosoio import AhnonlosoioCommand +from .diarioshuffle import DiarioshuffleCommand +from .dota import DotaCommand from .eat import EatCommand -from .pmots import PmotsCommand -from .peertube import PeertubeCommand +from .emojify import EmojifyCommand from .eval import EvalCommand from .exec import ExecCommand +from .fortune import FortuneCommand +from .givefiorygi import GivefiorygiCommand +from .givetreasure import GivetreasureCommand +from .help import HelpCommand +from .leagueoflegends import LeagueoflegendsCommand +from .magickfiorygi import MagickfiorygiCommand +from .magicktreasure import MagicktreasureCommand +from .matchmaking import MatchmakingCommand +from .peertubeupdates import PeertubeUpdatesCommand +from .ping import PingCommand +from .pmots import PmotsCommand +from .dog import DogCommand +from .rage import RageCommand +from .reminder import ReminderCommand +from .royalpackversion import RoyalpackCommand +from .ship import ShipCommand +from .smecds import SmecdsCommand +from .spell import SpellCommand +from .steammatch import SteammatchCommand +from .steampowered import SteampoweredCommand +from .treasure import TreasureCommand +from .trivia import TriviaCommand +from .userinfo import UserinfoCommand +from .osu import OsuCommand # Enter the commands of your Pack here! available_commands = [ + AhnonlosoioCommand, + AnswerCommand, + BrawlhallaCommand, + CatCommand, CiaoruoziCommand, ColorCommand, CvCommand, + CvstatsCommand, DiarioCommand, - RageCommand, - ReminderCommand, - ShipCommand, - SmecdsCommand, - VideochannelCommand, - PauseCommand, - PlayCommand, - QueueCommand, - SkipCommand, - SummonCommand, - YoutubeCommand, - SoundcloudCommand, - EmojifyCommand, - LeagueoflegendsCommand, DiarioquoteCommand, - PeertubeUpdatesCommand, - GooglevideoCommand, - YahoovideoCommand, - UserinfoCommand, - SpellCommand, - AhnonlosoioCommand, + DiarioshuffleCommand, + DotaCommand, EatCommand, - PmotsCommand, - PeertubeCommand, + EmojifyCommand, EvalCommand, ExecCommand, + FortuneCommand, + GivefiorygiCommand, + GivetreasureCommand, + HelpCommand, + LeagueoflegendsCommand, + MagickfiorygiCommand, + MagicktreasureCommand, + MatchmakingCommand, + PeertubeUpdatesCommand, + PingCommand, + PmotsCommand, + DogCommand, + RageCommand, + ReminderCommand, + RoyalpackCommand, + ShipCommand, + SmecdsCommand, + SpellCommand, + SteammatchCommand, + SteampoweredCommand, + TreasureCommand, + TriviaCommand, + UserinfoCommand, + OsuCommand, ] # Don't change this, it should automatically generate __all__ diff --git a/royalpack/commands/abstract/linker.py b/royalpack/commands/abstract/linker.py new file mode 100644 index 00000000..198df1f8 --- /dev/null +++ b/royalpack/commands/abstract/linker.py @@ -0,0 +1,202 @@ +from typing import * +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.serf.telegram as rst +import royalnet.backpack.tables as rbt +import abc +import logging +import asyncio as aio +from ...types import Updatable + + +log = logging.getLogger(__name__) + + +class LinkerCommand(rc.Command, metaclass=abc.ABCMeta): + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + + self.updater_task = None + if self.enabled(): + self.updater_task = self.loop.create_task(self.run_updater()) + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author(error_if_none=True) + if len(args) == 0: + message = [] + for obj in await self.get_updatables_of_user(session=data.session, user=author): + async def change(attribute: str, value: Any): + """A shortcut for self.__change.""" + await self._change(session=data.session, + obj=obj, + attribute=attribute, + new=value) + + await self.update(session=data.session, obj=obj, change=change) + message.append(self.describe(obj)) + if len(message) == 0: + raise rc.UserError("Nessun account connesso.") + await data.session_commit() + await data.reply("\n".join(message)) + else: + created = await self.create(session=data.session, user=author, args=args, data=data) + await data.session_commit() + if created is not None: + message = ["🔗 Account collegato!", "", self.describe(created)] + await data.reply("\n".join(message)) + + def describe(self, obj: Updatable) -> str: + """The text that should be appended to the report message for a given Updatable.""" + return str(obj) + + @abc.abstractmethod + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Updatable]: + """Get the updatables of a specific user.""" + ... + + @abc.abstractmethod + async def get_updatables(self, session) -> List[Updatable]: + """Return a list of all objects that should be updated at this updater cycle.""" + ... + + @abc.abstractmethod + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[Updatable]: + """Create a new updatable object for a user. + + This function is responsible for adding the object to the session.""" + ... + + @abc.abstractmethod + async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]): + """Update a single updatable object. Use the change method to change values on the object!""" + ... + + @abc.abstractmethod + async def on_increase(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute has increased from the old value.""" + ... + + @abc.abstractmethod + async def on_unchanged(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute stayed the same as the old value.""" + ... + + @abc.abstractmethod + async def on_decrease(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + """Called when the attribute has decreased from the old value.""" + ... + + @abc.abstractmethod + async def on_first(self, session, obj: Updatable, attribute: str, old: None, new: Any) -> None: + """Called when the attribute changed from None.""" + ... + + @abc.abstractmethod + async def on_reset(self, session, obj: Updatable, attribute: str, old: Any, new: None) -> None: + """Called when the attribute changed to None.""" + ... + + async def _change(self, + session, + obj, + attribute: str, + new) -> None: + """Set the value of an attribute of an object to a value, and call the corresponding method.""" + old = obj.__getattribute__(attribute) + if new == old: + await self.on_unchanged(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + else: + if old is None: + await self.on_first(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + elif new is None: + await self.on_reset(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + elif new > old: + await self.on_increase(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + else: + await self.on_decrease(session=session, + obj=obj, + attribute=attribute, + old=old, + new=new) + obj.__setattr__(attribute, new) + + def enabled(self) -> bool: + """Whether the updater is enabled or not.""" + return self.config[self.name]["updater"]["enabled"] and self.interface.name == "telegram" + + def period(self) -> int: + """The time between two updater cycles.""" + return self.config[self.name]["updater"]["period"] + + def delay(self) -> int: + """The time between two object updates.""" + return self.config[self.name]["updater"]["delay"] + + def target(self) -> int: + """The id of the Telegram chat where notifications should be sent.""" + return self.config[self.name]["updater"]["target"] + + async def run_updater(self): + log.info(f"Starting updater: {self.name}") + + while True: + log.debug(f"Updater cycle: {self.name}") + session = self.alchemy.Session() + objects = await self.get_updatables(session) + + for obj in objects: + log.debug(f"Updating: {obj} ({self.name})") + + async def change(attribute: str, value: Any): + """A shortcut for self.__change.""" + await self._change(session=session, + obj=obj, + attribute=attribute, + new=value) + + try: + await self.update(session=session, + obj=obj, + change=change) + except Exception as e: + ru.sentry_exc(e) + + delay = self.delay() + log.debug(f"Waiting for: {delay} seconds (delay)") + await aio.sleep(delay) + + log.debug(f"Committing updates: {self.name}") + await ru.asyncify(session.commit) + session.close() + + period = self.period() + log.debug(f"Waiting for: {period} seconds (period)") + await aio.sleep(period) + + async def notify(self, message): + await self.serf.api_call(self.serf.client.send_message, + chat_id=self.target(), + text=rst.escape(message), + parse_mode="HTML", + disable_webpage_preview=True) diff --git a/royalpack/commands/answer.py b/royalpack/commands/answer.py new file mode 100644 index 00000000..481147d4 --- /dev/null +++ b/royalpack/commands/answer.py @@ -0,0 +1,82 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import random +import datetime + + +class AnswerCommand(rc.Command): + name: str = "answer" + + description: str = "Fai una domanda al bot, che possa essere risposta con un sì o un no: lui ti risponderà!" + + syntax: str = "" + + _answers = [ + #Cerchiamo di tenere bilanciate le tre colonne, o almeno le prime due. + #Se avete un'idea ma metterebbe troppe opzioni in un'unica categoria, mettetela sotto commento. + + #risposte "sì" + "Sì.", + "Decisamente sì!", + "Uhm, secondo me sì.", + "Sì! Sì! SÌ!", + "Yup.", + "👍", + "Direi proprio di sì.", + "Assolutamente sì.", + "Ma certo!", + "✔️", + "👌", + "Esatto!", + "Senz'altro!", + "Ovviamente.", + "Questa domanda ha risposta affermativa.", + "Hell yeah.", + + #risposte "no" + "No.", + "Decisamente no!", + "Uhm, secondo me sì.", + "No, no, e ancora NO!", + "Nope.", + "👎", + "Direi proprio di no.", + "Assolutamente no.", + "Certo che no!", + "✖️", + "🙅", + "Neanche per idea!", + "Neanche per sogno!", + "Niente affatto!", + "Questa domanda ha risposta negativa.", + "Hell no.", + + #risposte "boh" + "Boh.", + "E io che ne so?!", + "Non so proprio rispondere", + "Non lo so", + "Mi rifiuto di rispondere alla domanda!", + "Non parlerò senza il mio avvocato!", + "Dunno.", + "Perché lo chiedi a me?", + "🤷 Ah, non lo so io! ¯\_(ツ)_/¯", + "🤷", + "¯\_(ツ)_/¯", + "No idea.", + "Dunno.", + "Boooooh!", + "Non ne ho la più pallida idea.", + ] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + + h = hash(datetime.datetime.now()) + + r = random.Random(x=h) + + message = r.sample(self._answers, 1)[0] + await data.reply(message) + + diff --git a/royalpack/commands/brawlhalla.py b/royalpack/commands/brawlhalla.py new file mode 100644 index 00000000..a4d2939b --- /dev/null +++ b/royalpack/commands/brawlhalla.py @@ -0,0 +1,169 @@ +from typing import * + +import asyncio +import logging +import aiohttp + +from royalnet.backpack import tables as rbt +import royalnet.commands as rc +import royalnet.utils as ru +from sqlalchemy import or_, and_ + +from .abstract.linker import LinkerCommand +from ..tables import Steam, Brawlhalla, BrawlhallaDuo +from ..types import BrawlhallaRank, BrawlhallaMetal, BrawlhallaTier, Updatable + +log = logging.getLogger(__name__) + + +class BrawlhallaCommand(LinkerCommand): + name: str = "brawlhalla" + + aliases = ["bh", "bruhalla", "bruhlalla"] + + description: str = "Visualizza le tue statistiche di Brawlhalla." + + syntax: str = "" + + def token(self): + return self.config['brawlhalla']['token'] + + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Brawlhalla]: + return user.steam + + async def get_updatables(self, session) -> List[Brawlhalla]: + return await ru.asyncify(session.query(self.alchemy.get(Steam)).all) + + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[Brawlhalla]: + raise rc.InvalidInputError("Brawlhalla accounts are automatically linked from Steam.") + + async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]): + BrawlhallaT = self.alchemy.get(Brawlhalla) + DuoT = self.alchemy.get(BrawlhallaDuo) + log.info(f"Updating: {obj}") + async with aiohttp.ClientSession() as hcs: + bh: Brawlhalla = obj.brawlhalla + if bh is None: + log.debug(f"Checking if player has an account...") + async with hcs.get(f"https://api.brawlhalla.com/search?steamid={obj.steamid.as_64}&api_key={self.token()}") as response: + if response.status != 200: + raise rc.ExternalError(f"Brawlhalla API /search returned {response.status}!") + j = await response.json() + if j == {} or j == []: + log.debug("No account found.") + return + bh = BrawlhallaT( + steam=obj, + brawlhalla_id=j["brawlhalla_id"], + name=j["name"] + ) + session.add(bh) + session.flush() + + async with hcs.get(f"https://api.brawlhalla.com/player/{bh.brawlhalla_id}/ranked?api_key={self.token()}") as response: + if response.status != 200: + raise rc.ExternalError(f"Brawlhalla API /ranked returned {response.status}!") + j = await response.json() + if j == {} or j == []: + log.debug("No ranked info found.") + else: + await self._change(session=session, obj=bh, attribute="rating_1v1", new=j["rating"]) + metal_name, tier_name = j["tier"].split(" ", 1) + metal = BrawlhallaMetal[metal_name.upper()] + tier = BrawlhallaTier(int(tier_name)) + rank = BrawlhallaRank(metal=metal, tier=tier) + await self._change(session=session, obj=bh, attribute="rank_1v1", new=rank) + + for jduo in j.get("2v2", []): + bhduo: Optional[BrawlhallaDuo] = await ru.asyncify( + session.query(DuoT) + .filter( + or_( + and_( + DuoT.id_one == jduo["brawlhalla_id_one"], + DuoT.id_two == jduo["brawlhalla_id_two"] + ), + and_( + DuoT.id_one == jduo["brawlhalla_id_two"], + DuoT.id_two == jduo["brawlhalla_id_one"] + ) + ) + ) + .one_or_none + ) + if bhduo is None: + if bh.brawlhalla_id == jduo["brawlhalla_id_one"]: + otherbh: Optional[Brawlhalla] = await ru.asyncify( + session.query(BrawlhallaT).get, jduo["brawlhalla_id_two"] + ) + else: + otherbh: Optional[Brawlhalla] = await ru.asyncify( + session.query(BrawlhallaT).get, jduo["brawlhalla_id_one"] + ) + if otherbh is None: + continue + bhduo = DuoT( + one=bh, + two=otherbh, + ) + + session.add(bhduo) + await self._change(session=session, obj=bhduo, attribute="rating_2v2", new=jduo["rating"]) + metal_name, tier_name = jduo["tier"].split(" ", 1) + metal = BrawlhallaMetal[metal_name.upper()] + tier = BrawlhallaTier(int(tier_name)) + rank = BrawlhallaRank(metal=metal, tier=tier) + await self._change(session=session, obj=bhduo, attribute="rank_2v2", new=rank) + + async def on_increase(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla! Congratulazioni!") + elif attribute == "rank_2v2": + await self.notify(f"📈 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono saliti a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla! Congratulazioni!") + + async def on_unchanged(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla.") + elif attribute == "rank_2v2": + await self.notify(f"📉 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono scesi a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla.") + + async def on_first(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: None, new: Any) -> None: + if attribute == "rank_1v1": + await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla!") + elif attribute == "rank_2v2": + await self.notify(f"🌟 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] si sono classificati a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla!") + + async def on_reset(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: None) -> None: + if attribute == "rank_1v1": + await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Brawlhalla.") + elif attribute == "rank_2v2": + await self.notify(f"⬜️ [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] non hanno più un rank su Brawlhalla.") + + def describe(self, obj: Steam) -> str: + bh = obj.brawlhalla + + string = [f"ℹ️ [b]{bh.name}[/b]", ""] + + if bh.rank_1v1: + string.append("👤 [b]1v1[/b]") + string.append(f"[b]{bh.rank_1v1}[/b] ({bh.rating_1v1} MMR)") + string.append("") + + if len(bh.duos) != 0: + string.append(f"👥 [b]2v2[/b]") + + for duo in sorted(bh.duos, key=lambda d: -d.rating_2v2): + other = duo.other(bh) + string.append(f"Con [b]{other.steam.user}[/b]: [b]{duo.rank_2v2}[/b] ({duo.rating_2v2} MMR)") + + if len(bh.duos) != 0: + string.append("") + + return "\n".join(string) diff --git a/royalpack/commands/cat.py b/royalpack/commands/cat.py new file mode 100644 index 00000000..fa45344a --- /dev/null +++ b/royalpack/commands/cat.py @@ -0,0 +1,28 @@ +from typing import * +import royalnet.commands as rc +import aiohttp +import io + + +class CatCommand(rc.Command): + name: str = "cat" + + description: str = "Invia un gatto casuale in chat." + + syntax: str = "" + + aliases = ["catto", "kat", "kitty", "kitten", "gatto", "miao", "garf", "basta"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + async with aiohttp.ClientSession() as session: + async with session.get("https://api.thecatapi.com/v1/images/search") as response: + if response.status >= 400: + raise rc.ExternalError(f"Request returned {response.status}") + result = await response.json() + assert len(result) == 1 + cat = result[0] + assert "url" in cat + url = cat["url"] + async with session.get(url) as response: + img = await response.content.read() + await data.reply_image(image=io.BytesIO(img)) diff --git a/royalpack/commands/ciaoruozi.py b/royalpack/commands/ciaoruozi.py index 65927064..be932f5b 100644 --- a/royalpack/commands/ciaoruozi.py +++ b/royalpack/commands/ciaoruozi.py @@ -1,3 +1,4 @@ +from typing import * import telegram from royalnet.commands import * @@ -5,12 +6,11 @@ from royalnet.commands import * class CiaoruoziCommand(Command): name: str = "ciaoruozi" - description: str = "Saluta Ruozi, un leggendario essere che una volta era in User Games." + description: str = "Saluta Ruozi, un leggendario essere che è tornato in Royal Games." async def run(self, args: CommandArgs, data: CommandData) -> None: if self.interface.name == "telegram": - update: telegram.Update = data.update - user: telegram.User = update.effective_user + user: telegram.User = data.message.from_user # Se sei Ruozi, salutati da solo! if user.id == 112437036: await data.reply("👋 Ciao me!") diff --git a/royalpack/commands/color.py b/royalpack/commands/color.py index a7432f45..d419f15a 100644 --- a/royalpack/commands/color.py +++ b/royalpack/commands/color.py @@ -1,3 +1,4 @@ +from typing import * from royalnet.commands import * diff --git a/royalpack/commands/cv.py b/royalpack/commands/cv.py index b6b98009..3dbb3d4d 100644 --- a/royalpack/commands/cv.py +++ b/royalpack/commands/cv.py @@ -77,7 +77,15 @@ class CvCommand(Command): activity += f" | 📺 {mact['name']}" # Custom Status elif mact["type"] == 4: - activity += f" | ❓ {mact['state']}" + if "emoji" in mact: + emoji = f"{mact['emoji']['name']}" + else: + emoji = f"❓" + if "state" in mact: + state = f" {mact['state']}" + else: + state = "" + activity += f" | {emoji}{state}" else: raise ExternalError(f"Unknown Discord activity type: {mact['type']}") diff --git a/royalpack/commands/cvstats.py b/royalpack/commands/cvstats.py new file mode 100644 index 00000000..eed97d70 --- /dev/null +++ b/royalpack/commands/cvstats.py @@ -0,0 +1,136 @@ +from typing import * +import logging +import asyncio +import datetime +import royalnet.commands as rc +import royalnet.utils as ru + +from ..tables import Cvstats + + +log = logging.getLogger(__name__) + + +class CvstatsCommand(rc.Command): + name: str = "cvstats" + + description: str = "" + + syntax: str = "" + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + if self.interface.name == "discord": + self.loop.create_task(self._updater(1800)) + + def _is_ryg_member(self, member: dict): + for role in member["roles"]: + if role["id"] == self.interface.config["Cv"]["displayed_role_id"]: + return True + return False + + async def _update(self, db_session): + log.info(f"Gathering Cvstats...") + while True: + try: + response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_cv") + except rc.ConfigurationError: + await asyncio.sleep(10) + continue + else: + break + + users_total = 0 + members_total = 0 + users_online = 0 + members_online = 0 + users_connected = 0 + members_connected = 0 + users_playing = 0 + members_playing = 0 + + # noinspection PyUnboundLocalVariable + for m in response["guild"]["members"]: + users_total += 1 + if self._is_ryg_member(m): + members_total += 1 + + if m["status"]["main"] != "offline" and m["status"]["main"] != "idle": + users_online += 1 + if self._is_ryg_member(m): + members_online += 1 + + if m["voice"] is not None and not m["voice"]["afk"]: + users_connected += 1 + if self._is_ryg_member(m): + members_connected += 1 + + for mact in m["activities"]: + if mact.get("type") == 0: + users_playing += 1 + if self._is_ryg_member(m): + members_playing += 1 + + assert users_online >= members_online + assert users_online >= users_connected + assert users_online >= users_playing + assert members_online >= members_connected + assert members_online >= members_playing + + log.debug(f"Total users: {users_total}") + log.debug(f"Total members: {members_total}") + log.debug(f"Online users: {users_online}") + log.debug(f"Online members: {members_online}") + log.debug(f"Connected users: {users_connected}") + log.debug(f"Connected members: {members_connected}") + log.debug(f"Playing users: {users_playing}") + log.debug(f"Playing members: {members_playing}") + + CvstatsT = self.alchemy.get(Cvstats) + + cvstats = CvstatsT( + timestamp=datetime.datetime.now(), + users_total=users_total, + members_total=members_total, + users_online=users_online, + members_online=members_online, + users_connected=users_connected, + members_connected=members_connected, + users_playing=users_playing, + members_playing=members_playing + ) + + log.debug("Saving to database...") + db_session.add(cvstats) + await ru.asyncify(db_session.commit) + log.debug("Done!") + + async def _updater(self, period: int): + log.info(f"Started updater with {period}s period") + while True: + log.info(f"Updating...") + session = self.alchemy.Session() + await self._update(session) + session.close() + log.info(f"Sleeping for {period}s") + await asyncio.sleep(period) + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + CvstatsT = self.alchemy.get(Cvstats) + + cvstats = data.session.query(CvstatsT).order_by(CvstatsT.timestamp.desc()).first() + + message = [ + f"ℹ️ [b]Statistiche[/b]", + f"Ultimo aggiornamento: [b]{cvstats.timestamp.strftime('%Y-%m-%d %H:%M')}[/b]", + f"Utenti totali: [b]{cvstats.users_total}[/b]", + f"Membri totali: [b]{cvstats.members_total}[/b]", + f"Utenti online: [b]{cvstats.users_online}[/b]", + f"Membri online: [b]{cvstats.members_online}[/b]", + f"Utenti connessi: [b]{cvstats.users_connected}[/b]", + f"Membri connessi: [b]{cvstats.members_connected}[/b]", + f"Utenti in gioco: [b]{cvstats.users_playing}[/b]", + f"Membri in gioco: [b]{cvstats.members_playing}[/b]" + ] + + await data.reply("\n".join(message)) diff --git a/royalpack/commands/diario.py b/royalpack/commands/diario.py index 6ba99de9..1221e2b9 100644 --- a/royalpack/commands/diario.py +++ b/royalpack/commands/diario.py @@ -1,11 +1,12 @@ +from typing import * import re import datetime import telegram import aiohttp -from typing import * -from royalnet.commands import * -from royalnet.utils import asyncify -from royalnet.backpack.tables import * +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.backpack.tables as rbt + from ..tables import * @@ -13,7 +14,7 @@ async def to_imgur(imgur_api_key, photosizes: List[telegram.PhotoSize], caption= # Select the largest photo largest_photo = sorted(photosizes, key=lambda p: p.width * p.height)[-1] # Get the photo url - photo_file: telegram.File = await asyncify(largest_photo.get_file) + photo_file: telegram.File = await ru.asyncify(largest_photo.get_file) # Forward the url to imgur, as an upload async with aiohttp.request("post", "https://api.imgur.com/3/upload", data={ "image": photo_file.file_path, @@ -25,21 +26,20 @@ async def to_imgur(imgur_api_key, photosizes: List[telegram.PhotoSize], caption= }) as request: response = await request.json() if not response["success"]: - raise CommandError("Imgur returned an error in the image upload.") + raise rc.CommandError("Imgur returned an error in the image upload.") return response["data"]["link"] -class DiarioCommand(Command): +class DiarioCommand(rc.Command): name: str = "diario" description: str = "Aggiungi una citazione al Diario." syntax = "[!] \"{testo}\" --[autore], [contesto]" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: if self.interface.name == "telegram": - update: telegram.Update = data.update - message: telegram.Message = update.message + message: telegram.Message = data.message reply: telegram.Message = message.reply_to_message creator = await data.get_author() # noinspection PyUnusedLocal @@ -71,12 +71,12 @@ class DiarioCommand(Command): media_url = None # Ensure there is a text or an image if not (text or media_url): - raise InvalidInputError("Il messaggio a cui hai risposto non contiene testo o immagini.") + raise rc.InvalidInputError("Il messaggio a cui hai risposto non contiene testo o immagini.") # Find the Royalnet account associated with the sender - quoted_tg = await asyncify(data.session.query(self.alchemy.get(Telegram)) - .filter_by(tg_id=reply.from_user.id) - .one_or_none) - quoted_account = quoted_tg.royal if quoted_tg is not None else None + quoted_tg = await ru.asyncify(data.session.query(self.alchemy.get(rbt.Telegram)) + .filter_by(tg_id=reply.from_user.id) + .one_or_none) + quoted_account = quoted_tg.user if quoted_tg is not None else None # Find the quoted name to assign quoted_user: telegram.User = reply.from_user quoted = quoted_user.full_name @@ -122,13 +122,13 @@ class DiarioCommand(Command): context = None # Find if there's a Royalnet account associated with the quoted name if quoted is not None: - quoted_alias = await asyncify( - data.session.query(self.alchemy.get(Alias)) - .filter_by(alias=quoted.lower()).one_or_none + quoted_alias = await ru.asyncify( + data.session.query(self.alchemy.get(rbt.Alias)) + .filter_by(alias=quoted.lower()).one_or_none ) else: quoted_alias = None - quoted_account = quoted_alias.royal if quoted_alias is not None else None + quoted_account = quoted_alias.user if quoted_alias is not None else None else: text = None quoted = None @@ -137,7 +137,7 @@ class DiarioCommand(Command): context = None # Ensure there is a text or an image if not (text or media_url): - raise InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") + raise rc.InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") # Create the diario quote diario = self.alchemy.get(Diario)(creator=creator, quoted_account=quoted_account, @@ -148,7 +148,7 @@ class DiarioCommand(Command): media_url=media_url, spoiler=spoiler) data.session.add(diario) - await asyncify(data.session.commit) + await ru.asyncify(data.session.commit) await data.reply(f"✅ {str(diario)}") else: # Find the creator of the quotes @@ -173,7 +173,7 @@ class DiarioCommand(Command): timestamp = datetime.datetime.now() # Ensure there is some text if not text: - raise InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") + raise rc.InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") # Or a quoted if not quoted: quoted = None @@ -181,17 +181,17 @@ class DiarioCommand(Command): context = None # Find if there's a Royalnet account associated with the quoted name if quoted is not None: - quoted_alias = await asyncify( - data.session.query(self.alchemy.get(Alias)) - .filter_by(alias=quoted.lower()) - .one_or_none + quoted_alias = await ru.asyncify( + data.session.query(self.alchemy.get(rbt.Alias)) + .filter_by(alias=quoted.lower()) + .one_or_none ) else: quoted_alias = None - quoted_account = quoted_alias.royal if quoted_alias is not None else None + quoted_account = quoted_alias.user if quoted_alias is not None else None if quoted_alias is not None and quoted_account is None: - raise UserError("Il nome dell'autore è ambiguo, quindi la riga non è stata aggiunta.\n" - "Per piacere, ripeti il comando con un nome più specifico!") + raise rc.UserError("Il nome dell'autore è ambiguo, quindi la riga non è stata aggiunta.\n" + "Per piacere, ripeti il comando con un nome più specifico!") # Create the diario quote diario = self.alchemy.Diario(creator=creator, quoted_account=quoted_account, @@ -202,5 +202,5 @@ class DiarioCommand(Command): media_url=None, spoiler=spoiler) data.session.add(diario) - await asyncify(data.session.commit) + await ru.asyncify(data.session.commit) await data.reply(f"✅ {str(diario)}") diff --git a/royalpack/commands/diarioquote.py b/royalpack/commands/diarioquote.py index c9be589d..4b35f725 100644 --- a/royalpack/commands/diarioquote.py +++ b/royalpack/commands/diarioquote.py @@ -1,9 +1,11 @@ -from royalnet.commands import * -from royalnet.utils import * +from typing import * +import royalnet.commands as rc +import royalnet.utils as ru + from ..tables import Diario -class DiarioquoteCommand(Command): +class DiarioquoteCommand(rc.Command): name: str = "diarioquote" description: str = "Cita una riga del diario." @@ -12,12 +14,12 @@ class DiarioquoteCommand(Command): syntax = "{id}" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: try: entry_id = int(args[0].lstrip("#")) except ValueError: - raise CommandError("L'id che hai specificato non è valido.") - entry: Diario = await asyncify(data.session.query(self.alchemy.get(Diario)).get, entry_id) + raise rc.CommandError("L'id che hai specificato non è valido.") + entry: Diario = await ru.asyncify(data.session.query(self.alchemy.get(Diario)).get, entry_id) if entry is None: - raise CommandError("Nessuna riga con quell'id trovata.") + raise rc.CommandError("Nessuna riga con quell'id trovata.") await data.reply(f"ℹ️ {entry}") diff --git a/royalpack/commands/diarioshuffle.py b/royalpack/commands/diarioshuffle.py new file mode 100644 index 00000000..8468a954 --- /dev/null +++ b/royalpack/commands/diarioshuffle.py @@ -0,0 +1,29 @@ +from typing import * +import royalnet.commands as rc +import royalnet.utils as ru +from sqlalchemy import func + +from ..tables import Diario + + +class DiarioshuffleCommand(rc.Command): + name: str = "diarioshuffle" + + description: str = "Cita una riga casuale del diario." + + aliases = ["dis", "dishuffle", "dish"] + + syntax = "" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + DiarioT = self.alchemy.get(Diario) + entry: List[Diario] = await ru.asyncify( + data.session + .query(DiarioT) + .order_by(func.random()) + .limit(1) + .one_or_none + ) + if entry is None: + raise rc.CommandError("Nessuna riga del diario trovata.") + await data.reply(f"ℹ️ {entry}") diff --git a/royalpack/commands/dog.py b/royalpack/commands/dog.py new file mode 100644 index 00000000..2179925e --- /dev/null +++ b/royalpack/commands/dog.py @@ -0,0 +1,181 @@ +from typing import * +import royalnet.commands as rc +import aiohttp +import io + + +class DogCommand(rc.Command): + name: str = "dog" + + description: str = "Invia un cane della razza specificata in chat." + + syntax: str = "[razza|list]" + + _breeds = [ + "affenpinscher", + "african", + "airedale", + "akita", + "appenzeller", + "australian-shepherd", + "basenji", + "beagle", + "bluetick", + "borzoi", + "bouvier", + "boxer", + "brabancon", + "briard", + "buhund-norwegian", + "bulldog-boston", + "bulldog-english", + "bulldog-french", + "bullterrier-staffordshire", + "cairn", + "cattledog-australian", + "chihuahua", + "chow", + "clumber", + "cockapoo", + "collie-border", + "coonhound", + "corgi-cardigan", + "cotondetulear", + "dachshund", + "dalmatian", + "dane-great", + "deerhound-scottish", + "dhole", + "dingo", + "doberman", + "elkhound-norwegian", + "entlebucher", + "eskimo", + "finnish-lapphund", + "frise-bichon", + "germanshepherd", + "greyhound-italian", + "groenendael", + "havanese", + "hound-afghan", + "hound-basset", + "hound-blood", + "hound-english", + "hound-ibizan", + "hound-plott", + "hound-walker", + "husky", + "keeshond", + "kelpie", + "komondor", + "kuvasz", + "labrador", + "leonberg", + "lhasa", + "malamute", + "malinois", + "maltese", + "mastiff-bull", + "mastiff-english", + "mastiff-tibetan", + "mexicanhairless", + "mix", + "mountain-bernese", + "mountain-swiss", + "newfoundland", + "otterhound", + "ovcharka-caucasian", + "papillon", + "pekinese", + "pembroke", + "pinscher-miniature", + "pitbull", + "pointer-german", + "pointer-germanlonghair", + "pomeranian", + "poodle-miniature", + "poodle-standard", + "poodle-toy", + "pug", + "puggle", + "pyrenees", + "redbone", + "retriever-chesapeake", + "retriever-curly", + "retriever-flatcoated", + "retriever-golden", + "ridgeback-rhodesian", + "rottweiler", + "saluki", + "samoyed", + "schipperke", + "schnauzer-giant", + "schnauzer-miniature", + "setter-english", + "setter-gordon", + "setter-irish", + "sheepdog-english", + "sheepdog-shetland", + "shiba", + "shihtzu", + "spaniel-blenheim", + "spaniel-brittany", + "spaniel-cocker", + "spaniel-irish", + "spaniel-japanese", + "spaniel-sussex", + "spaniel-welsh", + "springer-english", + "stbernard", + "terrier-american", + "terrier-australian", + "terrier-bedlington", + "terrier-border", + "terrier-dandie", + "terrier-fox", + "terrier-irish", + "terrier-kerryblue", + "terrier-lakeland", + "terrier-norfolk", + "terrier-norwich", + "terrier-patterdale", + "terrier-russell", + "terrier-scottish", + "terrier-sealyham", + "terrier-silky", + "terrier-tibetan", + "terrier-toy", + "terrier-westhighland", + "terrier-wheaten", + "terrier-yorkshire", + "vizsla", + "waterdog-spanish", + "weimaraner", + "whippet", + "wolfhound-irish", + ] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + breed = args.joined() + if breed: + if breed == "list": + await data.reply("\n".join(["ℹ️ Razze disponibili:", [f"[c]{breed}[/c]" for breed in self._breeds]])) + if breed in self._breeds: + url = f"https://dog.ceo/api/breed/{breed}/images/random" + else: + raise rc.InvalidInputError("Questa razza non è disponibile.\n") + else: + url = f"https://dog.ceo/api/breeds/image/random" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status >= 400: + raise rc.ExternalError(f"Request returned {response.status}") + result = await response.json() + assert "status" in result + assert result["status"] == "success" + assert "message" in result + url = result["message"] + async with session.get(url) as response: + img = await response.content.read() + await data.reply_image(image=io.BytesIO(img)) diff --git a/royalpack/commands/dota.py b/royalpack/commands/dota.py new file mode 100644 index 00000000..917285e6 --- /dev/null +++ b/royalpack/commands/dota.py @@ -0,0 +1,106 @@ +from typing import * +import logging +import aiohttp +import royalnet.commands as rc +import royalnet.utils as ru +from royalnet.backpack import tables as rbt +from .abstract.linker import LinkerCommand + +from ..tables import Steam, Dota +from ..types import DotaRank + +log = logging.getLogger(__name__) + + +class DotaCommand(LinkerCommand): + name: str = "dota" + + aliases = ["dota2", "doto", "doto2", "dotka", "dotka2"] + + description: str = "Visualizza le tue statistiche di Dota." + + syntax: str = "" + + def describe(self, obj: Steam) -> str: + string = f"ℹ️ [b]{obj.persona_name}[/b]\n" + if obj.dota.rank: + string += f"{obj.dota.rank}\n" + string += f"\n" \ + f"Wins: [b]{obj.dota.wins}[/b]\n" \ + f"Losses: [b]{obj.dota.losses}[/b]\n" \ + f"\n" + return string + + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Dota]: + return user.steam + + async def get_updatables(self, session) -> List[Dota]: + return await ru.asyncify(session.query(self.alchemy.get(Steam)).all) + + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[Dota]: + raise rc.InvalidInputError("Dota accounts are automatically linked from Steam.") + + async def update(self, session, obj: Steam, change: Callable[[str, Any], Awaitable[None]]): + log.debug(f"Getting player data from OpenDota...") + async with aiohttp.ClientSession() as hcs: + # Get profile data + async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/") as response: + if response.status != 200: + raise rc.ExternalError(f"OpenDota / returned {response.status}!") + p = await response.json() + # No such user + if "profile" not in p: + log.debug(f"Not found: {obj}") + return + # Get win/loss data + async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/wl") as response: + if response.status != 200: + raise rc.ExternalError(f"OpenDota /wl returned {response.status}!") + wl = await response.json() + # No such user + if wl["win"] == 0 and wl["lose"] == 0: + log.debug(f"Not found: {obj}") + return + # Find the Dota record, if it exists + dota: Dota = obj.dota + if dota is None: + # Autocreate the Dota record + dota = self.alchemy.get(Dota)(steam=obj) + session.add(dota) + session.flush() + + # Make a custom change function + async def change(attribute: str, new: Any): + await self._change(session=session, obj=dota, attribute=attribute, new=new) + + await change("wins", wl["win"]) + await change("losses", wl["lose"]) + if p["rank_tier"]: + await change("rank", DotaRank(rank_tier=p["rank_tier"])) + else: + await change("rank", None) + + async def on_increase(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + if attribute == "rank": + await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] su Dota 2! Congratulazioni!") + + async def on_unchanged(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None: + if attribute == "rank": + await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] su Dota 2.") + + async def on_first(self, session, obj: Dota, attribute: str, old: None, new: Any) -> None: + if attribute == "wins": + await self.notify(f"↔️ Account {obj} connesso a {obj.steam.user}!") + elif attribute == "rank": + await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato [b]{new}[/b] su Dota 2!") + + async def on_reset(self, session, obj: Dota, attribute: str, old: Any, new: None) -> None: + if attribute == "rank": + await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Dota 2.") diff --git a/royalpack/commands/eat.py b/royalpack/commands/eat.py index da10a809..a9766fcb 100644 --- a/royalpack/commands/eat.py +++ b/royalpack/commands/eat.py @@ -28,8 +28,7 @@ class EatCommand(Command): "evilbalu": "🚹 Hai mangiato {food}.\n[i]Sa di snado.[/i]", "balubis": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]", "goodbalu": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]", - "chiara": "🚺 Hai mangiato {food}.\n[i]Sa un po' di biscotto, ma per lo più sa di curcuma, pepe e spezie" - " varie.[/i]", + "chiara": "🚺 Hai mangiato {food}.\n[i]Sa un po' di biscotto, ma per lo più sa di curcuma e pepe.[/i]", "fabio": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]", "proto": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]", "marco": "🚹 Hai mangiato {food}.\n[i]Sa di carlino <.<[/i]", @@ -38,15 +37,28 @@ class EatCommand(Command): "maxsensei": "🚹 Hai mangiato {food}.\n[i]Sa di merda.[/i]", "steffo": "🚹 Hai mangiato {food}.\n[i]Sa di gelato e di Coca-Cola.[/i]", + # Sezione in cui mangi i professori dei membri Royal Games + "arrigo": "🖍 Hai mangiato {food}!\n[i]Ti scrive F: V→W sulla parete dello stomaco con i gessetti colorati.[/i]", + "bonisoli": "🖍 Hai mangiato {food}!\n[i]Ti scrive F: V→W sulla parete dello stomaco con i gessetti colorati.[/i]", + "montangero": "📝 Hai mangiato la {food}!\n[i]La digerisci in O(n!).[/i]", + "marongiu": "🔧 Hai mangiato {food}!\n[i]Il tuo apparato digerente viene trasformato in una pipeline.[/i]", + "mandreoli": "⚠️ Hai mangiato la {food}!\n[c]Error: Segmentation fault (core dumped)[/c]", + "la rocca": "📊 Hai mangiato {food}!\n[i]Si distribuisce nel tuo intestino come una Normale.[/i]", + "villani": "🐜 Hai mangiato {food}!\n[i]Crea una rete neurale sfruttando i tuoi neuroni e le tue cellule.[/i]", + "novellani": "❓Volevi mangiare {food}...\n[i]...ma invece trovi solo Dell'Amico.[/i]", + # Sezione delle supercazzole "antani": "❔ Hai mangiato {food}. \n[i]Con tarapia tapioco o scherziamo? No, mi permetta. Noi siamo in 4.\n" "Come se fosse antani anche per lei soltanto in due, oppure in quattro anche scribàcchi confaldina?\n" "Come antifurto, per esempio.[/i]", "indice": "☝️ Hai mangiato l'{food}. \n[i]Ecco, lo alzi. Lo vede, lo vede che stuzzica?[/i]", - # sezione con piante e anmali + # Sezione con piante e animali "cactus": "🌵 Hai mangiato un {food}.\n[i]Gli hai tolto le spine prima, vero?[/i]", "tango": "🌳 Hai mangiato un {food}, e un albero insieme ad esso.\n[i]Senti le tue ferite curarsi...[/i]", + "foglia": "🍁 Hai mangiato la {food}.\n[i]A te non la si fa![/i]", + "pug": "🐶 Hai provato a mangiare un {food}...\n[i]...Ma Mallllco si è precipitato in soccorso e lo ha salvato![/i]", + "carlino": "🐶 Hai provato a mangiare un {food}...\n[i]...Ma Mallllco si è precipitato in soccorso e lo ha salvato![/i]", "gatto": "🐱 Vieni fermato prima di poter compiere questo gesto orribile.\n" "[i]Il {food} verrà pettato da tutti per farlo riavere dal trauma.[/i]", "3 porcellini": "🐷 Hai mangiato i {food}.\n[i]La casa di mattoni non è bastata a fermarti![/i]", @@ -73,13 +85,19 @@ class EatCommand(Command): "little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]", "a little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]", "pollo": '🍗 Il {food} che hai appena mangiato proveniva dallo spazio.\n[i]Coccodè?[/i]', + "pranzo di marco": '🍗 Hai mangiato il {food}.\n[i]Ti senti lactose-free, ma un po\' povero in calcio.[/i]', + "pranzo di mallllco": '🍗 Hai mangiato il {food}.\n[i]Ti senti lactose-free, ma un po\' povero in calcio.[/i]', "gnocchetti": "🥘 Ullà, sono duri 'sti {food}!\n[i]Fai fatica a digerirli.[/i]", "spam": "🥫 Hai mangiato {food}. La famosa carne in gelatina, ovviamente!\n[i]A questo proposito, di " "sicuro sarai interessato all'acquisto di 1087 scatole di Simmenthal in offerta speciale![/i]", "riso": "🍚 Hai mangiato del {food}. Non ci resta che il Pianto! \n[i]Ba dum tsss![/i]", "gelato": "🍨 Mangiando del {food}, hai invocato Steffo.\n[i]Cedigli ora il tuo gelato.[/i]", + "gelato di steffo": "🍨 Hai provato a rubare il {food}...\n[i]...Ma sei arrivato tardi: l'ha già mangiato.[/i]", "biscotto": "🍪 Hai mangiato un {food} di contrabbando.\n[i]L'Inquisizione non lo saprà mai![/i]", "biscotti": "🍪 Hai mangiato tanti {food} di contrabbando.\n[i]Attento! L'Inquisizione è sulle tue tracce![/i]", + "crocchette di pollo": "🍗 Hai mangiato {food}!\n[i]Dio porco maledetto, infame, CAPRA, porca Madonna, Dio cane, " + "HAI PERSO. UN POMERIGGIO PER C- ooh se è questo dio cane, altro che sfondamento dei cieli " + "*roba non capibile*, sfondi tutti dio can li distruggi, non ci rimane più niente.[/i]", # Sezione delle bevande "acqua": "💧 Hai bevuto un po' d'{food}.\n[i]Ti depura e ti fa fare tanta plin plin![/i}", @@ -95,6 +113,7 @@ class EatCommand(Command): "kaffé": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]", "kaffe": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]", "birra": "🍺 Hai mangiato {food}.\n[i]Adesso sei un povero barbone alcolizzato.[/i]", + "martini": "🍸 Hai ordinato un {food}. Agitato, non mescolato.\n[i]Adesso hai licenza di uccidere![/i]", "redbull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]", "red bull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]", @@ -109,8 +128,20 @@ class EatCommand(Command): "red hat": "🐧 Hai mangiato {food}.\n[i]La tua anima appartiene a IBM, ora.[/i]", "redhat": "🐧 Hai mangiato {food}.\n[i]La tua anima appartiene a IBM, ora.[/i]", "linux from scratch": "🐧 Hai mangiato {food}.\n[i]Sei diventato un puzzle.[/i]", - + + # Citazioni da film (nello specifico dai Blues Brothers) + "pane bianco tostato liscio, quattro polli fritti e una coca": "🕶 Tu e tuo fratello avete ordinato {food}." + " Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta" + " quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]", + "pane bianco tostato liscio": "🕶 Tu e tuo fratello avete ordinato {food}, quattro polli fritti e una coca." + " Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta" + " quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]", + "quattro polli fritti e una coca": "🕶 Tu e tuo fratello avete ordinato pane bianco tostato liscio, {food}." + " Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta" + " quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]", + # Altro + "vendetta": "😈 Ti sei gustato la tua {food}.\n[i]Deliziosa, se servita fredda![/i]", "demone": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]", "diavolo": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]", "cacca": "💩 Che schifo! Hai mangiato {food}!\n[i]Allontati per favore, PLEH![/i]", @@ -121,12 +152,19 @@ class EatCommand(Command): "bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]", "royal bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]", "re": "👑 Hai mangiato il {food} avversario! \n[i]Scacco matto![/i]", + "furry": "🐕 Hai mangiato {food}.\n[i]OwO[/i]", + "qualcosa che non mi piace": "🥦 Hai assaggiato il cibo, ma non ti piace proprio./n[i]Dai, mangialo, che ti" + " fa bene! In africa i bambini muoiono di fame, e tu... ![/i]", + "qualcosa che non ti piace": "🥦 Hai assaggiato il cibo, ma non ti piace proprio./n[i]Dai, mangialo, che ti" + " fa bene! In africa i bambini muoiono di fame, e tu... ![/i]", "polvere": "☁️ Hai mangiato la {food}.\n[i]Ti hanno proprio battuto![/i]", "giaroun": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "giarone": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "sasso": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "bomba": "💣 Hai mangiato una {food}. Speriamo fosse solo calorica!\n[i]3... 2... 1...[/i]", - "ass": "🕳 Hai mangiato {food}./n[i]Bleah! Lo sai cosa fa quel coso per sopravvivere?[/i]", + "ass": "🕳 Hai mangiato {food}.\n[i]Bleah! Lo sai cosa fa quel coso per sopravvivere?[/i]", + "onion": "🗞 You ate the {food}. Ci sei proprio cascato!\n [i]Hai mai creduto a una notizia di Lercio," + " invece?[/i]", "uranio": "☢️ L'{food} che hai mangiato era radioattivo.\n[i]Stai brillando di verde![/i]", "tide pod": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]", "tide pods": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]", @@ -138,6 +176,19 @@ class EatCommand(Command): "nulla": "⬜️ Non hai mangiato {food}.\n[i]Hai ancora più fame.[/i]", "torta": "⬜️ Non hai mangiato niente.\n[i]La {food} è una menzogna![/i]", "cake": "⬜️ Non hai mangiato niente.\n[i]The {food} is a lie![/i]", + "markov": "🗨 Stai cercando di mangiare... un matematico russo di nome {food}?\n[i]Lo trovi un po' indigesto.[/i]", + "mia sul fiume": "💧 Hai mangiato il miglior piatto al mondo, la {food}, esclusivo ai membri Royal Games.\n" + "[i]Nessuno, tranne il bot, sa di cosa è fatta esattamente, ma una cosa è certa: è " + "buonissima![/i]", + "angelo": "👼 Oh mio dio! E' un {food}!\n[i]Ora hai un digramma ad onda blu.[/i]", + "unicode": "🍗 Hai mangiato {food}!\n๓ค 𝔫𝔬𝔫 [i]è[/i] 𝓼𝓾𝓬𝓬𝓮𝓼𝓼𝓸 𝕟𝕦𝕝𝕝𝕒.", + "eco": "🏔 Hai mangiato l'{food} eco eco!\n[i]Ma non è successo nulla ulla ulla.[/i]", + "disinfettante": "🧴Hai mangiato {food}!\n[i]Secondo Trump, ora sei molto più sano.[/i]", + + "terraria": "🌳 Hai provato a mangiare {food}, ma non ne sei stato all'Altezza (Coniglio).\n[i]Prova a mangiare qualcos'altro...[/i]", + "cooked fish": "🐟 Hai mangiato {food}.\n[i]Ora sei Well Fed per 20 minuti.[/i]", + "gestione": "🌐 Hai mangiato {food}, su cui si basa Condivisione.\n[i]Fa ridere di sotto, ma fa anche riflettere di sopra.[/i]", + "condivisione": "🌐 Hai mangiato {food}, basato su Gestione.\n[i]Fa ridere di sopra, ma fa anche riflettere di sotto.[/i]", } async def run(self, args: CommandArgs, data: CommandData) -> None: diff --git a/royalpack/commands/emojify.py b/royalpack/commands/emojify.py index 5aa9a2d6..4afe4ad7 100644 --- a/royalpack/commands/emojify.py +++ b/royalpack/commands/emojify.py @@ -1,8 +1,9 @@ +from typing import * import random -from royalnet.commands import * +import royalnet.commands as rc -class EmojifyCommand(Command): +class EmojifyCommand(rc.Command): name: str = "emojify" description: str = "Converti un messaggio in emoji." @@ -94,6 +95,6 @@ class EmojifyCommand(Command): new_string = new_string.replace(key, selected_emoji) return new_string - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: string = args.joined(require_at_least=1) await data.reply(self._emojify(string)) diff --git a/royalpack/commands/eval.py b/royalpack/commands/eval.py index dfd6a481..21de0da0 100644 --- a/royalpack/commands/eval.py +++ b/royalpack/commands/eval.py @@ -1,9 +1,9 @@ -import royalnet -from royalnet.commands import * -from royalnet.backpack.tables import * +from typing import * +import royalnet.commands as rc +import royalnet.backpack.tables as rbt -class EvalCommand(Command): +class EvalCommand(rc.Command): # oh god if there is a security vulnerability name: str = "eval" @@ -11,13 +11,13 @@ class EvalCommand(Command): syntax: str = "{espressione}" - async def run(self, args: CommandArgs, data: CommandData) -> None: - user: User = await data.get_author(error_if_none=True) - if user.role != "Admin": - raise CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + user: rbt.User = await data.get_author(error_if_none=True) + if "admin" not in user.roles: + raise rc.CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" "(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)") try: result = eval(args.joined(require_at_least=1)) except Exception as e: - raise CommandError(f"Eval fallito: {e}") + raise rc.CommandError(f"Eval fallito: {e}") await data.reply(repr(result)) diff --git a/royalpack/commands/exec.py b/royalpack/commands/exec.py index 9823da51..65c3b013 100644 --- a/royalpack/commands/exec.py +++ b/royalpack/commands/exec.py @@ -1,9 +1,9 @@ -import royalnet -from royalnet.commands import * -from royalnet.backpack.tables import * +from typing import * +import royalnet.commands as rc +import royalnet.backpack.tables as rbt -class ExecCommand(Command): +class ExecCommand(rc.Command): # oh god if there is a security vulnerability name: str = "exec" @@ -11,13 +11,13 @@ class ExecCommand(Command): syntax: str = "{script}" - async def run(self, args: CommandArgs, data: CommandData) -> None: - user: User = await data.get_author(error_if_none=True) - if user.role != "Admin": - raise CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + user: rbt.User = await data.get_author(error_if_none=True) + if "admin" not in user.roles: + raise rc.CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" "(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)") try: exec(args.joined(require_at_least=1)) except Exception as e: - raise CommandError(f"Esecuzione fallita: {e}") + raise rc.CommandError(f"Esecuzione fallita: {e}") await data.reply(f"✅ Fatto!") diff --git a/royalpack/commands/fortune.py b/royalpack/commands/fortune.py new file mode 100644 index 00000000..d662a1b8 --- /dev/null +++ b/royalpack/commands/fortune.py @@ -0,0 +1,53 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import random +import datetime + + +class FortuneCommand(rc.Command): + name: str = "fortune" + + description: str = "Quanto sarai fortunato oggi?" + + syntax: str = "" + + _fortunes = [ + "😄 Oggi sarà una fantastica giornata!", + "😌 Oggi sarà una giornata molto chill e rilassante.", + "💰 Oggi sui tuoi alberi cresceranno più Stelline!", + "🍎 Oggi un unicorno ti lascerà la sua Blessed Apple!", + "📈 Oggi il tuo team in ranked sarà più amichevole e competente del solito!", + "🏝 Oggi potrai raggiungere l'Isola Miraggio!", + "🐱 Oggi vedrai più gatti del solito su Internet!", + "🐶 Oggi vedrai più cani del solito su Internet!", + "🐦 Oggi vedrai più uccelli del solito su Internet!", + "🔥 Oggi vedrai più flame del solito su Internet!", + "🤬 Oggi vedrai più discorsi politici del solito su Internet!", + "🐌 Oggi incontrerai una chiocciola sperduta!", + "🎁 Oggi i dispenser di regali in centro funzioneranno senza problemi!", + "🥕 Oggi il tuo raccolto avrà qualità Iridium Star!", + "🔴 Oggi troverai più oggetti di rarità rossa del solito!", + "✨ Oggi farai molti più multicast!", + "♦️ Oggi troverai una Leggendaria Dorata!", + "⭐️ Oggi la stella della RYG ti sembrerà un pochino più dritta!", + "⭐️ Oggi la stella della RYG ti sembrerà anche più storta del solito!", + "💎 Oggi i tuoi avversari non riusciranno a deflettere i tuoi Emerald Splash!", + "⁉️ Oggi le tue supercazzole prematureranno un po' più a sinistra!", + "🌅 Oggi sarà il giorno dopo ieri e il giorno prima di domani!", + "🤖 Oggi il Royal Bot ti dirà qualcosa di molto utile!", + "💤 Oggi rischierai di addormentarti più volte!", + "🥪 Oggi ti verrà fame fuori orario!", + "😓 Oggi dirai molte stupidaggini!", + ] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author() + today = datetime.date.today() + + h = author.uid * hash(today) + + r = random.Random(x=h) + + message = r.sample(self._fortunes, 1)[0] + await data.reply(message) diff --git a/royalpack/commands/givefiorygi.py b/royalpack/commands/givefiorygi.py new file mode 100644 index 00000000..b9f536fc --- /dev/null +++ b/royalpack/commands/givefiorygi.py @@ -0,0 +1,42 @@ +from typing import * +import royalnet.commands as rc +import royalnet.backpack.tables as rbt + +from ..tables import FiorygiTransaction + + +class GivefiorygiCommand(rc.Command): + name: str = "givefiorygi" + + description: str = "Cedi fiorygi a un altro utente." + + syntax: str = "{destinatario} {quantità} {motivo}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author(error_if_none=True) + + user_arg = args[0] + qty_arg = args[1] + + if user_arg is None: + raise rc.InvalidInputError("Non hai specificato un destinatario!") + user = await rbt.User.find(self.alchemy, data.session, user_arg) + if user is None: + raise rc.InvalidInputError("L'utente specificato non esiste!") + if user.uid == author.uid: + raise rc.InvalidInputError("Non puoi inviare fiorygi a te stesso!") + + if qty_arg is None: + raise rc.InvalidInputError("Non hai specificato una quantità!") + try: + qty = int(qty_arg) + except ValueError: + raise rc.InvalidInputError("La quantità specificata non è un numero!") + if qty <= 0: + raise rc.InvalidInputError("La quantità specificata deve essere almeno 1!") + + if author.fiorygi.fiorygi < qty: + raise rc.InvalidInputError("Non hai abbastanza fiorygi per effettuare la transazione!") + + await FiorygiTransaction.spawn_fiorygi(data, author, -qty, f"aver ceduto fiorygi a {user}") + await FiorygiTransaction.spawn_fiorygi(data, user, qty, f"aver ricevuto fiorygi da {author}") diff --git a/royalpack/commands/givetreasure.py b/royalpack/commands/givetreasure.py new file mode 100644 index 00000000..554902b5 --- /dev/null +++ b/royalpack/commands/givetreasure.py @@ -0,0 +1,35 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import Treasure, FiorygiTransaction +from .magicktreasure import MagicktreasureCommand + + +class GivetreasureCommand(MagicktreasureCommand): + name: str = "givetreasure" + + description: str = "Crea un nuovo Treasure di Fiorygi (usando il tuo credito)" + + syntax: str = "{codice} {valore}" + + async def _permission_check(self, author, code, value, data): + if author.fiorygi.fiorygi < value: + raise rc.UserError("Non hai abbastanza fiorygi per creare questo Treasure.") + + async def _create_treasure(self, author, code, value, data): + TreasureT = self.alchemy.get(Treasure) + + treasure = await ru.asyncify(data.session.query(TreasureT).get, code) + if treasure is not None: + raise rc.UserError("Esiste già un Treasure con quel codice.") + + treasure = TreasureT( + code=code, + value=value, + redeemed_by=None + ) + + await FiorygiTransaction.spawn_fiorygi(data, author, -value, "aver creato un tesoro") + + return treasure diff --git a/royalpack/commands/googlevideo.py b/royalpack/commands/googlevideo.py deleted file mode 100644 index d07fe2ae..00000000 --- a/royalpack/commands/googlevideo.py +++ /dev/null @@ -1,16 +0,0 @@ -from .play import PlayCommand - - -class GooglevideoCommand(PlayCommand): - name: str = "googlevideo" - - aliases = ["gv"] - - description: str = "Cerca un video su Google Video e lo aggiunge alla coda della chat vocale." - - syntax = "{ricerca}" - - async def get_url(self, args): - return f"gvsearch:{args.joined()}" - - # Too bad gvsearch: always finds nothing. diff --git a/royalpack/commands/help.py b/royalpack/commands/help.py new file mode 100644 index 00000000..ca129d18 --- /dev/null +++ b/royalpack/commands/help.py @@ -0,0 +1,37 @@ +from typing import * +import royalnet +import royalnet.commands as rc + + +class HelpCommand(rc.Command): + name: str = "help" + + description: str = "Visualizza informazioni su un comando." + + syntax: str = "{comando}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + if len(args) == 0: + message = [ + "ℹ️ Comandi disponibili:" + ] + + for command in sorted(list(set(self.serf.commands.values())), key=lambda c: c.name): + message.append(f"- [c]{self.interface.prefix}{command.name}[/c]") + + await data.reply("\n".join(message)) + else: + name: str = args[0].lstrip(self.interface.prefix) + + try: + command: rc.Command = self.serf.commands[f"{self.interface.prefix}{name}"] + except KeyError: + raise rc.InvalidInputError("Il comando richiesto non esiste.") + + message = [ + f"ℹ️ [c]{self.interface.prefix}{command.name} {command.syntax}[/c]", + "", + f"{command.description}" + ] + + await data.reply("\n".join(message)) diff --git a/royalpack/commands/leagueoflegends.py b/royalpack/commands/leagueoflegends.py index 6efe6ec7..0925a9fb 100644 --- a/royalpack/commands/leagueoflegends.py +++ b/royalpack/commands/leagueoflegends.py @@ -1,97 +1,84 @@ -import typing +from typing import * import riotwatcher import logging import asyncio import sentry_sdk -from royalnet.commands import * -from royalnet.utils import * -from royalnet.serf.telegram import * -from ..tables import LeagueOfLegends -from ..utils import LeagueLeague +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.serf.telegram as rst +from royalnet.backpack import tables as rbt + +from .abstract.linker import LinkerCommand +from ..tables import LeagueOfLegends, FiorygiTransaction +from ..types import LeagueLeague, Updatable log = logging.getLogger(__name__) -class LeagueoflegendsCommand(Command): +class LeagueoflegendsCommand(LinkerCommand): name: str = "leagueoflegends" aliases = ["lol", "league"] - description: str = "Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche." + description: str = "Connetti un account di League of Legends a un account Royalnet, o visualizzane le statistiche." syntax = "[nomeevocatore]" - def __init__(self, interface: CommandInterface): + queue_names = { + "rank_soloq": "Solo/Duo", + "rank_flexq": "Flex", + } + + def __init__(self, interface: rc.CommandInterface): super().__init__(interface) - self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.config["Lol"]["token"]) - if self.interface.name == "telegram": - self.loop.create_task(self._updater(900)) + self._lolwatcher: Optional[riotwatcher.RiotWatcher] = None + self._tftwatcher: Optional[riotwatcher.RiotWatcher] = None + if self.enabled(): + self._lolwatcher = riotwatcher.LolWatcher(api_key=self.token()) + self._tftwatcher = riotwatcher.TftWatcher(api_key=self.token()) - async def _send(self, message): - client = self.serf.client - await self.serf.api_call(client.send_message, - chat_id=self.config["Telegram"]["main_group_id"], - text=escape(message), - parse_mode="HTML", - disable_webpage_preview=True) + def token(self): + return self.config["leagueoflegends"]["token"] - async def _notify(self, - obj: LeagueOfLegends, - attribute_name: str, - old_value: typing.Any, - new_value: typing.Any): - if self.interface.name == "telegram": - if isinstance(old_value, LeagueLeague): - # This is a rank change! - # Don't send messages for every rank change, send messages just if the TIER or RANK changes! - if old_value.tier == new_value.tier and old_value.rank == new_value.rank: - return - # Find the queue - queue_names = { - "rank_soloq": "Solo/Duo", - "rank_flexq": "Flex", - "rank_twtrq": "3v3", - "rank_tftq": "TFT" - } - # Prepare the message - if new_value > old_value: - message = f"📈 [b]{obj.user}[/b] è salito a {new_value} su League of Legends " \ - f"({queue_names[attribute_name]})! Congratulazioni!" - else: - message = f"📉 [b]{obj.user}[/b] è sceso a {new_value} su League of Legends " \ - f"({queue_names[attribute_name]})." - # Send the message - await self._send(message) - # Level up! - elif attribute_name == "summoner_level": - if new_value == 30 or (new_value >= 50 and (new_value % 25 == 0)): - await self._send(f"🆙 [b]{obj.user}[/b] è salito al livello [b]{new_value}[/b] su League of Legends!") + def region(self): + return self.config["leagueoflegends"]["region"] - @staticmethod - async def _change(obj: LeagueOfLegends, - attribute_name: str, - new_value: typing.Any, - callback: typing.Callable[ - [LeagueOfLegends, str, typing.Any, typing.Any], typing.Awaitable[None]]): - old_value = obj.__getattribute__(attribute_name) - if old_value != new_value: - await callback(obj, attribute_name, old_value, new_value) - obj.__setattr__(attribute_name, new_value) + def describe(self, obj: LeagueOfLegends) -> str: + string = f"ℹ️ [b]{obj.summoner_name}[/b]\n" \ + f"Lv. {obj.summoner_level}\n" \ + f"Mastery score: {obj.mastery_score}\n" \ + f"\n" + if obj.rank_soloq: + string += f"Solo: {obj.rank_soloq}\n" + if obj.rank_flexq: + string += f"Flex: {obj.rank_flexq}\n" + return string - async def _update(self, lol: LeagueOfLegends): - log.info(f"Updating: {lol}") - log.debug(f"Getting summoner data: {lol}") - summoner = await asyncify(self._riotwatcher.summoner.by_id, region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) - await self._change(lol, "profile_icon_id", summoner["profileIconId"], self._notify) - await self._change(lol, "summoner_name", summoner["name"], self._notify) - await self._change(lol, "puuid", summoner["puuid"], self._notify) - await self._change(lol, "summoner_level", summoner["summonerLevel"], self._notify) - await self._change(lol, "summoner_id", summoner["id"], self._notify) - await self._change(lol, "account_id", summoner["accountId"], self._notify) - log.debug(f"Getting leagues data: {lol}") - leagues = await asyncify(self._riotwatcher.league.by_summoner, region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) + async def get_updatables_of_user(self, session, user: rbt.User) -> List[LeagueOfLegends]: + return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).filter_by(user=user).all) + + async def get_updatables(self, session) -> List[LeagueOfLegends]: + return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).all) + + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[LeagueOfLegends]: + name = args.joined() + + # Connect a new League of Legends account to Royalnet + log.debug(f"Searching for: {name}") + summoner = self._lolwatcher.summoner.by_name(region=self.region(), summoner_name=name) + # Ensure the account isn't already connected to something else + leagueoflegends = await ru.asyncify( + session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none) + if leagueoflegends: + raise rc.CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.") + # Get rank information + log.debug(f"Getting leagues data: {name}") + leagues = self._lolwatcher.league.by_summoner(region=self.region(), + encrypted_summoner_id=summoner["id"]) soloq = LeagueLeague() flexq = LeagueLeague() twtrq = LeagueLeague() @@ -105,118 +92,80 @@ class LeagueoflegendsCommand(Command): twtrq = LeagueLeague.from_dict(league) if league["queueType"] == "RANKED_TFT": tftq = LeagueLeague.from_dict(league) - await self._change(lol, "rank_soloq", soloq, self._notify) - await self._change(lol, "rank_flexq", flexq, self._notify) - await self._change(lol, "rank_twtrq", twtrq, self._notify) - await self._change(lol, "rank_tftq", tftq, self._notify) - log.debug(f"Getting mastery data: {lol}") - mastery = await asyncify(self._riotwatcher.champion_mastery.scores_by_summoner, - region=self.config["Lol"]["region"], - encrypted_summoner_id=lol.summoner_id) - await self._change(lol, "mastery_score", mastery, self._notify) + # Get mastery score + log.debug(f"Getting mastery data: {name}") + mastery = self._lolwatcher.champion_mastery.scores_by_summoner(region=self.region(), + encrypted_summoner_id=summoner["id"]) + # Create database row + leagueoflegends = self.alchemy.get(LeagueOfLegends)( + region=self.region(), + user=user, + profile_icon_id=summoner["profileIconId"], + summoner_name=summoner["name"], + puuid=summoner["puuid"], + summoner_level=summoner["summonerLevel"], + summoner_id=summoner["id"], + account_id=summoner["accountId"], + rank_soloq=soloq, + rank_flexq=flexq, + rank_twtrq=twtrq, + rank_tftq=tftq, + mastery_score=mastery + ) - async def _updater(self, period: int): - log.info(f"Started updater with {period}s period") - while True: - log.info(f"Updating...") - session = self.alchemy.Session() - log.info("") - lols = session.query(self.alchemy.get(LeagueOfLegends)).all() - for lol in lols: - try: - await self._update(lol) - except Exception as e: - sentry_sdk.capture_exception(e) - log.error(f"Error while updating {lol.user.username}: {e}") - await asyncio.sleep(1) - await asyncify(session.commit) - session.close() - log.info(f"Sleeping for {period}s") - await asyncio.sleep(period) + await FiorygiTransaction.spawn_fiorygi( + data=data, + user=user, + qty=1, + reason="aver collegato a Royalnet il proprio account di League of Legends" + ) - @staticmethod - def _display(lol: LeagueOfLegends) -> str: - string = f"ℹ️ [b]{lol.summoner_name}[/b]\n" \ - f"Lv. {lol.summoner_level}\n" \ - f"Mastery score: {lol.mastery_score}\n" \ - f"\n" - if lol.rank_soloq: - string += f"Solo: {lol.rank_soloq}\n" - if lol.rank_flexq: - string += f"Flex: {lol.rank_flexq}\n" - if lol.rank_twtrq: - string += f"3v3: {lol.rank_twtrq}\n" - if lol.rank_tftq: - string += f"TFT: {lol.rank_tftq}\n" - return string + session.add(leagueoflegends) + return leagueoflegends - async def run(self, args: CommandArgs, data: CommandData) -> None: - author = await data.get_author(error_if_none=True) + async def update(self, session, obj: LeagueOfLegends, change: Callable[[str, Any], Awaitable[None]]): + log.debug(f"Getting summoner data: {obj}") + summoner = await ru.asyncify(self._lolwatcher.summoner.by_id, region=self.region(), + encrypted_summoner_id=obj.summoner_id) + await change("profile_icon_id", summoner["profileIconId"]) + await change("summoner_name", summoner["name"]) + await change("puuid", summoner["puuid"]) + await change("summoner_level", summoner["summonerLevel"]) + await change("summoner_id", summoner["id"]) + await change("account_id", summoner["accountId"]) + log.debug(f"Getting leagues data: {obj}") + leagues = await ru.asyncify(self._lolwatcher.league.by_summoner, region=self.region(), + encrypted_summoner_id=obj.summoner_id) + soloq = LeagueLeague() + flexq = LeagueLeague() + for league in leagues: + if league["queueType"] == "RANKED_SOLO_5x5": + soloq = LeagueLeague.from_dict(league) + if league["queueType"] == "RANKED_FLEX_SR": + flexq = LeagueLeague.from_dict(league) + await change("rank_soloq", soloq) + await change("rank_flexq", flexq) + log.debug(f"Getting mastery data: {obj}") + mastery = await ru.asyncify(self._lolwatcher.champion_mastery.scores_by_summoner, + region=self.region(), + encrypted_summoner_id=obj.summoner_id) + await change("mastery_score", mastery) - name = args.joined() + async def on_increase(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"📈 [b]{obj.user}[/b] è salito a {new} su League of Legends ({self.queue_names[attribute]})! Congratulazioni!") - if name: - # Connect a new League of Legends account to Royalnet - log.debug(f"Searching for: {name}") - summoner = self._riotwatcher.summoner.by_name(region=self.config["Lol"]["region"], summoner_name=name) - # Ensure the account isn't already connected to something else - leagueoflegends = await asyncify( - data.session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none) - if leagueoflegends: - raise CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.") - # Get rank information - log.debug(f"Getting leagues data: {name}") - leagues = self._riotwatcher.league.by_summoner(region=self.config["Lol"]["region"], - encrypted_summoner_id=summoner["id"]) - soloq = LeagueLeague() - flexq = LeagueLeague() - twtrq = LeagueLeague() - tftq = LeagueLeague() - for league in leagues: - if league["queueType"] == "RANKED_SOLO_5x5": - soloq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_FLEX_SR": - flexq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_FLEX_TT": - twtrq = LeagueLeague.from_dict(league) - if league["queueType"] == "RANKED_TFT": - tftq = LeagueLeague.from_dict(league) - # Get mastery score - log.debug(f"Getting mastery data: {name}") - mastery = self._riotwatcher.champion_mastery.scores_by_summoner(region=self.config["Lol"]["region"], - encrypted_summoner_id=summoner["id"]) - # Create database row - leagueoflegends = self.alchemy.get(LeagueOfLegends)( - region=self.config["Lol"]["region"], - user=author, - profile_icon_id=summoner["profileIconId"], - summoner_name=summoner["name"], - puuid=summoner["puuid"], - summoner_level=summoner["summonerLevel"], - summoner_id=summoner["id"], - account_id=summoner["accountId"], - rank_soloq=soloq, - rank_flexq=flexq, - rank_twtrq=twtrq, - rank_tftq=tftq, - mastery_score=mastery - ) - log.debug(f"Saving to the DB: {name}") - data.session.add(leagueoflegends) - await data.session_commit() - await data.reply(f"↔️ Account {leagueoflegends} connesso a {author}!") - else: - # Update and display the League of Legends stats for the current account - if len(author.leagueoflegends) == 0: - raise UserError("Nessun account di League of Legends trovato.") - message = "" - for account in author.leagueoflegends: - try: - await self._update(account) - message += self._display(account) - except riotwatcher.ApiError as e: - message += f"⚠️ [b]{account.summoner_name}[/b]\n" \ - f"{e}" - message += "\n" - await data.session_commit() - await data.reply(message) + async def on_unchanged(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a {new} su League of Legends ({self.queue_names[attribute]}).") + + async def on_first(self, session, obj: LeagueOfLegends, attribute: str, old: None, new: Any) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"🌟 [b]{obj.user}[/b] si è classificato {new} su League of Legends ({self.queue_names[attribute]}!") + + async def on_reset(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: None) -> None: + if attribute in self.queue_names.keys(): + await self.notify(f"⬜️ [b]{obj.user}[/b] non ha più un rank su League of Legends ({self.queue_names[attribute]}).") diff --git a/royalpack/commands/magickfiorygi.py b/royalpack/commands/magickfiorygi.py new file mode 100644 index 00000000..3f98f64e --- /dev/null +++ b/royalpack/commands/magickfiorygi.py @@ -0,0 +1,40 @@ +from typing import * +import royalnet.commands as rc +import royalnet.backpack.tables as rbt + +from ..tables import FiorygiTransaction + + +class MagickfiorygiCommand(rc.Command): + name: str = "magickfiorygi" + + description: str = "Crea fiorygi dal nulla." + + syntax: str = "{destinatario} {quantità} {motivo}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author(error_if_none=True) + if "banker" not in author.roles: + raise rc.UserError("Non hai permessi sufficienti per eseguire questo comando.") + + user_arg = args[0] + qty_arg = args[1] + reason_arg = " ".join(args[2:]) + + if user_arg is None: + raise rc.InvalidInputError("Non hai specificato un destinatario!") + user = await rbt.User.find(self.alchemy, data.session, user_arg) + if user is None: + raise rc.InvalidInputError("L'utente specificato non esiste!") + + if qty_arg is None: + raise rc.InvalidInputError("Non hai specificato una quantità!") + try: + qty = int(qty_arg) + except ValueError: + raise rc.InvalidInputError("La quantità specificata non è un numero!") + + if reason_arg == "": + raise rc.InvalidInputError("Non hai specificato un motivo!") + + await FiorygiTransaction.spawn_fiorygi(data, user, qty, reason_arg) diff --git a/royalpack/commands/magicktreasure.py b/royalpack/commands/magicktreasure.py new file mode 100644 index 00000000..8752d08a --- /dev/null +++ b/royalpack/commands/magicktreasure.py @@ -0,0 +1,53 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import Treasure + + +class MagicktreasureCommand(rc.Command): + name: str = "magicktreasure" + + description: str = "Crea un nuovo Treasure di Fiorygi (senza spendere i tuoi)." + + syntax: str = "{codice} {valore}" + + async def _permission_check(self, author, code, value, data): + if "banker" not in author.roles: + raise rc.UserError("Non hai permessi sufficienti per eseguire questo comando.") + return author + + async def _create_treasure(self, author, code, value, data): + TreasureT = self.alchemy.get(Treasure) + + treasure = await ru.asyncify(data.session.query(TreasureT).get, code) + if treasure is not None: + raise rc.UserError("Esiste già un Treasure con quel codice.") + + treasure = TreasureT( + code=code, + value=value, + redeemed_by=None + ) + + return treasure + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + await data.delete_invoking() + author = await data.get_author(error_if_none=True) + + code = args[0].lower() + try: + value = int(args[1]) + except ValueError: + raise rc.InvalidInputError("Il valore deve essere maggiore o uguale a 0.") + if value < 0: + raise rc.InvalidInputError("Il valore deve essere maggiore o uguale a 0.") + + await self._permission_check(author, code, value, data) + treasure = await self._create_treasure(author, code, value, data) + + data.session.add(treasure) + await data.session_commit() + + await data.reply("✅ Treasure creato!") diff --git a/royalpack/commands/matchmaking.py b/royalpack/commands/matchmaking.py new file mode 100644 index 00000000..fa601be5 --- /dev/null +++ b/royalpack/commands/matchmaking.py @@ -0,0 +1,89 @@ +from typing import * +import datetime +import re +import dateparser +import typing +import royalnet.commands as rc + +from ..tables import MMEvent +from ..utils import MMTask + + +class MatchmakingCommand(rc.Command): + name: str = "matchmaking" + + description: str = "Cerca persone per una partita a qualcosa!" + + syntax: str = "" + + aliases = ["mm", "lfg"] + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + + # Find all active MMEvents and run the tasks for them + session = self.alchemy.Session() + + # Create a new MMEvent and run it + if self.interface.name == "telegram": + MMEventT = self.alchemy.get(MMEvent) + active_mmevents = ( + session + .query(MMEventT) + .filter( + MMEventT.interface == self.interface.name, + MMEventT.interrupted == False + ) + .all() + ) + for mmevent in active_mmevents: + task = MMTask(mmevent.mmid, command=self) + task.start() + + @staticmethod + def _parse_args(args) -> Tuple[Optional[datetime.datetime], str, str]: + """Parse command arguments, either using the standard syntax or the Proto syntax.""" + try: + timestring, title, description = args.match(r"(?:\[\s*([^]]+)\s*]\s*)?([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL) + except rc.InvalidInputError: + timestring, title, description = args.match(r"(?:\s*(.+?)\s*\n\s*)?([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL) + if timestring is not None: + try: + dt: typing.Optional[datetime.datetime] = dateparser.parse(timestring, settings={ + "PREFER_DATES_FROM": "future" + }) + except OverflowError: + dt = None + if dt is None: + raise rc.InvalidInputError("La data che hai specificato non è valida.") + if dt <= datetime.datetime.now(): + raise rc.InvalidInputError("La data che hai specificato è nel passato.") + if dt - datetime.datetime.now() >= datetime.timedelta(days=366): + raise rc.InvalidInputError("Hai specificato una data tra più di un anno!\n" + "Se volevi scrivere un'orario, ricordati che le ore sono separate da " + "due punti (:) e non da punto semplice!") + else: + dt = None + return dt, title, description + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + """Handle a matchmaking command call.""" + author = await data.get_author(error_if_none=True) + + # Parse the arguments, either with the standard syntax or with the Proto syntax + dt, title, description = self._parse_args(args) + + # Add the MMEvent to the database + mmevent: MMEvent = self.alchemy.get(MMEvent)(creator=author, + datetime=dt, + title=title, + description=description, + interface=self.interface.name) + data.session.add(mmevent) + await data.session_commit() + + # Create and run a task for the newly created MMEvent + task = MMTask(mmevent.mmid, command=self) + task.start() + + await data.reply(f"🚩 Matchmaking creato!") diff --git a/royalpack/commands/osu.py b/royalpack/commands/osu.py new file mode 100644 index 00000000..dff74519 --- /dev/null +++ b/royalpack/commands/osu.py @@ -0,0 +1,125 @@ +from typing import * +import itsdangerous +import aiohttp + +from royalnet.backpack import tables as rbt +import royalnet.commands as rc +import royalnet.utils as ru + +from .abstract.linker import LinkerCommand +from ..types import Updatable +from ..tables import Osu +from ..stars.api_auth_login_osu import ApiAuthLoginOsuStar + + +class OsuCommand(LinkerCommand): + name = "osu" + + description = "Connetti e sincronizza il tuo account di osu!" + + @property + def client_id(self): + return self.config[self.name]['client_id'] + + @property + def client_secret(self): + return self.config[self.name]['client_secret'] + + @property + def base_url(self): + return self.config['base_url'] + + @property + def secret_key(self): + return self.config['secret_key'] + + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Osu]: + return user.osu + + async def get_updatables(self, session) -> List[Osu]: + return await ru.asyncify(session.query(self.alchemy.get(Osu)).all) + + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[Osu]: + serializer = itsdangerous.URLSafeSerializer(self.secret_key, salt="osu") + # TODO: Ensure the chat the link is being sent in is secure!!! + await data.reply("🔑 [b]Login necessario[/b]\n" + f"[url=https://osu.ppy.sh/oauth/authorize" + f"?client_id={self.client_id}" + f"&redirect_uri={self.base_url}{ApiAuthLoginOsuStar.path}" + f"&response_type=code" + f"&state={serializer.dumps(user.uid)}]" + f"Connetti account di osu! a {user.username}" + f"[/url]") + return None + + async def update(self, session, obj: Osu, change: Callable[[str, Any], Awaitable[None]]): + await obj.refresh_if_expired(client_id=self.client_id, + client_secret=self.client_secret, + base_url=self.base_url, + path=ApiAuthLoginOsuStar.path) + async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {obj.access_token}"}) as session: + async with session.get("https://osu.ppy.sh/api/v2/me/osu") as response: + m = await response.json() + obj.avatar_url = m["avatar_url"] + obj.username = m["username"] + if "statistics" in m: + await change("standard_pp", m["statistics"].get("pp")) + async with session.get("https://osu.ppy.sh/api/v2/me/taiko") as response: + m = await response.json() + if "statistics" in m: + await change("taiko_pp", m["statistics"].get("pp")) + async with session.get("https://osu.ppy.sh/api/v2/me/fruits") as response: + m = await response.json() + if "statistics" in m: + await change("catch_pp", m["statistics"].get("pp")) + async with session.get("https://osu.ppy.sh/api/v2/me/mania") as response: + m = await response.json() + if "statistics" in m: + await change("mania_pp", m["statistics"].get("pp")) + + async def on_increase(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None: + if attribute == "standard_pp": + await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu![/i]! Congratulazioni!") + elif attribute == "taiko_pp": + await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!taiko[/i]! Congratulazioni!") + elif attribute == "catch_pp": + await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!catch[/i]! Congratulazioni!") + elif attribute == "mania_pp": + await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!mania[/i]! Congratulazioni!") + + async def on_unchanged(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None: + if attribute == "standard_pp": + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu![/i].") + elif attribute == "taiko_pp": + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!taiko[/i].") + elif attribute == "catch_pp": + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!catch[/i].") + elif attribute == "mania_pp": + await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!mania[/i].") + + async def on_first(self, session, obj: Osu, attribute: str, old: None, new: Any) -> None: + if attribute == "standard_pp": + await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu![/i]!") + elif attribute == "taiko_pp": + await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!taiko[/i]!") + elif attribute == "catch_pp": + await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!catch[/i]!") + elif attribute == "mania_pp": + await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!mania[/i]!") + + async def on_reset(self, session, obj: Osu, attribute: str, old: Any, new: None) -> None: + if attribute == "standard_pp": + await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu![/i].") + elif attribute == "taiko_pp": + await self.notify(f" ⬜️[b]{obj.user}[/b] non è più classificato su [i]osu!taiko[/i].") + elif attribute == "catch_pp": + await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu!catch[/i].") + elif attribute == "mania_pp": + await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu!mania[/i].") diff --git a/royalpack/commands/pause.py b/royalpack/commands/pause.py deleted file mode 100644 index d4f6fc6a..00000000 --- a/royalpack/commands/pause.py +++ /dev/null @@ -1,27 +0,0 @@ -import discord -from typing import * -from royalnet.commands import * - - -class PauseCommand(Command): - name: str = "pause" - - aliases = ["resume"] - - description: str = "Metti in pausa o riprendi la riproduzione di un file." - - async def run(self, args: CommandArgs, data: CommandData) -> None: - if self.interface.name == "discord": - message: discord.Message = data.message - guild: discord.Guild = message.guild - guild_id: Optional[int] = guild.id - else: - guild_id = None - response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_pause", - guild_id=guild_id) - - if response["action"] == "paused": - await data.reply("⏸ Riproduzione messa in pausa.") - - elif response["action"] == "resumed": - await data.reply("▶️ Riproduzione ripresa!") diff --git a/royalpack/commands/peertube.py b/royalpack/commands/peertube.py deleted file mode 100644 index 9a8648dc..00000000 --- a/royalpack/commands/peertube.py +++ /dev/null @@ -1,24 +0,0 @@ -from .play import PlayCommand -from royalnet.commands import * -import aiohttp -import urllib.parse - - -class PeertubeCommand(PlayCommand): - name: str = "peertube" - - aliases = ["pt", "royaltube", "rt"] - - description: str = "Cerca un video su RoyalTube e lo aggiunge alla coda della chat vocale." - - syntax = "{ricerca}" - - async def get_url(self, args): - search = urllib.parse.quote(args.joined(require_at_least=1)) - async with aiohttp.ClientSession() as session: - async with session.get(self.config["Peertube"]["instance_url"] + - f"/api/v1/search/videos?search={search}") as response: - j = await response.json() - if j["total"] < 1: - raise InvalidInputError("Nessun video trovato.") - return f'{self.config["Peertube"]["instance_url"]}/videos/watch/{j["data"][0]["uuid"]}' diff --git a/royalpack/commands/peertubeupdates.py b/royalpack/commands/peertubeupdates.py index 03e37078..6c19a196 100644 --- a/royalpack/commands/peertubeupdates.py +++ b/royalpack/commands/peertubeupdates.py @@ -1,16 +1,17 @@ +from typing import * import aiohttp import asyncio import datetime import logging import dateparser -from royalnet.commands import * -from royalnet.serf.telegram.escape import escape +import royalnet.commands as rc +import royalnet.serf.telegram as rst log = logging.getLogger(__name__) -class PeertubeUpdatesCommand(Command): +class PeertubeUpdatesCommand(rc.Command): name: str = "peertubeupdates" description: str = "Guarda quando è uscito l'ultimo video su PeerTube." @@ -21,7 +22,7 @@ class PeertubeUpdatesCommand(Command): _latest_date: datetime.datetime = None - def __init__(self, interface: CommandInterface): + def __init__(self, interface: rc.CommandInterface): super().__init__(interface) if self.interface.name == "telegram": self.loop.create_task(self._ready_up()) @@ -33,6 +34,8 @@ class PeertubeUpdatesCommand(Command): async with session.get(self.config["Peertube"]["instance_url"] + "/feeds/videos.json?sort=-publishedAt&filter=local") as response: log.debug("Parsing jsonfeed") + if response.status != 200: + raise rc.ExternalError("Peertube is unavailable") j = await response.json() log.debug("Jsonfeed parsed successfully") return j @@ -41,14 +44,14 @@ class PeertubeUpdatesCommand(Command): client = self.interface.bot.client await self.interface.bot.safe_api_call(client.send_message, chat_id=self.config["Telegram"]["main_group_id"], - text=escape(message), + text=rst.escape(message), parse_mode="HTML", disable_webpage_preview=True) async def _ready_up(self): j = await self._get_json() if j["version"] != "https://jsonfeed.org/version/1": - raise ConfigurationError("url is not a jsonfeed") + raise rc.ConfigurationError("url is not a jsonfeed") videos = j["items"] for video in reversed(videos): date_modified = dateparser.parse(video["date_modified"]) @@ -72,7 +75,7 @@ class PeertubeUpdatesCommand(Command): f"{video['url']}") await asyncio.sleep(self.config["Peertube"]["feed_update_timeout"]) - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: if self.interface.name != "telegram": - raise UnsupportedError() + raise rc.UnsupportedError() await data.reply(f"ℹ️ Ultimo video caricato il: [b]{self._latest_date.isoformat()}[/b]") diff --git a/royalpack/commands/ping.py b/royalpack/commands/ping.py new file mode 100644 index 00000000..576f6e63 --- /dev/null +++ b/royalpack/commands/ping.py @@ -0,0 +1,41 @@ +import datetime +import asyncio +from typing import * +import royalnet +import royalnet.commands as rc + + +class PingCommand(rc.Command): + name: str = "ping" + + description: str = "Display the status of the Herald network." + + syntax: str = "" + + _targets = ["telegram", "discord", "matrix", "constellation"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + await data.reply("📶 Ping...") + + tasks = {} + + start = datetime.datetime.now() + for target in self._targets: + tasks[target] = self.loop.create_task(self.interface.call_herald_event(target, "pong")) + + await asyncio.sleep(10) + + lines = ["📶 [b]Pong![/b]", ""] + + for name, task in tasks.items(): + try: + d = task.result() + except (asyncio.CancelledError, asyncio.InvalidStateError): + lines.append(f"🔴 [c]{name}[/c]") + else: + end = datetime.datetime.fromtimestamp(d["timestamp"]) + delta = end - start + + lines.append(f"🔵 [c]{name}[/c] ({delta.microseconds // 1000} ms)") + + await data.reply("\n".join(lines)) diff --git a/royalpack/commands/play.py b/royalpack/commands/play.py deleted file mode 100644 index 90ca08e5..00000000 --- a/royalpack/commands/play.py +++ /dev/null @@ -1,62 +0,0 @@ -import pickle -import base64 -import discord -from typing import * -from royalnet.commands import * -from royalnet.utils import * - - -class PlayCommand(Command): - name: str = "play" - - aliases = ["p"] - - description: str = "Aggiunge un url alla coda della chat vocale." - - syntax = "{url}" - - async def get_url(self, args: CommandArgs): - return args.joined() - - async def run(self, args: CommandArgs, data: CommandData) -> None: - # if not (url.startswith("http://") or url.startswith("https://")): - # raise CommandError(f"Il comando [c]{self.interface.prefix}play[/c] funziona solo per riprodurre file da" - # f" un URL.\n" - # f"Se vuoi cercare un video, come misura temporanea puoi usare " - # f"[c]ytsearch:nomevideo[/c] o [c]scsearch:nomevideo[/c] come url.") - if self.interface.name == "discord": - message: discord.Message = data.message - guild: discord.Guild = message.guild - if guild is None: - guild_id = None - else: - guild_id: Optional[int] = guild.id - else: - guild_id = None - response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_play", - url=await self.get_url(args), - guild_id=guild_id) - - too_long: List[Dict[str, Any]] = response["too_long"] - if len(too_long) > 0: - await data.reply(f"⚠ {len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}" - f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}" - f" perchè durava{'' if len(too_long) == 1 else 'no'}" - f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi.") - - added: List[Dict[str, Any]] = response["added"] - if len(added) > 0: - reply = f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file alla coda:\n" - if self.interface.name == "discord": - await data.reply(reply) - for item in added: - embed = pickle.loads(base64.b64decode(bytes(item["stringified_base64_pickled_discord_embed"], - encoding="ascii"))) - # noinspection PyUnboundLocalVariable - await message.channel.send(embed=embed) - else: - reply += numberemojiformat([a["title"] for a in added]) - await data.reply(reply) - - if len(added) + len(too_long) == 0: - raise ExternalError("Nessun video trovato.") diff --git a/royalpack/commands/pmots.py b/royalpack/commands/pmots.py index 91f5ea54..e32cd289 100644 --- a/royalpack/commands/pmots.py +++ b/royalpack/commands/pmots.py @@ -1,11 +1,11 @@ from typing import * -from royalnet.commands import * +import royalnet.commands as rc -class PmotsCommand(Command): +class PmotsCommand(rc.Command): name: str = "pmots" description: str = "Confondi Proto!" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: await data.reply("👣 pmots pmots") diff --git a/royalpack/commands/queue.py b/royalpack/commands/queue.py deleted file mode 100644 index 751e2180..00000000 --- a/royalpack/commands/queue.py +++ /dev/null @@ -1,62 +0,0 @@ -import pickle -import base64 -import discord -from typing import * -from royalnet.commands import * -from royalnet.utils import * - - -class QueueCommand(Command): - name: str = "queue" - - aliases = ["q"] - - description: str = "Visualizza la coda di riproduzione attuale.." - - async def run(self, args: CommandArgs, data: CommandData) -> None: - if self.interface.name == "discord": - message: discord.Message = data.message - guild: discord.Guild = message.guild - guild_id: Optional[int] = guild.id - else: - guild_id = None - response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_queue", - guild_id=guild_id) - - queue_type = response["type"] - if queue_type == "RoyalQueue": - next_up = response["next_up"] - now_playing = response["now_playing"] - await data.reply(f"ℹ️ La coda contiene {len(next_up)} file.\n\n") - - if now_playing is not None: - reply = f"Attualmente, sta venendo riprodotto:\n" - if self.interface.name == "discord": - await data.reply(reply) - embed = pickle.loads(base64.b64decode(bytes(now_playing["stringified_base64_pickled_discord_embed"], - encoding="ascii"))) - # noinspection PyUnboundLocalVariable - await message.channel.send(embed=embed) - else: - reply += f"▶️ {now_playing['title']}\n\n" - await data.reply(reply) - else: - await data.reply("⏹ Attualmente, non sta venendo riprodotto nulla.") - - reply = "" - if len(next_up) >= 1: - reply += "I prossimi file in coda sono:\n" - if self.interface.name == "discord": - await data.reply(reply) - for item in next_up[:5]: - embed = pickle.loads(base64.b64decode(bytes(item["stringified_base64_pickled_discord_embed"], - encoding="ascii"))) - # noinspection PyUnboundLocalVariable - await message.channel.send(embed=embed) - else: - reply += numberemojiformat([a["title"] for a in next_up[:5]]) - await data.reply(reply) - else: - await data.reply("ℹ️ Non ci sono altri file in coda.") - else: - raise CommandError(f"Non so come visualizzare il contenuto di un [c]{queue_type}[/c].") diff --git a/royalpack/commands/rage.py b/royalpack/commands/rage.py index fa4a991a..c9f84280 100644 --- a/royalpack/commands/rage.py +++ b/royalpack/commands/rage.py @@ -1,20 +1,22 @@ -import typing +from typing import * import random -from royalnet.commands import * +import royalnet.commands as rc -class RageCommand(Command): +class RageCommand(rc.Command): name: str = "rage" aliases = ["balurage", "madden"] description: str = "Arrabbiati per qualcosa, come una software house californiana." - _MAD = ["MADDEN MADDEN MADDEN MADDEN", - "EA bad, praise Geraldo!", - "Stai sfogando la tua ira sul bot!", - "Basta, io cambio gilda!", - "Fondiamo la RRYG!"] + _MAD = [ + "MADDEN MADDEN MADDEN MADDEN", + "EA bad, praise Geraldo!", + "Stai sfogando la tua ira sul bot!", + "Basta, io cambio gilda!", + "Fondiamo la RRYG!" + ] - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: await data.reply(f"😠 {random.sample(self._MAD, 1)[0]}") diff --git a/royalpack/commands/reminder.py b/royalpack/commands/reminder.py index e5c63497..48d7075a 100644 --- a/royalpack/commands/reminder.py +++ b/royalpack/commands/reminder.py @@ -1,18 +1,19 @@ -import typing +from typing import * import dateparser import datetime import pickle import telegram import discord from sqlalchemy import and_ -from royalnet.commands import * -from royalnet.utils import * +import royalnet.commands as rc +import royalnet.utils as ru from royalnet.serf.telegram import escape as telegram_escape from royalnet.serf.discord import escape as discord_escape + from ..tables import Reminder -class ReminderCommand(Command): +class ReminderCommand(rc.Command): name: str = "reminder" aliases = ["calendar"] @@ -21,7 +22,7 @@ class ReminderCommand(Command): syntax: str = "[ {data} ] {messaggio}" - def __init__(self, interface: CommandInterface): + def __init__(self, interface: rc.CommandInterface): super().__init__(interface) session = interface.alchemy.Session() reminders = ( @@ -35,9 +36,9 @@ class ReminderCommand(Command): interface.loop.create_task(self._remind(reminder)) async def _remind(self, reminder): - await sleep_until(reminder.datetime) + await ru.sleep_until(reminder.datetime) if self.interface.name == "telegram": - chat_id: int = pickle.loads(reminder.raw_interface_data) + chat_id: int = pickle.loads(reminder.interface_data) client: telegram.Bot = self.serf.client await self.serf.api_call(client.send_message, chat_id=chat_id, @@ -45,19 +46,19 @@ class ReminderCommand(Command): parse_mode="HTML", disable_web_page_preview=True) elif self.interface.name == "discord": - channel_id: int = pickle.loads(reminder.raw_interface_data) + channel_id: int = pickle.loads(reminder.interface_data) client: discord.Client = self.serf.client channel = client.get_channel(channel_id) await channel.send(discord_escape(f"❗️ {reminder.message}")) - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: try: date_str, reminder_text = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*") - except InvalidInputError: + except rc.InvalidInputError: date_str, reminder_text = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*") try: - date: typing.Optional[datetime.datetime] = dateparser.parse(date_str, settings={ + date: Optional[datetime.datetime] = dateparser.parse(date_str, settings={ "PREFER_DATES_FROM": "future" }) except OverflowError: @@ -70,11 +71,11 @@ class ReminderCommand(Command): return await data.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]") if self.interface.name == "telegram": - interface_data = pickle.dumps(data.update.effective_chat.id) + interface_data = pickle.dumps(data.message.chat.id) elif self.interface.name == "discord": interface_data = pickle.dumps(data.message.channel.id) else: - raise UnsupportedError("This command does not support the current interface.") + raise rc.UnsupportedError("This command does not support the current interface.") creator = await data.get_author() reminder = self.interface.alchemy.get(Reminder)(creator=creator, interface_name=self.interface.name, @@ -83,4 +84,4 @@ class ReminderCommand(Command): message=reminder_text) self.interface.loop.create_task(self._remind(reminder)) data.session.add(reminder) - await asyncify(data.session.commit) + await ru.asyncify(data.session.commit) diff --git a/royalpack/commands/royalpackversion.py b/royalpack/commands/royalpackversion.py new file mode 100644 index 00000000..f2b17584 --- /dev/null +++ b/royalpack/commands/royalpackversion.py @@ -0,0 +1,25 @@ +from typing import * + +import pkg_resources +import royalnet.commands as rc + + +class RoyalpackCommand(rc.Command): + name: str = "royalpackversion" + + description: str = "Visualizza la versione attuale di Royalpack." + + syntax: str = "" + + @property + def royalpack_version(self): + return pkg_resources.get_distribution("royalpack").version + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + if __debug__: + message = f"ℹ️ Royalpack [url=https://github.com/Steffo99/royalpack/]Unreleased[/url]\n" + else: + message = f"ℹ️ Royalpack [url=https://github.com/Steffo99/royalpack/releases/tag/{self.royalpack_version}]{self.royalpack_version}[/url]\n" + if "69" in semantic: + message += "(Nice.)" + await data.reply(message) diff --git a/royalpack/commands/ship.py b/royalpack/commands/ship.py index ec681438..276998f8 100644 --- a/royalpack/commands/ship.py +++ b/royalpack/commands/ship.py @@ -1,15 +1,16 @@ +from typing import * import re -from royalnet.commands import * +import royalnet.commands as rc -class ShipCommand(Command): +class ShipCommand(rc.Command): name: str = "ship" description: str = "Crea una ship tra due nomi." syntax = "{nomeuno} {nomedue}" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: name_one = args[0] name_two = args[1] if name_two == "+": diff --git a/royalpack/commands/skip.py b/royalpack/commands/skip.py deleted file mode 100644 index 8b9a0f38..00000000 --- a/royalpack/commands/skip.py +++ /dev/null @@ -1,21 +0,0 @@ -import discord -from typing import * -from royalnet.commands import * - - -class SkipCommand(Command): - name: str = "skip" - - aliases = ["s"] - - description: str = "Salta il file attualmente in riproduzione." - - async def run(self, args: CommandArgs, data: CommandData) -> None: - if self.interface.name == "discord": - message: discord.Message = data.message - guild: discord.Guild = message.guild - guild_id: Optional[int] = guild.id - else: - guild_id = None - response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_skip", guild_id=guild_id) - await data.reply("⏩ File attuale saltato!") diff --git a/royalpack/commands/smecds.py b/royalpack/commands/smecds.py index 7789cdf1..9305bf7d 100644 --- a/royalpack/commands/smecds.py +++ b/royalpack/commands/smecds.py @@ -1,9 +1,9 @@ -import typing +from typing import * import random -from royalnet.commands import * +import royalnet.commands as rc -class SmecdsCommand(Command): +class SmecdsCommand(rc.Command): name: str = "smecds" aliases = ["secondomeecolpadellostagista"] @@ -61,6 +61,6 @@ class SmecdsCommand(Command): "dello Slime God", "del salassato", "della salsa", "di Senjougahara", "di Sugar", "della Stampa", "della Stampante"] - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: ds = random.sample(self._DS_LIST, 1)[0] await data.reply(f"🤔 Secondo me, è colpa {ds}.") diff --git a/royalpack/commands/soundcloud.py b/royalpack/commands/soundcloud.py deleted file mode 100644 index 0090e3dd..00000000 --- a/royalpack/commands/soundcloud.py +++ /dev/null @@ -1,14 +0,0 @@ -from .play import PlayCommand - - -class SoundcloudCommand(PlayCommand): - name: str = "soundcloud" - - aliases = ["sc"] - - description: str = "Cerca un video su SoundCloud e lo aggiunge alla coda della chat vocale." - - syntax = "{ricerca}" - - async def get_url(self, args): - return f"scsearch:{args.joined()}" diff --git a/royalpack/commands/spell.py b/royalpack/commands/spell.py index 6d68bf00..e815bfd2 100644 --- a/royalpack/commands/spell.py +++ b/royalpack/commands/spell.py @@ -1,19 +1,17 @@ from typing import * -from royalnet.commands import * -from royalnet.utils import * -from royalnet.backpack.tables import User -from sqlalchemy import func +import royalnet.commands as rc +import royalnet.utils as ru import royalspells as rs -class SpellCommand(Command): +class SpellCommand(rc.Command): name: str = "spell" description: str = "Genera casualmente una spell!" syntax = "{nome_spell}" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: spell_name = args.joined(require_at_least=1) spell = rs.Spell(spell_name) @@ -23,7 +21,7 @@ class SpellCommand(Command): dmg: rs.DamageComponent = spell.damage_component constant_str: str = f"{dmg.constant:+d}" if dmg.constant != 0 else "" rows.append(f"Danni: [b]{dmg.dice_number}d{dmg.dice_type}{constant_str}[/b]" - f" {andformat(dmg.damage_types, final=' e ')}") + f" {ru.andformat(dmg.damage_types, final=' e ')}") rows.append(f"Precisione: [b]{dmg.miss_chance}%[/b]") if dmg.repeat > 1: rows.append(f"Multiattacco: [b]×{dmg.repeat}[/b]") @@ -39,7 +37,7 @@ class SpellCommand(Command): stats: rs.StatsComponent = spell.stats_component rows.append("Il caster riceve: ") for stat_name in stats.stat_changes: - rows.append(f"[b]{stats.stat_changes[stat_name]}{stat_name}[/b]") + rows.append(f"[b]{stat_name}{stats.stat_changes[stat_name]}[/b]") rows.append("") if spell.status_effect_component: diff --git a/royalpack/commands/steammatch.py b/royalpack/commands/steammatch.py new file mode 100644 index 00000000..90cb2171 --- /dev/null +++ b/royalpack/commands/steammatch.py @@ -0,0 +1,102 @@ +from typing import * +import steam.webapi +import requests.exceptions +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.backpack.tables as rbt + +from ..tables import Steam + + +class SteamGame: + def __init__(self, + appid=None, + name=None, + playtime_forever=None, + img_icon_url=None, + img_logo_url=None, + has_community_visible_stats=None, + playtime_windows_forever=None, + playtime_mac_forever=None, + playtime_linux_forever=None, + playtime_2weeks=None): + self.appid = appid + self.name = name + self.playtime_forever = playtime_forever + self.img_icon_url = img_icon_url + self.img_logo_url = img_logo_url + self.has_community_visible_stats = has_community_visible_stats + self.playtime_windows_forever = playtime_windows_forever + self.playtime_mac_forever = playtime_mac_forever + self.playtime_linux_forever = playtime_linux_forever + self.playtime_2weeks = playtime_2weeks + + def __hash__(self): + return self.appid + + def __eq__(self, other): + if isinstance(other, SteamGame): + return self.appid == other.appid + return False + + def __str__(self): + return self.name + + def __repr__(self): + return f"<{self.__class__.__qualname__} {self.appid} ({self.name})>" + + +class SteammatchCommand(rc.Command): + name: str = "steammatch" + + description: str = "Vedi quali giochi hai in comune con uno o più membri!" + + syntax: str = "{royalnet_username}+" + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + self._api = steam.webapi.WebAPI(self.config["steampowered"]["token"]) + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + users = [] + + author = await data.get_author(error_if_none=True) + users.append(author) + + for arg in args: + user = await rbt.User.find(self.alchemy, data.session, arg) + users.append(user) + + if len(users) < 2: + raise rc.InvalidInputError("Devi specificare almeno un altro utente!") + + shared_games: Optional[set] = None + for user in users: + user_games = set() + if len(user.steam) == 0: + raise rc.UserError(f"{user} non ha un account Steam registrato!") + for steam_account in user.steam: + steam_account: Steam + try: + response = await ru.asyncify(self._api.IPlayerService.GetOwnedGames, + steamid=steam_account._steamid, + include_appinfo=True, + include_played_free_games=True, + include_free_sub=True, + appids_filter=0) + except requests.exceptions.HTTPError: + raise rc.ExternalError(f"L'account Steam di {user} è privato!") + games = response["response"]["games"] + for game in games: + user_games.add(SteamGame(**game)) + if shared_games is None: + shared_games = user_games + else: + shared_games = shared_games.intersection(user_games) + + message_rows = [f"🎮 Giochi in comune tra {ru.andformat([str(user) for user in users], final=' e ')}:"] + for game in sorted(list(shared_games), key=lambda g: g.name): + message_rows.append(f"- {game}") + + message = "\n".join(message_rows) + await data.reply(message) diff --git a/royalpack/commands/steampowered.py b/royalpack/commands/steampowered.py new file mode 100644 index 00000000..662c61c7 --- /dev/null +++ b/royalpack/commands/steampowered.py @@ -0,0 +1,125 @@ +from typing import * +import steam.steamid +import steam.webapi +import datetime +import royalnet.commands as rc +import royalnet.utils as ru +import logging +from royalnet.backpack import tables as rbt + +from .abstract.linker import LinkerCommand + +from ..tables import Steam, FiorygiTransaction +from ..types import Updatable + +log = logging.getLogger(__name__) + + +class SteampoweredCommand(LinkerCommand): + name: str = "steampowered" + + description: str = "Connetti e visualizza informazioni sul tuo account di Steam!" + + syntax: str = "{url_profilo}" + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + self._api = steam.webapi.WebAPI(self.token()) + + def token(self): + return self.config["steampowered"]["token"] + + async def get_updatables_of_user(self, session, user: rbt.User) -> List[Steam]: + return user.steam + + async def get_updatables(self, session) -> List[Steam]: + return await ru.asyncify(session.query(self.alchemy.get(Steam)).all) + + async def create(self, + session, + user: rbt.User, + args: rc.CommandArgs, + data: Optional[rc.CommandData] = None) -> Optional[Steam]: + url = args.joined() + steamid64 = await self._call(steam.steamid.steam64_from_url, url) + if steamid64 is None: + raise rc.InvalidInputError("Quel link non è associato ad alcun account Steam.") + response = await self._call(self._api.ISteamUser.GetPlayerSummaries_v2, steamids=steamid64) + r = response["response"]["players"][0] + steam_account = self.alchemy.get(Steam)( + user=user, + _steamid=int(steamid64), + persona_name=r["personaname"], + profile_url=r["profileurl"], + avatar=r["avatarfull"], + primary_clan_id=r["primaryclanid"], + account_creation_date=datetime.datetime.fromtimestamp(r["timecreated"]) + ) + + await FiorygiTransaction.spawn_fiorygi( + data=data, + user=user, + qty=1, + reason="aver collegato a Royalnet il proprio account di League of Legends" + ) + + session.add(steam_account) + return steam_account + + async def update(self, session, obj: Steam, change: Callable[[str, Any], Awaitable[None]]): + response = await self._call(self._api.ISteamUser.GetPlayerSummaries_v2, steamids=obj.steamid.as_64) + r = response["response"]["players"][0] + obj.persona_name = r["personaname"] + obj.profile_url = r["profileurl"] + obj.avatar = r["avatar"] + obj.primary_clan_id = r["primaryclanid"] + obj.account_creation_date = datetime.datetime.fromtimestamp(r["timecreated"]) + response = await self._call(self._api.IPlayerService.GetSteamLevel_v1, steamid=obj.steamid.as_64) + obj.account_level = response["response"]["player_level"] + response = await self._call(self._api.IPlayerService.GetOwnedGames_v1, + steamid=obj.steamid.as_64, + include_appinfo=False, + include_played_free_games=True, + include_free_sub=False, + appids_filter=None) + obj.owned_games_count = response["response"]["game_count"] + if response["response"]["game_count"] >= 0: + obj.most_played_game_2weeks = sorted(response["response"]["games"], key=lambda g: -g.get("playtime_2weeks", 0))[0]["appid"] + obj.most_played_game_forever = sorted(response["response"]["games"], key=lambda g: -g.get("playtime_forever", 0))[0]["appid"] + + async def on_increase(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_unchanged(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_decrease(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None: + pass + + async def on_first(self, session, obj: Updatable, attribute: str, old: None, new: Any) -> None: + pass + + async def on_reset(self, session, obj: Updatable, attribute: str, old: Any, new: None) -> None: + pass + + def describe(self, obj: Steam): + return f"ℹ️ [url={obj.profile_url}]{obj.persona_name}[/url]\n" \ + f"[b]Level {obj.account_level}[/b]\n" \ + f"\n" \ + f"Owned games: [b]{obj.owned_games_count}[/b]\n" \ + f"Most played 2 weeks: [url=https://store.steampowered.com/app/{obj.most_played_game_2weeks}]{obj.most_played_game_2weeks}[/url]\n" \ + f"Most played forever: [url=https://store.steampowered.com/app/{obj.most_played_game_forever}]{obj.most_played_game_forever}[/url]\n" \ + f"\n" \ + f"SteamID32: [c]{obj.steamid.as_32}[/c]\n" \ + f"SteamID64: [c]{obj.steamid.as_64}[/c]\n" \ + f"SteamID2: [c]{obj.steamid.as_steam2}[/c]\n" \ + f"SteamID3: [c]{obj.steamid.as_steam3}[/c]\n" \ + f"\n" \ + f"Created on: [b]{obj.account_creation_date}[/b]\n" + + async def _call(self, method, *args, **kwargs): + log.debug(f"Calling {method}") + try: + return await ru.asyncify(method, *args, **kwargs) + except Exception as e: + raise rc.ExternalError("\n".join(e.args).replace(self.token(), "HIDDEN")) diff --git a/royalpack/commands/summon.py b/royalpack/commands/summon.py deleted file mode 100644 index 4fc2cd3b..00000000 --- a/royalpack/commands/summon.py +++ /dev/null @@ -1,28 +0,0 @@ -import discord -from royalnet.commands import * - - -class SummonCommand(Command): - name: str = "summon" - - aliases = ["cv"] - - description: str = "Evoca il bot in un canale vocale." - - syntax: str = "[nomecanale]" - - async def run(self, args: CommandArgs, data: CommandData) -> None: - channel_name = args.joined() - if self.interface.name == "discord": - message: discord.Message = data.message - guild_id = message.guild.id - user_id = message.author.id - else: - guild_id = None - user_id = None - response = await self.interface.call_herald_event("discord", "discord_summon", - channel_name=channel_name, guild_id=guild_id, user_id=user_id) - if self.interface.name == "discord": - await data.reply(f"✅ Mi sono connesso in <#{response['channel']['id']}>!") - else: - await data.reply(f"✅ Mi sono connesso in [b]#{response['channel']['name']}[/b]!") diff --git a/royalpack/commands/treasure.py b/royalpack/commands/treasure.py new file mode 100644 index 00000000..bc7337d2 --- /dev/null +++ b/royalpack/commands/treasure.py @@ -0,0 +1,33 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import Treasure, FiorygiTransaction + + +class TreasureCommand(rc.Command): + name: str = "treasure" + + description: str = "Riscatta un Treasure che hai trovato da qualche parte." + + syntax: str = "{code}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + author = await data.get_author(error_if_none=True) + code = args[0].lower() + + TreasureT = self.alchemy.get(Treasure) + + treasure = await ru.asyncify(data.session.query(TreasureT).get, code) + if treasure is None: + raise rc.UserError("Non esiste nessun Treasure con quel codice.") + if treasure.redeemed_by is not None: + raise rc.UserError(f"Quel tesoro è già stato riscattato da {treasure.redeemed_by}.") + + treasure.redeemed_by = author + await data.session_commit() + await FiorygiTransaction.spawn_fiorygi(data, + author, + treasure.value, + f'aver trovato il tesoro "{treasure.code}"') + await data.reply("🤑 Tesoro riscattato!") diff --git a/royalpack/commands/trivia.py b/royalpack/commands/trivia.py new file mode 100644 index 00000000..d5226028 --- /dev/null +++ b/royalpack/commands/trivia.py @@ -0,0 +1,147 @@ +from typing import * +import asyncio +import aiohttp +import random +import uuid +import html +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.backpack.tables as rbt +from ..tables import TriviaScore + + +class TriviaCommand(rc.Command): + name: str = "trivia" + + aliases = ["t"] + + description: str = "Manda una domanda dell'OpenTDB in chat." + + syntax = "[credits|scores]" + + _letter_emojis = ["🇦", "🇧", "🇨", "🇩"] + + _medal_emojis = ["🥇", "🥈", "🥉", "🔹"] + + _correct_emoji = "✅" + + _wrong_emoji = "❌" + + _answer_time = 20 + + # _question_lock: bool = False + + def __init__(self, interface: rc.CommandInterface): + super().__init__(interface) + self._answerers: Dict[uuid.UUID, Dict[str, bool]] = {} + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + arg = args.optional(0) + if arg == "credits": + await data.reply(f"ℹ️ [c]{self.interface.prefix}{self.name}[/c] di [i]Steffo[/i]\n" + f"\n" + f"Tutte le domande vengono dall'[b]Open Trivia Database[/b] di [i]Pixeltail Games[/i]," + f" creatori di Tower Unite, e sono rilasciate sotto la licenza [b]CC BY-SA 4.0[/b].") + return + elif arg == "scores": + trivia_scores = await ru.asyncify(data.session.query(self.alchemy.get(TriviaScore)).all) + strings = ["🏆 [b]Trivia Leaderboards[/b]\n"] + for index, ts in enumerate(sorted(trivia_scores, key=lambda ts: -ts.score)): + if index > 3: + index = 3 + strings.append(f"{self._medal_emojis[index]} {ts.user.username}: [b]{ts.score:.0f}p[/b]" + f" ({ts.correct_answers}/{ts.total_answers})") + await data.reply("\n".join(strings)) + return + # if self._question_lock: + # raise rc.CommandError("C'è già un'altra domanda attiva!") + # self._question_lock = True + # Fetch the question + async with aiohttp.ClientSession() as session: + async with session.get("https://opentdb.com/api.php?amount=1") as response: + j = await response.json() + # Parse the question + if j["response_code"] != 0: + raise rc.CommandError(f"OpenTDB returned an error response_code ({j['response_code']}).") + question = j["results"][0] + text = f'❓ [b]{question["category"]}[/b]\n' \ + f'{html.unescape(question["question"])}' + # Prepare answers + correct_answer: str = question["correct_answer"] + wrong_answers: List[str] = question["incorrect_answers"] + answers: List[str] = [correct_answer, *wrong_answers] + if question["type"] == "multiple": + random.shuffle(answers) + elif question["type"] == "boolean": + answers.sort(key=lambda a: a) + answers.reverse() + else: + raise NotImplementedError("Unknown question type") + # Find the correct index + for index, answer in enumerate(answers): + if answer == correct_answer: + correct_index = index + break + else: + raise ValueError("correct_index not found") + # Add emojis + for index, answer in enumerate(answers): + answers[index] = f"{self._letter_emojis[index]} {html.unescape(answers[index])}" + # Create the question id + question_id = uuid.uuid4() + self._answerers[question_id] = {} + + # Create the correct and wrong functions + async def correct(data: rc.CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self._answerers[question_id][answerer_.uid] = True + except KeyError: + raise rc.UserError("Tempo scaduto!") + await data.reply("🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!") + + async def wrong(data: rc.CommandData): + answerer_ = await data.get_author(error_if_none=True) + try: + self._answerers[question_id][answerer_.uid] = False + except KeyError: + raise rc.UserError("Tempo scaduto!") + await data.reply("🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!") + + # Add question + keyboard: List[rc.KeyboardKey] = [] + for index, answer in enumerate(answers): + if index == correct_index: + keyboard.append(rc.KeyboardKey(interface=self.interface, + short=self._letter_emojis[index], + text=answers[index], + callback=correct)) + else: + keyboard.append(rc.KeyboardKey(interface=self.interface, + short=self._letter_emojis[index], + text=answers[index], + callback=wrong)) + async with data.keyboard(text=text, keys=keyboard): + await asyncio.sleep(self._answer_time) + results = f"❗️ Tempo scaduto!\n" \ + f"La risposta corretta era [b]{answers[correct_index]}[/b]!\n\n" + for answerer_id in self._answerers[question_id]: + answerer = data.session.query(self.alchemy.get(rbt.users.User)).get(answerer_id) + if answerer.trivia_score is None: + ts = self.interface.alchemy.get(TriviaScore)(user=answerer) + data.session.add(ts) + await ru.asyncify(data.session.commit) + previous_score = answerer.trivia_score.score + if self._answerers[question_id][answerer_id]: + results += self._correct_emoji + answerer.trivia_score.correct_answers += 1 + else: + results += self._wrong_emoji + answerer.trivia_score.wrong_answers += 1 + current_score = answerer.trivia_score.score + score_difference = current_score - previous_score + results += f" {answerer}: [b]{current_score:.0f}p[/b] ({score_difference:+.0f}p)\n" + await data.reply(results) + del self._answerers[question_id] + await ru.asyncify(data.session.commit) + # self._question_lock = False diff --git a/royalpack/commands/userinfo.py b/royalpack/commands/userinfo.py index 125ae951..2917f569 100644 --- a/royalpack/commands/userinfo.py +++ b/royalpack/commands/userinfo.py @@ -1,11 +1,9 @@ from typing import * -from royalnet.commands import * -from royalnet.utils import * -from royalnet.backpack.tables import User -from sqlalchemy import func +import royalnet.commands as rc +import royalnet.backpack.tables as rbt -class UserinfoCommand(Command): +class UserinfoCommand(rc.Command): name: str = "userinfo" aliases = ["uinfo", "ui", "useri"] @@ -14,31 +12,26 @@ class UserinfoCommand(Command): syntax = "[username]" - async def run(self, args: CommandArgs, data: CommandData) -> None: + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: username = args.optional(0) if username is None: - user: User = await data.get_author(error_if_none=True) + user: rbt.User = await data.get_author(error_if_none=True) else: - found: Optional[User] = await asyncify( - data.session - .query(self.alchemy.get(User)) - .filter(func.lower(self.alchemy.get(User).username) == func.lower(username)) - .one_or_none - ) + found: Optional[rbt.User] = await rbt.User.find(self.alchemy, data.session, username) if not found: - raise InvalidInputError("Utente non trovato.") + raise rc.InvalidInputError("Utente non trovato.") else: user = found r = [ - f"ℹ️ [b]{user.username}[/b] (ID: {user.uid})", - f"{user.role}", - "", + f"ℹ️ [url=https://ryg.steffo.eu/#/user/{user.uid}]{user.username}[/url]", + f"{', '.join(user.roles)}", ] - if user.fiorygi: - r.append(f"{user.fiorygi}") - r.append("") + if user.email: + r.append(f"{user.email}") + + r.append("") # Bios are a bit too long # if user.bio: @@ -50,18 +43,32 @@ class UserinfoCommand(Command): for account in user.discord: r.append(f"{account}") + for account in user.steam: + r.append(f"{account}") + if account.dota is not None: + r.append(f"{account.dota}") + if account.brawlhalla is not None: + r.append(f"{account.brawlhalla}") + for account in user.leagueoflegends: r.append(f"{account}") r.append("") - r.append(f"Ha creato [b]{len(user.diario_created)}[/b] righe di diario, e vi compare in" + r.append(f"Ha creato [b]{len(user.diario_created)}[/b] righe di " + f"[url=https://ryg.steffo.eu/#/diario]Diario[/url], e vi compare in" f" [b]{len(user.diario_quoted)}[/b] righe.") r.append("") if user.trivia_score: - r.append(f"Trivia: [b]{user.trivia_score.correct_answers}[/b] risposte corrette / " - f"{user.trivia_score.total_answers} totali") + r.append(f"Ha [b]{user.trivia_score.score:.0f}[/b] punti Trivia, avendo risposto correttamente a" + f" [b]{user.trivia_score.correct_answers}[/b] domande su" + f" [b]{user.trivia_score.total_answers}[/b].") + r.append("") + + if user.fiorygi: + r.append(f"Ha [b]{user.fiorygi}[/b].") + r.append("") await data.reply("\n".join(r)) diff --git a/royalpack/commands/videochannel.py b/royalpack/commands/videochannel.py deleted file mode 100644 index c613591b..00000000 --- a/royalpack/commands/videochannel.py +++ /dev/null @@ -1,49 +0,0 @@ -import typing -import discord -from royalnet.commands import * - - -class VideochannelCommand(Command): - name: str = "videochannel" - - aliases = ["golive", "live", "video"] - - description: str = "Converti il canale vocale in un canale video." - - syntax = "[nomecanale]" - - async def run(self, args: CommandArgs, data: CommandData) -> None: - if self.interface.name != "discord": - raise UnsupportedError(f"{self} non è supportato su {self.interface.name}.") - bot: discord.Client = self.serf.client - message: discord.Message = data.message - channel_name: str = args.optional(0) - if channel_name: - guild: typing.Optional[discord.Guild] = message.guild - if guild is not None: - channels: typing.List[discord.abc.GuildChannel] = guild.channels - else: - channels = bot.get_all_channels() - matching_channels: typing.List[discord.VoiceChannel] = [] - for channel in channels: - if isinstance(channel, discord.VoiceChannel): - if channel.name == channel_name: - matching_channels.append(channel) - if len(matching_channels) == 0: - raise InvalidInputError("Non esiste alcun canale vocale con il nome specificato.") - elif len(matching_channels) > 1: - raise UserError("Esiste più di un canale vocale con il nome specificato.") - channel = matching_channels[0] - else: - author: discord.Member = message.author - voice: typing.Optional[discord.VoiceState] = author.voice - if voice is None: - raise InvalidInputError("Non sei connesso a nessun canale vocale.") - channel = voice.channel - if author.is_on_mobile(): - await data.reply(f"📹 Per entrare in modalità video, clicca qui:\n" - f"\n" - f"[b]Attenzione: la modalità video non funziona su Android e iOS![/b]") - return - await data.reply(f"📹 Per entrare in modalità video, clicca qui:\n" - f"") diff --git a/royalpack/commands/yahoovideo.py b/royalpack/commands/yahoovideo.py deleted file mode 100644 index 482b1042..00000000 --- a/royalpack/commands/yahoovideo.py +++ /dev/null @@ -1,16 +0,0 @@ -from .play import PlayCommand - - -class YahoovideoCommand(PlayCommand): - name: str = "yahoovideo" - - aliases = ["yv"] - - description: str = "Cerca un video su Yahoo Video e lo aggiunge alla coda della chat vocale." - - syntax = "{ricerca}" - - async def get_url(self, args): - return f"yvsearch:{args.joined()}" - - # Too bad yvsearch: always finds nothing. diff --git a/royalpack/commands/youtube.py b/royalpack/commands/youtube.py deleted file mode 100644 index 870c8aca..00000000 --- a/royalpack/commands/youtube.py +++ /dev/null @@ -1,14 +0,0 @@ -from .play import PlayCommand - - -class YoutubeCommand(PlayCommand): - name: str = "youtube" - - aliases = ["yt"] - - description: str = "Cerca un video su YouTube e lo aggiunge alla coda della chat vocale." - - syntax = "{ricerca}" - - async def get_url(self, args): - return f"ytsearch:{args.joined()}" diff --git a/royalpack/events/__init__.py b/royalpack/events/__init__.py index 64834fb5..03ae8b08 100644 --- a/royalpack/events/__init__.py +++ b/royalpack/events/__init__.py @@ -5,6 +5,10 @@ from .discord_play import DiscordPlayEvent from .discord_skip import DiscordSkipEvent from .discord_queue import DiscordQueueEvent from .discord_pause import DiscordPauseEvent +from .discord_playable import DiscordPlaymodeEvent +from .discord_lazy_play import DiscordLazyPlayEvent +from .telegram_message import TelegramMessageEvent +from .pong import PongEvent # Enter the commands of your Pack here! available_events = [ @@ -14,6 +18,10 @@ available_events = [ DiscordSkipEvent, DiscordQueueEvent, DiscordPauseEvent, + DiscordPlaymodeEvent, + DiscordLazyPlayEvent, + TelegramMessageEvent, + PongEvent, ] # Don't change this, it should automatically generate __all__ diff --git a/royalpack/events/discord_lazy_play.py b/royalpack/events/discord_lazy_play.py new file mode 100644 index 00000000..cfdc6235 --- /dev/null +++ b/royalpack/events/discord_lazy_play.py @@ -0,0 +1,100 @@ +import datetime +from typing import * + +import discord +import royalnet.commands as rc +import royalnet.serf.discord as rsd +import royalnet.bard.discord as rbd + +from ..utils import RoyalQueue, RoyalPool + + +class DiscordLazyPlayEvent(rc.Event): + name = "discord_lazy_play" + + async def run(self, + urls: List[str], + guild_id: Optional[int] = None, + user: Optional[str] = None, + force_color: Optional[int] = None, + **kwargs) -> dict: + if not isinstance(self.serf, rsd.DiscordSerf): + raise rc.UnsupportedError() + + serf: rsd.DiscordSerf = self.serf + client: discord.Client = self.serf.client + guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None + candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild) + + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] + else: + raise rc.CommandError("Non so in che Server riprodurre questo file...\n" + "Invia il comando su Discord, per favore!") + + added: List[rbd.YtdlDiscord] = [] + too_long: List[rbd.YtdlDiscord] = [] + + for url in urls: + ytds = await rbd.YtdlDiscord.from_url(url) + if isinstance(voice_player.playing, RoyalQueue): + for index, ytd in enumerate(ytds): + if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): + too_long.append(ytd) + continue + added.append(ytd) + voice_player.playing.contents.append(ytd) + if not voice_player.voice_client.is_playing(): + await voice_player.start() + elif isinstance(voice_player.playing, RoyalPool): + for index, ytd in enumerate(ytds): + if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): + too_long.append(ytd) + continue + added.append(ytd) + voice_player.playing.full_pool.append(ytd) + voice_player.playing.remaining_pool.append(ytd) + if not voice_player.voice_client.is_playing(): + await voice_player.start() + else: + raise rc.CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!") + + main_channel: discord.TextChannel = client.get_channel(self.config["Discord"]["main_channel_id"]) + + if len(added) > 0: + if user: + await main_channel.send(rsd.escape(f"▶️ {user} ha aggiunto {len(added)} file _(lazy)_ alla coda:")) + else: + await main_channel.send(rsd.escape(f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file " + f"[i](lazy)[/i] alla coda:")) + for ytd in added[:5]: + embed: discord.Embed = ytd.embed() + if force_color: + embed._colour = discord.Colour(force_color) + await main_channel.send(embed=embed) + if len(added) > 5: + await main_channel.send(f"e altri {len(added) - 5}!") + + if len(too_long) > 0: + if user: + await main_channel.send(rsd.escape( + f"⚠ {len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}" + f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}" + f" perchè durava{'' if len(too_long) == 1 else 'no'}" + f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi." + )) + + if len(added) + len(too_long) == 0: + raise rc.InvalidInputError("Non è stato aggiunto nessun file alla coda.") + + return { + "added": [{ + "title": ytd.info.title, + } for ytd in added], + "too_long": [{ + "title": ytd.info.title, + } for ytd in too_long] + } diff --git a/royalpack/events/discord_pause.py b/royalpack/events/discord_pause.py index f7733f5c..b5131f9f 100644 --- a/royalpack/events/discord_pause.py +++ b/royalpack/events/discord_pause.py @@ -1,32 +1,37 @@ import discord from typing import * -from royalnet.commands import * +import royalnet.commands as rc from royalnet.serf.discord import * -class DiscordPauseEvent(Event): +class DiscordPauseEvent(rc.Event): name = "discord_pause" async def run(self, guild_id: Optional[int] = None, **kwargs) -> dict: if not isinstance(self.serf, DiscordSerf): - raise UnsupportedError() + raise rc.UnsupportedError() client: discord.Client = self.serf.client if len(self.serf.voice_players) == 1: voice_player: VoicePlayer = self.serf.voice_players[0] else: if guild_id is None: # TODO: trovare un modo per riprodurre canzoni su più server da Telegram - raise InvalidInputError("Non so in che Server riprodurre questo file...\n" - "Invia il comando su Discord, per favore!") + raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n" + "Invia il comando su Discord, per favore!") guild: discord.Guild = client.get_guild(guild_id) if guild is None: - raise InvalidInputError("Impossibile trovare il Server specificato.") - voice_player: VoicePlayer = self.serf.find_voice_player(guild) - if voice_player is None: - raise UserError("Il bot non è in nessun canale vocale.\n" - "Evocalo prima con [c]summon[/c]!") + raise rc.InvalidInputError("Impossibile trovare il Server specificato.") + candidate_players = self.serf.find_voice_players(guild) + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] + else: + raise rc.CommandError("Non so su che Server saltare canzone...\n" + "Invia il comando su Discord, per favore!") if voice_player.voice_client.is_paused(): voice_player.voice_client.resume() diff --git a/royalpack/events/discord_play.py b/royalpack/events/discord_play.py index 883eb863..4018ef96 100644 --- a/royalpack/events/discord_play.py +++ b/royalpack/events/discord_play.py @@ -1,62 +1,102 @@ -import discord -import pickle -import base64 import datetime from typing import * -from royalnet.commands import * -from royalnet.serf.discord import * -from royalnet.bard import * -from ..utils import RoyalQueue + +import discord +import royalnet.commands as rc +import royalnet.serf.discord as rsd +import royalnet.bard.discord as rbd + +from ..utils import RoyalQueue, RoyalPool -class DiscordPlayEvent(Event): +class DiscordPlayEvent(rc.Event): name = "discord_play" async def run(self, - url: str, + urls: List[str], guild_id: Optional[int] = None, + user: Optional[str] = None, + force_color: Optional[int] = None, **kwargs) -> dict: - if not isinstance(self.serf, DiscordSerf): - raise UnsupportedError() + if not isinstance(self.serf, rsd.DiscordSerf): + raise rc.UnsupportedError() + + serf: rsd.DiscordSerf = self.serf client: discord.Client = self.serf.client - if len(self.serf.voice_players) == 1: - voice_player: VoicePlayer = self.serf.voice_players[0] + guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None + candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild) + + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] else: - if guild_id is None: - # TODO: trovare un modo per riprodurre canzoni su più server da Telegram - raise InvalidInputError("Non so in che Server riprodurre questo file...\n" - "Invia il comando su Discord, per favore!") - guild: discord.Guild = client.get_guild(guild_id) - if guild is None: - raise InvalidInputError("Impossibile trovare il Server specificato.") - voice_player: VoicePlayer = self.serf.find_voice_player(guild) - if voice_player is None: - raise UserError("Il bot non è in nessun canale vocale.\n" - "Evocalo prima con [c]summon[/c]!") - ytds = await YtdlDiscord.from_url(url) - added: List[YtdlDiscord] = [] - too_long: List[YtdlDiscord] = [] - if isinstance(voice_player.playing, RoyalQueue): - for index, ytd in enumerate(ytds): - if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): - too_long.append(ytd) - continue - await ytd.convert_to_pcm() - added.append(ytd) - voice_player.playing.contents.append(ytd) - if not voice_player.voice_client.is_playing(): - await voice_player.start() - else: - raise CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!") + raise rc.CommandError("Non so in che Server riprodurre questo file...\n" + "Invia il comando su Discord, per favore!") + + added: List[rbd.YtdlDiscord] = [] + too_long: List[rbd.YtdlDiscord] = [] + + for url in urls: + ytds = await rbd.YtdlDiscord.from_url(url) + if isinstance(voice_player.playing, RoyalQueue): + for index, ytd in enumerate(ytds): + if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): + too_long.append(ytd) + continue + await ytd.convert_to_pcm() + added.append(ytd) + voice_player.playing.contents.append(ytd) + if not voice_player.voice_client.is_playing(): + await voice_player.start() + elif isinstance(voice_player.playing, RoyalPool): + for index, ytd in enumerate(ytds): + if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): + too_long.append(ytd) + continue + await ytd.convert_to_pcm() + added.append(ytd) + voice_player.playing.full_pool.append(ytd) + voice_player.playing.remaining_pool.append(ytd) + if not voice_player.voice_client.is_playing(): + await voice_player.start() + else: + raise rc.CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!") + + main_channel: discord.TextChannel = client.get_channel(self.config["Discord"]["main_channel_id"]) + + if len(added) > 0: + if user: + await main_channel.send(rsd.escape(f"▶️ {user} ha aggiunto {len(added)} file alla coda:")) + else: + await main_channel.send(rsd.escape(f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file alla" + f" coda:")) + for ytd in added[:5]: + embed: discord.Embed = ytd.embed() + if force_color: + embed._colour = discord.Colour(force_color) + await main_channel.send(embed=embed) + if len(added) > 5: + await main_channel.send(f"e altri {len(added) - 5}!") + + if len(too_long) > 0: + if user: + await main_channel.send(rsd.escape( + f"⚠ {len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}" + f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}" + f" perchè durava{'' if len(too_long) == 1 else 'no'}" + f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi." + )) + + if len(added) + len(too_long) == 0: + raise rc.InvalidInputError("Non è stato aggiunto nessun file alla coda.") + return { "added": [{ "title": ytd.info.title, - "stringified_base64_pickled_discord_embed": str(base64.b64encode(pickle.dumps(ytd.embed())), - encoding="ascii") } for ytd in added], "too_long": [{ "title": ytd.info.title, - "stringified_base64_pickled_discord_embed": str(base64.b64encode(pickle.dumps(ytd.embed())), - encoding="ascii") } for ytd in too_long] } diff --git a/royalpack/events/discord_playable.py b/royalpack/events/discord_playable.py new file mode 100644 index 00000000..ecd61cd3 --- /dev/null +++ b/royalpack/events/discord_playable.py @@ -0,0 +1,48 @@ +import datetime +from typing import * + +import discord +import royalnet.commands as rc +import royalnet.serf.discord as rsd +import royalnet.bard.discord as rbd + +from ..utils import RoyalQueue, RoyalPool + + +class DiscordPlaymodeEvent(rc.Event): + name = "discord_playmode" + + async def run(self, + playable_string: str, + guild_id: Optional[int] = None, + user: Optional[str] = None, + **kwargs) -> dict: + if not isinstance(self.serf, rsd.DiscordSerf): + raise rc.UnsupportedError() + + serf: rsd.DiscordSerf = self.serf + client: discord.Client = self.serf.client + guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None + candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild) + + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] + else: + raise rc.CommandError("Non so a che Server cambiare Playable...\n" + "Invia il comando su Discord, per favore!") + + if playable_string.upper() == "QUEUE": + playable = await RoyalQueue.create() + elif playable_string.upper() == "POOL": + playable = await RoyalPool.create() + else: + raise rc.InvalidInputError(f"Unknown playable '{playable_string.upper()}'") + + await voice_player.change_playing(playable) + + return { + "name": f"{playable.__class__.__qualname__}" + } diff --git a/royalpack/events/discord_queue.py b/royalpack/events/discord_queue.py index 6a668f05..63a0c59e 100644 --- a/royalpack/events/discord_queue.py +++ b/royalpack/events/discord_queue.py @@ -2,34 +2,39 @@ import discord import pickle import base64 from typing import * -from royalnet.commands import * +import royalnet.commands as rc from royalnet.serf.discord import * from ..utils import RoyalQueue -class DiscordQueueEvent(Event): +class DiscordQueueEvent(rc.Event): name = "discord_queue" async def run(self, guild_id: Optional[int] = None, **kwargs) -> dict: if not isinstance(self.serf, DiscordSerf): - raise UnsupportedError() + raise rc.UnsupportedError() client: discord.Client = self.serf.client if len(self.serf.voice_players) == 1: voice_player: VoicePlayer = self.serf.voice_players[0] else: if guild_id is None: # TODO: trovare un modo per riprodurre canzoni su più server da Telegram - raise InvalidInputError("Non so in che Server riprodurre questo file...\n" + raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n" "Invia il comando su Discord, per favore!") guild: discord.Guild = client.get_guild(guild_id) if guild is None: - raise InvalidInputError("Impossibile trovare il Server specificato.") - voice_player: VoicePlayer = self.serf.find_voice_player(guild) - if voice_player is None: - raise UserError("Il bot non è in nessun canale vocale.\n" - "Evocalo prima con [c]summon[/c]!") + raise rc.InvalidInputError("Impossibile trovare il Server specificato.") + candidate_players = self.serf.find_voice_players(guild) + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] + else: + raise rc.CommandError("Non so di che Server visualizzare la coda...\n" + "Invia il comando su Discord, per favore!") if isinstance(voice_player.playing, RoyalQueue): now_playing = voice_player.playing.now_playing return { @@ -46,5 +51,5 @@ class DiscordQueueEvent(Event): } for ytd in voice_player.playing.contents] } else: - raise CommandError(f"Non so come visualizzare il contenuto di un " + raise rc.CommandError(f"Non so come visualizzare il contenuto di un " f"[c]{voice_player.playing.__class__.__qualname__}[/c].") diff --git a/royalpack/events/discord_skip.py b/royalpack/events/discord_skip.py index cb186f35..d7ddc393 100644 --- a/royalpack/events/discord_skip.py +++ b/royalpack/events/discord_skip.py @@ -1,32 +1,37 @@ import discord from typing import * -from royalnet.commands import * +import royalnet.commands as rc from royalnet.serf.discord import * -class DiscordSkipEvent(Event): +class DiscordSkipEvent(rc.Event): name = "discord_skip" async def run(self, guild_id: Optional[int] = None, **kwargs) -> dict: if not isinstance(self.serf, DiscordSerf): - raise UnsupportedError() + raise rc.UnsupportedError() client: discord.Client = self.serf.client if len(self.serf.voice_players) == 1: voice_player: VoicePlayer = self.serf.voice_players[0] else: if guild_id is None: # TODO: trovare un modo per riprodurre canzoni su più server da Telegram - raise InvalidInputError("Non so in che Server riprodurre questo file...\n" - "Invia il comando su Discord, per favore!") + raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n" + "Invia il comando su Discord, per favore!") guild: discord.Guild = client.get_guild(guild_id) if guild is None: - raise InvalidInputError("Impossibile trovare il Server specificato.") - voice_player: VoicePlayer = self.serf.find_voice_player(guild) - if voice_player is None: - raise UserError("Il bot non è in nessun canale vocale.\n" - "Evocalo prima con [c]summon[/c]!") + raise rc.InvalidInputError("Impossibile trovare il Server specificato.") + candidate_players = self.serf.find_voice_players(guild) + if len(candidate_players) == 0: + raise rc.UserError("Il bot non è in nessun canale vocale.\n" + "Evocalo prima con [c]summon[/c]!") + elif len(candidate_players) == 1: + voice_player = candidate_players[0] + else: + raise rc.CommandError("Non so su che Server saltare canzone...\n" + "Invia il comando su Discord, per favore!") # Stop the playback of the current song voice_player.voice_client.stop() # Done! diff --git a/royalpack/events/discord_summon.py b/royalpack/events/discord_summon.py index 4f74f69d..1066d1b5 100644 --- a/royalpack/events/discord_summon.py +++ b/royalpack/events/discord_summon.py @@ -60,6 +60,12 @@ class DiscordSummonEvent(Event): # Connect to the channel try: await vp.connect(channel) + except OpusNotLoadedError: + raise ConfigurationError("libopus non è disponibile sul sistema in cui sta venendo eseguito questo bot," + " pertanto non è possibile con") + except DiscordTimeoutError: + raise ExternalError("Timeout durante la connessione al canale." + " Forse il bot non ha i permessi per entrarci?") except GuildAlreadyConnectedError: raise UserError("Il bot è già connesso in un canale vocale nel Server!\n" "Spostalo manualmente, o disconnettilo e riinvoca [c]summon[/c]!") diff --git a/royalpack/events/pong.py b/royalpack/events/pong.py new file mode 100644 index 00000000..b6943e5d --- /dev/null +++ b/royalpack/events/pong.py @@ -0,0 +1,11 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import datetime + + +class PongEvent(rc.Event): + name = "pong" + + async def run(self, **kwargs) -> dict: + return {"timestamp": datetime.datetime.now().timestamp()} diff --git a/royalpack/events/telegram_message.py b/royalpack/events/telegram_message.py new file mode 100644 index 00000000..2e8412f5 --- /dev/null +++ b/royalpack/events/telegram_message.py @@ -0,0 +1,28 @@ +import logging +import telegram +from typing import * +from royalnet.serf.telegram.telegramserf import TelegramSerf, escape +from royalnet.commands import * + + +log = logging.getLogger(__name__) + + +class TelegramMessageEvent(Event): + name = "telegram_message" + + async def run(self, chat_id, text, **kwargs) -> dict: + if not self.interface.name == "telegram": + raise UnsupportedError() + + # noinspection PyTypeChecker + serf: TelegramSerf = self.interface.serf + + log.debug("Forwarding message from Herald to Telegram.") + await serf.api_call(serf.client.send_message, + chat_id=chat_id, + text=escape(text), + parse_mode="HTML", + disable_web_page_preview=True) + + return {} diff --git a/royalpack/pycharm/test_apis.http b/royalpack/pycharm/test_apis.http new file mode 100644 index 00000000..e51c3cb9 --- /dev/null +++ b/royalpack/pycharm/test_apis.http @@ -0,0 +1,70 @@ +POST http://localhost:44445/api/login/royalnet/v1 +Content-Type: application/json + +{ + "username": "Steffo", + "password": "ciao" +} + +### + +POST http://localhost:44445/api/token/create/v1 +Content-Type: application/json + +{ + "token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE", + "duration": 31536000 +} + +### + +GET http://localhost:44445/api/bio/get/v1 +Content-Type: application/json + +{ + "token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE", + "id": 1 +} + +### + +POST http://localhost:44445/api/bio/set/v1 +Content-Type: application/json + +{ + "token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE", + "contents": "Ciao!" +} + +### + +GET http://localhost:44445/api/wiki/list/v1 + +### + +POST http://localhost:44445/api/wiki/edit/v1 +Content-Type: application/json + +{ + "token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE", + "title": "Prova!", + "contents": "Questa è una pagina wiki di prova.", + "format": "text", + "theme": "default" +} + +### + +POST http://localhost:44445/api/wiki/edit/v1 +Content-Type: application/json + +{ + "id": "80d54849-fab1-4458-9d89-2429773118ef", + "token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE", + "title": "Prova2", + "contents": "Questa è una pagina wiki di prova2.", + "format": "text", + "theme": "default" +} + +### diff --git a/royalpack/stars/__init__.py b/royalpack/stars/__init__.py index 209efcfa..07a22617 100644 --- a/royalpack/stars/__init__.py +++ b/royalpack/stars/__init__.py @@ -1,27 +1,38 @@ # Imports go here! -from .api_user_list import ApiUserListStar -from .api_user_get import ApiUserGetStar -from .api_diario_list import ApiDiarioListStar -from .api_diario_get import ApiDiarioGetStar +from .api_bio import ApiBioSetStar +from .api_diario import ApiDiarioGetStar +from .api_diario_list import ApiDiarioPagesStar from .api_discord_cv import ApiDiscordCvStar -from .api_wiki_get import ApiWikiGetStar -from .api_wiki_list import ApiUserListStar +from .api_discord_play import ApiDiscordPlayStar +from .api_fiorygi import ApiFiorygiStar +from .api_diario_random import ApiDiarioRandomStar +from .api_poll import ApiPollStar +from .api_poll_list import ApiPollsListStar +from .api_cvstats_latest import ApiCvstatsLatestStar +from .api_cvstats_avg import ApiCvstatsAvgStar +from .api_user_ryg import ApiUserRygStar +from .api_user_ryg_list import ApiUserRygListStar +from .api_user_avatar import ApiUserAvatarStar +from .api_auth_login_osu import ApiAuthLoginOsuStar # Enter the PageStars of your Pack here! available_page_stars = [ - ApiUserListStar, - ApiUserGetStar, - ApiDiarioListStar, + ApiBioSetStar, ApiDiarioGetStar, + ApiDiarioPagesStar, ApiDiscordCvStar, - ApiWikiGetStar, - ApiUserListStar, -] - -# Enter the ExceptionStars of your Pack here! -available_exception_stars = [ - + ApiDiscordPlayStar, + ApiFiorygiStar, + ApiDiarioRandomStar, + ApiPollStar, + ApiPollsListStar, + ApiCvstatsLatestStar, + ApiCvstatsAvgStar, + ApiUserRygStar, + ApiUserRygListStar, + ApiUserAvatarStar, + ApiAuthLoginOsuStar, ] # Don't change this, it should automatically generate __all__ -__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]] +__all__ = [star.__name__ for star in available_page_stars] diff --git a/royalpack/stars/api_auth_login_osu.py b/royalpack/stars/api_auth_login_osu.py new file mode 100644 index 00000000..51a2fb74 --- /dev/null +++ b/royalpack/stars/api_auth_login_osu.py @@ -0,0 +1,102 @@ +import royalnet.utils as ru +import royalnet.backpack.tables as rbt +import royalnet.constellation.api as rca +import royalnet.constellation.api.apierrors as rcae +import itsdangerous +import aiohttp +import aiohttp.client_exceptions +import datetime +from ..types import oauth_refresh +from ..tables import Osu, FiorygiTransaction + + +class ApiAuthLoginOsuStar(rca.ApiStar): + path = "/api/auth/login/osu/v1" + + parameters = { + "get": { + "code": "The code returned by the osu! API.", + "state": "(Optional) The state payload generated by the osu! command to link a new account. " + "If missing, just login." + } + } + + auth = { + "get": False, + } + + tags = ["auth"] + + @property + def client_id(self): + return self.config['osu']['client_id'] + + @property + def client_secret(self): + return self.config['osu']['client_secret'] + + @property + def base_url(self): + return self.config['base_url'] + + @property + def secret_key(self): + return self.config['secret_key'] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Login to Royalnet with your osu! account.""" + OsuT = self.alchemy.get(Osu) + TokenT = self.alchemy.get(rbt.Token) + + code = data.str("code") + state = data.str("state", optional=True) + + if state is not None: + serializer = itsdangerous.URLSafeSerializer(self.config["secret_key"], salt="osu") + uid = serializer.loads(state) + user = await rbt.User.find(self.alchemy, data.session, uid) + else: + user = None + + try: + t = await oauth_refresh(url="https://osu.ppy.sh/oauth/token", + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uri=f"{self.base_url}{self.path}", + refresh_code=code) + except aiohttp.client_exceptions.ClientResponseError: + raise rca.ForbiddenError("osu! API returned an error in the OAuth token exchange") + + async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {t['access_token']}"}) as session: + async with session.get("https://osu.ppy.sh/api/v2/me/") as response: + m = await response.json() + + if user is not None: + osu = OsuT( + user=user, + access_token=t["access_token"], + refresh_token=t["refresh_token"], + expiration_date=datetime.datetime.now() + datetime.timedelta(seconds=t["expires_in"]), + osu_id=m["id"], + username=m["username"] + ) + + data.session.add(osu) + else: + osu = await ru.asyncify( + data.session.query(OsuT).filter_by(osu_id=m["id"]).all + ) + if osu is None: + raise rcae.ForbiddenError("Unknown osu! account") + user = osu.user + + if self.config["osu"]["login"]["enabled"]: + token: rbt.Token = TokenT.generate(alchemy=self.alchemy, user=user, expiration_delta=datetime.timedelta(days=7)) + data.session.add(token) + await data.session_commit() + + return token.json() + else: + raise rcae.ForbiddenError("Account linked successfully; cannot use this account to generate a Royalnet" + " login token, as osu! login is currently disabled on this Royalnet instance.") \ No newline at end of file diff --git a/royalpack/stars/api_bio.py b/royalpack/stars/api_bio.py new file mode 100644 index 00000000..ace47037 --- /dev/null +++ b/royalpack/stars/api_bio.py @@ -0,0 +1,45 @@ +import royalnet.utils as ru +import royalnet.backpack.tables as rbt +import royalnet.constellation.api as rca +from ..tables import Bio + + +class ApiBioSetStar(rca.ApiStar): + path = "/api/bio/v2" + + parameters = { + "get": { + "uid": "The id of the user to get the bio of." + }, + "put": { + "contents": "The contents of the bio." + } + } + + auth = { + "get": False, + "put": True, + } + + tags = ["bio"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get the bio of a specific user.""" + user = await rbt.User.find(self.alchemy, data.session, data.int("uid")) + return user.bio.json() if user.bio else None + + @rca.magic + async def put(self, data: rca.ApiData) -> ru.JSON: + """Set the bio of current user.""" + contents = data["contents"] + BioT = self.alchemy.get(Bio) + user = await data.user() + bio = user.bio + if bio is None: + bio = BioT(user=user, contents=contents) + data.session.add(bio) + else: + bio.contents = contents + await data.session_commit() + return bio.json() diff --git a/royalpack/stars/api_cvstats_avg.py b/royalpack/stars/api_cvstats_avg.py new file mode 100644 index 00000000..f65eb79b --- /dev/null +++ b/royalpack/stars/api_cvstats_avg.py @@ -0,0 +1,164 @@ +import royalnet.constellation.api as rca +import royalnet.utils as ru + + +class ApiCvstatsAvgStar(rca.ApiStar): + path = "/api/cvstats/avg/v1" + + tags = ["cvstats"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get some averages on the cvstats.""" + results = data.session.execute(""" +SELECT * +FROM ( + SELECT date_part('hour', c.h) ph, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM ( + SELECT date_trunc('hour', c.timestamp) h, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM cvstats c + GROUP BY h + ) c + GROUP BY ph +) all_time +LEFT JOIN +( + SELECT date_part('hour', c.h) ph, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM ( + SELECT date_trunc('hour', c.timestamp) h, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM cvstats c + WHERE c.timestamp > current_timestamp - interval '7 day' + GROUP BY h + ) c + GROUP BY ph +) last_week ON last_week.ph = all_time.ph +LEFT JOIN +( + SELECT date_part('hour', c.h) ph, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM ( + SELECT date_trunc('hour', c.timestamp) h, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM cvstats c + WHERE c.timestamp > current_timestamp - interval '30 day' + GROUP BY h + ) c + GROUP BY ph +) last_month ON last_month.ph = all_time.ph +LEFT JOIN +( + SELECT date_part('hour', c.h) ph, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM ( + SELECT date_trunc('hour', c.timestamp) h, + AVG(c.members_connected) members_connected, + AVG(c.users_connected) users_connected, + AVG(c.members_online) members_online, + AVG(c.users_online) users_online, + AVG(c.members_playing) members_playing, + AVG(c.users_playing) users_playing, + AVG(c.members_total) members_total, + AVG(c.users_total) users_total + FROM cvstats c + WHERE c.timestamp > current_timestamp - interval '1 day' + GROUP BY h + ) c + GROUP BY ph +) last_day ON last_day.ph = all_time.ph; + """) + + return [{ + "h": r[0], + "all_time": { + "members_connected": float(r[1]) if r[1] is not None else None, + "users_connected": float(r[2]) if r[2] is not None else None, + "members_online": float(r[3]) if r[3] is not None else None, + "users_online": float(r[4]) if r[4] is not None else None, + "members_playing": float(r[5]) if r[5] is not None else None, + "users_playing": float(r[6]) if r[6] is not None else None, + "members_total": float(r[7]) if r[7] is not None else None, + "users_total": float(r[8]) if r[8] is not None else None, + }, + "last_week": { + "members_connected": float(r[10]) if r[10] is not None else None, + "users_connected": float(r[11]) if r[11] is not None else None, + "members_online": float(r[12]) if r[12] is not None else None, + "users_online": float(r[13]) if r[13] is not None else None, + "members_playing": float(r[14]) if r[14] is not None else None, + "users_playing": float(r[15]) if r[15] is not None else None, + "members_total": float(r[16]) if r[16] is not None else None, + "users_total": float(r[17]) if r[17] is not None else None, + }, + "last_month": { + "members_connected": float(r[19]) if r[19] is not None else None, + "users_connected": float(r[20]) if r[20] is not None else None, + "members_online": float(r[21]) if r[21] is not None else None, + "users_online": float(r[22]) if r[22] is not None else None, + "members_playing": float(r[23]) if r[23] is not None else None, + "users_playing": float(r[24]) if r[24] is not None else None, + "members_total": float(r[25]) if r[25] is not None else None, + "users_total": float(r[26]) if r[26] is not None else None, + }, + "last_day": { + "members_connected": float(r[28]) if r[28] is not None else None, + "users_connected": float(r[29]) if r[29] is not None else None, + "members_online": float(r[30]) if r[30] is not None else None, + "users_online": float(r[31]) if r[31] is not None else None, + "members_playing": float(r[32]) if r[32] is not None else None, + "users_playing": float(r[33]) if r[33] is not None else None, + "members_total": float(r[34]) if r[34] is not None else None, + }, + } for r in sorted(results.fetchall(), key=lambda s: s[0])] diff --git a/royalpack/stars/api_cvstats_latest.py b/royalpack/stars/api_cvstats_latest.py new file mode 100644 index 00000000..4c6d426c --- /dev/null +++ b/royalpack/stars/api_cvstats_latest.py @@ -0,0 +1,18 @@ +import royalnet.utils as ru +import royalnet.constellation.api as rca +from ..tables import Cvstats + + +class ApiCvstatsLatestStar(rca.ApiStar): + path = "/api/cvstats/latest/v1" + + tags = ["cvstats"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get the latest 500 cvstats recorded.""" + CvstatsT = self.alchemy.get(Cvstats) + + cvstats = data.session.query(CvstatsT).order_by(CvstatsT.timestamp.desc()).limit(500).all() + + return list(map(lambda c: c.json(), cvstats)) diff --git a/royalpack/stars/api_diario.py b/royalpack/stars/api_diario.py new file mode 100644 index 00000000..0e9246ec --- /dev/null +++ b/royalpack/stars/api_diario.py @@ -0,0 +1,24 @@ +import royalnet.constellation.api as rca +import royalnet.utils as ru +from ..tables import * + + +class ApiDiarioGetStar(rca.ApiStar): + path = "/api/diario/v2" + + parameters = { + "get": { + "id": "The id of the diario entry to get." + } + } + + tags = ["diario"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get a specific diario entry.""" + diario_id = data.int("id") + entry: Diario = await ru.asyncify(data.session.query(self.alchemy.get(Diario)).get, diario_id) + if entry is None: + raise rca.NotFoundError("No such diario entry.") + return entry.json() diff --git a/royalpack/stars/api_diario_get.py b/royalpack/stars/api_diario_get.py deleted file mode 100644 index 966fc408..00000000 --- a/royalpack/stars/api_diario_get.py +++ /dev/null @@ -1,21 +0,0 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * -from ..tables import * - - -class ApiDiarioGetStar(PageStar): - path = "/api/diario/get/{diario_id}" - - async def page(self, request: Request) -> JSONResponse: - diario_id_str = request.path_params.get("diario_id", "") - try: - diario_id = int(diario_id_str) - except (ValueError, TypeError): - return shoot(400, "Invalid diario_id") - async with self.alchemy.session_acm() as session: - entry: Diario = await asyncify(session.query(self.alchemy.get(Diario)).get, diario_id) - if entry is None: - return shoot(404, "No such user") - return JSONResponse(entry.json()) diff --git a/royalpack/stars/api_diario_list.py b/royalpack/stars/api_diario_list.py index 74043b1a..7bc47f50 100644 --- a/royalpack/stars/api_diario_list.py +++ b/royalpack/stars/api_diario_list.py @@ -1,34 +1,45 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * +from typing import * +import royalnet.constellation.api as rca +import royalnet.utils as ru from ..tables import * -class ApiDiarioListStar(PageStar): - path = "/api/diario/list" +class ApiDiarioPagesStar(rca.ApiStar): + path = "/api/diario/pages/v1" - async def page(self, request: Request) -> JSONResponse: - page_str = request.query_params.get("page", "0") + parameters = { + "get": { + "page": "The diario page you want to get. Can be negative to get the entries in reverse order." + } + } + + tags = ["diario"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get a diario page made of up to 500 diario entries.""" + page_str = data["page"] try: page = int(page_str) - except (ValueError, TypeError): - return shoot(400, "Invalid offset") - async with self.alchemy.session_acm() as session: - if page < 0: - page = -page-1 - entries: typing.List[Diario] = await asyncify( - session.query(self.alchemy.get(Diario)) - .order_by(self.alchemy.get(Diario).diario_id.desc()).limit(500) - .offset(page * 500) - .all - ) - else: - entries: typing.List[Diario] = await asyncify( - session.query(self.alchemy.get(Diario)) - .order_by(self.alchemy.get(Diario).diario_id) - .limit(500) - .offset(page * 500) - .all) - response = [entry.json() for entry in entries] - return JSONResponse(response) + except ValueError: + raise rca.InvalidParameterError("'page' is not a valid int.") + if page < 0: + page = -page-1 + entries: List[Diario] = await ru.asyncify( + data.session + .query(self.alchemy.get(Diario)) + .order_by(self.alchemy.get(Diario).diario_id.desc()).limit(500) + .offset(page * 500) + .all + ) + else: + entries: List[Diario] = await ru.asyncify( + data.session + .query(self.alchemy.get(Diario)) + .order_by(self.alchemy.get(Diario).diario_id) + .limit(500) + .offset(page * 500) + .all + ) + response = [entry.json() for entry in entries] + return response diff --git a/royalpack/stars/api_diario_random.py b/royalpack/stars/api_diario_random.py new file mode 100644 index 00000000..a095c887 --- /dev/null +++ b/royalpack/stars/api_diario_random.py @@ -0,0 +1,36 @@ +from typing import * +import royalnet.constellation.api as rca +import royalnet.utils as ru +from ..tables import * +from sqlalchemy import func + + +class ApiDiarioRandomStar(rca.ApiStar): + path = "/api/diario/random/v1" + + parameters = { + "get": { + "amount": "The number of diario entries to get." + } + } + + tags = ["diario"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get random diario entries.""" + DiarioT = self.alchemy.get(Diario) + try: + amount = int(data["amount"]) + except ValueError: + raise rca.InvalidParameterError("'amount' is not a valid int.") + entries: List[Diario] = await ru.asyncify( + data.session + .query(DiarioT) + .order_by(func.random()) + .limit(amount) + .all + ) + if len(entries) < amount: + raise rca.NotFoundError("Not enough diario entries.") + return list(map(lambda e: e.json(), entries)) diff --git a/royalpack/stars/api_discord_cv.py b/royalpack/stars/api_discord_cv.py index b6b4e8ef..1ee1108c 100644 --- a/royalpack/stars/api_discord_cv.py +++ b/royalpack/stars/api_discord_cv.py @@ -1,12 +1,16 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * +import royalnet.utils as ru +import royalnet.constellation.api as rca -class ApiDiscordCvStar(PageStar): - path = "/api/discord/cv" +class ApiDiscordCvStar(rca.ApiStar): + path = "/api/discord/cv/v1" - async def page(self, request: Request) -> JSONResponse: + tags = ["discord"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get the members status of a single Discord guild. + + Equivalent to calling /cv in a chat.""" response = await self.interface.call_herald_event("discord", "discord_cv") - return JSONResponse(response) + return response diff --git a/royalpack/stars/api_discord_play.py b/royalpack/stars/api_discord_play.py new file mode 100644 index 00000000..3633d826 --- /dev/null +++ b/royalpack/stars/api_discord_play.py @@ -0,0 +1,40 @@ +from typing import * +import royalnet.constellation.api as rca +import logging + + +log = logging.getLogger(__name__) + + +class ApiDiscordPlayStar(rca.ApiStar): + path = "/api/discord/play/v2" + + parameters = { + "post": { + "url": "The url of the audio file to add.", + "user": "The name to display in the File Added message.", + "guild_id": "The id of the guild owning the RoyalQueue to add the audio file to.", + } + } + + tags = ["discord"] + + @rca.magic + async def post(self, data: rca.ApiData) -> dict: + """Add a audio file to the RoyalQueue of a Discord Guild.""" + url = data["url"] + user = data.get("user") + guild_id_str = data.get("guild_id") + if guild_id_str: + try: + guild_id: Optional[int] = int(guild_id_str) + except (ValueError, TypeError): + raise rca.InvalidParameterError("'guild_id' is not a valid int.") + else: + guild_id = None + log.info(f"Received request to play {url} on guild_id {guild_id} via web") + response = await self.interface.call_herald_event("discord", "discord_play", + urls=[url], + guild_id=guild_id, + user=user) + return response diff --git a/royalpack/stars/api_fiorygi.py b/royalpack/stars/api_fiorygi.py new file mode 100644 index 00000000..ecad8d55 --- /dev/null +++ b/royalpack/stars/api_fiorygi.py @@ -0,0 +1,39 @@ +from typing import * +import royalnet.utils as ru +import royalnet.backpack.tables as rbt +import royalnet.constellation.api as rca +from ..tables import Fiorygi + + +class ApiFiorygiStar(rca.ApiStar): + path = "/api/fiorygi/v2" + + parameters = { + "get": { + "uid": "The user to get the fiorygi of." + } + } + + tags = ["fiorygi"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get fiorygi information about a specific user.""" + user = await rbt.User.find(self.alchemy, data.session, data.int("uid")) + if user.fiorygi is None: + return { + "fiorygi": 0, + "transactions": [], + "warning": "No associated fiorygi table" + } + fiorygi: Fiorygi = user.fiorygi + transactions: ru.JSON = sorted(fiorygi.transactions, key=lambda t: -t.id) + return { + "fiorygi": fiorygi.fiorygi, + "transactions": list(map(lambda t: { + "id": t.id, + "change": t.change, + "reason": t.reason, + "timestamp": t.timestamp.isoformat() if t.timestamp else None + }, transactions)) + } diff --git a/royalpack/stars/api_poll.py b/royalpack/stars/api_poll.py new file mode 100644 index 00000000..b60fe7cb --- /dev/null +++ b/royalpack/stars/api_poll.py @@ -0,0 +1,63 @@ +from typing import * +import datetime +import uuid +import royalnet.utils as ru +import royalnet.constellation.api as rca +from ..tables import Poll + + +class ApiPollStar(rca.ApiStar): + path = "/api/poll/v2" + + parameters = { + "get": { + "uuid": "The UUID of the poll to get.", + }, + "post": { + "question": "The question to ask in the poll.", + "description": "A longer Markdown-formatted description.", + "expires": "A ISO timestamp of the expiration date for the poll.", + } + } + + auth = { + "get": False, + "post": True + } + + tags = ["poll"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get a specific poll.""" + PollT = self.alchemy.get(Poll) + + try: + pid = uuid.UUID(data["uuid"]) + except (ValueError, AttributeError, TypeError): + raise rca.InvalidParameterError("'uuid' is not a valid UUID.") + + poll: Poll = await ru.asyncify(data.session.query(PollT).get, pid) + if poll is None: + raise rca.NotFoundError("No such page.") + + return poll.json() + + @rca.magic + async def post(self, data: rca.ApiData) -> ru.JSON: + """Create a new poll.""" + PollT = self.alchemy.get(Poll) + + poll = PollT( + id=uuid.uuid4(), + creator=await data.user(), + created=datetime.datetime.now(), + expires=datetime.datetime.fromisoformat(data["expires"]) if "expires" in data else None, + question=data["question"], + description=data.get("description"), + ) + + data.session.add(poll) + await data.session_commit() + + return poll.json() diff --git a/royalpack/stars/api_poll_list.py b/royalpack/stars/api_poll_list.py new file mode 100644 index 00000000..b686babf --- /dev/null +++ b/royalpack/stars/api_poll_list.py @@ -0,0 +1,25 @@ +from typing import * +import royalnet.constellation.api as rca +import royalnet.utils as ru +from ..tables import Poll + + +class ApiPollsListStar(rca.ApiStar): + path = "/api/poll/list/v2" + + tags = ["poll"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get a list of all polls.""" + PollT = self.alchemy.get(Poll) + + polls: List[Poll] = await ru.asyncify(data.session.query(PollT).all) + + return list(map(lambda p: { + "id": p.id, + "question": p.question, + "creator": p.creator.json(), + "expires": p.expires.isoformat(), + "created": p.created.isoformat(), + }, polls)) diff --git a/royalpack/stars/api_user_avatar.py b/royalpack/stars/api_user_avatar.py new file mode 100644 index 00000000..9128ce8f --- /dev/null +++ b/royalpack/stars/api_user_avatar.py @@ -0,0 +1,38 @@ +import re +import royalnet.utils as ru +import royalnet.constellation.api as rca + + +url_validation = re.compile(r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + + +class ApiUserAvatarStar(rca.ApiStar): + path = "/api/user/avatar/v2" + + parameters = { + "put": { + "avatar_url": "The url that the user wants to set as avatar." + } + } + + auth = { + "put": True, + } + + tags = ["user"] + + @rca.magic + async def put(self, data: rca.ApiData) -> ru.JSON: + """Set the avatar of current user.""" + avatar_url = data["avatar_url"] + user = await data.user() + if not re.match(url_validation, avatar_url): + raise rca.InvalidParameterError("avatar_url is not a valid url.") + user.avatar_url = avatar_url + await data.session_commit() + return user.json() diff --git a/royalpack/stars/api_user_get.py b/royalpack/stars/api_user_get.py deleted file mode 100644 index c8b95e6d..00000000 --- a/royalpack/stars/api_user_get.py +++ /dev/null @@ -1,21 +0,0 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * -from royalnet.backpack.tables import * - - -class ApiUserGetStar(PageStar): - path = "/api/user/get/{uid_str}" - - async def page(self, request: Request) -> JSONResponse: - uid_str = request.path_params.get("uid_str", "") - try: - uid = int(uid_str) - except (ValueError, TypeError): - return shoot(400, "Invalid uid") - async with self.alchemy.session_acm() as session: - user: User = await asyncify(session.query(self.alchemy.get(User)).get, uid) - if user is None: - return shoot(404, "No such user") - return JSONResponse(user.json()) diff --git a/royalpack/stars/api_user_list.py b/royalpack/stars/api_user_list.py deleted file mode 100644 index f520472c..00000000 --- a/royalpack/stars/api_user_list.py +++ /dev/null @@ -1,14 +0,0 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * -from royalnet.backpack.tables import * - - -class ApiUserListStar(PageStar): - path = "/api/user/list" - - async def page(self, request: Request) -> JSONResponse: - async with self.alchemy.session_acm() as session: - users: typing.List[User] = await asyncify(session.query(self.alchemy.get(User)).all) - return JSONResponse([user.json() for user in users]) diff --git a/royalpack/stars/api_user_ryg.py b/royalpack/stars/api_user_ryg.py new file mode 100644 index 00000000..5f0b42ea --- /dev/null +++ b/royalpack/stars/api_user_ryg.py @@ -0,0 +1,45 @@ +import royalnet.backpack.tables as rbt +import royalnet.constellation.api as rca + + +class ApiUserRygStar(rca.ApiStar): + path = "/api/user/ryg/v2" + + parameters = { + "get": { + "uid": "(Choose one) The id of the user to get information about.", + "alias": "(Choose one) The alias of the user to get information about.", + } + } + + tags = ["user"] + + async def get_user(self, data: rca.ApiData): + uid = data.int("uid", optional=True) + alias = data.str("alias", optional=True) + + if uid: + user = await rbt.User.find(self.alchemy, data.session, uid) + elif alias: + user = await rbt.User.find(self.alchemy, data.session, alias) + else: + raise rca.MissingParameterError("Neither uid or alias were specified.") + + if user is None: + raise rca.NotFoundError("No such user.") + + return user + + @rca.magic + async def get(self, data: rca.ApiData) -> dict: + """Get Royalpack information about a user.""" + user = await self.get_user(data) + result = { + **user.json(), + "bio": user.bio.json() if user.bio is not None else None, + "fiorygi": user.fiorygi.fiorygi if user.fiorygi is not None else None, + "steam": [steam.json() for steam in user.steam], + "leagueoflegends": [leagueoflegends.json() for leagueoflegends in user.leagueoflegends], + "trivia": user.trivia_score.json() if user.trivia_score is not None else None + } + return result diff --git a/royalpack/stars/api_user_ryg_list.py b/royalpack/stars/api_user_ryg_list.py new file mode 100644 index 00000000..13dba55b --- /dev/null +++ b/royalpack/stars/api_user_ryg_list.py @@ -0,0 +1,23 @@ +from starlette.responses import * +import royalnet.utils as ru +import royalnet.backpack.tables as rbt +import royalnet.constellation.api as rca + + +class ApiUserRygListStar(rca.ApiStar): + path = "/api/user/ryg/list/v1" + + tags = ["user"] + + @rca.magic + async def get(self, data: rca.ApiData) -> ru.JSON: + """Get Royalpack information about all user.""" + users: typing.List[rbt.User] = await ru.asyncify(data.session.query(self.alchemy.get(rbt.User)).all) + return [{ + **user.json(), + "bio": user.bio.json() if user.bio is not None else None, + "fiorygi": user.fiorygi.fiorygi if user.fiorygi is not None else None, + "steam": [steam.json() for steam in user.steam], + "leagueoflegends": [leagueoflegends.json() for leagueoflegends in user.leagueoflegends], + "trivia": user.trivia_score.json() if user.trivia_score is not None else None + } for user in users] diff --git a/royalpack/stars/api_wiki_get.py b/royalpack/stars/api_wiki_get.py deleted file mode 100644 index 72f5d3c6..00000000 --- a/royalpack/stars/api_wiki_get.py +++ /dev/null @@ -1,22 +0,0 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * -from ..tables import * -import uuid - - -class ApiWikiGetStar(PageStar): - path = "/api/wiki/get/{wiki_page_uuid}" - - async def page(self, request: Request) -> JSONResponse: - wiki_page_uuid_str = request.path_params.get("wiki_page_uuid", "") - try: - wiki_page_uuid = uuid.UUID(wiki_page_uuid_str) - except (ValueError, AttributeError, TypeError): - return shoot(400, "Invalid wiki_page_uuid") - async with self.alchemy.session_acm() as session: - wikipage: WikiPage = await asyncify(session.query(self.alchemy.get(WikiPage)).get, wiki_page_uuid) - if wikipage is None: - return shoot(404, "No such page") - return JSONResponse(wikipage.json_full()) diff --git a/royalpack/stars/api_wiki_list.py b/royalpack/stars/api_wiki_list.py deleted file mode 100644 index a0789947..00000000 --- a/royalpack/stars/api_wiki_list.py +++ /dev/null @@ -1,15 +0,0 @@ -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import * -from royalnet.utils import * -from royalnet.backpack.tables import * -from ..tables import * - - -class ApiUserListStar(PageStar): - path = "/api/wiki/list" - - async def page(self, request: Request) -> JSONResponse: - async with self.alchemy.session_acm() as session: - pages: typing.List[WikiPage] = await asyncify(session.query(self.alchemy.get(WikiPage)).all) - return JSONResponse([page.json_list() for page in pages]) diff --git a/royalpack/tables/__init__.py b/royalpack/tables/__init__.py index 5a201872..e951bb5c 100644 --- a/royalpack/tables/__init__.py +++ b/royalpack/tables/__init__.py @@ -1,29 +1,47 @@ # Imports go here! from .diario import Diario -from .aliases import Alias from .wikipages import WikiPage -from .wikirevisions import WikiRevision from .bios import Bio from .reminders import Reminder from .triviascores import TriviaScore -from .mmevents import MMEvent -from .mmresponse import MMResponse from .leagueoflegends import LeagueOfLegends from .fiorygi import Fiorygi +from .steam import Steam +from .dota import Dota +from .fiorygitransactions import FiorygiTransaction +from .brawlhalla import Brawlhalla +from .polls import Poll +from .pollcomments import PollComment +from .pollvotes import PollVote +from .brawlhalladuos import BrawlhallaDuo +from .mmevents import MMEvent +from .mmresponse import MMResponse +from .cvstats import Cvstats +from .treasure import Treasure +from .osu import Osu # Enter the tables of your Pack here! available_tables = [ Diario, - Alias, WikiPage, - WikiRevision, Bio, Reminder, TriviaScore, - MMEvent, - MMResponse, LeagueOfLegends, Fiorygi, + Steam, + Dota, + FiorygiTransaction, + Brawlhalla, + Poll, + PollComment, + PollVote, + BrawlhallaDuo, + MMEvent, + MMResponse, + Cvstats, + Treasure, + Osu, ] # Don't change this, it should automatically generate __all__ diff --git a/royalpack/tables/aliases.py b/royalpack/tables/aliases.py deleted file mode 100644 index 2a1c99ec..00000000 --- a/royalpack/tables/aliases.py +++ /dev/null @@ -1,28 +0,0 @@ -from sqlalchemy import Column, \ - Integer, \ - String, \ - ForeignKey -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declared_attr - - -class Alias: - __tablename__ = "aliases" - - @declared_attr - def royal_id(self): - return Column(Integer, ForeignKey("users.uid")) - - @declared_attr - def alias(self): - return Column(String, primary_key=True) - - @declared_attr - def royal(self): - return relationship("User", backref="aliases") - - def __repr__(self): - return f"" - - def __str__(self): - return f"{self.alias}->{self.royal_id}" diff --git a/royalpack/tables/bios.py b/royalpack/tables/bios.py index 25a3a630..ebd6bf0d 100644 --- a/royalpack/tables/bios.py +++ b/royalpack/tables/bios.py @@ -10,19 +10,24 @@ class Bio: __tablename__ = "bios" @declared_attr - def royal_id(self): + def user_id(self): return Column(Integer, ForeignKey("users.uid"), primary_key=True) @declared_attr - def royal(self): + def user(self): return relationship("User", backref=backref("bio", uselist=False)) @declared_attr def contents(self): return Column(Text, nullable=False, default="") + def json(self) -> dict: + return { + "contents": self.contents + } + def __repr__(self): - return f"" + return f"" def __str__(self): return self.contents diff --git a/royalpack/tables/brawlhalla.py b/royalpack/tables/brawlhalla.py new file mode 100644 index 00000000..cbc01466 --- /dev/null +++ b/royalpack/tables/brawlhalla.py @@ -0,0 +1,110 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr +import steam.steamid +from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal, Updatable + + +# noinspection PyAttributeOutsideInit +class Brawlhalla(Updatable): + __tablename__ = "brawlhalla" + + @declared_attr + def brawlhalla_id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def _steamid(self): + return Column(BigInteger, ForeignKey("steam._steamid"), unique=True) + + @declared_attr + def steam(self): + return relationship("Steam", backref=backref("brawlhalla", uselist=False)) + + @property + def steamid(self): + return steam.steamid.SteamID(self._steamid) + + @declared_attr + def name(self): + return Column(String, nullable=False) + + @declared_attr + def rating_1v1(self): + return Column(Integer) + + @declared_attr + def tier_1v1(self): + return Column(Enum(BrawlhallaTier)) + + @declared_attr + def metal_1v1(self): + return Column(Enum(BrawlhallaMetal)) + + @property + def rank_1v1(self): + if self.metal_1v1 is None: + return None + return BrawlhallaRank(metal=self.metal_1v1, tier=self.tier_1v1) + + @rank_1v1.setter + def rank_1v1(self, value): + if not isinstance(value, BrawlhallaRank): + raise TypeError("rank_1v1 can only be set to BrawlhallaRank values.") + self.metal_1v1 = value.metal + self.tier_1v1 = value.tier + + @property + def duos(self): + return [*self._duos_one, *self._duos_two] + + @property + def rating_2v2(self): + duos = sorted(self.duos, key=lambda d: -d.rating_2v2) + if len(duos) == 0: + return None + return duos[0].rating_2v2 + + @property + def tier_2v2(self): + duos = sorted(self.duos, key=lambda d: -d.rating_2v2) + if len(duos) == 0: + return None + return duos[0].tier_2v2 + + @property + def metal_2v2(self): + duos = sorted(self.duos, key=lambda d: -d.rating_2v2) + if len(duos) == 0: + return None + return duos[0].metal_2v2 + + @property + def rank_2v2(self): + duos = sorted(self.duos, key=lambda d: -d.rating_2v2) + if len(duos) == 0: + return None + return duos[0].rank_2v2 + + def json(self): + one_rank = self.rank_1v1 + two_rank = self.rank_2v2 + return { + "name": self.name, + "1v1": { + "rating": self.rating_1v1, + "metal": one_rank.metal.name, + "tier": one_rank.tier.name + } if one_rank is not None else None, + "2v2": { + "rating": self.rating_2v2, + "metal": two_rank.metal.name, + "tier": two_rank.tier.name + } if two_rank is not None else None + } + + def __repr__(self): + return f"" + + def __str__(self): + return f"[c]brawlhalla:{self.brawlhalla_id}[/c]" diff --git a/royalpack/tables/brawlhalladuos.py b/royalpack/tables/brawlhalladuos.py new file mode 100644 index 00000000..1561cc2d --- /dev/null +++ b/royalpack/tables/brawlhalladuos.py @@ -0,0 +1,58 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr +from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal + + +class BrawlhallaDuo: + __tablename__ = "brawlhalladuos" + + @declared_attr + def id_one(self): + return Column(Integer, ForeignKey("brawlhalla.brawlhalla_id"), primary_key=True) + + @declared_attr + def id_two(self): + return Column(Integer, ForeignKey("brawlhalla.brawlhalla_id"), primary_key=True) + + @declared_attr + def one(self): + return relationship("Brawlhalla", foreign_keys=self.id_one, backref=backref("_duos_one")) + + @declared_attr + def two(self): + return relationship("Brawlhalla", foreign_keys=self.id_two, backref=backref("_duos_two")) + + @declared_attr + def rating_2v2(self): + return Column(Integer) + + @declared_attr + def tier_2v2(self): + return Column(Enum(BrawlhallaTier)) + + @declared_attr + def metal_2v2(self): + return Column(Enum(BrawlhallaMetal)) + + @property + def rank_2v2(self): + return BrawlhallaRank(metal=self.metal_2v2, tier=self.tier_2v2) + + @rank_2v2.setter + def rank_2v2(self, value): + if not isinstance(value, BrawlhallaRank): + raise TypeError("rank_1v1 can only be set to BrawlhallaRank values.") + self.metal_2v2 = value.metal + self.tier_2v2 = value.tier + + def other(self, bh): + if bh == self.one: + return self.two + elif bh == self.two: + return self.one + else: + raise ValueError("Argument is unrelated to this duo.") + + def __repr__(self): + return f"" diff --git a/royalpack/tables/cvstats.py b/royalpack/tables/cvstats.py new file mode 100644 index 00000000..1165d3bc --- /dev/null +++ b/royalpack/tables/cvstats.py @@ -0,0 +1,60 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr + + +class Cvstats: + __tablename__ = "cvstats" + + @declared_attr + def id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def timestamp(self): + return Column(DateTime) + + @declared_attr + def members_connected(self): + return Column(Integer) + + @declared_attr + def users_connected(self): + return Column(Integer) + + @declared_attr + def members_online(self): + return Column(Integer) + + @declared_attr + def users_online(self): + return Column(Integer) + + @declared_attr + def members_playing(self): + return Column(Integer) + + @declared_attr + def users_playing(self): + return Column(Integer) + + @declared_attr + def members_total(self): + return Column(Integer) + + @declared_attr + def users_total(self): + return Column(Integer) + + def json(self): + return { + "timestamp": self.timestamp.isoformat(), + "users_total": self.users_total, + "members_total": self.members_total, + "users_online": self.users_online, + "members_online": self.members_online, + "users_connected": self.users_connected, + "members_connected": self.members_connected, + "users_playing": self.users_playing, + "members_playing": self.members_playing, + } \ No newline at end of file diff --git a/royalpack/tables/diario.py b/royalpack/tables/diario.py index 8422214d..fcd5166a 100644 --- a/royalpack/tables/diario.py +++ b/royalpack/tables/diario.py @@ -88,7 +88,7 @@ class Diario: text += f" da {str(self.creator)}" text += f" il {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n" if self.media_url is not None: - text += f"{self.media_url}\n" + text += f"[url={self.media_url}]Media[/url]\n" if self.text is not None: if self.spoiler: hidden = re.sub(r"\w", "█", self.text) diff --git a/royalpack/tables/dota.py b/royalpack/tables/dota.py new file mode 100644 index 00000000..0064be95 --- /dev/null +++ b/royalpack/tables/dota.py @@ -0,0 +1,92 @@ +from typing import * +from sqlalchemy import * +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declared_attr +from ..types import DotaMedal, DotaStars, DotaRank, Updatable +import steam.steamid + + +class Dota(Updatable): + __tablename__ = "dota" + + @declared_attr + def _steamid(self): + return Column(BigInteger, ForeignKey("steam._steamid"), primary_key=True) + + @declared_attr + def steam(self): + return relationship("Steam", backref=backref("dota", uselist=False)) + + @property + def steamid(self): + return steam.steamid.SteamID(self._steamid) + + @declared_attr + def _rank_tier(self): + return Column(Integer) + + @property + def medal(self) -> Optional[DotaMedal]: + if self._rank_tier is None: + return None + return DotaMedal(self._rank_tier // 10) + + @medal.setter + def medal(self, value: DotaMedal): + if not isinstance(value, DotaMedal): + raise AttributeError("medal can only be set to DotaMedal objects.") + self._rank_tier = value.value * 10 + self.stars.value + + @property + def stars(self) -> Optional[DotaStars]: + if self._rank_tier is None: + return None + return DotaStars(self._rank_tier % 10) + + @stars.setter + def stars(self, value: DotaStars): + if not isinstance(value, DotaStars): + raise AttributeError("stars can only be set to DotaStars objects.") + self._rank_tier = self.medal.value * 10 + value.value + + @property + def rank(self) -> Optional[DotaRank]: + if self._rank_tier is None: + return None + return DotaRank(self.medal, self.stars) + + @rank.setter + def rank(self, value: Optional[DotaRank]): + if value is None: + self._rank_tier = None + return + if not isinstance(value, DotaRank): + raise AttributeError("rank can only be set to DotaRank objects (or None).") + self._rank_tier = value.rank_tier + + @declared_attr + def wins(self): + return Column(Integer) + + @declared_attr + def losses(self): + return Column(Integer) + + def json(self): + rank = self.rank + + return { + "rank": { + "raw": self._rank_tier, + "medal": rank.medal.name, + "rank": rank.stars.name + } if self._rank_tier is not None else None, + "wins": self.wins, + "losses": self.losses + } + + def __repr__(self): + return f"" + + def __str__(self): + return f"[c]dota:{self._steamid}[/c]" diff --git a/royalpack/tables/fiorygi.py b/royalpack/tables/fiorygi.py index 65392d2f..81120b9d 100644 --- a/royalpack/tables/fiorygi.py +++ b/royalpack/tables/fiorygi.py @@ -19,7 +19,7 @@ class Fiorygi: return Column(Integer, nullable=False, default=0) def __repr__(self): - return f"" + return f"<{self.__class__.__name__} di {self.user}: {self.fiorygi}>" def __str__(self): return f"{self.fiorygi} fioryg" + ("i" if self.fiorygi != 1 else "") diff --git a/royalpack/tables/fiorygitransactions.py b/royalpack/tables/fiorygitransactions.py new file mode 100644 index 00000000..406381e8 --- /dev/null +++ b/royalpack/tables/fiorygitransactions.py @@ -0,0 +1,82 @@ +from typing import * +import datetime + +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr + +from .fiorygi import Fiorygi + +if TYPE_CHECKING: + from royalnet.commands import CommandData + + +class FiorygiTransaction: + __tablename__ = "fiorygitransactions" + + @declared_attr + def id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def change(self): + return Column(Integer, nullable=False) + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("fiorygi.user_id"), nullable=False) + + @declared_attr + def wallet(self): + return relationship("Fiorygi", backref=backref("transactions")) + + @property + def user(self): + return self.wallet.user + + @declared_attr + def reason(self): + return Column(String, nullable=False, default="") + + @declared_attr + def timestamp(self): + return Column(DateTime) + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.change:+} to {self.user.username} for {self.reason}>" + + @classmethod + async def spawn_fiorygi(cls, data: "CommandData", user, qty: int, reason: str): + if user.fiorygi is None: + data.session.add(data._interface.alchemy.get(Fiorygi)( + user_id=user.uid, + fiorygi=0 + )) + await data.session_commit() + + transaction = data._interface.alchemy.get(FiorygiTransaction)( + user_id=user.uid, + change=qty, + reason=reason, + timestamp=datetime.datetime.now() + ) + data.session.add(transaction) + + user.fiorygi.fiorygi += qty + await data.session_commit() + + if len(user.telegram) > 0: + user_str = user.telegram[0].mention() + else: + user_str = user.username + + if qty > 0: + msg = f"💰 [b]{user_str}[/b] ha ottenuto [b]{qty}[/b] fioryg{'i' if qty != 1 else ''} per [i]{reason}[/i]!" + elif qty == 0: + msg = f"❓ [b]{user_str}[/b] ha mantenuto i suoi fiorygi attuali per [i]{reason}[/i].\nWait, cosa?" + else: + msg = f"💸 [b]{user_str}[/b] ha perso [b]{-qty}[/b] fioryg{'i' if qty != -1 else ''} per [i]{reason}[/i]." + + await data._interface.call_herald_event("telegram", "telegram_message", + chat_id=data._interface.config["Telegram"]["main_group_id"], + text=msg) diff --git a/royalpack/tables/leagueoflegends.py b/royalpack/tables/leagueoflegends.py index 87763fc3..3871869d 100644 --- a/royalpack/tables/leagueoflegends.py +++ b/royalpack/tables/leagueoflegends.py @@ -1,10 +1,10 @@ from sqlalchemy import * from sqlalchemy.orm import relationship, composite from sqlalchemy.ext.declarative import declared_attr -from ..utils import LeagueRank, LeagueTier, LeagueLeague +from ..types import LeagueRank, LeagueTier, LeagueLeague, Updatable -class LeagueOfLegends: +class LeagueOfLegends(Updatable): __tablename__ = "leagueoflegends" @declared_attr @@ -147,108 +147,24 @@ class LeagueOfLegends: self.rank_flexq_fresh_blood, self.rank_flexq_veteran) - @declared_attr - def rank_twtrq_tier(self): - return Column(Enum(LeagueTier)) - - @declared_attr - def rank_twtrq_rank(self): - return Column(Enum(LeagueRank)) - - @declared_attr - def rank_twtrq_points(self): - return Column(Integer) - - @declared_attr - def rank_twtrq_wins(self): - return Column(Integer) - - @declared_attr - def rank_twtrq_losses(self): - return Column(Integer) - - @declared_attr - def rank_twtrq_inactive(self): - return Column(Boolean) - - @declared_attr - def rank_twtrq_hot_streak(self): - return Column(Boolean) - - @declared_attr - def rank_twtrq_fresh_blood(self): - return Column(Boolean) - - @declared_attr - def rank_twtrq_veteran(self): - return Column(Boolean) - - @declared_attr - def rank_twtrq(self): - return composite(LeagueLeague, - self.rank_twtrq_tier, - self.rank_twtrq_rank, - self.rank_twtrq_points, - self.rank_twtrq_wins, - self.rank_twtrq_losses, - self.rank_twtrq_inactive, - self.rank_twtrq_hot_streak, - self.rank_twtrq_fresh_blood, - self.rank_twtrq_veteran) - - @declared_attr - def rank_tftq_tier(self): - return Column(Enum(LeagueTier)) - - @declared_attr - def rank_tftq_rank(self): - return Column(Enum(LeagueRank)) - - @declared_attr - def rank_tftq_points(self): - return Column(Integer) - - @declared_attr - def rank_tftq_wins(self): - return Column(Integer) - - @declared_attr - def rank_tftq_losses(self): - return Column(Integer) - - @declared_attr - def rank_tftq_inactive(self): - return Column(Boolean) - - @declared_attr - def rank_tftq_hot_streak(self): - return Column(Boolean) - - @declared_attr - def rank_tftq_fresh_blood(self): - return Column(Boolean) - - @declared_attr - def rank_tftq_veteran(self): - return Column(Boolean) - - @declared_attr - def rank_tftq(self): - return composite(LeagueLeague, - self.rank_tftq_tier, - self.rank_tftq_rank, - self.rank_tftq_points, - self.rank_tftq_wins, - self.rank_tftq_losses, - self.rank_tftq_inactive, - self.rank_tftq_hot_streak, - self.rank_tftq_fresh_blood, - self.rank_tftq_veteran) - @declared_attr def mastery_score(self): return Column(Integer, nullable=False, default=0) + def json(self): + return { + "region": self.region, + "profile_icon_id": self.profile_icon_id, + "summoner_name": self.summoner_name, + "puuid": self.puuid, + "summoner_level": self.summoner_level, + "summoner_id": self.summoner_id, + "account_id": self.account_id, + "soloq": self.rank_soloq.json() if self.rank_soloq is not None else None, + "flexq": self.rank_flexq.json() if self.rank_flexq is not None else None, + "mastery_score": self.mastery_score, + } + def __repr__(self): return f"<{self.__class__.__qualname__} {str(self)}>" diff --git a/royalpack/tables/mmevents.py b/royalpack/tables/mmevents.py index f633c0f8..2c393ad0 100644 --- a/royalpack/tables/mmevents.py +++ b/royalpack/tables/mmevents.py @@ -21,7 +21,7 @@ class MMEvent: @declared_attr def datetime(self): - return Column(DateTime, nullable=False) + return Column(DateTime) @declared_attr def title(self): @@ -48,5 +48,9 @@ class MMEvent: def interface_data(self, value): self.raw_interface_data = pickle.dumps(value) + @declared_attr + def interrupted(self): + return Column(Boolean, nullable=False, default=False) + def __repr__(self): return f"" diff --git a/royalpack/tables/mmresponse.py b/royalpack/tables/mmresponse.py index 4d04931a..6135e46c 100644 --- a/royalpack/tables/mmresponse.py +++ b/royalpack/tables/mmresponse.py @@ -1,7 +1,7 @@ from sqlalchemy import * from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declared_attr -from ..utils import MMChoice +from ..types import MMChoice class MMResponse: diff --git a/royalpack/tables/osu.py b/royalpack/tables/osu.py new file mode 100644 index 00000000..99179a7d --- /dev/null +++ b/royalpack/tables/osu.py @@ -0,0 +1,94 @@ +from typing import * +import aiohttp +import datetime +from sqlalchemy import * +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declared_attr + +from ..types import Updatable, oauth_refresh + + +# noinspection PyAttributeOutsideInit +class Osu(Updatable): + __tablename__ = "osu" + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def user(self): + return relationship("User", backref=backref("osu")) + + @declared_attr + def access_token(self): + return Column(String, nullable=False) + + @declared_attr + def refresh_token(self): + return Column(String, nullable=False) + + @declared_attr + def expiration_date(self): + return Column(DateTime, nullable=False) + + @declared_attr + def osu_id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def username(self): + return Column(String, nullable=False) + + @declared_attr + def avatar_url(self): + return Column(String, nullable=False) + + @declared_attr + def standard_pp(self): + return Column(Float) + + @declared_attr + def taiko_pp(self): + return Column(Float) + + @declared_attr + def catch_pp(self): + return Column(Float) + + @declared_attr + def mania_pp(self): + return Column(Float) + + async def refresh(self, *, client_id, client_secret, base_url, path): + j = await oauth_refresh(url="https://osu.ppy.sh/oauth/token", + client_id=client_id, + client_secret=client_secret, + redirect_uri=f"{base_url}{path}", + refresh_code=self.refresh_token) + self.access_token = j["access_token"] + self.refresh_token = j["refresh_token"] + self.expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=j["expires_in"]) + + async def refresh_if_expired(self, *, client_id, client_secret, base_url, path): + if datetime.datetime.now() >= self.expiration_date: + await self.refresh(client_id=client_id, client_secret=client_secret, base_url=base_url, path=path) + + def json(self) -> dict: + return { + "osu_id": self.osu_id, + "username": self.username, + "avatar_url": self.avatar_url, + "standard": { + "pp": self.standard_pp, + }, + "taiko": { + "pp": self.taiko_pp, + }, + "catch": { + "pp": self.catch_pp, + }, + "mania": { + "pp": self.mania_pp, + }, + } diff --git a/royalpack/tables/pollcomments.py b/royalpack/tables/pollcomments.py new file mode 100644 index 00000000..f39851de --- /dev/null +++ b/royalpack/tables/pollcomments.py @@ -0,0 +1,41 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.dialects.postgresql import UUID +from ..types import PollMood + + +class PollComment: + __tablename__ = "pollcomments" + + @declared_attr + def id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def author_id(self): + return Column(Integer, ForeignKey("users.uid"), nullable=False) + + @declared_attr + def author(self): + return relationship("User", backref=backref("poll_comments_created")) + + @declared_attr + def poll_id(self): + return Column(UUID(as_uuid=True), ForeignKey("polls.id")) + + @declared_attr + def poll(self): + return relationship("Poll", backref=backref("comments")) + + @declared_attr + def posted(self): + return Column(DateTime, nullable=False) + + @declared_attr + def mood(self): + return Column(Enum(PollMood), nullable=False, default=PollMood.NEUTRAL) + + @declared_attr + def comment(self): + return Column(Text) diff --git a/royalpack/tables/polls.py b/royalpack/tables/polls.py new file mode 100644 index 00000000..d45d512d --- /dev/null +++ b/royalpack/tables/polls.py @@ -0,0 +1,64 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.dialects.postgresql import UUID + + +class Poll: + __tablename__ = "polls" + + @declared_attr + def id(self): + return Column(UUID(as_uuid=True), primary_key=True) + + @declared_attr + def question(self): + return Column(String, nullable=False) + + @declared_attr + def description(self): + return Column(Text, nullable=False, server_default="") + + @declared_attr + def creator_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def creator(self): + return relationship("User", backref=backref("polls_created")) + + @declared_attr + def expires(self): + return Column(DateTime) + + @declared_attr + def created(self): + return Column(DateTime, nullable=False) + + def json(self): + return { + "id": self.id, + "question": self.question, + "description": self.description, + "creator": self.creator.json(), + "expires": self.expires.isoformat(), + "created": self.created.isoformat(), + "votes": map( + lambda v: { + "caster": v.caster.json(), + "posted": v.posted.isoformat(), + "vote": v.vote.name + }, + sorted(self.votes, key=lambda v: v.posted) + ), + "comments": map( + lambda c: { + "id": c.id, + "comment": c.comment, + "creator": c.creator.json(), + "posted": c.posted.isoformat(), + "mood": c.mood.name, + }, + sorted(self.comments, key=lambda c: c.posted) + ) + } diff --git a/royalpack/tables/pollvotes.py b/royalpack/tables/pollvotes.py new file mode 100644 index 00000000..f4886c04 --- /dev/null +++ b/royalpack/tables/pollvotes.py @@ -0,0 +1,33 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr +from ..types import PollMood +from sqlalchemy.dialects.postgresql import UUID + + +class PollVote: + __tablename__ = "pollvotes" + + @declared_attr + def caster_id(self): + return Column(Integer, ForeignKey("users.uid"), primary_key=True) + + @declared_attr + def caster(self): + return relationship("User", backref=backref("poll_votes_cast")) + + @declared_attr + def poll_id(self): + return Column(UUID(as_uuid=True), ForeignKey("polls.id"), primary_key=True) + + @declared_attr + def poll(self): + return relationship("Poll", backref=backref("votes")) + + @declared_attr + def posted(self): + return Column(DateTime, nullable=False) + + @declared_attr + def vote(self): + return Column(Enum(PollMood), nullable=False, default=PollMood.NEUTRAL) diff --git a/royalpack/tables/steam.py b/royalpack/tables/steam.py new file mode 100644 index 00000000..1e4fedbd --- /dev/null +++ b/royalpack/tables/steam.py @@ -0,0 +1,86 @@ +from sqlalchemy import * +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext.declarative import declared_attr +import steam.steamid + + +class Steam: + __tablename__ = "steam" + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def user(self): + return relationship("User", backref=backref("steam")) + + @declared_attr + def _steamid(self): + return Column(BigInteger, primary_key=True) + + @property + def steamid(self): + return steam.steamid.SteamID(self._steamid) + + @declared_attr + def persona_name(self): + return Column(String) + + @declared_attr + def profile_url(self): + return Column(String) + + @declared_attr + def avatar(self): + return Column(String) + + @declared_attr + def primary_clan_id(self): + return Column(BigInteger) + + @declared_attr + def account_creation_date(self): + return Column(DateTime) + + @declared_attr + def account_level(self): + return Column(Integer, nullable=False, default=0) + + @declared_attr + def owned_games_count(self): + return Column(Integer, nullable=False, default=0) + + @declared_attr + def most_played_game_2weeks(self): + return Column(Integer, nullable=False, default=753) + + @declared_attr + def most_played_game_forever(self): + return Column(Integer, nullable=False, default=753) + + def json(self): + return { + "steamid2": self.steamid.as_steam2, + "steamid3": self.steamid.as_steam3, + "steamid32": self.steamid.as_32, + "steamid64": self.steamid.as_64, + "persona_name": self.persona_name, + "profile_url": self.profile_url, + "avatar": self.avatar, + "primary_clan_id": self.primary_clan_id, + "account_creation_date": self.account_creation_date.isoformat(), + "account_level": self.account_level, + "owned_games_count": self.owned_games_count, + "most_played_game_2weeks": self.most_played_game_2weeks, + "most_played_game_forever": self.most_played_game_forever, + + "dota": self.dota.json() if self.dota is not None else None, + "brawlhalla": self.brawlhalla.json() if self.brawlhalla is not None else None + } + + def __repr__(self): + return f"" + + def __str__(self): + return f"[c]steam:{self._steamid}[/c]" diff --git a/royalpack/tables/treasure.py b/royalpack/tables/treasure.py new file mode 100644 index 00000000..daf1018a --- /dev/null +++ b/royalpack/tables/treasure.py @@ -0,0 +1,23 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr + + +class Treasure: + __tablename__ = "treasures" + + @declared_attr + def code(self): + return Column(String, primary_key=True) + + @declared_attr + def redeemed_by_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def redeemed_by(self): + return relationship("User") + + @declared_attr + def value(self): + return Column(Integer, nullable=False) diff --git a/royalpack/tables/triviascores.py b/royalpack/tables/triviascores.py index 39d251ae..8f240983 100644 --- a/royalpack/tables/triviascores.py +++ b/royalpack/tables/triviascores.py @@ -9,11 +9,11 @@ class TriviaScore: __tablename__ = "triviascores" @declared_attr - def royal_id(self): + def user_id(self): return Column(Integer, ForeignKey("users.uid"), primary_key=True) @declared_attr - def royal(self): + def user(self): return relationship("User", backref=backref("trivia_score", uselist=False)) @declared_attr @@ -34,7 +34,22 @@ class TriviaScore: @property def correct_rate(self): + if self.total_answers == 0: + return 0.0 return self.correct_answers / self.total_answers + @property + def score(self) -> float: + return (self.correct_answers + self.correct_answers * self.correct_rate) * 10 + + def json(self): + return { + "correct": self.correct_answers, + "wrong": self.wrong_answers, + "total": self.total_answers, + "rate": self.correct_rate, + "score": self.score + } + def __repr__(self): - return f"" + return f"" diff --git a/royalpack/tables/wikipages.py b/royalpack/tables/wikipages.py index c1fadfc9..facfdd02 100644 --- a/royalpack/tables/wikipages.py +++ b/royalpack/tables/wikipages.py @@ -31,7 +31,7 @@ class WikiPage: @declared_attr def theme(self): - return Column(String) + return Column(String, nullable=False, default="default") @property def page_short_id(self): diff --git a/royalpack/tables/wikirevisions.py b/royalpack/tables/wikirevisions.py deleted file mode 100644 index 456376d7..00000000 --- a/royalpack/tables/wikirevisions.py +++ /dev/null @@ -1,48 +0,0 @@ -from sqlalchemy import Column, \ - Integer, \ - Text, \ - DateTime, \ - ForeignKey -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declared_attr - - -class WikiRevision: - """A wiki page revision. - - Warning: - Requires PostgreSQL!""" - __tablename__ = "wikirevisions" - - @declared_attr - def revision_id(self): - return Column(UUID(as_uuid=True), primary_key=True) - - @declared_attr - def page_id(self): - return Column(UUID(as_uuid=True), ForeignKey("wikipages.page_id"), nullable=False) - - @declared_attr - def page(self): - return relationship("WikiPage", foreign_keys=self.page_id, backref="revisions") - - @declared_attr - def author_id(self): - return Column(Integer, ForeignKey("users.uid"), nullable=False) - - @declared_attr - def author(self): - return relationship("User", foreign_keys=self.author_id, backref="wiki_contributions") - - @declared_attr - def timestamp(self): - return Column(DateTime, nullable=False) - - @declared_attr - def reason(self): - return Column(Text) - - @declared_attr - def diff(self): - return Column(Text) diff --git a/royalpack/types/__init__.py b/royalpack/types/__init__.py new file mode 100644 index 00000000..4e8f08a7 --- /dev/null +++ b/royalpack/types/__init__.py @@ -0,0 +1,33 @@ +from .mmchoice import MMChoice +from .mminterfacedata import MMInterfaceData, MMInterfaceDataTelegram +from .leaguetier import LeagueTier +from .leaguerank import LeagueRank +from .leagueleague import LeagueLeague +from .dotamedal import DotaMedal +from .dotastars import DotaStars +from .dotarank import DotaRank +from .brawlhallatier import BrawlhallaTier +from .brawlhallametal import BrawlhallaMetal +from .brawlhallarank import BrawlhallaRank +from .pollmood import PollMood +from .updatable import Updatable +from .oauth_refresh import oauth_refresh + + +__all__ = [ + "MMChoice", + "MMInterfaceData", + "MMInterfaceDataTelegram", + "LeagueTier", + "LeagueRank", + "LeagueLeague", + "DotaMedal", + "DotaStars", + "DotaRank", + "BrawlhallaMetal", + "BrawlhallaRank", + "BrawlhallaTier", + "PollMood", + "Updatable", + "oauth_refresh", +] diff --git a/royalpack/types/brawlhallametal.py b/royalpack/types/brawlhallametal.py new file mode 100644 index 00000000..6ab20f92 --- /dev/null +++ b/royalpack/types/brawlhallametal.py @@ -0,0 +1,23 @@ +import enum + + +class BrawlhallaMetal(enum.Enum): + TIN = 0 + BRONZE = 1 + SILVER = 2 + GOLD = 3 + PLATINUM = 4 + DIAMOND = 5 + + def __str__(self): + return self.name.capitalize() + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + if other is None: + return True + if not isinstance(other, self.__class__): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + return self.value > other.value diff --git a/royalpack/types/brawlhallarank.py b/royalpack/types/brawlhallarank.py new file mode 100644 index 00000000..f1419dc1 --- /dev/null +++ b/royalpack/types/brawlhallarank.py @@ -0,0 +1,36 @@ +from .brawlhallametal import BrawlhallaMetal +from .brawlhallatier import BrawlhallaTier + + +class BrawlhallaRank: + __slots__ = "metal", "tier" + + def __init__(self, metal: BrawlhallaMetal, tier: BrawlhallaTier): + self.metal: BrawlhallaMetal = metal + self.tier: BrawlhallaTier = tier + + def __gt__(self, other): + if other is None: + return True + if not isinstance(other, self.__class__): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + if self.metal > other.metal: + return True + elif self.metal < other.metal: + return False + elif self.tier > other.tier: + return True + return False + + def __eq__(self, other): + if other is None: + return False + if not isinstance(other, self.__class__): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + return self.metal == other.metal and self.tier == other.tier + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {self.metal} {self.tier}>" + + def __str__(self): + return f"{self.metal} {self.tier}" diff --git a/royalpack/types/brawlhallatier.py b/royalpack/types/brawlhallatier.py new file mode 100644 index 00000000..cac66713 --- /dev/null +++ b/royalpack/types/brawlhallatier.py @@ -0,0 +1,23 @@ +import enum + + +class BrawlhallaTier(enum.Enum): + ZERO = 0 + I = 1 + II = 2 + III = 3 + IV = 4 + V = 5 + + def __str__(self): + return str(self.value) + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + if other is None: + return True + if not isinstance(other, self.__class__): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + return self.value > other.value diff --git a/royalpack/types/dotamedal.py b/royalpack/types/dotamedal.py new file mode 100644 index 00000000..865bf3c4 --- /dev/null +++ b/royalpack/types/dotamedal.py @@ -0,0 +1,21 @@ +import enum + + +class DotaMedal(enum.Enum): + HERALD = 1 + GUARDIAN = 2 + CRUSADER = 3 + ARCHON = 4 + LEGEND = 5 + ANCIENT = 6 + DIVINE = 7 + IMMORTAL = 8 + + def __str__(self): + return self.name.capitalize() + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + return self.value > other.value diff --git a/royalpack/types/dotarank.py b/royalpack/types/dotarank.py new file mode 100644 index 00000000..46d3b5d0 --- /dev/null +++ b/royalpack/types/dotarank.py @@ -0,0 +1,46 @@ +from .dotamedal import DotaMedal +from .dotastars import DotaStars + + +class DotaRank: + __slots__ = "medal", "stars" + + def __init__(self, medal: DotaMedal = None, stars: DotaStars = None, *, rank_tier: int = None): + if rank_tier is not None: + self.medal: DotaMedal = DotaMedal(rank_tier // 10) + self.stars: DotaStars = DotaStars(rank_tier % 10) + else: + if medal is None or stars is None: + raise AttributeError("Missing medal, stars or rank_tier.") + self.medal = medal + self.stars = stars + + def __gt__(self, other): + if other is None: + return True + if not isinstance(other, DotaRank): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + if self.medal > other.medal: + return True + elif self.medal < other.medal: + return False + elif self.stars > other.stars: + return True + return False + + def __eq__(self, other): + if other is None: + return False + if not isinstance(other, DotaRank): + raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}") + return self.medal == other.medal and self.stars == other.stars + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {self.medal.name} {self.stars.name}>" + + def __str__(self): + return f"{self.medal} {self.stars}" + + @property + def rank_tier(self) -> int: + return (self.medal.value * 10 + self.stars.value) \ No newline at end of file diff --git a/royalpack/types/dotastars.py b/royalpack/types/dotastars.py new file mode 100644 index 00000000..1693c552 --- /dev/null +++ b/royalpack/types/dotastars.py @@ -0,0 +1,20 @@ +import enum + + +class DotaStars(enum.Enum): + I = 1 + II = 2 + III = 3 + IV = 4 + V = 5 + VI = 6 + VII = 7 + + def __str__(self): + return self.name.upper() + + def __repr__(self): + return f"{self.__class__.__qualname__}.{self.name}" + + def __gt__(self, other): + return self.value > other.value diff --git a/royalpack/utils/leagueleague.py b/royalpack/types/leagueleague.py similarity index 91% rename from royalpack/utils/leagueleague.py rename to royalpack/types/leagueleague.py index 85c82c88..7a965346 100644 --- a/royalpack/utils/leagueleague.py +++ b/royalpack/types/leagueleague.py @@ -150,3 +150,16 @@ class LeagueLeague: fresh_blood=d["freshBlood"], veteran=d["veteran"], ) + + def json(self): + return { + "tier": self.tier.name if self.tier is not None else None, + "rank": self.rank.name if self.tier is not None else None, + "points": self.points, + "wins": self.wins, + "losses": self.losses, + "inactive": self.inactive, + "hot_streak": self.hot_streak, + "fresh_blood": self.fresh_blood, + "veteran": self.veteran, + } diff --git a/royalpack/utils/leaguerank.py b/royalpack/types/leaguerank.py similarity index 100% rename from royalpack/utils/leaguerank.py rename to royalpack/types/leaguerank.py diff --git a/royalpack/utils/leaguetier.py b/royalpack/types/leaguetier.py similarity index 100% rename from royalpack/utils/leaguetier.py rename to royalpack/types/leaguetier.py diff --git a/royalpack/utils/mmchoice.py b/royalpack/types/mmchoice.py similarity index 68% rename from royalpack/utils/mmchoice.py rename to royalpack/types/mmchoice.py index 5f54a507..1a479dde 100644 --- a/royalpack/utils/mmchoice.py +++ b/royalpack/types/mmchoice.py @@ -3,10 +3,8 @@ import enum class MMChoice(enum.Enum): YES = "🔵" - MAYBE = "❔" LATE_SHORT = "🕐" LATE_MEDIUM = "🕒" LATE_LONG = "🕗" - NO_TIME = "🔴" - NO_INTEREST = "❌" - NO_TECH = "❗️" + MAYBE = "❔" + NO = "❌" diff --git a/royalpack/utils/mminterfacedata.py b/royalpack/types/mminterfacedata.py similarity index 100% rename from royalpack/utils/mminterfacedata.py rename to royalpack/types/mminterfacedata.py diff --git a/royalpack/types/oauth_refresh.py b/royalpack/types/oauth_refresh.py new file mode 100644 index 00000000..62e3e6b2 --- /dev/null +++ b/royalpack/types/oauth_refresh.py @@ -0,0 +1,14 @@ +import aiohttp + + +async def oauth_refresh(*, url, client_id, client_secret, redirect_uri, refresh_code): + async with aiohttp.ClientSession() as session: + async with session.post(url, data={ + "client_id": client_id, + "client_secret": client_secret, + "code": refresh_code, + "grant_type": "authorization_code", + "redirect_uri": redirect_uri + }) as response: + j = await response.json() + return j diff --git a/royalpack/types/pollmood.py b/royalpack/types/pollmood.py new file mode 100644 index 00000000..e5bf11d4 --- /dev/null +++ b/royalpack/types/pollmood.py @@ -0,0 +1,7 @@ +import enum + + +class PollMood(enum.Enum): + POSITIVE = 1 + NEUTRAL = 0 + NEGATIVE = -1 diff --git a/royalpack/types/updatable.py b/royalpack/types/updatable.py new file mode 100644 index 00000000..db18377e --- /dev/null +++ b/royalpack/types/updatable.py @@ -0,0 +1,2 @@ +class Updatable: + pass diff --git a/royalpack/utils/__init__.py b/royalpack/utils/__init__.py index 1dc3db68..554230b4 100644 --- a/royalpack/utils/__init__.py +++ b/royalpack/utils/__init__.py @@ -1,16 +1,9 @@ -from .mmchoice import MMChoice -from .mminterfacedata import MMInterfaceData, MMInterfaceDataTelegram -from .leaguetier import LeagueTier -from .leaguerank import LeagueRank -from .leagueleague import LeagueLeague from .royalqueue import RoyalQueue +from .royalpool import RoyalPool +from .mmtask import MMTask __all__ = [ - "MMChoice", - "MMInterfaceData", - "MMInterfaceDataTelegram", - "LeagueTier", - "LeagueRank", - "LeagueLeague", "RoyalQueue", + "RoyalPool", + "MMTask", ] diff --git a/royalpack/utils/mmtask.py b/royalpack/utils/mmtask.py new file mode 100644 index 00000000..0d0f3cbc --- /dev/null +++ b/royalpack/utils/mmtask.py @@ -0,0 +1,456 @@ +import contextlib +import random +from typing import * +import logging +import datetime +import enum +import asyncio as aio + +from telegram import InlineKeyboardMarkup as InKM +from telegram import InlineKeyboardButton as InKB +from telegram import Message as PTBMessage +from telegram import TelegramError + +import royalnet.commands as rc +import royalnet.utils as ru +import royalnet.serf.telegram as rst + +from ..types import MMChoice, MMInterfaceDataTelegram +from ..tables import MMEvent, MMResponse, FiorygiTransaction + + +class Interrupts(enum.Enum): + TIME_RAN_OUT = enum.auto() + MANUAL_START = enum.auto() + MANUAL_DELETE = enum.auto() + + +log = logging.getLogger(__name__) + + +mmchoice_sorting = { + MMChoice.YES: -4, + MMChoice.LATE_SHORT: -3, + MMChoice.LATE_MEDIUM: -2, + MMChoice.LATE_LONG: -1, + MMChoice.MAYBE: 0, + MMChoice.NO: 1 +} + + +class MMTask: + def __init__(self, mmid: int, *, command: rc.Command): + log.debug(f"Creating task for: {mmid}") + + self.loop: aio.AbstractEventLoop = command.loop + self.task: Optional[aio.Task] = None + self.queue: aio.Queue = aio.Queue(loop=self.loop) + self.command: rc.Command = command + self.mmid: int = mmid + + self._session: Optional = None + self._EventT: Optional[Type[MMEvent]] = None + self._ResponseT: Optional[Type[MMResponse]] = None + self._mmevent: Optional[MMEvent] = None + + @property + def is_running(self): + return self.task is not None + + def sync(self): + self._session.refresh(self._mmevent) + + def get_response_line(self, response: MMResponse): + self.sync() + + # noinspection PyListCreation + line = [] + + # Emoji + line.append(f"{response.choice.value}") + + # Mention the user if he said yes, otherwise just display his name + if response.choice == MMChoice.NO: + line.append(f"{response.user.telegram[0].name()}") + else: + line.append(f"{response.user.telegram[0].mention()}") + + # Late time + if response.choice == MMChoice.LATE_SHORT: + td = self._mmevent.datetime + datetime.timedelta(minutes=10) + line.append(f"[{td.strftime('%H:%M')}]") + + elif response.choice == MMChoice.LATE_MEDIUM: + td = self._mmevent.datetime + datetime.timedelta(minutes=30) + line.append(f"[{td.strftime('%H:%M')}]") + + elif response.choice == MMChoice.LATE_LONG: + td = self._mmevent.datetime + datetime.timedelta(minutes=60) + line.append(f"[{td.strftime('%H:%M')}+]") + + # Creator + if response.user == self._mmevent.creator: + line.append("👑") + + # Result + return " ".join(line) + + @property + def channel_text(self) -> str: + self.sync() + + # noinspection PyListCreation + text = [] + + # First line + if self._mmevent.datetime is None: + text.append(f"🌐 [Prossimamente] [b]{self._mmevent.title}[/b]") + else: + text.append(f"🚩 [{self._mmevent.datetime.strftime('%Y-%m-%d %H:%M')}] [b]{self._mmevent.title}[/b]") + + # Description + if self._mmevent.description: + text.append(f"{self._mmevent.description}") + + # Spacer + text.append("") + + # Responses + responses = sorted(self._mmevent.responses, key=lambda r: mmchoice_sorting[r.choice]) + for response in responses: + text.append(self.get_response_line(response)) + + # Result + return "\n".join(text) + + @property + def start_text(self) -> str: + self.sync() + + # noinspection PyListCreation + text = [] + + # First line + if self._mmevent.datetime is None: + text.append(f"🌐 Le iscrizioni all'evento [b]{self._mmevent.title}[/b] sono terminate!") + else: + text.append(f"🚩 L'evento [b]{self._mmevent.title}[/b] è iniziato!") + + # Description + if self._mmevent.description: + text.append(f"{self._mmevent.description}") + + # Spacer + text.append("") + + # Responses + responses = sorted(self._mmevent.responses, key=lambda r: mmchoice_sorting[r.choice]) + for response in responses: + text.append(self.get_response_line(response)) + + # Result + return "\n".join(text) + + @property + def delete_text(self) -> str: + return f"🗑 L'evento [b]{self._mmevent.title}[/b] è stato eliminato." + + def get_answer_callback(self, choice: MMChoice): + async def callback(data: rc.CommandData): + # Find the user who clicked on the button + user = await data.get_author(error_if_none=True) + + # Get the related MMEvent + mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid) + + # Check if the user had already responded + mmresponse: MMResponse = await ru.asyncify( + data.session.query(self._ResponseT).filter_by(user=user, mmevent=mmevent).one_or_none + ) + + if mmresponse is None: + # If they didn't respond, create a new MMResponse + mmresponse = self._ResponseT(user=user, mmevent=mmevent, choice=choice) + data.session.add(mmresponse) + + # Drop fiorygi + if random.randrange(100) < self.command.config["Matchmaking"]["fiorygi_award_chance"]: + await FiorygiTransaction.spawn_fiorygi(data, user, 1, "aver risposto a un matchmaking") + else: + # Change their response + mmresponse.choice = choice + await data.session_commit() + + await self.telegram_channel_message_update() + + await data.reply(f"{choice.value} Hai risposto al matchmaking!") + return callback + + def get_delete_callback(self): + async def callback(data: rc.CommandData): + # Find the user who clicked on the button + user = await data.get_author(error_if_none=True) + + # Get the related MMEvent + mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid) + + # Ensure the user has the required roles to start the matchmaking + if user != mmevent.creator and "admin" not in user.roles: + raise rc.UserError("Non hai i permessi per eliminare questo matchmaking!") + + # Interrupt the matchmaking with the MANUAL_DELETE reason + await self.queue.put(Interrupts.MANUAL_DELETE) + + await data.reply(f"🗑 Evento eliminato!") + return callback + + def get_start_callback(self): + async def callback(data: rc.CommandData): + # Find the user who clicked on the button + user = await data.get_author(error_if_none=True) + + # Get the related MMEvent + mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid) + + # Ensure the user has the required roles to start the matchmaking + if user != mmevent.creator and "admin" not in user.roles: + raise rc.UserError("Non hai i permessi per eliminare questo matchmaking!") + + # Interrupt the matchmaking with the MANUAL_DELETE reason + await self.queue.put(Interrupts.MANUAL_START) + + await data.reply(f"🚩 Evento avviato!") + return callback + + @property + def royalnet_keyboard(self): + # noinspection PyListCreation + rows = [] + + rows.append([ + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.YES.value}", + text="Ci sarò!", + callback=self.get_answer_callback(MMChoice.YES) + ), + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.MAYBE.value}", + text="Forse...", + callback=self.get_answer_callback(MMChoice.MAYBE) + ), + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.NO.value}", + text="Non mi interessa.", + callback=self.get_answer_callback(MMChoice.NO) + ), + ]) + + if self._mmevent.datetime is not None: + rows.append([ + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.LATE_SHORT.value}", + text="10 min", + callback=self.get_answer_callback(MMChoice.LATE_SHORT) + ), + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.LATE_MEDIUM.value}", + text="30 min", + callback=self.get_answer_callback(MMChoice.LATE_MEDIUM) + ), + rc.KeyboardKey( + interface=self.command.interface, + short=f"{MMChoice.LATE_LONG.value}", + text="60 min", + callback=self.get_answer_callback(MMChoice.LATE_LONG) + ) + ]) + + rows.append([ + rc.KeyboardKey( + interface=self.command.interface, + short=f"🗑", + text="Elimina", + callback=self.get_delete_callback() + ), + rc.KeyboardKey( + interface=self.command.interface, + short=f"🚩", + text="Inizia", + callback=self.get_start_callback() + ), + ]) + + return rows + + @property + def telegram_keyboard(self): + # noinspection PyListCreation + rows = [] + key_id = 0 + + for r_row in self.royalnet_keyboard: + row = [] + for r_key in r_row: + # Generate a unique callback string + callback_str = f"mm{self.mmid}_{key_id}" + + # Create a InlineKeyboardButton with that callback string + row.append(InKB(f"{r_key.short} {r_key.text}", callback_data=callback_str)) + + # Increase the key_id + key_id += 1 + rows.append(row) + + # Return the resulting InlineKeyboardMarkup + return InKM(rows) + + def register_telegram_keyboard(self, inkm: InKM): + # noinspection PyListCreation + royalnet_keyboard = self.royalnet_keyboard + for x, row in enumerate(inkm.inline_keyboard): + for y, key in enumerate(row): + key: InKB + self.command.interface.serf.register_keyboard_key(key.callback_data, key=royalnet_keyboard[x][y]) + + def unregister_telegram_keyboard(self, inkm: InKM): + for row in inkm.inline_keyboard: + for key in row: + key: InKB + self.command.interface.serf.unregister_keyboard_key(key.callback_data) + + async def wait_until_due(self): + """When the event is due, interrupt the MMTask with the TIME_RAN_OUT reason.""" + if self._mmevent.datetime is None: + return + await ru.sleep_until(self._mmevent.datetime) + await self.queue.put(Interrupts.TIME_RAN_OUT) + + @property + def telegram_channel_id(self): + return self.command.config["Matchmaking"]["mm_telegram_channel_id"] + + @property + def telegram_group_id(self): + return self.command.config["Matchmaking"]["mm_telegram_group_id"] + + @contextlib.asynccontextmanager + async def telegram_channel_message(self): + + # Generate the InlineKeyboardMarkup + inkm = self.telegram_keyboard + + # Bind the Royalnet buttons to the Telegram keyboard + log.debug(f"Registering keyboard for: {self.mmid}") + self.register_telegram_keyboard(inkm) + + # If the event has no associated interface data... + if self._mmevent.interface_data is None: + # Send the channel message + log.debug(f"Sending message for: {self.mmid}") + message: PTBMessage = await self.command.interface.serf.api_call( + self.command.interface.serf.client.send_message, + chat_id=self.telegram_channel_id, + text=rst.escape(self.channel_text), + parse_mode="HTML", + disable_webpage_preview=True, + reply_markup=inkm + ) + + # Register the interface data on the database + self._mmevent.interface_data = MMInterfaceDataTelegram( + chat_id=self.telegram_channel_id, + message_id=message.message_id + ) + self._session.commit() + + # Wait until the event starts + yield + + # Delete the channel message + log.debug(f"Deleting message for: {self.mmid}") + await self.command.interface.serf.api_call( + self.command.interface.serf.client.delete_message, + chat_id=self._mmevent.interface_data.chat_id, + message_id=self._mmevent.interface_data.message_id + ) + + # Unregister the Telegram keyboard bindings + log.debug(f"Unregistering keyboard for: {self.mmid}") + self.unregister_telegram_keyboard(inkm) + + async def telegram_channel_message_update(self): + log.debug(f"Updating message for: {self.mmid}") + try: + await ru.asyncify( + self.command.interface.serf.client.edit_message_text, + chat_id=self._mmevent.interface_data.chat_id, + text=rst.escape(self.channel_text), + message_id=self._mmevent.interface_data.message_id, + parse_mode="HTML", + disable_web_page_preview=True, + reply_markup=self.telegram_keyboard + ) + except TelegramError as e: + log.warning(f"TelegramError during update: {e}") + + async def telegram_group_message_start(self): + await self.command.interface.serf.api_call( + self.command.interface.serf.client.send_message, + chat_id=self.telegram_group_id, + text=rst.escape(self.start_text), + parse_mode="HTML", + disable_webpage_preview=True + ) + + async def telegram_group_message_delete(self): + await self.command.interface.serf.api_call( + self.command.interface.serf.client.send_message, + chat_id=self.telegram_group_id, + text=rst.escape(self.delete_text), + parse_mode="HTML", + disable_webpage_preview=True + ) + + def start(self): + log.debug(f"Starting task for: {self.mmid}") + self.task = self.loop.create_task(self.run()) + + @ru.sentry_async_wrap() + async def run(self): + log.debug(f"Running task for: {self.mmid}") + + # Create a new session for the MMTask + self._session = self.command.alchemy.Session() + self._EventT = self.command.alchemy.get(MMEvent) + self._ResponseT = self.command.alchemy.get(MMResponse) + self._mmevent: MMEvent = self._session.query(self._EventT).get(self.mmid) + + if self._mmevent is None: + raise rc.InvalidInputError(f"No event exists with the mmid {self.mmid}.") + + if self._mmevent.interface != "telegram": + raise rc.UnsupportedError("Currently only the Telegram interface is supported.") + + async with self.telegram_channel_message(): + self.loop.create_task(self.wait_until_due()) + + # Sleep until something interrupts the task + interrupt = await self.queue.get() + + # Mark the event as interrupted + self._mmevent.interrupted = True + self._session.commit() + + # Send a group notification if the MMEvent wasn't deleted + if interrupt != Interrupts.MANUAL_DELETE: + await self.telegram_group_message_start() + else: + await self.telegram_group_message_delete() + + # Close the database session + await ru.asyncify(self._session.close) \ No newline at end of file diff --git a/royalpack/utils/royalpool.py b/royalpack/utils/royalpool.py new file mode 100644 index 00000000..40301e4c --- /dev/null +++ b/royalpack/utils/royalpool.py @@ -0,0 +1,54 @@ +import logging +from typing import Optional, List, AsyncGenerator, Tuple, Any, Dict +from royalnet.bard.discord import YtdlDiscord +from royalnet.serf.discord import Playable +import discord +import random + + +log = logging.getLogger(__name__) + + +class RoyalPool(Playable): + """A pool of :class:`YtdlDiscord` that will be played in a loop.""" + def __init__(self, start_with: Optional[List[YtdlDiscord]] = None): + super().__init__() + self.full_pool: List[YtdlDiscord] = [] + self.remaining_pool: List[YtdlDiscord] = [] + self.now_playing: Optional[YtdlDiscord] = None + if start_with is not None: + self.full_pool = [*self.full_pool, *start_with] + log.debug(f"Created new {self.__class__.__qualname__} containing: {self.full_pool}") + + async def _generator(self) \ + -> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]: + yield + while True: + if len(self.remaining_pool) == 0: + if len(self.full_pool) == 0: + log.debug(f"Nothing in the pool, yielding None...") + yield None + continue + else: + self.remaining_pool = self.full_pool.copy() + random.shuffle(self.remaining_pool) + log.debug(f"Dequeuing an item...") + # Get the first YtdlDiscord of the queue + self.now_playing: YtdlDiscord = self.remaining_pool.pop(0) + log.debug(f"Yielding FileAudioSource from: {self.now_playing}") + # Create a FileAudioSource from the YtdlDiscord + # If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding + async with self.now_playing.spawn_audiosource() as fas: + # Yield the resulting AudioSource + yield fas + + async def destroy(self): + for file in self.full_pool: + log.debug(f"Deleting: {file}") + await file.delete_asap() + log.debug(f"Deleting: {file.ytdl_file}") + await file.ytdl_file.delete_asap() + log.debug(f"Deleted successfully!") + self.full_pool = [] + self.remaining_pool = [] + self.now_playing = None diff --git a/royalpack/utils/royalqueue.py b/royalpack/utils/royalqueue.py index a70aa04a..a8af9110 100644 --- a/royalpack/utils/royalqueue.py +++ b/royalpack/utils/royalqueue.py @@ -1,6 +1,6 @@ import logging from typing import Optional, List, AsyncGenerator, Tuple, Any, Dict -from royalnet.bard import YtdlDiscord +from royalnet.bard.discord import YtdlDiscord from royalnet.serf.discord import Playable import discord @@ -43,3 +43,20 @@ class RoyalQueue(Playable): log.debug(f"Deleting: {self.now_playing.ytdl_file}") await self.now_playing.ytdl_file.delete_asap() log.debug(f"Deleted successfully!") + self.now_playing = None + + async def destroy(self): + if self.now_playing is not None: + log.debug(f"Deleting: {self.now_playing}") + await self.now_playing.delete_asap() + log.debug(f"Deleting: {self.now_playing.ytdl_file}") + await self.now_playing.ytdl_file.delete_asap() + log.debug(f"Deleted successfully!") + self.now_playing = None + for file in self.contents: + log.debug(f"Deleting: {file}") + await file.delete_asap() + log.debug(f"Deleting: {file.ytdl_file}") + await file.ytdl_file.delete_asap() + log.debug(f"Deleted successfully!") + self.contents = [] diff --git a/royalpack/version.py b/royalpack/version.py deleted file mode 100644 index a0d642bb..00000000 --- a/royalpack/version.py +++ /dev/null @@ -1 +0,0 @@ -semantic = "5.1.9"