Tactical Media Broadcast Hub — initial release
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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
160
apps/api/migrations/023_virtual_channel_import.sql
Normal file
160
apps/api/migrations/023_virtual_channel_import.sql
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
-- 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue