Implement reactions and image sync
This commit is contained in:
@@ -243,3 +243,309 @@ async def apply_read_receipts(
|
||||
await sync_to_async(message.save)(update_fields=dirty)
|
||||
updated += 1
|
||||
return updated
|
||||
|
||||
|
||||
async def apply_reaction(
|
||||
user,
|
||||
identifier,
|
||||
*,
|
||||
target_message_id="",
|
||||
target_ts=0,
|
||||
emoji="",
|
||||
source_service="",
|
||||
actor="",
|
||||
remove=False,
|
||||
payload=None,
|
||||
):
|
||||
log.debug(
|
||||
"reaction-bridge history-apply start user=%s person_identifier=%s target_message_id=%s target_ts=%s source=%s actor=%s remove=%s emoji=%s",
|
||||
getattr(user, "id", "-"),
|
||||
getattr(identifier, "id", "-"),
|
||||
str(target_message_id or "") or "-",
|
||||
int(target_ts or 0),
|
||||
str(source_service or "") or "-",
|
||||
str(actor or "") or "-",
|
||||
bool(remove),
|
||||
str(emoji or "") or "-",
|
||||
)
|
||||
queryset = Message.objects.filter(
|
||||
user=user,
|
||||
session__identifier=identifier,
|
||||
).select_related("session")
|
||||
|
||||
target = None
|
||||
target_uuid = str(target_message_id or "").strip()
|
||||
if target_uuid:
|
||||
target = await sync_to_async(
|
||||
lambda: queryset.filter(id=target_uuid).order_by("-ts").first()
|
||||
)()
|
||||
|
||||
if target is None:
|
||||
try:
|
||||
ts_value = int(target_ts or 0)
|
||||
except Exception:
|
||||
ts_value = 0
|
||||
if ts_value > 0:
|
||||
lower = ts_value - 10_000
|
||||
upper = ts_value + 10_000
|
||||
window_rows = await sync_to_async(list)(
|
||||
queryset.filter(ts__gte=lower, ts__lte=upper).order_by("ts")[:200]
|
||||
)
|
||||
if window_rows:
|
||||
target = min(
|
||||
window_rows,
|
||||
key=lambda row: (
|
||||
abs(int(row.ts or 0) - ts_value),
|
||||
-int(row.ts or 0),
|
||||
),
|
||||
)
|
||||
log.debug(
|
||||
"reaction-bridge history-apply ts-match target_ts=%s picked_message_id=%s picked_ts=%s candidates=%s",
|
||||
ts_value,
|
||||
str(target.id),
|
||||
int(target.ts or 0),
|
||||
len(window_rows),
|
||||
)
|
||||
|
||||
if target is None:
|
||||
log.warning(
|
||||
"reaction-bridge history-apply miss user=%s person_identifier=%s target_message_id=%s target_ts=%s",
|
||||
getattr(user, "id", "-"),
|
||||
getattr(identifier, "id", "-"),
|
||||
str(target_message_id or "") or "-",
|
||||
int(target_ts or 0),
|
||||
)
|
||||
return None
|
||||
|
||||
reactions = list((target.receipt_payload or {}).get("reactions") or [])
|
||||
reaction_key = (
|
||||
str(source_service or "").strip().lower(),
|
||||
str(actor or "").strip(),
|
||||
str(emoji or "").strip(),
|
||||
)
|
||||
|
||||
merged = []
|
||||
replaced = False
|
||||
for item in reactions:
|
||||
row = dict(item or {})
|
||||
row_key = (
|
||||
str(row.get("source_service") or "").strip().lower(),
|
||||
str(row.get("actor") or "").strip(),
|
||||
str(row.get("emoji") or "").strip(),
|
||||
)
|
||||
if row_key == reaction_key:
|
||||
row["removed"] = bool(remove)
|
||||
row["updated_at"] = int(target_ts or target.ts or 0)
|
||||
row["payload"] = dict(payload or {})
|
||||
merged.append(row)
|
||||
replaced = True
|
||||
continue
|
||||
merged.append(row)
|
||||
|
||||
if not replaced:
|
||||
merged.append(
|
||||
{
|
||||
"emoji": str(emoji or ""),
|
||||
"source_service": str(source_service or ""),
|
||||
"actor": str(actor or ""),
|
||||
"removed": bool(remove),
|
||||
"updated_at": int(target_ts or target.ts or 0),
|
||||
"payload": dict(payload or {}),
|
||||
}
|
||||
)
|
||||
|
||||
receipt_payload = dict(target.receipt_payload or {})
|
||||
receipt_payload["reactions"] = merged
|
||||
target.receipt_payload = receipt_payload
|
||||
await sync_to_async(target.save)(update_fields=["receipt_payload"])
|
||||
log.debug(
|
||||
"reaction-bridge history-apply ok message_id=%s reactions=%s",
|
||||
str(target.id),
|
||||
len(merged),
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def _iter_bridge_refs(receipt_payload, source_service):
|
||||
payload = dict(receipt_payload or {})
|
||||
refs = payload.get("bridge_refs") or {}
|
||||
rows = refs.get(str(source_service or "").strip().lower()) or []
|
||||
return [dict(row or {}) for row in rows if isinstance(row, dict)]
|
||||
|
||||
|
||||
def _set_bridge_refs(receipt_payload, source_service, rows):
|
||||
payload = dict(receipt_payload or {})
|
||||
refs = dict(payload.get("bridge_refs") or {})
|
||||
refs[str(source_service or "").strip().lower()] = list(rows or [])
|
||||
payload["bridge_refs"] = refs
|
||||
return payload
|
||||
|
||||
|
||||
async def save_bridge_ref(
|
||||
user,
|
||||
identifier,
|
||||
*,
|
||||
source_service,
|
||||
local_message_id="",
|
||||
local_ts=0,
|
||||
xmpp_message_id="",
|
||||
upstream_message_id="",
|
||||
upstream_author="",
|
||||
upstream_ts=0,
|
||||
):
|
||||
# TODO(edit-sync): persist upstream edit identifiers/version vectors here so
|
||||
# edit operations can target exact upstream message revisions.
|
||||
# TODO(delete-sync): persist upstream deletion tombstone metadata here and
|
||||
# keep bridge refs resolvable even after local message redaction.
|
||||
source_key = str(source_service or "").strip().lower()
|
||||
if not source_key:
|
||||
return None
|
||||
|
||||
queryset = Message.objects.filter(
|
||||
user=user,
|
||||
session__identifier=identifier,
|
||||
).select_related("session")
|
||||
|
||||
target = None
|
||||
message_id = str(local_message_id or "").strip()
|
||||
if message_id:
|
||||
target = await sync_to_async(
|
||||
lambda: queryset.filter(id=message_id).order_by("-ts").first()
|
||||
)()
|
||||
|
||||
if target is None:
|
||||
try:
|
||||
ts_value = int(local_ts or 0)
|
||||
except Exception:
|
||||
ts_value = 0
|
||||
if ts_value > 0:
|
||||
lower = ts_value - 10_000
|
||||
upper = ts_value + 10_000
|
||||
rows = await sync_to_async(list)(
|
||||
queryset.filter(ts__gte=lower, ts__lte=upper).order_by("-ts")[:200]
|
||||
)
|
||||
if rows:
|
||||
target = min(
|
||||
rows,
|
||||
key=lambda row: (
|
||||
abs(int(row.ts or 0) - ts_value),
|
||||
-int(row.ts or 0),
|
||||
),
|
||||
)
|
||||
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
row = {
|
||||
"xmpp_message_id": str(xmpp_message_id or "").strip(),
|
||||
"upstream_message_id": str(upstream_message_id or "").strip(),
|
||||
"upstream_author": str(upstream_author or "").strip(),
|
||||
"upstream_ts": int(upstream_ts or 0),
|
||||
"updated_at": int(local_ts or target.ts or 0),
|
||||
}
|
||||
|
||||
existing = _iter_bridge_refs(target.receipt_payload or {}, source_key)
|
||||
merged = []
|
||||
for item in existing:
|
||||
same_xmpp = row["xmpp_message_id"] and (
|
||||
str(item.get("xmpp_message_id") or "").strip() == row["xmpp_message_id"]
|
||||
)
|
||||
same_upstream = row["upstream_message_id"] and (
|
||||
str(item.get("upstream_message_id") or "").strip()
|
||||
== row["upstream_message_id"]
|
||||
)
|
||||
if same_xmpp or same_upstream:
|
||||
continue
|
||||
merged.append(item)
|
||||
merged.append(row)
|
||||
if len(merged) > 100:
|
||||
merged = merged[-100:]
|
||||
|
||||
target.receipt_payload = _set_bridge_refs(
|
||||
target.receipt_payload or {},
|
||||
source_key,
|
||||
merged,
|
||||
)
|
||||
await sync_to_async(target.save)(update_fields=["receipt_payload"])
|
||||
return {
|
||||
"local_message_id": str(target.id),
|
||||
"local_ts": int(target.ts or 0),
|
||||
**row,
|
||||
}
|
||||
|
||||
|
||||
async def resolve_bridge_ref(
|
||||
user,
|
||||
identifier,
|
||||
*,
|
||||
source_service,
|
||||
xmpp_message_id="",
|
||||
upstream_message_id="",
|
||||
upstream_author="",
|
||||
upstream_ts=0,
|
||||
):
|
||||
source_key = str(source_service or "").strip().lower()
|
||||
if not source_key:
|
||||
return None
|
||||
|
||||
rows = await sync_to_async(list)(
|
||||
Message.objects.filter(
|
||||
user=user,
|
||||
session__identifier=identifier,
|
||||
)
|
||||
.order_by("-ts")
|
||||
.only("id", "ts", "receipt_payload")[:500]
|
||||
)
|
||||
|
||||
xmpp_id = str(xmpp_message_id or "").strip()
|
||||
upstream_id = str(upstream_message_id or "").strip()
|
||||
author = str(upstream_author or "").strip()
|
||||
try:
|
||||
target_ts = int(upstream_ts or 0)
|
||||
except Exception:
|
||||
target_ts = 0
|
||||
|
||||
# 1) exact IDs first
|
||||
for message in rows:
|
||||
refs = _iter_bridge_refs(message.receipt_payload or {}, source_key)
|
||||
for ref in refs:
|
||||
if xmpp_id and str(ref.get("xmpp_message_id") or "").strip() == xmpp_id:
|
||||
return {
|
||||
"local_message_id": str(message.id),
|
||||
"local_ts": int(message.ts or 0),
|
||||
**dict(ref or {}),
|
||||
}
|
||||
if upstream_id and (
|
||||
str(ref.get("upstream_message_id") or "").strip() == upstream_id
|
||||
):
|
||||
return {
|
||||
"local_message_id": str(message.id),
|
||||
"local_ts": int(message.ts or 0),
|
||||
**dict(ref or {}),
|
||||
}
|
||||
|
||||
# 2) timestamp proximity with optional author tie-break
|
||||
best = None
|
||||
best_key = None
|
||||
if target_ts > 0:
|
||||
for message in rows:
|
||||
refs = _iter_bridge_refs(message.receipt_payload or {}, source_key)
|
||||
for ref in refs:
|
||||
row_ts = int(ref.get("upstream_ts") or 0)
|
||||
if row_ts <= 0:
|
||||
continue
|
||||
gap = abs(row_ts - target_ts)
|
||||
if gap > 15_000:
|
||||
continue
|
||||
row_author = str(ref.get("upstream_author") or "").strip()
|
||||
author_penalty = 0 if (not author or author == row_author) else 1
|
||||
freshness = int(ref.get("updated_at") or 0)
|
||||
key = (gap, author_penalty, -freshness)
|
||||
if best is None or key < best_key:
|
||||
best = {
|
||||
"local_message_id": str(message.id),
|
||||
"local_ts": int(message.ts or 0),
|
||||
**dict(ref or {}),
|
||||
}
|
||||
best_key = key
|
||||
return best
|
||||
|
||||
Reference in New Issue
Block a user