@marktiderman/genesis-support 0.2.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.
@@ -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;