From 59ec81a807a5986bbf09fb7171143ea752ed23c1 Mon Sep 17 00:00:00 2001 From: Myles Gray Date: Sun, 30 Oct 2022 11:20:46 +0000 Subject: [PATCH 1/5] Initial debugging of server querying functionality --- .gitignore | 3 +- Pipfile | 4 +- Pipfile.lock | 82 +++++++++++++++++++++++--- app/bot.py | 8 +-- app/gamequery.py | 91 +++++++++++++++++++++++++++++ app/network.py | 71 +++++++++++++++++++++++ app/servers.py | 146 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 app/gamequery.py create mode 100644 app/network.py create mode 100644 app/servers.py diff --git a/.gitignore b/.gitignore index dc7b0ba..243e5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc .env __pycache__/ -.vscode/ \ No newline at end of file +.vscode/ +.mypy_cache/ \ No newline at end of file diff --git a/Pipfile b/Pipfile index 028616b..f945d74 100644 --- a/Pipfile +++ b/Pipfile @@ -6,10 +6,12 @@ name = "pypi" [packages] requests = "==2.28.1" py-cord = "==2.2.2" +steamquery = "==1.0.2" [dev-packages] pylint = "*" autopep8 = "*" +mypy = "*" [requires] -python_version = "3.8" +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index db9851c..0adf2d3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "b15e2973f84e61990d7951a4d8e597d50c5312545a76ab329e6f296ba49d7a06" + "sha256": "c7f993f32abe98a5be1560aeaa22b95557e6301efb7a8c00008fc7df533bc1a8" }, "pipfile-spec": 6, "requires": { - "python_version": "3.8" + "python_version": "3.11" }, "sources": [ { @@ -149,6 +149,13 @@ "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, + "deprecation": { + "hashes": [ + "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", + "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a" + ], + "version": "==2.1.0" + }, "frozenlist": { "hashes": [ "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", @@ -287,6 +294,14 @@ "markers": "python_version >= '3.7'", "version": "==6.0.2" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, "py-cord": { "hashes": [ "sha256:6e8dbdd78c26040081240c6f342687289c7b5287652c452a41f17ca5beaa3d06", @@ -295,6 +310,14 @@ "index": "pypi", "version": "==2.2.2" }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, "requests": { "hashes": [ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", @@ -303,6 +326,14 @@ "index": "pypi", "version": "==2.28.1" }, + "steamquery": { + "hashes": [ + "sha256:075902080d60dea2533943d2e7d7e851ee0a9fe8db1e4dcf4f5b45cd32e4aa19", + "sha256:7b9ddc7d3f0952823a4654c541f58166484adcd6df5f2d5f16708ae8d6957cd7" + ], + "index": "pypi", + "version": "==1.0.2" + }, "urllib3": { "hashes": [ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", @@ -388,11 +419,11 @@ }, "autopep8": { "hashes": [ - "sha256:99afd5a60d30c6d426319e15db2ed7833d2da38f7f94a8753a1953509cc78e5d", - "sha256:f0058220e4cc0ef6121996fc8ec1c32f0735e446be23c4e1f692de0bf23174dd" + "sha256:8b1659c7f003e693199f52caffdc06585bb0716900bbc6a7442fd931d658c077", + "sha256:ad924b42c2e27a1ac58e432166cc4588f5b80747de02d0d35b1ecbd3e7d57207" ], "index": "pypi", - "version": "==1.7.1" + "version": "==2.0.0" }, "dill": { "hashes": [ @@ -443,6 +474,43 @@ "markers": "python_version >= '3.6'", "version": "==0.7.0" }, + "mypy": { + "hashes": [ + "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d", + "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24", + "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046", + "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e", + "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3", + "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5", + "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20", + "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda", + "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1", + "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146", + "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206", + "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746", + "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6", + "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e", + "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc", + "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a", + "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8", + "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763", + "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2", + "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947", + "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40", + "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b", + "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795", + "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c" + ], + "index": "pypi", + "version": "==0.982" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "platformdirs": { "hashes": [ "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", @@ -488,7 +556,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "wrapt": { @@ -558,7 +626,7 @@ "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.11'", "version": "==1.14.1" } } diff --git a/app/bot.py b/app/bot.py index 4c6a91e..6b8238c 100644 --- a/app/bot.py +++ b/app/bot.py @@ -60,7 +60,7 @@ async def on_ready(): print('Connected to API') -@ bot.slash_command(name="boot", description="Boots the game server") +@bot.slash_command(name="boot", description="Boots the game server") async def _boot(ctx): try: response = requests.get(WOL_URL, timeout=2) @@ -75,7 +75,7 @@ async def _boot(ctx): traceback.print_exc() -@ bot.slash_command(name="shutdown", description="Shuts down the game server") +@bot.slash_command(name="shutdown", description="Shuts down the game server") async def _shutdown(ctx): try: response = requests.get(SHUTDOWN_URL, timeout=2) @@ -89,7 +89,7 @@ async def _shutdown(ctx): traceback.print_exc() -@ bot.slash_command(name="reboot", description="Reboots the game server") +@bot.slash_command(name="reboot", description="Reboots the game server") async def _reboot(ctx): try: response = requests.get(REBOOT_URL, timeout=2) @@ -103,7 +103,7 @@ async def _reboot(ctx): traceback.print_exc() -@ bot.slash_command(name="status", description="Checks current power status of game server") +@bot.slash_command(name="status", description="Checks current power status of game server") async def _status(ctx): try: response = requests.get(LIVENESS_URL, timeout=2) diff --git a/app/gamequery.py b/app/gamequery.py new file mode 100644 index 0000000..929ec67 --- /dev/null +++ b/app/gamequery.py @@ -0,0 +1,91 @@ +""" +Queries SteamQuery, DCS and SpaceEngineers instances +""" +import traceback +from steam import SteamQuery +import network +from server import Server + + +def get_players(ipaddress: int, port: int, password="") -> dict: + """ + Returns a dict with the current number of players connected + to the server as well as the max players supported + """ + try: + server = _steam_server_connection(server_ip=ipaddress, port=port) + server_state = _lint_steamquery_output(server.query_server_info()) + return {"current_players": server_state["players"], + "max_players": server_state["max_players"]} + + except Exception: + print("Could not get server info") + traceback.print_exc() + raise + + +def get_players_details(ipaddress: int, port: int, password="") -> list: + """ + Returns a list with all current player objects containing + names, scores and durations on the server + """ + try: + server = _steam_server_connection(server_ip=ipaddress, port=port) + player_info = _lint_steamquery_output(server.query_player_info()) + return player_info + + except Exception: + print("Could not get player info") + traceback.print_exc() + raise + +# Creates and returns server connection object + + +def _steam_server_connection(server_ip: int, port: int) -> object: + """ + Creates a steam query server connection object and passes it back. + """ + try: + # Check if IP address is valid + if not network.valid_ip_address(server_ip): + raise ValueError("IP Address Invalid") + + # Check if port is valid + if not network.valid_port(port): + raise ValueError("PORT environment variable is invalid") + + # Construct SteamQuery session + print(f'Connecting to {server_ip}:{port}') + server = SteamQuery(server_ip, port) + return server + + except Exception: + print("Unable to connect to server") + traceback.print_exc() + raise + + +def _lint_steamquery_output(query) -> object: + """ + Checks if SteamQuery output should have been an exception + and if so raises one, kill me + """ + # SteamQuery lib returns errors as strings, so need to + # check if "error" key is present to detect exceptions + # when errored, it is always passed back as a dict + # + # If the query is a list, then it is a valid response + # in any case + if isinstance(query, list): + return query + else: + try: + if "error" in query.keys(): + raise ConnectionError(str(query)) + else: + return query + except Exception: + print("Error passed back from SteamQuery") + traceback.print_exc() + raise diff --git a/app/network.py b/app/network.py new file mode 100644 index 0000000..8d1a8ef --- /dev/null +++ b/app/network.py @@ -0,0 +1,71 @@ +""" +Provides functions for establishing network locations and +communications, port and IP verification +""" +import traceback +from ipaddress import ip_address, IPv6Address, IPv4Address + +import requests + +# Gets the external IP address where the server is running +# this assumes that the outbound IP after NAT and inbound IP +# before NAT are the same IP address. +# +# Unknown how this will behave in IPv6 environments, but the +# assumption is that it will work just the same as no NAT is +# used in IPv6 and the external IPv6 address will be the +# global address of the machine + + +def get_external_ip() -> str: + """ + Gets the current external IP of where the app is running. + This uses ifconfig.me and assumes it is not blocked or down. + """ + try: + response = requests.get('https://ifconfig.me/ip', timeout=5) + server_ip = response.content.decode() + print(f'Discovered IP address is {server_ip}') + return str(server_ip) + + except Exception: + print("External IP could not be found, ifconfig.me may be down or blocked") + traceback.print_exc() + raise + +# Validates if the IP address given is valid + + +def valid_ip_address(ipaddress: int) -> int: + """ + Checks if the IP address passed is a Valid IPv4 or IPv6 address + """ + try: + if isinstance(ip_address(ipaddress), (IPv4Address, IPv6Address)): + return True + else: + return False + + except ValueError: + print("IP address is invalid") + traceback.print_exc() + raise + +# Validates if the given port is in valid range + + +def valid_port(port: int) -> bool: + """ + Checks if a given port is in the valid list of ranges for UDP ports + """ + try: + port = int(port) + if port > 0 and port <= 65535: + print(f'PORT {port} is valid') + return True + raise ValueError(f'PORT {port} is not in valid range 1-65535') + + except Exception: + print("PORT is not valid") + traceback.print_exc() + raise diff --git a/app/servers.py b/app/servers.py new file mode 100644 index 0000000..d53a2e5 --- /dev/null +++ b/app/servers.py @@ -0,0 +1,146 @@ +""" +Provides Server data class type for storing game +server config info +""" +import traceback +import json +from dataclasses import dataclass +from enum import Enum + + +class ServerType(Enum): + """ + Enum for storing supported backend server types + """ + STEAM = 1 + DCS = 2 + SPACE_ENGINEERS = 3 + + +@dataclass +class Server: + """ + Object that stores a given game server + configuration + """ + name: str + ip_address: str + port: int + password: str + server_type: ServerType + + +server_list = {} + + +def add_server(name, ip_address, port, server_type, password=""): + """ + Adds a server to the actively monitored list + """ + try: + if not name in server_list: + if server_type in ServerType.__members__: + server = Server(name=name, ip_address=ip_address, + port=port, password=password, server_type=ServerType[server_type]) + server_list[name] = server + return server + raise TypeError(f"Server type: {server_type} is invalid") + raise ValueError("Server name already taken") + except Exception: + print("Failed to add server") + traceback.print_exc() + raise + + +def delete_server(name): + """ + Deletes a server from the actively monitored list + """ + server_list.pop(name) + + +def update_server(name, server): + """ + Updates a server entry in the bot's monitoring list + Side note: this is essentially the same op as add_server + but without the duplicate key check to allow for overwrites + """ + server_list[name] = server + + +def get_server(name): + """ + Returns server object based on server name + """ + return server_list.get(name) + + +def list_servers(): + """ + Lists servers currently monitored by the bot + """ + print(server_list) + + +def save_servers(): + """ + Saves out server config to disk as json + """ + server_serialised = [] + for server in server_list.values(): + print(server.server_type.name) + print(server.__dict__) + server_serialised.append(server.__dict__) + _save_settings(server_serialised) + + +def load_servers(): + """ + Loads servers from disk, overwrites any existing entries of the same name + """ + servers = _load_settings() + try: + for server in servers: + if not server['server_type'] in ServerType.__members__: + raise TypeError( + f"Server type: '{server['server_type']}' \ + for server '{server['name']}' is invalid") + server_obj = Server(name=server['name'], ip_address=server['ip_address'], + port=server['port'], password=server['password'], + server_type=ServerType[server['server_type']]) + update_server(server['name'], server_obj) + except Exception: + print("Couldn't load settings into bot") + traceback.print_exc() + raise + + +def _save_settings(jsonstring): + """ + Writes out server config settings to disk in json + """ + try: + with open('servers.json', 'w', encoding='utf8') as configfile: + json.dump(jsonstring, configfile, indent=4, sort_keys=True) + except Exception: + print("Failed to save settings to disk") + traceback.print_exc() + return 0 + return 1 + + +def _load_settings(): + """ + Writes out server config settings to disk in json + """ + try: + with open('servers.json', 'r', encoding='utf8') as configfile: + jsonstring = json.load(configfile) + return jsonstring + except FileNotFoundError: + print("Settings file not found") + raise + except Exception: + print("Failed to load settings from disk") + traceback.print_exc() + raise From 1243550ab01685ed12ea5ad9d6e9a270ef714811 Mon Sep 17 00:00:00 2001 From: "Ryan J. Gray" Date: Sun, 30 Oct 2022 17:38:31 +0000 Subject: [PATCH 2/5] Tweaking server JSON serialisation Allows server type to be serialised to an int, and read back in. --- app/servers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/servers.py b/app/servers.py index d53a2e5..8861aee 100644 --- a/app/servers.py +++ b/app/servers.py @@ -5,10 +5,10 @@ import traceback import json from dataclasses import dataclass -from enum import Enum +from enum import IntEnum -class ServerType(Enum): +class ServerType(IntEnum): """ Enum for storing supported backend server types """ @@ -44,7 +44,7 @@ def add_server(name, ip_address, port, server_type, password=""): port=port, password=password, server_type=ServerType[server_type]) server_list[name] = server return server - raise TypeError(f"Server type: {server_type} is invalid") + raise TypeError(f"Server type '{server_type}' is invalid") raise ValueError("Server name already taken") except Exception: print("Failed to add server") @@ -101,13 +101,13 @@ def load_servers(): servers = _load_settings() try: for server in servers: - if not server['server_type'] in ServerType.__members__: - raise TypeError( - f"Server type: '{server['server_type']}' \ - for server '{server['name']}' is invalid") + #if not server['server_type'] in ServerType: + # raise TypeError( + # f"Server type: '{server['server_type']}' \ + # for server '{server['name']}' is invalid") server_obj = Server(name=server['name'], ip_address=server['ip_address'], port=server['port'], password=server['password'], - server_type=ServerType[server['server_type']]) + server_type=ServerType(server['server_type'])) update_server(server['name'], server_obj) except Exception: print("Couldn't load settings into bot") From fb63e74bfbd40cf83515afbd63f0af9eeefeadda Mon Sep 17 00:00:00 2001 From: Myles Gray Date: Mon, 31 Oct 2022 19:51:00 +0000 Subject: [PATCH 3/5] Using a string, Enum class to allow for easier data serialisation --- app/servers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/servers.py b/app/servers.py index 8861aee..dfe6282 100644 --- a/app/servers.py +++ b/app/servers.py @@ -5,16 +5,16 @@ import traceback import json from dataclasses import dataclass -from enum import IntEnum +from enum import Enum -class ServerType(IntEnum): +class ServerType(str, Enum): """ Enum for storing supported backend server types """ - STEAM = 1 - DCS = 2 - SPACE_ENGINEERS = 3 + STEAM = 'STEAM' + DCS = 'DCS' + SPACE_ENGINEERS = 'SPACE_ENGINNERS' @dataclass @@ -88,7 +88,7 @@ def save_servers(): """ server_serialised = [] for server in server_list.values(): - print(server.server_type.name) + print(server.server_type.value) print(server.__dict__) server_serialised.append(server.__dict__) _save_settings(server_serialised) @@ -101,10 +101,10 @@ def load_servers(): servers = _load_settings() try: for server in servers: - #if not server['server_type'] in ServerType: - # raise TypeError( - # f"Server type: '{server['server_type']}' \ - # for server '{server['name']}' is invalid") + if not server['server_type'] in ServerType.__members__: + raise TypeError( + f"Server type: '{server['server_type']}' \ + for server '{server['name']}' is invalid") server_obj = Server(name=server['name'], ip_address=server['ip_address'], port=server['port'], password=server['password'], server_type=ServerType(server['server_type'])) From bd4a78856a725035123a370c1c4a34a7b9c75ed5 Mon Sep 17 00:00:00 2001 From: Myles Gray Date: Thu, 3 Nov 2022 22:21:15 +0000 Subject: [PATCH 4/5] First pass at testing --- .gitignore | 5 +- Pipfile | 2 + Pipfile.lock | 113 +++++++++++++++++++++++++++- app/gamequery.py | 2 +- app/servers.py | 38 ++++++---- tests/__init__.py | 0 tests/test_servers.py | 171 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_servers.py diff --git a/.gitignore b/.gitignore index 243e5a4..6fcc0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ .env __pycache__/ .vscode/ -.mypy_cache/ \ No newline at end of file +.mypy_cache/ +.pytest_cache/ +htmlcov/ +.coverage \ No newline at end of file diff --git a/Pipfile b/Pipfile index f945d74..3c00156 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,8 @@ steamquery = "==1.0.2" pylint = "*" autopep8 = "*" mypy = "*" +coverage = "*" +pytest-cov = "*" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 0adf2d3..0255fa5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c7f993f32abe98a5be1560aeaa22b95557e6301efb7a8c00008fc7df533bc1a8" + "sha256": "4ca337157b8d0f90b47d13631aea3764fe972607558db7d05e70db1af6cab65b" }, "pipfile-spec": 6, "requires": { @@ -417,6 +417,14 @@ "markers": "python_full_version >= '3.7.2'", "version": "==2.12.12" }, + "attrs": { + "hashes": [ + "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", + "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + ], + "markers": "python_version >= '3.5'", + "version": "==22.1.0" + }, "autopep8": { "hashes": [ "sha256:8b1659c7f003e693199f52caffdc06585bb0716900bbc6a7442fd931d658c077", @@ -425,6 +433,62 @@ "index": "pypi", "version": "==2.0.0" }, + "coverage": { + "hashes": [ + "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", + "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", + "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", + "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", + "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", + "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", + "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", + "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", + "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", + "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", + "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", + "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", + "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", + "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", + "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", + "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", + "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", + "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", + "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", + "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", + "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", + "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", + "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", + "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", + "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", + "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", + "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", + "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", + "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", + "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", + "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", + "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", + "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", + "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", + "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", + "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", + "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", + "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", + "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", + "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", + "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", + "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", + "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", + "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", + "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", + "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", + "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", + "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", + "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", + "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + ], + "index": "pypi", + "version": "==6.5.0" + }, "dill": { "hashes": [ "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", @@ -433,6 +497,13 @@ "markers": "python_version >= '3.7'", "version": "==0.3.6" }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, "isort": { "hashes": [ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", @@ -511,6 +582,14 @@ ], "version": "==0.4.3" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, "platformdirs": { "hashes": [ "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", @@ -519,6 +598,14 @@ "markers": "python_version >= '3.7'", "version": "==2.5.2" }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, "pycodestyle": { "hashes": [ "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", @@ -535,6 +622,30 @@ "index": "pypi", "version": "==2.15.5" }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, + "pytest": { + "hashes": [ + "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", + "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" + ], + "markers": "python_version >= '3.7'", + "version": "==7.2.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", + "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" + ], + "index": "pypi", + "version": "==4.0.0" + }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", diff --git a/app/gamequery.py b/app/gamequery.py index 929ec67..0c19987 100644 --- a/app/gamequery.py +++ b/app/gamequery.py @@ -4,7 +4,7 @@ import traceback from steam import SteamQuery import network -from server import Server +from servers import Server def get_players(ipaddress: int, port: int, password="") -> dict: diff --git a/app/servers.py b/app/servers.py index dfe6282..78f7676 100644 --- a/app/servers.py +++ b/app/servers.py @@ -4,9 +4,12 @@ """ import traceback import json +import logging from dataclasses import dataclass from enum import Enum +logging.basicConfig(level=logging.WARN) + class ServerType(str, Enum): """ @@ -14,7 +17,7 @@ class ServerType(str, Enum): """ STEAM = 'STEAM' DCS = 'DCS' - SPACE_ENGINEERS = 'SPACE_ENGINNERS' + SPACE_ENGINEERS = 'SPACE_ENGINEERS' @dataclass @@ -26,8 +29,8 @@ class Server: name: str ip_address: str port: int - password: str server_type: ServerType + password: str server_list = {} @@ -41,13 +44,13 @@ def add_server(name, ip_address, port, server_type, password=""): if not name in server_list: if server_type in ServerType.__members__: server = Server(name=name, ip_address=ip_address, - port=port, password=password, server_type=ServerType[server_type]) + port=port, server_type=ServerType[server_type], password=password) server_list[name] = server return server raise TypeError(f"Server type '{server_type}' is invalid") raise ValueError("Server name already taken") except Exception: - print("Failed to add server") + logging.error("Failed to add server") traceback.print_exc() raise @@ -56,7 +59,12 @@ def delete_server(name): """ Deletes a server from the actively monitored list """ - server_list.pop(name) + try: + return server_list.pop(name) + except KeyError: + logging.error(f"Failed to delete server, '{name}' does not exist.") + traceback.print_exc() + raise def update_server(name, server): @@ -65,7 +73,13 @@ def update_server(name, server): Side note: this is essentially the same op as add_server but without the duplicate key check to allow for overwrites """ - server_list[name] = server + try: + server_list[name] = server + return True + except Exception: + logging.error("Failed to update server") + traceback.print_exc() + raise def get_server(name): @@ -79,7 +93,7 @@ def list_servers(): """ Lists servers currently monitored by the bot """ - print(server_list) + return server_list def save_servers(): @@ -88,8 +102,6 @@ def save_servers(): """ server_serialised = [] for server in server_list.values(): - print(server.server_type.value) - print(server.__dict__) server_serialised.append(server.__dict__) _save_settings(server_serialised) @@ -110,7 +122,7 @@ def load_servers(): server_type=ServerType(server['server_type'])) update_server(server['name'], server_obj) except Exception: - print("Couldn't load settings into bot") + logging.error("Couldn't load settings into bot") traceback.print_exc() raise @@ -123,7 +135,7 @@ def _save_settings(jsonstring): with open('servers.json', 'w', encoding='utf8') as configfile: json.dump(jsonstring, configfile, indent=4, sort_keys=True) except Exception: - print("Failed to save settings to disk") + logging.error("Failed to save settings to disk") traceback.print_exc() return 0 return 1 @@ -138,9 +150,9 @@ def _load_settings(): jsonstring = json.load(configfile) return jsonstring except FileNotFoundError: - print("Settings file not found") + logging.error("Settings file not found") raise except Exception: - print("Failed to load settings from disk") + logging.error("Failed to load settings from disk") traceback.print_exc() raise diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_servers.py b/tests/test_servers.py new file mode 100644 index 0000000..22940fe --- /dev/null +++ b/tests/test_servers.py @@ -0,0 +1,171 @@ +""" +Tests for server module +""" +import unittest +from app import servers + + +class ListServersTests(unittest.TestCase): + """ + Class for list_servers tests + """ + + # def test_list_servers(self): + # """ + # Tests if a dictionary of servers is returned when added + # """ + # expected_result = {} + + # servers.add_server( + # 'DCS Server 2', '10.56.0.175', 10309, 'DCS') + # servers.add_server( + # 'Arma Stasi Altis', '10.56.0.180', 2303, 'STEAM') + # servers.add_server( + # 'Sprace Engineers URBW', '10.56.0.175', 27019, 'SPACE_ENGINEERS') + + # self.assertEqual(servers.list_servers(), expected_result) + + +class AddServersTests(unittest.TestCase): + """ + Class for add_servers tests + """ + + def test_add_servers_positive(self): + """ + Tests if Server objects are created with valid values + """ + dcs_server = servers.add_server( + 'DCS Server 1', '10.56.0.175', 10309, 'DCS') + arma_server = servers.add_server( + 'Arma Server 1', '172.16.69.180', 2303, 'STEAM') + se_server = servers.add_server( + 'Space Engineers Server 1', '192.168.1.179', 27019, 'SPACE_ENGINEERS') + + self.assertIsInstance(dcs_server, servers.Server) + self.assertEqual(dcs_server.name, 'DCS Server 1') + self.assertEqual(dcs_server.ip_address, '10.56.0.175') + self.assertEqual(dcs_server.port, 10309) + self.assertEqual(dcs_server.server_type, servers.ServerType.DCS) + self.assertIsInstance(arma_server, servers.Server) + self.assertEqual(arma_server.name, 'Arma Server 1') + self.assertEqual(arma_server.ip_address, '172.16.69.180') + self.assertEqual(arma_server.port, 2303) + self.assertEqual(arma_server.server_type, servers.ServerType.STEAM) + self.assertIsInstance(se_server, servers.Server) + self.assertEqual(se_server.name, 'Space Engineers Server 1') + self.assertEqual(se_server.ip_address, '192.168.1.179') + self.assertEqual(se_server.port, 27019) + self.assertEqual(se_server.server_type, + servers.ServerType.SPACE_ENGINEERS) + + def test_add_servers_invalid_server_type(self): + """ + Tests if a TypeError is thrown if an invalid type is specified + """ + + self.assertRaises(TypeError, servers.add_server, + 'Test', '10.0.0.1', 1000, 'FAIL') + + def test_add_servers_duplicate_name(self): + """ + Tests if a ValueError is thrown if a duplicate server name is specified + """ + servers.add_server('Test1', '10.0.0.1', 1000, 'DCS') + + self.assertRaises(ValueError, servers.add_server, + 'Test1', '10.0.0.1', 1000, 'DCS') + + def test_add_servers_invalid_port(self): + """ + Tests if a ValueError is thrown if a port outside valid 1-65535 range is set + """ + + self.assertRaises(ValueError, servers.add_server, + 'Test2', '10.0.0.1', 65536, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test3', '10.0.0.1', -1, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test4', '10.0.0.1', 0, 'DCS') + + def test_add_servers_invalid_ipv4(self): + """ + Tests if a ValueError is thrown if an invalid IPv4 address is supplied + """ + + self.assertRaises(ValueError, servers.add_server, + 'Test5', '12345', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test6', '256.256.256.256', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test7', '1.2.3.4.5', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test8', 'string', 1000, 'DCS') + + def test_add_servers_invalid_ipv6(self): + """ + Tests if a ValueError is thrown if an invalid IPv6 address is supplied + """ + + self.assertRaises(ValueError, servers.add_server, + 'Test9', '12345', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test10', '256.256.256.256', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test11', '1.2.3.4.5', 1000, 'DCS') + self.assertRaises(ValueError, servers.add_server, + 'Test12', 'string', 1000, 'DCS') + + +class DeleteServersTests(unittest.TestCase): + """ + Class for add_servers tests + """ + + def test_delete_servers_positive(self): + """ + Tests if Server is successfully deleted + """ + + servers.add_server('Delete me', '10.56.0.175', 10309, 'DCS') + + self.assertTrue(servers.delete_server('Delete me')) + + def test_delete_servers_raise_key_error(self): + """ + Tests if KeyError is raised if server doesn't exist + """ + + with self.assertRaises(KeyError): + servers.delete_server('I dont exist') + + +class UpdateServersTests(unittest.TestCase): + """ + Class for add_servers tests + """ + + def test_update_servers_positive(self): + """ + Tests if Server is successfully updated + """ + + servers.add_server('Update me', '10.0.0.1', 1000, 'DCS') + update = servers.Server('Update me', '1.1.1.1', + 1000, 'password', servers.ServerType.DCS) + + self.assertTrue(servers.update_server('Update me', update)) + + def test_update_servers_invalid_type(self): + """ + Tests if Server is successfully updated + """ + + update = servers.Server('Update me fail', '1.1.1.1', + 1000, 'password', servers.ServerType.DCS) + + self.assertFalse(servers.update_server('Update me fail', update)) + + +if __name__ == '__main__': + unittest.main() From c49560463cbd61b8bdb564d8b82550006c6945d3 Mon Sep 17 00:00:00 2001 From: Myles Gray Date: Mon, 30 Jan 2023 20:39:18 +0000 Subject: [PATCH 5/5] Resolve merge conflicts --- .env.example | 4 +- .github/workflows/1password.yaml | 21 + .github/workflows/codeql.yml | 74 +-- .github/workflows/lint-and-test.yaml | 53 ++ .github/workflows/release.yaml | 11 +- Dockerfile | 2 +- Pipfile | 9 +- Pipfile.lock | 804 +++++++++++++++------------ app/bot.py | 185 ++++-- app/gamequery.py | 87 ++- app/servers.py | 66 ++- docker-compose.yml | 4 +- readme.md | 6 +- tests/test_servers.py | 114 ++-- 14 files changed, 915 insertions(+), 525 deletions(-) create mode 100644 .github/workflows/1password.yaml create mode 100644 .github/workflows/lint-and-test.yaml diff --git a/.env.example b/.env.example index 2b80291..9fb3e7c 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,6 @@ DISCORD_TOKEN= WOL_URL= SHUTDOWN_URL= REBOOT_URL= -LIVENESS_URL= \ No newline at end of file +LIVENESS_URL= +COOLDOWN=300 +POWERBOT_ROLE=@everyone,7162737183646261597,groupname \ No newline at end of file diff --git a/.github/workflows/1password.yaml b/.github/workflows/1password.yaml new file mode 100644 index 0000000..205db68 --- /dev/null +++ b/.github/workflows/1password.yaml @@ -0,0 +1,21 @@ +name: 1Password Secrets Test + +on: + workflow_dispatch: {} + +env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + +jobs: + ship: + runs-on: ubuntu-latest + steps: + - name: Install 1Password CLI + run: | + wget https://cache.agilebits.com/dist/1P/op2/pkg/v2.8.0-beta.05/op_linux_amd64_v2.8.0-beta.05.zip -O op.zip && \ + unzip -d op op.zip && \ + sudo mv op/op /usr/local/bin && \ + rm -r op.zip op + + - name: Read Field from 1Password + run: op read "op://Kharms Dev/Test Secret/password" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d229260..6299abc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,13 +12,14 @@ name: "CodeQL" on: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '24 12 * * 1' + workflow_dispatch: + # push: + # branches: [ "master" ] + # pull_request: + # # The branches below must be a subset of the branches above + # branches: [ "master" ] + # schedule: + # - cron: '24 12 * * 1' jobs: analyze: @@ -32,43 +33,42 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lint-and-test.yaml b/.github/workflows/lint-and-test.yaml new file mode 100644 index 0000000..cf1887e --- /dev/null +++ b/.github/workflows/lint-and-test.yaml @@ -0,0 +1,53 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Test + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + workflow_dispatch: + workflow_run: + workflows: + - CodeQL + types: + - completed + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + fail-fast: false + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "pipenv" + - name: Install pipenv + run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python + - name: Install dependencies + if: steps.cache-pipenv.outputs.cache-hit != 'true' + run: | + pipenv install --deploy --dev + - name: Run autopep8 + id: autopep8 + run: | + pipenv run autopep8 --recursive --in-place --aggressive --aggressive . + - name: Fail if autopep8 made changes + if: steps.autopep8.outputs.exit-code == 2 + run: exit 1 + - name: Test with pytest + run: | + pipenv run test -v diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5d177fe..1797334 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,13 +1,18 @@ name: Release on: - workflow_dispatch: - push: - branches: [master] + workflow_run: + workflows: + - Test + types: + - completed + branches: + - "master" jobs: publish: runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout repo uses: actions/checkout@master diff --git a/Dockerfile b/Dockerfile index 94ac3c8..635800e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine@sha256:41ea2b8caa7fa3740fd0f0a1cad0c26961707a7e3c7cc49559f54b277ef86fb3 as base +FROM python:3.8-alpine@sha256:6fb0e290e9c69d7c58f75a6b881380081ff73017440f8fa804dfd880aa908179 as base LABEL org.opencontainers.image.authors="Myles Gray" LABEL org.opencontainers.image.source='https://github.com/mylesagray/discord-power-bot' diff --git a/Pipfile b/Pipfile index 3c00156..3bc9790 100644 --- a/Pipfile +++ b/Pipfile @@ -4,9 +4,11 @@ verify_ssl = true name = "pypi" [packages] -requests = "==2.28.1" +requests = "==2.28.2" py-cord = "==2.2.2" steamquery = "==1.0.2" +marshmallow = "*" +marshmallow-dataclass = "*" [dev-packages] pylint = "*" @@ -17,3 +19,8 @@ pytest-cov = "*" [requires] python_version = "3.11" +pytest = "*" +exceptiongroup = "*" + +[scripts] +test = "python -m pytest --cov=app --cov-report=html" diff --git a/Pipfile.lock b/Pipfile.lock index 0255fa5..724a92b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,10 +1,12 @@ { "_meta": { "hash": { - "sha256": "4ca337157b8d0f90b47d13631aea3764fe972607558db7d05e70db1af6cab65b" + "sha256": "6f8ee46198734bc657cd4af50335fb420ce2470001181d7d06d0937058ffcb71" }, "pipfile-spec": 6, "requires": { + "exceptiongroup": "*", + "pytest": "*", "python_version": "3.11" }, "sources": [ @@ -111,11 +113,11 @@ }, "aiosignal": { "hashes": [ - "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", - "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "markers": "python_version >= '3.7'", + "version": "==1.3.1" }, "async-timeout": { "hashes": [ @@ -127,19 +129,19 @@ }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.6'", + "version": "==22.2.0" }, "certifi": { "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "markers": "python_version >= '3.6'", - "version": "==2022.9.24" + "version": "==2022.12.7" }, "charset-normalizer": { "hashes": [ @@ -158,68 +160,83 @@ }, "frozenlist": { "hashes": [ - "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", - "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04", - "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944", - "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845", - "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f", - "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f", - "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb", - "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2", - "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3", - "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f", - "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a", - "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131", - "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1", - "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa", - "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8", - "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b", - "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2", - "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e", - "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b", - "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b", - "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10", - "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170", - "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8", - "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b", - "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989", - "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd", - "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03", - "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519", - "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189", - "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b", - "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca", - "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff", - "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96", - "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d", - "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5", - "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2", - "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204", - "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a", - "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8", - "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141", - "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792", - "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9", - "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c", - "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c", - "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221", - "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d", - "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc", - "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f", - "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9", - "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef", - "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3", - "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab", - "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b", - "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db", - "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988", - "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6", - "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc", - "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83", - "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be" + "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c", + "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f", + "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a", + "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784", + "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27", + "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d", + "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3", + "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678", + "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a", + "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483", + "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8", + "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf", + "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99", + "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c", + "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48", + "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5", + "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56", + "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e", + "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1", + "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401", + "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4", + "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e", + "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649", + "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a", + "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d", + "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0", + "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6", + "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d", + "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b", + "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6", + "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf", + "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef", + "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7", + "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842", + "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba", + "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420", + "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b", + "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d", + "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332", + "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936", + "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816", + "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91", + "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420", + "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448", + "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411", + "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4", + "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32", + "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b", + "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0", + "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530", + "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669", + "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7", + "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1", + "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5", + "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce", + "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4", + "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e", + "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2", + "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d", + "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9", + "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642", + "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0", + "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703", + "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb", + "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1", + "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13", + "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab", + "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38", + "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb", + "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb", + "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81", + "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8", + "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd", + "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4" ], "markers": "python_version >= '3.7'", - "version": "==1.3.1" + "version": "==1.3.3" }, "idna": { "hashes": [ @@ -229,78 +246,116 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "marshmallow": { + "hashes": [ + "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", + "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" + ], + "index": "pypi", + "version": "==3.19.0" + }, + "marshmallow-dataclass": { + "hashes": [ + "sha256:b7b0ee28fa9e88edd00db324762848d3182fe5dba82c3b897cb525c402ffd143", + "sha256:d9dbce8d356a8dad2b80fecf15cac97f42a48397eefa594693874a2a6539ea84" + ], + "index": "pypi", + "version": "==8.5.11" + }, "multidict": { "hashes": [ - "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", - "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", - "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", - "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", - "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", - "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", - "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", - "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", - "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", - "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", - "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", - "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", - "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", - "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", - "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", - "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", - "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", - "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", - "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", - "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", - "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", - "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", - "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", - "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", - "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", - "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", - "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", - "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", - "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", - "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", - "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", - "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", - "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", - "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", - "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", - "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", - "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", - "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", - "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", - "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", - "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", - "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", - "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", - "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", - "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", - "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", - "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", - "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", - "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", - "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", - "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", - "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", - "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", - "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", - "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", - "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", - "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", - "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", - "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" + "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", + "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", + "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", + "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", + "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", + "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", + "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", + "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", + "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", + "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", + "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", + "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", + "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", + "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", + "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", + "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", + "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", + "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", + "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", + "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", + "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", + "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", + "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", + "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", + "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", + "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", + "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", + "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", + "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", + "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", + "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", + "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", + "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", + "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", + "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", + "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", + "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", + "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", + "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", + "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", + "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", + "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", + "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", + "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", + "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", + "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", + "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", + "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", + "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", + "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", + "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", + "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", + "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", + "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", + "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", + "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", + "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", + "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", + "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", + "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", + "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", + "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", + "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", + "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", + "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", + "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", + "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", + "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", + "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", + "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", + "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", + "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", + "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", + "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" ], "markers": "python_version >= '3.7'", - "version": "==6.0.2" + "version": "==6.0.4" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.0" }, "py-cord": { "hashes": [ @@ -310,21 +365,13 @@ "index": "pypi", "version": "==2.2.2" }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "index": "pypi", - "version": "==2.28.1" + "version": "==2.28.2" }, "steamquery": { "hashes": [ @@ -334,208 +381,265 @@ "index": "pypi", "version": "==1.0.2" }, + "typing-extensions": { + "hashes": [ + "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", + "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + ], + "markers": "python_version >= '3.7'", + "version": "==4.4.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188", + "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d" + ], + "version": "==0.8.0" + }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.14" }, "yarl": { "hashes": [ - "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb", - "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3", - "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035", - "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453", - "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d", - "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a", - "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231", - "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f", - "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae", - "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b", - "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3", - "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507", - "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd", - "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae", - "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe", - "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c", - "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4", - "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64", - "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357", - "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54", - "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461", - "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4", - "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497", - "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0", - "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1", - "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957", - "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350", - "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780", - "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843", - "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548", - "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6", - "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40", - "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee", - "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b", - "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6", - "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0", - "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e", - "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880", - "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc", - "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e", - "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead", - "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28", - "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf", - "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd", - "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae", - "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0", - "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0", - "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae", - "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda", - "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546", - "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802", - "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be", - "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07", - "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936", - "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272", - "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc", - "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a", - "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28", - "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b" + "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87", + "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89", + "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a", + "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08", + "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996", + "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077", + "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901", + "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e", + "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee", + "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574", + "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165", + "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634", + "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229", + "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b", + "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f", + "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7", + "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf", + "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89", + "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0", + "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1", + "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe", + "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf", + "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76", + "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951", + "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863", + "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06", + "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562", + "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6", + "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c", + "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e", + "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1", + "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3", + "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3", + "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778", + "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8", + "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2", + "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b", + "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d", + "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f", + "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c", + "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581", + "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918", + "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c", + "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e", + "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220", + "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37", + "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739", + "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77", + "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6", + "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42", + "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946", + "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5", + "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d", + "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146", + "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a", + "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83", + "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef", + "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80", + "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588", + "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5", + "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2", + "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef", + "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826", + "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05", + "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516", + "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0", + "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4", + "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2", + "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0", + "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd", + "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8", + "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b", + "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1", + "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c" ], "markers": "python_version >= '3.7'", - "version": "==1.8.1" + "version": "==1.8.2" } }, "develop": { "astroid": { "hashes": [ - "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83", - "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f" + "sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b", + "sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1" ], "markers": "python_full_version >= '3.7.2'", - "version": "==2.12.12" + "version": "==2.13.3" }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.6'", + "version": "==22.2.0" }, "autopep8": { "hashes": [ - "sha256:8b1659c7f003e693199f52caffdc06585bb0716900bbc6a7442fd931d658c077", - "sha256:ad924b42c2e27a1ac58e432166cc4588f5b80747de02d0d35b1ecbd3e7d57207" + "sha256:be5bc98c33515b67475420b7b1feafc8d32c1a69862498eda4983b45bffd2687", + "sha256:d27a8929d8dcd21c0f4b3859d2d07c6c25273727b98afc984c039df0f0d86566" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.0.1" }, "coverage": { "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab", + "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851", + "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265", + "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0", + "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a", + "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5", + "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6", + "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311", + "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada", + "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f", + "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8", + "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc", + "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73", + "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf", + "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e", + "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352", + "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c", + "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c", + "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c", + "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda", + "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d", + "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0", + "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3", + "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d", + "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038", + "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c", + "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8", + "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa", + "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09", + "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b", + "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c", + "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a", + "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52", + "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3", + "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146", + "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a", + "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f", + "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4", + "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c", + "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75", + "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040", + "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063", + "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050", + "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7", + "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222", + "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912", + "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801", + "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d", + "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06", + "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8", + "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2" ], "index": "pypi", - "version": "==6.5.0" + "version": "==7.1.0" }, "dill": { "hashes": [ "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==0.3.6" }, + "exceptiongroup": { + "hashes": [ + "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", + "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.0" + }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "isort": { "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.10.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==5.12.0" }, "lazy-object-proxy": { "hashes": [ - "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", - "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", - "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", - "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", - "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", - "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", - "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", - "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", - "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", - "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", - "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", - "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", - "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", - "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", - "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", - "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", - "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", - "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", - "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" + "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", + "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", + "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", + "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", + "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", + "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", + "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", + "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", + "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", + "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", + "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", + "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", + "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", + "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", + "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", + "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", + "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", + "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", + "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", + "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", + "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", + "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", + "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", + "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", + "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", + "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", + "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", + "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", + "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", + "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", + "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", + "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", + "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", + "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", + "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", + "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" ], "markers": "python_version >= '3.7'", - "version": "==1.8.0" + "version": "==1.9.0" }, "mccabe": { "hashes": [ @@ -547,33 +651,39 @@ }, "mypy": { "hashes": [ - "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d", - "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24", - "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046", - "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e", - "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3", - "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5", - "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20", - "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda", - "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1", - "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146", - "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206", - "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746", - "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6", - "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e", - "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc", - "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a", - "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8", - "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763", - "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2", - "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947", - "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40", - "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b", - "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795", - "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c" + "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d", + "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6", + "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf", + "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f", + "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813", + "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33", + "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad", + "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05", + "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297", + "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06", + "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd", + "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243", + "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305", + "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476", + "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711", + "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70", + "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5", + "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461", + "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab", + "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c", + "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d", + "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135", + "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93", + "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648", + "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a", + "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb", + "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3", + "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372", + "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb", + "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef" ], "index": "pypi", - "version": "==0.982" + "version": "==0.991" }, "mypy-extensions": { "hashes": [ @@ -584,19 +694,19 @@ }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.0" }, "platformdirs": { "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" ], "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "version": "==2.6.2" }, "pluggy": { "hashes": [ @@ -608,35 +718,27 @@ }, "pycodestyle": { "hashes": [ - "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", - "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" + "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", + "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" ], "markers": "python_version >= '3.6'", - "version": "==2.9.1" + "version": "==2.10.0" }, "pylint": { "hashes": [ - "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df", - "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004" + "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e", + "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5" ], "index": "pypi", - "version": "==2.15.5" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "version": "==2.15.10" }, "pytest": { "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], "markers": "python_version >= '3.7'", - "version": "==7.2.0" + "version": "==7.2.1" }, "pytest-cov": { "hashes": [ @@ -651,7 +753,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "tomlkit": { @@ -737,7 +839,7 @@ "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" ], - "markers": "python_version >= '3.11'", + "markers": "python_version < '3.11'", "version": "==1.14.1" } } diff --git a/app/bot.py b/app/bot.py index 6b8238c..b35be9b 100644 --- a/app/bot.py +++ b/app/bot.py @@ -5,8 +5,12 @@ import sys import json import traceback +from asyncio import TimeoutError as asyncTimeoutError +from typing import List import requests import discord +from discord.ext import commands +import gamequery def env_defined(key): @@ -16,39 +20,84 @@ def env_defined(key): return key in os.environ and len(os.environ[key]) > 0 -DISCORD_CHANNEL = [] +DISCORD_CHANNEL: List[str] = [] + # env variables are defaults, if no config file exists it'll be created. # If no env is set, stop the bot -if not env_defined("DISCORD_TOKEN"): - print("Missing bot token from .env") - sys.exit() -DISCORD_TOKEN = os.environ["DISCORD_TOKEN"] - -if not env_defined("WOL_URL"): - print("Missing wake on lan URL from .env") - sys.exit() -WOL_URL = os.environ["WOL_URL"] - -if not env_defined("SHUTDOWN_URL"): - print("Missing shutdown URL from .env") - sys.exit() -SHUTDOWN_URL = os.environ["SHUTDOWN_URL"] - -if not env_defined("REBOOT_URL"): - print("Missing liveness URL from .env") +try: + DISCORD_TOKEN = os.environ["DISCORD_TOKEN"] + WOL_URL = os.environ["WOL_URL"] + SHUTDOWN_URL = os.environ["SHUTDOWN_URL"] + REBOOT_URL = os.environ["REBOOT_URL"] + LIVENESS_URL = os.environ["LIVENESS_URL"] +except KeyError as e: + print(f"Missing {str(e)} token from .env.") sys.exit() -REBOOT_URL = os.environ["REBOOT_URL"] -if not env_defined("LIVENESS_URL"): - print("Missing liveness URL from .env") - sys.exit() -LIVENESS_URL = os.environ["LIVENESS_URL"] +# Defaulting COOLDOWN to 300s unless set by the user +if env_defined("COOLDOWN"): + COOLDOWN: int = int(os.environ["COOLDOWN"]) +else: + COOLDOWN: int = 300 + +# Defaulting POWERBOT_ROLE to "@everyone" unless set by the user +if env_defined("POWERBOT_ROLE"): + POWERBOT_ROLE = os.environ["POWERBOT_ROLE"].split(",") + # commands.has_any_role() takes either a role name as string, + # or a role ID as integer. Can't just map() a mixed list. + for i in range(0, len(POWERBOT_ROLE)): + if POWERBOT_ROLE[i].isdigit(): + POWERBOT_ROLE[i] = int(POWERBOT_ROLE[i]) +else: + POWERBOT_ROLE = "@everyone" intents = discord.Intents.default() DESC = "Bot to control the power to physical game server" bot = discord.Bot(description=DESC, intents=intents) +def check_cooldown(ctx): + """ + Each of boot, shutdown, restart triggers a cooldown for itself. + This function is designed to link their cooldowns to avoid hangups. + """ + if ctx.command.name in ["boot", "reboot", "shutdown"]: + boot_cd = bot.get_application_command(name="boot").is_on_cooldown(ctx) + reboot_cd = bot.get_application_command( + name="reboot").is_on_cooldown(ctx) + shutdown_cd = bot.get_application_command( + name="shutdown").is_on_cooldown(ctx) + if boot_cd or reboot_cd or shutdown_cd: + return False + return True + + +async def on_application_command_error(ctx, error): + """ + Responds to a user calling a function that's currently on cooldown. + """ + if isinstance(error, commands.CommandOnCooldown): + cooldown = round(ctx.command.get_cooldown_retry_after(ctx)) + await ctx.respond(f'`/{ctx.command.name}` is currently on cooldown. ' + f'Please wait another {cooldown}s before retrying.' + f'or retry with `/sudo {ctx.command.name}`.') + elif isinstance(error, discord.errors.CheckFailure): + boot_cd = bot.get_application_command( + name="boot").get_cooldown_retry_after(ctx) + reboot_cd = bot.get_application_command( + name="reboot").get_cooldown_retry_after(ctx) + shutdown_cd = bot.get_application_command( + name="shutdown").get_cooldown_retry_after(ctx) + # Since only one command can ever be on an individual cooldown, + # addition works + cooldown = round(boot_cd + reboot_cd + shutdown_cd) + await ctx.respond(f'`/{ctx.command.name}` is currently on cooldown. ' + f'Please wait another {cooldown}s before retrying.' + f'or retry with `/sudo {ctx.command.name}`.') + else: + raise error # Here we raise other errors to ensure they aren't ignored + + @bot.event async def on_ready(): """ @@ -61,6 +110,10 @@ async def on_ready(): @bot.slash_command(name="boot", description="Boots the game server") +@commands.cooldown(rate=1, per=float(COOLDOWN), type=commands.BucketType.guild) +@commands.check(check_cooldown) +# https://github.com/Pycord-Development/pycord/issues/974 +@commands.has_any_role(*POWERBOT_ROLE) async def _boot(ctx): try: response = requests.get(WOL_URL, timeout=2) @@ -76,34 +129,92 @@ async def _boot(ctx): @bot.slash_command(name="shutdown", description="Shuts down the game server") +@commands.cooldown(rate=1, per=float(COOLDOWN), type=commands.BucketType.guild) +@commands.check(check_cooldown) +# https://github.com/Pycord-Development/pycord/issues/974 +@commands.has_any_role(*POWERBOT_ROLE) async def _shutdown(ctx): try: - response = requests.get(SHUTDOWN_URL, timeout=2) - if response.status_code == 200: - game = discord.Activity( - name="Powering down...", type=discord.ActivityType.playing) - await bot.change_presence(status=discord.Status.do_not_disturb, activity=game) - await ctx.respond('Server shut down!') + if gamequery.is_anyone_active(): + await ctx.respond('Server can\'t be shut down, someone is online!') + else: + response = requests.get(SHUTDOWN_URL, timeout=2) + if response.status_code == 200: + game = discord.Activity( + name="Powering down...", type=discord.ActivityType.playing) + await bot.change_presence(status=discord.Status.do_not_disturb, activity=game) + await ctx.respond('Server shut down!') except Exception: await ctx.respond('Server is already offline') traceback.print_exc() @bot.slash_command(name="reboot", description="Reboots the game server") +@commands.cooldown(rate=1, per=float(COOLDOWN), type=commands.BucketType.guild) +@commands.check(check_cooldown) +# https://github.com/Pycord-Development/pycord/issues/974 +@commands.has_any_role(*POWERBOT_ROLE) async def _reboot(ctx): - try: - response = requests.get(REBOOT_URL, timeout=2) - if response.status_code == 200: - game = discord.Activity( - name="Rebooting...", type=discord.ActivityType.playing) - await bot.change_presence(status=discord.Status.streaming, activity=game) - await ctx.respond('Server rebooting!') + if gamequery.is_anyone_active(): + await ctx.respond('Server can\'t be rebooted, someone is online!') + else: + response = requests.get(REBOOT_URL, timeout=2) + if response.status_code == 200: + game = discord.Activity( + name="Rebooting...", type=discord.ActivityType.playing) + await bot.change_presence(status=discord.Status.streaming, activity=game) + await ctx.respond('Server rebooting!') except Exception: await ctx.respond('Server is already offline') traceback.print_exc() -@bot.slash_command(name="status", description="Checks current power status of game server") +@_boot.error +@_shutdown.error +@_reboot.error +async def _error(ctx, error): + await on_application_command_error(ctx, error) + + +@bot.slash_command(name="sudo", description="Use commands regardless of their cooldown") +# https://github.com/Pycord-Development/pycord/issues/974 +@commands.has_any_role(*POWERBOT_ROLE) +@discord.option( + "command", + description="Command to be run ignoring any cooldown.", + choices=["boot", "reboot", "shutdown"] +) +async def _sudo(ctx, command): + """ + Allows to bypass cooldowns that are usually placed upon power functions + by resetting them all and then invoking the supplied command. + """ + embed = discord.Embed(type="rich", colour=discord.Colour.red()) + embed.title = '<:warning:1043511363441537046>' \ + ' WARNING <:warning:1043511363441537046>' + embed.description = f'Are you sure that you want to force `{command}`? ' \ + 'If yes, react with <:sos:1043671788007211108>.' + res = await ctx.respond(embed=embed) + msg = await res.original_response() + # default emojis need to be unicode + await msg.add_reaction('\N{Squared SOS}') + + def check(reaction, user): + return user == ctx.author and str(reaction.emoji) == '\N{Squared SOS}' + try: + await bot.wait_for('reaction_add', timeout=5.0, check=check) + except asyncTimeoutError: + await msg.clear_reactions() + else: + bot.get_application_command(name="boot").reset_cooldown(ctx) + bot.get_application_command(name="reboot").reset_cooldown(ctx) + bot.get_application_command(name="shutdown").reset_cooldown(ctx) + await bot.get_application_command(command).invoke(ctx) + await ctx.respond(f'`{command}` executed.') + + +@bot.slash_command(name="status", + description="Checks current power status of game server") async def _status(ctx): try: response = requests.get(LIVENESS_URL, timeout=2) diff --git a/app/gamequery.py b/app/gamequery.py index 0c19987..b35a705 100644 --- a/app/gamequery.py +++ b/app/gamequery.py @@ -2,47 +2,92 @@ Queries SteamQuery, DCS and SpaceEngineers instances """ import traceback +import logging from steam import SteamQuery import network -from servers import Server +from servers import Server, ServerType, list_servers, get_server -def get_players(ipaddress: int, port: int, password="") -> dict: +def is_anyone_active() -> bool: """ - Returns a dict with the current number of players connected - to the server as well as the max players supported + Checks all known servers for users currently logged in + returns a bool """ try: - server = _steam_server_connection(server_ip=ipaddress, port=port) - server_state = _lint_steamquery_output(server.query_server_info()) - return {"current_players": server_state["players"], - "max_players": server_state["max_players"]} - - except Exception: - print("Could not get server info") + player_count = 0 + for server in list_servers(): + server = get_server(server) + player_count += get_players(server).get('current_players') + if player_count > 0: + return True + else: + return False + except: + logging.error("Couldn't query servers for active players") traceback.print_exc() raise -def get_players_details(ipaddress: int, port: int, password="") -> list: +def get_players(server: Server) -> dict: + """ + Returns a dict with the current number of players connected + to the server as well as the max players supported + """ + if server['server_type'] is ServerType.STEAM: + try: + steamquery = _steam_server_connection( + server_ip=str(server['ip_address']), port=server['port']) + server_state = _lint_steamquery_output( + steamquery.query_server_info()) + return {"current_players": server_state["players"], + "max_players": server_state["max_players"]} + + except Exception: + print("Could not get server info") + traceback.print_exc() + raise + elif server['server_type'] is ServerType.SPACE_ENGINEERS: + return {"current_players": 0, + "max_players": 0} + + elif server['server_type'] is ServerType.DCS: + return {"current_players": 0, + "max_players": 0} + + else: + print(f'Cannot query unrecognised server type {server_type}') + + +def get_players_details(server: Server) -> list: """ Returns a list with all current player objects containing names, scores and durations on the server """ - try: - server = _steam_server_connection(server_ip=ipaddress, port=port) - player_info = _lint_steamquery_output(server.query_player_info()) - return player_info + if server['server_type'] is ServerType.STEAM: + try: + steamquery = _steam_server_connection( + server_ip=str(server['ip_address']), port=server['port']) + player_info = _lint_steamquery_output( + steamquery.query_player_info()) + return player_info + + except Exception: + print("Could not get player info") + traceback.print_exc() + raise + elif server['server_type'] is ServerType.SPACE_ENGINEERS: + pass + + elif server['server_type'] is ServerType.DCS: + pass + else: + print(f'Cannot query unrecognised server type {server_type}') - except Exception: - print("Could not get player info") - traceback.print_exc() - raise # Creates and returns server connection object -def _steam_server_connection(server_ip: int, port: int) -> object: +def _steam_server_connection(server_ip: str, port: int) -> object: """ Creates a steam query server connection object and passes it back. """ diff --git a/app/servers.py b/app/servers.py index 78f7676..2f6464d 100644 --- a/app/servers.py +++ b/app/servers.py @@ -5,8 +5,8 @@ import traceback import json import logging -from dataclasses import dataclass from enum import Enum +from marshmallow import Schema, fields, validate logging.basicConfig(level=logging.WARN) @@ -20,34 +20,38 @@ class ServerType(str, Enum): SPACE_ENGINEERS = 'SPACE_ENGINEERS' -@dataclass -class Server: +class Server(Schema): """ Object that stores a given game server configuration """ - name: str - ip_address: str - port: int - server_type: ServerType - password: str + name = fields.Str() + ip_address = fields.IP() + port = fields.Int(validate=validate.Range(1, 65535)) + password = fields.Str() + server_type = fields.Enum(ServerType) server_list = {} -def add_server(name, ip_address, port, server_type, password=""): +def add_server(name: str, ip_address: str, port: int, server_type: str, + password: str = "") -> Server: """ Adds a server to the actively monitored list """ try: if not name in server_list: - if server_type in ServerType.__members__: - server = Server(name=name, ip_address=ip_address, - port=port, server_type=ServerType[server_type], password=password) - server_list[name] = server - return server - raise TypeError(f"Server type '{server_type}' is invalid") + server_info = { + 'name': name, + 'ip_address': ip_address, + 'port': port, + 'server_type': server_type, + 'password': password + } + server = Server().load(server_info) + server_list[name] = server + return server raise ValueError("Server name already taken") except Exception: logging.error("Failed to add server") @@ -59,27 +63,25 @@ def delete_server(name): """ Deletes a server from the actively monitored list """ - try: - return server_list.pop(name) - except KeyError: - logging.error(f"Failed to delete server, '{name}' does not exist.") - traceback.print_exc() - raise + if name == '*': + server_list.clear() + else: + server_list.pop(name) -def update_server(name, server): +def update_server(name: str, server_info: dict): """ Updates a server entry in the bot's monitoring list Side note: this is essentially the same op as add_server but without the duplicate key check to allow for overwrites """ try: + schema = Server() + server = schema.load(server_info) server_list[name] = server - return True - except Exception: + except ValueError: logging.error("Failed to update server") traceback.print_exc() - raise def get_server(name): @@ -89,7 +91,7 @@ def get_server(name): return server_list.get(name) -def list_servers(): +def list_servers() -> dict: """ Lists servers currently monitored by the bot """ @@ -101,8 +103,9 @@ def save_servers(): Saves out server config to disk as json """ server_serialised = [] + schema = Server() for server in server_list.values(): - server_serialised.append(server.__dict__) + server_serialised.append(schema.dump(server)) _save_settings(server_serialised) @@ -117,10 +120,7 @@ def load_servers(): raise TypeError( f"Server type: '{server['server_type']}' \ for server '{server['name']}' is invalid") - server_obj = Server(name=server['name'], ip_address=server['ip_address'], - port=server['port'], password=server['password'], - server_type=ServerType(server['server_type'])) - update_server(server['name'], server_obj) + update_server(server['name'], server) except Exception: logging.error("Couldn't load settings into bot") traceback.print_exc() @@ -137,11 +137,9 @@ def _save_settings(jsonstring): except Exception: logging.error("Failed to save settings to disk") traceback.print_exc() - return 0 - return 1 -def _load_settings(): +def _load_settings() -> dict: """ Writes out server config settings to disk in json """ diff --git a/docker-compose.yml b/docker-compose.yml index 80c50b6..9b80232 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,4 +4,6 @@ services: image: ghcr.io/kharms-dev/discord-power-bot:latest build: . env_file: .env - restart: unless-stopped \ No newline at end of file + restart: unless-stopped + volumes: + - servers.json:/home/appuser/servers.json \ No newline at end of file diff --git a/readme.md b/readme.md index 02c703b..ccd0893 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,10 @@ ## Usage * Fill out `.env` file with your Discord token and URLs. +* Fill out `servers.json` with your server details and types to allow querying of clients on power requests +* Optional: set `COOLDOWN` for `boot`, `reboot` and `shutdown` cooldown timers in seconds, if left empty it defaults to `300`. +* Optional: set `POWERBOT_ROLE` to limit access to `boot`, `reboot` and `shutdown`. This takes a comma separated list of either role names or role ids, if left unset defaults to the `@everyone` role. + For WOL service, use this Docker image: @@ -15,5 +19,5 @@ docker compose up -d Bare docker cli: ```sh -docker run --env-file=/.env --restart=unless-stopped ghcr.io/mylesagray/discord-power-bot:latest +docker run --env-file=/.env --restart=unless-stopped -v servers.json:/home/appuser/servers.json ghcr.io/mylesagray/discord-power-bot:latest ``` diff --git a/tests/test_servers.py b/tests/test_servers.py index 22940fe..dda1776 100644 --- a/tests/test_servers.py +++ b/tests/test_servers.py @@ -2,6 +2,10 @@ Tests for server module """ import unittest +from ipaddress import IPv4Address, IPv6Address + +from marshmallow import ValidationError + from app import servers @@ -40,31 +44,30 @@ def test_add_servers_positive(self): arma_server = servers.add_server( 'Arma Server 1', '172.16.69.180', 2303, 'STEAM') se_server = servers.add_server( - 'Space Engineers Server 1', '192.168.1.179', 27019, 'SPACE_ENGINEERS') - - self.assertIsInstance(dcs_server, servers.Server) - self.assertEqual(dcs_server.name, 'DCS Server 1') - self.assertEqual(dcs_server.ip_address, '10.56.0.175') - self.assertEqual(dcs_server.port, 10309) - self.assertEqual(dcs_server.server_type, servers.ServerType.DCS) - self.assertIsInstance(arma_server, servers.Server) - self.assertEqual(arma_server.name, 'Arma Server 1') - self.assertEqual(arma_server.ip_address, '172.16.69.180') - self.assertEqual(arma_server.port, 2303) - self.assertEqual(arma_server.server_type, servers.ServerType.STEAM) - self.assertIsInstance(se_server, servers.Server) - self.assertEqual(se_server.name, 'Space Engineers Server 1') - self.assertEqual(se_server.ip_address, '192.168.1.179') - self.assertEqual(se_server.port, 27019) - self.assertEqual(se_server.server_type, + 'Space Engineers Server 1', 'fe80::a00:20ff:feb9:17fa', 27019, 'SPACE_ENGINEERS') + + self.assertEqual(dcs_server['name'], 'DCS Server 1') + self.assertEqual(dcs_server['ip_address'], IPv4Address('10.56.0.175')) + self.assertEqual(dcs_server['port'], 10309) + self.assertEqual(dcs_server['server_type'], servers.ServerType.DCS) + self.assertEqual(arma_server['name'], 'Arma Server 1') + self.assertEqual(arma_server['ip_address'], + IPv4Address('172.16.69.180')) + self.assertEqual(arma_server['port'], 2303) + self.assertEqual(arma_server['server_type'], servers.ServerType.STEAM) + self.assertEqual(se_server['name'], 'Space Engineers Server 1') + self.assertEqual(se_server['ip_address'], + IPv6Address('fe80::a00:20ff:feb9:17fa')) + self.assertEqual(se_server['port'], 27019) + self.assertEqual(se_server['server_type'], servers.ServerType.SPACE_ENGINEERS) def test_add_servers_invalid_server_type(self): """ - Tests if a TypeError is thrown if an invalid type is specified + Tests if a marshmallow.exceptions.ValidationError is thrown if an invalid type is specified """ - self.assertRaises(TypeError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test', '10.0.0.1', 1000, 'FAIL') def test_add_servers_duplicate_name(self): @@ -81,11 +84,11 @@ def test_add_servers_invalid_port(self): Tests if a ValueError is thrown if a port outside valid 1-65535 range is set """ - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test2', '10.0.0.1', 65536, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test3', '10.0.0.1', -1, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test4', '10.0.0.1', 0, 'DCS') def test_add_servers_invalid_ipv4(self): @@ -93,13 +96,13 @@ def test_add_servers_invalid_ipv4(self): Tests if a ValueError is thrown if an invalid IPv4 address is supplied """ - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test5', '12345', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test6', '256.256.256.256', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test7', '1.2.3.4.5', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test8', 'string', 1000, 'DCS') def test_add_servers_invalid_ipv6(self): @@ -107,13 +110,13 @@ def test_add_servers_invalid_ipv6(self): Tests if a ValueError is thrown if an invalid IPv6 address is supplied """ - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test9', '12345', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test10', '256.256.256.256', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test11', '1.2.3.4.5', 1000, 'DCS') - self.assertRaises(ValueError, servers.add_server, + self.assertRaises(ValidationError, servers.add_server, 'Test12', 'string', 1000, 'DCS') @@ -129,7 +132,7 @@ def test_delete_servers_positive(self): servers.add_server('Delete me', '10.56.0.175', 10309, 'DCS') - self.assertTrue(servers.delete_server('Delete me')) + self.assertIsNone(servers.delete_server('Delete me')) def test_delete_servers_raise_key_error(self): """ @@ -151,20 +154,57 @@ def test_update_servers_positive(self): """ servers.add_server('Update me', '10.0.0.1', 1000, 'DCS') - update = servers.Server('Update me', '1.1.1.1', - 1000, 'password', servers.ServerType.DCS) + server_info = { + 'name': 'Update me', + 'ip_address': '1.1.1.1', + 'port': 1000, + 'server_type': 'DCS', + 'password': 'password' + } - self.assertTrue(servers.update_server('Update me', update)) + self.assertIsNone(servers.update_server('Update me', server_info)) def test_update_servers_invalid_type(self): """ Tests if Server is successfully updated """ - update = servers.Server('Update me fail', '1.1.1.1', - 1000, 'password', servers.ServerType.DCS) + with self.assertRaises(ValidationError): + server_info = { + 'name': 'Update me', + 'ip_address': '1.1.1.1', + 'port': 1000, + 'server_type': 'Fail', + 'password': 'password' + } + servers.update_server('Update me', server_info) + + +class SerialiseDeserialiseServersTest(unittest.TestCase): + """ + Class to test data (de)serialisation + """ + + def test_data_serialise_equal_deserialise(self): + """ + Tests that objects are successfully serialised to disk + """ + servers.delete_server('*') + + server_list = {} + server_list['DCS Server Serialise'] = servers.add_server( + 'DCS Server Serialise', '10.56.0.175', 10309, 'DCS') + server_list['Arma Server Serialise'] = servers.add_server( + 'Arma Server Serialise', '172.16.69.180', 2303, 'STEAM') + server_list['Space Engineers Server Serialise'] = servers.add_server( + 'Space Engineers Server Serialise', 'fe80::a00:20ff:feb9:17fa', + 27019, 'SPACE_ENGINEERS') - self.assertFalse(servers.update_server('Update me fail', update)) + self.assertIsNone(servers.save_servers()) + self.assertIsNone(servers.load_servers()) + print(servers.list_servers()) + print(server_list) + self.assertEqual(servers.list_servers(), server_list) if __name__ == '__main__':