Implement attachment view
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import re
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import aiohttp
|
||||
from asgiref.sync import sync_to_async
|
||||
@@ -27,6 +29,50 @@ from core.models import (
|
||||
)
|
||||
from core.util import logs
|
||||
|
||||
URL_PATTERN = re.compile(r"https?://[^\s<>'\"\\]+")
|
||||
|
||||
|
||||
def _clean_url(value):
|
||||
return str(value or "").strip().rstrip(".,);:!?\"'")
|
||||
|
||||
|
||||
def _filename_from_url(url_value):
|
||||
path = urlsplit(str(url_value or "")).path
|
||||
name = path.rsplit("/", 1)[-1]
|
||||
return name or "attachment"
|
||||
|
||||
|
||||
def _extract_xml_attachment_urls(message_stanza):
|
||||
urls = []
|
||||
|
||||
def _add(candidate):
|
||||
cleaned = _clean_url(candidate)
|
||||
if not cleaned:
|
||||
return
|
||||
if not cleaned.startswith("http://") and not cleaned.startswith("https://"):
|
||||
return
|
||||
if cleaned not in urls:
|
||||
urls.append(cleaned)
|
||||
|
||||
# Explicit attachments and OOB payloads.
|
||||
for node in message_stanza.xml.findall(".//{urn:xmpp:attachments}attachment"):
|
||||
_add(node.attrib.get("url"))
|
||||
for node in message_stanza.xml.findall(".//{jabber:x:oob}x/{jabber:x:oob}url"):
|
||||
_add(node.text)
|
||||
|
||||
# XMPP references frequently carry attachment URIs.
|
||||
for node in message_stanza.xml.findall(".//{urn:xmpp:reference:0}reference"):
|
||||
_add(node.attrib.get("uri"))
|
||||
|
||||
# Generic fallback for custom namespaces and rich message payloads.
|
||||
for node in message_stanza.xml.iter():
|
||||
for key in ("url", "uri", "href", "src"):
|
||||
_add(node.attrib.get(key))
|
||||
for match in URL_PATTERN.findall(str(node.text or "")):
|
||||
_add(match)
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
class XMPPComponent(ComponentXMPP):
|
||||
|
||||
@@ -821,38 +867,69 @@ class XMPPComponent(ComponentXMPP):
|
||||
recipient_domain = recipient_jid
|
||||
|
||||
# Extract message body
|
||||
body = msg["body"] if msg["body"] else "[No Body]"
|
||||
body = msg["body"] if msg["body"] else ""
|
||||
|
||||
attachments = []
|
||||
self.log.info(f"Full XMPP Message: {ET.tostring(msg.xml, encoding='unicode')}")
|
||||
|
||||
# Extract attachments from standard XMPP <attachments> (if present)
|
||||
# Extract attachments from standard XMPP payloads.
|
||||
for att in msg.xml.findall(".//{urn:xmpp:attachments}attachment"):
|
||||
url_value = _clean_url(att.attrib.get("url"))
|
||||
if not url_value:
|
||||
continue
|
||||
attachments.append(
|
||||
{
|
||||
"url": att.attrib.get("url"),
|
||||
"filename": att.attrib.get("filename"),
|
||||
"content_type": att.attrib.get("content_type"),
|
||||
"url": url_value,
|
||||
"filename": att.attrib.get("filename")
|
||||
or _filename_from_url(url_value),
|
||||
"content_type": att.attrib.get("content_type")
|
||||
or "application/octet-stream",
|
||||
}
|
||||
)
|
||||
|
||||
# Extract attachments from XEP-0066 <x><url> format (Out of Band Data)
|
||||
# Extract attachments from XEP-0066 OOB payloads.
|
||||
for oob in msg.xml.findall(".//{jabber:x:oob}x/{jabber:x:oob}url"):
|
||||
url_value = _clean_url(oob.text)
|
||||
if not url_value:
|
||||
continue
|
||||
attachments.append(
|
||||
{
|
||||
"url": oob.text,
|
||||
"filename": oob.text.split("/")[-1], # Extract filename from URL
|
||||
"content_type": "application/octet-stream", # Generic content-type
|
||||
"url": url_value,
|
||||
"filename": _filename_from_url(url_value),
|
||||
"content_type": "application/octet-stream",
|
||||
}
|
||||
)
|
||||
|
||||
# Fallback extraction for alternate attachment encodings.
|
||||
extracted_urls = _extract_xml_attachment_urls(msg)
|
||||
existing_urls = {str(item.get("url") or "").strip() for item in attachments}
|
||||
for url_value in extracted_urls:
|
||||
if url_value in existing_urls:
|
||||
continue
|
||||
attachments.append(
|
||||
{
|
||||
"url": url_value,
|
||||
"filename": _filename_from_url(url_value),
|
||||
"content_type": "application/octet-stream",
|
||||
}
|
||||
)
|
||||
|
||||
if (not body or body.strip().lower() in {"[no body]", "(no text)"}) and attachments:
|
||||
attachment_urls = [
|
||||
str(item.get("url") or "").strip()
|
||||
for item in attachments
|
||||
if str(item.get("url") or "").strip()
|
||||
]
|
||||
if attachment_urls:
|
||||
body = "\n".join(attachment_urls)
|
||||
|
||||
self.log.info(f"Extracted {len(attachments)} attachments from XMPP message.")
|
||||
# Log extracted information with variable name annotations
|
||||
log_message = (
|
||||
f"Sender JID: {sender_jid}, Sender Username: {sender_username}, Sender Domain: {sender_domain}, "
|
||||
f"Sender Resource: {sender_resource if sender_resource else '[No Resource]'}, "
|
||||
f"Recipient JID: {recipient_jid}, Recipient Username: {recipient_username}, Recipient Domain: {recipient_domain}, "
|
||||
f"Body: {body}"
|
||||
f"Body: {body or '[No Body]'}"
|
||||
)
|
||||
self.log.info(log_message)
|
||||
|
||||
@@ -960,6 +1037,11 @@ class XMPPComponent(ComponentXMPP):
|
||||
)
|
||||
self.log.info(f"MANIP11 {manipulations}")
|
||||
if not manipulations:
|
||||
await self.ur.stopped_typing(
|
||||
"xmpp",
|
||||
identifier=identifier,
|
||||
payload={"reason": "message_sent"},
|
||||
)
|
||||
await identifier.send(
|
||||
body,
|
||||
attachments,
|
||||
@@ -984,6 +1066,11 @@ class XMPPComponent(ComponentXMPP):
|
||||
text=result,
|
||||
ts=int(now().timestamp() * 1000),
|
||||
)
|
||||
await self.ur.stopped_typing(
|
||||
"xmpp",
|
||||
identifier=identifier,
|
||||
payload={"reason": "message_sent"},
|
||||
)
|
||||
await identifier.send(
|
||||
result,
|
||||
attachments,
|
||||
@@ -1052,6 +1139,29 @@ class XMPPComponent(ComponentXMPP):
|
||||
self.log.info(f"Sending XMPP message: {msg.xml}")
|
||||
msg.send()
|
||||
|
||||
async def send_chat_state(self, recipient_jid, sender_jid, started):
|
||||
"""Send XMPP chat-state update to the client."""
|
||||
msg = self.make_message(mto=recipient_jid, mfrom=sender_jid, mtype="chat")
|
||||
state_tag = "composing" if started else "paused"
|
||||
msg.xml.append(
|
||||
ET.Element(f"{{http://jabber.org/protocol/chatstates}}{state_tag}")
|
||||
)
|
||||
self.log.info(
|
||||
"Sending XMPP chat-state %s: %s -> %s",
|
||||
state_tag,
|
||||
sender_jid,
|
||||
recipient_jid,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
async def send_typing_for_person(self, user, person_identifier, started):
|
||||
sender_jid = (
|
||||
f"{person_identifier.person.name.lower()}|"
|
||||
f"{person_identifier.service}@{settings.XMPP_JID}"
|
||||
)
|
||||
recipient_jid = f"{user.username}@{settings.XMPP_ADDRESS}"
|
||||
await self.send_chat_state(recipient_jid, sender_jid, started)
|
||||
|
||||
async def send_from_external(
|
||||
self, user, person_identifier, text, is_outgoing_message, attachments=[]
|
||||
):
|
||||
@@ -1110,3 +1220,9 @@ class XMPPClient(ClientBase):
|
||||
|
||||
self.client.connect()
|
||||
# self.client.process()
|
||||
|
||||
async def start_typing_for_person(self, user, person_identifier):
|
||||
await self.client.send_typing_for_person(user, person_identifier, True)
|
||||
|
||||
async def stop_typing_for_person(self, user, person_identifier):
|
||||
await self.client.send_typing_for_person(user, person_identifier, False)
|
||||
|
||||
Reference in New Issue
Block a user