Dockerfile: add postgresql-client and bake db-customization/ SQL scripts into the image at /vnc/db-customization/. config/migrate.sh: detection + migration script with two modes: - pre-upgrade: runs idempotent prosody-13-new-deployment.sql on 13.0.x databases; skips 0.11.6 (unsafe pre-upgrade) and new deployments. - post-upgrade: waits for Prosody table, then runs the appropriate scripts — full 0.11.6->13.0.6 migration (rules-triggers + migration-once + new-deployment) or idempotent drift correction for 13.0.x. Helm chart: two Job templates (db-migration-pre-upgrade.yaml, db-migration-post-upgrade.yaml) gated by dbMigration.enabled (default true). Both reuse the Prosody image and DB credentials from existing values. backoffLimit: 0, hook-delete-policy: hook-succeeded. Also tracks the db-customization SQL files (previously untracked, now referenced by the Dockerfile ADD). Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
275 lines
11 KiB
PL/PgSQL
275 lines
11 KiB
PL/PgSQL
-- Idempotent rules, views and triggers for Prosody 13.0.6
|
|
--
|
|
-- This script creates the rules, views and trigger functions that had to be
|
|
-- adapted to the new MUC storage layout (affiliations stored as one row per
|
|
-- bare JID; `_affiliations` / `_occupants` keys gone).
|
|
--
|
|
-- It is IDEMPOTENT: every object is created with `or replace` / `drop if
|
|
-- exists` first, so it can be re-run safely on any 13.0.6 database.
|
|
--
|
|
-- It only creates the objects that CHANGED vs the 0.11.6 scripts
|
|
-- (`prosody-queries-noowner.sql.notifyfix` + `prosody-trigger-noowner.sql`).
|
|
-- It does NOT recreate the many rules/views/triggers that are unaffected by
|
|
-- the storage change — those remain defined by the original two scripts.
|
|
--
|
|
-- The `room_nick_jid_map` maintenance assumes all allowed clients use their
|
|
-- bare JID as their nickname in rooms, so the map is derived purely from
|
|
-- affiliations (nickname = "<room@host>/<bare-jid>").
|
|
--
|
|
-- Prerequisite: the `prosody` and `prosodyarchive` tables must already exist.
|
|
-- The script aborts if they do not.
|
|
|
|
\set ON_ERROR_STOP on
|
|
|
|
-- Guard: do nothing if the Prosody tables are missing.
|
|
do $$
|
|
begin
|
|
if not exists (select 1 from information_schema.tables where table_name = 'prosody') then
|
|
raise exception 'Table "prosody" does not exist. Apply the regular db-customization scripts first.';
|
|
end if;
|
|
if not exists (select 1 from information_schema.tables where table_name = 'prosodyarchive') then
|
|
raise exception 'Table "prosodyarchive" does not exist. Apply the regular db-customization scripts first.';
|
|
end if;
|
|
end $$;
|
|
|
|
-- ===========================================================================
|
|
-- VIEWS
|
|
-- ===========================================================================
|
|
|
|
-- `room_membership`: was `where prosody.key='_affiliations'` with
|
|
-- `jsonb_object_keys(value::jsonb)`. Affiliations are now one row per bare JID
|
|
-- (`key=<jid>`, `value=<affiliation string>`). Column shape is preserved so
|
|
-- all consumers (update_muc_recent, insert_unread_message_ids_muc,
|
|
-- insert_unread_mention_ids_muc, fprocess_messages muc_log branches) work
|
|
-- unchanged.
|
|
create or replace view room_membership as
|
|
select
|
|
prosody.key as username,
|
|
''::text as user_room_nickname,
|
|
prosody.host as host,
|
|
prosody.user as room
|
|
from prosody
|
|
where prosody.store = 'config'
|
|
and prosody.host like 'conference.%'
|
|
and prosody.key like '%@%'
|
|
and prosody.value in ('owner','admin','member','outcast','none');
|
|
|
|
-- `room_nicknames` is obsolete (read `key='_occupants'`, which no longer
|
|
-- exists). Drop it so stale references do not linger. `room_nick_jid_map`
|
|
-- (maintained below from affiliations) replaces it for nickname resolution.
|
|
drop view if exists room_nicknames cascade;
|
|
|
|
-- ===========================================================================
|
|
-- TRIGGER: group_owners maintenance (replaces the old `update_group_owners` rule)
|
|
-- ===========================================================================
|
|
--
|
|
-- Was a rule `on insert to prosody where key='_affiliations' ...`. Now owners
|
|
-- are rows with `key=<bare jid>`, `value='owner'`. A trigger is used instead
|
|
-- of a rule because `group_owners` already has an INSERT INSTEAD rule
|
|
-- (`upsert_group_owners`); combining DO-INSTEAD and DO rules on the same
|
|
-- insert produces ambiguous/conflicting behaviour. The trigger fires on the
|
|
-- `_data` row (always written on every room save) and reconciles
|
|
-- `group_owners` for the whole room, mirroring how `fupdate_room_nick_jid`
|
|
-- rebuilds `room_nick_jid_map`.
|
|
create or replace function fupdate_group_owners()
|
|
returns trigger AS
|
|
$BODY$
|
|
DECLARE
|
|
_room text;
|
|
BEGIN
|
|
_room := NEW.user || '@' || NEW.host;
|
|
|
|
-- remove owners that are no longer affiliated as owner
|
|
delete from group_owners
|
|
where room = _room
|
|
and owner not in (
|
|
select key from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value = 'owner'
|
|
);
|
|
|
|
-- upsert current owners. group_owners has an INSERT INSTEAD rule
|
|
-- (upsert_group_owners) so ON CONFLICT cannot be used; insert the room
|
|
-- if missing, otherwise update the owner.
|
|
insert into group_owners (room, owner, created, updated)
|
|
select
|
|
_room,
|
|
sub.owner,
|
|
extract(epoch from now())::integer,
|
|
extract(epoch from now())::integer
|
|
from (
|
|
select key as owner
|
|
from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value = 'owner'
|
|
order by key
|
|
limit 1
|
|
) sub
|
|
where not exists (
|
|
select 1 from group_owners go where go.room = _room
|
|
);
|
|
|
|
update group_owners
|
|
set owner = sub.owner,
|
|
updated = extract(epoch from now())::integer
|
|
from (
|
|
select key as owner
|
|
from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value = 'owner'
|
|
order by key
|
|
limit 1
|
|
) sub
|
|
where group_owners.room = _room
|
|
and group_owners.owner <> sub.owner;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$BODY$
|
|
LANGUAGE plpgsql VOLATILE;
|
|
|
|
drop trigger if exists update_group_owners on prosody;
|
|
create trigger update_group_owners after insert on prosody
|
|
for each row
|
|
when (NEW.store = 'config' and NEW.key = '_data' and NEW.host like 'conference.%')
|
|
execute procedure fupdate_group_owners();
|
|
|
|
-- ===========================================================================
|
|
-- RULES ON `prosodyarchive`
|
|
-- ===========================================================================
|
|
|
|
-- `update_room_nick_jid_map` refreshed `room_nick_jid_map` from the now-dropped
|
|
-- `room_nicknames` view on every `muc_log` insert. With `_occupants` gone it
|
|
-- is dead; `room_nick_jid_map` is maintained by the `fupdate_room_nick_jid`
|
|
-- trigger on the `config` store plus the (unchanged) invite rules.
|
|
drop rule if exists update_room_nick_jid_map on prosodyarchive cascade;
|
|
|
|
-- ===========================================================================
|
|
-- TRIGGER FUNCTION: fupdate_room_nick_jid
|
|
-- ===========================================================================
|
|
|
|
-- Was: trigger `when (NEW.key='_affiliations')`, body rebuilt the room's map
|
|
-- from `jsonb_object_keys(NEW.value::jsonb)`. Now: there is no `_affiliations`
|
|
-- row; affiliations are one row per bare JID.
|
|
--
|
|
-- Approach: fire on the `_data` row, which is always written on every room
|
|
-- save (it is always present in `freeze()` output), and rebuild
|
|
-- `room_nick_jid_map` from the current set of affiliation rows for the room.
|
|
-- (The kv `set` DELETEs then re-inserts all rows in one transaction; the
|
|
-- `_data` row is inserted after the affiliation rows, so they are already
|
|
-- present when this trigger fires.)
|
|
--
|
|
-- Nickname = "<room@host>/<bare-jid>" — bare JID is the only nickname used by
|
|
-- allowed clients, so the map is derived purely from affiliations.
|
|
create or replace function fupdate_room_nick_jid()
|
|
returns trigger AS
|
|
$BODY$
|
|
DECLARE
|
|
_room text;
|
|
BEGIN
|
|
_room := NEW.user || '@' || NEW.host;
|
|
|
|
-- remove mapping entries for users no longer affiliated
|
|
delete from room_nick_jid_map
|
|
where room_name = _room
|
|
and user_jid not in (
|
|
select key from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value in ('owner','admin','member','outcast','none')
|
|
);
|
|
|
|
-- mark recent_history rows for dropped members as deleted
|
|
update recent_history_table
|
|
set deleted = true
|
|
where target = _room
|
|
and username not in (
|
|
select key from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value in ('owner','admin','member','outcast','none')
|
|
);
|
|
|
|
-- upsert current members (nickname = room/bare-jid). room_nick_jid_map has
|
|
-- an INSERT INSTEAD rule (insert_room_nick_jid_map) so ON CONFLICT cannot
|
|
-- be used; insert rows that are missing, update the rest.
|
|
insert into room_nick_jid_map (room_name, user_jid, nickname, since)
|
|
select
|
|
_room,
|
|
key,
|
|
_room || '/' || key,
|
|
(extract(epoch from now())::integer - 60)
|
|
from prosody
|
|
where store = 'config'
|
|
and host = NEW.host
|
|
and "user" = NEW.user
|
|
and key like '%@%'
|
|
and value in ('owner','admin','member','outcast','none')
|
|
and not exists (
|
|
select 1 from room_nick_jid_map r
|
|
where r.room_name = _room
|
|
and r.user_jid = prosody.key
|
|
and r.nickname = _room || '/' || prosody.key
|
|
);
|
|
|
|
update room_nick_jid_map r
|
|
set since = (extract(epoch from now())::integer - 60)
|
|
from prosody
|
|
where prosody.store = 'config'
|
|
and prosody.host = NEW.host
|
|
and prosody."user" = NEW.user
|
|
and prosody.key like '%@%'
|
|
and prosody.value in ('owner','admin','member','outcast','none')
|
|
and r.room_name = _room
|
|
and r.user_jid = prosody.key
|
|
and r.nickname = _room || '/' || prosody.key;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$BODY$
|
|
LANGUAGE plpgsql VOLATILE;
|
|
|
|
-- (Re)create the trigger. Drop the old `when (NEW.key='_affiliations')`
|
|
-- trigger first; the duplicate function definitions from the 0.11.6 script
|
|
-- are consolidated by the `create or replace function` above.
|
|
drop trigger if exists update_room_nick_jid on prosody;
|
|
create trigger update_room_nick_jid after insert on prosody
|
|
for each row
|
|
when (NEW.store = 'config' and NEW.key = '_data' and NEW.host like 'conference.%')
|
|
execute procedure fupdate_room_nick_jid();
|
|
|
|
-- ===========================================================================
|
|
-- TRIGGER FUNCTION: fupdate_groupchat (UNCHANGED — kept here for completeness)
|
|
-- ===========================================================================
|
|
--
|
|
-- `fupdate_groupchat` reads `key='_data'`, which 13.0.6 still writes with a
|
|
-- JSON value containing `subject`. It therefore keeps working as-is. It is
|
|
-- already created by `prosody-trigger-noowner.sql`; no change is needed and
|
|
-- it is NOT recreated here to avoid drifting from the original definition.
|
|
|
|
-- ===========================================================================
|
|
-- VERIFICATION (optional, commented out)
|
|
-- ===========================================================================
|
|
-- Inspect the storage layout for a known room to confirm affiliations are
|
|
-- stored as one row per bare JID:
|
|
--
|
|
-- select key, type, value from prosody
|
|
-- where store='config' and host='conference.<domain>' and user='<room-node>';
|
|
--
|
|
-- Expected: rows with key = bare JID and value in
|
|
-- (owner, admin, member, outcast, none), plus _jid / _data /
|
|
-- _affiliation_data meta rows.
|