Implement reactions and image sync
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import aiohttp
|
||||
@@ -12,7 +15,7 @@ from slixmpp.stanza import Message
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.xmlstream.stanzabase import ET
|
||||
|
||||
from core.clients import ClientBase
|
||||
from core.clients import ClientBase, transport
|
||||
from core.messaging import ai, history, replies, utils
|
||||
from core.models import (
|
||||
ChatSession,
|
||||
@@ -30,6 +33,9 @@ from core.models import (
|
||||
from core.util import logs
|
||||
|
||||
URL_PATTERN = re.compile(r"https?://[^\s<>'\"\\]+")
|
||||
EMOJI_ONLY_PATTERN = re.compile(
|
||||
r"^[\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F\u200D\u2640-\u2642\u2764]+$"
|
||||
)
|
||||
|
||||
|
||||
def _clean_url(value):
|
||||
@@ -42,6 +48,12 @@ def _filename_from_url(url_value):
|
||||
return name or "attachment"
|
||||
|
||||
|
||||
def _content_type_from_filename_or_url(url_value, default="application/octet-stream"):
|
||||
filename = _filename_from_url(url_value)
|
||||
guessed, _ = mimetypes.guess_type(filename)
|
||||
return guessed or default
|
||||
|
||||
|
||||
def _extract_xml_attachment_urls(message_stanza):
|
||||
urls = []
|
||||
|
||||
@@ -74,6 +86,46 @@ def _extract_xml_attachment_urls(message_stanza):
|
||||
return urls
|
||||
|
||||
|
||||
def _extract_xmpp_reaction(message_stanza):
|
||||
nodes = message_stanza.xml.findall(".//{urn:xmpp:reactions:0}reactions")
|
||||
if not nodes:
|
||||
return None
|
||||
node = nodes[0]
|
||||
target_id = str(node.attrib.get("id") or "").strip()
|
||||
emojis = []
|
||||
for child in node.findall("{urn:xmpp:reactions:0}reaction"):
|
||||
value = str(child.text or "").strip()
|
||||
if value:
|
||||
emojis.append(value)
|
||||
return {
|
||||
"target_id": target_id,
|
||||
"emoji": emojis[0] if emojis else "",
|
||||
"remove": len(emojis) == 0,
|
||||
}
|
||||
|
||||
|
||||
def _extract_xmpp_reply_target_id(message_stanza):
|
||||
reply = message_stanza.xml.find(".//{urn:xmpp:reply:0}reply")
|
||||
if reply is None:
|
||||
return ""
|
||||
return str(reply.attrib.get("id") or reply.attrib.get("to") or "").strip()
|
||||
|
||||
|
||||
def _parse_greentext_reaction(body_text):
|
||||
lines = [line.strip() for line in str(body_text or "").splitlines() if line.strip()]
|
||||
if len(lines) != 2:
|
||||
return None
|
||||
if not lines[0].startswith(">"):
|
||||
return None
|
||||
quoted = lines[0][1:].strip()
|
||||
emoji = lines[1].strip()
|
||||
if not quoted or not emoji:
|
||||
return None
|
||||
if not EMOJI_ONLY_PATTERN.match(emoji):
|
||||
return None
|
||||
return {"quoted_text": quoted, "emoji": emoji}
|
||||
|
||||
|
||||
class XMPPComponent(ComponentXMPP):
|
||||
|
||||
"""
|
||||
@@ -82,6 +134,7 @@ class XMPPComponent(ComponentXMPP):
|
||||
|
||||
def __init__(self, ur, jid, secret, server, port):
|
||||
self.ur = ur
|
||||
self._upload_config_warned = False
|
||||
|
||||
self.log = logs.get_logger("XMPP")
|
||||
|
||||
@@ -130,6 +183,8 @@ class XMPPComponent(ComponentXMPP):
|
||||
self.log.error(f"Failed to enable Carbons: {e}")
|
||||
|
||||
def get_identifier(self, msg):
|
||||
xmpp_message_id = str(msg.get("id") or "").strip()
|
||||
|
||||
# Extract sender JID (full format: user@domain/resource)
|
||||
sender_jid = str(msg["from"])
|
||||
|
||||
@@ -798,10 +853,43 @@ class XMPPComponent(ComponentXMPP):
|
||||
or getattr(settings, "XMPP_UPLOAD_JID", "")
|
||||
).strip()
|
||||
if not upload_service_jid:
|
||||
self.log.error(
|
||||
"XMPP upload service is not configured. Set XMPP_UPLOAD_SERVICE."
|
||||
)
|
||||
return None
|
||||
discovered = None
|
||||
try:
|
||||
discovered = await self["xep_0363"].find_upload_service()
|
||||
except Exception as exc:
|
||||
self.log.debug("XMPP upload service discovery failed: %s", exc)
|
||||
if discovered:
|
||||
discovered_jid = ""
|
||||
try:
|
||||
discovered_jid = str(getattr(discovered, "jid", "") or "").strip()
|
||||
except Exception:
|
||||
discovered_jid = ""
|
||||
|
||||
if not discovered_jid:
|
||||
raw_discovered = str(discovered or "").strip()
|
||||
if raw_discovered.startswith("<"):
|
||||
try:
|
||||
node = ET.fromstring(raw_discovered)
|
||||
discovered_jid = str(node.attrib.get("from") or "").strip()
|
||||
except Exception:
|
||||
discovered_jid = ""
|
||||
else:
|
||||
discovered_jid = raw_discovered
|
||||
|
||||
upload_service_jid = discovered_jid
|
||||
if upload_service_jid:
|
||||
self.log.info(
|
||||
"Discovered XMPP upload service via XEP-0363: %s",
|
||||
upload_service_jid,
|
||||
)
|
||||
else:
|
||||
if not self._upload_config_warned:
|
||||
self.log.warning(
|
||||
"XMPP upload service not configured/discoverable; skipping attachment upload. "
|
||||
"Set XMPP_UPLOAD_SERVICE (or XMPP_UPLOAD_JID)."
|
||||
)
|
||||
self._upload_config_warned = True
|
||||
return None
|
||||
|
||||
try:
|
||||
slot = await self["xep_0363"].request_slot(
|
||||
@@ -849,6 +937,8 @@ class XMPPComponent(ComponentXMPP):
|
||||
def sym(value):
|
||||
msg.reply(f"[>] {value}").send()
|
||||
|
||||
xmpp_message_id = str(msg.get("id") or "").strip()
|
||||
|
||||
# Extract sender JID (full format: user@domain/resource)
|
||||
sender_jid = str(msg["from"])
|
||||
|
||||
@@ -872,6 +962,9 @@ class XMPPComponent(ComponentXMPP):
|
||||
|
||||
# Extract message body
|
||||
body = msg["body"] if msg["body"] else ""
|
||||
parsed_reaction = _extract_xmpp_reaction(msg)
|
||||
parsed_reply_target = _extract_xmpp_reply_target_id(msg)
|
||||
greentext_reaction = _parse_greentext_reaction(body)
|
||||
|
||||
attachments = []
|
||||
self.log.debug(
|
||||
@@ -898,11 +991,12 @@ class XMPPComponent(ComponentXMPP):
|
||||
url_value = _clean_url(oob.text)
|
||||
if not url_value:
|
||||
continue
|
||||
guessed_content_type = _content_type_from_filename_or_url(url_value)
|
||||
attachments.append(
|
||||
{
|
||||
"url": url_value,
|
||||
"filename": _filename_from_url(url_value),
|
||||
"content_type": "application/octet-stream",
|
||||
"content_type": guessed_content_type,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -912,11 +1006,12 @@ class XMPPComponent(ComponentXMPP):
|
||||
for url_value in extracted_urls:
|
||||
if url_value in existing_urls:
|
||||
continue
|
||||
guessed_content_type = _content_type_from_filename_or_url(url_value)
|
||||
attachments.append(
|
||||
{
|
||||
"url": url_value,
|
||||
"filename": _filename_from_url(url_value),
|
||||
"content_type": "application/octet-stream",
|
||||
"content_type": guessed_content_type,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -931,6 +1026,17 @@ class XMPPComponent(ComponentXMPP):
|
||||
if attachment_urls:
|
||||
body = "\n".join(attachment_urls)
|
||||
|
||||
relay_body = body
|
||||
attachment_urls_for_body = [
|
||||
str(item.get("url") or "").strip()
|
||||
for item in attachments
|
||||
if str(item.get("url") or "").strip()
|
||||
]
|
||||
if attachment_urls_for_body:
|
||||
joined_urls = "\n".join(attachment_urls_for_body).strip()
|
||||
if str(relay_body or "").strip() == joined_urls:
|
||||
relay_body = ""
|
||||
|
||||
self.log.debug("Extracted %s attachments from XMPP message", len(attachments))
|
||||
# Log extracted information with variable name annotations
|
||||
log_message = (
|
||||
@@ -1021,6 +1127,106 @@ class XMPPComponent(ComponentXMPP):
|
||||
# sym(str(person.__dict__))
|
||||
# sym(f"Service: {recipient_service}")
|
||||
|
||||
if parsed_reaction or greentext_reaction:
|
||||
# TODO(web-ui-react): expose explicit web compose reaction actions
|
||||
# that call this same bridge path (without text heuristics).
|
||||
# TODO(edit-sync): extend bridge mapping to include edit message ids
|
||||
# and reconcile upstream edit capability differences in UI.
|
||||
# TODO(retract-sync): propagate delete/retract state through this
|
||||
# same mapping layer for protocol parity.
|
||||
reaction_payload = parsed_reaction or {
|
||||
"target_id": parsed_reply_target,
|
||||
"emoji": str((greentext_reaction or {}).get("emoji") or ""),
|
||||
"remove": False,
|
||||
}
|
||||
if not str(reaction_payload.get("target_id") or "").strip():
|
||||
text_hint = str((greentext_reaction or {}).get("quoted_text") or "")
|
||||
hint_match = transport.resolve_bridge_from_text_hint(
|
||||
user_id=identifier.user_id,
|
||||
person_id=identifier.person_id,
|
||||
service=recipient_service,
|
||||
text_hint=text_hint,
|
||||
)
|
||||
reaction_payload["target_id"] = str(
|
||||
(hint_match or {}).get("xmpp_message_id") or ""
|
||||
)
|
||||
|
||||
self.log.debug(
|
||||
"reaction-bridge xmpp-inbound actor=%s service=%s target_xmpp_id=%s emoji=%s remove=%s via=%s",
|
||||
sender_username,
|
||||
recipient_service,
|
||||
str(reaction_payload.get("target_id") or "") or "-",
|
||||
str(reaction_payload.get("emoji") or "") or "-",
|
||||
bool(reaction_payload.get("remove")),
|
||||
"xmpp:reactions" if parsed_reaction else "greentext",
|
||||
)
|
||||
|
||||
bridge = transport.resolve_bridge_from_xmpp(
|
||||
user_id=identifier.user_id,
|
||||
person_id=identifier.person_id,
|
||||
service=recipient_service,
|
||||
xmpp_message_id=str(reaction_payload.get("target_id") or ""),
|
||||
)
|
||||
if not bridge:
|
||||
bridge = await history.resolve_bridge_ref(
|
||||
user=identifier.user,
|
||||
identifier=identifier,
|
||||
source_service=recipient_service,
|
||||
xmpp_message_id=str(reaction_payload.get("target_id") or ""),
|
||||
)
|
||||
if not bridge:
|
||||
self.log.warning(
|
||||
"reaction-bridge xmpp-resolve-miss actor=%s service=%s target_xmpp_id=%s",
|
||||
sender_username,
|
||||
recipient_service,
|
||||
str(reaction_payload.get("target_id") or "") or "-",
|
||||
)
|
||||
sym("Could not find upstream message for this reaction.")
|
||||
return
|
||||
|
||||
sent_ok = await transport.send_reaction(
|
||||
recipient_service,
|
||||
identifier.identifier,
|
||||
emoji=str(reaction_payload.get("emoji") or ""),
|
||||
target_message_id=str((bridge or {}).get("upstream_message_id") or ""),
|
||||
target_timestamp=int((bridge or {}).get("upstream_ts") or 0),
|
||||
target_author=str((bridge or {}).get("upstream_author") or ""),
|
||||
remove=bool(reaction_payload.get("remove")),
|
||||
)
|
||||
if not sent_ok:
|
||||
self.log.warning(
|
||||
"reaction-bridge upstream-send-failed actor=%s service=%s recipient=%s target_upstream_id=%s target_upstream_ts=%s",
|
||||
sender_username,
|
||||
recipient_service,
|
||||
identifier.identifier,
|
||||
str((bridge or {}).get("upstream_message_id") or "") or "-",
|
||||
int((bridge or {}).get("upstream_ts") or 0),
|
||||
)
|
||||
sym("Upstream protocol did not accept this reaction.")
|
||||
return
|
||||
|
||||
await history.apply_reaction(
|
||||
user=identifier.user,
|
||||
identifier=identifier,
|
||||
target_message_id=str((bridge or {}).get("local_message_id") or ""),
|
||||
target_ts=int((bridge or {}).get("upstream_ts") or 0),
|
||||
emoji=str(reaction_payload.get("emoji") or ""),
|
||||
source_service="xmpp",
|
||||
actor=sender_username,
|
||||
remove=bool(reaction_payload.get("remove")),
|
||||
payload={
|
||||
"target_xmpp_id": str(reaction_payload.get("target_id") or ""),
|
||||
"xmpp_message_id": xmpp_message_id,
|
||||
},
|
||||
)
|
||||
self.log.debug(
|
||||
"reaction-bridge xmpp-apply-ok actor=%s service=%s local_message_id=%s",
|
||||
sender_username,
|
||||
recipient_service,
|
||||
str((bridge or {}).get("local_message_id") or "") or "-",
|
||||
)
|
||||
return
|
||||
|
||||
# tss = await identifier.send(body, attachments=attachments)
|
||||
# AM FIXING https://git.zm.is/XF/GIA/issues/5
|
||||
session, _ = await sync_to_async(ChatSession.objects.get_or_create)(
|
||||
@@ -1028,7 +1234,7 @@ class XMPPComponent(ComponentXMPP):
|
||||
user=identifier.user,
|
||||
)
|
||||
self.log.debug("Storing outbound XMPP message in history")
|
||||
await history.store_message(
|
||||
local_message = await history.store_message(
|
||||
session=session,
|
||||
sender="XMPP",
|
||||
text=body,
|
||||
@@ -1051,8 +1257,14 @@ class XMPPComponent(ComponentXMPP):
|
||||
payload={"reason": "message_sent"},
|
||||
)
|
||||
await identifier.send(
|
||||
body,
|
||||
relay_body,
|
||||
attachments,
|
||||
metadata={
|
||||
"xmpp_source_id": xmpp_message_id,
|
||||
"xmpp_source_ts": int(now().timestamp() * 1000),
|
||||
"xmpp_body": relay_body,
|
||||
"legacy_message_id": str(local_message.id),
|
||||
},
|
||||
)
|
||||
self.log.debug("Message sent unaltered")
|
||||
return
|
||||
@@ -1061,7 +1273,7 @@ class XMPPComponent(ComponentXMPP):
|
||||
chat_history = await history.get_chat_history(session)
|
||||
await utils.update_last_interaction(session)
|
||||
prompt = replies.generate_mutate_reply_prompt(
|
||||
body,
|
||||
relay_body,
|
||||
identifier.person,
|
||||
manip,
|
||||
chat_history,
|
||||
@@ -1082,6 +1294,12 @@ class XMPPComponent(ComponentXMPP):
|
||||
await identifier.send(
|
||||
result,
|
||||
attachments,
|
||||
metadata={
|
||||
"xmpp_source_id": xmpp_message_id,
|
||||
"xmpp_source_ts": int(now().timestamp() * 1000),
|
||||
"xmpp_body": result,
|
||||
"legacy_message_id": str(local_message.id),
|
||||
},
|
||||
)
|
||||
self.log.debug("Message sent with modifications")
|
||||
|
||||
@@ -1123,10 +1341,13 @@ class XMPPComponent(ComponentXMPP):
|
||||
)
|
||||
|
||||
# Send XMPP message immediately after successful upload
|
||||
await self.send_xmpp_message(
|
||||
xmpp_msg_id = await self.send_xmpp_message(
|
||||
recipient_jid, sender_jid, upload_url, attachment_url=upload_url
|
||||
)
|
||||
return upload_url
|
||||
return {
|
||||
"url": upload_url,
|
||||
"xmpp_message_id": xmpp_msg_id,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"Error uploading {att['filename']} to XMPP: {e}")
|
||||
@@ -1137,6 +1358,9 @@ class XMPPComponent(ComponentXMPP):
|
||||
):
|
||||
"""Sends an XMPP message with either text or an attachment URL."""
|
||||
msg = self.make_message(mto=recipient_jid, mfrom=sender_jid, mtype="chat")
|
||||
if not msg.get("id"):
|
||||
msg["id"] = uuid.uuid4().hex
|
||||
msg_id = str(msg.get("id") or "").strip()
|
||||
msg["body"] = body_text # Body must contain only text or the URL
|
||||
|
||||
if attachment_url:
|
||||
@@ -1148,6 +1372,127 @@ class XMPPComponent(ComponentXMPP):
|
||||
|
||||
self.log.debug("Sending XMPP message: %s", msg.xml)
|
||||
msg.send()
|
||||
return msg_id
|
||||
|
||||
async def send_xmpp_reaction(
|
||||
self,
|
||||
recipient_jid,
|
||||
sender_jid,
|
||||
*,
|
||||
target_xmpp_id: str,
|
||||
emoji: str,
|
||||
remove: bool = False,
|
||||
):
|
||||
msg = self.make_message(mto=recipient_jid, mfrom=sender_jid, mtype="chat")
|
||||
if not msg.get("id"):
|
||||
msg["id"] = uuid.uuid4().hex
|
||||
msg["body"] = ""
|
||||
reactions_node = ET.Element(
|
||||
"{urn:xmpp:reactions:0}reactions",
|
||||
{"id": str(target_xmpp_id or "").strip()},
|
||||
)
|
||||
if not remove and str(emoji or "").strip():
|
||||
reaction_node = ET.SubElement(
|
||||
reactions_node,
|
||||
"{urn:xmpp:reactions:0}reaction",
|
||||
)
|
||||
reaction_node.text = str(emoji)
|
||||
msg.xml.append(reactions_node)
|
||||
msg.send()
|
||||
return str(msg.get("id") or "").strip()
|
||||
|
||||
async def apply_external_reaction(
|
||||
self,
|
||||
user,
|
||||
person_identifier,
|
||||
*,
|
||||
source_service,
|
||||
emoji,
|
||||
remove,
|
||||
upstream_message_id="",
|
||||
upstream_ts=0,
|
||||
actor="",
|
||||
payload=None,
|
||||
):
|
||||
self.log.debug(
|
||||
"reaction-bridge external-in source=%s user=%s person=%s upstream_id=%s upstream_ts=%s emoji=%s remove=%s",
|
||||
source_service,
|
||||
user.id,
|
||||
person_identifier.person_id,
|
||||
str(upstream_message_id or "") or "-",
|
||||
int(upstream_ts or 0),
|
||||
str(emoji or "") or "-",
|
||||
bool(remove),
|
||||
)
|
||||
bridge = transport.resolve_bridge_from_upstream(
|
||||
user_id=user.id,
|
||||
person_id=person_identifier.person_id,
|
||||
service=source_service,
|
||||
upstream_message_id=str(upstream_message_id or ""),
|
||||
upstream_ts=int(upstream_ts or 0),
|
||||
)
|
||||
if not bridge:
|
||||
bridge = await history.resolve_bridge_ref(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
source_service=source_service,
|
||||
upstream_message_id=str(upstream_message_id or ""),
|
||||
upstream_author=str(actor or ""),
|
||||
upstream_ts=int(upstream_ts or 0),
|
||||
)
|
||||
if not bridge:
|
||||
self.log.warning(
|
||||
"reaction-bridge external-resolve-miss source=%s user=%s person=%s upstream_id=%s upstream_ts=%s",
|
||||
source_service,
|
||||
user.id,
|
||||
person_identifier.person_id,
|
||||
str(upstream_message_id or "") or "-",
|
||||
int(upstream_ts or 0),
|
||||
)
|
||||
return False
|
||||
|
||||
target_xmpp_id = str((bridge or {}).get("xmpp_message_id") or "").strip()
|
||||
if not target_xmpp_id:
|
||||
self.log.warning(
|
||||
"reaction-bridge external-target-missing source=%s user=%s person=%s",
|
||||
source_service,
|
||||
user.id,
|
||||
person_identifier.person_id,
|
||||
)
|
||||
return False
|
||||
|
||||
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_xmpp_reaction(
|
||||
recipient_jid,
|
||||
sender_jid,
|
||||
target_xmpp_id=target_xmpp_id,
|
||||
emoji=str(emoji or ""),
|
||||
remove=bool(remove),
|
||||
)
|
||||
await history.apply_reaction(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
target_message_id=str((bridge or {}).get("local_message_id") or ""),
|
||||
target_ts=int((bridge or {}).get("upstream_ts") or 0),
|
||||
emoji=str(emoji or ""),
|
||||
source_service=source_service,
|
||||
actor=str(actor or person_identifier.identifier),
|
||||
remove=bool(remove),
|
||||
payload=dict(payload or {}),
|
||||
)
|
||||
self.log.debug(
|
||||
"reaction-bridge external-apply-ok source=%s user=%s person=%s xmpp_id=%s local_message_id=%s",
|
||||
source_service,
|
||||
user.id,
|
||||
person_identifier.person_id,
|
||||
target_xmpp_id,
|
||||
str((bridge or {}).get("local_message_id") or "") or "-",
|
||||
)
|
||||
return True
|
||||
|
||||
async def send_chat_state(self, recipient_jid, sender_jid, started):
|
||||
"""Send XMPP chat-state update to the client."""
|
||||
@@ -1173,18 +1518,74 @@ class XMPPComponent(ComponentXMPP):
|
||||
await self.send_chat_state(recipient_jid, sender_jid, started)
|
||||
|
||||
async def send_from_external(
|
||||
self, user, person_identifier, text, is_outgoing_message, attachments=[]
|
||||
self,
|
||||
user,
|
||||
person_identifier,
|
||||
text,
|
||||
is_outgoing_message,
|
||||
attachments=[],
|
||||
source_ref=None,
|
||||
):
|
||||
"""Handles sending XMPP messages with text and attachments."""
|
||||
|
||||
sender_jid = f"{person_identifier.person.name.lower()}|{person_identifier.service}@{settings.XMPP_JID}"
|
||||
recipient_jid = f"{person_identifier.user.username}@{settings.XMPP_ADDRESS}"
|
||||
if is_outgoing_message:
|
||||
await self.send_xmpp_message(recipient_jid, sender_jid, f"YOU: {text}")
|
||||
xmpp_id = await self.send_xmpp_message(
|
||||
recipient_jid,
|
||||
sender_jid,
|
||||
f"YOU: {text}",
|
||||
)
|
||||
transport.record_bridge_mapping(
|
||||
user_id=user.id,
|
||||
person_id=person_identifier.person_id,
|
||||
service=person_identifier.service,
|
||||
xmpp_message_id=xmpp_id,
|
||||
xmpp_ts=int(time.time() * 1000),
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
text_preview=str(text or ""),
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
)
|
||||
await history.save_bridge_ref(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
source_service=person_identifier.service,
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
||||
xmpp_message_id=xmpp_id,
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
)
|
||||
|
||||
# Step 1: Send text message separately
|
||||
elif text:
|
||||
await self.send_xmpp_message(recipient_jid, sender_jid, text)
|
||||
xmpp_id = await self.send_xmpp_message(recipient_jid, sender_jid, text)
|
||||
transport.record_bridge_mapping(
|
||||
user_id=user.id,
|
||||
person_id=person_identifier.person_id,
|
||||
service=person_identifier.service,
|
||||
xmpp_message_id=xmpp_id,
|
||||
xmpp_ts=int(time.time() * 1000),
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
text_preview=str(text or ""),
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
)
|
||||
await history.save_bridge_ref(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
source_service=person_identifier.service,
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
||||
xmpp_message_id=xmpp_id,
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
)
|
||||
|
||||
if not attachments:
|
||||
return [] # No attachments to process
|
||||
@@ -1193,7 +1594,7 @@ class XMPPComponent(ComponentXMPP):
|
||||
valid_uploads = await self.request_upload_slots(recipient_jid, attachments)
|
||||
self.log.debug("Got upload slots")
|
||||
if not valid_uploads:
|
||||
self.log.warning("No valid upload slots obtained.")
|
||||
self.log.debug("No valid upload slots obtained; attachment relay skipped")
|
||||
return []
|
||||
|
||||
# Step 3: Upload each file and send its message immediately after upload
|
||||
@@ -1201,8 +1602,33 @@ class XMPPComponent(ComponentXMPP):
|
||||
self.upload_and_send(att, slot, recipient_jid, sender_jid)
|
||||
for att, slot in valid_uploads
|
||||
]
|
||||
uploaded_urls = await asyncio.gather(*upload_tasks) # Upload files concurrently
|
||||
return [url for url in uploaded_urls if url]
|
||||
uploaded_rows = await asyncio.gather(*upload_tasks) # Upload files concurrently
|
||||
normalized_rows = [dict(row or {}) for row in uploaded_rows if row]
|
||||
for row in normalized_rows:
|
||||
transport.record_bridge_mapping(
|
||||
user_id=user.id,
|
||||
person_id=person_identifier.person_id,
|
||||
service=person_identifier.service,
|
||||
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
||||
xmpp_ts=int(time.time() * 1000),
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
text_preview=str(row.get("url") or text or ""),
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
)
|
||||
await history.save_bridge_ref(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
source_service=person_identifier.service,
|
||||
local_message_id=str((source_ref or {}).get("legacy_message_id") or ""),
|
||||
local_ts=int((source_ref or {}).get("xmpp_source_ts") or int(time.time() * 1000)),
|
||||
xmpp_message_id=str(row.get("xmpp_message_id") or "").strip(),
|
||||
upstream_message_id=str((source_ref or {}).get("upstream_message_id") or ""),
|
||||
upstream_author=str((source_ref or {}).get("upstream_author") or ""),
|
||||
upstream_ts=int((source_ref or {}).get("upstream_ts") or 0),
|
||||
)
|
||||
return [str(row.get("url") or "").strip() for row in normalized_rows if str(row.get("url") or "").strip()]
|
||||
|
||||
|
||||
class XMPPClient(ClientBase):
|
||||
|
||||
Reference in New Issue
Block a user