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/.
88 lines
5.3 KiB
SQL
88 lines
5.3 KiB
SQL
-- 015: WP15 Telegram live channel archive. All identities are tenant pinned.
|
|
CREATE TABLE feed_subscriptions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
type text NOT NULL CHECK (type = 'telegram_channel'),
|
|
connection_id uuid NOT NULL,
|
|
channel_id text NOT NULL CHECK (btrim(channel_id) <> ''),
|
|
health text NOT NULL DEFAULT 'active' CHECK (health IN ('active', 'bot_removed', 'admin_required')),
|
|
health_checked_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (workspace_id, connection_id, channel_id),
|
|
UNIQUE (id, workspace_id),
|
|
FOREIGN KEY (connection_id, workspace_id) REFERENCES connections (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT
|
|
);
|
|
CREATE INDEX feed_subscriptions_channel ON feed_subscriptions (workspace_id, channel_id);
|
|
|
|
CREATE TABLE archived_feed_items (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
remote_ref text NOT NULL CHECK (remote_ref ~ '^(-?[0-9]+|@[A-Za-z0-9_]+):[0-9]+$'),
|
|
channel_id text NOT NULL,
|
|
message_id bigint NOT NULL CHECK (message_id > 0),
|
|
media_group_id text,
|
|
text_content text NOT NULL DEFAULT '',
|
|
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
|
|
revision integer NOT NULL DEFAULT 1 CHECK (revision > 0),
|
|
media_refs jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(media_refs) = 'array'),
|
|
attribution jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(attribution) = 'object'),
|
|
protected_content boolean NOT NULL DEFAULT false,
|
|
posted_at timestamptz NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (workspace_id, remote_ref),
|
|
UNIQUE (id, workspace_id),
|
|
CHECK (remote_ref = channel_id || ':' || message_id::text)
|
|
);
|
|
CREATE INDEX archived_feed_items_cursor ON archived_feed_items (workspace_id, posted_at DESC, id DESC);
|
|
|
|
CREATE TABLE archived_feed_revisions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
item_id uuid NOT NULL,
|
|
revision integer NOT NULL CHECK (revision > 0),
|
|
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
|
|
text_content text NOT NULL,
|
|
media_refs jsonb NOT NULL CHECK (jsonb_typeof(media_refs) = 'array'),
|
|
attribution jsonb NOT NULL CHECK (jsonb_typeof(attribution) = 'object'),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (workspace_id, item_id, revision),
|
|
FOREIGN KEY (item_id, workspace_id) REFERENCES archived_feed_items (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT
|
|
);
|
|
|
|
-- drafts predates tenant-pinned references.
|
|
ALTER TABLE drafts ADD CONSTRAINT drafts_id_workspace_key UNIQUE (id, workspace_id);
|
|
|
|
CREATE TABLE archived_feed_drafts (
|
|
workspace_id uuid NOT NULL,
|
|
item_id uuid NOT NULL,
|
|
draft_id uuid NOT NULL UNIQUE,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (workspace_id, item_id, draft_id),
|
|
FOREIGN KEY (item_id, workspace_id) REFERENCES archived_feed_items (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT,
|
|
FOREIGN KEY (draft_id, workspace_id) REFERENCES drafts (id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT
|
|
);
|
|
|
|
CREATE TRIGGER trg_feed_subscriptions_ws_immutable BEFORE UPDATE ON feed_subscriptions
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_archived_feed_items_ws_immutable BEFORE UPDATE ON archived_feed_items
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_archived_feed_revisions_ws_immutable BEFORE UPDATE ON archived_feed_revisions
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_archived_feed_drafts_ws_immutable BEFORE UPDATE ON archived_feed_drafts
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
|
|
ALTER TABLE feed_subscriptions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_items ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_revisions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_drafts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE feed_subscriptions FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_items FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_revisions FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE archived_feed_drafts FORCE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation ON feed_subscriptions USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON archived_feed_items USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON archived_feed_revisions USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON archived_feed_drafts 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 feed_subscriptions, archived_feed_items, archived_feed_revisions, archived_feed_drafts TO sp_app;
|