commit 5d75c332d6e6965642ce875a89ca259c7a898166 Author: Malasaur Date: Sun Dec 14 16:32:01 2025 +0100 Genesis commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92afa22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +venv/ diff --git a/libminecraft.py b/libminecraft.py new file mode 100644 index 0000000..4b5918f --- /dev/null +++ b/libminecraft.py @@ -0,0 +1,111 @@ +import asyncio +import atexit +from re import compile +from typing import Any, Callable + +from aiohttp import ClientSession + + +class Minecraft: + def __init__(self, host: str, port: int, password: str): + self.host = host.rstrip("/") + self.port = port + self.headers = {"Authorization": password} + self.session = ClientSession() + atexit.register(self.exit) + self._hooks: list[Callable] = [] + + def exit(self): + asyncio.run(self.session.close()) + + async def _get(self, method: str, **json: Any): + async with self.session.get( + f"{self.host}:{self.port}{method}", json=json, headers=self.headers + ) as response: + if response.status == 200: + return await response.json() + raise ConnectionError( + f"Error while sending {method} to Server: HTTP code {response.status}." + ) + + async def _stream(self, method: str, **json: Any): + async with self.session.get( + f"{self.host}:{self.port}{method}", json=json, headers=self.headers + ) as response: + if response.status == 200: + async for chunk, _ in response.content.iter_chunks(): + yield chunk + else: + raise ConnectionError( + f"Error while sending {method} to Server: HTTP code {response.status}." + ) + + async def start(self): + return await self._get("/start") + + async def status(self): + return await self._get("/status") + + async def stop(self, countdown: int = 60, reason: str = "", timeout: int = 10): + return await self._get( + "/stop", countdown=countdown, reason=reason, timeout=timeout + ) + + async def restart(self, countdown: int = 60, reason: str = "", timeout: int = 10): + return await self._get( + "/restart", countdown=countdown, reason=reason, timeout=timeout + ) + + async def maintainance( + self, countdown: int = 60, reason: str = "", timeout: int = 10 + ): + return await self._get( + "/maintainance", countdown=countdown, reason=reason, timeout=timeout + ) + + async def command(self, command: str): + return await self._get("/command", command=command) + + async def logs_stream(self): + async for chunk in self._stream("/logs/stream"): + yield chunk.decode().strip() + + async def logs_tail(self, back: int = 10): + async for chunk in self._stream("/logs/tail", back=back): + yield chunk.decode().strip() + + def onConsoleLog(self, pattern: str | None = None) -> Callable: + def wrapper(func: Callable): + compiled = compile(pattern) if pattern else None + + async def callback(line: str): + if compiled is None: + func(line) + return + match = compiled.fullmatch(line) + if match is None: + return + groups = match.groups() + if groups: + func(*groups) + else: + func(line) + + self._hooks.append(callback) + + return func + + return wrapper + + def onPlayerChat(self) -> Callable: + def wrapper(func: Callable): + return self.onConsoleLog( + r"\[[0-9]{2}:[0-9]{2}:[0-9]{2}\] \[Server thread\/INFO\]: <(.*)> (.*)" + )(func) + + return wrapper + + async def loop(self): + async for line in self.logs_stream(): + for callback in self._hooks: + await callback(line) diff --git a/libsignal.py b/libsignal.py new file mode 100644 index 0000000..91fa167 --- /dev/null +++ b/libsignal.py @@ -0,0 +1,202 @@ +import asyncio +import atexit +from json import loads +from time import time +from typing import Callable +from uuid import uuid4 + +from aiohttp import ClientSession +from sseclient import SSEClient + + +class User: + def __init__( + self, + name: str, + number: str, + id: str, + first_nickname: str | None = None, + last_nickname: str | None = None, + note: str | None = None, + ): + """Creates a new User. + Args: + name: The User's username. + number: The User's phone number. + id: The User's Signal UUID. + first_nickname: + last_nickname: + note: + """ + + self.name = name + self.number = number + self.id = id + self.first_nickname = first_nickname + self.last_nickname = last_nickname + self.note = note + + def __repr__(self) -> str: + return f'' + + +class Group: + def __init__(self, name: str, id: str): + """Creates a new Group. + Args: + name: the Group's name. + id: the Group's Signal UUID. + """ + + self.name = name + self.id = id + + def __repr__(self) -> str: + return f'' + + +class ReceivedMessage: + def __init__(self, message: str, user: User, group: Group | None): + """Creates a new ReceivedMessage. + Args: + message: The Message's body. + user: The User that sent the Message. + group: The Group where the message was sent on, or None for a direct Message. + """ + + self.message = message + self.user = user + self.group = group + + def __repr__(self) -> str: + return f'' + + +class Signal: + def __init__(self, host: str, port: int): + self.url = host.rstrip("/") + ":" + str(port) + self.session = ClientSession() + atexit.register(self.exit) + self._hooks: dict[str, Callable] = {} + self.usersCache = {} + self.usersCacheTime = 0 + + def exit(self): + asyncio.run(self.session.close()) + + async def _post(self, method: str, **params): + async with self.session.post( + self.url + "/api/v1/rpc", + json={ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": str(uuid4()), + }, + ) as response: + if response.status == 200: + return await response.json() + raise ConnectionError( + f"Error while sending {method} to Server: HTTP code {response.status}." + ) + + async def sendMessage(self, message: str, recipient: User | Group): + if isinstance(recipient, User): + return await self._post("send", message=message, recipient=recipient.id) + if isinstance(recipient, Group): + return await self._post("send", message=message, groupId=recipient.id) + + async def listGroups(self) -> list[Group]: + return [ + Group(group["name"], group["id"]) + for group in (await self._post("listGroups"))["result"] + ] + + async def listUsers(self) -> list[User]: + return [ + User( + user["name"], + user["number"], + user["uuid"], + user["nickGivenName"], + user["nickFamilyName"], + user["note"], + ) + for user in (await self._post("listContacts"))["result"] + ] + + async def getUser( + self, + *, + number: str | None = None, + id: str | None = None, + first_nickname: str | None = None, + last_nickname: str | None = None, + ) -> User | None: + for user in await self.listUsers(): + if number and user.number == number: + return user + if id and user.id == id: + return user + if first_nickname and user.first_nickname == first_nickname: + return user + if last_nickname and user.last_nickname == last_nickname: + return user + + return None + + def onMessage(self, group: Group | None = None) -> Callable: + def wrapper(func: Callable): + async def callback(data: dict): + envelope = data.get("envelope", {}) + dataMessage = envelope.get("dataMessage", {}) + groupInfo = dataMessage.get("groupInfo", {}) + + msg_user = User( + envelope.get("sourceName"), + envelope.get("sourceNumber"), + envelope.get("sourceUuid"), + ) + + msg_group = None + if groupInfo: + msg_group = Group( + groupInfo.get("groupName"), groupInfo.get("groupId") + ) + + msg_body = envelope.get("dataMessage", {}).get("message") + if msg_body is not None: + if group is None or (msg_group and msg_group.id == group.id): + await func( + ReceivedMessage( + msg_body, + (await self.getUser(number=msg_user.number)) + or msg_user, + msg_group, + ) + ) + + self._hooks[func.__name__] = callback + return func + + return wrapper + + def onMessageRaw(self) -> Callable: + def wrapper(func: Callable): + async def callback(data: dict): + await func(data) + + self._hooks[func.__name__] = callback + return func + + return wrapper + + async def loop(self): + for msg in SSEClient(self.url + "/api/v1/events"): + try: + data = loads(msg.data) + except Exception: + data = {} + + for callback in self._hooks.values(): + await callback(data) diff --git a/main.py b/main.py new file mode 100644 index 0000000..9b7989a --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +import asyncio + +from libminecraft import Minecraft +from libsignal import Group, ReceivedMessage, Signal + +RChat = Group("RS Chat", "5PlbXaPmWZQkhmuyyC/fkWTy8K+BqomjK7byVDyxmpo=") + + +async def main(): + minecraft = Minecraft("http://localhost", 42101, "1234") + signal = Signal("http://localhost", 42069) + + ########################## + # === RETARDS SERVER === # + ########################## + + @signal.onMessage(RChat) + async def onRChatMessage(message: ReceivedMessage): + await minecraft.command( + f'tellraw @a "<{message.user.last_nickname or message.user.name}> {message.message.replace('"', '\\"')}"' + ) + + @minecraft.onPlayerChat() + async def onRServerMessage(player: str, message: str): + await signal.sendMessage(f"<{player}> {message}", RChat) + + await signal.loop() + + +asyncio.run(main())