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/.
160 lines
8.2 KiB
PL/PgSQL
160 lines
8.2 KiB
PL/PgSQL
-- 023: WP7 bulk source import + live-settings guard.
|
|
--
|
|
-- Three additive pieces, one migration:
|
|
--
|
|
-- (1) `virtual_channel_sources` (013) is widened to admit *imported* sources.
|
|
-- 013's CHECK only modelled hand-entered HLS URLs and local playlists; a
|
|
-- PeerTube channel/live imported in bulk carries `remote_ref` +
|
|
-- `connection_id` and has an HLS URL only once a live is resolvable. The
|
|
-- dedupe key UNIQUE (workspace_id, virtual_channel_id, remote_ref) from
|
|
-- 013 is unchanged and stays the only dedupe authority: imports write
|
|
-- INSERT ... ON CONFLICT DO NOTHING RETURNING id.
|
|
--
|
|
-- (2) `virtual_channel_imports`: one row per bulk import request. The HTTP
|
|
-- request only parses + validates + creates the job row; resolution and
|
|
-- inserts run in the background, and the row is the progress/report
|
|
-- document (processed/total + imported/duplicate/invalid/failed counts
|
|
-- and a bounded per-entry outcome list). A `running` row whose heartbeat
|
|
-- (`updated_at`) went stale is reaped to `abandoned` on read, so a
|
|
-- process crash never leaves a job spinning forever.
|
|
--
|
|
-- (3) `virtual_channel_live_settings`: the channel's live output settings
|
|
-- (profile/resolution, latency, replay policy) behind an optimistic
|
|
-- concurrency `version`. Writers take the channel row FOR UPDATE (one
|
|
-- writer per channel), then CAS on `version`; a stale version or a
|
|
-- started live is a 409 with nothing written. The CHECKs mirror the
|
|
-- app-side validator (WP7 acceptance: permanentLive + saveReplay and an
|
|
-- out-of-range latencyMode are rejected by the DB on a direct insert
|
|
-- too), and a trigger keeps `version` monotone.
|
|
--
|
|
-- House style throughout: tenant-composite keys (id, workspace_id), composite
|
|
-- FKs that pin every child to one workspace's channel, reject_workspace_change
|
|
-- / reject_owner_change immutability, FORCE RLS with the NULLIF tenant policy,
|
|
-- grants to sp_app (003 adds the default-privilege side for future tables).
|
|
|
|
SET LOCAL lock_timeout = '5s';
|
|
|
|
-- (1) Imported sources: kind 'hls' now requires an HLS URL *or* a remote_ref
|
|
-- (a channel imported now, playback attached when its live starts). The
|
|
-- playlist arm is untouched: local, connectionless, remote_ref-less.
|
|
ALTER TABLE virtual_channel_sources DROP CONSTRAINT virtual_channel_sources_check;
|
|
ALTER TABLE virtual_channel_sources ADD CONSTRAINT virtual_channel_sources_check CHECK (
|
|
(
|
|
kind = 'hls' AND playlist_loop IS NULL
|
|
AND (hls_url IS NOT NULL OR remote_ref IS NOT NULL)
|
|
) OR (
|
|
kind = 'playlist' AND hls_url IS NULL AND remote_ref IS NULL
|
|
AND connection_id IS NULL AND playlist_loop IS NOT NULL
|
|
)
|
|
);
|
|
|
|
-- (2) Import jobs.
|
|
CREATE TABLE virtual_channel_imports (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
virtual_channel_id uuid NOT NULL,
|
|
-- Connection the import resolves through (default host for bare handles).
|
|
connection_id uuid,
|
|
status text NOT NULL DEFAULT 'running'
|
|
CHECK (status IN ('running', 'completed', 'failed', 'abandoned')),
|
|
-- total counts every input row (valid or not); processed advances per row.
|
|
total int NOT NULL DEFAULT 0 CHECK (total >= 0 AND total <= 5000),
|
|
processed int NOT NULL DEFAULT 0 CHECK (processed >= 0 AND processed <= total),
|
|
imported int NOT NULL DEFAULT 0 CHECK (imported >= 0),
|
|
duplicate int NOT NULL DEFAULT 0 CHECK (duplicate >= 0),
|
|
invalid int NOT NULL DEFAULT 0 CHECK (invalid >= 0),
|
|
failed int NOT NULL DEFAULT 0 CHECK (failed >= 0),
|
|
-- Bounded per-entry report (first IMPORT_REPORT_ENTRY_LIMIT rows): what the
|
|
-- user pasted, what happened, why. Counts are always authoritative.
|
|
entries jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(entries) = 'array'),
|
|
error text,
|
|
created_by uuid,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
completed_at timestamptz,
|
|
UNIQUE (id, workspace_id),
|
|
FOREIGN KEY (workspace_id, virtual_channel_id) REFERENCES virtual_channels (workspace_id, id)
|
|
ON DELETE CASCADE ON UPDATE RESTRICT,
|
|
FOREIGN KEY (connection_id, workspace_id) REFERENCES connections (id, workspace_id)
|
|
ON DELETE NO ACTION ON UPDATE RESTRICT DEFERRABLE INITIALLY DEFERRED
|
|
);
|
|
|
|
CREATE TRIGGER trg_vci_ws_immutable BEFORE UPDATE ON virtual_channel_imports
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_vci_owner_immutable BEFORE UPDATE ON virtual_channel_imports
|
|
FOR EACH ROW EXECUTE FUNCTION reject_owner_change('virtual_channel_id');
|
|
|
|
ALTER TABLE virtual_channel_imports ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE virtual_channel_imports FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation ON virtual_channel_imports
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
|
|
CREATE INDEX virtual_channel_imports_channel
|
|
ON virtual_channel_imports (workspace_id, virtual_channel_id, created_at DESC);
|
|
|
|
-- (3) Channel live settings, version-fenced.
|
|
CREATE TABLE virtual_channel_live_settings (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
virtual_channel_id uuid NOT NULL,
|
|
-- Output shape: a named preset, or an explicit triple (custom).
|
|
output_profile text NOT NULL DEFAULT '720p30'
|
|
CHECK (output_profile IN ('720p30', '1080p30', '1080p60', 'custom')),
|
|
width int CHECK (width IS NULL OR (width >= 16 AND width <= 7680 AND width % 2 = 0)),
|
|
height int CHECK (height IS NULL OR (height >= 16 AND height <= 4320 AND height % 2 = 0)),
|
|
fps int CHECK (fps IS NULL OR (fps >= 1 AND fps <= 120)),
|
|
-- LiveVideoLatencyMode: 1=Default, 2=High latency, 3=Small latency.
|
|
latency_mode int NOT NULL DEFAULT 1 CHECK (latency_mode IN (1, 2, 3)),
|
|
permanent_live boolean NOT NULL DEFAULT false,
|
|
save_replay boolean NOT NULL DEFAULT false,
|
|
-- Replay privacy etc.; only meaningful while save_replay is on.
|
|
replay_settings jsonb CHECK (replay_settings IS NULL OR jsonb_typeof(replay_settings) = 'object'),
|
|
-- Latch: once the channel's live has started, settings are frozen (409).
|
|
live_started_at timestamptz,
|
|
-- Optimistic concurrency: every accepted write bumps version exactly once.
|
|
version bigint NOT NULL DEFAULT 0 CHECK (version >= 0),
|
|
updated_by uuid,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (workspace_id, virtual_channel_id),
|
|
UNIQUE (id, workspace_id),
|
|
FOREIGN KEY (workspace_id, virtual_channel_id) REFERENCES virtual_channels (workspace_id, id)
|
|
ON DELETE CASCADE ON UPDATE RESTRICT,
|
|
CHECK (
|
|
(output_profile <> 'custom' AND width IS NULL AND height IS NULL AND fps IS NULL)
|
|
OR (output_profile = 'custom' AND width IS NOT NULL AND height IS NOT NULL AND fps IS NOT NULL)
|
|
),
|
|
-- PeerTube rejects saveReplay + permanentLive (400); we refuse it earlier
|
|
-- and the DB refuses it for direct inserts.
|
|
CHECK (NOT (permanent_live AND save_replay)),
|
|
CHECK (save_replay OR replay_settings IS NULL)
|
|
);
|
|
|
|
CREATE TRIGGER trg_vcls_ws_immutable BEFORE UPDATE ON virtual_channel_live_settings
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_vcls_owner_immutable BEFORE UPDATE ON virtual_channel_live_settings
|
|
FOR EACH ROW EXECUTE FUNCTION reject_owner_change('virtual_channel_id');
|
|
|
|
-- version never regresses; a stale CAS writer cannot rewind the fence.
|
|
CREATE FUNCTION reject_live_settings_version_regression() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.version < OLD.version THEN
|
|
RAISE EXCEPTION 'live settings version cannot decrease' USING ERRCODE = '23514';
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
|
|
CREATE TRIGGER trg_vcls_version_monotone BEFORE UPDATE ON virtual_channel_live_settings
|
|
FOR EACH ROW EXECUTE FUNCTION reject_live_settings_version_regression();
|
|
|
|
ALTER TABLE virtual_channel_live_settings ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE virtual_channel_live_settings FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation ON virtual_channel_live_settings
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON virtual_channel_imports, virtual_channel_live_settings TO sp_app;
|