Lightweight containerized prosody tooling + moved auth scripts + xmpp reconnect/auth stabilization

This commit is contained in:
2026-03-05 02:18:12 +00:00
parent 0718a06c19
commit 2140c5facf
69 changed files with 3767 additions and 144 deletions

View File

@@ -642,6 +642,12 @@ class HandleMessage(Command):
actor=(
effective_source_uuid or effective_source_number or ""
),
target_author=str(
(reaction_payload.get("raw") or {}).get("targetAuthorUuid")
or (reaction_payload.get("raw") or {}).get("targetAuthor")
or (reaction_payload.get("raw") or {}).get("targetAuthorNumber")
or ""
),
remove=bool(reaction_payload.get("remove")),
payload=reaction_payload.get("raw") or {},
)
@@ -1308,6 +1314,12 @@ class SignalClient(ClientBase):
emoji=str(reaction_payload.get("emoji") or ""),
source_service="signal",
actor=(source_uuid or source_number or ""),
target_author=str(
(reaction_payload.get("raw") or {}).get("targetAuthorUuid")
or (reaction_payload.get("raw") or {}).get("targetAuthor")
or (reaction_payload.get("raw") or {}).get("targetAuthorNumber")
or ""
),
remove=bool(reaction_payload.get("remove")),
payload=reaction_payload.get("raw") or {},
)
@@ -1453,6 +1465,12 @@ class SignalClient(ClientBase):
emoji=str(reaction_payload.get("emoji") or ""),
source_service="signal",
actor=(source_uuid or source_number or ""),
target_author=str(
(reaction_payload.get("raw") or {}).get("targetAuthorUuid")
or (reaction_payload.get("raw") or {}).get("targetAuthor")
or (reaction_payload.get("raw") or {}).get("targetAuthorNumber")
or ""
),
remove=bool(reaction_payload.get("remove")),
payload=reaction_payload.get("raw") or {},
)

View File

@@ -240,13 +240,17 @@ async def send_reaction(
):
base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
sender_number = settings.SIGNAL_NUMBER
if not recipient_uuid or not target_timestamp:
normalized_recipient = normalize_signal_recipient(recipient_uuid)
normalized_target_author = normalize_signal_recipient(
str(target_author or normalized_recipient)
)
if not normalized_recipient or not target_timestamp:
return False
payload = {
"recipient": recipient_uuid,
"recipient": normalized_recipient,
"reaction": str(emoji or ""),
"target_author": str(target_author or recipient_uuid),
"target_author": normalized_target_author,
"timestamp": int(target_timestamp),
"remove": bool(remove),
}

View File

@@ -17,6 +17,7 @@ from django.core.cache import cache
from core.clients import signalapi
from core.messaging import media_bridge
from core.transports.capabilities import supports, unsupported_reason
from core.util import logs
log = logs.get_logger("transport")
@@ -32,6 +33,10 @@ def _service_key(service: str) -> str:
return str(service or "").strip().lower()
def _capability_checks_enabled() -> bool:
return bool(getattr(settings, "CAPABILITY_ENFORCEMENT_ENABLED", True))
def _runtime_key(service: str) -> str:
return f"gia:service:runtime:{_service_key(service)}"
@@ -898,6 +903,10 @@ async def send_reaction(
remove: bool = False,
):
service_key = _service_key(service)
if _capability_checks_enabled() and not supports(service_key, "reactions"):
reason = unsupported_reason(service_key, "reactions")
log.warning("capability-check failed service=%s feature=reactions: %s", service_key, reason)
return False
if not str(emoji or "").strip() and not remove:
return False
@@ -968,6 +977,13 @@ async def send_reaction(
async def start_typing(service: str, recipient: str):
service_key = _service_key(service)
if _capability_checks_enabled() and not supports(service_key, "typing"):
log.warning(
"capability-check failed service=%s feature=typing: %s",
service_key,
unsupported_reason(service_key, "typing"),
)
return False
if service_key == "signal":
await signalapi.start_typing(recipient)
return True
@@ -998,6 +1014,13 @@ async def start_typing(service: str, recipient: str):
async def stop_typing(service: str, recipient: str):
service_key = _service_key(service)
if _capability_checks_enabled() and not supports(service_key, "typing"):
log.warning(
"capability-check failed service=%s feature=typing: %s",
service_key,
unsupported_reason(service_key, "typing"),
)
return False
if service_key == "signal":
await signalapi.stop_typing(recipient)
return True

View File

@@ -135,6 +135,9 @@ class XMPPComponent(ComponentXMPP):
def __init__(self, ur, jid, secret, server, port):
self.ur = ur
self._upload_config_warned = False
self._reconnect_task = None
self._reconnect_delay_seconds = 1.0
self._reconnect_delay_max_seconds = 30.0
self.log = logs.get_logger("XMPP")
@@ -821,14 +824,49 @@ class XMPPComponent(ComponentXMPP):
async def session_start(self, *args):
self.log.info("XMPP session started")
self._reconnect_delay_seconds = 1.0
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
self._reconnect_task = None
await self.enable_carbons()
async def _reconnect_loop(self):
try:
while True:
delay = float(self._reconnect_delay_seconds)
await asyncio.sleep(delay)
try:
self.log.info("XMPP reconnect attempt delay_s=%.1f", delay)
connected = self.connect()
if connected is False:
raise RuntimeError("connect returned false")
self.process(forever=False)
return
except Exception as exc:
self.log.warning("XMPP reconnect attempt failed: %s", exc)
self._reconnect_delay_seconds = min(
self._reconnect_delay_max_seconds,
max(1.0, float(self._reconnect_delay_seconds) * 2.0),
)
except asyncio.CancelledError:
return
finally:
self._reconnect_task = None
def _schedule_reconnect(self):
if self._reconnect_task and not self._reconnect_task.done():
return
self._reconnect_task = self.loop.create_task(self._reconnect_loop())
def on_disconnected(self, *args):
"""
Handles XMPP disconnection and triggers a reconnect loop.
"""
self.log.warning("XMPP disconnected, attempting to reconnect...")
self.connect()
self.log.warning(
"XMPP disconnected, scheduling reconnect attempt in %.1fs",
float(self._reconnect_delay_seconds),
)
self._schedule_reconnect()
async def request_upload_slot(self, recipient, filename, content_type, size):
"""
@@ -1716,7 +1754,7 @@ class XMPPClient(ClientBase):
self.client.loop = self.loop
self.client.connect()
# self.client.process()
self.client.process(forever=False)
async def start_typing_for_person(self, user, person_identifier):
await self.client.send_typing_for_person(user, person_identifier, True)