mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
73 lines
2.8 KiB
PL/PgSQL
73 lines
2.8 KiB
PL/PgSQL
-- Channel roster snapshot fence (keep in sync with migrations/0032).
|
|
-- Prevent mixed-version relay pods from publishing a stale NIP-29 member
|
|
-- snapshot after a newer canonical roster has been committed.
|
|
--
|
|
-- Old binaries already serialize kind 39002 replacement on the replacement
|
|
-- advisory key. This trigger adds the channel-membership key at INSERT time,
|
|
-- after that canonical key, and validates every p tag against the current
|
|
-- active membership set and roles. New binaries take both keys in the same
|
|
-- order before capture and replacement. Thus old and new writers remain
|
|
-- compatible during a rolling deploy.
|
|
CREATE OR REPLACE FUNCTION guard_channel_roster_snapshot()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
canonical_members TEXT[];
|
|
snapshot_members TEXT[];
|
|
BEGIN
|
|
IF NEW.kind <> 39002 OR NEW.channel_id IS NULL THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
PERFORM pg_advisory_xact_lock(hashtextextended(
|
|
'buzz_channel_membership:' || NEW.community_id::text || ':' || NEW.channel_id::text,
|
|
0
|
|
));
|
|
|
|
SELECT COALESCE(
|
|
array_agg(encode(cm.pubkey, 'hex') || ':' || cm.role::text ORDER BY cm.pubkey),
|
|
ARRAY[]::TEXT[]
|
|
)
|
|
INTO canonical_members
|
|
FROM channel_members cm
|
|
WHERE cm.community_id = NEW.community_id
|
|
AND cm.channel_id = NEW.channel_id
|
|
AND cm.removed_at IS NULL;
|
|
|
|
-- A roster is canonical only when every p tag uses the emitted four-field
|
|
-- shape, contains a 32-byte hex pubkey and valid authoritative role, has no
|
|
-- duplicate members, and exactly matches the active membership rows.
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
|
WHERE roster_tag.tag_json->>0 = 'p'
|
|
AND (
|
|
jsonb_array_length(roster_tag.tag_json) <> 4
|
|
OR COALESCE(roster_tag.tag_json->>1, '') !~ '^[0-9a-fA-F]{64}$'
|
|
OR roster_tag.tag_json->>2 <> ''
|
|
OR COALESCE(roster_tag.tag_json->>3, '') NOT IN ('owner', 'admin', 'bot', 'member', 'guest')
|
|
)
|
|
) THEN
|
|
RAISE EXCEPTION 'kind 39002 roster contains an invalid p tag'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
|
|
SELECT COALESCE(
|
|
array_agg(
|
|
lower((roster_tag.tag_json->>1)) || ':' || (roster_tag.tag_json->>3)
|
|
ORDER BY decode((roster_tag.tag_json->>1), 'hex')
|
|
),
|
|
ARRAY[]::TEXT[]
|
|
)
|
|
INTO snapshot_members
|
|
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
|
WHERE roster_tag.tag_json->>0 = 'p';
|
|
|
|
IF snapshot_members IS DISTINCT FROM canonical_members THEN
|
|
RAISE EXCEPTION 'kind 39002 roster does not match canonical channel membership'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|