diff --git a/.gitignore b/.gitignore index 5941b8b..809e79d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.env -/test.py \ No newline at end of file +/test.py +/app.py \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b07d9d..3699532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.1] - 12/13/2023 +### General +- "find" and "update" methods should now work. +- "list" methods now return a dictionary containing the ID and NAME instead of a partial class. You will have to use "get" methods to convert them to classes + +### Known Issues +- list_servers_by_connection, and update_server all throw "Access denied" +- get_own_api_key throws "Incorrect API key from user: USERNAME" + ## [0.0.1] - 11/29/2023 Initial release \ No newline at end of file diff --git a/README.md b/README.md index 2ab8a10..c2aa863 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python](https://img.shields.io/pypi/pyversions/multicraft)](https://www.python.org/downloads/) ![Downloads](https://img.shields.io/pypi/dm/multicraft) ![Status](https://img.shields.io/pypi/status/multicraft) -[![Issues](https://img.shields.io/github/issues/legopitstop/multicraft)](https://github.com/legopitstop/multicraft-py/issues) +[![Issues](https://img.shields.io/github/issues/legopitstop/multicraft-py)](https://github.com/legopitstop/multicraft-py/issues) Interact with your Minecraft server from hosts that use [Multicraft](https://www.multicraft.org/) using Python. @@ -16,12 +16,36 @@ pip3 install multicraft Update existing installation: `pip3 install multicraft --upgrade` ## Features -- Authed requests - Includes a handful of common multicraft hosts. +- Manage users, players, commands, schedules, and databases. +- Start, stop, or restart your server. +- Run console commands (give, kill, whitelist, op, etc) +- Read your servers current cpu and memmory usage. +- Send a chat message. -See the docs for more information. +See the [docs](https://github.com/legopitstop/multicraft-py/wiki) for more information. ## Dependencies |Name|Description| |--|--| |[requests](https://pypi.org/project/requests/) | Requests is a simple, yet elegant, HTTP library. | + +## Example +```py +from multicraft import MulticraftAPI + +api = MulticraftAPI( + url = 'https://localhost/api.php', + user = 'username', + key = 'apiKey' +) + +owner = api.get_user_id(api.user) + +owned_servers = api.list_servers_by_owner(owner) +print(owned_servers) + +for id in owned_servers.keys(): + server = api.get_server(id) + print(server) +``` \ No newline at end of file diff --git a/dist/multicraft-0.0.1.tar.gz b/dist/multicraft-0.0.1.tar.gz new file mode 100644 index 0000000..a0bf6cf Binary files /dev/null and b/dist/multicraft-0.0.1.tar.gz differ diff --git a/dist/multicraft-0.0.2.tar.gz b/dist/multicraft-0.0.2.tar.gz new file mode 100644 index 0000000..6755a55 Binary files /dev/null and b/dist/multicraft-0.0.2.tar.gz differ diff --git a/examples.py b/examples.py index 13bded4..5c889bd 100644 --- a/examples.py +++ b/examples.py @@ -1,6 +1,5 @@ import multicraft - api = multicraft.MulticraftAPI( url = 'https://localhost/api.php', user = 'username', diff --git a/multicraft.egg-info/PKG-INFO b/multicraft.egg-info/PKG-INFO new file mode 100644 index 0000000..81a695c --- /dev/null +++ b/multicraft.egg-info/PKG-INFO @@ -0,0 +1,70 @@ +Metadata-Version: 2.1 +Name: multicraft +Version: 0.0.2 +Summary: Interact with your Minecraft server from hosts that use Multicraft using Python +Home-page: https://github.com/legopitstop/multicraft-py +Author: Legopitstop +Author-email: officiallegopitstop@gmail.com +License: MIT +Keywords: multicraft,minecraft,minecraftserver,server +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Build Tools +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python :: 3.11 +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE + +# multicraft + +[![PyPI](https://img.shields.io/pypi/v/multicraft)](https://pypi.org/project/multicraft/) +[![Python](https://img.shields.io/pypi/pyversions/multicraft)](https://www.python.org/downloads/) +![Downloads](https://img.shields.io/pypi/dm/multicraft) +![Status](https://img.shields.io/pypi/status/multicraft) +[![Issues](https://img.shields.io/github/issues/legopitstop/multicraft-py)](https://github.com/legopitstop/multicraft-py/issues) + +Interact with your Minecraft server from hosts that use [Multicraft](https://www.multicraft.org/) using Python. + +## Installation +Install the module with pip: +```bat +pip3 install multicraft +``` +Update existing installation: `pip3 install multicraft --upgrade` + +## Features +- Includes a handful of common multicraft hosts. +- Manage users, players, commands, schedules, and databases. +- Start, stop, or restart your server. +- Run console commands (give, kill, whitelist, op, etc) +- Read your servers current cpu and memmory usage. +- Send a chat message. + +See the [docs](https://github.com/legopitstop/multicraft-py/wiki) for more information. + +## Dependencies +|Name|Description| +|--|--| +|[requests](https://pypi.org/project/requests/) | Requests is a simple, yet elegant, HTTP library. | + +## Example +```py +from multicraft import MulticraftAPI + +api = MulticraftAPI( + url = 'https://localhost/api.php', + user = 'username', + key = 'apiKey' +) + +owner = api.get_user_id(api.user) + +owned_servers = api.list_servers_by_owner(owner) +print(owned_servers) + +for id in owned_servers.keys(): + server = api.get_server(id) + print(server) +``` diff --git a/multicraft.egg-info/SOURCES.txt b/multicraft.egg-info/SOURCES.txt new file mode 100644 index 0000000..3330f04 --- /dev/null +++ b/multicraft.egg-info/SOURCES.txt @@ -0,0 +1,13 @@ +LICENSE +README.md +setup.cfg +setup.py +multicraft/__init__.py +multicraft/api.py +multicraft/hosts.py +multicraft/model.py +multicraft.egg-info/PKG-INFO +multicraft.egg-info/SOURCES.txt +multicraft.egg-info/dependency_links.txt +multicraft.egg-info/requires.txt +multicraft.egg-info/top_level.txt \ No newline at end of file diff --git a/multicraft.egg-info/dependency_links.txt b/multicraft.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/multicraft.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/multicraft.egg-info/requires.txt b/multicraft.egg-info/requires.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/multicraft.egg-info/requires.txt @@ -0,0 +1 @@ +requests diff --git a/multicraft.egg-info/top_level.txt b/multicraft.egg-info/top_level.txt new file mode 100644 index 0000000..5ac79f9 --- /dev/null +++ b/multicraft.egg-info/top_level.txt @@ -0,0 +1 @@ +multicraft diff --git a/multicraft/__init__.py b/multicraft/__init__.py index c3c0d8f..ef444c0 100644 --- a/multicraft/__init__.py +++ b/multicraft/__init__.py @@ -1,870 +1,11 @@ -# https://multicraft.org/site/docs/api - -from .hosts import * -from .model import * -import os -import datetime -import requests -import hmac -import hashlib -import json - +""" +Interact with your Minecraft server from hosts that use [Multicraft](https://www.multicraft.org/) using Python. +""" class MulticraftException(Exception): pass -class MulticraftAPI: - def __init__(self, url:str, user:str, key:str=None): - """ - Create a new instance of Multicraft API - - :param url: The API url. You can find common hosts in hosts.py - :type url: str - :param user: The username of the account to use - :type user: str - :param key: The API key to login with, defaults to os.getenv('KEY') if None - :type key: str, optional - """ - self.url = str(url) - self.user = str(user) - self.key = os.getenv('KEY') if key is None else str(key) - self.user_agent = 'multicraft (https://github.com/legopitstop/multicraft-py/)' - - # Internal - - def _generateSignature(self, method:str, content:dict={}) -> dict: - keystr = '' - params = content.copy() if content is not None else {} - params["_MulticraftAPIMethod"] = str(method) - params["_MulticraftAPIUser"] = self.user - for param in params: - v = params[param] - if isinstance(v, list): - keystr += param + json.dumps(v) - else: - keystr += param + str(params[param]) - key = hmac.new(str(self.key).encode("UTF-8"), str(keystr).encode(), hashlib.sha256 ).hexdigest() - params["_MulticraftAPIKey"] = key - return params - - def _send_request(self, method:str, content:dict={}) -> dict: - params = self._generateSignature(method, content) - res = requests.post(self.url, params, headers={'user-agent': self.user_agent}) - data = json.loads(res.text) - if data.get('success'): - return data.get('data') - raise MulticraftException(data['errors']) - - # Config - - def set_user_agent(self, user_agent_string:str): - """ - Set the User-Agent string to be used for all requests. - - :param user_agent_string: A string specifying the User-Agent header - :type user_agent_string: str - """ - self.user_agent = str(user_agent_string) - - # User functions - - def get_current_user(self) -> User: - """ - Get the current user - - :return: The current user - :rtype: User - """ - res = self._send_request('getCurrentUser') - return User.from_json(res['User']) - - # TODO - Incorrect API key from user: legopitstop - # (USER, ["name"], ["test"], True) - def update_user(self, user_id:int, field:list, value:list, send_mail:bool=False): - data = { - 'id': int(user_id), - 'field': field, - 'value': value, - 'send_mail': int(send_mail) - } - res = self._send_request('updateUser', data) - return res - - def get_user_role(self, user_id:int, server_id:int) -> Role|None: - """ - Get the users role - - :param user_id: The id of the user to get - :type user_id: int - :param server_id: The id of the server that this user belongs to - :type server_id: int - :return: The users role - :rtype: Role|None - """ - data ={ - 'user_id': int(user_id), - 'server_id': int(server_id) - } - res = self._send_request('getUserRole', data)['role'] - if res == '': return None - return Role[str(res)] - - def set_user_role(self, user_id:int, server_id:int, role:Role) -> None: - """ - Sets the users role - - :param user_id: The id of the user to set - :type user_id: int - :param server_id: The id of the server that this user belongs to - :type server_id: int - :param role: The role to set - :type role: Role - """ - - if not isinstance(role, Role) and role is not None: raise TypeError(f"Expected Role but got '{role.__class__.__name__}' instead") - data = { - 'user_id': int(user_id), - 'server_id': int(server_id), - 'role': '' if role is None else role._value_ - } - self._send_request('setUserRole', data) - - def get_user_ftp_access(self, user_id:int, server_id:int) -> Mode|None: - """ - Get the users ftp access - - :param user_id: The id of the user to get - :type user_id: int - :param server_id: The id of the server that this user belongs to - :type server_id: int - :return: The users mode - :rtype: Mode|None - """ - res = self._send_request('getUserFtpAccess', { - 'user_id': int(user_id), - 'server_id': int(server_id) - })['mode'] - if res == '': return None - return Mode[str(res)] - - def set_user_ftp_access(self, user_id:int, server_id:int, mode:Mode) -> None: - """ - Sets the users ftp access - - :param user_id: The id of the user to set - :type user_id: int - :param server_id: The id of the server that this user belongs to - :type server_id: int - :param mode: The mode to set - :type mode: Mode - """ - if not isinstance(mode, Mode) and mode is not None: raise TypeError(f"Expected Mode but got '{mode.__class__.__name__}' instead") - self._send_request('setUserFtpAccess', { - 'user_id': int(user_id), - 'server_id': int(server_id), - 'mode': '' if mode is None else mode._value_ - }) - - def get_user_id(self, name:str) -> int: - """ - Get the users id from name - - :param name: The name of the user - :type name: str - :return: The id of the user - :rtype: int - """ - return self._send_request('getUserId', { - 'name': str(name) - })['id'] - - # TODO - Access denied - def generate_user_api_key(self, user_id:int): - return self._send_request('generateUserApiKey', { - 'user_id': int(user_id) - }) - - # TODO - Access denied - def get_user_api_key(self, user_id:int): - return self._send_request('getUserApiKey', { - 'user_id': int(user_id) - }) - - # TODO - def get_own_api_key(self, password:str, generate=0, gauth_code:str=''): - """ - _summary_ - - :param password: _description_ - :type password: str - :param generate: _description_, defaults to 0 - :type generate: int, optional - :param gauth_code: _description_, defaults to '' - :type gauth_code: str, optional - :return: _description_ - :rtype: _type_ - """ - return self._send_request('getOwnApiKey', { - 'password': str(password), - 'generate': str(generate), - 'gauth_code': str(gauth_code) - }) - - # Player functions - - def list_players(self, server_id:int) -> list[Player]: - """ - Get a list of all players - - :param server_id: The id of the server - :type server_id: int - :return: List of server players - :rtype: list[Player] - """ - res = self._send_request('listPlayers', { - 'server_id': int(server_id) - }) - return [Player(k, v) for k, v in dict(res['Players']).items()] - - def get_player(self, player_id:int) -> Player: - """ - Get the player from id - - :param player_id: The id of the player to get - :type player_id: int - :return: The player from id - :rtype: Player - """ - res = self._send_request('getPlayer', { - 'id': int(player_id) - })['Player'] - return Player.from_json(res) - - # TODO - Incorrect API key from user: legopitstop - def find_players(self, server_id:int, field:list, value:list): - """ - Get player from field-value - - :param server_id: The id of the server - :type server_id: int - :param field: The field to match - :type field: list - :param value: The value to match - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('findPlayers', { - 'id': int(server_id), - 'field': field, - 'value': value - }) - - # TODO - Player does not have an attribute "" - # (24471794, ['role'], ['none']) - def update_player(self, player_id:int, field:list, value:list): - """ - Update this player - - :param player_id: The id of the player to update - :type player_id: int - :param field: The field to update - :type field: list - :param value: The value to update - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('updatePlayer', { - 'id': int(player_id), - 'field': field, - 'value': value - }) - - def create_player(self, server_id:int, name:str, op_command:int=0) -> Player: - """ - Create a new player - - :param server_id: The id of the server to create the player for - :type server_id: int - :param name: The name of the player to create - :type name: str - :param op_command: The op command, defaults to 0 - :type op_command: int, optional - :return: The created player - :rtype: Player - """ - data = { - 'server_id': int(server_id), - 'name': str(name), - 'op_command': int(op_command) - } - id = self._send_request('createPlayer', data)['id'] - return Player(id, **data) - - def delete_player(self, player_id:int): - """ - Remove a player - - :param player_id: The id of the player to remove - :type player_id: int - :return: _description_ - :rtype: _type_ - """ - return self._send_request('deletePlayer', { - 'id': int(player_id) - }) - - def assign_player_to_user(self, player_id:int, user_id:int) -> None: - """ - Assign a player to a user - - :param player_id: The id of the player to assign - :type player_id: int - :param user_id: The id of the user to assign - :type user_id: int - """ - self._send_request('assignPlayerToUser', { - 'player_id': int(player_id), - 'user_id': int(user_id) - }) - - # Command functions - - def list_commands(self, server_id:int) -> list[Command]: - """ - Get a list of all commands - - :param server_id: The id of the server - :type server_id: int - :return: List of server commands - :rtype: list[Command] - """ - res = self._send_request('listCommands', { - 'server_id': int(server_id) - }) - return [Command(k, v) for k, v in res['Commands'].items()] - - # TODO - Your request is invalid. - # (SERVER, ['name'], ['Player List']) - def find_commands(self, server_id:int, field:list, value:list): - """ - Get command from field-value - - :param server_id: The id of the server - :type server_id: int - :param field: The field to match - :type field: list - :param value: The value to match - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('findCommands', { - 'server_id': int(server_id), - 'filed': field, - 'value': value - }) - - def get_command(self, command_id:int) -> Command: - """ - Get the command from id - - :param command_id: The id of the command to get - :type command_id: int - :return: The command from id - :rtype: Command - """ - res = self._send_request('getCommand', { - 'id': int(command_id) - })['Command'] - return Command.from_json(res) - - # TODO - Command does not have an attribute "" - # (5428782, ['level'], ['50']) - def update_command(self, command_id:int, field:list, value:list): - """ - Update this command - - :param command_id: The id of the command to update - :type command_id: int - :param field: The field to update - :type field: list - :param value: The value to update - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('updateCommand', { - 'id': int(command_id), - 'field': field, - 'value': value - }) - - def create_command(self, server_id:int, name:str, role:Role, chat:str, response:str, run:str) -> Command: - """ - Create a new command - - :param server_id: The id of the server to create the command for - :type server_id: int - :param name: The name of the command - :type name: str - :param role: The role of the command - :type role: Role - :param chat: The chat command to run - :type chat: str - :param response: The response text - :type response: str - :param run: The command to run - :type run: str - :return: The created command - :rtype: Command - """ - if not isinstance(role, Role): raise TypeError(f"Expected Role but got '{role.__class__.__name__}' instead") - data = { - 'server_id': int(server_id), - 'name': str(name), - 'role': role._value_, - 'chat': str(chat), - 'response': str(response), - 'run': str(run) - } - id = self._send_request('createCommand', data)['id'] - return Command(id, **data) - - def delete_command(self, command_id:int) -> None: - """ - Remove a command - - :param command_id: The id of the command to remove - :type command_id: int - """ - self._send_request('deleteCommand', { - 'id': int(command_id) - }) - - # Server functions - - def list_servers_by_owner(self, user_id:int) -> list[Server]: - """ - Get a list of all servers that this user owns +__version__ = '0.0.2' - :param user_id: The id of the user - :type user_id: int - :return: List of servers that this user owns - :rtype: list[Server] - """ - res = self._send_request('listServersByOwner', { - 'user_id': int(user_id) - })['Servers'] - return [Server(k, v) for k, v in res.items()] - - # TODO - Access denied - def list_servers_by_connection(self, connection_id:int): - res = self._send_request('listServersByConnection', { - 'connection_id': int(connection_id) - }) - return res - - # TODO Access denied - # (SERVER, ['name'], ['test']) - def update_server(self, server_id:int, field:list, value:list): - """ - Update this server - - :param server_id: The id of the server to update - :type server_id: int - :param field: The field to update - :type field: list - :param value: The value to update - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('updateServer', { - 'id': int(server_id), - 'field': field, - 'value': value - }) - - def start_server(self, server_id:int) -> None: - """ - Start this server - - :param server_id: The id of the server to start - :type server_id: int - """ - self._send_request('startServer', { - 'id': int(server_id) - }) - - def stop_server(self, server_id:int) -> None: - """ - Stop this server - - :param server_id: The id of the server to stop - :type server_id: int - """ - self._send_request('stopServer', { - 'id': int(server_id) - }) - - def restart_server(self, server_id:int) -> None: - """ - Restart this server - - :param server_id: The id of the server to restart - :type server_id: int - """ - self._send_request('restartServer', { - 'id': int(server_id) - }) - - def kill_server(self, server_id:int) -> None: - """ - Kill this server - - :param server_id: The id of the server to kill - :type server_id: int - """ - self._send_request('killServer', { - 'id': int(server_id) - }) - - def send_console_command(self, server_id:int, command:str) -> None: - """ - Send this command to console - - :param server_id: The id of the server to send the command to - :type server_id: int - :param command: The command to run - :type command: str - """ - self._send_request('sendConsoleCommand', { - 'server_id': int(server_id), - 'command': str(command) - }) - - def run_command(self, server_id:int, command_id:int, run_for:int=0) -> None: - """ - Run a command from id - - :param server_id: The id of the server that this command belongs to - :type server_id: int - :param command_id: The id of the command to run - :type command_id: int - :param run_for: The id of the player to run this command for, defaults to 0 - :type run_for: int, optional - """ - self._send_request('runCommand', { - 'server_id': int(server_id), - 'command_id': int(command_id), - 'run_for': int(run_for) - }) - - def clear_server_log(self, server_id:int) -> None: - """ - Clear the server log history - - :param server_id: The id of the server log to clear - :type server_id: int - """ - self._send_request('clearServerLog', { - 'id': int(server_id) - }) - - def clear_server_chat(self, server_id:int) -> None: - """ - Clear the server chat history - - :param server_id: The id of the server chat to clear - :type server_id: int - """ - self._send_request('clearServerChat', { - 'id': int(server_id) - }) - - def get_server(self, server_id:int) -> Server: - """ - Get the server from id - - :param server_id: The id of the server to get - :type server_id: int - :return: The server from id - :rtype: Server - """ - res = self._send_request('getServer', { - 'id': int(server_id) - }) - res['Server']['id'] = server_id - return Server.from_json(res['Server']) - - def get_server_status(self, server_id:int, player_list:bool=False) -> ServerStatus: - """ - Get the server's status - - :param server_id: The id of the server - :type server_id: int - :param player_list: Whether or not it should include a list of online players, defaults to False - :type player_list: bool, optional - :return: The ServerStatus - :rtype: ServerStatus - """ - if not isinstance(player_list, bool): raise TypeError(f"Expected bool but got '{player_list.__class__.__name__}' instead") - res = self._send_request('getServerStatus', { - 'id': int(server_id), - 'player_list': 1 if player_list else 0 - }) - return ServerStatus.from_json(res) - - def get_server_log(self, server_id:int) -> list[str]: - """ - Get the server's log - - :param server_id: The id of the server - :type server_id: int - :return: The server's log - :rtype: list[str] - """ - res = self._send_request('getServerLog', { - 'id': int(server_id) - }) - return [str(x['line']) for x in res] - - def get_server_chat(self, server_id:int) -> list[ChatMessage]: - """ - Get the server's chat - - :param server_id: The id of the server - :type server_id: int - :return: The server's chat - :rtype: list[ChatMessage] - """ - res = self._send_request('getServerChat', { - 'id': int(server_id) - }) - return [ChatMessage.from_json(x) for x in res] - - def get_server_resources(self, server_id:int) -> ServerResources: - """ - Get the server's resources - - :param server_id: The id of the server - :type server_id: int - :return: The server's resources - :rtype: ServerResources - """ - res = self._send_request('getServerResources', { - 'id': int(server_id) - }) - return ServerResources.from_json(res) - - # Schedule functions - - def list_schedules(self, server_id:int) -> list[Schedule]: - """ - Get a list of all schedules - - :param server_id: The id of the server - :type server_id: int - :return: List of schedules - :rtype: list[Schedule] - """ - res = self._send_request('listSchedules', { - 'server_id': int(server_id) - })['Schedules'] - return [Schedule(k,v) for k,v in res.items()] - - # TODO - Schedule does not have an attribute "" - # (SERVER, ['name'], ['test']) - def find_schedules(self, server_id:int, field:list, value:list): - """ - Get schedule from field-value - - :param server_id: The id of the server - :type server_id: int - :param field: The field to match - :type field: list - :param value: The value to match - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('findSchedules', { - 'server_id': int(server_id), - 'field': field, - 'value': value - }) - - def update_schedule(self, schedule_id:int, field:list, value:list): - """ - Update this schedule - - :param schedule_id: The id of the schedule to update - :type schedule_id: int - :param field: The field to update - :type field: list - :param value: The value to update - :type value: list - :return: _description_ - :rtype: _type_ - """ - return self._send_request('updateSchedule', { - 'id': int(schedule_id), - 'field': field, - 'value': value - }) - - def get_schedule(self, schedule_id:int) -> Schedule: - """ - Get the schedule from id - - :param schedule_id: The id of the schedule to get - :type schedule_id: int - :return: The schedule from id - :rtype: Schedule - """ - res = self._send_request('getSchedule', { - 'id': int(schedule_id) - })['Schedule'] - return Schedule.from_json(res) - - def create_schedule(self, server_id:int, name:str, ts:datetime.datetime, command:int, interval:int=0, status:ScheduleStatus=ScheduleStatus.scheduled, _for:int=0) -> Schedule: - """ - Create a new schedule - - :param server_id: The id of the server to create the schedule for - :type server_id: int - :param name: The name of the schedule - :type name: str - :param ts: The datetime to run this schedule - :type ts: datetime.datetime - :param command: The id of the command to run for this schedule - :type command: int - :param interval: Time (in seconds) before it runs this schedule again, defaults to 0 - :type interval: int, optional - :param status: The schedule's status, defaults to ScheduleStatus.scheduled - :type status: ScheduleStatus, optional - :param _for: The player to run this schedule for, defaults to 0 - :type _for: int, optional - :return: The created schedule - :rtype: Schedule - """ - if not isinstance(ts, datetime.datetime): raise TypeError(f"Expected datetime.datetime but got '{ts.__class__.__name__}' instead") - if not isinstance(status, ScheduleStatus): raise TypeError(f"Expected ScheduleStatus but got '{ts.__class__.__name__}' instead") - data = { - 'server_id': int(server_id), - 'name': str(name), - 'ts': int(ts.timestamp()), - 'interval': int(interval), - 'cmd':int(command), - 'status': status._value_, - 'for': int(_for) - } - id = self._send_request('createSchedule', data)['id'] - return Schedule(id, **data) - - def delete_schedule(self, schedule_id:int) -> None: - """ - Remove a schedule - - :param schedule_id: The id of the schedule to remove - :type schedule_id: int - """ - return self._send_request('deleteSchedule', { - 'id': int(schedule_id) - }) - - # Database functions - - def get_database_info(self, server_id:int) -> Database: - """ - Get info about the database - - :param server_id: The id of the server to get the database from - :type server_id: int - :return: Info about the database - :rtype: Database - """ - res = self._send_request('getDatabaseInfo', { - 'server_id': int(server_id) - }) - return Database.from_json(res) - - def create_database(self, server_id:int, name:str=None, password:str=None) -> Database: - """ - Create a new database (Limit of one database per server) - - :param server_id: The id of the server to create the database for - :type server_id: int - :param name: The name of the database (Autogenerated for some hosts), defaults to None - :type name: str, optional - :param password: The password of the database (Autogenerated for some hosts), defaults to None - :type password: str, optional - :return: The created database - :rtype: Database - """ - data = { - 'server_id': int(server_id), - 'name': str(name), - 'password': str(password) - } - res = self._send_request('createDatabase', data) - return Database.from_json(res) - - def change_database_password(self, server_id:int, database_id:int=None, password:str=None) -> Database: - """ - Change the database password - - :param server_id: The id of the server that the database belongs to - :type server_id: int - :param database_id: The id of the database (For some hosts this is the same as the server id), defaults to server_id if None - :type database_id: int - :param password: The new password for the database (Autogenerated for some hosts), defaults to None - :type password: str, optional - :return: The updated database with the new password - :rtype: Database - """ - res = self._send_request('changeDatabasePassword', { - 'server_id': int(server_id), - 'database_id': int(server_id) if database_id is None else int(database_id), - 'password': str(password) - }) - return Database.from_json(res) - - def delete_database(self, server_id:int, database_id:int=None) -> None: - """ - Remove a database - - :param server_id: The id of the server that this database belongs to - :type server_id: int - :param database_id: The id of the database (For some hosts this is the same as the server id), defaults to server_id if None - :type database_id: int - """ - self._send_request('deleteDatabase', { - 'server_id': int(server_id), - 'database_id': int(server_id) if database_id is None else int(database_id) - }) - - def start_server_backup(self, server_id:int) -> None: - """ - Starts a backup of the server - - :param server_id: The id of the server to backup - :type server_id: int - """ - self._send_request('startServerBackup', { - 'id': int(server_id) - }) - - def get_server_backup_status(self, server_id:int) -> Backup: - """ - Get the backup status of the server - - :param server_id: The id of the server - :type server_id: int - :return: The backup status of the server - :rtype: Backup - """ - res = self._send_request('getServerBackupStatus', { - 'id': int(server_id) - }) - return Backup.from_json(res) +from .hosts import * +from .model import * +from .api import MulticraftAPI +from .app import MulticraftApp diff --git a/multicraft/__pycache__/__init__.cpython-311.pyc b/multicraft/__pycache__/__init__.cpython-311.pyc index 2fb9cdf..dd51db2 100644 Binary files a/multicraft/__pycache__/__init__.cpython-311.pyc and b/multicraft/__pycache__/__init__.cpython-311.pyc differ diff --git a/multicraft/__pycache__/api.cpython-311.pyc b/multicraft/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000..fc6f78c Binary files /dev/null and b/multicraft/__pycache__/api.cpython-311.pyc differ diff --git a/multicraft/__pycache__/app.cpython-311.pyc b/multicraft/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..6d1f722 Binary files /dev/null and b/multicraft/__pycache__/app.cpython-311.pyc differ diff --git a/multicraft/__pycache__/model.cpython-311.pyc b/multicraft/__pycache__/model.cpython-311.pyc index 0a4fc80..3e63abb 100644 Binary files a/multicraft/__pycache__/model.cpython-311.pyc and b/multicraft/__pycache__/model.cpython-311.pyc differ diff --git a/multicraft/api.py b/multicraft/api.py new file mode 100644 index 0000000..0119d00 --- /dev/null +++ b/multicraft/api.py @@ -0,0 +1,844 @@ +import os +import datetime +import requests +import hmac +import hashlib +import json + +from . import MulticraftException, User, Role, Mode, Player, Command, Server, ServerStatus, ChatMessage, ServerResources, Schedule, ScheduleStatus, Database, Backup + +__all__ = ['MulticraftAPI'] + +class MulticraftAPI: + def __init__(self, url:str, user:str, key:str=None): + """ + Create a new instance of Multicraft API + + :param url: The API url. You can find common hosts in hosts.py + :type url: str + :param user: The username of the account to use + :type user: str + :param key: The API key to login with, defaults to os.getenv('KEY') if None + :type key: str, optional + """ + self.url = str(url) + self.user = str(user) + self.key = os.getenv('KEY') if key is None else str(key) + self.user_agent = 'multicraft (https://github.com/legopitstop/multicraft-py/)' + + # Internal + + def _generateSignature(self, params:dict) -> str: + signature = '' + for param in self._reduce(params): + signature += param + str(params[param]) + + return hmac.new(str(self.key).encode("UTF-8"), str(signature).encode(), hashlib.sha256 ).hexdigest() + + def _reduce(self, obj:dict) -> dict: + res = {} + for k, v in obj.items(): + if isinstance(v, list): res[k] = json.dumps(v) + else: res[k] = v + return res + + def _call(self, method:str, params:dict={}) -> dict: + """ + Internal method + """ + params = self._reduce(params) + params["_MulticraftAPIMethod"] = str(method) + params["_MulticraftAPIUser"] = self.user + params["_MulticraftAPIKey"] = self._generateSignature(params) + + res = requests.post(self.url, params, headers={'user-agent': self.user_agent}) + data = json.loads(res.text) + if data.get('success'): + return data.get('data') + err = str(data['errors'][0]).replace('"', '"') + raise MulticraftException(err) + + # Config + + def set_user_agent(self, user_agent_string:str): + """ + Set the User-Agent string to be used for all requests. + + :param user_agent_string: A string specifying the User-Agent header + :type user_agent_string: str + """ + self.user_agent = str(user_agent_string) + + # User functions + + def get_current_user(self) -> User: + """ + Get the current user + + :return: The current user + :rtype: User + """ + res = self._call('getCurrentUser') + return User.from_json(res['User']) + + def get_user_role(self, user_id:int, server_id:int) -> Role|None: + """ + Get the users role + + :param user_id: The id of the user to get + :type user_id: int + :param server_id: The id of the server that this user belongs to + :type server_id: int + :return: The users role + :rtype: Role|None + """ + data ={ + 'user_id': int(user_id), + 'server_id': int(server_id) + } + res = self._call('getUserRole', data)['role'] + if res == '': return None + return Role[str(res)] + + def set_user_role(self, user_id:int, server_id:int, role:Role) -> None: + """ + Sets the users role + + :param user_id: The id of the user to set + :type user_id: int + :param server_id: The id of the server that this user belongs to + :type server_id: int + :param role: The role to set + :type role: Role + """ + + if not isinstance(role, Role) and role is not None: raise TypeError(f"Expected Role but got '{role.__class__.__name__}' instead") + data = { + 'user_id': int(user_id), + 'server_id': int(server_id), + 'role': '' if role is None else role._value_ + } + self._call('setUserRole', data) + + def get_user_ftp_access(self, user_id:int, server_id:int) -> Mode|None: + """ + Get the users ftp access + + :param user_id: The id of the user to get + :type user_id: int + :param server_id: The id of the server that this user belongs to + :type server_id: int + :return: The users mode + :rtype: Mode|None + """ + res = self._call('getUserFtpAccess', { + 'user_id': int(user_id), + 'server_id': int(server_id) + })['mode'] + if res == '': return None + return Mode[str(res)] + + def set_user_ftp_access(self, user_id:int, server_id:int, mode:Mode) -> None: + """ + Sets the users ftp access + + :param user_id: The id of the user to set + :type user_id: int + :param server_id: The id of the server that this user belongs to + :type server_id: int + :param mode: The mode to set + :type mode: Mode + """ + if not isinstance(mode, Mode) and mode is not None: raise TypeError(f"Expected Mode but got '{mode.__class__.__name__}' instead") + self._call('setUserFtpAccess', { + 'user_id': int(user_id), + 'server_id': int(server_id), + 'mode': '' if mode is None else mode._value_ + }) + + def get_user_id(self, name:str) -> int: + """ + Get the users id from name + + :param name: The name of the user + :type name: str + :return: The id of the user + :rtype: int + """ + return self._call('getUserId', { + 'name': str(name) + })['id'] + + # TODO - Incorrect API key from user: USERNAME + def get_own_api_key(self, password:str, generate=0, gauth_code:str=''): + """ + _summary_ + + :param password: _description_ + :type password: str + :param generate: _description_, defaults to 0 + :type generate: int, optional + :param gauth_code: _description_, defaults to '' + :type gauth_code: str, optional + :return: _description_ + :rtype: _type_ + """ + return self._call('getOwnApiKey', { + 'password': str(password), + 'generate': str(generate), + 'gauth_code': str(gauth_code) + }) + + # Player functions + + def list_players(self, server_id:int) -> dict[str, str]: + """ + Get a list of all players + + :param server_id: The id of the server + :type server_id: int + :return: The players {ID: NAME} + :rtype: dict + """ + res = self._call('listPlayers', { + 'server_id': int(server_id) + })['Players'] + return {} if isinstance(res, list) else res + + def get_player(self, player_id:int) -> Player: + """ + Get the player from id + + :param player_id: The id of the player to get + :type player_id: int + :return: The player from id + :rtype: Player + """ + res = self._call('getPlayer', { + 'id': int(player_id) + })['Player'] + return Player.from_json(res) + + def find_players(self, server_id:int, field:list[str], value:list[str]) -> dict[str, str]: + """ + Get player from field-value + + Fields: name, lastseen, ip, banned, status, previps, quitreason, op + + :param server_id: The id of the server + :type server_id: int + :param field: The field to match + :type field: list + :param value: The value to match + :type value: list + :return: The found players {ID: NAME} + :rtype: dict + """ + res = self._call('findPlayers', { + 'server_id': int(server_id), + 'field': field, + 'value': value + })['Players'] + return {} if isinstance(res, list) else res + + def update_player(self, player_id:int, field:list[str], value:list[str]) -> None: + """ + Update this player + + :param player_id: The id of the player to update + :type player_id: int + :param field: The field to update + :type field: list + :param value: The value to update + :type value: list + """ + self._call('updatePlayer', { + 'id': int(player_id), + 'field': field, + 'value': value + }) + + def create_player(self, server_id:int, name:str, op_command:int=0) -> Player: + """ + Create a new player + + :param server_id: The id of the server to create the player for + :type server_id: int + :param name: The name of the player to create + :type name: str + :param op_command: The op command, defaults to 0 + :type op_command: int, optional + :return: The created player + :rtype: Player + """ + data = { + 'server_id': int(server_id), + 'name': str(name), + 'op_command': int(op_command) + } + id = self._call('createPlayer', data)['id'] + return Player(id, **data) + + def delete_player(self, player_id:int): + """ + Remove a player + + :param player_id: The id of the player to remove + :type player_id: int + :return: _description_ + :rtype: _type_ + """ + return self._call('deletePlayer', { + 'id': int(player_id) + }) + + def assign_player_to_user(self, player_id:int, user_id:int) -> None: + """ + Assign a player to a user + + :param player_id: The id of the player to assign + :type player_id: int + :param user_id: The id of the user to assign + :type user_id: int + """ + self._call('assignPlayerToUser', { + 'player_id': int(player_id), + 'user_id': int(user_id) + }) + + # Command functions + + def list_commands(self, server_id:int) -> dict[str,str]: + """ + Get a list of all commands + + :param server_id: The id of the server + :type server_id: int + :return: The commands {ID: NAME} + :rtype: dict + """ + res = self._call('listCommands', { + 'server_id': int(server_id) + })['Commands'] + return {} if isinstance(res, list) else res + + def find_commands(self, server_id:int, field:list[str], value:list[str]) -> dict[int, str]: + """ + Get command from field-value + + Fields: name, chat, response, run, prereq + + :param server_id: The id of the server + :type server_id: int + :param field: The field to match + :type field: list + :param value: The value to match + :type value: list + :return: The found commands {ID: NAME} + :rtype: dict + """ + res = self._call('findCommands', { + 'server_id': int(server_id), + 'field': field, + 'value': value + })['Commands'] + return {} if isinstance(res, list) else res + + def get_command(self, command_id:int) -> Command: + """ + Get the command from id + + :param command_id: The id of the command to get + :type command_id: int + :return: The command from id + :rtype: Command + """ + res = self._call('getCommand', { + 'id': int(command_id) + })['Command'] + return Command.from_json(res) + + def update_command(self, command_id:int, field:list[str], value:list[str]) -> None: + """ + Update this command + + :param command_id: The id of the command to update + :type command_id: int + :param field: The field to update + :type field: list + :param value: The value to update + :type value: list + """ + self._call('updateCommand', { + 'id': int(command_id), + 'field': field, + 'value': value + }) + + def create_command(self, server_id:int, name:str, role:Role, chat:str, response:str, run:str) -> Command: + """ + Create a new command + + :param server_id: The id of the server to create the command for + :type server_id: int + :param name: The name of the command + :type name: str + :param role: The role of the command + :type role: Role + :param chat: The chat command to run + :type chat: str + :param response: The response text + :type response: str + :param run: The command to run + :type run: str + :return: The created command + :rtype: Command + """ + if not isinstance(role, Role): raise TypeError(f"Expected Role but got '{role.__class__.__name__}' instead") + data = { + 'server_id': int(server_id), + 'name': str(name), + 'role': role._value_, + 'chat': str(chat), + 'response': str(response), + 'run': str(run) + } + id = self._call('createCommand', data)['id'] + return Command(id, **data) + + def delete_command(self, command_id:int) -> None: + """ + Remove a command + + :param command_id: The id of the command to remove + :type command_id: int + """ + self._call('deleteCommand', { + 'id': int(command_id) + }) + + # Server functions + + def list_servers_by_owner(self, user_id:int) -> dict[str,str]: + """ + Get a list of all servers that this user owns + + :param user_id: The id of the user + :type user_id: int + :return: The servers that this user owns {ID: NAME} + :rtype: dict + """ + res = self._call('listServersByOwner', { + 'user_id': int(user_id) + })['Servers'] + return {} if isinstance(res, list) else res + + # TODO - Access denied + def list_servers_by_connection(self, connection_id:int): + res = self._call('listServersByConnection', { + 'connection_id': int(connection_id) + }) + return res + + # TODO Access denied + def update_server(self, server_id:int, field:list[str], value:list[str]): + """ + Update this server + + :param server_id: The id of the server to update + :type server_id: int + :param field: The field to update + :type field: list + :param value: The value to update + :type value: list + :return: The updated server + :rtype: Server + """ + return self._call('updateServer', { + 'id': int(server_id), + 'field': field, + 'value': value + }) + + def start_server(self, server_id:int) -> None: + """ + Start this server + + :param server_id: The id of the server to start + :type server_id: int + """ + self._call('startServer', { + 'id': int(server_id) + }) + + def stop_server(self, server_id:int) -> None: + """ + Stop this server + + :param server_id: The id of the server to stop + :type server_id: int + """ + self._call('stopServer', { + 'id': int(server_id) + }) + + def restart_server(self, server_id:int) -> None: + """ + Restart this server + + :param server_id: The id of the server to restart + :type server_id: int + """ + self._call('restartServer', { + 'id': int(server_id) + }) + + def kill_server(self, server_id:int) -> None: + """ + Kill this server + + :param server_id: The id of the server to kill + :type server_id: int + """ + self._call('killServer', { + 'id': int(server_id) + }) + + def send_console_command(self, server_id:int, command:str) -> None: + """ + Send this command to console + + :param server_id: The id of the server to send the command to + :type server_id: int + :param command: The command to run + :type command: str + """ + self._call('sendConsoleCommand', { + 'server_id': int(server_id), + 'command': str(command) + }) + + def run_command(self, server_id:int, command_id:int, run_for:int=0) -> None: + """ + Run a command from id + + :param server_id: The id of the server that this command belongs to + :type server_id: int + :param command_id: The id of the command to run + :type command_id: int + :param run_for: The id of the player to run this command for, defaults to 0 + :type run_for: int, optional + """ + self._call('runCommand', { + 'server_id': int(server_id), + 'command_id': int(command_id), + 'run_for': int(run_for) + }) + + def clear_server_log(self, server_id:int) -> None: + """ + Clear the server log history + + :param server_id: The id of the server log to clear + :type server_id: int + """ + self._call('clearServerLog', { + 'id': int(server_id) + }) + + def clear_server_chat(self, server_id:int) -> None: + """ + Clear the server chat history + + :param server_id: The id of the server chat to clear + :type server_id: int + """ + self._call('clearServerChat', { + 'id': int(server_id) + }) + + def get_server(self, server_id:int) -> Server: + """ + Get the server from id + + :param server_id: The id of the server to get + :type server_id: int + :return: The server from id + :rtype: Server + """ + res = self._call('getServer', { + 'id': int(server_id) + }) + res['Server']['id'] = server_id + return Server.from_json(res['Server']) + + def get_server_status(self, server_id:int, player_list:bool=False) -> ServerStatus: + """ + Get the server's status + + :param server_id: The id of the server + :type server_id: int + :param player_list: Whether or not it should include a list of online players, defaults to False + :type player_list: bool, optional + :return: The ServerStatus + :rtype: ServerStatus + """ + if not isinstance(player_list, bool): raise TypeError(f"Expected bool but got '{player_list.__class__.__name__}' instead") + res = self._call('getServerStatus', { + 'id': int(server_id), + 'player_list': 1 if player_list else 0 + }) + return ServerStatus.from_json(res) + + def get_server_log(self, server_id:int) -> list[str]: + """ + Get the server's log + + :param server_id: The id of the server + :type server_id: int + :return: The server's log + :rtype: list[str] + """ + res = self._call('getServerLog', { + 'id': int(server_id) + }) + return [str(x['line']) for x in res] + + def get_server_chat(self, server_id:int) -> list[ChatMessage]: + """ + Get the server's chat + + :param server_id: The id of the server + :type server_id: int + :return: The server's chat + :rtype: list[ChatMessage] + """ + res = self._call('getServerChat', { + 'id': int(server_id) + }) + return [ChatMessage.from_json(x) for x in res] + + def get_server_resources(self, server_id:int) -> ServerResources: + """ + Get the server's resources + + :param server_id: The id of the server + :type server_id: int + :return: The server's resources + :rtype: ServerResources + """ + res = self._call('getServerResources', { + 'id': int(server_id) + }) + return ServerResources.from_json(res) + + # Schedule functions + + def list_schedules(self, server_id:int) -> dict[str, str]: + """ + Get a list of all schedules + + :param server_id: The id of the server + :type server_id: int + :return: The schedules {ID: NAME} + :rtype: dict + """ + res = self._call('listSchedules', { + 'server_id': int(server_id) + })['Schedules'] + return {} if isinstance(res, list) else res + + def find_schedules(self, server_id:int, field:list[str], value:list[str]) -> dict[str,str]: + """ + Get schedule from field-value + + Fields: name, interval, status, command, args + + :param server_id: The id of the server + :type server_id: int + :param field: The field to match + :type field: list + :param value: The value to match + :type value: list + :return: The found schedules {ID: NAME} + :rtype: dict + """ + res = self._call('findSchedules', { + 'server_id': int(server_id), + 'field': field, + 'value': value + })['Schedules'] + return {} if isinstance(res, list) else res + + def update_schedule(self, schedule_id:int, field:list[str], value:list[str]) -> None: + """ + Update this schedule + + :param schedule_id: The id of the schedule to update + :type schedule_id: int + :param field: The field to update + :type field: list + :param value: The value to update + :type value: list + """ + self._call('updateSchedule', { + 'id': int(schedule_id), + 'field': field, + 'value': value + }) + + def get_schedule(self, schedule_id:int) -> Schedule: + """ + Get the schedule from id + + :param schedule_id: The id of the schedule to get + :type schedule_id: int + :return: The schedule from id + :rtype: Schedule + """ + res = self._call('getSchedule', { + 'id': int(schedule_id) + })['Schedule'] + return Schedule.from_json(res) + + def create_schedule(self, server_id:int, name:str, ts:datetime.datetime, command:int, interval:int=0, status:ScheduleStatus=ScheduleStatus.scheduled, _for:int=0) -> Schedule: + """ + Create a new schedule + + :param server_id: The id of the server to create the schedule for + :type server_id: int + :param name: The name of the schedule + :type name: str + :param ts: The datetime to run this schedule + :type ts: datetime.datetime + :param command: The id of the command to run for this schedule + :type command: int + :param interval: Time (in seconds) before it runs this schedule again, defaults to 0 + :type interval: int, optional + :param status: The schedule's status, defaults to ScheduleStatus.scheduled + :type status: ScheduleStatus, optional + :param _for: The player to run this schedule for, defaults to 0 + :type _for: int, optional + :return: The created schedule + :rtype: Schedule + """ + if not isinstance(ts, datetime.datetime): raise TypeError(f"Expected datetime.datetime but got '{ts.__class__.__name__}' instead") + if not isinstance(status, ScheduleStatus): raise TypeError(f"Expected ScheduleStatus but got '{ts.__class__.__name__}' instead") + data = { + 'server_id': int(server_id), + 'name': str(name), + 'ts': int(ts.timestamp()), + 'interval': int(interval), + 'cmd':int(command), + 'status': status._value_, + 'for': int(_for) + } + id = self._call('createSchedule', data)['id'] + return Schedule(id, **data) + + def delete_schedule(self, schedule_id:int) -> None: + """ + Remove a schedule + + :param schedule_id: The id of the schedule to remove + :type schedule_id: int + """ + return self._call('deleteSchedule', { + 'id': int(schedule_id) + }) + + # Database functions + + def get_database_info(self, server_id:int) -> Database: + """ + Get info about the database + + :param server_id: The id of the server to get the database from + :type server_id: int + :return: Info about the database + :rtype: Database + """ + res = self._call('getDatabaseInfo', { + 'server_id': int(server_id) + }) + return Database.from_json(res) + + def create_database(self, server_id:int, name:str=None, password:str=None) -> Database: + """ + Create a new database (Limit of one database per server) + + :param server_id: The id of the server to create the database for + :type server_id: int + :param name: The name of the database (Autogenerated for some hosts), defaults to None + :type name: str, optional + :param password: The password of the database (Autogenerated for some hosts), defaults to None + :type password: str, optional + :return: The created database + :rtype: Database + """ + data = { + 'server_id': int(server_id), + 'name': str(name), + 'password': str(password) + } + res = self._call('createDatabase', data) + return Database.from_json(res) + + def change_database_password(self, server_id:int, database_id:int=None, password:str=None) -> Database: + """ + Change the database password + + :param server_id: The id of the server that the database belongs to + :type server_id: int + :param database_id: The id of the database (For some hosts this is the same as the server id), defaults to server_id if None + :type database_id: int + :param password: The new password for the database (Autogenerated for some hosts), defaults to None + :type password: str, optional + :return: The updated database with the new password + :rtype: Database + """ + res = self._call('changeDatabasePassword', { + 'server_id': int(server_id), + 'database_id': int(server_id) if database_id is None else int(database_id), + 'password': str(password) + }) + return Database.from_json(res) + + def delete_database(self, server_id:int, database_id:int=None) -> None: + """ + Remove a database + + :param server_id: The id of the server that this database belongs to + :type server_id: int + :param database_id: The id of the database (For some hosts this is the same as the server id), defaults to server_id if None + :type database_id: int + """ + self._call('deleteDatabase', { + 'server_id': int(server_id), + 'database_id': int(server_id) if database_id is None else int(database_id) + }) + + def start_server_backup(self, server_id:int) -> None: + """ + Starts a backup of the server + + :param server_id: The id of the server to backup + :type server_id: int + """ + self._call('startServerBackup', { + 'id': int(server_id) + }) + + def get_server_backup_status(self, server_id:int) -> Backup: + """ + Get the backup status of the server + + :param server_id: The id of the server + :type server_id: int + :return: The backup status of the server + :rtype: Backup + """ + res = self._call('getServerBackupStatus', { + 'id': int(server_id) + }) + return Backup.from_json(res) diff --git a/multicraft/app.py b/multicraft/app.py new file mode 100644 index 0000000..78c14f2 --- /dev/null +++ b/multicraft/app.py @@ -0,0 +1,135 @@ +""" +A bear boned multicraft client UI +""" +from tkinter import Tk, Event, Label, Listbox, Frame, Button + +from . import MulticraftAPI + +__all__ = ['MulticraftApp'] + +pages = [] + +def page(cls): + def wrapper(): + global pages + pages.append(cls) + return wrapper() + +class Page: + def __init__(self, frame, api:MulticraftAPI, name): + self.frame = frame + self.api = api + self.name = name + + frm = Frame(frame) + self._nav_lbl = Label(frm, text='Servers > legopitstop\'s Minecraft Server') + self._nav_lbl.grid(row=0, column=0, sticky='nwe') + frm.grid(row=0, column=0, sticky='ew') + +@page +class HomePage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Home') + self._status_lbl = Label(frame, text='P') + self._start_btn = Button(frame, text='Start', command=lambda s=self.api.server: self.api.start_server(s)) + self._stop_btn = Button(frame, text='Stop', command=lambda s=self.api.server: self.api.stop_server(s)) + self._restart_btn = Button(frame, text='Restart', command=lambda s=self.api.server: self.api.restart_server(s)) + self._kill_btn = Button(frame, text='Kill', command=lambda s=self.api.server: self.api.kill_server(s)) + + self._status_lbl.grid(row=1, column=0) + self._start_btn.grid(row=1, column=1) + self._stop_btn.grid(row=1, column=2) + self._restart_btn.grid(row=1, column=3) + self._kill_btn.grid(row=1, column=4) + +@page +class ConsolePage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Console') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class ChatPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Chat') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class PlayersPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Players') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class BackupManagerPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Backup Manager') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class CommandsPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Commands') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class ScheduledTasksPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Scheduled Tasks') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class UsersPage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'Users') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +@page +class DatabasePage(Page): + def __init__(self, frame, api): + super().__init__(frame, api, 'MySQL Database') + Label(frame, text=self.name).grid(row=0, column=0, sticky='nw') + +# App +class MulticraftApp(Tk, MulticraftAPI): + def __init__(self, url:str, user:str, server:int, key:str=None): + Tk.__init__(self) + self.api = MulticraftAPI(url, user, key) + self.api.server = server + self.title('multicraft') + self.geometry('700x600') + + self.pages = [] + + # Widgets + self._pages = Listbox(self) + self._pages.grid(row=0, column=0, sticky='nsw') + self._pages.bind('<>', self._listbox_select) + + # Responsive + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + + def _listbox_select(self, e:Event): + cur = self._pages.curselection() + if len(cur) == 0: return + self.page(cur[0]) + + def page(self, page_id:int): + for page in self.pages: page.frame.forget() + p = self.pages[page_id] + p.frame.grid(row=0, column=1, sticky='nesw') + + def mainloop(self): + global pages + for p in pages: + frame = Frame(self) + frame.grid_rowconfigure(1, weight=1) + frame.grid_columnconfigure(0, weight=1) + page = p(frame, self.api) + self._pages.insert('end', page.name) + self.pages.append(page) + self.page(0) + super().mainloop() + + diff --git a/multicraft/model.py b/multicraft/model.py index c4119c2..b0ecddd 100644 --- a/multicraft/model.py +++ b/multicraft/model.py @@ -512,6 +512,38 @@ def from_json(cls, data:dict): if 'players' in data: self.players = data.pop('players') return self +class ServerResources: + def __init__(self, cpu:float, memory:float, quota:int): + setattr(self, '_cpu', cpu) + setattr(self, '_memory', memory) + setattr(self, '_quota', quota) + + def __repr__(self): + return str(self) + + def __str__(self) -> str: + return f"ServerResources(cpu={self.cpu}, memory={self.memory}, quota={self.quota})" + + @property + def cpu(self) -> float: + return getattr(self, '_cpu') + + @property + def memory(self) -> float: + return getattr(self, '_memory') + + @property + def quota(self) -> int: + return getattr(self, '_quota') + + @classmethod + def from_json(cls, data:dict): + self = cls.__new__(cls) + setattr(self, '_cpu', float(data.get('cpu'))) + setattr(self, '_memory', float(data.get('memory'))) + setattr(self, '_quota', int(data.get('quota'))) + return self + class ChatMessage: def __init__(self, text:str, name:str, time:datetime): self.text = text @@ -736,38 +768,6 @@ def from_json(cls, data:dict): def delete(self): raise NotImplementedError() -class ServerResources: - def __init__(self, cpu:float, memory:float, quota:int): - setattr(self, '_cpu', cpu) - setattr(self, '_memory', memory) - setattr(self, '_quota', quota) - - def __repr__(self): - return str(self) - - def __str__(self) -> str: - return f"ServerResources(cpu={self.cpu}, memory={self.memory}, quota={self.quota})" - - @property - def cpu(self) -> float: - return getattr(self, '_cpu') - - @property - def memory(self) -> float: - return getattr(self, '_memory') - - @property - def quota(self) -> int: - return getattr(self, '_quota') - - @classmethod - def from_json(cls, data:dict): - self = cls.__new__(cls) - setattr(self, '_cpu', float(data.get('cpu'))) - setattr(self, '_memory', float(data.get('memory'))) - setattr(self, '_quota', int(data.get('quota'))) - return self - class Schedule: def __init__(self, id:int, name:str, server_id:int=None, scheduled_ts:datetime.datetime=None, last_run_ts:datetime.datetime=None, interval:float=None, command:int=None, run_for:int=None, status:int=None, args:str=None, hidden:bool=None, **kw): self.id = id @@ -1098,4 +1098,3 @@ def from_json(cls, data:dict): if 'file' in data: self.file = data.pop('file') if 'time' in data: self.time = data.pop('time') return self - diff --git a/setup.py b/setup.py index cf3d8db..31b107c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setuptools.setup( name='multicraft', - version='0.0.1', + version='0.0.2', author='Legopitstop', description='Interact with your Minecraft server from hosts that use Multicraft using Python', long_description=long_description,