From 715d0527bfa59c146cbfa5810b9789621a4dacc3 Mon Sep 17 00:00:00 2001 From: Malasaur Date: Tue, 16 Dec 2025 01:15:17 +0100 Subject: [PATCH] Refactored --- libbot.py | 8 + libminecraft.py | 4 +- libsignal.py | 344 ++++++++++++++++++++++++------- main.py | 42 +--- modules/everyone.py | 11 + modules/minecraftSignalBridge.py | 24 +++ 6 files changed, 327 insertions(+), 106 deletions(-) create mode 100644 libbot.py create mode 100644 modules/everyone.py create mode 100644 modules/minecraftSignalBridge.py diff --git a/libbot.py b/libbot.py new file mode 100644 index 0000000..03cc312 --- /dev/null +++ b/libbot.py @@ -0,0 +1,8 @@ +from config import Config +from libminecraft import Minecraft +from libsignal import Signal + +signal = Signal(Config.SIGNAL_HOST, Config.SIGNAL_PORT) +minecraft = Minecraft( + Config.MINECRAFT_HOST, Config.MINECRAFT_PORT, Config.MINECRAFT_PASSWORD +) diff --git a/libminecraft.py b/libminecraft.py index f75a204..74a4aaa 100644 --- a/libminecraft.py +++ b/libminecraft.py @@ -82,9 +82,9 @@ class Minecraft: return groups = match.groups() if groups: - func(*groups) + await func(*groups) else: - func(line) + await func(line) self._hooks.append(callback) diff --git a/libsignal.py b/libsignal.py index 413551f..369e088 100644 --- a/libsignal.py +++ b/libsignal.py @@ -1,11 +1,11 @@ import asyncio from json import loads -from typing import Callable +from pathlib import Path +from typing import Callable, Literal from uuid import uuid4 from aiohttp import ClientSession from config import Config -from sseclient import SSEClient class User: @@ -41,6 +41,10 @@ class User: self.profile_last_name = profile_last_name self.username = username + @classmethod + def self(cls): + return cls("", number="+393406838100") + def __repr__(self) -> str: return f'' @@ -66,49 +70,93 @@ class Group: 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. - """ +class Sticker: + def __init__(self, pack_id: str, id: int): + self.pack_id = pack_id + self.id = id - self.message = message - self.user = user - self.group = group - def __repr__(self) -> str: - group = ( - ' on "' + (self.group.name or self.group.id) + '"' - if self.group and self.group - else "" +class MessageMention: + def __init__(self, start: int, length: int, recipient: User): + self.start = start + self.length = length + self.recipient = recipient + + def get(self) -> str: + return ( + f"{self.start}:{self.length}:{self.recipient.number or self.recipient.id}" ) - return f'' -class MessageMention: ... +class MessageStyle: + def __init__( + self, + start: int, + length: int, + style: Literal["BOLD", "ITALIC", "SPOILER", "STRIKETHROUGH", "MONOSPACE"], + ): + self.start = start + self.length = length + self.style = style - -class MessageStyles: ... + def get(self) -> str: + return f"{self.start}:{self.length}:{self.style}" class Message: def __init__( self, - recipient: User | Group, - sticker: str | None = None, + text: str | None = None, + user: User | None = None, + group: Group | None = None, + timestamp: int | None = None, + attachments: list[Path] = [], + view_once: bool = False, # TODO: fix + sticker: Sticker | None = None, mentions: list[MessageMention] = [], - styles: list[MessageStyles] = [], - view_once: bool = False, - ): ... + styles: list[MessageStyle] = [], + quote: "Message | Poll | None" = None, + edit: "Message | None" = None, + ): + self.text = text + + self.user = user + self.group = group + self.timestamp = timestamp + + self.attachments = attachments + self.view_once = view_once + self.sticker = sticker + self.mentions = mentions + self.styles = styles + self.quote = quote + self.edit = edit + + +class Poll: + def __init__( + self, + question: str, + options: list[str], + user: User | None = None, + group: Group | None = None, + timestamp: int | None = None, + multiple: bool = True, + ): + self.question = question + self.options = options + + self.user = user + self.group = group + self.timestamp = timestamp + + self.multiple = multiple class Signal: def __init__(self, host: str, port: int): self.url = host.rstrip("/") + ":" + str(port) - self._hooks: dict[str, Callable] = {} + self._hooks: list[Callable] = [] async def _post(self, method: str, **params): async with ClientSession() as session: @@ -127,11 +175,129 @@ class Signal: f"Error while sending {method} to Server: HTTP code {response.status}." ) - async def sendMessage(self, message: str, recipient: User | Group): + async def sendMessage(self, message: Message, recipient: User | Group): + params: dict = {} if isinstance(recipient, User): - return await self._post("send", message=message, recipient=recipient.id) + params["recipient"] = recipient.id if isinstance(recipient, Group): - return await self._post("send", message=message, groupId=recipient.id) + params["groupId"] = recipient.id + + if message.attachments: + params["attachment"] = [ + str(file.absolute()) for file in message.attachments + ] + if message.view_once: + params["viewOnce"] = message.view_once + + if message.text: + params["message"] = message.text + if message.mentions: + params["mention"] = [mention.get() for mention in message.mentions] + if message.styles: + params["textStyle"] = [style.get() for style in message.styles] + if message.sticker: + params["sticker"] = message.sticker.pack_id + ":" + str(message.sticker.id) + + if message.quote: + if message.quote.timestamp: + params["quoteTimestamp"] = message.quote.timestamp + if message.quote.user: + params["quoteAuthor"] = message.quote.user.id + + if isinstance(message.quote, Message): + if message.quote.mentions: + params["quoteMention"] = [ + mention.get() for mention in message.quote.mentions + ] + if message.quote.styles: + params["quoteTextStyle"] = [ + style.get() for style in message.quote.styles + ] + if message.quote.attachments: + params["quoteAttachment"] = [ + str(file.absolute()) for file in message.quote.attachments + ] + + if message.edit: + params["editTimestamp"] = message.edit.timestamp + + result = await self._post("send", **params) + + timestamp = result.get("result", {}).get("timestamp") + if timestamp: + message.timestamp = timestamp + message.user = User.self() + + return result + + async def sendReaction(self, emoji: str, message: Message, recipient: User | Group): + params: dict = {"emoji": emoji} + if isinstance(recipient, User): + params["recipient"] = recipient.id + if isinstance(recipient, Group): + params["groupId"] = recipient.id + + if message.user: + params["targetAuthor"] = message.user.id + + if message.timestamp: + params["targetTimestamp"] = message.timestamp + + return await self._post("sendReaction", **params) + + async def startTyping(self, recipient: User | Group): + params: dict = {} + if isinstance(recipient, User): + params["recipient"] = recipient.id + if isinstance(recipient, Group): + params["groupId"] = recipient.id + + return await self._post("sendTyping", **params) + + async def stopTyping(self, recipient: User | Group): + params: dict = {"stop": True} + if isinstance(recipient, User): + params["recipient"] = recipient.id + if isinstance(recipient, Group): + params["groupId"] = recipient.id + + return await self._post("sendTyping", **params) + + async def deleteMessage(self, message: Message, recipient: User | Group): + params: dict = {} + if isinstance(recipient, User): + params["recipient"] = recipient.id + if isinstance(recipient, Group): + params["groupId"] = recipient.id + + params["targetTimestamp"] = message.timestamp + + return await self._post("remoteDelete", **params) + + # TODO: implement polls + """ + async def sendPoll(self, poll: Poll, recipient: User | Group): + params: dict = {} + if isinstance(recipient, User): + params["recipient"] = recipient.id + if isinstance(recipient, Group): + params["groupId"] = recipient.id + + params["question"] = poll.question + params["options"] = poll.options + + if not poll.multiple: + params["noMulti"] = not poll.multiple + + result = await self._post("sendPollCreate", **params) + + timestamp = result.get("result", {}).get("timestamp") + if timestamp: + poll.timestamp = timestamp + poll.user = User.self() + + return result + """ async def listGroups(self) -> list[Group]: return [ @@ -139,9 +305,11 @@ class Signal: id=group["id"], name=group["name"], description=group["description"], - # members=[self.getUser(member["id"]) for member in group["members"]], - # admins=[self.getUser(admin["id"]) for admin in group["admins"]], - # banned=[self.getUser(ban["id"]) for ban in group["banned"]], + members=[ + await self.getUser(member["uuid"]) for member in group["members"] + ], + admins=[await self.getUser(admin["uuid"]) for admin in group["admins"]], + banned=[await self.getUser(ban["uuid"]) for ban in group["banned"]], ) for group in (await self._post("listGroups"))["result"] ] @@ -167,6 +335,18 @@ class Signal: for user in (await self._post("listContacts"))["result"] ] + async def getGroup(self, id: str) -> Group: + for group in await self.listGroups(): + if group.id == id: + return group + return Group(id) + + async def getUser(self, id: str) -> User: + for user in await self.listUsers(): + if user.id == id: + return user + return User(id) + def onMessage( self, *, @@ -176,50 +356,74 @@ class Signal: def wrapper(func: Callable): async def callback(data: dict): envelope = data.get("envelope", {}) - dataMessage = envelope.get("dataMessage", {}) - groupInfo = dataMessage.get("groupInfo", {}) + if envelope and envelope.get("dataMessage"): + dataMessage = envelope["dataMessage"] - msg_user = User( - envelope.get("sourceName"), - envelope.get("sourceNumber"), - envelope.get("sourceUuid"), - ) + message: str | None = dataMessage["message"] - msg_group = None - if groupInfo: - msg_group = Group( - groupInfo.get("groupName"), groupInfo.get("groupId") + groupId: str | None = None + + if dataMessage.get("groupInfo"): + groupId = dataMessage["groupInfo"]["groupId"] + + sticker: Sticker | None = None + if dataMessage.get("sticker"): + s = dataMessage["sticker"] + sticker = Sticker(s["packId"], s["stickerId"]) + + # attachments + # contacts + + viewOnce: bool = dataMessage["viewOnce"] + + sourceUuid: str = envelope["sourceUuid"] + timestamp: int = envelope["timestamp"] + + msg_user: User = await self.getUser(sourceUuid) + msg_group: Group | None = ( + (await self.getGroup(groupId)) if groupId else None ) - msg = None - msg_body = envelope.get("dataMessage", {}).get("message") - if msg_body is not None: - msg = ReceivedMessage(msg_body, msg_user, msg_group) + msg = Message( + text=message, + user=msg_user, + group=msg_group, + timestamp=timestamp, + sticker=sticker, + view_once=viewOnce, + ) + + await self._post( + "sendReceipt", + recipient=msg_user.id, + targetTimestamp=timestamp, + type="viewed", + ) if not user and not group: - return func(msg) + return await func(msg) if isinstance(user, User) and user.id == msg_user.id: - return func(msg) + return await func(msg) if ( isinstance(group, Group) and msg_group and group.id == msg_group.id ): - return func(msg) + return await func(msg) if isinstance(user, list): for usr in user: if usr.id == msg_user.id: - return func(msg) + return await func(msg) if isinstance(group, list) and msg_group: for grp in group: if grp.id == msg_group.id: - return func(msg) + return await func(msg) - self._hooks[func.__name__] = callback + self._hooks.append(callback) return func return wrapper @@ -227,26 +431,24 @@ class Signal: def onMessageRaw(self) -> Callable: def wrapper(func: Callable): async def callback(data: dict): - func(data) + await func(data) - self._hooks[func.__name__] = callback + self._hooks.append(callback) return func return wrapper async def loop(self): - for msg in SSEClient(self.url + "/api/v1/events"): - try: - data = loads(msg.data.encode("latin1").decode()) - except Exception: - data = {} + async with ClientSession() as session: + async with session.get(self.url + "/api/v1/events") as response: + if response.status == 200: + async for msg in response.content: + msg_data = msg.decode().strip() + if msg_data.startswith("data"): + try: + data = loads(msg_data.lstrip("data:")) + except Exception: + data = {} - for callback in self._hooks.values(): - await callback(data) - - -signal = Signal(Config.SIGNAL_HOST, Config.SIGNAL_PORT) - -user = asyncio.run(signal.listGroups())[1] - -print(user.__dict__) + for callback in self._hooks: + asyncio.create_task(callback(data)) diff --git a/main.py b/main.py index 46fd9da..c8501fa 100644 --- a/main.py +++ b/main.py @@ -1,41 +1,17 @@ import asyncio +import os +from importlib import import_module -from libminecraft import Minecraft -from libsignal import Group, ReceivedMessage, Signal +from libbot import minecraft, signal -RChat = Group("RS Chat", "5PlbXaPmWZQkhmuyyC/fkWTy8K+BqomjK7byVDyxmpo=") +for module in os.listdir("modules"): + if module.endswith(".py"): + import_module("modules." + module.rstrip(".py")) async def main(): - minecraft = Minecraft("http://localhost", 42101, "1234") - signal = Signal("http://localhost", 42069) - - ########################## - # === RETARDS SERVER === # - ########################## - - @signal.onMessage(RChat) - async def onRChatMessage(message: ReceivedMessage): - username = message.user.last_nickname or message.user.name - msg = message.message.replace('"', '\\"') - await minecraft.command(f'tellraw @a "<{username}> {msg}"') - - @minecraft.onPlayerChat() - async def onRServerMessage(player: str, message: str): - await signal.sendMessage(f"<{player}> {message}", RChat) - - ################## - # === GLOBAL === # - ################## - - @signal.onMessage() - async def onMessage(message: ReceivedMessage): - if "clanker" in message.message.lower(): - await signal.sendMessage( - "Don't use the c-word nigga", message.group or message.user - ) - - await signal.loop() + await asyncio.gather(minecraft.loop(), signal.loop()) -asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main()) diff --git a/modules/everyone.py b/modules/everyone.py new file mode 100644 index 0000000..7380fdd --- /dev/null +++ b/modules/everyone.py @@ -0,0 +1,11 @@ +from libbot import signal +from libsignal import Message + + +@signal.onMessage() +async def onMessage(message: Message): + if message.user and message.text and "@everyone" in message.text: + await signal.sendMessage( + Message("EVERYBODY LISTEN!!!!", quote=message), + message.group or message.user, + ) diff --git a/modules/minecraftSignalBridge.py b/modules/minecraftSignalBridge.py new file mode 100644 index 0000000..ff9d0b5 --- /dev/null +++ b/modules/minecraftSignalBridge.py @@ -0,0 +1,24 @@ +import asyncio + +from libbot import minecraft, signal +from libsignal import Message, MessageStyle + +RSChat = asyncio.run(signal.getGroup("5PlbXaPmWZQkhmuyyC/fkWTy8K+BqomjK7byVDyxmpo=")) + + +@minecraft.onPlayerChat() +async def onMinecraftMessage(player: str, message: str): + await signal.sendMessage( + Message( + f"<{player}> {message}", styles=[MessageStyle(0, len(player) + 2, "BOLD")] + ), + RSChat, + ) + await minecraft.command("say hi") + + +@signal.onMessage() +async def onSignalMessage(message: Message): + if message.user and message.user.note and "!rserver" in message.user.note: + username = message.user.last_nickname + await minecraft.command(f"tellraw @a <{username}> {message.text}")