From e1c78e735833004413445603d74955d44644dbdf Mon Sep 17 00:00:00 2001 From: Stefan Saenger Date: Wed, 15 Jul 2026 13:46:50 +0200 Subject: [PATCH] feat: add pre-upgrade and post-upgrade DB migration hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: --- Dockerfile | 3 +- config/migrate.sh | 154 +- .../prosody-13-migration-once.sql | 194 ++ .../prosody-13-new-deployment.sql | 2075 +++++++++++++++++ .../prosody-13-rules-triggers.sql | 274 +++ db-customization/prosody-queries-noowner.sql | 1346 +++++++++++ db-customization/prosody-trigger-noowner.sql | 752 ++++++ .../templates/db-migration-post-upgrade.yaml | 41 + .../templates/db-migration-pre-upgrade.yaml | 41 + helm/prosody/values.yaml | 6 + 10 files changed, 4882 insertions(+), 4 deletions(-) create mode 100644 db-customization/prosody-13-migration-once.sql create mode 100644 db-customization/prosody-13-new-deployment.sql create mode 100644 db-customization/prosody-13-rules-triggers.sql create mode 100644 db-customization/prosody-queries-noowner.sql create mode 100644 db-customization/prosody-trigger-noowner.sql create mode 100644 helm/prosody/templates/db-migration-post-upgrade.yaml create mode 100644 helm/prosody/templates/db-migration-pre-upgrade.yaml diff --git a/Dockerfile b/Dockerfile index b267743..5e7e0f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ RUN apk update && apk upgrade && \ apk add gettext lua5.4 lua5.4-socket lua5.4-dbi-postgresql lua5.4-expat lua5.4-sql-postgres \ lua5.4-filesize lua5.4-lpeg lua5.4-hiredis lua5.4-filesystem lua5.4-ldap \ lua5.4-sec lua5.4-lzlib lua5.4-cjson \ - libidn-dev libidn icu libpq expat \ + libidn-dev libidn icu libpq postgresql-client expat \ openssl nodejs nagios-plugins-tcp && \ rm -rf /var/cache/apk/* @@ -56,6 +56,7 @@ RUN adduser -D -u 1001 -S -h /var/lib/prosody -H -G prosody prosody ADD vnctalk /vnc/vnctalk RUN cd /vnc/vnctalk && cp -Rp * /usr/local/lib/prosody/modules/ ADD config /vnc/config +ADD db-customization /vnc/db-customization USER prosody ENV __FLUSH_LOG=yes diff --git a/config/migrate.sh b/config/migrate.sh index 16d1a2a..5292be4 100755 --- a/config/migrate.sh +++ b/config/migrate.sh @@ -1,5 +1,153 @@ #!/bin/sh -# -# dummy migrate script - preparation for new chart +set -e -echo "dummy - all done" +MODE="${1:-pre-upgrade}" +SQL_DIR="/vnc/db-customization" + +log() { + echo "[migrate:$MODE] $*" +} + +# Map Prosody env vars to PostgreSQL env vars +export PGHOST="${prosodyDBhost}" +export PGPORT="${prosodyDBport:-5432}" +export PGUSER="${prosodyDBuser}" +export PGDATABASE="${prosodyDBname}" +export PGPASSWORD="${prosodyDBpass}" + +PSQL="psql -v ON_ERROR_STOP=1" + +# --------------------------------------------------------------------------- +# Detection helpers +# --------------------------------------------------------------------------- + +prosody_table_exists() { + $PSQL -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'prosody')" 2>/dev/null | tr -d '[:space:]' +} + +derived_tables_exist() { + $PSQL -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'processed_messages')" 2>/dev/null | tr -d '[:space:]' +} + +room_membership_view_exists() { + $PSQL -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.views WHERE table_name = 'room_membership')" 2>/dev/null | tr -d '[:space:]' +} + +room_membership_uses_old_format() { + $PSQL -tAc "SELECT pg_get_viewdef('room_membership'::regclass, true)" 2>/dev/null | grep -q "_affiliations" +} + +wait_for_prosody_table() { + local max="${1:-60}" + local i=0 + while [ "$i" -lt "$max" ]; do + if [ "$(prosody_table_exists)" = "t" ]; then + return 0 + fi + log "waiting for prosody table to exist ($i/$max)..." + sleep 2 + i=$((i + 2)) + done + return 1 +} + +# --------------------------------------------------------------------------- +# Migration runners +# --------------------------------------------------------------------------- + +run_new_deployment() { + log "running prosody-13-new-deployment.sql (idempotent)..." + $PSQL -f "$SQL_DIR/prosody-13-new-deployment.sql" + log "prosody-13-new-deployment.sql completed." +} + +run_rules_triggers() { + log "running prosody-13-rules-triggers.sql..." + $PSQL -f "$SQL_DIR/prosody-13-rules-triggers.sql" + log "prosody-13-rules-triggers.sql completed." +} + +run_migration_once() { + log "running prosody-13-migration-once.sql..." + $PSQL -f "$SQL_DIR/prosody-13-migration-once.sql" + log "prosody-13-migration-once.sql completed." +} + +# --------------------------------------------------------------------------- +# Pre-upgrade mode +# --------------------------------------------------------------------------- + +migrate_pre_upgrade() { + if [ "$(prosody_table_exists)" != "t" ]; then + log "prosody table does not exist. Prosody has not started yet. Skipping." + return 0 + fi + + if [ "$(derived_tables_exist)" != "t" ]; then + log "no derived tables found. Skipping (will be handled post-install)." + return 0 + fi + + if [ "$(room_membership_view_exists)" = "t" ]; then + if room_membership_uses_old_format; then + log "WARNING: 0.11.6 format detected (room_membership uses _affiliations)." + log "Running 13.0 rules/triggers against 0.11.6 data is unsafe pre-upgrade." + log "The migration will run in the post-upgrade hook after the new image starts." + return 0 + fi + fi + + log "13.0 format detected. Running idempotent schema check..." + run_new_deployment +} + +# --------------------------------------------------------------------------- +# Post-upgrade mode +# --------------------------------------------------------------------------- + +migrate_post_upgrade() { + if ! wait_for_prosody_table 120; then + log "prosody table still does not exist after 120s. Skipping." + return 0 + fi + + if [ "$(derived_tables_exist)" != "t" ]; then + log "new deployment detected (no derived tables)." + run_new_deployment + return 0 + fi + + if [ "$(room_membership_view_exists)" = "t" ] && room_membership_uses_old_format; then + log "0.11.6 format detected (room_membership uses _affiliations)." + log "Running full 0.11.6 -> 13.0.6 migration..." + run_rules_triggers + run_migration_once + run_new_deployment + log "0.11.6 -> 13.0.6 migration completed." + return 0 + fi + + log "13.0 format detected. Running idempotent schema check..." + run_new_deployment +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +log "starting (mode=$MODE)" + +case "$MODE" in + pre-upgrade) + migrate_pre_upgrade + ;; + post-upgrade) + migrate_post_upgrade + ;; + *) + log "ERROR: unknown mode '$MODE'. Use 'pre-upgrade' or 'post-upgrade'." + exit 1 + ;; +esac + +log "done." diff --git a/db-customization/prosody-13-migration-once.sql b/db-customization/prosody-13-migration-once.sql new file mode 100644 index 0000000..c013d8c --- /dev/null +++ b/db-customization/prosody-13-migration-once.sql @@ -0,0 +1,194 @@ +-- Migration script — Prosody 0.11.6 -> 13.0.6 +-- +-- Run EXACTLY ONCE, after the Prosody image has been upgraded to 13.0.6 and +-- AFTER the existing db-customization scripts +-- (prosody-queries-noowner.sql.notifyfix + prosody-trigger-noowner.sql) have +-- been applied to this database. +-- +-- This script: +-- 1. Reconciles derived tables with the new MUC storage layout (affiliations +-- stored as one row per bare JID in the `prosody` `config` store, no +-- `_affiliations` / `_occupants` keys). +-- 2. Removes the now-obsolete `room_nicknames` view and the +-- `update_room_nick_jid_map` rule that depended on it. +-- 3. Is a NO-OP if run a second time (all statements are idempotent), but it +-- is intended to be run only once. +-- +-- Prerequisite: the `prosody` and `prosodyarchive` tables must already exist +-- (created by the 13.0.6 storage backend / the regular db-customization +-- scripts). This script aborts if they do not. +-- +-- The `room_nick_jid_map` rebuild assumes all allowed clients use their bare +-- JID as their nickname in rooms, so the map can be created purely from +-- affiliations (nickname = "/"). + +\set ON_ERROR_STOP on + +-- Guard: bail out 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 $$; + +-- --------------------------------------------------------------------------- +-- 1. Drop obsolete objects that referenced the old `_affiliations` / `_occupants` rows +-- --------------------------------------------------------------------------- + +-- The `room_nicknames` view read `key='_occupants'`, which no longer exists in +-- 13.0.6 (occupant state moved to the `state` store and is only populated on +-- graceful shutdown, so it is not usable for live nickname resolution). +drop view if exists room_nicknames cascade; + +-- `update_room_nick_jid_map` refreshed `room_nick_jid_map` from `room_nicknames` +-- on every `muc_log` insert; with the view gone it is dead. +drop rule if exists update_room_nick_jid_map on prosodyarchive cascade; + +-- --------------------------------------------------------------------------- +-- 2. Backfill `room_nick_jid_map` from the new per-affiliation rows +-- --------------------------------------------------------------------------- + +-- Remove rows for affiliations that no longer exist (rooms whose members were +-- changed under 13.0.6 before this migration, or stale entries from the old +-- `_occupants`-based population). +delete from room_nick_jid_map +where room_name in ( + select p.user || '@' || p.host + from prosody p + where p.store = 'config' and p.host like 'conference.%' + group by p.user, p.host +) +and not exists ( + select 1 from prosody p2 + where p2.store = 'config' + and p2.host like 'conference.%' + and p2.key like '%@%' + and p2.value in ('owner','admin','member','outcast','none') + and p2.user || '@' || p2.host = room_nick_jid_map.room_name + and p2.key = room_nick_jid_map.user_jid +); + +-- Upsert current members. Nickname = "/" (bare JID is the +-- only nickname used by allowed clients). 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 + p.user || '@' || p.host as room_name, + p.key as user_jid, + (p.user || '@' || p.host) || '/' || p.key as nickname, + (extract(epoch from now())::integer - 60) as since +from prosody p +where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value in ('owner','admin','member','outcast','none') + and not exists ( + select 1 from room_nick_jid_map r + where r.room_name = p.user || '@' || p.host + and r.user_jid = p.key + and r.nickname = (p.user || '@' || p.host) || '/' || p.key + ); + +update room_nick_jid_map r +set since = (extract(epoch from now())::integer - 60) +from prosody p +where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value in ('owner','admin','member','outcast','none') + and r.room_name = p.user || '@' || p.host + and r.user_jid = p.key + and r.nickname = (p.user || '@' || p.host) || '/' || p.key; + +-- --------------------------------------------------------------------------- +-- 3. Reconcile `group_owners` with the new per-affiliation rows +-- --------------------------------------------------------------------------- + +-- Drop owners that are no longer present as `value='owner'` affiliation rows. +delete from group_owners go +where not exists ( + select 1 from prosody p + where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value = 'owner' + and p.user || '@' || p.host = go.room + and p.key = go.owner +); + +-- Upsert current owners (one row per room; pick the first owner by JID). +-- group_owners has an INSERT INSTEAD rule (upsert_group_owners) so ON CONFLICT +-- cannot be used; insert rooms that are missing, update the rest. +insert into group_owners (room, owner, created, updated) +select + sub.room, + sub.owner, + extract(epoch from now())::integer as created, + extract(epoch from now())::integer as updated +from ( + select + p.user || '@' || p.host as room, + min(p.key) as owner + from prosody p + where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value = 'owner' + group by p.user, p.host +) sub +where not exists ( + select 1 from group_owners go where go.room = sub.room +); + +update group_owners go +set owner = sub.owner, + updated = extract(epoch from now())::integer +from ( + select + p.user || '@' || p.host as room, + min(p.key) as owner + from prosody p + where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value = 'owner' + group by p.user, p.host +) sub +where go.room = sub.room + and go.owner <> sub.owner; + +-- --------------------------------------------------------------------------- +-- 4. Mark `recent_history_table` rows for users no longer affiliated +-- --------------------------------------------------------------------------- + +-- Members removed from a room before this migration may still have non-deleted +-- recent-history rows. Mark them deleted now (the rewritten trigger in the +-- idempotent script will keep this in sync going forward). +update recent_history_table rht +set deleted = true +where rht.type = 'groupchat' + and rht.target like '%@conference.%' + and not exists ( + select 1 from prosody p + where p.store = 'config' + and p.host like 'conference.%' + and p.key like '%@%' + and p.value in ('owner','admin','member','outcast','none') + and p.user || '@' || p.host = rht.target + and p.key = rht.username + ) + and exists ( + select 1 from prosody p3 + where p3.store = 'config' + and p3.host like 'conference.%' + and p3.user || '@' || p3.host = rht.target + ); + +-- Done. The derived tables now match the 13.0.6 storage layout. Going forward +-- the rewritten rules/triggers from `prosody-13-migration-rules-triggers.sql` +-- keep them consistent. diff --git a/db-customization/prosody-13-new-deployment.sql b/db-customization/prosody-13-new-deployment.sql new file mode 100644 index 0000000..068d5f7 --- /dev/null +++ b/db-customization/prosody-13-new-deployment.sql @@ -0,0 +1,2075 @@ +-- ============================================================================= +-- vnctalk-prosody 13.0.6 — full DB customization for NEW deployments +-- ============================================================================= +-- This is the single idempotent script for fresh Prosody 13.0.6 deployments. +-- It REPLACES the two legacy 0.11.6 scripts: +-- - prosody-queries-noowner.sql.notifyfix +-- - prosody-trigger-noowner.sql +-- +-- Differences from the legacy scripts: +-- * MUC rules/views/triggers adapted to the 13.0.6 storage layout +-- (affiliations stored one row per bare JID in the `prosody` `config` +-- store; `_affiliations` / `_occupants` keys no longer exist). +-- * `room_nicknames` view and `update_room_nick_jid_map` rule removed +-- (occupant data is no longer in the DB during normal operation; +-- `room_nick_jid_map` is derived purely from affiliations — all allowed +-- clients use their bare JID as their room nickname). +-- * `update_group_owners` rule replaced by a trigger (the legacy rule +-- conflicted with the existing `upsert_group_owners` INSTEAD rule). +-- * All `ALTER TABLE … ADD COLUMN` made idempotent (`IF NOT EXISTS`). +-- * One-time backfill INSERT/UPDATE statements removed (they matched +-- nothing on an empty DB and referenced the obsolete `_affiliations` key). +-- +-- Idempotent: safe to re-run. Every table uses `IF NOT EXISTS`, every rule / +-- view / function uses `CREATE OR REPLACE`, and triggers are dropped before +-- re-creation. `ALTER TABLE … ADD COLUMN IF NOT EXISTS` is a no-op if the +-- column already exists. +-- +-- Prerequisite: the Prosody storage tables `prosody` and `prosodyarchive` +-- must already exist (created by mod_storage_sql when Prosody first starts +-- against this DB). This script aborts if they are missing. +-- ============================================================================= + +\set ON_ERROR_STOP on + +do $$ +begin + if not exists (select 1 from information_schema.tables where table_name = 'prosody') then + raise exception 'Table "prosody" does not exist. Start Prosody 13.0.6 once so mod_storage_sql creates the storage tables, then re-run this script.'; + end if; + if not exists (select 1 from information_schema.tables where table_name = 'prosodyarchive') then + raise exception 'Table "prosodyarchive" does not exist. Start Prosody 13.0.6 once so mod_storage_sql creates the storage tables, then re-run this script.'; + end if; +end $$; + +create extension if not exists pgcrypto; + +-- ============================================================================= +-- TABLES +-- ============================================================================= + +create table if not exists processed_messages ( + sort_id bigint, + body text, + date integer, + "from" text, + "to" text, + owner text, + broadcast text, + "broadcast-sender" text, + room text, + type text, + id text, + receipts text, + x_attachment text, + x_location text, + x_replaceMsgId text, + x_origMessage text, + x_vncConference text, + x_forwardMessage text, + mention text, + PRIMARY KEY (sort_id) +); +create index if not exists si2 on processed_messages ("from"); +create index if not exists si3 on processed_messages ("to"); +create index if not exists si4 on processed_messages (owner); +create index if not exists si5 on processed_messages (broadcast); +create index if not exists si6 on processed_messages (room); +create index if not exists si7 on processed_messages (type); +create index if not exists si8 on processed_messages (id); +alter table processed_messages add column if not exists htmlbody text; +alter table processed_messages add column if not exists topicid text; +alter table processed_messages add column if not exists parent0 text; +alter table processed_messages add column if not exists parent text; +alter table processed_messages add column if not exists topic text; +alter table processed_messages add column if not exists updated integer; +alter table processed_messages add column if not exists broadcast_title text; +alter table processed_messages add column if not exists group_action text; +alter table processed_messages add column if not exists "encrypted" text; +alter table processed_messages add column if not exists encryption text; +alter table processed_messages add column if not exists reactions text; +alter table processed_messages add column if not exists expiry integer; +alter table processed_messages add column if not exists starredBy text[]; +create index if not exists si9 on processed_messages (expiry); +create index if not exists di1 on processed_messages (date); + +create table if not exists recent_history_table ( + username text, + target text, + type text, + timestamp integer, + message text, + original_message text, + x_attachment text, + x_conference text, + incoming boolean, + PRIMARY KEY (target, username) +); +alter table recent_history_table add column if not exists message_id text; +alter table recent_history_table add column if not exists received_receipt boolean; +alter table recent_history_table add column if not exists updated_at integer; +alter table recent_history_table add column if not exists mute_sound integer; +alter table recent_history_table add column if not exists mute_notification integer; +alter table recent_history_table add column if not exists has_data boolean; +alter table recent_history_table add column if not exists broadcast_title text; +alter table recent_history_table add column if not exists mentions text; +alter table recent_history_table add column if not exists sort_id bigint; +alter table recent_history_table add column if not exists deleted boolean; +alter table recent_history_table add column if not exists pad_read integer; +alter table recent_history_table add column if not exists has_pads boolean; +alter table recent_history_table add column if not exists last_mention_time integer; +alter table recent_history_table add column if not exists last_avatar_update integer; +alter table recent_history_table add column if not exists direct_e2e boolean; +alter table recent_history_table add column if not exists has_active_call boolean; +alter table recent_history_table add column if not exists last_callstate_update integer; +alter table recent_history_table add column if not exists x_conference_start integer; +alter table recent_history_table add column if not exists retention_time integer; +alter table recent_history_table add column if not exists x_conference_scheduler text; +alter table recent_history_table add column if not exists is_favourite boolean; +alter table recent_history_table add column if not exists is_pinned boolean; +alter table recent_history_table add column if not exists audience_only boolean; +alter table recent_history_table add column if not exists has_iom boolean; +alter table recent_history_table add column if not exists pin_order integer; +update recent_history_table set updated_at=0 where updated_at is NULL; +update recent_history_table set mute_notification=0 where mute_notification is NULL; +update recent_history_table set mute_sound=0 where mute_sound is NULL; +update recent_history_table set has_data=true where has_data is NULL; + +create table if not exists archive_inactive_table ( + username text, + target text, + timestamp integer, + content text, + PRIMARY KEY (target, username) +); + +create table if not exists broadcast_audience ( + broadcast_owner text, + broadcast_target text, + audience jsonb, + title text, + PRIMARY KEY (broadcast_owner, broadcast_target) +); +alter table broadcast_audience add column if not exists tags jsonb; +alter table broadcast_audience add column if not exists description text; + +create table if not exists group_owners ( + room text, + owner text, + primary key (room) +); +alter table group_owners add column if not exists created integer; +alter table group_owners add column if not exists updated integer; + +create table if not exists group_avatarids ( + room text, + avatarid text, + primary key (room) +); + +create table if not exists conferenceMapping ( + conferenceKey text, + value text, + primary key (conferenceKey) +); + +create table if not exists conferenceMap ( + conferenceKey text, + value text, + primary key (conferenceKey) +); + +create table if not exists exclude_from_history ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create table if not exists read_conversation ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create table if not exists read_pad ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create table if not exists external_email_invites ( + username text, + meeting text, + email text, + timestamp integer, + PRIMARY KEY (username, meeting, email) +); + +drop view if exists total_mentions cascade; +drop table if exists total_mentions cascade; +create table if not exists totalmentions ( + username text, + target text, + key text, + type text, + PRIMARY KEY (target, username, key) +); + +create table if not exists room_nick_jid_map ( + room_name text, + user_jid text, + nickname text, + primary key (room_name, user_jid, nickname) +); +alter table room_nick_jid_map add column if not exists since integer default 0; + +create table if not exists unread_message_ids ( + message_id text, + sender text, + receipient text, + timestamp integer, + primary key ( message_id, sender, receipient) +); + +create table if not exists mute_conversation ( + username text, + target text, + PRIMARY KEY (target, username) +); + +create table if not exists mute_notification ( + username text, + target text, + type integer, + PRIMARY KEY (target, username) +); + +create table if not exists profile_update_queue ( + username text, + timestamp integer, + PRIMARY KEY (username) +); + +create table if not exists unread_message_mention_ids ( + message_id text, + sender text, + receipient text, + timestamp integer, + primary key ( message_id, sender, receipient) +); + +create table if not exists call_tracking ( + callid bigint, + started_at integer, + updated_at integer, + caller text, + conferenceId text, + receipient text, + state text, + PRIMARY KEY (callid) +); +create index if not exists cri1 on call_tracking (receipient); +create index if not exists csi1 on call_tracking (started_at); + +create table if not exists no_notify_before ( + username text, + before integer, + PRIMARY KEY (username) +); + +create table if not exists ep_author_ids ( + username text, + authorid text, + primary key (username) +); + +create table if not exists ep_conv_group ( + convid text, + groupid text, + primary key (convid) +); + +create table if not exists ep_pad_names ( + padid text, + groupid text, + padname text, + primary key (padid, groupid) +); + +create table if not exists fcm_log ( + id SERIAL, + msgid text, + fromjid text, + tojid text, + tofcmtoken text, + todevicetype text, + timestamp integer, + opstatus text, + primary key(id) +); + +create table if not exists remote_muc_names ( + username text, + remotemuc text, + displayname text, + primary key (username, remotemuc) +); + +create table if not exists recordings ( + convkey text, + callid text, + fileid text, + timestamp integer, + primary key (convkey, fileid, timestamp) +); + +create table if not exists surveymap ( + id SERIAL, + groupchat text, + jid text, + survey text, + expiry integer, + PRIMARY KEY (id, groupchat, jid, survey) +); +create index if not exists ri3 on surveymap(expiry); +alter table surveymap add column if not exists name text; +alter table surveymap add column if not exists description text; +alter table surveymap add column if not exists status text; +alter table surveymap add column if not exists settings text; + +create table if not exists surveyresults ( + id SERIAL, + surveyid integer, + results text, + jid text, + PRIMARY KEY (id, jid) +); +create index if not exists ri2 on surveyresults(surveyid); +alter table surveyresults add column if not exists status text; + +create table if not exists emojireactions ( + msgid text, + reactor text, + reaction text, + PRIMARY KEY (msgid, reactor) +); + +create table if not exists room_join_requests ( + requester text, + room text, + date integer, + PRIMARY KEY (requester, room) +); + +create table if not exists user_custom_images ( + id SERIAL, + jid text, + updated integer, + type text, + filename text, + base64data text, + primary key(jid, filename) +); +create index if not exists uci0 on user_custom_images(id); +create index if not exists uci1 on user_custom_images(jid); +create index if not exists uci2 on user_custom_images(updated); + +create table if not exists groupchat ( + roomjid text NOT NULL, + roomtitle text, + PRIMARY KEY (roomjid) +); + +create sequence if not exists broadcast_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- ============================================================================= +-- RULES — upsert / dedup rules on the derived tables +-- ============================================================================= + +create or replace rule upsert_processed_messages as + on insert to processed_messages where (exists (select 1 from processed_messages where sort_id=NEW.sort_id)) + do instead + update processed_messages set + body = NEW.body, + htmlbody = NEW.htmlbody, + topicid = NEW.topicid, + parent0 = NEW.parent0, + parent = NEW.parent, + topic = NEW.topic, + date = NEW.date, + "from" = NEW.from, + "to" = NEW.to, + owner = NEW.owner, + broadcast = NEW.broadcast, + "broadcast-sender" = NEW."broadcast-sender", + room = NEW.room, + type = NEW.type, + id = NEW.id, + receipts = NEW.receipts, + x_attachment = NEW.x_attachment, + x_location = NEW.x_location, + x_replaceMsgId = NEW.x_replaceMsgId, + x_origMessage = NEW.x_origMessage, + x_vncConference = NEW.x_vncConference, + group_action = NEW.group_action, + mention = NEW.mention, + expiry = NEW.expiry + where sort_id=NEW.sort_id; + +create or replace rule insert_into_archive_inactive as + on insert to archive_inactive_table where (exists (select 1 from archive_inactive_table where username=NEW.username and target=NEW.target)) + do instead + update archive_inactive_table set timestamp=NEW.timestamp, + content=NEW.content + where target=NEW.target and username=NEW.username; + +create or replace rule update_recent_archive as + on update to archive_inactive_table + do update recent_history_table set updated_at=extract(epoch from now())::integer + where target=NEW.target and username=NEW.username; + +create or replace rule upsert_broadcast_audience as + on insert to broadcast_audience where (exists (select 1 from broadcast_audience where broadcast_target=NEW.broadcast_target)) + do instead + update broadcast_audience set + audience=NEW.audience, + title=NEW.title, + tags=NEW.tags, + description=NEW.description + where broadcast_target=NEW.broadcast_target; + +create or replace rule upsert_group_owners as + on insert to group_owners where (exists (select 1 from group_owners where room=NEW.room)) + do instead + update group_owners set + owner=NEW.owner + where room=NEW.room; + +create or replace rule upsert_group_avatarids as + on insert to group_avatarids where (exists (select 1 from group_avatarids where room=NEW.room)) + do instead + update group_avatarids set + avatarid=NEW.avatarid + where room=NEW.room; + +create or replace rule upsert_confmap as + on insert to conferenceMapping where (exists (select 1 from conferenceMapping where conferenceKey=NEW.conferenceKey)) + do instead update conferenceMapping + set value=NEW.value + where conferenceKey=NEW.conferenceKey; + +create or replace rule upsert_conferencemap as + on insert to conferenceMap where (exists (select 1 from conferenceMap where conferenceKey=NEW.conferenceKey)) + do instead update conferenceMap + set value=NEW.value + where conferenceKey=NEW.conferenceKey; + +create or replace rule insert_into_exclude_from_history as + on insert to exclude_from_history where (exists (select 1 from exclude_from_history where username=NEW.username and target=NEW.target)) + do instead + update exclude_from_history set timestamp=NEW.timestamp + where username=NEW.username and target=NEW.target; + +create or replace rule insert_into_read_conversation as + on insert to read_conversation where (exists (select 1 from read_conversation where username=NEW.username and target=NEW.target)) + do instead + update read_conversation set timestamp=NEW.timestamp + where target=NEW.target and username=NEW.username; + +create or replace rule insert_into_read_pad as + on insert to read_pad where (exists (select 1 from read_pad where username=NEW.username and target=NEW.target)) + do instead + update read_pad set timestamp=NEW.timestamp + where target=NEW.target and username=NEW.username; + +create or replace rule inser_external_email_invites as + on insert to external_email_invites where (exists (select 1 from external_email_invites where username=NEW.username and meeting=NEW.meeting and email=NEW.meeting)) + do instead nothing; + +create or replace rule update_read_conversation as + on update to read_conversation do + update recent_history_table set + updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule update_read_pad as + on update to read_pad do + update recent_history_table set + pad_read=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule upsert_totalmentions as + on insert to totalmentions where (exists (select 1 from totalmentions where username=NEW.username and target=NEW.target and key=NEW.key)) + do instead nothing; + +create or replace rule + insert_room_nick_jid_map as + on insert to room_nick_jid_map where (exists ( select 1 from room_nick_jid_map where room_name=NEW.room_name and user_jid=NEW.user_jid and nickname=NEW.nickname)) + do instead nothing; + +create or replace rule + insert_mute_conversation as + on insert to mute_conversation where (exists ( select 1 from mute_conversation where username=NEW.username and target=NEW.target)) + do instead nothing; + +create or replace rule + update_mute_conversation as + on update to mute_conversation + do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule upsert_mute_notification as + on insert to mute_notification where (exists ( select 1 from mute_notification where username=NEW.username and target=NEW.target)) + do instead update mute_notification + set type=NEW.type where username=NEW.username and target=NEW.target; + +create or replace rule update_mute_notification as + on update to mute_notification do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule insert_mute_notification as + on insert to mute_notification do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule upsert_profile_update_queue as + on insert to profile_update_queue where (exists (select 1 from profile_update_queue where username=NEW.username)) + do instead update profile_update_queue set + timestamp=NEW.timestamp + where username=NEW.username; + +create or replace rule + upsert_unread_message_ids as + on insert to unread_message_ids where (exists ( select 1 from unread_message_ids where message_id=NEW.message_id and sender=NEW.sender and receipient=NEW.receipient)) + do instead nothing; + +create or replace rule + upsert_unread_message_mention_ids as + on insert to unread_message_mention_ids where (exists ( select 1 from unread_message_mention_ids where message_id=NEW.message_id and sender=NEW.sender and receipient=NEW.receipient)) + do instead nothing; + +create or replace rule upsert_remote_muc_names as + on insert to remote_muc_names where (exists (select 1 from remote_muc_names where username=NEW.username and remotemuc=NEW.remotemuc)) + do instead update remote_muc_names set + displayname = NEW.displayname + where username=NEW.username and remotemuc=NEW.remotemuc; + +create or replace rule upsert_ep_author_ids as + on insert to ep_author_ids where (exists (select 1 from ep_author_ids where username=NEW.username)) + do instead update ep_author_ids set + authorid=NEW.authorid + where username=NEW.username; + +create or replace rule upsert_ep_conv_ids as + on insert to ep_conv_group where (exists (select 1 from ep_conv_group where convid=NEW.convid)) + do instead update ep_conv_group set + groupid=NEW.groupid + where convid=NEW.convid; + +create or replace rule upsert_ep_pad_names as + on insert to ep_pad_names where (exists (select 1 from ep_pad_names where padid=NEW.padid and groupid=NEW.groupid)) + do instead update ep_pad_names set + padname=NEW.padname + where padid=NEW.padid and groupid=NEW.groupid; + +create or replace rule upsert_no_notifiy as + on insert to no_notify_before where (exists (select 1 from no_notify_before where username=NEW.username)) + do instead + update no_notify_before set + before=NEW.before + where username=NEW.username; + +create or replace rule upsert_survey_results as + on insert to surveyresults where (exists (select 1 from surveyresults where surveyid=NEW.surveyid and jid=NEW.jid)) + do instead + update surveyresults set + results = NEW.results, + status = NEW.status + where surveyid = NEW.surveyid and jid=NEW.jid; + +create or replace rule upsert_emojireactions as + on insert to emojireactions where (exists (select 1 from emojireactions where msgid=NEW.msgid and reactor=NEW.reactor)) + do instead + update emojireactions set + reaction = NEW.reaction + where msgid=NEW.msgid and reactor=NEW.reactor; + +create or replace rule upsert_room_join_requests as + on insert to room_join_requests where (exists (select 1 from room_join_requests where requester=NEW.requester and room=NEW.room)) + do instead + update room_join_requests set + date = extract(epoch from now())::integer + where requester=NEW.requester and room=NEW.room; + +create or replace RULE upsert_groupchat AS + ON INSERT TO groupchat WHERE (EXISTS ( SELECT 1 FROM groupchat WHERE (roomjid = new.roomjid))) + DO INSTEAD + UPDATE groupchat SET roomtitle = new.roomtitle + WHERE (groupchat.roomjid = new.roomjid); + +-- ============================================================================= +-- RULES — on `prosody` (kv store) +-- ============================================================================= + +-- vcard_muc room avatar cache (unchanged — vcard_muc store still uses key='') +create or replace rule cache_group_avatarids as + on insert to prosody where NEW.store='vcard_muc' and NEW.key='' and NEW.type='json' and NEW.host like 'conference.%' + do insert into group_avatarids (room, avatarid) select + NEW.user||'@'||NEW.host as room, + encode(digest(decode(split_part(split_part(split_part(NEW.value, 'BINVAL', 2), '__array":["', 2),'"',1), 'base64'), 'sha1'), 'hex') as groupavarid; + +-- vcard profile-update queue (unchanged) +create or replace rule update_profile_queue_from_insert as + on insert to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') + do insert into profile_update_queue (username, timestamp) select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + +create or replace rule update_profile_queue_from_update as + on update to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') + do insert into profile_update_queue (username, timestamp) select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + +-- muc_remote nickname map (unchanged — VNCtalk store) +create or replace rule update_muc_remote_name as + on insert to prosody where (NEW.store='muc_remote' and not (NEW.value ilike '%/%')) + do insert into remote_muc_names (username, remotemuc, displayname) select NEW.user||'@'||NEW.host as username, NEW.key as remotemuc, NEW.value as displayname; + +create or replace rule update_room_nick_jid_map_remote as + on insert to prosody where NEW.store='muc_remote' and (NEW.key like '%/%') and (NEW.value like '%@%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part(NEW.key,'/',1)) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + extract(epoch from now())::integer; + +-- NOTE: the legacy `update_group_owners` rule (key='_affiliations') is GONE. +-- group_owners is now maintained by the `fupdate_group_owners` trigger fired +-- on the `_data` row (see trigger section below), because a rule conflicted +-- with the existing `upsert_group_owners` INSTEAD rule. + +-- ============================================================================= +-- VIEWS (created early — several prosodyarchive rules below depend on them) +-- ============================================================================= + +-- room_membership: 13.0.6 layout — affiliations are one row per bare JID +-- (key=, value=). Replaces the legacy +-- `where key='_affiliations'` + jsonb_object_keys form. +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 view if exists room_nicknames cascade; + +-- ============================================================================= +-- RULES — on `prosodyarchive` +-- ============================================================================= + +-- mentions +create or replace rule update_muc_mention as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + +create or replace rule update_muc_remote_mention as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + NEW.with as target, + NEW.key as key, + 'groupchat' as type; + +-- NOTE: the legacy `update_room_nick_jid_map` rule (which read the now-dropped +-- `room_nicknames` view) is GONE. room_nick_jid_map is maintained by the +-- `fupdate_room_nick_jid` trigger on the `config` store plus the invite rules +-- below. + +-- room_nick_jid_map from invite messages (unchanged — XML-parsed) +create or replace rule update_room_nick_jid_map_invitee as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||(split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))) as nickname, + (case + when ((xpath('/message/x:x/x:invite/x:reason/text() | /jc:message/x:x/x:invite/x:reason/text()', NEW.value::xml, ARRAY[ARRAY['x', 'http://jabber.org/protocol/muc#user'], ARRAY['jc', 'jabber:client']]))[1]::text = '1') then 0 + else extract(epoch from now())::integer + end) as since; + +create or replace rule update_room_nick_jid_map_inviter as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2)) as nickname, + extract(epoch from now())::integer; + +create or replace rule update_room_nick_jid_map_remote_inv as + on insert to prosodyarchive where NEW.store = 'muc_remote_inv' + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + NEW.user||'@'||NEW.host as room_name, + NEW.with as user_jid, + NEW.user||'@'||NEW.host||'/'||NEW.with as nickname, + extract(epoch from now())::integer; + +create or replace rule init_read_conversation_invitee as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into read_conversation (username, target, timestamp) + select + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as username, + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as target, + (extract(epoch from now())::integer - 60) as timestamp; + +-- single-chat recent history +create or replace rule update_single_chat_recent as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'chat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (case + when (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + NEW.when as updated_at, + true as has_data, + false as deleted + where + NEW.store='archive' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL + and ( ( + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] IS NOT NULL AND + (xpath('//vncTalkConference/conferenceId/text() | //xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text ilike '%#%' + ) or (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] IS NULL ) + AND NOT (NEW.with like '%@conference.%') + AND NOT (NEW.with = NEW.user || '@' || NEW.host) + order by target,timestamp desc; + +-- broadcast recent history +create or replace rule update_broadcast_recent as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, broadcast_title, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as target, + 'broadcast' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_title, + (case + when (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + NEW.when as updated_at, + true as has_data, + false as deleted + where + NEW.store='archive' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NOT NULL + AND NOT (NEW.with like '%@conference.%') + order by timestamp desc; + +-- broadcast audience +create or replace rule update_broadcast_audience as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title, tags, description) select + NEW.with as broadcast_owner, + (xpath('//message/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_target, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:to/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as audience, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as title, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:tag/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as tags, + (xpath('//message/xc:vncTalkBroadcast/@description', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as description + where + NEW.store='archive' + AND (xpath('//message/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + ; + +create or replace rule update_broadcast_audience_remote as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title) select + NEW.with as broadcast_owner, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_target, + '[]' as audience, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as title + WHERE + NEW.store='archive' + AND (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (split_part(NEW.with, '@', 2) <> NEW.host); + +-- groupchat recent history (uses room_membership view — see VIEWS section) +create or replace rule update_muc_recent as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text as mentions, + (case + when ((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + NEW.when as updated_at, + ((xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] is not null) as deleted, + true as has_data + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room and NEW.store='muc_log' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + order by username, room_membership.user_room_nickname desc; + +create or replace rule update_muc_remote_recent as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, has_data) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'groupchat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text as mentions, + (case + when ((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text = room_nick_jid_map.nickname) then false + else true + end) as incoming, + false as received_receipt, + NEW.when as updated_at, + true as has_data + from room_nick_jid_map where NEW.with=room_nick_jid_map.room_name and (room_nick_jid_map.user_jid = (NEW.user || '@' || NEW.host)) and NEW.store='muc_remote' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + order by username, room_nick_jid_map.nickname desc; + +drop rule if exists update_receipt_recent on prosodyarchive cascade; + +-- mark conversations read +create or replace rule mark_single_chat_read as + on insert to prosodyarchive where NEW.store = 'archive' + AND split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + do insert into read_conversation (username, target, "timestamp") + SELECT + (new."user" || '@'::text) || new.host AS username, + NEW.with as target, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.with; + +create or replace rule mark_muc_read as + on insert to prosodyarchive where NEW.store = 'muc_log' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text IS NULL + do insert into read_conversation (username, target, timestamp) select + room_nick_jid_map.user_jid AS username, + (new."user" || '@'::text) || new.host AS target, + new."when" AS "timestamp" + FROM room_nick_jid_map + WHERE room_nick_jid_map.room_name = ((new."user" || '@'::text) || new.host) AND room_nick_jid_map.nickname = (xpath('//message/@from | //jc:message/@from'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text + ; + +-- unread message ids +create or replace rule insert_unread_message_ids_chat as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into unread_message_ids (message_id, sender, receipient, timestamp) + select + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.with as sender, + split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) as receipient, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) <> NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL + AND NOT (NEW.with like '%@conference.%') + ; + +create or replace rule insert_unread_message_ids_broadcast as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into unread_message_ids (message_id, sender, receipient, timestamp) + select + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as receipient, + split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) as receipient, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) <> NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NOT NULL + AND NOT (NEW.with like '%@conference.%') + ; + +create or replace rule remove_unread_message_ids_deleted_chat as + on insert to prosodyarchive where NEW.store = 'archive' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + +create or replace rule insert_unread_message_ids_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into unread_message_ids (receipient, message_id, sender, timestamp) + select + DISTINCT room_membership.username AS receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.user || '@' || NEW.host as sender, + NEW.when::integer as timestamp + from room_membership + where room_membership.host=NEW.host and room_membership.room=NEW.user and room_membership.username not + in (select username from room_membership where user_room_nickname=((xpath('//message/@from | //jc:message/@from'::text, NEW.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text)) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +; + +create or replace rule remove_unread_message_ids_deleted_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + +create or replace rule insert_unread_message_ids_remotemuc as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into unread_message_ids (receipient, message_id, sender, timestamp) + select + NEW.user || '@' || NEW.host as receipient, + NEW.key as message_id, + NEW.with as sender, + NEW.when as timestamp +where (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',2) <> NEW.user || '@' || NEW.host) +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL +AND (NEW.with like '%@conference.%') +; + +create or replace rule remove_unread_message_ids_deleted_remotemuc as + on insert to prosodyarchive where NEW.store = 'muc_remote' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + +-- unread mention ids +create or replace rule insert_unread_mention_ids_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into unread_message_mention_ids (receipient, message_id, sender, timestamp) + select + DISTINCT room_membership.username AS receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.user || '@' || NEW.host as sender, + NEW.when::integer as timestamp + from room_membership + where room_membership.host=NEW.host and room_membership.room=NEW.user and room_membership.username not + in (select username from room_membership where user_room_nickname=((xpath('//message/@from | //jc:message/@from'::text, NEW.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text)) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', new.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text like '%xmpp:'||room_membership.username||'%' +; + +create or replace rule insert_unread_message_ids_remotemuc_mention as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into unread_message_mention_ids (receipient, message_id, sender, timestamp) + select + NEW.user || '@' || NEW.host as receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.with as sender, + NEW.when as timestamp +where (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',2) <> NEW.user || '@' || NEW.host) +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL +AND (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', new.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text like '%xmpp:'||NEW.user||'@'||NEW.host||'%' +AND (NEW.with like '%@conference.%') +; + +-- recent_history upsert rules +create or replace rule instert_into_chat_recent as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and NEW.timestamp is not null) + do instead + update recent_history_table set + type=NEW.type, + message=NEW.message, + original_message=NEW.original_message, + x_attachment=NEW.x_attachment, + x_conference=NEW.x_conference, + timestamp=NEW.timestamp, + incoming=NEW.incoming, + message_id=NEW.message_id, + received_receipt=null, + updated_at=NEW.timestamp, + has_data=true, + broadcast_title=NEW.broadcast_title, + sort_id=NEW.sort_id, + deleted=false, + mentions=NEW.mentions + where target=NEW.target and username=NEW.username and ((recent_history_table.deleted = false) or (NEW.deleted IS NOT NULL)); + +create or replace rule instert_into_chat_recent_mute as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and (NEW.timestamp is null) and (NEW.mute_notification is null)) + do instead + update recent_history_table set + mute_sound = NEW.mute_sound, + updated_at=EXTRACT(EPOCH FROM now())::integer + where target=NEW.target and username=NEW.username; + +create or replace rule instert_into_chat_recent_notify as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and (NEW.timestamp is null) and (NEW.mute_sound is null)) + do instead + update recent_history_table set + mute_notification = NEW.mute_notification, + updated_at=EXTRACT(EPOCH FROM now())::integer + where target=NEW.target and username=NEW.username; + +-- ============================================================================= +-- VIEWS +-- ============================================================================= + +create or replace view total_mentions as +select + username, + target, + 'groupchat' as type, + count(*) as total +from totalmentions group by username, target; + +create or replace view unread_conversation_ids as + select + read_conversation.username as username, + read_conversation.target as target, + jsonb_agg(unread_message_idjoin.message_id) as unreadids + from read_conversation + join unread_message_ids as unread_message_idjoin on + ( unread_message_idjoin.receipient=read_conversation.username and unread_message_idjoin.sender=read_conversation.target and unread_message_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_ids.receipient as username, + unread_message_ids.sender as target, + jsonb_agg(unread_message_ids.message_id) as unreadids +from unread_message_ids +where unread_message_ids.receipient||'#'||unread_message_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_ids.receipient, unread_message_ids.sender +; + +create or replace view unread_conversation_counts as + select + read_conversation.username as username, + read_conversation.target as target, + count(unread_message_idjoin.message_id) as unreadidcount + from read_conversation + join unread_message_ids as unread_message_idjoin on + ( unread_message_idjoin.receipient=read_conversation.username and unread_message_idjoin.sender=read_conversation.target and unread_message_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_ids.receipient as username, + unread_message_ids.sender as target, + count(unread_message_ids.message_id) as unreadidcount +from unread_message_ids +where unread_message_ids.receipient||'#'||unread_message_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_ids.receipient, unread_message_ids.sender +; + +create or replace view unread_mention_ids as + select + read_conversation.username as username, + read_conversation.target as target, + jsonb_agg(unread_message_mention_idjoin.message_id) as unreadids + from read_conversation + join unread_message_mention_ids as unread_message_mention_idjoin on + ( unread_message_mention_idjoin.receipient=read_conversation.username and unread_message_mention_idjoin.sender=read_conversation.target and unread_message_mention_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_mention_ids.receipient as username, + unread_message_mention_ids.sender as target, + jsonb_agg(unread_message_mention_ids.message_id) as unreadids +from unread_message_mention_ids +where unread_message_mention_ids.receipient||'#'||unread_message_mention_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_mention_ids.receipient, unread_message_mention_ids.sender +; + +create or replace view user_notify_opt0 as + select prosody.user||'@'||prosody.host as email from prosody where + ( prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"0%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\"0%' ); + +create or replace view user_notify_opt1 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"1%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"1%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"1%' + or prosody.value like '%emailNotification"},"__array":["\\"1%' + ); + +create or replace view user_notify_opt2 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"2%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"2%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"2%' + or prosody.value like '%emailNotification"},"__array":["\\"2%'); + +create or replace view user_notify_opt3 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"3%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"3%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"3%' + or prosody.value like '%emailNotification"},"__array":["\\"3%'); + +create or replace view user_notify_opt4 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"4%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"4%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"4%' + or prosody.value like '%emailNotification"},"__array":["\\"4%'); + +create or replace view digest_messages as +select + prosodyarchive.user||'@'||prosodyarchive.host as email, + prosodyarchive.when as when, + (xpath('/message/@from | /jc:message/@from', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS from, + (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS body, + (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as broadcast, + (xpath('/message/xc:attachment | /jc:message/xc:attachment', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text AS x_attachment, + (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text as x_vncConference, + (xpath('/message/@from | /jc:message/@from', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS target, + NULL as enddtime, + NULL as starttime, + 'chat' as type +from prosodyarchive +where prosodyarchive.store='offline' and (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, prosodyarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, prosodyarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +union +select + recent_history_table.username as email, + mucarchive.when as when, + (xpath('/message/@from | /jc:message/@from', mucarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS from, + (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', mucarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS body, + (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as broadcast, + (xpath('/message/xc:attachment | /jc:message/xc:attachment', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text AS x_attachment, + (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text as x_vncConference, + recent_history_table.target as target, + recent_history_table.timestamp as endtime, + read_conversation.timestamp as starttime, + 'groupchat' as type +from + recent_history_table, read_conversation +join prosodyarchive as mucarchive on ( + mucarchive.user = split_part(read_conversation.target, '@', 1) and + mucarchive.host = split_part(read_conversation.target, '@', 2) and + mucarchive.store='muc_log' and + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text and + mucarchive.when > read_conversation.timestamp and + ((xpath('//message/@from | //jc:message/@from'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text + not in (select nickname from room_nick_jid_map where room_name=read_conversation.target and user_jid=read_conversation.username)) + ) +where + recent_history_table.type='groupchat' + AND recent_history_table.username=read_conversation.username + AND recent_history_table.target=read_conversation.target + AND (recent_history_table.timestamp > read_conversation.timestamp) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +; + +create or replace view room_membernames as SELECT room_nick_jid_map.room_name, + jsonb_agg(room_nick_jid_map.user_jid) AS members, + split_part(split_part(mucjoin.value, '"subject":'::text, 2), '"'::text, 2) AS title, + jsonb_agg( + CASE + WHEN ((((((split_part(split_part(split_part(namejoin.value, 'NICKNAME","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'GIVEN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FAMILY","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) = ' '::text THEN namejoin."user" + ELSE (((((split_part(split_part(split_part(namejoin.value, 'NICKNAME","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'GIVEN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FAMILY","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) + END) AS membernames + FROM room_nick_jid_map + LEFT JOIN prosody mucjoin ON room_nick_jid_map.room_name = ((mucjoin."user" || '@'::text) || mucjoin.host) AND mucjoin.store = 'config'::text AND mucjoin.key = '_data'::text AND mucjoin.value ~~ '%"subject":%'::text + LEFT JOIN prosody namejoin ON room_nick_jid_map.user_jid = ((namejoin."user" || '@'::text) || namejoin.host) AND namejoin.store = 'vcard'::text AND namejoin.key = ''::text AND namejoin.type = 'json'::text + GROUP BY room_nick_jid_map.room_name, mucjoin.value; + +create or replace view inverseconfmap as + select + split_part(split_part(value, 'value":"',2), '","', 1) as meeting, + conferencekey as key + from conferencemap; + +-- ============================================================================= +-- TEXT SEARCH CONFIGURATION +-- (guarded so the script is idempotent even if the dictionary/template is missing) +-- ============================================================================= + +do $$ +begin + if not exists (select 1 from pg_ts_config where cfgname = 'english_nostop') then + create text search configuration public.english_nostop ( parser = pg_catalog."default" ); + end if; +end $$; + +-- Reconcile mappings idempotently: remove all existing mappings for this +-- configuration, then re-add the desired set (only if the dictionary exists). +do $$ +begin + delete from pg_ts_config_map where mapcfg = (select oid from pg_ts_config where cfgname = 'english_nostop'); + if exists (select 1 from pg_ts_dict where dictname = 'english_stem_nostop') then + alter text search configuration public.english_nostop add mapping for asciiword with public.english_stem_nostop; + alter text search configuration public.english_nostop add mapping for word with public.english_stem_nostop; + alter text search configuration public.english_nostop add mapping for hword_part with public.english_stem_nostop; + alter text search configuration public.english_nostop add mapping for hword_asciipart with public.english_stem_nostop; + alter text search configuration public.english_nostop add mapping for asciihword with public.english_stem_nostop; + alter text search configuration public.english_nostop add mapping for hword with public.english_stem_nostop; + end if; + alter text search configuration public.english_nostop add mapping for numword with simple; + alter text search configuration public.english_nostop add mapping for email with simple; + alter text search configuration public.english_nostop add mapping for url with simple; + alter text search configuration public.english_nostop add mapping for host with simple; + alter text search configuration public.english_nostop add mapping for sfloat with simple; + alter text search configuration public.english_nostop add mapping for version with simple; + alter text search configuration public.english_nostop add mapping for hword_numpart with simple; + alter text search configuration public.english_nostop add mapping for numhword with simple; + alter text search configuration public.english_nostop add mapping for url_path with simple; + alter text search configuration public.english_nostop add mapping for file with simple; + alter text search configuration public.english_nostop add mapping for "float" with simple; + alter text search configuration public.english_nostop add mapping for "int" with simple; + alter text search configuration public.english_nostop add mapping for uint with simple; +end $$; + +-- ============================================================================= +-- TRIGGER FUNCTIONS +-- ============================================================================= + +-- fprocess_messages — main message-processing trigger on prosodyarchive. +-- Body unchanged from 0.11.6; it relies on room_nick_jid_map (table) and +-- room_membership (view), both of which are kept correct by the 13.0.6 +-- triggers/views above. +create or replace function fprocess_messages() + returns trigger AS + $BODY$ + DECLARE + _body text; + _htmlbody text; + _topic text; + _parent text; + _parent0 text; + _x_attachment text; + _x_location text; + _x_replaceMsgId text; + _original_message text; + _x_conference text; + _x_forwardMessage text; + _group_action text; + _mention text; + _x_encrypted text; + _encryption text; + _message_id text; + _origtarget text; + _from_bare text; + _x_invite text; + _x_broadcast text; + _x_when integer; + _from text; + _to text; + _received_id text; + _broadcast_title text; + _x_conference_scheduler text; + _x_conferencekey text; + _x_conference_from text; + _x_role text; + _x_affiliation text; + + BEGIN + _body := (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text; + _htmlbody := (xpath('//message/html/* | //message/xim:html/* | //jc:message/xim:html/*', NEW.value::xml, ARRAY[ARRAY['xim', 'http://jabber.org/protocol/xhtml-im'], ARRAY['jc', 'jabber:client']]))[1]::text; + _topic := (xpath('/message/xc:topic/@name | /jc:message/xc:topic/@name', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _parent0 := (xpath('/message/xc:topic/@parent0 | /jc:message/xc:topic/@parent0', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _parent := (xpath('/message/xc:topic/@parent | /jc:message/xc:topic/@parent', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_attachment := (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_location := (xpath('/message/xc:location | /jc:message/xc:location', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_replaceMsgId := (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', NEW.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + _original_message := (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference := (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_forwardMessage := (xpath('/message/xc:forwardMessage | /jc:message/xc:forwardMessage', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _group_action := (xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _mention := (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::jsonb; + _x_encrypted := (xpath('//jc:message/xo:encrypted |//message/xo:encrypted', NEW.value::xml , ARRAY[ARRAY['jc','jabber:client'],ARRAY['xo','urn:xmpp:omemo:1']]))[1]::text; + _encryption := (xpath('//jc:message/xo:encryption |//message/xo:encryption', NEW.value::xml , ARRAY[ARRAY['jc','jabber:client'],ARRAY['xo','urn:xmpp:eme:0']]))[1]::text; + _message_id := (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text; + _broadcast_title := (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]; + _origtarget := (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text; + _x_invite := (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text; + _x_broadcast := xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]); + _x_when := extract(epoch from now())::integer; + _from := (xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text; + _from_bare := split_part(_from, '/', 1); + _to := (xpath('/message/@to | /jc:message/@to', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text; + _x_conference_scheduler := (xpath('/message/xc:vncTalkConferenceScheduler | /jc:message/xc:vncTalkConferenceScheduler', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_role := (xpath('//message/mu:x/mu:item/@role', NEW.value::xml, ARRAY[ARRAY['mu','http://jabber.org/protocol/muc#user']]))[1]::text; + _x_affiliation := (xpath('//message/mu:x/mu:item/@affiliation', NEW.value::xml, ARRAY[ARRAY['mu','http://jabber.org/protocol/muc#user']]))[1]::text; + + if NEW.store = 'muc_log' then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + room_nick_jid_map.user_jid as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.user||'@'||NEW.host as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + + if (_x_role = 'participant' and _x_affiliation = 'none') then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + split_part(_from, '/', 2) as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.user||'@'||NEW.host as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption; + + end if; + + if (jsonb_array_length(_mention::jsonb) > 0) then + insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(_mention::jsonb)::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + end if; + + + if NEW.value like '%ENDED_CALLleave%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference_from := (xpath('//xc:vncTalkConference/xc:from/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'leave' + where callid in (select callid from call_tracking where receipient=_x_conference_from and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + if NEW.value like '%join%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference_from := (xpath('//xc:vncTalkConference/xc:from/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update recent_history_table + set has_active_call = true, + updated_at=_x_when, + last_callstate_update=_x_when + where target=NEW.user||'@'||NEW.host; + update call_tracking set + updated_at = _x_when, + state = 'join' + where callid in (select callid from call_tracking where receipient=_x_conference_from and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + if _x_conference_scheduler is not null then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data, x_conference_scheduler, x_conference_start) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, + _message_id AS message_id, + _original_message as original_message, + _x_attachment as x_attachment, _x_conference as x_conference, _mention as mentions, + (case + when (_from = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + extract(epoch from now()) as updated_at, + ((xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] is not null) as deleted, + true as has_data, + _x_conference_scheduler as x_conference_scheduler, + extract(epoch from to_timestamp(((xpath('/message/xc:vncTalkConferenceScheduler/xc:startTime/text() | /jc:message/xc:vncTalkConferenceScheduler/xc:startTime/text()', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text), 'YYYY-MM-DD HH24:MI:SS')) as x_conference_start + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room + order by username, room_membership.user_room_nickname desc; + end if; + + if (_body is not null) and (_body <> '') then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _mention as mentions, + (case + when (_from = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + _x_when as updated_at, + (_group_action is not null) as deleted, + true as has_data + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room + order by username, room_membership.user_room_nickname desc; + + + if (_x_conference is null) then + insert into read_conversation (username, target, timestamp) select + room_nick_jid_map.user_jid AS username, + (new."user" || '@'::text) || new.host AS target, + _x_when AS "timestamp" + FROM room_nick_jid_map + WHERE room_nick_jid_map.room_name = ((new."user" || '@'::text) || new.host) AND room_nick_jid_map.nickname = _from; + end if; + end if; + end if; + + if NEW.store = 'muc_remote' then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + room_nick_jid_map.user_jid as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.with as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as _x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + + -- mentions + if (jsonb_array_length(_mention::jsonb) > 0) then + insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(_mention::jsonb)::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + end if; + if (_group_action = 'UPDATE_ENCRYPTION') then + if ((xpath('//group_action/data/text() | //xc:group_action/xc:data/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text = 1) then + update recent_history_table set direct_e2e=true where target=NEW.with; + else + update recent_history_table set direct_e2e=false where target=NEW.with; + end if; + end if; + + if NEW.value like '%ENDED_CALLjoin%' then + update recent_history_table + set has_active_call = true, + updated_at=extract(epoch from now())::integer, + last_callstate_update=extract(epoch from now())::integer + where target=NEW.with; + end if; + + if _x_conference_scheduler is not null then + update recent_history_table + set x_conference_scheduler=_x_conference_scheduler, + x_conference_start=extract(epoch from to_timestamp(((xpath('/message/xc:vncTalkConferenceScheduler/xc:startTime/text() | /jc:message/xc:vncTalkConferenceScheduler/xc:startTime/text()', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text), 'YYYY-MM-DD HH:MI:SS')) + where recent_history_table.target=NEW.with; + end if; + + if (_body is not null) and (_body <> '') then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + NEW.user||'@'||NEW.host as username, + NEW.with as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _mention as mentions, + (case + when (NEW.user||'@'||NEW.host = room_nick_jid_map.user_jid) then false + else true + end) as incoming, + false as received_receipt, + _x_when as updated_at, + (_group_action is not null) as deleted, + true as has_data + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + end if; + + end if; + + if NEW.store = 'archive' then + -- processed_messages + if (NEW.with != NEW.user||'@'||NEW.host) then + insert into processed_messages (sort_id, body, htmlbody, date, "from", "to", "broadcast-sender", owner, type, id, broadcast, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, broadcast_title, group_action, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, + extract(epoch from now())::integer as date, + (case + when (_origtarget IS NULL) then _from_bare + else _origtarget + end) as from, + _to AS "to", + (case + when _origtarget IS NULL then + NULL + else _from_bare + end) as "broadcast-sender", + NEW.user||'@'||NEW.host as owner, + 'chat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _origtarget as broadcast, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, + _broadcast_title as broadcast_title, + _group_action as group_action, _x_encrypted as "encrypted", _encryption as encryption + where + _x_invite IS NULL + ; + + + if NEW.value like '%invite%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + insert into conferencemap (conferencekey, value) + select + _x_conferencekey as conferencekey, + '{"value":"'||(xpath('//xc:vncTalkConference/xc:jitsiRoom/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]||'","jitsiurl":"'||(xpath('//xc:vncTalkConference/xc:jitsiURL/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]||'"}' as value + ; + if NEW.with = _to then + insert into call_tracking (callid, started_at, caller, conferenceId, receipient, state) + select + NEW.sort_id as callid, + _x_when as started_at, + _from_bare as caller, + _x_conferencekey as conferenceId, + _to as receipient, + 'invite' as state + ; + end if; + end if; + + if NEW.value like '%leave%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'leave' + where callid in (select callid from call_tracking where (receipient=_from_bare or caller=_from_bare) and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + if NEW.value like '%join%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'join' + where callid in (select callid from call_tracking where receipient=_from_bare and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + + if (NEW.with like '%@conference.%') then + -- init read conversation + insert into read_conversation (username, target, timestamp) + select + (split_part(_to, '/',1)) as username, + (split_part(_from, '/',1)) as target, + (extract(epoch from now())::integer - 60) as timestamp; + -- update room_nick_jid_map_inviter + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part(_from, '/',1)) as room_name, + split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2) as user_jid, + ((split_part(_from, '/',1))||'/'||split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2)) as nickname, + extract(epoch from now())::integer; + -- update room_nick_jid_map_invitee + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||(split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))) as nickname, + extract(epoch from now())::integer as since; + + else + + if (_body is not null) and ((_x_conference is null) or (_x_conference like '%#%')) and (_x_invite is null) then + if (_origtarget is null) then + -- update chat recent + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'chat' as type, + _x_when as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + (case + when (split_part(_to, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + _x_when as updated_at, + true as has_data, + false as deleted + order by target,timestamp desc; + end if; + if (_origtarget is not null) and (_x_broadcast is not null) then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, broadcast_title, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + _origtarget as target, 'broadcast' as type, _x_when as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _broadcast_title as broadcast_title, + (case + when (split_part(_to, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + _x_when as updated_at, + true as has_data, + false as deleted + order by timestamp desc; + end if; + + if (_origtarget is not null) and (_x_broadcast is not null) then + insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title) select + NEW.with as broadcast_owner, + _origtarget as broadcast_target, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:*/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as audience, + _broadcast_title as title; + end if; + + end if; + end if; + end if; + + if (_to = NEW.with) and (_body is not null) and (_body <> '') then + insert into read_conversation (username, target, "timestamp") select + (new."user" || '@'::text) || new.host AS username, + NEW.with as target, + _x_when as timestamp; + end if; + + + end if; + + if NEW.store = 'kick' then + delete from room_nick_jid_map where (room_name=NEW.user||'@'||NEW.host) and (user_jid=NEW.with); + update read_conversation set timestamp=_x_when + 30 where username=NEW.with and (target=NEW.user||'@'||NEW.host); + update recent_history_table set deleted = true where ((target=NEW.user||'@'||NEW.host) and (username = NEW.with)); + end if; + + if NEW.store = 'receipts' then + _received_id := (xpath('//received/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text; + + update processed_messages set receipts = _x_when::text, updated = _x_when + where (processed_messages.id = _received_id) + and (processed_messages.owner = NEW.user||'@'||NEW.host) ; + + update recent_history_table set received_receipt=true, updated_at = _x_when + where (recent_history_table.message_id = _received_id) + and (recent_history_table.username = NEW.user||'@'||NEW.host); + + end if; + + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists process_messages on prosodyarchive; +create trigger process_messages after insert on prosodyarchive + for each row execute procedure fprocess_messages(); + +-- fupdate_room_nick_jid — 13.0.6: fires on the `_data` row (always written on +-- every room save) and rebuilds room_nick_jid_map from the per-affiliation +-- rows. Nickname = "/" (bare JID is the only nickname +-- used by allowed clients). +create or replace function fupdate_room_nick_jid() + returns trigger AS + $BODY$ + DECLARE + _room text; + BEGIN + _room := NEW.user || '@' || NEW.host; + + 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') + ); + + 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') + ); + + 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; + +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(); + +-- fupdate_group_owners — 13.0.6: replaces the legacy `update_group_owners` +-- rule (which keyed off `_affiliations` and conflicted with the +-- `upsert_group_owners` INSTEAD rule). Fires on the `_data` row and +-- reconciles group_owners from the per-affiliation owner rows. +create or replace function fupdate_group_owners() + returns trigger AS + $BODY$ + DECLARE + _room text; + BEGIN + _room := NEW.user || '@' || NEW.host; + + 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' + ); + + 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(); + +-- fupdate_room_nick_jid_remote (unchanged — VNCtalk muc_remote store) +create or replace function fupdate_room_nick_jid_remote() + returns trigger AS + $BODY$ + BEGIN + insert into room_nick_jid_map (room_name, user_jid, nickname, since) select + split_part(NEW.key, '/',1) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + (extract(epoch from now())::integer - 60) as since + where (NEW.key ilike '%/%'); + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_room_nick_jid_remote on prosody; +create trigger update_room_nick_jid_remote after insert on prosody + for each row + when (NEW.store='muc_remote' and (NEW.key ilike '%/%')) + execute procedure fupdate_room_nick_jid_remote(); + +-- fupdate_mention_stamp (unchanged) +create or replace function fupdate_mention_stamp() + returns trigger AS + $BODY$ + BEGIN + update recent_history_table set last_mention_time=extract(epoch from now())::integer + where username=NEW.username and target=NEW.target; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_mention_stamp on totalmentions; +create trigger update_mention_stamp after insert on totalmentions + for each row execute procedure fupdate_mention_stamp(); + +-- fupdate_recent_avatar (unchanged — avatarupdate store) +create or replace function fupdate_recent_avatar() + returns trigger AS + $BODY$ + BEGIN + update recent_history_table + set + last_avatar_update = NEW.value::integer, + updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target=NEW.user||'@'||new.host and NEW.store='avatarupdate'; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_recent_avatar on prosody; +create trigger update_recent_avatar after insert on prosody + for each row + when (NEW.store='avatarupdate') + execute procedure fupdate_recent_avatar(); + +-- fupdate_iom_callstates (unchanged — callactive store) +create or replace function fupdate_iom_callstates() + returns trigger as + $BODY$ + BEGIN + update recent_history_table + set has_active_call=true, + last_callstate_update = EXTRACT(EPOCH FROM now())::integer, + updated_at = EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target = NEW.user and NEW.value='callactive'; + update recent_history_table + set has_active_call=false, + last_callstate_update = EXTRACT(EPOCH FROM now())::integer, + updated_at = EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target = NEW.user and NEW.value='callinactive'; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_iom_callstates on prosody; +create trigger update_iom_callstates after insert on prosody + for each row + when (NEW.store='callactive') + execute procedure fupdate_iom_callstates(); + +-- fupdate_msg_reactions / fupdate_msg_delreactions (unchanged — emojireactions) +create or replace function fupdate_msg_reactions() + returns trigger AS + $BODY$ + BEGIN + update processed_messages + set updated = EXTRACT(EPOCH FROM now())::integer, + reactions = r.v from (select jsonb_agg(jsonb_build_object(reactor, reaction)) as v from emojireactions where emojireactions.msgid=NEW.msgid) as r + where processed_messages.id=NEW.msgid; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_msg_reactions on emojireactions; +create trigger update_msg_reactions after insert or update on emojireactions +for each row execute procedure fupdate_msg_reactions(); + +create or replace function fupdate_msg_delreactions() + returns trigger AS + $BODY$ + BEGIN + update processed_messages + set updated = EXTRACT(EPOCH FROM now())::integer, + reactions = r.v from (select jsonb_agg(jsonb_build_object(reactor, reaction)) as v from emojireactions where emojireactions.msgid=OLD.msgid) as r + where processed_messages.id=OLD.msgid; + RETURN OLD; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_msg_delreactions on emojireactions; +create trigger update_msg_delreactions after delete on emojireactions +for each row execute procedure fupdate_msg_delreactions(); + +-- fupdate_groupchat (unchanged — _data row still written by 13.0.6) +create or replace FUNCTION fupdate_groupchat() + RETURNS trigger AS + $BODY$ + BEGIN + if NEW.store = 'config' AND NEW.key='_data' then + insert into groupchat (roomjid, roomtitle) select + NEW.user||'@'||NEW.host as roomjid, + NEW.value::jsonb->>'subject' as roomtitle; + end if; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +drop trigger if exists update_groupchat on prosody; +create trigger update_groupchat after insert on prosody +for each row when (new.key = '_data'::text and new.store = 'config'::text) +EXECUTE PROCEDURE fupdate_groupchat(); diff --git a/db-customization/prosody-13-rules-triggers.sql b/db-customization/prosody-13-rules-triggers.sql new file mode 100644 index 0000000..8fe0dab --- /dev/null +++ b/db-customization/prosody-13-rules-triggers.sql @@ -0,0 +1,274 @@ +-- 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 = "/"). +-- +-- 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=`, `value=`). 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=`, `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 = "/" — 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.' and user=''; +-- +-- Expected: rows with key = bare JID and value in +-- (owner, admin, member, outcast, none), plus _jid / _data / +-- _affiliation_data meta rows. diff --git a/db-customization/prosody-queries-noowner.sql b/db-customization/prosody-queries-noowner.sql new file mode 100644 index 0000000..1d100f8 --- /dev/null +++ b/db-customization/prosody-queries-noowner.sql @@ -0,0 +1,1346 @@ +-- database schema +create extension if not exists pgcrypto; + +-- comment to test upgrade + +create table if not exists processed_messages ( + sort_id bigint, + body text, + date integer, + "from" text, + "to" text, + owner text, + broadcast text, + "broadcast-sender" text, + room text, + type text, + id text, + receipts text, + x_attachment text, + x_location text, + x_replaceMsgId text, + x_origMessage text, + x_vncConference text, + x_forwardMessage text, + mention text, + PRIMARY KEY (sort_id) +); + +create table if not exists recent_history_table ( + username text, + target text, + type text, + timestamp integer, + message text, + original_message text, + x_attachment text, + x_conference text, + incoming boolean, + PRIMARY KEY (target, username) +); +alter table recent_history_table add column message_id text; +alter table recent_history_table add column received_receipt boolean; +alter table recent_history_table add column updated_at integer; +alter table recent_history_table add column mute_sound integer; +alter table recent_history_table add column mute_notification integer; +alter table recent_history_table add column has_data boolean; +alter table recent_history_table add column broadcast_title text; +alter table recent_history_table add column mentions text; +alter table recent_history_table add column sort_id bigint; +alter table recent_history_table add column deleted boolean; +alter table recent_history_table add column pad_read integer; +alter table recent_history_table add column has_pads boolean; +alter table recent_history_table add column last_mention_time integer; +alter table recent_history_table add column last_avatar_update integer; +alter table recent_history_table add column direct_e2e boolean; +alter table recent_history_table add column has_active_call boolean; +alter table recent_history_table add column last_callstate_update integer; +alter table recent_history_table add column x_conference_start integer; +alter table recent_history_table add column retention_time integer; +alter table recent_history_table add column x_conference_scheduler text; +alter table recent_history_table add column is_favourite boolean; +alter table recent_history_table add column is_pinned boolean; +alter table recent_history_table add column audience_only boolean; +alter table recent_history_table add column has_iom boolean; +alter table recent_history_table add column pin_order integer; + +update recent_history_table set updated_at=0 where updated_at is NULL; +update recent_history_table set mute_notification=0 where mute_notification is NULL; +update recent_history_table set mute_sound=0 where mute_sound is NULL; +update recent_history_table set has_data=true where has_data is NULL; + + +create table if not exists archive_inactive_table ( + username text, + target text, + timestamp integer, + content text, + PRIMARY KEY (target, username) +); + +create or replace rule insert_into_archive_inactive as + on insert to archive_inactive_table where (exists (select 1 from archive_inactive_table where username=NEW.username and target=NEW.target)) + do instead + update archive_inactive_table set timestamp=NEW.timestamp, + content=NEW.content + where target=NEW.target and username=NEW.username; + +create or replace rule update_recent_archive as + on update to archive_inactive_table + do update recent_history_table set updated_at=extract(epoch from now())::integer + where target=NEW.target and username=NEW.username; + +create table if not exists broadcast_audience ( + broadcast_owner text, + broadcast_target text, + audience jsonb, + title text, + PRIMARY KEY (broadcast_owner, broadcast_target) +); + +alter table broadcast_audience add column tags jsonb; +alter table broadcast_audience add column description text; + +create or replace rule upsert_broadcast_audience as + on insert to broadcast_audience where (exists (select 1 from broadcast_audience where broadcast_target=NEW.broadcast_target)) + do instead + update broadcast_audience set + audience=NEW.audience, + title=NEW.title, + tags=NEW.tags, + description=NEW.description + where broadcast_target=NEW.broadcast_target; + + +create table if not exists group_owners ( + room text, + owner text, + primary key (room) +); + +alter table group_owners add column created integer; +alter table group_owners add column updated integer; + +create or replace rule upsert_group_owners as + on insert to group_owners where (exists (select 1 from group_owners where room=NEW.room)) + do instead + update group_owners set + owner=NEW.owner + where room=NEW.room; + + + +create table if not exists group_avatarids ( + room text, + avatarid text, + primary key (room) +); + +create or replace rule upsert_group_avatarids as + on insert to group_avatarids where (exists (select 1 from group_avatarids where room=NEW.room)) + do instead + update group_avatarids set + avatarid=NEW.avatarid + where room=NEW.room; + + +create or replace rule cache_group_avatarids as + on insert to prosody where NEW.store='vcard_muc' and NEW.key='' and NEW.type='json' and NEW.host like 'conference.%' + do insert into group_avatarids (room, avatarid) select + NEW.user||'@'||NEW.host as room, + encode(digest(decode(split_part(split_part(split_part(NEW.value, 'BINVAL', 2), '__array":["', 2),'"',1), 'base64'), 'sha1'), 'hex') as groupavarid; + + +create table if not exists conferenceMapping ( + conferenceKey text, + value text, + primary key (conferenceKey) +); + +create or replace rule upsert_confmap as + on insert to conferenceMapping where (exists (select 1 from conferenceMapping where conferenceKey=NEW.conferenceKey)) + do instead update conferenceMapping + set value=NEW.value + where conferenceKey=NEW.conferenceKey; + +create table if not exists conferenceMap ( + conferenceKey text, + value text, + primary key (conferenceKey) +); + +create or replace rule upsert_conferencemap as + on insert to conferenceMap where (exists (select 1 from conferenceMap where conferenceKey=NEW.conferenceKey)) + do instead update conferenceMap + set value=NEW.value + where conferenceKey=NEW.conferenceKey; + + + + +create table if not exists exclude_from_history ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create or replace rule insert_into_exclude_from_history as + on insert to exclude_from_history where (exists (select 1 from exclude_from_history where username=NEW.username and target=NEW.target)) + do instead + update exclude_from_history set timestamp=NEW.timestamp + where username=NEW.username and target=NEW.target; + + +create table if not exists read_conversation ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create or replace rule insert_into_read_conversation as + on insert to read_conversation where (exists (select 1 from read_conversation where username=NEW.username and target=NEW.target)) + do instead + update read_conversation set timestamp=NEW.timestamp + where target=NEW.target and username=NEW.username; + + +create table if not exists read_pad ( + username text, + target text, + timestamp integer, + PRIMARY KEY (target, username) +); + +create or replace rule insert_into_read_pad as + on insert to read_pad where (exists (select 1 from read_pad where username=NEW.username and target=NEW.target)) + do instead + update read_pad set timestamp=NEW.timestamp + where target=NEW.target and username=NEW.username; + + +create table if not exists external_email_invites ( + username text, + meeting text, + email text, + timestamp integer, + PRIMARY KEY (username, meeting, email) +); + +create or replace rule inser_external_email_invites as + on insert to external_email_invites where (exists (select 1 from external_email_invites where username=NEW.username and meeting=NEW.meeting and email=NEW.meeting)) + do instead nothing; + + +create or replace rule update_read_conversation as + on update to read_conversation do + update recent_history_table set + updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule update_read_pad as + on update to read_pad do + update recent_history_table set + pad_read=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +drop table if exists total_mentions cascade; + +create table if not exists totalmentions ( + username text, + target text, + key text, + type text, + PRIMARY KEY (target, username, key) +); + +create rule upsert_totalmentions as + on insert to totalmentions where (exists (select 1 from totalmentions where username=NEW.username and target=NEW.target and key=NEW.key)) + do instead nothing; + +create or replace view total_mentions as +select + username, + target, + 'groupchat' as type, + count(*) as total +from totalmentions group by username, target; + + +-- rule to do update instead of insert when record exists +create or replace rule instert_into_chat_recent as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and NEW.timestamp is not null) + do instead + update recent_history_table set + type=NEW.type, + message=NEW.message, + original_message=NEW.original_message, + x_attachment=NEW.x_attachment, + x_conference=NEW.x_conference, + timestamp=NEW.timestamp, + incoming=NEW.incoming, + message_id=NEW.message_id, + received_receipt=null, + updated_at=NEW.timestamp, + has_data=true, + broadcast_title=NEW.broadcast_title, + sort_id=NEW.sort_id, + deleted=false, + mentions=NEW.mentions + where target=NEW.target and username=NEW.username and ((recent_history_table.deleted = false) or (NEW.deleted IS NOT NULL)); +-- where target=NEW.target and username=NEW.username; + +-- rule to do update instead of insert when record exists +create or replace rule instert_into_chat_recent_mute as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and (NEW.timestamp is null) and (NEW.mute_notification is null)) + do instead + update recent_history_table set + mute_sound = NEW.mute_sound, + updated_at=EXTRACT(EPOCH FROM now())::integer + where target=NEW.target and username=NEW.username; + +create or replace rule instert_into_chat_recent_notify as + on insert to recent_history_table where ((exists (select 1 from recent_history_table where target=NEW.target and username=NEW.username)) and (NEW.timestamp is null) and (NEW.mute_sound is null)) + do instead + update recent_history_table set + mute_notification = NEW.mute_notification, + updated_at=EXTRACT(EPOCH FROM now())::integer + where target=NEW.target and username=NEW.username; + + +-- create or replace rule update_muc_mention as +-- on update to recent_history_table where (NEW.type = 'groupchat' AND (NEW.mentions ilike '%xmpp:'||NEW.username||'%')) +-- do insert into total_mentions (username, target, type, total) select +-- NEW.username as username, +-- NEW.target as target, +-- 'groupchat' as type, +-- 0 as total; + +create or replace rule update_muc_mention as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + + +-- muc remote mentions +create or replace rule update_muc_remote_mention as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + NEW.with as target, + NEW.key as key, + 'groupchat' as type; + + +create table if not exists room_nick_jid_map ( + room_name text, + user_jid text, + nickname text, + primary key (room_name, user_jid, nickname) +); + +alter table room_nick_jid_map add column since integer default 0; + +create or replace rule + insert_room_nick_jid_map as + on insert to room_nick_jid_map where (exists ( select 1 from room_nick_jid_map where room_name=NEW.room_name and user_jid=NEW.user_jid and nickname=NEW.nickname)) + do instead nothing; +-- check +-- update room_nick_jid_map +-- set since = NEW.since +-- where room_name=NEW.room_name and user_jid=NEW.user_jid and nickname=NEW.nickname; + +-- do instead nothing; + +create or replace view room_nicknames as + select + test.value as username, + test.key as user_room_nickname, + prosody.host as host, + prosody.user as room, + prosody.user || '@' || prosody.host as room_name + from prosody, jsonb_each_text(prosody.value::jsonb) as test + where + prosody.key='_occupants' ; + + +create or replace rule update_group_owners as + on insert to prosody where NEW.key='_affiliations' and (NEW.host like 'conference.%') and NEW.store='config' and (NEW.value ilike '%":"owner"%') + do insert into group_owners (room, owner) + select NEW.user||'@'||NEW.host as room, + reverse(split_part(reverse(split_part(NEW.value, '":"owner"',1)),'"',1)) as owner; + + +-- to remove I suppose! +create or replace rule update_room_nick_jid_map as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into room_nick_jid_map (nickname, user_jid, room_name, since) + select + distinct on (room_nicknames.user_room_nickname) room_nicknames.user_room_nickname as nickname, + room_nicknames.username as user_jid, + room_nicknames.room_name as room_name, + extract(epoch from now())::integer as since + from room_nicknames where room_nicknames.host=NEW.host and room_nicknames.room=NEW.user + on conflict do nothing; + +-- update room_nick_jid from invite messages +create or replace rule update_room_nick_jid_map_invitee as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||(split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))) as nickname, + (case + when ((xpath('/message/x:x/x:invite/x:reason/text() | /jc:message/x:x/x:invite/x:reason/text()', NEW.value::xml, ARRAY[ARRAY['x', 'http://jabber.org/protocol/muc#user'], ARRAY['jc', 'jabber:client']]))[1]::text = '1') then 0 + else extract(epoch from now())::integer + end) as since; + + -- extract(epoch from now())::integer as since; + +create or replace rule update_room_nick_jid_map_inviter as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2)) as nickname, + extract(epoch from now())::integer; + +create or replace rule update_room_nick_jid_map_remote_inv as + on insert to prosodyarchive where NEW.store = 'muc_remote_inv' + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + NEW.user||'@'||NEW.host as room_name, + NEW.with as user_jid, + NEW.user||'@'||NEW.host||'/'||NEW.with as nickname, + extract(epoch from now())::integer; + + +create or replace rule init_read_conversation_invitee as + on insert to prosodyarchive where NEW.store = 'archive' and (NEW.with like '%@conference.%') + do insert into read_conversation (username, target, timestamp) + select + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as username, + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as target, + (extract(epoch from now())::integer - 60) as timestamp; + + + +-- end updtae + +create or replace rule update_room_nick_jid_map_remote as + on insert to prosody where NEW.store='muc_remote' and (NEW.key like '%/%') and (NEW.value like '%@%') + do insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part(NEW.key,'/',1)) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + extract(epoch from now())::integer; + +create or replace view room_membership as + select + jsonb_object_keys(prosody.value::jsonb) as username, + ''::text as user_room_nickname, + prosody.host as host, + prosody.user as room + from prosody + where prosody.key='_affiliations'; + + +-- rule to extract new singlechat message from prosodyarchive +create or replace rule update_single_chat_recent as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'chat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (case + when (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + NEW.when as updated_at, + true as has_data, + false as deleted + where + NEW.store='archive' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL + and ( ( + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] IS NOT NULL AND + (xpath('//vncTalkConference/conferenceId/text() | //xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text ilike '%#%' + ) or (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] IS NULL ) + AND NOT (NEW.with like '%@conference.%') + AND NOT (NEW.with = NEW.user || '@' || NEW.host) + order by target,timestamp desc; + +-- rule to add broadcast +create or replace rule update_broadcast_recent as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, broadcast_title, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as target, + 'broadcast' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_title, + (case + when (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + NEW.when as updated_at, + true as has_data, + false as deleted + where + NEW.store='archive' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NOT NULL + AND NOT (NEW.with like '%@conference.%') + order by timestamp desc; + +-- rule to update broadcast audience list + +create or replace rule update_broadcast_audience as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title, tags, description) select + NEW.with as broadcast_owner, + (xpath('//message/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_target, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:to/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as audience, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as title, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:tag/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as tags, + (xpath('//message/xc:vncTalkBroadcast/@description', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as description + where + NEW.store='archive' + AND (xpath('//message/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + ; + + +create or replace rule update_broadcast_audience_remote as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title) select + NEW.with as broadcast_owner, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] as broadcast_target, + '[]' as audience, + (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as title + WHERE + NEW.store='archive' + AND (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1] IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (split_part(NEW.with, '@', 2) <> NEW.host); + +-- rule to extract new groupchat message from prosodyarchive +create or replace rule update_muc_recent as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text as mentions, + (case + when ((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + NEW.when as updated_at, + ((xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] is not null) as deleted, + true as has_data + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room and NEW.store='muc_log' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + order by username, room_membership.user_room_nickname desc; + + +create or replace rule update_muc_remote_recent as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, has_data) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'groupchat' as type, + NEW.when as timestamp, + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1] as message, + (xpath('/message/@id | /jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS message_id, + (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as original_message, + (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_attachment, + (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] as x_conference, + (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text as mentions, + (case + when ((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text = room_nick_jid_map.nickname) then false + else true + end) as incoming, + false as received_receipt, + NEW.when as updated_at, + true as has_data + from room_nick_jid_map where NEW.with=room_nick_jid_map.room_name and (room_nick_jid_map.user_jid = (NEW.user || '@' || NEW.host)) and NEW.store='muc_remote' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + order by username, room_nick_jid_map.nickname desc; + +-- add direct_e2e field here +-- drop this +drop rule if exists update_receipt_recent on prosodyarchive cascade; + + + + +-- rule to mark conversation read on new singlechat message from prosodyarchive +create or replace rule mark_single_chat_read as + on insert to prosodyarchive where NEW.store = 'archive' + AND split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + do insert into read_conversation (username, target, "timestamp") + SELECT + (new."user" || '@'::text) || new.host AS username, + NEW.with as target, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) = NEW.with; + + +-- rule to extract new groupchat message from prosodyarchive +create or replace rule mark_muc_read as + on insert to prosodyarchive where NEW.store = 'muc_log' + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text IS NULL + do insert into read_conversation (username, target, timestamp) select + room_nick_jid_map.user_jid AS username, + (new."user" || '@'::text) || new.host AS target, + new."when" AS "timestamp" + FROM room_nick_jid_map + WHERE room_nick_jid_map.room_name = ((new."user" || '@'::text) || new.host) AND room_nick_jid_map.nickname = (xpath('//message/@from | //jc:message/@from'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text + ; + + +create table if not exists unread_message_ids ( + message_id text, + sender text, + receipient text, + timestamp integer, + primary key ( message_id, sender, receipient) +); + + +create or replace rule + upsert_unread_message_ids as + on insert to unread_message_ids where (exists ( select 1 from unread_message_ids where message_id=NEW.message_id and sender=NEW.sender and receipient=NEW.receipient)) + do instead nothing; + + +create or replace rule insert_unread_message_ids_chat as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into unread_message_ids (message_id, sender, receipient, timestamp) + select + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.with as sender, + split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) as receipient, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) <> NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL + AND NOT (NEW.with like '%@conference.%') + ; + +create or replace rule insert_unread_message_ids_broadcast as + on insert to prosodyarchive where NEW.store = 'archive' + do insert into unread_message_ids (message_id, sender, receipient, timestamp) + select + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as receipient, + split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) as receipient, + NEW.when as timestamp + where split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1) <> NEW.with + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + and (xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']])) IS NOT NULL + and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NOT NULL + AND NOT (NEW.with like '%@conference.%') + ; + + + +create or replace rule remove_unread_message_ids_deleted_chat as + on insert to prosodyarchive where NEW.store = 'archive' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + + + +create or replace rule insert_unread_message_ids_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into unread_message_ids (receipient, message_id, sender, timestamp) + select + DISTINCT room_membership.username AS receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.user || '@' || NEW.host as sender, + NEW.when::integer as timestamp + from room_membership + where room_membership.host=NEW.host and room_membership.room=NEW.user and room_membership.username not + in (select username from room_membership where user_room_nickname=((xpath('//message/@from | //jc:message/@from'::text, NEW.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text)) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +; + +create or replace rule remove_unread_message_ids_deleted_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + +create or replace rule insert_unread_message_ids_remotemuc as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into unread_message_ids (receipient, message_id, sender, timestamp) + select + NEW.user || '@' || NEW.host as receipient, + NEW.key as message_id, + NEW.with as sender, + NEW.when as timestamp +where (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',2) <> NEW.user || '@' || NEW.host) +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL +AND (NEW.with like '%@conference.%') +; + +create or replace rule remove_unread_message_ids_deleted_remotemuc as + on insert to prosodyarchive where NEW.store = 'muc_remote' and (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NOT NULL + do delete from unread_message_ids where message_id=(xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + + +create or replace view unread_conversation_ids as + select + read_conversation.username as username, + read_conversation.target as target, + jsonb_agg(unread_message_idjoin.message_id) as unreadids + from read_conversation + join unread_message_ids as unread_message_idjoin on + ( unread_message_idjoin.receipient=read_conversation.username and unread_message_idjoin.sender=read_conversation.target and unread_message_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_ids.receipient as username, + unread_message_ids.sender as target, + jsonb_agg(unread_message_ids.message_id) as unreadids +from unread_message_ids +where unread_message_ids.receipient||'#'||unread_message_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_ids.receipient, unread_message_ids.sender +; + + +create table if not exists mute_conversation ( + username text, + target text, + PRIMARY KEY (target, username) +); + +create or replace rule + insert_mute_conversation as + on insert to mute_conversation where (exists ( select 1 from mute_conversation where username=NEW.username and target=NEW.target)) + do instead nothing; + +create or replace rule + update_mute_conversation as + on update to mute_conversation + do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create table if not exists mute_notification ( + username text, + target text, + type integer, + PRIMARY KEY (target, username) +); + + +create or replace rule upsert_mute_notification as + on insert to mute_notification where (exists ( select 1 from mute_notification where username=NEW.username and target=NEW.target)) + do instead update mute_notification + set type=NEW.type where username=NEW.username and target=NEW.target; + +create or replace rule update_mute_notification as + on update to mute_notification do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace rule insert_mute_notification as + on insert to mute_notification do update recent_history_table + set updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.username=NEW.username and recent_history_table.target=NEW.target; + +create or replace view unread_conversation_counts as + select + read_conversation.username as username, + read_conversation.target as target, + count(unread_message_idjoin.message_id) as unreadidcount + from read_conversation + join unread_message_ids as unread_message_idjoin on + ( unread_message_idjoin.receipient=read_conversation.username and unread_message_idjoin.sender=read_conversation.target and unread_message_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_ids.receipient as username, + unread_message_ids.sender as target, + count(unread_message_ids.message_id) as unreadidcount +from unread_message_ids +where unread_message_ids.receipient||'#'||unread_message_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_ids.receipient, unread_message_ids.sender +; + + +create table profile_update_queue ( + username text, + timestamp integer, + PRIMARY KEY (username) +); + + +create or replace rule upsert_profile_update_queue as + on insert to profile_update_queue where (exists (select 1 from profile_update_queue where username=NEW.username)) + do instead update profile_update_queue set + timestamp=NEW.timestamp + where username=NEW.username; + +create or replace rule update_profile_queue_from_insert as + on insert to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') + do insert into profile_update_queue (username, timestamp) select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + +create or replace rule update_profile_queue_from_update as + on update to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') + do insert into profile_update_queue (username, timestamp) select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + + +create table if not exists unread_message_mention_ids ( + message_id text, + sender text, + receipient text, + timestamp integer, + primary key ( message_id, sender, receipient) +); + + +create or replace rule + upsert_unread_message_mention_ids as + on insert to unread_message_mention_ids where (exists ( select 1 from unread_message_mention_ids where message_id=NEW.message_id and sender=NEW.sender and receipient=NEW.receipient)) + do instead nothing; + + +create or replace rule insert_unread_mention_ids_muc as + on insert to prosodyarchive where NEW.store = 'muc_log' + do insert into unread_message_mention_ids (receipient, message_id, sender, timestamp) + select + DISTINCT room_membership.username AS receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.user || '@' || NEW.host as sender, + NEW.when::integer as timestamp + from room_membership + where room_membership.host=NEW.host and room_membership.room=NEW.user and room_membership.username not + in (select username from room_membership where user_room_nickname=((xpath('//message/@from | //jc:message/@from'::text, NEW.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text)) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL + AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL + AND (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', new.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text like '%xmpp:'||room_membership.username||'%' +; + +-- ToDo: insert_unread_mention_ids_muc_remote + +create or replace rule insert_unread_message_ids_remotemuc as + on insert to prosodyarchive where NEW.store = 'muc_remote' + do insert into unread_message_mention_ids (receipient, message_id, sender, timestamp) + select + NEW.user || '@' || NEW.host as receipient, + (case + when (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text IS NULL then NEW.key + else (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text + end) as message_id, + NEW.with as sender, + NEW.when as timestamp +where (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',2) <> NEW.user || '@' || NEW.host) +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text +AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, new.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +AND (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', new.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +AND (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text IS NULL +and (xpath('//message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text IS NULL +AND (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', new.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text like '%xmpp:'||NEW.user||'@'||NEW.host||'%' +AND (NEW.with like '%@conference.%') +; + + + +create or replace view unread_mention_ids as + select + read_conversation.username as username, + read_conversation.target as target, + jsonb_agg(unread_message_mention_idjoin.message_id) as unreadids + from read_conversation + join unread_message_mention_ids as unread_message_mention_idjoin on + ( unread_message_mention_idjoin.receipient=read_conversation.username and unread_message_mention_idjoin.sender=read_conversation.target and unread_message_mention_idjoin.timestamp>read_conversation.timestamp) + group by read_conversation.username, read_conversation.target +union + select + unread_message_mention_ids.receipient as username, + unread_message_mention_ids.sender as target, + jsonb_agg(unread_message_mention_ids.message_id) as unreadids +from unread_message_mention_ids +where unread_message_mention_ids.receipient||'#'||unread_message_mention_ids.sender not in (select username||'#'||target from read_conversation) +group by unread_message_mention_ids.receipient, unread_message_mention_ids.sender +; + + + +-- some required migrations to fill data... + +insert into totalmentions (username, target, key, type) + select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', prosodyarchive.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + prosodyarchive.user||'@'||prosodyarchive.host as target, + prosodyarchive.key as key, + 'groupchat' as type +from prosodyarchive +where (prosodyarchive.value like '%xmpp:%') and (prosodyarchive.value like '%reference type=''mention'' uri=''xmpp%') and prosodyarchive.store='muc_log'; + + +insert into totalmentions (username, target, key, type) + select + split_part(split_part(jsonb_array_elements(to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', prosodyarchive.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::text, 'xmpp:',2), '"',1) as username, + prosodyarchive.with as target, + prosodyarchive.key as key, + 'groupchat' as type +from prosodyarchive +where (prosodyarchive.value like '%xmpp:%') and (prosodyarchive.value like '%reference type=''mention'' uri=''xmpp%') and prosodyarchive.store='muc_remote'; + +insert into group_owners (room, owner) +select + prosody.user||'@'||prosody.host as room, + reverse(split_part(reverse(split_part(prosody.value, '":"owner"',1)),'"',1)) as owner +from prosody +where + prosody.key='_affiliations' and (reverse(split_part(reverse(split_part(prosody.value, '":"owner"',1)),'"',1)) like '%@%'); + +insert into group_avatarids (room, avatarid) +select + prosody.user||'@'||prosody.host as room, + encode(digest(decode(split_part(split_part(split_part(prosody.value, 'BINVAL', 2), '__array":["', 2),'"',1), 'base64'), 'sha1'), 'hex') as groupavarid +from prosody where prosody.store='vcard_muc' and prosody.key='' and prosody.type='json' and prosody.host like 'conference.%'; + +create table if not exists no_notify_before( + username text, + before integer, + PRIMARY KEY (username) +); + +create or replace rule upsert_no_notifiy as + on insert to no_notify_before where (exists (select 1 from no_notify_before where username=NEW.username)) + do instead + update no_notify_before set + before=NEW.before + where username=NEW.username; + +create or replace view user_notify_opt0 as + select prosody.user||'@'||prosody.host as email from prosody where + ( prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"0%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\"0%' ); + +create or replace view user_notify_opt1 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"1%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"1%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"1%' + or prosody.value like '%emailNotification"},"__array":["\\"1%' + ); + +create or replace view user_notify_opt2 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"2%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"2%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"2%' + or prosody.value like '%emailNotification"},"__array":["\\"2%'); + +create or replace view user_notify_opt3 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"3%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"3%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"3%' + or prosody.value like '%emailNotification"},"__array":["\\"3%'); + +create or replace view user_notify_opt4 as + select prosody.user||'@'||prosody.host as email from prosody where ( + prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"name":"document","__array":["\\"4%' + or prosody.value like '%emailNotification"},"name":"document","__array":["\\"4%' + or prosody.value like '%emailNotification","xmlns":"stanza:io:json"},"__array":["\\"4%' + or prosody.value like '%emailNotification"},"__array":["\\"4%'); + + +create or replace view digest_messages as +select + prosodyarchive.user||'@'||prosodyarchive.host as email, + prosodyarchive.when as when, + (xpath('/message/@from | /jc:message/@from', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS from, + (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS body, + (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as broadcast, + (xpath('/message/xc:attachment | /jc:message/xc:attachment', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text AS x_attachment, + (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', prosodyarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text as x_vncConference, + (xpath('/message/@from | /jc:message/@from', prosodyarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS target, + NULL as enddtime, + NULL as starttime, + 'chat' as type +from prosodyarchive +where prosodyarchive.store='offline' and (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, prosodyarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, prosodyarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +union +select + recent_history_table.username as email, + mucarchive.when as when, + (xpath('/message/@from | /jc:message/@from', mucarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS from, + (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', mucarchive.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text AS body, + (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text as broadcast, + (xpath('/message/xc:attachment | /jc:message/xc:attachment', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text AS x_attachment, + (xpath('/message/xc:vncTalkConference | /jc:message/xc:vncTalkConference', mucarchive.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text as x_vncConference, + recent_history_table.target as target, + recent_history_table.timestamp as endtime, + read_conversation.timestamp as starttime, + 'groupchat' as type +from + recent_history_table, read_conversation +join prosodyarchive as mucarchive on ( + mucarchive.user = split_part(read_conversation.target, '@', 1) and + mucarchive.host = split_part(read_conversation.target, '@', 2) and + mucarchive.store='muc_log' and + (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text and + mucarchive.when > read_conversation.timestamp and + ((xpath('//message/@from | //jc:message/@from'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text + not in (select nickname from room_nick_jid_map where room_name=read_conversation.target and user_jid=read_conversation.username)) + ) +where + recent_history_table.type='groupchat' + AND recent_history_table.username=read_conversation.username + AND recent_history_table.target=read_conversation.target + AND (recent_history_table.timestamp > read_conversation.timestamp) + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text <> ''::text + AND (xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, mucarchive.value::xml, ARRAY[ARRAY['jc'::text, 'jabber:client'::text]]))[1]::text IS NOT NULL +; + + +create or replace view room_membernames as SELECT room_nick_jid_map.room_name, + jsonb_agg(room_nick_jid_map.user_jid) AS members, + split_part(split_part(mucjoin.value, '"subject":'::text, 2), '"'::text, 2) AS title, + jsonb_agg( + CASE + WHEN ((((((split_part(split_part(split_part(namejoin.value, 'NICKNAME","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'GIVEN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FAMILY","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) = ' '::text THEN namejoin."user" + ELSE (((((split_part(split_part(split_part(namejoin.value, 'NICKNAME","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'GIVEN","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1)) || ' '::text) || split_part(split_part(split_part(namejoin.value, 'FAMILY","'::text, 2), '__array":["'::text, 2), '"]}'::text, 1) + END) AS membernames + FROM room_nick_jid_map + LEFT JOIN prosody mucjoin ON room_nick_jid_map.room_name = ((mucjoin."user" || '@'::text) || mucjoin.host) AND mucjoin.store = 'config'::text AND mucjoin.key = '_data'::text AND mucjoin.value ~~ '%"subject":%'::text + LEFT JOIN prosody namejoin ON room_nick_jid_map.user_jid = ((namejoin."user" || '@'::text) || namejoin.host) AND namejoin.store = 'vcard'::text AND namejoin.key = ''::text AND namejoin.type = 'json'::text + GROUP BY room_nick_jid_map.room_name, mucjoin.value; + + +create table if not exists ep_author_ids ( + username text, + authorid text, + primary key (username) +); + +create or replace rule upsert_ep_author_ids as + on insert to ep_author_ids where (exists (select 1 from ep_author_ids where username=NEW.username)) + do instead update ep_author_ids set + authorid=NEW.authorid + where username=NEW.username; + + +create table if not exists ep_conv_group ( + convid text, + groupid text, + primary key (convid) +); + +create or replace rule upsert_ep_conv_ids as + on insert to ep_conv_group where (exists (select 1 from ep_conv_group where convid=NEW.convid)) + do instead update ep_conv_group set + groupid=NEW.groupid + where convid=NEW.convid; + + +create table if not exists ep_pad_names ( + padid text, + groupid text, + padname text, + primary key (padid, groupid) +); + +create or replace rule upsert_ep_pad_names as + on insert to ep_pad_names where (exists (select 1 from ep_pad_names where padid=NEW.padid and groupid=NEW.groupid)) + do instead update ep_pad_names set + padname=NEW.padname + where padid=NEW.padid and groupid=NEW.groupid; + + +create table if not exists fcm_log( + id SERIAL, + msgid text, + fromjid text, + tojid text, + tofcmtoken text, + todevicetype text, + timestamp integer, + opstatus text, + primary key(id) +); + + +create table if not exists remote_muc_names ( + username text, + remotemuc text, + displayname text, + primary key (username, remotemuc) +); + +create table if not exists recordings ( + convkey text, + callid text, + fileid text, + timestamp integer, + primary key (convkey, fileid, timestamp) +); + +create or replace rule upsert_remote_muc_names as + on insert to remote_muc_names where (exists (select 1 from remote_muc_names where username=NEW.username and remotemuc=NEW.remotemuc)) + do instead update remote_muc_names set + displayname = NEW.displayname + where username=NEW.username and remotemuc=NEW.remotemuc; + +insert into remote_muc_names (username, remotemuc, displayname) select prosody.user||'@'||prosody.host as username, prosody.key as remotemuc, prosody.value as displayname from prosody where prosody.store='muc_remote' and not (prosody.key ilike '%/%'); + +create or replace rule update_muc_remote_name as + on insert to prosody where (NEW.store='muc_remote' and not (NEW.value ilike '%/%')) + do insert into remote_muc_names (username, remotemuc, displayname) select NEW.user||'@'||NEW.host as username, NEW.key as remotemuc, NEW.value as displayname; + + +create or replace view inverseconfmap as + select + split_part(split_part(value, 'value":"',2), '","', 1) as meeting, + conferencekey as key + from conferencemap; + + + +-- +-- Name: english_nostop; Type: TEXT SEARCH CONFIGURATION; Schema: public; Owner: postgres +-- + +CREATE TEXT SEARCH CONFIGURATION public.english_nostop ( + PARSER = pg_catalog."default" ); + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR asciiword WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR word WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR numword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR email WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR url WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR host WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR sfloat WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR version WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR hword_numpart WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR hword_part WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR hword_asciipart WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR numhword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop +ADD MAPPING FOR asciihword WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR hword WITH public.english_stem_nostop; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR url_path WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR file WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR "float" WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR "int" WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.english_nostop + ADD MAPPING FOR uint WITH simple; + + + +CREATE SEQUENCE if not exists broadcast_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + + + update recent_history_table + set last_mention_time=upd.timestamp from + ( select username, target, timestamp from + (select split_part(split_part(unnest(string_to_array(mention, ',')), '"xmpp:', 2), '"',1) as username, + room as target, processed_messages.date as timestamp from processed_messages where type='groupchat' and (mention is not null) order by processed_messages.date desc) + as mig where username ilike '%@%' ) as upd + where recent_history_table.username=upd.username and recent_history_table.target=upd.target; + + + + + + + + + + +create table if not exists surveymap ( + id SERIAL, + groupchat text, + jid text, + survey text, + expiry integer, + PRIMARY KEY (id, groupchat, jid, survey) +); + + +create table if not exists surveyresults ( + id SERIAL, + surveyid integer, + results text, + jid text, + PRIMARY KEY (id, jid) +); +create index ri3 on surveymap(expiry); + +alter table surveymap add column name text; +alter table surveymap add column description text; +alter table surveymap add column status text; +alter table surveymap add column settings text; + +alter table surveyresults add column status text; + +create or replace rule upsert_survey_results as + on insert to surveyresults where (exists (select 1 from surveyresults where surveyid=NEW.surveyid and jid=NEW.jid)) + do instead + update surveyresults set + results = NEW.results, + status = NEW.status + where surveyid = NEW.surveyid and jid=NEW.jid; + +create index ri2 on surveyresults(surveyid); + +create table if not exists emojireactions ( + msgid text, + reactor text, + reaction text, + PRIMARY KEY (msgid, reactor) +); + +create or replace rule upsert_emojireactions as + on insert to emojireactions where (exists (select 1 from emojireactions where msgid=NEW.msgid and reactor=NEW.reactor)) + do instead + update emojireactions set + reaction = NEW.reaction + where msgid=NEW.msgid and reactor=NEW.reactor; + +create table if not exists room_join_requests ( + requester text, + room text, + date integer, + PRIMARY KEY (requester, room) +); + +create or replace rule upsert_room_join_requests as + on insert to room_join_requests where (exists (select 1 from room_join_requests where requester=NEW.requester and room=NEW.room)) + do instead + update room_join_requests set + date = extract(epoch from now())::integer + where requester=NEW.requester and room=NEW.room; + + +create table if not exists user_custom_images ( + id SERIAL, + jid text, + updated integer, + type text, + filename text, + base64data text, + primary key(jid, filename) +); + +create index uci0 on user_custom_images(id); +create index uci1 on user_custom_images(jid); +create index uci2 on user_custom_images(updated); diff --git a/db-customization/prosody-trigger-noowner.sql b/db-customization/prosody-trigger-noowner.sql new file mode 100644 index 0000000..f93edbd --- /dev/null +++ b/db-customization/prosody-trigger-noowner.sql @@ -0,0 +1,752 @@ +-- for processing inserts with trigger + +create table if not exists processed_messages ( + sort_id bigint, + body text, + date integer, + "from" text, + "to" text, + owner text, + broadcast text, + "broadcast-sender" text, + room text, + type text, + id text, + receipts text, + x_attachment text, + x_location text, + x_replaceMsgId text, + x_origMessage text, + x_vncConference text, + x_forwardMessage text, + mention text, + PRIMARY KEY (sort_id) +); + + +create index si2 on processed_messages ("from"); +create index si3 on processed_messages ("to"); +create index si4 on processed_messages (owner); +create index si5 on processed_messages (broadcast); +create index si6 on processed_messages (room); +create index si7 on processed_messages (type); +create index si8 on processed_messages (id); + +alter table processed_messages add column htmlbody text; +alter table processed_messages add column topicid text; +alter table processed_messages add column parent0 text; +alter table processed_messages add column parent text; +alter table processed_messages add column topic text; +alter table processed_messages add column updated integer; +alter table processed_messages add column broadcast_title text; +alter table processed_messages add column group_action text; +alter table processed_messages add column "encrypted" text; +alter table processed_messages add column encryption text; +alter table processed_messages add column reactions text; +alter table processed_messages add column expiry integer; +alter table processed_messages add column starredBy text[]; +create index si9 on processed_messages (expiry); +create index di1 on processed_messages (date); + + +create or replace rule upsert_processed_messages as + on insert to processed_messages where (exists (select 1 from processed_messages where sort_id=NEW.sort_id)) + do instead + update processed_messages set + body = NEW.body, + htmlbody = NEW.htmlbody, + topicid = NEW.topicid, + parent0 = NEW.parent0, + parent = NEW.parent, + topic = NEW.topic, + date = NEW.date, + "from" = NEW.from, + "to" = NEW.to, + owner = NEW.owner, + broadcast = NEW.broadcast, + "broadcast-sender" = NEW."broadcast-sender", + room = NEW.room, + type = NEW.type, + id = NEW.id, + receipts = NEW.receipts, + x_attachment = NEW.x_attachment, + x_location = NEW.x_location, + x_replaceMsgId = NEW.x_replaceMsgId, + x_origMessage = NEW.x_origMessage, + x_vncConference = NEW.x_vncConference, + group_action = NEW.group_action, + mention = NEW.mention, + expiry = NEW.expiry + where sort_id=NEW.sort_id; + +create table if not exists call_tracking ( + callid bigint, + started_at integer, + updated_at integer, + caller text, + conferenceId text, + receipient text, + state text, + PRIMARY KEY (callid) +); + +create index cri1 on call_tracking (receipient); +create index csi1 on call_tracking (started_at); + +create or replace function fprocess_messages() + returns trigger AS + $BODY$ + DECLARE + _body text; + _htmlbody text; + _topic text; + _parent text; + _parent0 text; + _x_attachment text; + _x_location text; + _x_replaceMsgId text; + _original_message text; + _x_conference text; + _x_forwardMessage text; + _group_action text; + _mention text; + _x_encrypted text; + _encryption text; + _message_id text; + _origtarget text; + _from_bare text; + _x_invite text; + _x_broadcast text; + _x_when integer; + _from text; + _to text; + _received_id text; + _broadcast_title text; + _x_conference_scheduler text; + _x_conferencekey text; + _x_conference_from text; + _x_role text; + _x_affiliation text; + + BEGIN + _body := (xpath('/message/body/text() | /message/jc:body/text() | /jc:message/jc:body/text()', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text; + _htmlbody := (xpath('//message/html/* | //message/xim:html/* | //jc:message/xim:html/*', NEW.value::xml, ARRAY[ARRAY['xim', 'http://jabber.org/protocol/xhtml-im'], ARRAY['jc', 'jabber:client']]))[1]::text; + _topic := (xpath('/message/xc:topic/@name | /jc:message/xc:topic/@name', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _parent0 := (xpath('/message/xc:topic/@parent0 | /jc:message/xc:topic/@parent0', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _parent := (xpath('/message/xc:topic/@parent | /jc:message/xc:topic/@parent', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_attachment := (xpath('//attachment | //xc:attachment', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_location := (xpath('/message/xc:location | /jc:message/xc:location', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_replaceMsgId := (xpath('/message/mc0:replace/@id | /jc:message/mc0:replace/@id', NEW.value::xml, ARRAY[ARRAY['mc0', 'urn:xmpp:message-correct:0'], ARRAY['jc', 'jabber:client']]))[1]::text; + _original_message := (xpath('//originalMessage | //xc:originalMessage', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference := (xpath('//vncTalkConference | //xc:vncTalkConference', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_forwardMessage := (xpath('/message/xc:forwardMessage | /jc:message/xc:forwardMessage', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _group_action := (xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _mention := (to_jsonb(xpath('/message/xr:reference/@uri | /jc:message/xr:reference/@uri', NEW.value::xml, ARRAY[ARRAY['xr', 'urn:xmpp:reference:0'], ARRAY['jc', 'jabber:client']])))::jsonb; + _x_encrypted := (xpath('//jc:message/xo:encrypted |//message/xo:encrypted', NEW.value::xml , ARRAY[ARRAY['jc','jabber:client'],ARRAY['xo','urn:xmpp:omemo:1']]))[1]::text; + _encryption := (xpath('//jc:message/xo:encryption |//message/xo:encryption', NEW.value::xml , ARRAY[ARRAY['jc','jabber:client'],ARRAY['xo','urn:xmpp:eme:0']]))[1]::text; + _message_id := (xpath('//message/@id | //jc:message/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text; + _broadcast_title := (xpath('//message/xc:vncTalkBroadcast/@title', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]; + _origtarget := (xpath('//message/@origtarget | /message/xc:vncTalkBroadcast/@origtarget', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))[1]::text; + _x_invite := (xpath('//message/nsx:x/nsx:invite/@from'::text, new.value::xml, ARRAY[ARRAY['nsx'::text, 'http://jabber.org/protocol/muc#user'::text]]))[1]::text; + _x_broadcast := xpath('//message/xc:vncTalkBroadcast'::text, new.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]); + _x_when := extract(epoch from now())::integer; + _from := (xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text; + _from_bare := split_part(_from, '/', 1); + _to := (xpath('/message/@to | /jc:message/@to', NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:client']]))[1]::text; + _x_conference_scheduler := (xpath('/message/xc:vncTalkConferenceScheduler | /jc:message/xc:vncTalkConferenceScheduler', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text; + _x_role := (xpath('//message/mu:x/mu:item/@role', NEW.value::xml, ARRAY[ARRAY['mu','http://jabber.org/protocol/muc#user']]))[1]::text; + _x_affiliation := (xpath('//message/mu:x/mu:item/@affiliation', NEW.value::xml, ARRAY[ARRAY['mu','http://jabber.org/protocol/muc#user']]))[1]::text; + + if NEW.store = 'muc_log' then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + room_nick_jid_map.user_jid as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.user||'@'||NEW.host as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + + if (_x_role = 'participant' and _x_affiliation = 'none') then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + split_part(_from, '/', 2) as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.user||'@'||NEW.host as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption; + + end if; + + if (jsonb_array_length(_mention::jsonb) > 0) then + insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(_mention::jsonb)::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + end if; + + + if NEW.value like '%ENDED_CALLleave%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference_from := (xpath('//xc:vncTalkConference/xc:from/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'leave' + where callid in (select callid from call_tracking where receipient=_x_conference_from and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + if NEW.value like '%join%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + _x_conference_from := (xpath('//xc:vncTalkConference/xc:from/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update recent_history_table + set has_active_call = true, + updated_at=_x_when, + last_callstate_update=_x_when + where target=NEW.user||'@'||NEW.host; + update call_tracking set + updated_at = _x_when, + state = 'join' + where callid in (select callid from call_tracking where receipient=_x_conference_from and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + if _x_conference_scheduler is not null then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data, x_conference_scheduler, x_conference_start) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, + _message_id AS message_id, + _original_message as original_message, + _x_attachment as x_attachment, _x_conference as x_conference, _mention as mentions, + (case + when (_from = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + extract(epoch from now()) as updated_at, + ((xpath('//group_action/type/text() | //xc:group_action/xc:type/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1] is not null) as deleted, + true as has_data, + _x_conference_scheduler as x_conference_scheduler, + extract(epoch from to_timestamp(((xpath('/message/xc:vncTalkConferenceScheduler/xc:startTime/text() | /jc:message/xc:vncTalkConferenceScheduler/xc:startTime/text()', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text), 'YYYY-MM-DD HH24:MI:SS')) as x_conference_start + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room + order by username, room_membership.user_room_nickname desc; + end if; + + if (_body is not null) and (_body <> '') then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + distinct on (room_membership.username) room_membership.username as username, + NEW.user || '@' || NEW.host as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _mention as mentions, + (case + when (_from = room_membership.user_room_nickname) then false + else true + end) as incoming, + false as received_receipt, + _x_when as updated_at, + (_group_action is not null) as deleted, + true as has_data + from room_membership where NEW.host=room_membership.host and NEW.user=room_membership.room + order by username, room_membership.user_room_nickname desc; + + + if (_x_conference is null) then + insert into read_conversation (username, target, timestamp) select + room_nick_jid_map.user_jid AS username, + (new."user" || '@'::text) || new.host AS target, + _x_when AS "timestamp" + FROM room_nick_jid_map + WHERE room_nick_jid_map.room_name = ((new."user" || '@'::text) || new.host) AND room_nick_jid_map.nickname = _from; + end if; + end if; + end if; + + if NEW.store = 'muc_remote' then + insert into processed_messages (sort_id, body, htmlbody, topic, parent0, parent, date, "from", "to", owner, room, type, id, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, group_action, mention, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, _topic as topic, _parent0 as parent0, _parent as parent, + extract(epoch from now())::integer as date, + room_nick_jid_map.user_jid as "from", + NEW.user||'@'||NEW.host as "to", + NEW.user||'@'||NEW.host as owner, + NEW.with as room, + 'groupchat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as _x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, _group_action as group_action, _mention as mention, + _x_encrypted as "encrypted", _encryption as encryption + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + + -- mentions + if (jsonb_array_length(_mention::jsonb) > 0) then + insert into totalmentions (username, target, key, type) select + split_part(split_part(jsonb_array_elements(_mention::jsonb)::text, 'xmpp:',2), '"',1) as username, + NEW.user || '@' || NEW.host as target, + NEW.key as key, + 'groupchat' as type; + end if; + if (_group_action = 'UPDATE_ENCRYPTION') then + if ((xpath('//group_action/data/text() | //xc:group_action/xc:data/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]::text = 1) then + update recent_history_table set direct_e2e=true where target=NEW.with; + else + update recent_history_table set direct_e2e=false where target=NEW.with; + end if; + end if; + + if NEW.value like '%ENDED_CALLjoin%' then + update recent_history_table + set has_active_call = true, + updated_at=extract(epoch from now())::integer, + last_callstate_update=extract(epoch from now())::integer + where target=NEW.with; + end if; + + if _x_conference_scheduler is not null then + update recent_history_table + set x_conference_scheduler=_x_conference_scheduler, + x_conference_start=extract(epoch from to_timestamp(((xpath('/message/xc:vncTalkConferenceScheduler/xc:startTime/text() | /jc:message/xc:vncTalkConferenceScheduler/xc:startTime/text()', NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk'], ARRAY['jc', 'jabber:client']]))[1]::text), 'YYYY-MM-DD HH:MI:SS')) + where recent_history_table.target=NEW.with; + end if; + + if (_body is not null) and (_body <> '') then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, mentions, incoming, received_receipt, updated_at, deleted, has_data) select + NEW.user||'@'||NEW.host as username, + NEW.with as target, + 'groupchat' as type, + extract(epoch from now()) as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _mention as mentions, + (case + when (NEW.user||'@'||NEW.host = room_nick_jid_map.user_jid) then false + else true + end) as incoming, + false as received_receipt, + _x_when as updated_at, + (_group_action is not null) as deleted, + true as has_data + from room_nick_jid_map + where (room_nick_jid_map.nickname = _from); + end if; + + end if; + + if NEW.store = 'archive' then + -- processed_messages + if (NEW.with != NEW.user||'@'||NEW.host) then + insert into processed_messages (sort_id, body, htmlbody, date, "from", "to", "broadcast-sender", owner, type, id, broadcast, + x_attachment, x_location, x_replaceMsgId, x_origMessage, x_vncConference, x_forwardMessage, broadcast_title, group_action, "encrypted", encryption) + select + NEW.sort_id as sort_id, + _body as body, _htmlbody as htmlbody, + extract(epoch from now())::integer as date, + (case + when (_origtarget IS NULL) then _from_bare + else _origtarget + end) as from, + _to AS "to", + (case + when _origtarget IS NULL then + NULL + else _from_bare + end) as "broadcast-sender", + NEW.user||'@'||NEW.host as owner, + 'chat' as type, + (case + when _message_id IS NULL then NEW.key + else _message_id + end) as id, + _origtarget as broadcast, + _x_attachment as x_attachment, _x_location as x_location, _x_replaceMsgId as x_replaceMsgId, _original_message as original_message, + _x_conference as x_conference, _x_forwardMessage as x_forwardMessage, + _broadcast_title as broadcast_title, + _group_action as group_action, _x_encrypted as "encrypted", _encryption as encryption + where + _x_invite IS NULL + ; + + + if NEW.value like '%invite%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + insert into conferencemap (conferencekey, value) + select + _x_conferencekey as conferencekey, + '{"value":"'||(xpath('//xc:vncTalkConference/xc:jitsiRoom/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]||'","jitsiurl":"'||(xpath('//xc:vncTalkConference/xc:jitsiURL/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]||'"}' as value + ; + if NEW.with = _to then + insert into call_tracking (callid, started_at, caller, conferenceId, receipient, state) + select + NEW.sort_id as callid, + _x_when as started_at, + _from_bare as caller, + _x_conferencekey as conferenceId, + _to as receipient, + 'invite' as state + ; + end if; + end if; + + if NEW.value like '%leave%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'leave' + where callid in (select callid from call_tracking where (receipient=_from_bare or caller=_from_bare) and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + if NEW.value like '%join%' then + _x_conferencekey := (xpath('//xc:vncTalkConference/xc:conferenceId/text()', NEW.value::xml, ARRAY[ARRAY['xc','xmpp:vnctalk']]))[1]; + update call_tracking set + updated_at = _x_when, + state = 'join' + where callid in (select callid from call_tracking where receipient=_from_bare and conferenceid=_x_conferencekey order by callid desc limit 1); + end if; + + + if (NEW.with like '%@conference.%') then + -- init read conversation + insert into read_conversation (username, target, timestamp) + select + (split_part(_to, '/',1)) as username, + (split_part(_from, '/',1)) as target, + (extract(epoch from now())::integer - 60) as timestamp; + -- update room_nick_jid_map_inviter + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part(_from, '/',1)) as room_name, + split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2) as user_jid, + ((split_part(_from, '/',1))||'/'||split_part(split_part(xpath('//body/text() | /jc:message/jc:body/text() | //jc:body/text()'::text, NEW.value::xml, ARRAY[ARRAY['jc', 'jabber:x:conference']])::text, '/', 1), '"', 2)) as nickname, + extract(epoch from now())::integer; + -- update room_nick_jid_map_invitee + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + (split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as room_name, + (split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1)) as user_jid, + ((split_part((xpath('//message/@from | //jabberclient:message/@from', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))||'/'||(split_part((xpath('//message/@to | //jabberclient:message/@to', NEW.value::xml, ARRAY[ARRAY['jabberclient','jabber:client']]))[1]::text, '/',1))) as nickname, + extract(epoch from now())::integer as since; + + else + + if (_body is not null) and ((_x_conference is null) or (_x_conference like '%#%')) and (_x_invite is null) then + if (_origtarget is null) then + -- update chat recent + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + NEW.with as target, + 'chat' as type, + _x_when as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + (case + when (split_part(_to, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + _x_when as updated_at, + true as has_data, + false as deleted + order by target,timestamp desc; + end if; + if (_origtarget is not null) and (_x_broadcast is not null) then + insert into recent_history_table (username, target, type, timestamp, message, message_id, original_message, x_attachment, x_conference, broadcast_title, incoming, received_receipt, updated_at, has_data, deleted) select + NEW.user || '@' || NEW.host as username, + _origtarget as target, 'broadcast' as type, _x_when as timestamp, + _body as message, _message_id as message_id, _original_message as original_message, _x_attachment as x_attachment, _x_conference as x_conference, + _broadcast_title as broadcast_title, + (case + when (split_part(_to, '/',1) = NEW.user || '@' || NEW.host) then true + else false + end) as incoming, + NULL as received_receipt, + _x_when as updated_at, + true as has_data, + false as deleted + order by timestamp desc; + end if; + + if (_origtarget is not null) and (_x_broadcast is not null) then + insert into broadcast_audience (broadcast_owner, broadcast_target, audience, title) select + NEW.with as broadcast_owner, + _origtarget as broadcast_target, + array_to_json(xpath('//message/xc:vncTalkBroadcast/xc:*/text()'::text, NEW.value::xml, ARRAY[ARRAY['xc', 'xmpp:vnctalk']]))::jsonb as audience, + _broadcast_title as title; + end if; + + end if; + end if; + end if; + + if (_to = NEW.with) and (_body is not null) and (_body <> '') then + insert into read_conversation (username, target, "timestamp") select + (new."user" || '@'::text) || new.host AS username, + NEW.with as target, + _x_when as timestamp; + end if; + + + end if; + + if NEW.store = 'kick' then + delete from room_nick_jid_map where (room_name=NEW.user||'@'||NEW.host) and (user_jid=NEW.with); + update read_conversation set timestamp=_x_when + 30 where username=NEW.with and (target=NEW.user||'@'||NEW.host); + update recent_history_table set deleted = true where ((target=NEW.user||'@'||NEW.host) and (username = NEW.with)); + end if; + + if NEW.store = 'receipts' then + _received_id := (xpath('//received/@id', NEW.value::xml, ARRAY[ARRAY['jc','jabber:client']]))[1]::text; + + update processed_messages set receipts = _x_when::text, updated = _x_when + where (processed_messages.id = _received_id) + and (processed_messages.owner = NEW.user||'@'||NEW.host) ; + + update recent_history_table set received_receipt=true, updated_at = _x_when + where (recent_history_table.message_id = _received_id) + and (recent_history_table.username = NEW.user||'@'||NEW.host); + + end if; + + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger process_messages after insert on prosodyarchive + for each row execute procedure fprocess_messages(); + + +-- create or replace function fupdate_room_nick_jid_old() +create or replace function fupdate_room_nick_jid() + returns trigger AS + $BODY$ + BEGIN + delete from room_nick_jid_map where (room_name=NEW.user||'@'||NEW.host) and (user_jid not in (select jsonb_object_keys(NEW.value::jsonb) as user_jid)); + update recent_history_table set deleted = true where ((target=NEW.user||'@'||NEW.host) and username not in (select jsonb_object_keys(NEW.value::jsonb))); + insert into room_nick_jid_map (room_name, user_jid, nickname, since) select + NEW.user||'@'||NEW.host as room_name, + jsonb_object_keys(NEW.value::jsonb) as user_jid, + NEW.user||'@'||NEW.host||'/'||jsonb_object_keys(NEW.value::jsonb) as nickname, + (extract(epoch from now())::integer - 60) as since; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + + +create or replace function fupdate_room_nick_jid() + returns trigger AS + $BODY$ + BEGIN + delete from room_nick_jid_map where (room_name=NEW.user||'@'||NEW.host) and (user_jid not in (select jsonb_object_keys(NEW.value::jsonb) as user_jid)); + update recent_history_table set deleted = true where ((target=NEW.user||'@'||NEW.host) and username not in (select jsonb_object_keys(NEW.value::jsonb))); + insert into room_nick_jid_map (room_name, user_jid, nickname, since) select + NEW.user||'@'||NEW.host as room_name, + jsonb_object_keys(NEW.value::jsonb) as user_jid, + NEW.user||'@'||NEW.host||'/'||jsonb_object_keys(NEW.value::jsonb) as nickname, + (extract(epoch from now())::integer - 60) as since; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + + +create trigger update_room_nick_jid after insert on prosody + for each row + when (NEW.key='_affiliations') + execute procedure fupdate_room_nick_jid(); + +create or replace function fupdate_mention_stamp() + returns trigger AS + $BODY$ + BEGIN + update recent_history_table set last_mention_time=extract(epoch from now())::integer + where username=NEW.username and target=NEW.target; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create or replace function fupdate_room_nick_jid_remote() + returns trigger AS + $BODY$ + BEGIN + insert into room_nick_jid_map (room_name, user_jid, nickname, since) select + split_part(NEW.key, '/',1) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + (extract(epoch from now())::integer - 60) as since + where (NEW.key ilike '%/%'); + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger update_room_nick_jid_remote after insert on prosody + for each row + when (NEW.store='muc_remote' and (NEW.key ilike '%/%')) + execute procedure fupdate_room_nick_jid_remote(); + + +create trigger update_mention_stamp after insert on totalmentions + for each row execute procedure fupdate_mention_stamp(); + + +create or replace function fupdate_recent_avatar() + returns trigger AS + $BODY$ + BEGIN + update recent_history_table + set + last_avatar_update = NEW.value::integer, + updated_at=EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target=NEW.user||'@'||new.host and NEW.store='avatarupdate'; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger update_recent_avatar after insert on prosody + for each row + when (NEW.store='avatarupdate') + execute procedure fupdate_recent_avatar(); + + +create or replace function fupdate_iom_callstates() + returns trigger as + $BODY$ + BEGIN + update recent_history_table + set has_active_call=true, + last_callstate_update = EXTRACT(EPOCH FROM now())::integer, + updated_at = EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target = NEW.user and NEW.value='callactive'; + update recent_history_table + set has_active_call=false, + last_callstate_update = EXTRACT(EPOCH FROM now())::integer, + updated_at = EXTRACT(EPOCH FROM now())::integer + where recent_history_table.target = NEW.user and NEW.value='callinactive'; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger update_iom_callstates after insert on prosody + for each row + when (NEW.store='callactive') + execute procedure fupdate_iom_callstates(); + + +create or replace function fupdate_msg_reactions() + returns trigger AS + $BODY$ + BEGIN + update processed_messages + set updated = EXTRACT(EPOCH FROM now())::integer, + reactions = r.v from (select jsonb_agg(jsonb_build_object(reactor, reaction)) as v from emojireactions where emojireactions.msgid=NEW.msgid) as r + where processed_messages.id=NEW.msgid; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger update_msg_reactions after insert or update on emojireactions +for each row execute procedure fupdate_msg_reactions(); + + +create or replace function fupdate_msg_delreactions() + returns trigger AS + $BODY$ + BEGIN + update processed_messages + set updated = EXTRACT(EPOCH FROM now())::integer, + reactions = r.v from (select jsonb_agg(jsonb_build_object(reactor, reaction)) as v from emojireactions where emojireactions.msgid=OLD.msgid) as r + where processed_messages.id=OLD.msgid; + RETURN OLD; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + + +create trigger update_msg_delreactions after delete on emojireactions +for each row execute procedure fupdate_msg_delreactions(); + + +CREATE TABLE if not exists groupchat ( + roomjid text NOT NULL, + roomtitle text, + PRIMARY KEY (roomjid) +); + +CREATE or replace RULE upsert_groupchat AS + ON INSERT TO groupchat WHERE (EXISTS ( SELECT 1 FROM groupchat WHERE (roomjid = new.roomjid))) + DO INSTEAD + UPDATE groupchat SET roomtitle = new.roomtitle + WHERE (groupchat.roomjid = new.roomjid); + + +CREATE or replace FUNCTION fupdate_groupchat() + RETURNS trigger AS + $BODY$ + BEGIN + if NEW.store = 'config' AND NEW.key='_data' then + insert into groupchat (roomjid, roomtitle) select + NEW.user||'@'||NEW.host as roomjid, + NEW.value::jsonb->>'subject' as roomtitle; + end if; + RETURN NEW; + END; + $BODY$ +LANGUAGE plpgsql VOLATILE; + +create trigger update_groupchat AFTER INSERT ON prosody +FOR EACH ROW WHEN (new.key = '_data'::text AND new.store = 'config'::text) +EXECUTE PROCEDURE fupdate_groupchat(); diff --git a/helm/prosody/templates/db-migration-post-upgrade.yaml b/helm/prosody/templates/db-migration-post-upgrade.yaml new file mode 100644 index 0000000..352d612 --- /dev/null +++ b/helm/prosody/templates/db-migration-post-upgrade.yaml @@ -0,0 +1,41 @@ +{{- if .Values.dbMigration.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "prosody.fullname" . }}-db-post-upgrade + labels: + {{- include "prosody.labels" . | nindent 4 }} + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "5" + helm.sh/hook-delete-policy: hook-succeeded +spec: + backoffLimit: 0 + template: + metadata: + labels: + {{- include "prosody.selectorLabels" . | nindent 8 }} + spec: + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: db-migration + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/sh", "/vnc/config/migrate.sh", "post-upgrade"] + env: + {{- range $key, $value := $.Values.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- range $envName, $secretKey := $.Values.secretEnv.keys }} + - name: {{ $envName }} + valueFrom: + secretKeyRef: + name: {{ $.Values.secretEnv.existingSecret | quote }} + key: {{ $secretKey | quote }} + {{- end }} +{{- end }} diff --git a/helm/prosody/templates/db-migration-pre-upgrade.yaml b/helm/prosody/templates/db-migration-pre-upgrade.yaml new file mode 100644 index 0000000..d4c46cf --- /dev/null +++ b/helm/prosody/templates/db-migration-pre-upgrade.yaml @@ -0,0 +1,41 @@ +{{- if .Values.dbMigration.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "prosody.fullname" . }}-db-pre-upgrade + labels: + {{- include "prosody.labels" . | nindent 4 }} + annotations: + helm.sh/hook: pre-upgrade + helm.sh/hook-weight: "-5" + helm.sh/hook-delete-policy: hook-succeeded +spec: + backoffLimit: 0 + template: + metadata: + labels: + {{- include "prosody.selectorLabels" . | nindent 8 }} + spec: + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: db-migration + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/sh", "/vnc/config/migrate.sh", "pre-upgrade"] + env: + {{- range $key, $value := $.Values.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- range $envName, $secretKey := $.Values.secretEnv.keys }} + - name: {{ $envName }} + valueFrom: + secretKeyRef: + name: {{ $.Values.secretEnv.existingSecret | quote }} + key: {{ $secretKey | quote }} + {{- end }} +{{- end }} diff --git a/helm/prosody/values.yaml b/helm/prosody/values.yaml index c48ba98..db7f8bb 100644 --- a/helm/prosody/values.yaml +++ b/helm/prosody/values.yaml @@ -40,6 +40,12 @@ tlsUpdates: enabled: false secretName: none +# Pre-upgrade and post-upgrade Helm hook Jobs that detect the database +# state and run the appropriate SQL migrations from db-customization/. +# The hooks use the same image and DB credentials as the Prosody container. +dbMigration: + enabled: true + service: type: NodePort port: 5280