From 09546dcd261f9ee85a14ae243c142589a4f848ff Mon Sep 17 00:00:00 2001 From: skelmis Date: Thu, 27 Aug 2020 23:35:26 +1200 Subject: [PATCH] Add examples usages. --- examples/Parsers.py | 75 ++++++++++++++ examples/Player.py | 232 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 examples/Parsers.py create mode 100644 examples/Player.py diff --git a/examples/Parsers.py b/examples/Parsers.py new file mode 100644 index 0000000..99c7571 --- /dev/null +++ b/examples/Parsers.py @@ -0,0 +1,75 @@ +import re +import json + +""" +A file for Player utilities, focused around parsing chat and making it human readable. + +The DefaultParser should be able to handle most situations currently, +however, there are known weakness's in the approach but as it stands, +it is better then other examples I have seen. + +DefaultParser - Tested on mc-central, should work decent globally +""" + +# TODO Parse banner messages, example: https://gyazo.com/c0a4cfee23a31fe8b6e4c7c7848e5e5a + + +def DefaultParser(data): + try: + # Convert to valid python dict + data = json.loads(data) + + # Create the prefix & text + prefixing = True + data = data["extra"] + stringDict = {"prefix": [], "message": []} + dm = False + + if isinstance(data[len(data) - 1], str): + # Given the last item is a string, rather then dictionary + # we can safely assume that this is in fact a /msg + dm = True + + for i, item in enumerate(data): + # Remove minecraft character stuff + if dm and i == len(data) - 1: + stringDict["message"].append(item) + continue + + text = re.sub( + r"\§c|\§f|\§b|\§d|\§a|\§1|\§2|\§3|\§4|\§5|\§6|\§7|\§8|\§9|\§0", + "", + item["text"], + ) + + if text.lstrip().rstrip() == ":" and prefixing: + # No longer need to handle the before message + prefixing = False + continue + elif prefixing: + stringDict["prefix"].append(text) + elif not prefixing: + if "extra" in item: + # Chat parsing for text means this is most likely another nested dict in list situation + if len(item["extra"]) > 0: + if "text" in item["extra"][0]: + text = item["extra"][0]["text"] + stringDict["message"].append(text) + + prefix = "".join(stringDict["prefix"]) + text = " ".join(stringDict["message"]).rstrip().lstrip() + + if len(prefix) > 0 and len(text) > 0: + message = ": ".join([prefix, text]) + elif len(prefix) > 0: + message = prefix + elif len(text) > 0: + message = text + + message = message.lstrip().rstrip() + + return message + + except Exception as e: + # print(f"Unable to parse: {data}\nException: {e}") + return False diff --git a/examples/Player.py b/examples/Player.py new file mode 100644 index 0000000..a88ad7e --- /dev/null +++ b/examples/Player.py @@ -0,0 +1,232 @@ +import re +import time +import asyncio +from concurrent.futures.thread import ThreadPoolExecutor + +from minecraft import authentication +from minecraft.exceptions import YggdrasilError +from minecraft.networking.connection import Connection +from minecraft.networking.packets import serverbound, clientbound + +from Parsers import DefaultParser + + +class Player: + """ + A class built to handle all required actions to maintain: + - Gaining auth tokens, and connecting to online minecraft servers. + - Clientbound chat + - Serverbound chat + + Warnings + -------- + This class explicitly expects a username & password, then expects to + be able to connect to a server in online mode. + If you wish to add different functionality please + """ + + def __init__(self, username, password, *, admins=None): + """ + Initialize things such as kickout, admins and auth + + Parameters + ---------- + username : String + Used for authentication + password : String + Used for authentication + admins : list, optional + The minecraft accounts to auto accept tpa's requests from + + Raises + ------ + YggdrasilError : minecraft.exceptions + Username or Password was incorrect + + """ + self.kickout = False + self.admins = [] if admins is None else admins + + self.auth_token = authentication.AuthenticationToken() + self.auth_token.authenticate(username, password) + + def Parser(self, data): + """ + Converts the chat packet received from the server + into human readable strings + + Parameters + ---------- + data : JSON + The chat data json receive from the server + + Returns + ------- + message : String + The text received from the server in human readable form + + """ + message = DefaultParser(data) # This is where you would call other parsers + + if not message: + return False + + if "teleport" in message.lower(): + self.HandleTpa(message) + + return message + + def HandleTpa(self, message): + """ + Using the given message, figure out whether or not to accept the tpa + + Parameters + ---------- + message : String + The current chat, where 'tpa' was found in message.lower() + + """ + try: + found = re.search( + "(.+?) has requested that you teleport to them.", message + ).group(1) + if found in self.admins: + self.SendChat("/tpyes") + return + except AttributeError: + pass + + try: + found = re.search("(.+?) has requested to teleport to you.", message).group( + 1 + ) + if found in self.admins: + self.SendChat("/tpyes") + return + except AttributeError: + pass + + def SendChat(self, msg): + """ + Send a given message to the server + + Parameters + ---------- + msg : String + The message to send to the server + + """ + msg = str(msg) + if len(msg) > 0: + packet = serverbound.play.ChatPacket() + packet.message = msg + self.connection.write_packet(packet) + + def ReceiveChat(self, chat_packet): + """ + The listener for ClientboundChatPackets + + Parameters + ---------- + chat_packet : ClientboundChatPacket + The incoming chat packet + chat_packet.json : JSON + The chat packet to pass of to our Parser for handling + + """ + message = self.Parser(chat_packet.json_data) + if not message: + # This means our Parser failed lol + print("Parser failed") + return + + print(message) + + def SetServer(self, ip, port=25565, handler=None): + """ + Sets the server, ready for connection + + Parameters + ---------- + ip : str + The server to connect to + port : int, optional + The port to connect on + handler : Function pointer, optional + Points to the function used to handle Clientbound chat packets + + """ + handler = handler or self.ReceiveChat + + self.ip = ip + self.port = port + self.connection = Connection( + ip, port, auth_token=self.auth_token, handle_exception=print + ) + + self.connection.register_packet_listener( + handler, clientbound.play.ChatMessagePacket + ) + + self.connection.exception_handler(print) + + def Connect(self): + """ + Actually connect to the server for this player and maintain said connection + + """ + self.connection.connect() + + print(f"Connected to server with: {self.auth_token.username}") + + count = 0 + while True: + time.sleep(1) + count += 1 + if self.kickout: + break + elif count == 50: + break + + def Disconnect(self): + """ + In order to disconnect the client, and break the blocking loop + this method must be called + + """ + self.kickout = True + self.connection.disconnect() + + +async def Main(): + try: + player = Player("Account Email/Username", "Account Password") + except YggdrasilError as e: + # Authentication Error + print("Incorrect Login", e) + return + + player.SetServer("Server to connect to.") + + # We do this to ensure it is non blocking as Connect() is a + # forever loop used to maintain a connection to a server + executor = ThreadPoolExecutor() + executor.submit(player.Connect) + + # Forever do things unless the user wants us to logout + while True: + message = input("What should I do/say?\n") + + # Disconnect the client from the server before finishing everything up + if message.lower() in ["logout", "disconnected", "exit"]: + player.Disconnect() + print("Disconnected") + return + + # Send the message to the server via the player + player.SendChat(message) + + +# Simply run our program +if __name__ == "__main__": + asyncio.run(Main())