Refactored
This commit is contained in:
parent
e7248723f5
commit
715d0527bf
6 changed files with 327 additions and 106 deletions
344
libsignal.py
344
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'<User "{self.name}" at "{self.number}">'
|
||||
|
||||
|
|
@ -66,49 +70,93 @@ class Group:
|
|||
return f'<Group "{self.name}" at "{self.id}">'
|
||||
|
||||
|
||||
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'<Message "{self.message}" by "{self.user.name}"{group}>'
|
||||
|
||||
|
||||
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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue