# Deferred processing library import asyncio from typing import Annotated, Optional from uuid import UUID from asgiref.sync import sync_to_async from django.utils import timezone as dj_timezone from pydantic import BaseModel, ValidationError from core.clients import serviceapi from core.messaging import natural from core.models import Message, PersonIdentifier, QueuedMessage from core.util import logs log = logs.get_logger("deferred") class DeferredDetail(BaseModel): reply_to_self: bool reply_to_others: bool is_outgoing_message: bool class DeferredRequest(BaseModel): type: str method: str user_id: Optional[int] = None message_id: Optional[Annotated[str, UUID]] = None identifier: Optional[str] = None msg: Optional[str] = None service: Optional[str] = None detail: Optional[DeferredDetail] = None attachments: Optional[list] = None async def send_message(db_obj): identifier = db_obj.session.identifier recipient_uuid = identifier.identifier service = identifier.service text = db_obj.text async def send(value): return await serviceapi.send_message_raw( service, recipient_uuid, value, ) # returns ts async def start_t(): return await serviceapi.start_typing(service, recipient_uuid) async def stop_t(): return await serviceapi.stop_typing(service, recipient_uuid) tss = await natural.natural_send_message( text, send, start_t, stop_t, ) # list of ts # result = await send_message_raw(recipient_uuid, text) await sync_to_async(db_obj.delete)() result = [x for x in tss if x] # all trueish ts if result: # if at least one message was sent ts1 = result.pop() # pick a time if isinstance(ts1, bool): ts1 = int(dj_timezone.now().timestamp() * 1000) log.info("Stored outbound message for %s: %s", service, text) await sync_to_async(Message.objects.create)( user=db_obj.session.user, session=db_obj.session, custom_author="BOT", text=text, ts=ts1, # use that time in db delivered_ts=ts1, read_source_service=service, ) async def process_deferred(data: dict, **kwargs): try: validated_data = DeferredRequest(**data) log.info(f"Validated Data: {validated_data}") # Process the validated data except ValidationError as e: log.info(f"Validation Error: {e}") return method = validated_data.method user_id = validated_data.user_id message_id = validated_data.message_id if method == "accept_message": try: message = await sync_to_async(QueuedMessage.objects.get)( user_id=user_id, id=message_id, ) log.info(f"Got {message}") except QueuedMessage.DoesNotExist: log.info(f"Didn't get message from {message_id}") return await send_message(message) elif method == "xmpp": # send xmpp message xmpp = kwargs.get("xmpp") service = validated_data.service msg = validated_data.msg attachments = validated_data.attachments # Get User from identifier identifiers = PersonIdentifier.objects.filter( identifier=validated_data.identifier, service=service, ) xmpp_attachments = [] # attachments = [] # Asynchronously fetch all attachments tasks = [serviceapi.fetch_attachment(service, att) for att in attachments] fetched_attachments = await asyncio.gather(*tasks) for fetched, att in zip(fetched_attachments, attachments): if not fetched: log.warning( "Failed to fetch attachment %s from %s.", att.get("id"), service, ) continue # Attach fetched file to XMPP xmpp_attachments.append( { "content": fetched["content"], "content_type": fetched["content_type"], "filename": fetched["filename"], "size": fetched["size"], } ) for identifier in identifiers: # recipient_jid = f"{identifier.user.username}@{settings.XMPP_ADDRESS}" user = identifier.user log.info( "Sending %s attachments from %s to XMPP.", len(xmpp_attachments), service, ) await xmpp.send_from_external( user, identifier, msg, validated_data.detail, attachments=xmpp_attachments, ) else: log.warning(f"Method not yet supported: {method}") return