Skip to content

Commit b1d1f2d

Browse files
3mcdtefkah
andauthored
feat: coar notify (#1425)
Co-authored-by: Thomas F. K. Jorna <hello@tefkah.com>
1 parent 200d295 commit b1d1f2d

8 files changed

Lines changed: 636 additions & 259 deletions

File tree

core/prisma/seed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import { logger } from "logger"
77
import { isUniqueConstraintError } from "~/kysely/errors"
88
import { env } from "~/lib/env/env"
99
import { seedBlank } from "./seeds/blank"
10-
// import { seedCoarNotify } from "./seeds/coar-notify"
10+
import { seedCoarNotify } from "./seeds/coar-notify"
1111
import { seedLegacy } from "./seeds/legacy"
1212
import { seedStarter } from "./seeds/starter"
1313

1414
const legacyId = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa" as CommunitiesId
1515
const starterId = "bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb" as CommunitiesId
1616
const blankId = "cccccccc-cccc-4ccc-cccc-cccccccccccc" as CommunitiesId
17-
const _coarNotifyId = "dddddddd-dddd-4ddd-dddd-dddddddddddd" as CommunitiesId
17+
const coarNotifyId = "dddddddd-dddd-4ddd-dddd-dddddddddddd" as CommunitiesId
1818

1919
async function main() {
2020
// do not seed arcadia if the minimal seed flag is set
@@ -51,7 +51,7 @@ async function main() {
5151

5252
await seedBlank(blankId)
5353

54-
// await seedCoarNotify(coarNotifyId)
54+
await seedCoarNotify(coarNotifyId)
5555
}
5656
main()
5757
.then(async () => {

core/prisma/seeds/coar-notify.ts

Lines changed: 225 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
2525
Published: "dddddddd-dddd-4ddd-dddd-dddddddddd14" as StagesId,
2626
// Outbound review request stages (for our Submissions)
2727
Submissions: "dddddddd-dddd-4ddd-dddd-dddddddddd17" as StagesId,
28-
ReviewRequested: "dddddddd-dddd-4ddd-dddd-dddddddddd11" as StagesId,
2928
AwaitingResponse: "dddddddd-dddd-4ddd-dddd-dddddddddd18" as StagesId,
3029
}
3130

3231
const WEBHOOK_PATH = "coar-inbox"
3332

3433
// Default remote inbox URL - can be changed in UI for testing
35-
const REMOTE_INBOX_URL = "http://localhost:4000/api/inbox"
34+
const REMOTE_INBOX_URL = "http://localhost:4001/api/inbox"
3635

3736
return seedCommunity(
3837
{
@@ -48,11 +47,13 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
4847
Payload: { schemaName: CoreSchemaType.String },
4948
SourceURL: { schemaName: CoreSchemaType.String },
5049
RelatedPub: { schemaName: CoreSchemaType.String, relation: true },
50+
Author: { schemaName: CoreSchemaType.MemberId },
5151
},
5252
pubTypes: {
5353
Submission: {
5454
Title: { isTitle: true },
5555
Content: { isTitle: false },
56+
Author: { isTitle: false },
5657
},
5758
Notification: {
5859
Title: { isTitle: true },
@@ -80,12 +81,23 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
8081
existing: true,
8182
role: MemberRole.admin,
8283
},
84+
joeAuthor: {
85+
id: "dddddddd-dddd-4ddd-dddd-dddddddddd02" as UsersId,
86+
firstName: "Joe",
87+
lastName: "Author",
88+
email: "joe-author@pubpub.org",
89+
password: "pubpub-joe",
90+
role: MemberRole.contributor,
91+
},
8392
},
8493
pubs: [
8594
{
8695
pubType: "Submission",
8796
stage: "Submissions",
88-
values: { Title: "Sample Submission for Review" },
97+
values: {
98+
Title: "Sample Submission for Review",
99+
Author: "dddddddd-dddd-4ddd-dddd-dddddddddd02",
100+
},
89101
},
90102
],
91103
stages: {
@@ -103,6 +115,17 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
103115
config: { path: WEBHOOK_PATH },
104116
},
105117
],
118+
// Only process incoming Offer notifications (review requests from external services)
119+
condition: {
120+
type: AutomationConditionBlockType.AND,
121+
items: [
122+
{
123+
kind: "condition",
124+
type: "jsonata",
125+
expression: "'Offer' in $.json.type",
126+
},
127+
],
128+
},
106129
actions: [
107130
{
108131
action: Action.createPub,
@@ -211,13 +234,44 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
211234
// Entry point for our own submissions that we want to request reviews for
212235
Submissions: {
213236
id: STAGE_IDS.Submissions,
214-
// No automations - this is just an entry/holding stage
237+
automations: {
238+
// Manual action to request a review from a remote repository
239+
"Request Review": {
240+
icon: {
241+
name: "send",
242+
color: "#f59e0b",
243+
},
244+
triggers: [
245+
{
246+
event: AutomationEvent.manual,
247+
config: {},
248+
},
249+
],
250+
condition: {
251+
type: AutomationConditionBlockType.AND,
252+
items: [
253+
{
254+
kind: "condition",
255+
type: "jsonata",
256+
expression: "$.pub.pubType.name = 'Submission'",
257+
},
258+
],
259+
},
260+
actions: [
261+
{
262+
action: Action.move,
263+
config: { stage: STAGE_IDS.AwaitingResponse },
264+
},
265+
],
266+
},
267+
},
215268
},
216-
// When we request a review from an external service
217-
ReviewRequested: {
218-
id: STAGE_IDS.ReviewRequested,
269+
// Waiting for response from external service after requesting a review
270+
AwaitingResponse: {
271+
id: STAGE_IDS.AwaitingResponse,
219272
automations: {
220-
"Offer Review": {
273+
// Send the Offer when a submission enters this stage
274+
"Send Review Offer": {
221275
icon: {
222276
name: "send",
223277
color: "#f59e0b",
@@ -228,6 +282,16 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
228282
config: {},
229283
},
230284
],
285+
condition: {
286+
type: AutomationConditionBlockType.AND,
287+
items: [
288+
{
289+
kind: "condition",
290+
type: "jsonata",
291+
expression: "$.pub.pubType.name = 'Submission'",
292+
},
293+
],
294+
},
231295
actions: [
232296
{
233297
action: Action.http,
@@ -255,22 +319,54 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
255319
inbox: REMOTE_INBOX_URL,
256320
type: "Service",
257321
},
322+
origin: {
323+
id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}",
324+
inbox: `{{ $.env.PUBPUB_URL }}/api/v0/c/{{ $.community.slug }}/site/webhook/${WEBHOOK_PATH}`,
325+
type: "Service",
326+
},
258327
},
259328
},
260329
},
330+
],
331+
},
332+
// Process incoming responses (Accept/Reject/Announce) from external services
333+
"Process Response": {
334+
icon: {
335+
name: "mail",
336+
color: "#3b82f6",
337+
},
338+
triggers: [
261339
{
262-
action: Action.move,
263-
config: { stage: STAGE_IDS.AwaitingResponse },
340+
event: AutomationEvent.webhook,
341+
config: { path: WEBHOOK_PATH },
342+
},
343+
],
344+
// Only process Accept, Reject, or Announce responses (not Offer)
345+
condition: {
346+
type: AutomationConditionBlockType.AND,
347+
items: [
348+
{
349+
kind: "condition",
350+
type: "jsonata",
351+
expression:
352+
"'Accept' in $.json.type or 'Reject' in $.json.type or 'Announce' in $.json.type",
353+
},
354+
],
355+
},
356+
// Resolver finds the Submission pub that matches the inReplyTo field
357+
// The inReplyTo will be like "urn:uuid:<pub-id>" from our sent Offer
358+
resolver: `{{ $replace($.json.inReplyTo, "http://localhost:3000/c/coar-notify/pub/", "") }} = $.pub.id`,
359+
actions: [
360+
{
361+
action: Action.log,
362+
config: {
363+
text: "Received response: {{ $.json.type }} for pub {{ $.pub.values.title }} ({{ $.pub.id }})",
364+
},
264365
},
265366
],
266367
},
267368
},
268369
},
269-
// Waiting for Accept/Reject response from external service
270-
AwaitingResponse: {
271-
id: STAGE_IDS.AwaitingResponse,
272-
// No automations - we wait for incoming Accept/Reject notifications
273-
},
274370
Published: {
275371
id: STAGE_IDS.Published,
276372
automations: {
@@ -299,7 +395,7 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
299395
"type": ["Announce", "coar-notify:ReviewAction"],
300396
"id": "urn:uuid:" & $.pub.id,
301397
"object": {
302-
"id": $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/" & $.pub.id,
398+
"id": "http://localhost:8080" & $.community.slug & "/reviews/" & $.pub.id,
303399
"type": ["Page", "sorg:Review"],
304400
"as:inReplyTo": $.pub.out.RelatedPub.values.SourceURL
305401
},
@@ -313,6 +409,119 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
313409
},
314410
],
315411
},
412+
"Build Site": {
413+
icon: {
414+
name: "globe",
415+
color: "#6366f1",
416+
},
417+
triggers: [
418+
{
419+
event: AutomationEvent.pubEnteredStage,
420+
config: {},
421+
},
422+
{
423+
event: AutomationEvent.manual,
424+
config: {},
425+
},
426+
],
427+
condition: {
428+
type: AutomationConditionBlockType.AND,
429+
items: [
430+
{
431+
kind: "condition",
432+
type: "jsonata",
433+
expression: "$.pub.pubType.name = 'Review'",
434+
},
435+
],
436+
},
437+
actions: [
438+
{
439+
action: Action.buildSite,
440+
config: {
441+
css: `:root {
442+
--color-bg: #ffffff;
443+
--color-text: #1a1a1a;
444+
--color-muted: #6b7280;
445+
--color-border: #e5e7eb;
446+
--color-accent: #3b82f6;
447+
--font-sans: system-ui, -apple-system, sans-serif;
448+
--font-mono: ui-monospace, monospace;
449+
}
450+
451+
* { box-sizing: border-box; margin: 0; padding: 0; }
452+
453+
body {
454+
font-family: var(--font-sans);
455+
line-height: 1.6;
456+
color: var(--color-text);
457+
background: var(--color-bg);
458+
max-width: 800px;
459+
margin: 0 auto;
460+
padding: 2rem 1rem;
461+
}
462+
463+
h1, h2, h3 { line-height: 1.3; margin-bottom: 0.5em; }
464+
h1 { font-size: 2rem; }
465+
h2 { font-size: 1.5rem; color: var(--color-muted); }
466+
467+
.pub-field { margin-bottom: 1.5rem; }
468+
.pub-field-label {
469+
font-size: 0.75rem;
470+
text-transform: uppercase;
471+
letter-spacing: 0.05em;
472+
color: var(--color-muted);
473+
margin-bottom: 0.25rem;
474+
}
475+
.pub-field-value { font-size: 1rem; }
476+
.pub-field-value:empty::after { content: "—"; color: var(--color-muted); }
477+
478+
a { color: var(--color-accent); }
479+
pre, code { font-family: var(--font-mono); font-size: 0.875rem; }
480+
pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
481+
482+
.review-list { list-style: none; }
483+
.review-list li { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--color-border); }
484+
.review-list li:last-child { border-bottom: none; }`,
485+
subpath: "reviews",
486+
pages: [
487+
{
488+
// Individual review page - one per Review pub
489+
slug: "$.pub.id",
490+
// Use all published pubs
491+
filter: '$.pub.pubType.name = "Review"',
492+
transform: `'<article>' &
493+
'<h1>' & $.pub.title & '</h1>' &
494+
$join(
495+
$map(
496+
$filter($keys($.pub.values), function($v){ $not($contains($v, ":")) }),
497+
function($v){
498+
'<div class="pub-field">' &
499+
'<div class="pub-field-label">' & $v & '</div>' &
500+
'<div class="pub-field-value">' &
501+
$string($lookup($.pub.values, $v)) & '</div>' &
502+
'</div>'
503+
}
504+
),
505+
''
506+
) &
507+
'<p><a href="/">← Back to all reviews</a></p>' &
508+
'</article>'`,
509+
},
510+
{
511+
// Index page - lists all published reviews
512+
slug: '"/"',
513+
filter: '$.pub.pubType.name = "Review"',
514+
transform: `'<h1>Published Reviews</h1>' &
515+
'<ul class="review-list">' &
516+
'<li><a href="/coar-notify/reviews/' & $.pub.id & '">' & $.pub.title & '</a></li>' &
517+
'</ul>'`,
518+
},
519+
],
520+
outputMap: [],
521+
},
522+
},
523+
],
524+
},
316525
},
317526
},
318527
Accepted: {
@@ -492,9 +701,6 @@ export async function seedCoarNotify(communityId?: CommunitiesId) {
492701
},
493702
// Outbound request flow: our submissions requesting external reviews
494703
Submissions: {
495-
to: ["ReviewRequested"],
496-
},
497-
ReviewRequested: {
498704
to: ["AwaitingResponse"],
499705
},
500706
},

mock-notify/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"dev": "next dev -p 4000",
7+
"dev": "next dev -p 4001",
88
"build": "next build",
9-
"start": "next start -p 4000",
9+
"start": "next start -p 4001",
1010
"type-check": "tsc --noEmit",
1111
"type-check:go": "tsgo --noEmit"
1212
},

0 commit comments

Comments
 (0)