@marktiderman/genesis-support 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -4
- package/dist/{block-fZjTS2sY.d.ts → block-ClKVvk-d.d.ts} +10 -2
- package/dist/chunk-MMBNG2NK.js +89 -0
- package/dist/chunk-MMBNG2NK.js.map +1 -0
- package/dist/{chunk-ZV4KJBUF.js → chunk-OSB6G6Y3.js} +18 -88
- package/dist/chunk-OSB6G6Y3.js.map +1 -0
- package/dist/{chunk-4VFYVQUF.js → chunk-QNUAKW7K.js} +136 -7
- package/dist/chunk-QNUAKW7K.js.map +1 -0
- package/dist/{copy-CuTArBFg.d.ts → copy-D3rc0SCl.d.ts} +141 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +32 -6
- package/dist/native.js +37 -14
- package/dist/native.js.map +1 -1
- package/dist/web-chat.d.ts +43 -0
- package/dist/web-chat.js +140 -0
- package/dist/web-chat.js.map +1 -0
- package/dist/web.d.ts +1 -1
- package/dist/web.js +2 -1
- package/dist/web.js.map +1 -1
- package/migrations/0001_genesis_support_chat.sql +616 -0
- package/migrations/tests/0001_genesis_support_chat.test.sql +288 -0
- package/package.json +60 -8
- package/dist/chunk-4VFYVQUF.js.map +0 -1
- package/dist/chunk-ZV4KJBUF.js.map +0 -1
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
-- Genesis support on chat: a report's conversation runs on genesis-chat
|
|
2
|
+
--
|
|
3
|
+
-- The `support-chat` block of `@marktiderman/genesis-support`. It needs both halves it joins:
|
|
4
|
+
-- the support tables (`0006_genesis_support.sql` from `@marktiderman/genesis-supabase`) and
|
|
5
|
+
-- the genesis_chat schema (`0001_genesis_chat.sql` and `0002_genesis_chat_coaching.sql` from
|
|
6
|
+
-- `@marktiderman/genesis-chat`). Apply it after all three.
|
|
7
|
+
--
|
|
8
|
+
-- A report stays the record: its type, status, message, screenshots and device context live
|
|
9
|
+
-- in public.support_reports as before. Its conversation (the person and staff replies) moves
|
|
10
|
+
-- to a genesis_chat conversation of kind `support`, one per report:
|
|
11
|
+
--
|
|
12
|
+
-- - the person who sent the report is its only member;
|
|
13
|
+
-- - staff (whoever `private.genesis_support_staff` says) read and reply as observers, and
|
|
14
|
+
-- write internal notes (`visibility: 'observers'`) the person never sees;
|
|
15
|
+
-- - so a report's thread gets live updates, voice notes, photos and files, push, an
|
|
16
|
+
-- assignee (who is pushed every message) and internal notes, from genesis-chat.
|
|
17
|
+
--
|
|
18
|
+
-- Kept in step, by triggers here:
|
|
19
|
+
--
|
|
20
|
+
-- report inserted its conversation is made and the person made its member
|
|
21
|
+
-- report status the conversation's status follows it (open and submitted: open,
|
|
22
|
+
-- in_progress: pending, addressed: solved, wont_fix: closed); a staff
|
|
23
|
+
-- tool that sets the conversation's status (genesis_chat.set_status)
|
|
24
|
+
-- changes the report's the same way. The report is the source.
|
|
25
|
+
-- report title, message the conversation's title
|
|
26
|
+
-- a message the report's last_user_reply_at or last_staff_reply_at, and the
|
|
27
|
+
-- writer's side read (an internal note moves neither)
|
|
28
|
+
-- reading the thread genesis_chat.mark_read moves the report's user_read_at (the person)
|
|
29
|
+
-- or staff_read_at (staff); support_mark_read moves the person's
|
|
30
|
+
-- place in the conversation; so `unread` keeps working
|
|
31
|
+
-- assignee only staff are assigned (support_assign_report, or set_status)
|
|
32
|
+
-- report deleted the conversation goes, with its messages; its files are queued for
|
|
33
|
+
-- removal by genesis-chat's own rules (the `storage.remove` effect),
|
|
34
|
+
-- screenshots by 0006's. Account deletion deletes the reports, so the
|
|
35
|
+
-- conversations go with them.
|
|
36
|
+
--
|
|
37
|
+
-- support_replies, for one release: clients from before this block still read and write
|
|
38
|
+
-- public.support_replies, so both are mirrored. A reply written there becomes a chat message
|
|
39
|
+
-- (pushed like any other); a chat message (not an internal note) becomes a reply there, with
|
|
40
|
+
-- an edit or delete carried over. A message with only a file says so in the mirrored reply.
|
|
41
|
+
-- While the mirror runs, 0006's limit (60 replies per person per hour) holds every message,
|
|
42
|
+
-- since each passes through support_replies. The next release removes the mirror: a
|
|
43
|
+
-- migration that drops the two mirror triggers below, moves the reply limit onto
|
|
44
|
+
-- genesis_chat.messages, and then may drop public.support_replies.
|
|
45
|
+
--
|
|
46
|
+
-- Existing reports: every report gets its conversation, and every reply in
|
|
47
|
+
-- support_replies is copied in order (staff replies as observers, the person's as the
|
|
48
|
+
-- member, keeping created_at), with no push, no live hint and no link preview (the copy
|
|
49
|
+
-- asserts it queued no effect). private.genesis_support_chat_backfill() does it and can run
|
|
50
|
+
-- again: it adds only what is missing.
|
|
51
|
+
--
|
|
52
|
+
-- One thing changes for the person: a chat message carries its sender's id, so the person
|
|
53
|
+
-- can learn the user id of the staff member who answered (support_replies hid it). Name
|
|
54
|
+
-- staff as "Support" in the app's resolveUser if that matters.
|
|
55
|
+
--
|
|
56
|
+
-- Use: `genesis add support-chat` copies this file after the three it needs. Never edit it
|
|
57
|
+
-- once applied; change it with a new migration.
|
|
58
|
+
--
|
|
59
|
+
-- Verify: the pgTAP test beside this file (`tests/0001_genesis_support_chat.test.sql`).
|
|
60
|
+
|
|
61
|
+
create schema if not exists private;
|
|
62
|
+
|
|
63
|
+
-- Helpers -----------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
-- Is this person support staff? The app's hook (0006), or nobody when it isn't there yet.
|
|
66
|
+
create or replace function private.genesis_support_chat_is_staff(person uuid)
|
|
67
|
+
returns boolean
|
|
68
|
+
language plpgsql
|
|
69
|
+
stable
|
|
70
|
+
security definer
|
|
71
|
+
set search_path = ''
|
|
72
|
+
as $$
|
|
73
|
+
begin
|
|
74
|
+
if person is null or to_regprocedure('private.genesis_support_staff(uuid)') is null then
|
|
75
|
+
return false;
|
|
76
|
+
end if;
|
|
77
|
+
return coalesce(private.genesis_support_staff(person), false);
|
|
78
|
+
end;
|
|
79
|
+
$$;
|
|
80
|
+
|
|
81
|
+
-- The `support` kind's observer check: staff read (and, by the kind, post in) a report's
|
|
82
|
+
-- conversation without being in it.
|
|
83
|
+
create or replace function private.genesis_support_chat_observer(conversation_id uuid, user_id uuid)
|
|
84
|
+
returns boolean
|
|
85
|
+
language plpgsql
|
|
86
|
+
stable
|
|
87
|
+
security definer
|
|
88
|
+
set search_path = ''
|
|
89
|
+
as $$
|
|
90
|
+
begin
|
|
91
|
+
if not exists (select 1 from genesis_chat.conversations c
|
|
92
|
+
where c.id = genesis_support_chat_observer.conversation_id
|
|
93
|
+
and c.kind = 'support' and c.subject_type = 'support_report') then
|
|
94
|
+
return false;
|
|
95
|
+
end if;
|
|
96
|
+
return private.genesis_support_chat_is_staff(genesis_support_chat_observer.user_id);
|
|
97
|
+
end;
|
|
98
|
+
$$;
|
|
99
|
+
|
|
100
|
+
-- A report's status as a conversation status.
|
|
101
|
+
create or replace function private.genesis_support_chat_status(report_status text)
|
|
102
|
+
returns text
|
|
103
|
+
language sql
|
|
104
|
+
immutable
|
|
105
|
+
set search_path = ''
|
|
106
|
+
as $$
|
|
107
|
+
select case report_status
|
|
108
|
+
when 'open' then 'open'
|
|
109
|
+
when 'submitted' then 'open'
|
|
110
|
+
when 'in_progress' then 'pending'
|
|
111
|
+
when 'addressed' then 'solved'
|
|
112
|
+
when 'wont_fix' then 'closed'
|
|
113
|
+
end
|
|
114
|
+
$$;
|
|
115
|
+
|
|
116
|
+
-- A conversation status as a report status, keeping `current` when it already maps there
|
|
117
|
+
-- (open covers open and submitted).
|
|
118
|
+
create or replace function private.genesis_support_report_status(chat_status text, current_status text)
|
|
119
|
+
returns text
|
|
120
|
+
language sql
|
|
121
|
+
immutable
|
|
122
|
+
set search_path = ''
|
|
123
|
+
as $$
|
|
124
|
+
select case
|
|
125
|
+
when private.genesis_support_chat_status(current_status) is not distinct from chat_status then current_status
|
|
126
|
+
else coalesce(case chat_status
|
|
127
|
+
when 'open' then 'open'
|
|
128
|
+
when 'pending' then 'in_progress'
|
|
129
|
+
when 'solved' then 'addressed'
|
|
130
|
+
when 'closed' then 'wont_fix'
|
|
131
|
+
end, current_status)
|
|
132
|
+
end
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
-- The conversation's title: the report's, or the first line of its message.
|
|
136
|
+
create or replace function private.genesis_support_chat_title(title text, message text)
|
|
137
|
+
returns text
|
|
138
|
+
language sql
|
|
139
|
+
immutable
|
|
140
|
+
set search_path = ''
|
|
141
|
+
as $$
|
|
142
|
+
select left(coalesce(nullif(btrim(title), ''), nullif(btrim(split_part(btrim(message), E'\n', 1)), ''), 'Support'), 120)
|
|
143
|
+
$$;
|
|
144
|
+
|
|
145
|
+
-- A report's conversation, or null.
|
|
146
|
+
create or replace function private.genesis_support_chat_conversation_of(report uuid)
|
|
147
|
+
returns uuid
|
|
148
|
+
language sql
|
|
149
|
+
stable
|
|
150
|
+
security definer
|
|
151
|
+
set search_path = ''
|
|
152
|
+
as $$
|
|
153
|
+
select c.id from genesis_chat.conversations c
|
|
154
|
+
where c.kind = 'support' and c.subject_type = 'support_report' and c.subject_id = report::text
|
|
155
|
+
$$;
|
|
156
|
+
|
|
157
|
+
-- The report a conversation is about, or null (any other conversation).
|
|
158
|
+
create or replace function private.genesis_support_chat_report_of(conversation uuid)
|
|
159
|
+
returns uuid
|
|
160
|
+
language plpgsql
|
|
161
|
+
stable
|
|
162
|
+
security definer
|
|
163
|
+
set search_path = ''
|
|
164
|
+
as $$
|
|
165
|
+
declare
|
|
166
|
+
c genesis_chat.conversations;
|
|
167
|
+
begin
|
|
168
|
+
select * into c from genesis_chat.conversations where id = conversation;
|
|
169
|
+
if c.kind is distinct from 'support' or c.subject_type is distinct from 'support_report' then
|
|
170
|
+
return null;
|
|
171
|
+
end if;
|
|
172
|
+
return (select r.id from public.support_reports r where r.id = c.subject_id::uuid);
|
|
173
|
+
end;
|
|
174
|
+
$$;
|
|
175
|
+
|
|
176
|
+
-- The kind --------------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
-- One member (the person who reported), staff as observers who post, no threads, and only
|
|
179
|
+
-- the database adds members.
|
|
180
|
+
select genesis_chat.register_kind('support', 'Support',
|
|
181
|
+
p_max_members => 2,
|
|
182
|
+
p_membership => 'app',
|
|
183
|
+
p_threads => false,
|
|
184
|
+
p_observer_check => 'private.genesis_support_chat_observer(uuid, uuid)',
|
|
185
|
+
p_observers_post => true);
|
|
186
|
+
|
|
187
|
+
-- The report's conversation, made once, its person its member and its status the report's.
|
|
188
|
+
create or replace function private.genesis_support_chat_ensure(report public.support_reports)
|
|
189
|
+
returns uuid
|
|
190
|
+
language plpgsql
|
|
191
|
+
security definer
|
|
192
|
+
set search_path = ''
|
|
193
|
+
as $$
|
|
194
|
+
declare
|
|
195
|
+
conv uuid;
|
|
196
|
+
begin
|
|
197
|
+
conv := genesis_chat.ensure_conversation('support', 'support_report', report.id::text,
|
|
198
|
+
private.genesis_support_chat_title(report.title, report.message));
|
|
199
|
+
perform genesis_chat.sync_members(conv, array[report.user_id], 'member');
|
|
200
|
+
update genesis_chat.conversations
|
|
201
|
+
set status = private.genesis_support_chat_status(report.status), created_by = coalesce(created_by, report.user_id)
|
|
202
|
+
where id = conv
|
|
203
|
+
and (status is distinct from private.genesis_support_chat_status(report.status) or created_by is null);
|
|
204
|
+
return conv;
|
|
205
|
+
end;
|
|
206
|
+
$$;
|
|
207
|
+
|
|
208
|
+
-- Reports -> conversations --------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
create or replace function private.genesis_support_chat_report_added()
|
|
211
|
+
returns trigger
|
|
212
|
+
language plpgsql
|
|
213
|
+
security definer
|
|
214
|
+
set search_path = ''
|
|
215
|
+
as $$
|
|
216
|
+
begin
|
|
217
|
+
perform private.genesis_support_chat_ensure(new);
|
|
218
|
+
return null;
|
|
219
|
+
end;
|
|
220
|
+
$$;
|
|
221
|
+
|
|
222
|
+
create trigger genesis_support_chat_report_added
|
|
223
|
+
after insert on public.support_reports
|
|
224
|
+
for each row execute function private.genesis_support_chat_report_added();
|
|
225
|
+
|
|
226
|
+
-- Status and title go to the conversation; the person's read time to their place in it.
|
|
227
|
+
create or replace function private.genesis_support_chat_report_changed()
|
|
228
|
+
returns trigger
|
|
229
|
+
language plpgsql
|
|
230
|
+
security definer
|
|
231
|
+
set search_path = ''
|
|
232
|
+
as $$
|
|
233
|
+
declare
|
|
234
|
+
conv uuid := private.genesis_support_chat_conversation_of(new.id);
|
|
235
|
+
latest bigint;
|
|
236
|
+
begin
|
|
237
|
+
if conv is null then
|
|
238
|
+
return null;
|
|
239
|
+
end if;
|
|
240
|
+
if new.status is distinct from old.status or new.title is distinct from old.title or new.message is distinct from old.message then
|
|
241
|
+
update genesis_chat.conversations
|
|
242
|
+
set status = private.genesis_support_chat_status(new.status),
|
|
243
|
+
title = private.genesis_support_chat_title(new.title, new.message)
|
|
244
|
+
where id = conv
|
|
245
|
+
and (status is distinct from private.genesis_support_chat_status(new.status)
|
|
246
|
+
or title is distinct from private.genesis_support_chat_title(new.title, new.message));
|
|
247
|
+
if found then
|
|
248
|
+
perform genesis_chat.broadcast('genesis-chat:conv:' || conv, 'conversation', jsonb_build_object('conversation_id', conv));
|
|
249
|
+
end if;
|
|
250
|
+
end if;
|
|
251
|
+
if new.user_read_at is not null and new.user_read_at is distinct from old.user_read_at then
|
|
252
|
+
select coalesce(max(m.seq), 0) into latest from genesis_chat.messages m
|
|
253
|
+
where m.conversation_id = conv and m.created_at <= new.user_read_at;
|
|
254
|
+
update genesis_chat.members set last_read_seq = latest
|
|
255
|
+
where conversation_id = conv and user_id = new.user_id and last_read_seq < latest;
|
|
256
|
+
end if;
|
|
257
|
+
return null;
|
|
258
|
+
end;
|
|
259
|
+
$$;
|
|
260
|
+
|
|
261
|
+
create trigger genesis_support_chat_report_changed
|
|
262
|
+
after update on public.support_reports
|
|
263
|
+
for each row execute function private.genesis_support_chat_report_changed();
|
|
264
|
+
|
|
265
|
+
create or replace function private.genesis_support_chat_report_deleted()
|
|
266
|
+
returns trigger
|
|
267
|
+
language plpgsql
|
|
268
|
+
security definer
|
|
269
|
+
set search_path = ''
|
|
270
|
+
as $$
|
|
271
|
+
begin
|
|
272
|
+
delete from genesis_chat.conversations c
|
|
273
|
+
where c.kind = 'support' and c.subject_type = 'support_report' and c.subject_id = old.id::text;
|
|
274
|
+
return null;
|
|
275
|
+
end;
|
|
276
|
+
$$;
|
|
277
|
+
|
|
278
|
+
create trigger genesis_support_chat_report_deleted
|
|
279
|
+
after delete on public.support_reports
|
|
280
|
+
for each row execute function private.genesis_support_chat_report_deleted();
|
|
281
|
+
|
|
282
|
+
-- Conversations -> reports ----------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
-- Only staff are assigned a report (the assignee is pushed every message).
|
|
285
|
+
create or replace function private.genesis_support_chat_assignee_check()
|
|
286
|
+
returns trigger
|
|
287
|
+
language plpgsql
|
|
288
|
+
security definer
|
|
289
|
+
set search_path = ''
|
|
290
|
+
as $$
|
|
291
|
+
begin
|
|
292
|
+
if new.assignee_id is not null and not private.genesis_support_chat_is_staff(new.assignee_id) then
|
|
293
|
+
raise exception 'support: only staff are assigned a report' using errcode = '42501';
|
|
294
|
+
end if;
|
|
295
|
+
return new;
|
|
296
|
+
end;
|
|
297
|
+
$$;
|
|
298
|
+
|
|
299
|
+
create trigger genesis_support_chat_assignee_check
|
|
300
|
+
before insert or update of assignee_id on genesis_chat.conversations
|
|
301
|
+
for each row when (new.kind = 'support' and new.assignee_id is not null)
|
|
302
|
+
execute function private.genesis_support_chat_assignee_check();
|
|
303
|
+
|
|
304
|
+
-- A status set on the conversation (genesis_chat.set_status) moves the report's.
|
|
305
|
+
create or replace function private.genesis_support_chat_status_changed()
|
|
306
|
+
returns trigger
|
|
307
|
+
language plpgsql
|
|
308
|
+
security definer
|
|
309
|
+
set search_path = ''
|
|
310
|
+
as $$
|
|
311
|
+
begin
|
|
312
|
+
update public.support_reports r
|
|
313
|
+
set status = private.genesis_support_report_status(new.status, r.status)
|
|
314
|
+
where r.id = new.subject_id::uuid
|
|
315
|
+
and private.genesis_support_chat_status(r.status) is distinct from new.status;
|
|
316
|
+
return null;
|
|
317
|
+
end;
|
|
318
|
+
$$;
|
|
319
|
+
|
|
320
|
+
create trigger genesis_support_chat_status_changed
|
|
321
|
+
after update of status on genesis_chat.conversations
|
|
322
|
+
for each row when (new.kind = 'support' and new.subject_type = 'support_report'
|
|
323
|
+
and new.status is not null and new.status is distinct from old.status)
|
|
324
|
+
execute function private.genesis_support_chat_status_changed();
|
|
325
|
+
|
|
326
|
+
-- A message moves the report on: the writer's side has read it, the other side has something
|
|
327
|
+
-- new. An internal note moves nothing the person sees. For one release, it is also mirrored
|
|
328
|
+
-- into support_replies for older clients (which applies 0006's checks and reply limit).
|
|
329
|
+
create or replace function private.genesis_support_chat_message_posted()
|
|
330
|
+
returns trigger
|
|
331
|
+
language plpgsql
|
|
332
|
+
security definer
|
|
333
|
+
set search_path = ''
|
|
334
|
+
as $$
|
|
335
|
+
declare
|
|
336
|
+
rid uuid := private.genesis_support_chat_report_of(new.conversation_id);
|
|
337
|
+
begin
|
|
338
|
+
if rid is null or new.kind <> 'text' or new.visibility <> 'everyone' then
|
|
339
|
+
return null;
|
|
340
|
+
end if;
|
|
341
|
+
if not (new.meta ? 'support_reply_id') and to_regclass('public.support_replies') is not null then
|
|
342
|
+
insert into public.support_replies (report_id, body, chat_message_id)
|
|
343
|
+
values (rid,
|
|
344
|
+
coalesce(nullif(left(btrim(new.body), 10000), ''), 'Sent an attachment. Update the app to see it.'),
|
|
345
|
+
new.id);
|
|
346
|
+
end if;
|
|
347
|
+
if new.sender_role = 'observer' then
|
|
348
|
+
update public.support_reports
|
|
349
|
+
set last_staff_reply_at = greatest(coalesce(last_staff_reply_at, new.created_at), new.created_at),
|
|
350
|
+
staff_read_at = greatest(coalesce(staff_read_at, new.created_at), new.created_at)
|
|
351
|
+
where id = rid
|
|
352
|
+
and (last_staff_reply_at is null or last_staff_reply_at < new.created_at
|
|
353
|
+
or staff_read_at is null or staff_read_at < new.created_at);
|
|
354
|
+
else
|
|
355
|
+
update public.support_reports
|
|
356
|
+
set last_user_reply_at = greatest(coalesce(last_user_reply_at, new.created_at), new.created_at),
|
|
357
|
+
user_read_at = greatest(coalesce(user_read_at, new.created_at), new.created_at)
|
|
358
|
+
where id = rid
|
|
359
|
+
and (last_user_reply_at is null or last_user_reply_at < new.created_at
|
|
360
|
+
or user_read_at is null or user_read_at < new.created_at);
|
|
361
|
+
end if;
|
|
362
|
+
return null;
|
|
363
|
+
end;
|
|
364
|
+
$$;
|
|
365
|
+
|
|
366
|
+
create trigger genesis_support_chat_message_posted
|
|
367
|
+
after insert on genesis_chat.messages
|
|
368
|
+
for each row execute function private.genesis_support_chat_message_posted();
|
|
369
|
+
|
|
370
|
+
-- The mirror, for one release: an edit or a delete in chat reaches support_replies.
|
|
371
|
+
create or replace function private.genesis_support_chat_message_changed()
|
|
372
|
+
returns trigger
|
|
373
|
+
language plpgsql
|
|
374
|
+
security definer
|
|
375
|
+
set search_path = ''
|
|
376
|
+
as $$
|
|
377
|
+
begin
|
|
378
|
+
if to_regclass('public.support_replies') is null then
|
|
379
|
+
return null;
|
|
380
|
+
end if;
|
|
381
|
+
if new.deleted_at is not null then
|
|
382
|
+
delete from public.support_replies where chat_message_id = new.id;
|
|
383
|
+
elsif btrim(new.body) <> '' then
|
|
384
|
+
update public.support_replies set body = left(btrim(new.body), 10000)
|
|
385
|
+
where chat_message_id = new.id and body is distinct from left(btrim(new.body), 10000);
|
|
386
|
+
end if;
|
|
387
|
+
return null;
|
|
388
|
+
end;
|
|
389
|
+
$$;
|
|
390
|
+
|
|
391
|
+
create trigger genesis_support_chat_message_changed
|
|
392
|
+
after update of body, deleted_at on genesis_chat.messages
|
|
393
|
+
for each row when (new.body is distinct from old.body or new.deleted_at is distinct from old.deleted_at)
|
|
394
|
+
execute function private.genesis_support_chat_message_changed();
|
|
395
|
+
|
|
396
|
+
-- Reading the conversation is reading the report: the person's place moves user_read_at,
|
|
397
|
+
-- a staff member's (an observer row) staff_read_at. Read to the end means read now.
|
|
398
|
+
create or replace function private.genesis_support_chat_read()
|
|
399
|
+
returns trigger
|
|
400
|
+
language plpgsql
|
|
401
|
+
security definer
|
|
402
|
+
set search_path = ''
|
|
403
|
+
as $$
|
|
404
|
+
declare
|
|
405
|
+
report public.support_reports;
|
|
406
|
+
latest bigint;
|
|
407
|
+
read_at timestamptz;
|
|
408
|
+
begin
|
|
409
|
+
if new.last_read_seq <= 0 or (tg_op = 'UPDATE' and new.last_read_seq <= old.last_read_seq) then
|
|
410
|
+
return null;
|
|
411
|
+
end if;
|
|
412
|
+
select * into report from public.support_reports r where r.id = private.genesis_support_chat_report_of(new.conversation_id);
|
|
413
|
+
if report.id is null then
|
|
414
|
+
return null;
|
|
415
|
+
end if;
|
|
416
|
+
select c.last_seq into latest from genesis_chat.conversations c where c.id = new.conversation_id;
|
|
417
|
+
read_at := case when new.last_read_seq >= latest then clock_timestamp()
|
|
418
|
+
else (select m.created_at from genesis_chat.messages m
|
|
419
|
+
where m.conversation_id = new.conversation_id and m.seq = new.last_read_seq) end;
|
|
420
|
+
if read_at is null then
|
|
421
|
+
return null;
|
|
422
|
+
end if;
|
|
423
|
+
if new.user_id = report.user_id and new.role <> 'observer' then
|
|
424
|
+
update public.support_reports set user_read_at = read_at
|
|
425
|
+
where id = report.id and (user_read_at is null or user_read_at < read_at);
|
|
426
|
+
elsif new.role = 'observer' then
|
|
427
|
+
update public.support_reports set staff_read_at = read_at
|
|
428
|
+
where id = report.id and (staff_read_at is null or staff_read_at < read_at);
|
|
429
|
+
end if;
|
|
430
|
+
return null;
|
|
431
|
+
end;
|
|
432
|
+
$$;
|
|
433
|
+
|
|
434
|
+
create trigger genesis_support_chat_read
|
|
435
|
+
after insert or update of last_read_seq on genesis_chat.members
|
|
436
|
+
for each row execute function private.genesis_support_chat_read();
|
|
437
|
+
|
|
438
|
+
-- support_replies -> chat (the mirror, for one release) --------------------------------
|
|
439
|
+
|
|
440
|
+
-- Which chat message a reply is (or came from). Not readable by people.
|
|
441
|
+
alter table public.support_replies
|
|
442
|
+
add column chat_message_id uuid unique references genesis_chat.messages (id) on delete set null;
|
|
443
|
+
|
|
444
|
+
-- A reply written by an older client (0006 has checked who wrote it and counted it) becomes
|
|
445
|
+
-- a message in the report's conversation, as the person or as staff (an observer).
|
|
446
|
+
create or replace function private.genesis_support_chat_reply_added()
|
|
447
|
+
returns trigger
|
|
448
|
+
language plpgsql
|
|
449
|
+
security definer
|
|
450
|
+
set search_path = ''
|
|
451
|
+
as $$
|
|
452
|
+
declare
|
|
453
|
+
report public.support_reports;
|
|
454
|
+
conv uuid;
|
|
455
|
+
msg uuid;
|
|
456
|
+
begin
|
|
457
|
+
select * into report from public.support_reports where id = new.report_id;
|
|
458
|
+
conv := coalesce(private.genesis_support_chat_conversation_of(report.id), private.genesis_support_chat_ensure(report));
|
|
459
|
+
insert into genesis_chat.messages (conversation_id, seq, client_id, sender_id, sender_role, body, created_at, meta)
|
|
460
|
+
values (conv, 0, new.id, new.author_id, case when new.from_staff then 'observer' else 'member' end,
|
|
461
|
+
left(new.body, 10000), new.created_at, jsonb_build_object('support_reply_id', new.id))
|
|
462
|
+
on conflict do nothing
|
|
463
|
+
returning id into msg;
|
|
464
|
+
if msg is not null then
|
|
465
|
+
update public.support_replies set chat_message_id = msg where id = new.id;
|
|
466
|
+
end if;
|
|
467
|
+
return null;
|
|
468
|
+
end;
|
|
469
|
+
$$;
|
|
470
|
+
|
|
471
|
+
create trigger genesis_support_chat_reply_added
|
|
472
|
+
after insert on public.support_replies
|
|
473
|
+
for each row when (new.chat_message_id is null)
|
|
474
|
+
execute function private.genesis_support_chat_reply_added();
|
|
475
|
+
|
|
476
|
+
-- For people ----------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
-- A report's conversation, for its person or staff (null for anyone else, or none). Also
|
|
479
|
+
-- how a client learns this block is installed.
|
|
480
|
+
create or replace function public.support_chat_conversation(report uuid)
|
|
481
|
+
returns uuid
|
|
482
|
+
language sql
|
|
483
|
+
stable
|
|
484
|
+
security invoker
|
|
485
|
+
set search_path = ''
|
|
486
|
+
as $$
|
|
487
|
+
select c.id from genesis_chat.conversations c
|
|
488
|
+
where c.kind = 'support' and c.subject_type = 'support_report' and c.subject_id = report::text
|
|
489
|
+
$$;
|
|
490
|
+
|
|
491
|
+
comment on function public.support_chat_conversation(uuid) is
|
|
492
|
+
'The genesis_chat conversation of a support report, if the caller may read it. Genesis support on chat.';
|
|
493
|
+
|
|
494
|
+
-- Staff: who has a report (a staff member, or null for nobody).
|
|
495
|
+
create or replace function public.support_assign_report(report uuid, assignee uuid default null)
|
|
496
|
+
returns void
|
|
497
|
+
language plpgsql
|
|
498
|
+
security definer
|
|
499
|
+
set search_path = ''
|
|
500
|
+
as $$
|
|
501
|
+
declare
|
|
502
|
+
conv uuid;
|
|
503
|
+
begin
|
|
504
|
+
if not public.support_is_staff() then
|
|
505
|
+
raise exception 'support_assign_report: only staff' using errcode = '42501';
|
|
506
|
+
end if;
|
|
507
|
+
conv := private.genesis_support_chat_conversation_of(report);
|
|
508
|
+
if conv is null then
|
|
509
|
+
raise exception 'support_assign_report: no such report' using errcode = 'P0002';
|
|
510
|
+
end if;
|
|
511
|
+
update genesis_chat.conversations set assignee_id = assignee where id = conv and assignee_id is distinct from assignee;
|
|
512
|
+
perform genesis_chat.broadcast('genesis-chat:conv:' || conv, 'conversation', jsonb_build_object('conversation_id', conv));
|
|
513
|
+
end;
|
|
514
|
+
$$;
|
|
515
|
+
|
|
516
|
+
comment on function public.support_assign_report(uuid, uuid) is
|
|
517
|
+
'Staff set who has a report (a staff member, who is pushed its messages, or null). Genesis support on chat.';
|
|
518
|
+
|
|
519
|
+
-- Existing reports and replies ------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
-- Every report gets its conversation; every reply not yet copied is copied in order, as its
|
|
522
|
+
-- writer, at its time. No push, no live hint, no link preview: history is not news.
|
|
523
|
+
create or replace function private.genesis_support_chat_backfill()
|
|
524
|
+
returns void
|
|
525
|
+
language plpgsql
|
|
526
|
+
security definer
|
|
527
|
+
set search_path = ''
|
|
528
|
+
as $$
|
|
529
|
+
declare
|
|
530
|
+
effects_before bigint;
|
|
531
|
+
report public.support_reports;
|
|
532
|
+
reply public.support_replies;
|
|
533
|
+
conv uuid;
|
|
534
|
+
msg uuid;
|
|
535
|
+
has_0002 boolean := exists (select 1 from pg_trigger where tgrelid = 'genesis_chat.messages'::regclass and tgname = 'messages_effects_0002');
|
|
536
|
+
begin
|
|
537
|
+
select count(*) into effects_before from genesis_chat.effects;
|
|
538
|
+
alter table genesis_chat.messages disable trigger messages_posted;
|
|
539
|
+
alter table genesis_chat.messages disable trigger genesis_support_chat_message_posted;
|
|
540
|
+
if has_0002 then
|
|
541
|
+
alter table genesis_chat.messages disable trigger messages_effects_0002;
|
|
542
|
+
end if;
|
|
543
|
+
alter table genesis_chat.members disable trigger members_changed;
|
|
544
|
+
alter table genesis_chat.members disable trigger genesis_support_chat_read;
|
|
545
|
+
|
|
546
|
+
for report in select * from public.support_reports order by created_at, id loop
|
|
547
|
+
perform private.genesis_support_chat_ensure(report);
|
|
548
|
+
end loop;
|
|
549
|
+
|
|
550
|
+
for reply in select * from public.support_replies p where p.chat_message_id is null order by p.report_id, p.created_at, p.id loop
|
|
551
|
+
conv := private.genesis_support_chat_conversation_of(reply.report_id);
|
|
552
|
+
select m.id into msg from genesis_chat.messages m where m.conversation_id = conv and m.client_id = reply.id;
|
|
553
|
+
if msg is null then
|
|
554
|
+
insert into genesis_chat.messages (conversation_id, seq, client_id, sender_id, sender_role, body, created_at, meta)
|
|
555
|
+
values (conv, 0, reply.id, reply.author_id, case when reply.from_staff then 'observer' else 'member' end,
|
|
556
|
+
left(reply.body, 10000), reply.created_at, jsonb_build_object('support_reply_id', reply.id))
|
|
557
|
+
returning id into msg;
|
|
558
|
+
end if;
|
|
559
|
+
update public.support_replies set chat_message_id = msg where id = reply.id;
|
|
560
|
+
end loop;
|
|
561
|
+
|
|
562
|
+
-- The person's place: everything up to when they last read the report.
|
|
563
|
+
update genesis_chat.members gm
|
|
564
|
+
set last_read_seq = x.latest
|
|
565
|
+
from (select c.id as conversation_id, r.user_id,
|
|
566
|
+
(select coalesce(max(m.seq), 0) from genesis_chat.messages m
|
|
567
|
+
where m.conversation_id = c.id and m.created_at <= r.user_read_at) as latest
|
|
568
|
+
from public.support_reports r
|
|
569
|
+
join genesis_chat.conversations c
|
|
570
|
+
on c.kind = 'support' and c.subject_type = 'support_report' and c.subject_id = r.id::text
|
|
571
|
+
where r.user_read_at is not null) x
|
|
572
|
+
where gm.conversation_id = x.conversation_id and gm.user_id = x.user_id and gm.last_read_seq < x.latest;
|
|
573
|
+
|
|
574
|
+
alter table genesis_chat.messages enable trigger messages_posted;
|
|
575
|
+
alter table genesis_chat.messages enable trigger genesis_support_chat_message_posted;
|
|
576
|
+
if has_0002 then
|
|
577
|
+
alter table genesis_chat.messages enable trigger messages_effects_0002;
|
|
578
|
+
end if;
|
|
579
|
+
alter table genesis_chat.members enable trigger members_changed;
|
|
580
|
+
alter table genesis_chat.members enable trigger genesis_support_chat_read;
|
|
581
|
+
if (select count(*) from genesis_chat.effects) <> effects_before then
|
|
582
|
+
raise exception 'support: copying replies into chat queued side effects';
|
|
583
|
+
end if;
|
|
584
|
+
end;
|
|
585
|
+
$$;
|
|
586
|
+
|
|
587
|
+
comment on function private.genesis_support_chat_backfill() is
|
|
588
|
+
'Gives every support report its chat conversation and copies support_replies into it, adding only what is missing. Genesis support on chat.';
|
|
589
|
+
|
|
590
|
+
select private.genesis_support_chat_backfill();
|
|
591
|
+
|
|
592
|
+
-- Privileges by name ------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
revoke execute on function public.support_chat_conversation(uuid) from public, anon;
|
|
595
|
+
grant execute on function public.support_chat_conversation(uuid) to authenticated, service_role;
|
|
596
|
+
revoke execute on function public.support_assign_report(uuid, uuid) from public, anon;
|
|
597
|
+
grant execute on function public.support_assign_report(uuid, uuid) to authenticated;
|
|
598
|
+
|
|
599
|
+
revoke execute on function private.genesis_support_chat_is_staff(uuid) from public, anon, authenticated;
|
|
600
|
+
revoke execute on function private.genesis_support_chat_observer(uuid, uuid) from public, anon, authenticated;
|
|
601
|
+
revoke execute on function private.genesis_support_chat_status(text) from public, anon, authenticated;
|
|
602
|
+
revoke execute on function private.genesis_support_report_status(text, text) from public, anon, authenticated;
|
|
603
|
+
revoke execute on function private.genesis_support_chat_title(text, text) from public, anon, authenticated;
|
|
604
|
+
revoke execute on function private.genesis_support_chat_conversation_of(uuid) from public, anon, authenticated;
|
|
605
|
+
revoke execute on function private.genesis_support_chat_report_of(uuid) from public, anon, authenticated;
|
|
606
|
+
revoke execute on function private.genesis_support_chat_ensure(public.support_reports) from public, anon, authenticated;
|
|
607
|
+
revoke execute on function private.genesis_support_chat_report_added() from public, anon, authenticated;
|
|
608
|
+
revoke execute on function private.genesis_support_chat_report_changed() from public, anon, authenticated;
|
|
609
|
+
revoke execute on function private.genesis_support_chat_report_deleted() from public, anon, authenticated;
|
|
610
|
+
revoke execute on function private.genesis_support_chat_assignee_check() from public, anon, authenticated;
|
|
611
|
+
revoke execute on function private.genesis_support_chat_status_changed() from public, anon, authenticated;
|
|
612
|
+
revoke execute on function private.genesis_support_chat_message_posted() from public, anon, authenticated;
|
|
613
|
+
revoke execute on function private.genesis_support_chat_message_changed() from public, anon, authenticated;
|
|
614
|
+
revoke execute on function private.genesis_support_chat_read() from public, anon, authenticated;
|
|
615
|
+
revoke execute on function private.genesis_support_chat_reply_added() from public, anon, authenticated;
|
|
616
|
+
revoke execute on function private.genesis_support_chat_backfill() from public, anon, authenticated;
|