from rest_framework import status import requests from requests.exceptions import RequestException import orjson from django.conf import settings import aiohttp import base64 import asyncio async def start_typing(uuid): url = f"http://signal:8080/v1/typing_indicator/{settings.SIGNAL_NUMBER}" data = {"recipient": uuid} async with aiohttp.ClientSession() as session: async with session.put(url, json=data) as response: return await response.text() # Optional: Return response content async def stop_typing(uuid): url = f"http://signal:8080/v1/typing_indicator/{settings.SIGNAL_NUMBER}" data = {"recipient": uuid} async with aiohttp.ClientSession() as session: async with session.delete(url, json=data) as response: return await response.text() # Optional: Return response content async def download_and_encode_base64(file_url, filename, content_type): """ Downloads a file from a given URL asynchronously, converts it to Base64, and returns it in Signal's expected format. Args: file_url (str): The URL of the file to download. filename (str): The name of the file. content_type (str): The MIME type of the file. Returns: str | None: The Base64 encoded attachment string in Signal's expected format, or None on failure. """ try: async with aiohttp.ClientSession() as session: 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") # Format according to Signal's expected structure return f"data:{content_type};filename={filename};base64,{base64_encoded}" except aiohttp.ClientError as e: # log.error(f"Failed to download file: {file_url}, error: {e}") return None async def send_message_raw(recipient_uuid, text=None, attachments=[]): """ Sends a message using the Signal REST API, ensuring attachment links are not included in the text body. Args: recipient_uuid (str): The UUID of the recipient. text (str, optional): The message to send. attachments (list, optional): A list of attachment dictionaries with URL, filename, and content_type. Returns: int | bool: Timestamp if successful, False otherwise. """ url = "http://signal:8080/v2/send" data = { "recipients": [recipient_uuid], "number": settings.SIGNAL_NUMBER, "base64_attachments": [] } # Asynchronously download and encode all attachments tasks = [download_and_encode_base64(att["url"], att["filename"], att["content_type"]) 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]): #log.info("Removing message body since it only contains an attachment link.") text = None # Don't send the link as text if text: data["message"] = text async with aiohttp.ClientSession() as session: async with session.post(url, json=data) as response: response_text = await response.text() response_status = response.status if response_status == status.HTTP_201_CREATED: ts = orjson.loads(response_text).get("timestamp", None) return ts if ts else False return False async def fetch_signal_attachment(attachment_id): """ Asynchronously fetches an attachment from Signal. Args: attachment_id (str): The Signal attachment ID. Returns: dict | None: { "content": , "content_type": , "filename": } or None if the request fails. """ url = f"http://signal:8080/v1/attachments/{attachment_id}" try: async with aiohttp.ClientSession() as session: async with session.get(url, timeout=10) as response: if response.status != 200: return None # Failed request content_type = response.headers.get("Content-Type", "application/octet-stream") content = await response.read() size = int(response.headers.get("Content-Length", len(content))) filename = attachment_id # Default fallback filename content_disposition = response.headers.get("Content-Disposition") if content_disposition: parts = content_disposition.split(";") for part in parts: if "filename=" in part: filename = part.split("=", 1)[1].strip().strip('"') return { "content": content, "content_type": content_type, "filename": filename, "size": size, } except aiohttp.ClientError: return None # Network error def download_and_encode_base64_sync(file_url, filename, content_type): """ Downloads a file from a given URL, converts it to Base64, and returns it in Signal's expected format. Args: file_url (str): The URL of the file to download. filename (str): The name of the file. content_type (str): The MIME type of the file. Returns: str: The Base64 encoded attachment string in Signal's expected format. """ try: response = requests.get(file_url, timeout=10) response.raise_for_status() file_data = response.content base64_encoded = base64.b64encode(file_data).decode("utf-8") # Format according to Signal's expected structure return f"data:{content_type};filename={filename};base64,{base64_encoded}" except requests.RequestException as e: #log.error(f"Failed to download file: {file_url}, error: {e}") return None def send_message_raw_sync(recipient_uuid, text=None, attachments=[]): """ Sends a message using the Signal REST API, ensuring attachment links are not included in the text body. Args: recipient_uuid (str): The UUID of the recipient. text (str, optional): The message to send. attachments (list, optional): A list of attachment dictionaries with URL, filename, and content_type. Returns: int | bool: Timestamp if successful, False otherwise. """ url = "http://signal:8080/v2/send" data = { "recipients": [recipient_uuid], "number": settings.SIGNAL_NUMBER, "base64_attachments": [] } # Convert attachments to Base64 for att in attachments: base64_data = download_and_encode_base64_sync(att["url"], att["filename"], att["content_type"]) if base64_data: data["base64_attachments"].append(base64_data) # Remove the message body if it only contains an attachment link if text and (text.strip() in [att["url"] for att in attachments]): #log.info("Removing message body since it only contains an attachment link.") text = None # Don't send the link as text if text: data["message"] = text try: response = requests.post(url, json=data, timeout=10) response.raise_for_status() except requests.RequestException as e: #log.error(f"Failed to send Signal message: {e}") return False if response.status_code == status.HTTP_201_CREATED: # Signal server returns 201 on success try: ts = orjson.loads(response.text).get("timestamp", None) return ts if ts else False except orjson.JSONDecodeError: return False return False # If response status is not 201