Add bulk calendar invitation delivery
This commit is contained in:
@@ -14,6 +14,7 @@ from govoplan_campaign.backend.schemas import (
|
||||
)
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.postbox import PostboxDeliveryReceiptSummaryRef
|
||||
from govoplan_core.core.calendar import CalendarInvitationRef
|
||||
from govoplan_core.core.change_sequence import (
|
||||
encode_sequence_watermark,
|
||||
max_sequence_id,
|
||||
@@ -49,6 +50,7 @@ from govoplan_campaign.backend.response_security import (
|
||||
public_campaign_payload,
|
||||
public_delivery_result_message,
|
||||
)
|
||||
from govoplan_campaign.backend.integrations import calendar_integration
|
||||
|
||||
|
||||
from govoplan_campaign.backend.route_support import (
|
||||
@@ -70,6 +72,7 @@ def _job_summary_payload(
|
||||
job: CampaignJob,
|
||||
*,
|
||||
reviewed_keys: set[str] | None = None,
|
||||
calendar_invitation: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
review_key = _job_review_key(job)
|
||||
return {
|
||||
@@ -116,12 +119,18 @@ def _job_summary_payload(
|
||||
for item in (job.resolved_attachments or [])
|
||||
if isinstance(item, dict)
|
||||
),
|
||||
"calendar_invitation": calendar_invitation
|
||||
or _calendar_invitation_state_from_provenance(job),
|
||||
}
|
||||
|
||||
|
||||
def _job_detail_payload(job: CampaignJob) -> dict[str, object]:
|
||||
def _job_detail_payload(
|
||||
job: CampaignJob,
|
||||
*,
|
||||
calendar_invitation: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
**_job_summary_payload(job),
|
||||
**_job_summary_payload(job, calendar_invitation=calendar_invitation),
|
||||
"message_id_header": job.message_id_header,
|
||||
"issues": job.issues_snapshot or [],
|
||||
"attachments": public_campaign_payload(job.resolved_attachments or []),
|
||||
@@ -135,6 +144,105 @@ def _job_detail_payload(job: CampaignJob) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _calendar_invitation_state_from_provenance(
|
||||
job: CampaignJob,
|
||||
) -> dict[str, object] | None:
|
||||
provenance = (
|
||||
job.delivery_provenance
|
||||
if isinstance(getattr(job, "delivery_provenance", None), dict)
|
||||
else {}
|
||||
)
|
||||
invitation = provenance.get("calendar_invitation")
|
||||
if not isinstance(invitation, dict):
|
||||
return None
|
||||
request = invitation.get("request")
|
||||
request_payload = request if isinstance(request, dict) else {}
|
||||
return {
|
||||
"available": False,
|
||||
"state": str(invitation.get("state") or "prepared"),
|
||||
"correlation_id": request_payload.get("correlation_id"),
|
||||
"calendar_id": invitation.get("calendar_id")
|
||||
or request_payload.get("calendar_id"),
|
||||
"uid": invitation.get("uid"),
|
||||
"rsvp_status": "NEEDS-ACTION",
|
||||
"attendees": request_payload.get("attendees") or [],
|
||||
"last_error": invitation.get("last_error"),
|
||||
"degraded_reasons": invitation.get("degraded_reasons") or [],
|
||||
}
|
||||
|
||||
|
||||
def _calendar_invitation_payload(ref: CalendarInvitationRef) -> dict[str, object]:
|
||||
attendees = [dict(item) for item in ref.attendees]
|
||||
statuses = [_calendar_attendee_status(item) for item in attendees]
|
||||
unique_statuses = sorted(set(statuses))
|
||||
rsvp_status = unique_statuses[0] if len(unique_statuses) == 1 else "MIXED"
|
||||
return {
|
||||
"available": True,
|
||||
"state": "mirrored",
|
||||
"correlation_id": ref.correlation_id,
|
||||
"event_id": ref.event_id,
|
||||
"calendar_id": ref.calendar_id,
|
||||
"uid": ref.uid,
|
||||
"rsvp_status": rsvp_status,
|
||||
"attendees": attendees,
|
||||
"external_state": ref.external_state,
|
||||
"outbox_operation_id": ref.outbox_operation_id,
|
||||
"reply_ingress": ref.reply_ingress,
|
||||
"recurrence_supported": ref.recurrence_supported,
|
||||
"degraded_reasons": list(ref.degraded_reasons),
|
||||
}
|
||||
|
||||
|
||||
def _calendar_attendee_status(attendee: Mapping[str, object]) -> str:
|
||||
params = attendee.get("params")
|
||||
if not isinstance(params, Mapping):
|
||||
return "NEEDS-ACTION"
|
||||
value = params.get("PARTSTAT")
|
||||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||
value = value[0] if value else None
|
||||
return str(value or "NEEDS-ACTION").upper()
|
||||
|
||||
|
||||
def _calendar_invitations_for_jobs(
|
||||
session: Session,
|
||||
jobs: Sequence[CampaignJob],
|
||||
) -> dict[str, dict[str, object]]:
|
||||
correlations_by_job: dict[str, str] = {}
|
||||
for job in jobs:
|
||||
provenance = (
|
||||
job.delivery_provenance
|
||||
if isinstance(getattr(job, "delivery_provenance", None), dict)
|
||||
else {}
|
||||
)
|
||||
invitation = provenance.get("calendar_invitation")
|
||||
request = invitation.get("request") if isinstance(invitation, dict) else None
|
||||
correlation_id = (
|
||||
str(request.get("correlation_id") or "").strip()
|
||||
if isinstance(request, dict)
|
||||
else ""
|
||||
)
|
||||
if correlation_id:
|
||||
correlations_by_job[job.id] = correlation_id
|
||||
if not correlations_by_job:
|
||||
return {}
|
||||
integration = calendar_integration()
|
||||
if not integration.available:
|
||||
return {}
|
||||
try:
|
||||
refs = integration.get_invitations(
|
||||
session,
|
||||
tenant_id=jobs[0].tenant_id,
|
||||
correlation_ids=tuple(correlations_by_job.values()),
|
||||
)
|
||||
except Exception: # Calendar reporting must not hide Campaign evidence.
|
||||
return {}
|
||||
return {
|
||||
job_id: _calendar_invitation_payload(refs[correlation_id])
|
||||
for job_id, correlation_id in correlations_by_job.items()
|
||||
if correlation_id in refs
|
||||
}
|
||||
|
||||
|
||||
def _job_attempts_payload(
|
||||
send_attempts: list[SendAttempt],
|
||||
imap_attempts: list[ImapAppendAttempt],
|
||||
@@ -766,8 +874,16 @@ def _campaign_jobs_page_response(
|
||||
)
|
||||
if changed_job_ids is not None:
|
||||
jobs = [job for job in jobs if job.id in changed_job_ids]
|
||||
calendar_invitations = _calendar_invitations_for_jobs(session, jobs)
|
||||
return CampaignJobsResponse(
|
||||
jobs=[_job_summary_payload(job, reviewed_keys=reviewed_keys) for job in jobs],
|
||||
jobs=[
|
||||
_job_summary_payload(
|
||||
job,
|
||||
reviewed_keys=reviewed_keys,
|
||||
calendar_invitation=calendar_invitations.get(job.id),
|
||||
)
|
||||
for job in jobs
|
||||
],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
|
||||
Reference in New Issue
Block a user