@headless-lms/core 0.3.0 → 0.4.0
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/dist/assets/events.d.ts +9 -9
- package/dist/assets/index.d.ts +3 -2
- package/dist/assets/index.js +2 -1
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +4 -3
- package/dist/assets/service.d.ts +0 -1
- package/dist/assets/service.js +0 -4
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +69 -62
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +4 -4
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +3 -17
- package/dist/automations/community-mail.js +1 -21
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +11 -4
- package/dist/automations/index.js +8 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +119 -5
- package/dist/automations/service.d.ts +10 -13
- package/dist/automations/service.js +21 -83
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +130 -80
- package/dist/content/events.js +11 -0
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +25 -6
- package/dist/content/service.d.ts +4 -4
- package/dist/content/service.js +24 -8
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/chats.d.ts +9 -4
- package/dist/discussion/chats.js +80 -47
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +194 -28
- package/dist/discussion/events.js +74 -17
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -2
- package/dist/discussion/index.js +6 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +107 -24
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +18 -8
- package/dist/discussion/posts.js +207 -155
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.d.ts +17 -8
- package/dist/discussion/service.js +105 -41
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +2 -1
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/ports.d.ts +7 -3
- package/dist/entitlements/service.d.ts +5 -2
- package/dist/entitlements/service.js +23 -3
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +5 -4
- package/dist/identity/index.js +3 -2
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +78 -54
- package/dist/integrations/events.d.ts +3 -3
- package/dist/organizations/events.d.ts +32 -32
- package/dist/organizations/index.d.ts +2 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +3 -0
- package/dist/organizations/service.d.ts +2 -0
- package/dist/organizations/service.js +7 -0
- package/dist/organizations/service.js.map +1 -1
- package/dist/progress/events.d.ts +14 -14
- package/dist/reporting/member/index.d.ts +2 -2
- package/dist/reporting/member/model.d.ts +56 -23
- package/dist/reporting/member/ports.d.ts +9 -6
- package/dist/reporting/member/service.d.ts +33 -14
- package/dist/reporting/member/service.js +138 -60
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/errors.d.ts +13 -1
- package/dist/shared/errors.js +18 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +3 -0
- package/dist/shared/id.js +4 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +2 -2
- package/dist/types/email-templates.d.ts +97 -3
- package/dist/types/identity.d.ts +2 -2
- package/dist/types/index.d.ts +7 -6
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- package/dist/types/schemas/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +62 -1
- package/dist/types/schemas/content.js +25 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +85 -12
- package/dist/types/schemas/discussion.js +47 -13
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -5
- package/dist/types/schemas/index.js +6 -5
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/progress.d.ts +3 -3
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -1
package/dist/discussion/posts.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { genId } from "../shared/id.js";
|
|
2
|
-
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
2
|
+
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
|
|
3
3
|
import { noopLogger } from "../shared/logger.js";
|
|
4
4
|
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
5
5
|
import { discussionEvents } from "./events.js";
|
|
6
6
|
import { authorView } from "./authors.js";
|
|
7
|
+
import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
|
|
8
|
+
import { POST_PUBLISH_JOB } from "./jobs.js";
|
|
9
|
+
import { activeSubscription } from "./subscriptions.js";
|
|
7
10
|
import "./ports.js";
|
|
8
11
|
//#region src/discussion/posts.ts
|
|
9
12
|
const PUBLISHED_ONLY = ["published"];
|
|
10
|
-
const DUE_BATCH_SIZE = 100;
|
|
11
13
|
const MODERATOR_VISIBLE = [
|
|
12
14
|
"published",
|
|
13
15
|
"pending",
|
|
14
16
|
"removed"
|
|
15
17
|
];
|
|
18
|
+
const DRAFT = {
|
|
19
|
+
status: "draft",
|
|
20
|
+
scheduledAt: null,
|
|
21
|
+
publishedAt: null,
|
|
22
|
+
removedBy: null
|
|
23
|
+
};
|
|
16
24
|
var PostServiceImpl = class {
|
|
17
25
|
repo;
|
|
18
26
|
access;
|
|
@@ -20,9 +28,12 @@ var PostServiceImpl = class {
|
|
|
20
28
|
authors;
|
|
21
29
|
profiles;
|
|
22
30
|
comments;
|
|
31
|
+
subscriptions;
|
|
32
|
+
mentions;
|
|
23
33
|
images;
|
|
24
34
|
fileUrls;
|
|
25
35
|
uow;
|
|
36
|
+
scheduler;
|
|
26
37
|
logger;
|
|
27
38
|
constructor(params) {
|
|
28
39
|
this.repo = params.repo;
|
|
@@ -31,9 +42,12 @@ var PostServiceImpl = class {
|
|
|
31
42
|
this.authors = params.authors;
|
|
32
43
|
this.profiles = params.profiles;
|
|
33
44
|
this.comments = params.comments;
|
|
45
|
+
this.subscriptions = params.subscriptions;
|
|
46
|
+
this.mentions = params.mentions;
|
|
34
47
|
this.images = params.images;
|
|
35
48
|
this.fileUrls = params.fileUrls;
|
|
36
49
|
this.uow = params.uow;
|
|
50
|
+
this.scheduler = params.scheduler;
|
|
37
51
|
this.logger = params.logger ?? noopLogger;
|
|
38
52
|
}
|
|
39
53
|
async visibleTitles(orgId, actor, postIds) {
|
|
@@ -42,6 +56,9 @@ var PostServiceImpl = class {
|
|
|
42
56
|
const open = new Set((await Promise.all(communityIds.map((communityId) => this.access.openSpaceIds(orgId, actor, communityId)))).flat());
|
|
43
57
|
return Object.fromEntries(found.filter((post) => open.has(post.spaceId) && this.canSee(actor, post)).map((post) => [post.id, post.title]));
|
|
44
58
|
}
|
|
59
|
+
publishedBetween(orgId, communityId, from, to) {
|
|
60
|
+
return this.repo.publishedBetween(orgId, communityId, from, to);
|
|
61
|
+
}
|
|
45
62
|
async create(orgId, actor, input) {
|
|
46
63
|
const space = await this.requireOpen(orgId, actor, input.spaceId, input.communityId);
|
|
47
64
|
await this.requireOwnedImage(orgId, actor.id, input.coverAssetId);
|
|
@@ -66,18 +83,16 @@ var PostServiceImpl = class {
|
|
|
66
83
|
updatedAt: now,
|
|
67
84
|
...landing
|
|
68
85
|
};
|
|
69
|
-
const
|
|
86
|
+
const mentioned = await this.mentionable(orgId, space.id, post.title, post.body);
|
|
87
|
+
const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
|
|
70
88
|
const created = await posts.insert(orgId, post);
|
|
89
|
+
await mentions.sync(orgId, "post", created.id, mentioned, now);
|
|
71
90
|
const events = [discussionEvents.postCreated.make({
|
|
72
91
|
orgId,
|
|
73
92
|
data: created,
|
|
74
93
|
metadata: this.meta(actor, created.status === "pending" ? null : void 0)
|
|
75
94
|
})];
|
|
76
|
-
if (created.status === "published") events.push(
|
|
77
|
-
orgId,
|
|
78
|
-
data: created,
|
|
79
|
-
metadata: this.meta(actor, null)
|
|
80
|
-
}));
|
|
95
|
+
if (created.status === "published") events.push(this.published(orgId, created, this.meta(actor, null), mentioned));
|
|
81
96
|
await outbox.append(events);
|
|
82
97
|
return created;
|
|
83
98
|
});
|
|
@@ -87,6 +102,7 @@ var PostServiceImpl = class {
|
|
|
87
102
|
status: saved.status,
|
|
88
103
|
actorId: actor.id
|
|
89
104
|
});
|
|
105
|
+
if (saved.status === "scheduled") await this.arm(orgId, actor, saved, null);
|
|
90
106
|
return this.viewOf(orgId, actor, saved);
|
|
91
107
|
}
|
|
92
108
|
async update(orgId, postId, actor, patch) {
|
|
@@ -96,13 +112,12 @@ var PostServiceImpl = class {
|
|
|
96
112
|
if (patch.topicId !== void 0) this.assertTopic(space, patch.topicId);
|
|
97
113
|
if (typeof patch.coverAssetId === "string") await this.requireOwnedImage(orgId, actor.id, patch.coverAssetId);
|
|
98
114
|
if (!this.patchChanges(post, patch)) return this.viewOf(orgId, actor, post);
|
|
99
|
-
const
|
|
115
|
+
const retext = patch.title !== void 0 || patch.body !== void 0;
|
|
116
|
+
const candidates = retext ? await this.mentionable(orgId, post.spaceId, patch.title ?? post.title, patch.body ?? post.body) : [];
|
|
117
|
+
const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
|
|
100
118
|
const next = await this.mustUpdate(posts, orgId, postId, patch);
|
|
101
|
-
await
|
|
102
|
-
|
|
103
|
-
data: next,
|
|
104
|
-
metadata: this.meta(actor, next.status === "pending" ? post.status : void 0)
|
|
105
|
-
})]);
|
|
119
|
+
const added = retext ? await mentions.sync(orgId, "post", postId, candidates, /* @__PURE__ */ new Date()) : [];
|
|
120
|
+
await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0), next.status === "published" ? keepAllowed(candidates, added) : [])]);
|
|
106
121
|
return next;
|
|
107
122
|
});
|
|
108
123
|
this.logger.info("post updated", {
|
|
@@ -125,6 +140,7 @@ var PostServiceImpl = class {
|
|
|
125
140
|
status: saved.status,
|
|
126
141
|
actorId: actor.id
|
|
127
142
|
});
|
|
143
|
+
if (post.status === "scheduled") await this.disarm(orgId, postId);
|
|
128
144
|
return this.viewOf(orgId, actor, saved);
|
|
129
145
|
}
|
|
130
146
|
async schedule(orgId, postId, actor, scheduledAt) {
|
|
@@ -133,17 +149,14 @@ var PostServiceImpl = class {
|
|
|
133
149
|
if (post.status === "published" || post.status === "removed" || post.status === "pending") throw new ConflictError("This post cannot be scheduled");
|
|
134
150
|
if (scheduledAt <= /* @__PURE__ */ new Date()) throw new InvalidInputError("A scheduled time must be in the future");
|
|
135
151
|
const updated = await this.uow.run(async ({ posts, outbox }) => {
|
|
136
|
-
const next = await
|
|
152
|
+
const next = await posts.updateIf(orgId, postId, { status: post.status }, {
|
|
137
153
|
status: "scheduled",
|
|
138
154
|
scheduledAt,
|
|
139
155
|
publishedAt: null,
|
|
140
156
|
removedBy: null
|
|
141
157
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
data: next,
|
|
145
|
-
metadata: this.meta(actor, post.status)
|
|
146
|
-
})]);
|
|
158
|
+
if (!next) throw new ConflictError("This post cannot be scheduled");
|
|
159
|
+
await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
|
|
147
160
|
return next;
|
|
148
161
|
});
|
|
149
162
|
this.logger.info("post scheduled", {
|
|
@@ -152,25 +165,132 @@ var PostServiceImpl = class {
|
|
|
152
165
|
scheduledAt: scheduledAt.toISOString(),
|
|
153
166
|
actorId: actor.id
|
|
154
167
|
});
|
|
168
|
+
await this.arm(orgId, actor, updated, post.status === "scheduled" ? post.scheduledAt : null);
|
|
169
|
+
return this.viewOf(orgId, actor, updated);
|
|
170
|
+
}
|
|
171
|
+
async unschedule(orgId, postId, actor) {
|
|
172
|
+
const { post } = await this.loadVisible(orgId, actor, postId);
|
|
173
|
+
this.assertAuthor(actor, post);
|
|
174
|
+
if (post.status !== "scheduled") throw new ConflictError("This post is not scheduled");
|
|
175
|
+
const updated = await this.uow.run(async ({ posts, outbox }) => {
|
|
176
|
+
const next = await posts.updateIf(orgId, postId, { status: "scheduled" }, DRAFT);
|
|
177
|
+
if (!next) throw new ConflictError("This post is not scheduled");
|
|
178
|
+
await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
|
|
179
|
+
return next;
|
|
180
|
+
});
|
|
181
|
+
this.logger.info("post unscheduled", {
|
|
182
|
+
orgId,
|
|
183
|
+
postId,
|
|
184
|
+
actorId: actor.id
|
|
185
|
+
});
|
|
186
|
+
await this.disarm(orgId, postId);
|
|
155
187
|
return this.viewOf(orgId, actor, updated);
|
|
156
188
|
}
|
|
189
|
+
async publishScheduled(orgId, postId, now) {
|
|
190
|
+
const post = await this.repo.find(orgId, postId);
|
|
191
|
+
if (!post || post.status !== "scheduled" || post.scheduledAt === null) {
|
|
192
|
+
this.logger.debug("scheduled post no longer scheduled", {
|
|
193
|
+
orgId,
|
|
194
|
+
postId
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (post.scheduledAt > now) {
|
|
199
|
+
await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, postId, {
|
|
200
|
+
orgId,
|
|
201
|
+
postId
|
|
202
|
+
});
|
|
203
|
+
this.logger.info("scheduled post timer moved", {
|
|
204
|
+
orgId,
|
|
205
|
+
postId,
|
|
206
|
+
scheduledAt: post.scheduledAt.toISOString()
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const space = await this.content.getSpace(orgId, post.spaceId);
|
|
211
|
+
if (!space) {
|
|
212
|
+
this.logger.error("scheduled post has no space", {
|
|
213
|
+
orgId,
|
|
214
|
+
postId,
|
|
215
|
+
spaceId: post.spaceId
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const role = await this.authors.roleOf(orgId, post.orgUserId);
|
|
220
|
+
const refused = !await this.submittable(orgId, post, space, role);
|
|
221
|
+
const patch = refused ? DRAFT : this.submissionOf(space.settings.requireReview, role, now);
|
|
222
|
+
const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
|
|
223
|
+
const next = await posts.updateIf(orgId, postId, {
|
|
224
|
+
status: "scheduled",
|
|
225
|
+
dueBy: now
|
|
226
|
+
}, patch);
|
|
227
|
+
if (!next) return null;
|
|
228
|
+
await outbox.append([await this.landed(mentions, orgId, next, this.meta(null, post.status))]);
|
|
229
|
+
return next;
|
|
230
|
+
});
|
|
231
|
+
if (!saved) {
|
|
232
|
+
this.logger.debug("scheduled post already handled", {
|
|
233
|
+
orgId,
|
|
234
|
+
postId
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.logger.info("scheduled post submitted", {
|
|
239
|
+
orgId,
|
|
240
|
+
postId,
|
|
241
|
+
status: saved.status,
|
|
242
|
+
refused
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async arm(orgId, actor, post, previousScheduledAt) {
|
|
246
|
+
if (post.scheduledAt === null) return;
|
|
247
|
+
try {
|
|
248
|
+
await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, post.id, {
|
|
249
|
+
orgId,
|
|
250
|
+
postId: post.id
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.logger.error("post timer not set", {
|
|
255
|
+
orgId,
|
|
256
|
+
postId: post.id,
|
|
257
|
+
err
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
await this.uow.run(async ({ posts, outbox }) => {
|
|
261
|
+
const reverted = await posts.updateIf(orgId, post.id, {
|
|
262
|
+
status: "scheduled",
|
|
263
|
+
scheduledAt: post.scheduledAt
|
|
264
|
+
}, previousScheduledAt ? { scheduledAt: previousScheduledAt } : DRAFT);
|
|
265
|
+
if (!reverted) return;
|
|
266
|
+
await outbox.append([this.updated(orgId, reverted, this.meta(actor, reverted.status === "draft" ? post.status : void 0))]);
|
|
267
|
+
});
|
|
268
|
+
throw new UnavailableError("The post could not be scheduled; try again", { postId: post.id });
|
|
269
|
+
}
|
|
270
|
+
async disarm(orgId, postId) {
|
|
271
|
+
try {
|
|
272
|
+
await this.scheduler.cancel(POST_PUBLISH_JOB, postId);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
this.logger.warn("post timer not cancelled", {
|
|
275
|
+
orgId,
|
|
276
|
+
postId,
|
|
277
|
+
err
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
157
281
|
async approve(orgId, postId, actor, communityId) {
|
|
158
282
|
const { post } = await this.loadVisible(orgId, actor, postId, communityId);
|
|
159
283
|
if (!this.canModerate(actor)) throw new ForbiddenError("You cannot review this post");
|
|
160
284
|
if (post.status !== "pending") throw new ConflictError("This post is not awaiting review");
|
|
161
285
|
const now = /* @__PURE__ */ new Date();
|
|
162
|
-
const updated = await this.uow.run(async ({ posts, outbox }) => {
|
|
286
|
+
const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
|
|
163
287
|
const next = await this.mustUpdate(posts, orgId, postId, {
|
|
164
288
|
status: "published",
|
|
165
289
|
publishedAt: now,
|
|
166
290
|
scheduledAt: null,
|
|
167
291
|
removedBy: null
|
|
168
292
|
});
|
|
169
|
-
await outbox.append([
|
|
170
|
-
orgId,
|
|
171
|
-
data: next,
|
|
172
|
-
metadata: this.meta(actor, post.status)
|
|
173
|
-
})]);
|
|
293
|
+
await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
|
|
174
294
|
return next;
|
|
175
295
|
});
|
|
176
296
|
this.logger.info("post approved", {
|
|
@@ -218,16 +338,7 @@ var PostServiceImpl = class {
|
|
|
218
338
|
const updated = await this.uow.run(async ({ posts, outbox }) => {
|
|
219
339
|
const next = await this.mustUpdate(posts, orgId, postId, this.submissionOf(space.settings.requireReview, role, now));
|
|
220
340
|
const metadata = this.meta(actor, post.status);
|
|
221
|
-
|
|
222
|
-
orgId,
|
|
223
|
-
data: next,
|
|
224
|
-
metadata
|
|
225
|
-
}) : discussionEvents.postUpdated.make({
|
|
226
|
-
orgId,
|
|
227
|
-
data: next,
|
|
228
|
-
metadata
|
|
229
|
-
});
|
|
230
|
-
await outbox.append([event]);
|
|
341
|
+
await outbox.append([next.status === "published" ? this.published(orgId, next, metadata, []) : this.updated(orgId, next, metadata)]);
|
|
231
342
|
return next;
|
|
232
343
|
});
|
|
233
344
|
this.logger.info("post restored", {
|
|
@@ -256,7 +367,20 @@ var PostServiceImpl = class {
|
|
|
256
367
|
async setReaction(orgId, postId, actor, emoji) {
|
|
257
368
|
const { post, space } = await this.loadVisible(orgId, actor, postId);
|
|
258
369
|
if (!(post.reactionsEnabled ?? space.settings.reactions)) throw new ForbiddenError("Reactions are off on this post");
|
|
259
|
-
await this.
|
|
370
|
+
await this.uow.run(async ({ posts, outbox }) => {
|
|
371
|
+
if (!await posts.setReaction(orgId, postId, actor.id, emoji) || emoji === null) return;
|
|
372
|
+
await outbox.append([discussionEvents.postReactionAdded.make({
|
|
373
|
+
orgId,
|
|
374
|
+
data: {
|
|
375
|
+
postId,
|
|
376
|
+
spaceId: post.spaceId,
|
|
377
|
+
communityId: post.communityId,
|
|
378
|
+
orgUserId: actor.id,
|
|
379
|
+
reaction: emoji
|
|
380
|
+
},
|
|
381
|
+
metadata: this.meta(actor)
|
|
382
|
+
})]);
|
|
383
|
+
});
|
|
260
384
|
this.logger.debug("post reaction set", {
|
|
261
385
|
orgId,
|
|
262
386
|
postId,
|
|
@@ -264,24 +388,6 @@ var PostServiceImpl = class {
|
|
|
264
388
|
});
|
|
265
389
|
return (await this.repo.reactionsOf(orgId, [postId], actor.id))[postId] ?? { reactions: {} };
|
|
266
390
|
}
|
|
267
|
-
async follow(orgId, postId, actor) {
|
|
268
|
-
await this.loadVisible(orgId, actor, postId);
|
|
269
|
-
await this.repo.follow(orgId, postId, actor.id);
|
|
270
|
-
this.logger.debug("post followed", {
|
|
271
|
-
orgId,
|
|
272
|
-
postId,
|
|
273
|
-
orgUserId: actor.id
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
async unfollow(orgId, postId, actor) {
|
|
277
|
-
await this.loadVisible(orgId, actor, postId);
|
|
278
|
-
await this.repo.unfollow(orgId, postId, actor.id);
|
|
279
|
-
this.logger.debug("post unfollowed", {
|
|
280
|
-
orgId,
|
|
281
|
-
postId,
|
|
282
|
-
orgUserId: actor.id
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
391
|
async bookmark(orgId, postId, actor) {
|
|
286
392
|
await this.loadVisible(orgId, actor, postId);
|
|
287
393
|
await this.repo.bookmark(orgId, postId, actor.id);
|
|
@@ -452,82 +558,6 @@ var PostServiceImpl = class {
|
|
|
452
558
|
find(orgId, postId) {
|
|
453
559
|
return this.repo.find(orgId, postId);
|
|
454
560
|
}
|
|
455
|
-
followerIds(orgId, postId) {
|
|
456
|
-
return this.repo.followerIds(orgId, postId);
|
|
457
|
-
}
|
|
458
|
-
hasDue(orgId, now) {
|
|
459
|
-
return this.repo.hasDue(orgId, now);
|
|
460
|
-
}
|
|
461
|
-
async publishDue(orgId, now) {
|
|
462
|
-
let afterId;
|
|
463
|
-
let count = 0;
|
|
464
|
-
let refused = 0;
|
|
465
|
-
for (;;) {
|
|
466
|
-
const due = await this.repo.listDue(orgId, now, DUE_BATCH_SIZE, afterId);
|
|
467
|
-
if (due.length === 0) break;
|
|
468
|
-
const batch = await this.submitDue(orgId, now, due);
|
|
469
|
-
count += batch.count;
|
|
470
|
-
refused += batch.refused;
|
|
471
|
-
if (due.length < DUE_BATCH_SIZE) break;
|
|
472
|
-
afterId = due[due.length - 1].id;
|
|
473
|
-
}
|
|
474
|
-
if (count > 0) this.logger.info("due posts submitted", {
|
|
475
|
-
orgId,
|
|
476
|
-
count,
|
|
477
|
-
refused
|
|
478
|
-
});
|
|
479
|
-
else this.logger.debug("no posts due", { orgId });
|
|
480
|
-
}
|
|
481
|
-
async submitDue(orgId, now, due) {
|
|
482
|
-
const spaces = /* @__PURE__ */ new Map();
|
|
483
|
-
const roles = /* @__PURE__ */ new Map();
|
|
484
|
-
const refused = /* @__PURE__ */ new Set();
|
|
485
|
-
for (const post of due) {
|
|
486
|
-
if (!spaces.has(post.spaceId)) spaces.set(post.spaceId, await this.content.getSpace(orgId, post.spaceId));
|
|
487
|
-
if (!roles.has(post.orgUserId)) roles.set(post.orgUserId, await this.authors.roleOf(orgId, post.orgUserId));
|
|
488
|
-
const space = spaces.get(post.spaceId);
|
|
489
|
-
const role = roles.get(post.orgUserId) ?? null;
|
|
490
|
-
if (space && !await this.submittable(orgId, post, space, role)) refused.add(post.id);
|
|
491
|
-
}
|
|
492
|
-
return {
|
|
493
|
-
count: await this.uow.run(async ({ posts, outbox }) => {
|
|
494
|
-
const events = [];
|
|
495
|
-
for (const post of due) {
|
|
496
|
-
const current = await posts.find(orgId, post.id);
|
|
497
|
-
if (!current || current.status !== "scheduled" || current.scheduledAt === null || current.scheduledAt > now) continue;
|
|
498
|
-
const space = spaces.get(current.spaceId);
|
|
499
|
-
if (!space) {
|
|
500
|
-
this.logger.error("scheduled post has no space", {
|
|
501
|
-
orgId,
|
|
502
|
-
postId: current.id,
|
|
503
|
-
spaceId: current.spaceId
|
|
504
|
-
});
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const next = await posts.update(orgId, current.id, refused.has(current.id) ? {
|
|
508
|
-
status: "draft",
|
|
509
|
-
scheduledAt: null,
|
|
510
|
-
publishedAt: null,
|
|
511
|
-
removedBy: null
|
|
512
|
-
} : this.submissionOf(space.settings.requireReview, roles.get(current.orgUserId) ?? null, now));
|
|
513
|
-
if (!next) continue;
|
|
514
|
-
const metadata = this.meta(null, current.status);
|
|
515
|
-
events.push(next.status === "published" ? discussionEvents.postPublished.make({
|
|
516
|
-
orgId,
|
|
517
|
-
data: next,
|
|
518
|
-
metadata
|
|
519
|
-
}) : discussionEvents.postUpdated.make({
|
|
520
|
-
orgId,
|
|
521
|
-
data: next,
|
|
522
|
-
metadata
|
|
523
|
-
}));
|
|
524
|
-
}
|
|
525
|
-
if (events.length > 0) await outbox.append(events);
|
|
526
|
-
return events.length;
|
|
527
|
-
}),
|
|
528
|
-
refused: refused.size
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
561
|
async submittable(orgId, post, space, role) {
|
|
532
562
|
if (role === null || !this.topicFits(space, post.topicId)) return false;
|
|
533
563
|
return this.access.canOpen(orgId, {
|
|
@@ -549,14 +579,16 @@ var PostServiceImpl = class {
|
|
|
549
579
|
async viewsOf(orgId, actor, posts) {
|
|
550
580
|
if (posts.length === 0) return [];
|
|
551
581
|
const postIds = posts.map((post) => post.id);
|
|
552
|
-
const
|
|
582
|
+
const now = /* @__PURE__ */ new Date();
|
|
583
|
+
const [records, reactions, commentCounts, subscribed, bookmarked, mentioned] = await Promise.all([
|
|
553
584
|
this.profiles.authorsOf(orgId, [...new Set(posts.map((post) => post.orgUserId))]),
|
|
554
585
|
this.repo.reactionsOf(orgId, postIds, actor.id),
|
|
555
586
|
this.comments.publishedCommentCounts(orgId, postIds),
|
|
556
|
-
this.
|
|
557
|
-
this.repo.bookmarkedAmong(orgId, postIds, actor.id)
|
|
587
|
+
this.subscriptions.forViewer(orgId, actor.id, "post", postIds),
|
|
588
|
+
this.repo.bookmarkedAmong(orgId, postIds, actor.id),
|
|
589
|
+
mentionViews(this.mentions, this.profiles, orgId, "post", postIds)
|
|
558
590
|
]);
|
|
559
|
-
const
|
|
591
|
+
const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
|
|
560
592
|
const saved = new Set(bookmarked);
|
|
561
593
|
return posts.map((post) => {
|
|
562
594
|
const record = records[post.orgUserId];
|
|
@@ -569,25 +601,17 @@ var PostServiceImpl = class {
|
|
|
569
601
|
reactions: counts?.reactions ?? {},
|
|
570
602
|
viewerReaction: counts?.viewerReaction ?? null,
|
|
571
603
|
commentCount: commentCounts[post.id] ?? 0,
|
|
572
|
-
|
|
573
|
-
bookmarked: saved.has(post.id)
|
|
604
|
+
subscription: activeSubscription(subscriptionOf.get(post.id), now),
|
|
605
|
+
bookmarked: saved.has(post.id),
|
|
606
|
+
mentions: mentioned[post.id] ?? []
|
|
574
607
|
};
|
|
575
608
|
});
|
|
576
609
|
}
|
|
577
610
|
async commitSubmission(orgId, post, space, role, actor, now) {
|
|
578
|
-
return this.uow.run(async ({ posts, outbox }) => {
|
|
579
|
-
const next = await
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
orgId,
|
|
583
|
-
data: next,
|
|
584
|
-
metadata
|
|
585
|
-
}) : discussionEvents.postUpdated.make({
|
|
586
|
-
orgId,
|
|
587
|
-
data: next,
|
|
588
|
-
metadata
|
|
589
|
-
});
|
|
590
|
-
await outbox.append([event]);
|
|
611
|
+
return this.uow.run(async ({ posts, mentions, outbox }) => {
|
|
612
|
+
const next = await posts.updateIf(orgId, post.id, { status: post.status }, this.submissionOf(space.settings.requireReview, role, now));
|
|
613
|
+
if (!next) throw new ConflictError("This post cannot be submitted");
|
|
614
|
+
await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
|
|
591
615
|
return next;
|
|
592
616
|
});
|
|
593
617
|
}
|
|
@@ -598,11 +622,7 @@ var PostServiceImpl = class {
|
|
|
598
622
|
if (unchanged(post)) return this.viewOf(orgId, actor, post);
|
|
599
623
|
const updated = await this.uow.run(async ({ posts, outbox }) => {
|
|
600
624
|
const next = await this.mustUpdate(posts, orgId, postId, patch);
|
|
601
|
-
await outbox.append([
|
|
602
|
-
orgId,
|
|
603
|
-
data: next,
|
|
604
|
-
metadata: this.meta(actor, next.status === "pending" ? post.status : void 0)
|
|
605
|
-
})]);
|
|
625
|
+
await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0))]);
|
|
606
626
|
return next;
|
|
607
627
|
});
|
|
608
628
|
this.logger.info(log, {
|
|
@@ -710,6 +730,38 @@ var PostServiceImpl = class {
|
|
|
710
730
|
return post[key] !== value;
|
|
711
731
|
});
|
|
712
732
|
}
|
|
733
|
+
async mentionable(orgId, spaceId, title, body) {
|
|
734
|
+
return this.openersOf(orgId, spaceId, mentionTokens(title, body));
|
|
735
|
+
}
|
|
736
|
+
async openersOf(orgId, spaceId, orgUserIds) {
|
|
737
|
+
if (orgUserIds.length === 0) return [];
|
|
738
|
+
return keepAllowed(orgUserIds, await this.access.spaceOpenersAmong(orgId, spaceId, orgUserIds));
|
|
739
|
+
}
|
|
740
|
+
async landed(mentions, orgId, post, metadata) {
|
|
741
|
+
if (post.status !== "published") return this.updated(orgId, post, metadata);
|
|
742
|
+
const mentioned = await this.openersOf(orgId, post.spaceId, await mentions.ofItem(orgId, "post", post.id));
|
|
743
|
+
return this.published(orgId, post, metadata, mentioned);
|
|
744
|
+
}
|
|
745
|
+
published(orgId, post, metadata, mentionedOrgUserIds) {
|
|
746
|
+
return discussionEvents.postPublished.make({
|
|
747
|
+
orgId,
|
|
748
|
+
data: {
|
|
749
|
+
...post,
|
|
750
|
+
mentionedOrgUserIds
|
|
751
|
+
},
|
|
752
|
+
metadata
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
updated(orgId, post, metadata, mentionedOrgUserIds = []) {
|
|
756
|
+
return discussionEvents.postUpdated.make({
|
|
757
|
+
orgId,
|
|
758
|
+
data: {
|
|
759
|
+
...post,
|
|
760
|
+
mentionedOrgUserIds
|
|
761
|
+
},
|
|
762
|
+
metadata
|
|
763
|
+
});
|
|
764
|
+
}
|
|
713
765
|
meta(actor, previousStatus) {
|
|
714
766
|
return {
|
|
715
767
|
...actor ? { actorOrgUserId: actor.id } : {},
|