Implement AI workspace and mitigation workflow

This commit is contained in:
2026-02-15 04:27:28 +00:00
parent de2b9a9bbb
commit 2d3b8fdac6
64 changed files with 7669 additions and 769 deletions

View File

@@ -1,12 +1,12 @@
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
import base64
import aiohttp
import orjson
import requests
from django.conf import settings
from requests.exceptions import RequestException
from rest_framework import status
async def start_typing(uuid):
@@ -18,6 +18,7 @@ async def start_typing(uuid):
async with session.put(url, json=data) as response:
return await response.text() # Optional: Return response content
async def stop_typing(uuid):
base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
url = f"{base}/v1/typing_indicator/{settings.SIGNAL_NUMBER}"
@@ -27,6 +28,7 @@ async def stop_typing(uuid):
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,
@@ -51,12 +53,15 @@ async def download_and_encode_base64(file_url, filename, content_type):
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}"
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.
@@ -75,11 +80,14 @@ async def send_message_raw(recipient_uuid, text=None, attachments=[]):
data = {
"recipients": [recipient_uuid],
"number": settings.SIGNAL_NUMBER,
"base64_attachments": []
"base64_attachments": [],
}
# Asynchronously download and encode all attachments
tasks = [download_and_encode_base64(att["url"], att["filename"], att["content_type"]) for att in 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)
@@ -87,7 +95,7 @@ async def send_message_raw(recipient_uuid, text=None, attachments=[]):
# 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.")
# log.info("Removing message body since it only contains an attachment link.")
text = None # Don't send the link as text
if text:
@@ -103,6 +111,7 @@ async def send_message_raw(recipient_uuid, text=None, attachments=[]):
return ts if ts else False
return False
async def fetch_signal_attachment(attachment_id):
"""
Asynchronously fetches an attachment from Signal.
@@ -111,7 +120,7 @@ async def fetch_signal_attachment(attachment_id):
attachment_id (str): The Signal attachment ID.
Returns:
dict | None:
dict | None:
{
"content": <binary file data>,
"content_type": <MIME type>,
@@ -128,7 +137,9 @@ async def fetch_signal_attachment(attachment_id):
if response.status != 200:
return None # Failed request
content_type = response.headers.get("Content-Type", "application/octet-stream")
content_type = response.headers.get(
"Content-Type", "application/octet-stream"
)
content = await response.read()
size = int(response.headers.get("Content-Length", len(content)))
@@ -150,7 +161,6 @@ async def fetch_signal_attachment(attachment_id):
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.
@@ -173,7 +183,7 @@ def download_and_encode_base64_sync(file_url, filename, content_type):
# 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}")
# log.error(f"Failed to download file: {file_url}, error: {e}")
return None
@@ -193,18 +203,20 @@ def send_message_raw_sync(recipient_uuid, text=None, attachments=[]):
data = {
"recipients": [recipient_uuid],
"number": settings.SIGNAL_NUMBER,
"base64_attachments": []
"base64_attachments": [],
}
# Convert attachments to Base64
for att in attachments:
base64_data = download_and_encode_base64_sync(att["url"], att["filename"], att["content_type"])
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.")
# log.info("Removing message body since it only contains an attachment link.")
text = None # Don't send the link as text
if text:
@@ -214,10 +226,12 @@ def send_message_raw_sync(recipient_uuid, text=None, attachments=[]):
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}")
# 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
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