Implement reactions and image sync

This commit is contained in:
2026-02-17 21:23:03 +00:00
parent 6bc8a0ab88
commit dc28745fc3
14 changed files with 2011 additions and 202 deletions

View File

@@ -28,7 +28,7 @@ async def stop_typing(uuid):
return await response.text() # Optional: Return response content
async def download_and_encode_base64(file_url, filename, content_type):
async def download_and_encode_base64(file_url, filename, content_type, session=None):
"""
Downloads a file from a given URL asynchronously, converts it to Base64,
and returns it in Signal's expected format.
@@ -42,10 +42,17 @@ async def download_and_encode_base64(file_url, filename, content_type):
str | None: The Base64 encoded attachment string in Signal's expected format, or None on failure.
"""
try:
async with aiohttp.ClientSession() as session:
if session is not None:
async with session.get(file_url, timeout=10) as response:
if response.status != 200:
# log.error(f"Failed to download file: {file_url}, status: {response.status}")
return None
file_data = await response.read()
base64_encoded = base64.b64encode(file_data).decode("utf-8")
return f"data:{content_type};filename={filename};base64,{base64_encoded}"
async with aiohttp.ClientSession() as local_session:
async with local_session.get(file_url, timeout=10) as response:
if response.status != 200:
return None
file_data = await response.read()
@@ -82,19 +89,39 @@ async def send_message_raw(recipient_uuid, text=None, attachments=None):
"base64_attachments": [],
}
# Asynchronously download and encode all attachments
async def _attachment_to_base64(attachment, session):
row = dict(attachment or {})
filename = row.get("filename") or "attachment.bin"
content_type = row.get("content_type") or "application/octet-stream"
content = row.get("content")
if isinstance(content, memoryview):
content = content.tobytes()
elif isinstance(content, bytearray):
content = bytes(content)
if isinstance(content, bytes):
encoded = base64.b64encode(content).decode("utf-8")
return f"data:{content_type};filename={filename};base64,{encoded}"
file_url = row.get("url")
if not file_url:
return None
return await download_and_encode_base64(file_url, filename, content_type, session)
# Asynchronously resolve and encode all attachments
attachments = attachments or []
tasks = [
download_and_encode_base64(att["url"], att["filename"], att["content_type"])
for att in attachments
]
encoded_attachments = await asyncio.gather(*tasks)
async with aiohttp.ClientSession() as session:
tasks = [_attachment_to_base64(att, session) for att in attachments]
encoded_attachments = await asyncio.gather(*tasks)
# Filter out failed downloads (None values)
data["base64_attachments"] = [att for att in encoded_attachments if att]
# Remove the message body if it only contains an attachment link
if text and (text.strip() in [att["url"] for att in attachments]):
attachment_urls = {
str((att or {}).get("url") or "").strip()
for att in attachments
if str((att or {}).get("url") or "").strip()
}
if text and text.strip() in attachment_urls:
# log.info("Removing message body since it only contains an attachment link.")
text = None # Don't send the link as text
@@ -112,6 +139,42 @@ async def send_message_raw(recipient_uuid, text=None, attachments=None):
return False
async def send_reaction(
recipient_uuid,
emoji,
target_timestamp=None,
target_author=None,
remove=False,
):
base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
sender_number = settings.SIGNAL_NUMBER
if not recipient_uuid or not target_timestamp:
return False
payload = {
"recipient": recipient_uuid,
"reaction": str(emoji or ""),
"target_author": str(target_author or recipient_uuid),
"timestamp": int(target_timestamp),
"remove": bool(remove),
}
candidate_urls = [f"{base}/v1/reactions/{sender_number}"]
timeout = aiohttp.ClientTimeout(total=20)
async with aiohttp.ClientSession(timeout=timeout) as session:
for url in candidate_urls:
for method in ("post",):
try:
request = getattr(session, method)
async with request(url, json=payload) as response:
if 200 <= response.status < 300:
return True
except Exception:
continue
return False
async def fetch_signal_attachment(attachment_id):
"""
Asynchronously fetches an attachment from Signal.