Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
103 lines
5.8 KiB
PL/PgSQL
103 lines
5.8 KiB
PL/PgSQL
-- 018 is retained unchanged: local anp_publishing_it records its application.
|
|
-- These corrections use independently tracked resumable stages.
|
|
-- wp3:phase expand transaction
|
|
SET LOCAL lock_timeout = '5s';
|
|
ALTER TABLE publishing_attempts ADD COLUMN first_dispatch_started_at timestamptz;
|
|
CREATE OR REPLACE FUNCTION publishing_attempts_guard() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF (NEW.schedule_id, NEW.draft_id, NEW.connection_id, NEW.attempt_no, NEW.idempotency_key,
|
|
NEW.schedule_generation, NEW.post_snapshot, NEW.post_hash, NEW.scheduled_by_user_id,
|
|
NEW.required_permission, NEW.approved_revision, NEW.reviewer_user_id,
|
|
NEW.credential_id, NEW.credential_generation, NEW.created_at)
|
|
IS DISTINCT FROM
|
|
(OLD.schedule_id, OLD.draft_id, OLD.connection_id, OLD.attempt_no, OLD.idempotency_key,
|
|
OLD.schedule_generation, OLD.post_snapshot, OLD.post_hash, OLD.scheduled_by_user_id,
|
|
OLD.required_permission, OLD.approved_revision, OLD.reviewer_user_id,
|
|
OLD.credential_id, OLD.credential_generation, OLD.created_at) THEN
|
|
RAISE EXCEPTION 'publishing attempt snapshot is immutable' USING ERRCODE = '23514';
|
|
END IF;
|
|
IF OLD.status IN ('completed', 'abandoned') AND NEW.status IS DISTINCT FROM OLD.status THEN
|
|
RAISE EXCEPTION 'publishing attempt % is terminal', OLD.status USING ERRCODE = '23514';
|
|
END IF;
|
|
IF OLD.first_dispatch_started_at IS NOT NULL AND NEW.first_dispatch_started_at IS DISTINCT FROM OLD.first_dispatch_started_at THEN
|
|
RAISE EXCEPTION 'first dispatch time is immutable' USING ERRCODE = '23514';
|
|
END IF;
|
|
IF NEW.dispatch_count < OLD.dispatch_count THEN
|
|
RAISE EXCEPTION 'dispatch_count cannot decrease' USING ERRCODE = '23514';
|
|
END IF;
|
|
IF NEW.provider_retry_deadline IS DISTINCT FROM OLD.provider_retry_deadline
|
|
AND OLD.provider_retry_deadline IS NOT NULL THEN
|
|
RAISE EXCEPTION 'provider_retry_deadline is fixed at first dispatch' USING ERRCODE = '23514';
|
|
END IF;
|
|
IF NEW.owner_token IS NOT NULL AND NEW.owner_token IS DISTINCT FROM OLD.owner_token THEN
|
|
NEW.claim_epoch := OLD.claim_epoch + 1;
|
|
ELSE
|
|
NEW.claim_epoch := OLD.claim_epoch;
|
|
END IF;
|
|
NEW.updated_at := now();
|
|
RETURN NEW;
|
|
END $$;
|
|
|
|
CREATE OR REPLACE FUNCTION drafts_revision_fence() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.publication_attempt < OLD.publication_attempt THEN
|
|
RAISE EXCEPTION 'publication_attempt cannot decrease' USING ERRCODE = '23514';
|
|
END IF;
|
|
IF NEW.post IS DISTINCT FROM OLD.post THEN
|
|
IF EXISTS (SELECT 1 FROM publishing_attempts a
|
|
WHERE a.workspace_id = OLD.workspace_id AND a.draft_id = OLD.id
|
|
AND (a.status = 'unknown'
|
|
OR (a.status IN ('ready', 'in_progress') AND a.dispatch_started_at IS NOT NULL))) THEN
|
|
RAISE EXCEPTION 'draft content is locked while a dispatch is unsettled' USING ERRCODE = '55000';
|
|
END IF;
|
|
NEW.revision := OLD.revision + 1;
|
|
NEW.approved_revision := NULL;
|
|
NEW.approved_hash := NULL;
|
|
NEW.approved_at := NULL;
|
|
NEW.reviewer_user_id := NULL;
|
|
ELSE
|
|
NEW.revision := OLD.revision;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
-- wp3:phase dispatch_backfill batch
|
|
WITH batch AS (
|
|
SELECT id FROM publishing_attempts WHERE first_dispatch_started_at IS NULL AND dispatch_count > 0 LIMIT 1000 FOR UPDATE
|
|
)
|
|
UPDATE publishing_attempts a SET first_dispatch_started_at = CASE WHEN a.dispatch_count > 1 OR a.dispatch_started_at IS NULL THEN a.created_at ELSE a.dispatch_started_at END FROM batch WHERE a.id = batch.id;
|
|
-- wp3:phase receipt_recovery_index concurrent
|
|
CREATE INDEX CONCURRENTLY publishing_receipts_recovery ON publishing_receipts (workspace_id, attempt_id, received_at);
|
|
-- wp3:phase cutover transaction
|
|
SET LOCAL lock_timeout = '5s';
|
|
DROP FUNCTION publishing_due_schedules(integer);
|
|
CREATE FUNCTION publishing_due_schedules(p_limit integer DEFAULT 100)
|
|
RETURNS TABLE(workspace_id uuid, schedule_id uuid, idempotency_key text, generation bigint, publish_at timestamptz, evidence_only boolean)
|
|
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
|
SELECT due.workspace_id, due.schedule_id, due.idempotency_key, due.generation, due.publish_at, due.evidence_only
|
|
FROM (
|
|
SELECT sp.workspace_id, sp.id AS schedule_id, sp.idempotency_key, sp.generation,
|
|
sp.publish_at, sp.next_attempt_at AS due_at, false AS evidence_only
|
|
FROM scheduled_publications sp
|
|
WHERE sp.state = 'pending' AND sp.execution_version = 1 AND sp.next_attempt_at <= now() AND NOT EXISTS (SELECT 1 FROM publishing_attempts a JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id WHERE a.idempotency_key = sp.idempotency_key AND a.workspace_id = sp.workspace_id)
|
|
UNION ALL
|
|
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
|
|
sp.publish_at, a.lease_until AS due_at, false AS evidence_only
|
|
FROM publishing_attempts a
|
|
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
|
|
WHERE a.status = 'in_progress' AND a.lease_until < now() AND NOT EXISTS (SELECT 1 FROM publishing_receipts r WHERE r.attempt_id = a.id AND r.workspace_id = a.workspace_id)
|
|
UNION ALL
|
|
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
|
|
sp.publish_at, r.received_at AS due_at, true AS evidence_only
|
|
FROM publishing_attempts a
|
|
JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id
|
|
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
|
|
WHERE (a.status IN ('ready', 'failed', 'unknown') OR (a.status = 'in_progress' AND a.lease_until < now()))
|
|
) due
|
|
ORDER BY due.due_at, due.schedule_id
|
|
LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 100)
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION publishing_due_schedules(integer) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION publishing_due_schedules(integer) TO sp_app;
|