@rpcbase/server 0.594.0 → 0.595.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/convertHeifToWebp-BT1n72DQ.js +148 -0
- package/dist/convertHeifToWebp-BT1n72DQ.js.map +1 -0
- package/dist/email-Co1GNjlT.js +7204 -0
- package/dist/email-Co1GNjlT.js.map +1 -0
- package/dist/handler-BqlcQ9sE.js +245 -0
- package/dist/handler-BqlcQ9sE.js.map +1 -0
- package/dist/handler-C92_gJkX.js +743 -0
- package/dist/handler-C92_gJkX.js.map +1 -0
- package/dist/handler-CapJTGzc.js +795 -0
- package/dist/handler-CapJTGzc.js.map +1 -0
- package/dist/handler-Fk-5fOSm.js +119 -0
- package/dist/handler-Fk-5fOSm.js.map +1 -0
- package/dist/htmlTemplate.d.ts +16 -0
- package/dist/htmlTemplate.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5140 -5286
- package/dist/index.js.map +1 -1
- package/dist/notifications.js +273 -382
- package/dist/notifications.js.map +1 -1
- package/dist/postProcessors-iffHvje4.js +89 -0
- package/dist/postProcessors-iffHvje4.js.map +1 -0
- package/dist/queryWindow-DP8zD0lb.js +555 -0
- package/dist/queryWindow-DP8zD0lb.js.map +1 -0
- package/dist/render_resend-B1SSQ4f7.js +7 -0
- package/dist/render_resend-B1SSQ4f7.js.map +1 -0
- package/dist/rts/index.js +990 -1207
- package/dist/rts/index.js.map +1 -1
- package/dist/schemas-B2fUvgYo.js +4151 -0
- package/dist/schemas-B2fUvgYo.js.map +1 -0
- package/dist/shared-DfrVDSp0.js +79 -0
- package/dist/shared-DfrVDSp0.js.map +1 -0
- package/dist/ssrMiddleware.d.ts +7 -3
- package/dist/ssrMiddleware.d.ts.map +1 -1
- package/dist/uploads/worker.js +139 -168
- package/dist/uploads/worker.js.map +1 -1
- package/dist/uploads.js +12 -22
- package/dist/uploads.js.map +1 -1
- package/package.json +1 -1
- package/dist/convertHeifToWebp-C-DGXZ2k.js +0 -169
- package/dist/convertHeifToWebp-C-DGXZ2k.js.map +0 -1
- package/dist/email-BCf24GmK.js +0 -8071
- package/dist/email-BCf24GmK.js.map +0 -1
- package/dist/handler-BqoKvylN.js +0 -147
- package/dist/handler-BqoKvylN.js.map +0 -1
- package/dist/handler-CUOJ51-w.js +0 -903
- package/dist/handler-CUOJ51-w.js.map +0 -1
- package/dist/handler-D-XdgeG_.js +0 -986
- package/dist/handler-D-XdgeG_.js.map +0 -1
- package/dist/handler-F0gFTzvh.js +0 -275
- package/dist/handler-F0gFTzvh.js.map +0 -1
- package/dist/postProcessors-D27fGZP0.js +0 -107
- package/dist/postProcessors-D27fGZP0.js.map +0 -1
- package/dist/queryWindow-Cdr7K-S1.js +0 -714
- package/dist/queryWindow-Cdr7K-S1.js.map +0 -1
- package/dist/render_resend_false-MiC__Smr.js +0 -6
- package/dist/render_resend_false-MiC__Smr.js.map +0 -1
- package/dist/schemas-Drf83ni9.js +0 -4517
- package/dist/schemas-Drf83ni9.js.map +0 -1
- package/dist/shared-CJrm9Wjp.js +0 -104
- package/dist/shared-CJrm9Wjp.js.map +0 -1
package/dist/notifications.js
CHANGED
|
@@ -1,400 +1,291 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as sendEmail } from "./email-Co1GNjlT.js";
|
|
2
2
|
import { models } from "@rpcbase/db";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return acc;
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/notifications/routes.ts
|
|
5
|
+
var routes = Object.entries({ .../* #__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-C92_gJkX.js") }) }).reduce((acc, [path, mod]) => {
|
|
6
|
+
acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
|
|
7
|
+
return acc;
|
|
9
8
|
}, {});
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
const normalizeOptionalString = (value) => {
|
|
18
|
-
const normalized = typeof value === "string" ? value.trim() : "";
|
|
19
|
-
return normalized || void 0;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/notifications/createNotification.ts
|
|
11
|
+
var normalizeRequiredString = (value, name) => {
|
|
12
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
13
|
+
if (!normalized) throw new Error(`${name} is required`);
|
|
14
|
+
return normalized;
|
|
20
15
|
};
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
var normalizeOptionalString = (value) => {
|
|
17
|
+
return (typeof value === "string" ? value.trim() : "") || void 0;
|
|
23
18
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (typeof value === "string") {
|
|
27
|
-
const trimmed = value.trim();
|
|
28
|
-
return trimmed || void 0;
|
|
29
|
-
}
|
|
30
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
31
|
-
if (typeof value === "boolean") return String(value);
|
|
32
|
-
if (typeof value === "bigint") return value.toString();
|
|
33
|
-
if (value instanceof Date && Number.isFinite(value.getTime())) return value.toISOString();
|
|
34
|
-
return void 0;
|
|
19
|
+
var normalizeUrlMode = (value) => {
|
|
20
|
+
return value === "merge_current_search" ? "merge_current_search" : "navigate";
|
|
35
21
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} catch {
|
|
44
|
-
return normalizedUrl.startsWith("?") ? normalizedUrl : void 0;
|
|
45
|
-
}
|
|
22
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
var toNotificationDataString = (value) => {
|
|
24
|
+
if (typeof value === "string") return value.trim() || void 0;
|
|
25
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
26
|
+
if (typeof value === "boolean") return String(value);
|
|
27
|
+
if (typeof value === "bigint") return value.toString();
|
|
28
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) return value.toISOString();
|
|
46
29
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (input.link) {
|
|
59
|
-
data.link = input.link;
|
|
60
|
-
if (input.urlMode !== "navigate") {
|
|
61
|
-
data.linkMode = input.urlMode;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return Object.keys(data).length > 0 ? data : void 0;
|
|
30
|
+
var normalizeNotificationActionLink = (url, urlMode) => {
|
|
31
|
+
const normalizedUrl = normalizeOptionalString(url);
|
|
32
|
+
if (!normalizedUrl) return void 0;
|
|
33
|
+
if (urlMode === "navigate") return normalizedUrl;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(normalizedUrl, "https://rpcbase.local/");
|
|
36
|
+
return `${parsed.search}${parsed.hash}` || void 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return normalizedUrl.startsWith("?") ? normalizedUrl : void 0;
|
|
39
|
+
}
|
|
65
40
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
};
|
|
41
|
+
var buildNotificationData = (input) => {
|
|
42
|
+
const data = {};
|
|
43
|
+
for (const [key, value] of Object.entries(input.metadata ?? {})) {
|
|
44
|
+
const normalized = toNotificationDataString(value);
|
|
45
|
+
if (normalized) data[key] = normalized;
|
|
46
|
+
}
|
|
47
|
+
if (input.data) Object.assign(data, input.data);
|
|
48
|
+
if (input.link) {
|
|
49
|
+
data.link = input.link;
|
|
50
|
+
if (input.urlMode !== "navigate") data.linkMode = input.urlMode;
|
|
51
|
+
}
|
|
52
|
+
return Object.keys(data).length > 0 ? data : void 0;
|
|
80
53
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
const platform = buildNotificationPlatform(input.platform, link);
|
|
97
|
-
return {
|
|
98
|
-
userId,
|
|
99
|
-
...topic ? {
|
|
100
|
-
topic
|
|
101
|
-
} : {},
|
|
102
|
-
...tag ? {
|
|
103
|
-
tag
|
|
104
|
-
} : {},
|
|
105
|
-
deliveryId: randomUUID(),
|
|
106
|
-
title,
|
|
107
|
-
...body ? {
|
|
108
|
-
body
|
|
109
|
-
} : {},
|
|
110
|
-
...image ? {
|
|
111
|
-
image
|
|
112
|
-
} : {},
|
|
113
|
-
...data ? {
|
|
114
|
-
data
|
|
115
|
-
} : {},
|
|
116
|
-
...platform ? {
|
|
117
|
-
platform
|
|
118
|
-
} : {},
|
|
119
|
-
drawerActive: true,
|
|
120
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
121
|
-
};
|
|
54
|
+
var buildNotificationPlatform = (platform, link) => {
|
|
55
|
+
if (!link) return platform;
|
|
56
|
+
const webpush = isRecord(platform?.webpush) ? platform.webpush : {};
|
|
57
|
+
const fcmOptions = isRecord(webpush.fcmOptions) ? webpush.fcmOptions : {};
|
|
58
|
+
return {
|
|
59
|
+
...platform,
|
|
60
|
+
webpush: {
|
|
61
|
+
...webpush,
|
|
62
|
+
fcmOptions: {
|
|
63
|
+
...fcmOptions,
|
|
64
|
+
link
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
122
68
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
69
|
+
var createNotificationDocument = (input) => {
|
|
70
|
+
const userId = normalizeRequiredString(input.userId, "createNotification: userId");
|
|
71
|
+
const title = normalizeRequiredString(input.title, "createNotification: title");
|
|
72
|
+
const topic = normalizeOptionalString(input.topic);
|
|
73
|
+
const tag = normalizeOptionalString(input.tag);
|
|
74
|
+
const body = normalizeOptionalString(input.body);
|
|
75
|
+
const image = normalizeOptionalString(input.image);
|
|
76
|
+
const urlMode = normalizeUrlMode(input.urlMode);
|
|
77
|
+
const link = normalizeNotificationActionLink(input.url, urlMode);
|
|
78
|
+
const data = buildNotificationData({
|
|
79
|
+
data: input.data,
|
|
80
|
+
metadata: input.metadata,
|
|
81
|
+
link,
|
|
82
|
+
urlMode
|
|
83
|
+
});
|
|
84
|
+
const platform = buildNotificationPlatform(input.platform, link);
|
|
85
|
+
return {
|
|
86
|
+
userId,
|
|
87
|
+
...topic ? { topic } : {},
|
|
88
|
+
...tag ? { tag } : {},
|
|
89
|
+
deliveryId: randomUUID(),
|
|
90
|
+
title,
|
|
91
|
+
...body ? { body } : {},
|
|
92
|
+
...image ? { image } : {},
|
|
93
|
+
...data ? { data } : {},
|
|
94
|
+
...platform ? { platform } : {},
|
|
95
|
+
drawerActive: true,
|
|
96
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
97
|
+
};
|
|
130
98
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
const modelCtx = {
|
|
136
|
-
req: ctx.req,
|
|
137
|
-
ability
|
|
138
|
-
};
|
|
139
|
-
const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
|
|
140
|
-
const notification = createNotificationDocument(input);
|
|
141
|
-
const delivery = await NotificationDeliveryModel.create(notification);
|
|
142
|
-
const doc = notification.tag ? await NotificationModel.findOneAndReplace({
|
|
143
|
-
userId: notification.userId,
|
|
144
|
-
tag: notification.tag,
|
|
145
|
-
archivedAt: {
|
|
146
|
-
$exists: false
|
|
147
|
-
}
|
|
148
|
-
}, notification, {
|
|
149
|
-
new: true,
|
|
150
|
-
upsert: true,
|
|
151
|
-
sort: {
|
|
152
|
-
drawerActive: -1,
|
|
153
|
-
createdAt: -1
|
|
154
|
-
}
|
|
155
|
-
}) : await NotificationModel.create(notification);
|
|
156
|
-
if (!doc) {
|
|
157
|
-
throw new Error("createNotification: failed to persist notification");
|
|
158
|
-
}
|
|
159
|
-
await NotificationDeliveryModel.updateOne({
|
|
160
|
-
_id: delivery._id
|
|
161
|
-
}, {
|
|
162
|
-
$set: {
|
|
163
|
-
notificationId: doc._id.toString()
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
return {
|
|
167
|
-
id: doc._id.toString()
|
|
168
|
-
};
|
|
99
|
+
var assertCreateNotificationsForUserInput = (input) => {
|
|
100
|
+
if (typeof input.userId !== "string" || input.userId.trim().length === 0) throw new Error("createNotificationsForUser: userId is required");
|
|
101
|
+
if (!Array.isArray(input.notifications)) throw new Error("createNotificationsForUser: notifications must be an array");
|
|
169
102
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
tag: document.tag,
|
|
195
|
-
archivedAt: {
|
|
196
|
-
$exists: false
|
|
197
|
-
}
|
|
198
|
-
}, document, {
|
|
199
|
-
new: true,
|
|
200
|
-
upsert: true,
|
|
201
|
-
sort: {
|
|
202
|
-
drawerActive: -1,
|
|
203
|
-
createdAt: -1
|
|
204
|
-
}
|
|
205
|
-
}) : await NotificationModel.create(document);
|
|
206
|
-
if (!doc) {
|
|
207
|
-
throw new Error("createNotificationsForUser: failed to persist notification");
|
|
208
|
-
}
|
|
209
|
-
await NotificationDeliveryModel.updateOne({
|
|
210
|
-
_id: delivery._id
|
|
211
|
-
}, {
|
|
212
|
-
$set: {
|
|
213
|
-
notificationId: doc._id.toString()
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
ids.push(doc._id.toString());
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
ids
|
|
220
|
-
};
|
|
103
|
+
var createNotification = async (ctx, input, ability) => {
|
|
104
|
+
if (!ability.can("create", "RBNotification")) throw new Error("createNotification: forbidden");
|
|
105
|
+
const modelCtx = {
|
|
106
|
+
req: ctx.req,
|
|
107
|
+
ability
|
|
108
|
+
};
|
|
109
|
+
const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
|
|
110
|
+
const notification = createNotificationDocument(input);
|
|
111
|
+
const delivery = await NotificationDeliveryModel.create(notification);
|
|
112
|
+
const doc = notification.tag ? await NotificationModel.findOneAndReplace({
|
|
113
|
+
userId: notification.userId,
|
|
114
|
+
tag: notification.tag,
|
|
115
|
+
archivedAt: { $exists: false }
|
|
116
|
+
}, notification, {
|
|
117
|
+
new: true,
|
|
118
|
+
upsert: true,
|
|
119
|
+
sort: {
|
|
120
|
+
drawerActive: -1,
|
|
121
|
+
createdAt: -1
|
|
122
|
+
}
|
|
123
|
+
}) : await NotificationModel.create(notification);
|
|
124
|
+
if (!doc) throw new Error("createNotification: failed to persist notification");
|
|
125
|
+
await NotificationDeliveryModel.updateOne({ _id: delivery._id }, { $set: { notificationId: doc._id.toString() } });
|
|
126
|
+
return { id: doc._id.toString() };
|
|
221
127
|
};
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
128
|
+
var createNotificationsForUser = async (ctx, input, ability) => {
|
|
129
|
+
assertCreateNotificationsForUserInput(input);
|
|
130
|
+
if (input.notifications.length === 0) return { ids: [] };
|
|
131
|
+
if (!ability.can("create", "RBNotification")) throw new Error("createNotificationsForUser: forbidden");
|
|
132
|
+
const modelCtx = {
|
|
133
|
+
req: ctx.req,
|
|
134
|
+
ability
|
|
135
|
+
};
|
|
136
|
+
const [NotificationModel, NotificationDeliveryModel] = await Promise.all([models.getUnsafe("RBNotification", modelCtx), models.getUnsafe("RBNotificationDelivery", modelCtx)]);
|
|
137
|
+
const ids = [];
|
|
138
|
+
for (const notification of input.notifications) {
|
|
139
|
+
const document = createNotificationDocument({
|
|
140
|
+
...notification,
|
|
141
|
+
userId: input.userId
|
|
142
|
+
});
|
|
143
|
+
const delivery = await NotificationDeliveryModel.create(document);
|
|
144
|
+
const doc = document.tag ? await NotificationModel.findOneAndReplace({
|
|
145
|
+
userId: document.userId,
|
|
146
|
+
tag: document.tag,
|
|
147
|
+
archivedAt: { $exists: false }
|
|
148
|
+
}, document, {
|
|
149
|
+
new: true,
|
|
150
|
+
upsert: true,
|
|
151
|
+
sort: {
|
|
152
|
+
drawerActive: -1,
|
|
153
|
+
createdAt: -1
|
|
154
|
+
}
|
|
155
|
+
}) : await NotificationModel.create(document);
|
|
156
|
+
if (!doc) throw new Error("createNotificationsForUser: failed to persist notification");
|
|
157
|
+
await NotificationDeliveryModel.updateOne({ _id: delivery._id }, { $set: { notificationId: doc._id.toString() } });
|
|
158
|
+
ids.push(doc._id.toString());
|
|
159
|
+
}
|
|
160
|
+
return { ids };
|
|
228
161
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/notifications/digest.ts
|
|
164
|
+
var DAY_MS = 1440 * 60 * 1e3;
|
|
165
|
+
var WEEK_MS = 7 * DAY_MS;
|
|
166
|
+
var getDigestWindowMs = (frequency) => {
|
|
167
|
+
if (frequency === "daily") return DAY_MS;
|
|
168
|
+
if (frequency === "weekly") return WEEK_MS;
|
|
169
|
+
return 0;
|
|
232
170
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const platform = notification.platform;
|
|
237
|
-
if (!platform || typeof platform !== "object" || Array.isArray(platform)) return "";
|
|
238
|
-
const webpush = platform.webpush;
|
|
239
|
-
if (!webpush || typeof webpush !== "object" || Array.isArray(webpush)) return "";
|
|
240
|
-
const fcmOptions = webpush.fcmOptions;
|
|
241
|
-
if (!fcmOptions || typeof fcmOptions !== "object" || Array.isArray(fcmOptions)) return "";
|
|
242
|
-
const link = fcmOptions.link;
|
|
243
|
-
return typeof link === "string" ? link.trim() : "";
|
|
171
|
+
var formatIso = (value) => {
|
|
172
|
+
if (!(value instanceof Date)) return void 0;
|
|
173
|
+
return value.toISOString();
|
|
244
174
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return record;
|
|
175
|
+
var getNotificationLink = (notification) => {
|
|
176
|
+
const dataLink = notification.data?.link?.trim();
|
|
177
|
+
if (dataLink) return dataLink;
|
|
178
|
+
const platform = notification.platform;
|
|
179
|
+
if (!platform || typeof platform !== "object" || Array.isArray(platform)) return "";
|
|
180
|
+
const webpush = platform.webpush;
|
|
181
|
+
if (!webpush || typeof webpush !== "object" || Array.isArray(webpush)) return "";
|
|
182
|
+
const fcmOptions = webpush.fcmOptions;
|
|
183
|
+
if (!fcmOptions || typeof fcmOptions !== "object" || Array.isArray(fcmOptions)) return "";
|
|
184
|
+
const link = fcmOptions.link;
|
|
185
|
+
return typeof link === "string" ? link.trim() : "";
|
|
259
186
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const settings = await SettingsModel.findOne({
|
|
272
|
-
userId
|
|
273
|
-
}).lean();
|
|
274
|
-
const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
|
|
275
|
-
const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
|
|
276
|
-
if (digestFrequency === "off") {
|
|
277
|
-
return {
|
|
278
|
-
ok: true,
|
|
279
|
-
sent: false,
|
|
280
|
-
skippedReason: "digest_off"
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
const now = /* @__PURE__ */ new Date();
|
|
284
|
-
const windowMs = getDigestWindowMs(digestFrequency);
|
|
285
|
-
const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null;
|
|
286
|
-
const since = lastSentAt ?? new Date(now.getTime() - windowMs);
|
|
287
|
-
if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) {
|
|
288
|
-
return {
|
|
289
|
-
ok: true,
|
|
290
|
-
sent: false,
|
|
291
|
-
skippedReason: "not_due"
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
const preferencesByTopic = buildPreferencesByTopic(settings);
|
|
295
|
-
const disabledTopics = Object.entries(preferencesByTopic).filter(([, pref]) => pref.emailDigest === false).map(([topic]) => topic);
|
|
296
|
-
const query = {
|
|
297
|
-
userId,
|
|
298
|
-
archivedAt: {
|
|
299
|
-
$exists: false
|
|
300
|
-
},
|
|
301
|
-
readAt: {
|
|
302
|
-
$exists: false
|
|
303
|
-
},
|
|
304
|
-
createdAt: {
|
|
305
|
-
$gt: since
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
if (disabledTopics.length > 0) {
|
|
309
|
-
query.topic = {
|
|
310
|
-
$nin: disabledTopics
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
const notifications = await NotificationModel.find(query).sort({
|
|
314
|
-
createdAt: -1
|
|
315
|
-
}).limit(50).lean();
|
|
316
|
-
if (!notifications.length) {
|
|
317
|
-
await SettingsModel.updateOne({
|
|
318
|
-
userId
|
|
319
|
-
}, {
|
|
320
|
-
$set: {
|
|
321
|
-
lastDigestSentAt: now
|
|
322
|
-
},
|
|
323
|
-
$setOnInsert: {
|
|
324
|
-
userId,
|
|
325
|
-
digestFrequency: "weekly"
|
|
326
|
-
}
|
|
327
|
-
}, {
|
|
328
|
-
upsert: true
|
|
329
|
-
});
|
|
330
|
-
return {
|
|
331
|
-
ok: true,
|
|
332
|
-
sent: false,
|
|
333
|
-
skippedReason: "empty"
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
const UserModel = await models.getGlobal("RBUser", ctx);
|
|
337
|
-
const user = await UserModel.findById(userId, {
|
|
338
|
-
email: 1
|
|
339
|
-
}).lean();
|
|
340
|
-
const email = typeof user?.email === "string" ? user.email.trim() : "";
|
|
341
|
-
if (!email) {
|
|
342
|
-
return {
|
|
343
|
-
ok: true,
|
|
344
|
-
sent: false,
|
|
345
|
-
skippedReason: "missing_email"
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
const subject = "Notifications digest";
|
|
349
|
-
const rows = notifications.map((n) => {
|
|
350
|
-
const title = typeof n.title === "string" ? n.title : "";
|
|
351
|
-
const body = typeof n.body === "string" ? n.body : "";
|
|
352
|
-
const url = getNotificationLink(n);
|
|
353
|
-
const createdAt = formatIso(n.createdAt) ?? "";
|
|
354
|
-
const line = url ? `<li><strong>${title}</strong><br>${body}<br><a href="${url}">${url}</a><br><small>${createdAt}</small></li>` : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`;
|
|
355
|
-
return line;
|
|
356
|
-
}).join("");
|
|
357
|
-
const html = `<div><p>Here is your notifications digest:</p><ul>${rows}</ul></div>`;
|
|
358
|
-
let emailResult;
|
|
359
|
-
try {
|
|
360
|
-
emailResult = await sendEmail({
|
|
361
|
-
to: email,
|
|
362
|
-
subject,
|
|
363
|
-
html
|
|
364
|
-
}, {
|
|
365
|
-
missingTransport: "skip"
|
|
366
|
-
});
|
|
367
|
-
} catch (error) {
|
|
368
|
-
return {
|
|
369
|
-
ok: false,
|
|
370
|
-
error: error instanceof Error ? error.message : "Email delivery failed"
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
await SettingsModel.updateOne({
|
|
374
|
-
userId
|
|
375
|
-
}, {
|
|
376
|
-
$set: {
|
|
377
|
-
lastDigestSentAt: now
|
|
378
|
-
},
|
|
379
|
-
$setOnInsert: {
|
|
380
|
-
userId,
|
|
381
|
-
digestFrequency: "weekly"
|
|
382
|
-
}
|
|
383
|
-
}, {
|
|
384
|
-
upsert: true
|
|
385
|
-
});
|
|
386
|
-
return {
|
|
387
|
-
ok: true,
|
|
388
|
-
sent: emailResult.skipped !== true,
|
|
389
|
-
...emailResult.skipped ? {
|
|
390
|
-
skippedReason: "email_skipped"
|
|
391
|
-
} : {}
|
|
392
|
-
};
|
|
187
|
+
var buildPreferencesByTopic = (settings) => {
|
|
188
|
+
const record = {};
|
|
189
|
+
const raw = settings?.topicPreferences;
|
|
190
|
+
if (!Array.isArray(raw)) return record;
|
|
191
|
+
for (const pref of raw) {
|
|
192
|
+
if (!pref || typeof pref !== "object") continue;
|
|
193
|
+
const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
|
|
194
|
+
if (!topic) continue;
|
|
195
|
+
record[topic] = { emailDigest: pref.emailDigest === true };
|
|
196
|
+
}
|
|
197
|
+
return record;
|
|
393
198
|
};
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
199
|
+
var sendNotificationsDigestForUser = async (ctx, { userId, ability, force = false }) => {
|
|
200
|
+
const modelCtx = {
|
|
201
|
+
req: ctx.req,
|
|
202
|
+
ability
|
|
203
|
+
};
|
|
204
|
+
const SettingsModel = await models.get("RBNotificationSettings", modelCtx);
|
|
205
|
+
const NotificationModel = await models.get("RBNotification", modelCtx);
|
|
206
|
+
const settings = await SettingsModel.findOne({ userId }).lean();
|
|
207
|
+
const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
|
|
208
|
+
const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
|
|
209
|
+
if (digestFrequency === "off") return {
|
|
210
|
+
ok: true,
|
|
211
|
+
sent: false,
|
|
212
|
+
skippedReason: "digest_off"
|
|
213
|
+
};
|
|
214
|
+
const now = /* @__PURE__ */ new Date();
|
|
215
|
+
const windowMs = getDigestWindowMs(digestFrequency);
|
|
216
|
+
const lastSentAt = settings?.lastDigestSentAt instanceof Date ? settings.lastDigestSentAt : null;
|
|
217
|
+
const since = lastSentAt ?? new Date(now.getTime() - windowMs);
|
|
218
|
+
if (!force && lastSentAt && now.getTime() - lastSentAt.getTime() < windowMs) return {
|
|
219
|
+
ok: true,
|
|
220
|
+
sent: false,
|
|
221
|
+
skippedReason: "not_due"
|
|
222
|
+
};
|
|
223
|
+
const preferencesByTopic = buildPreferencesByTopic(settings);
|
|
224
|
+
const disabledTopics = Object.entries(preferencesByTopic).filter(([, pref]) => pref.emailDigest === false).map(([topic]) => topic);
|
|
225
|
+
const query = {
|
|
226
|
+
userId,
|
|
227
|
+
archivedAt: { $exists: false },
|
|
228
|
+
readAt: { $exists: false },
|
|
229
|
+
createdAt: { $gt: since }
|
|
230
|
+
};
|
|
231
|
+
if (disabledTopics.length > 0) query.topic = { $nin: disabledTopics };
|
|
232
|
+
const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(50).lean();
|
|
233
|
+
if (!notifications.length) {
|
|
234
|
+
await SettingsModel.updateOne({ userId }, {
|
|
235
|
+
$set: { lastDigestSentAt: now },
|
|
236
|
+
$setOnInsert: {
|
|
237
|
+
userId,
|
|
238
|
+
digestFrequency: "weekly"
|
|
239
|
+
}
|
|
240
|
+
}, { upsert: true });
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
sent: false,
|
|
244
|
+
skippedReason: "empty"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const user = await (await models.getGlobal("RBUser", ctx)).findById(userId, { email: 1 }).lean();
|
|
248
|
+
const email = typeof user?.email === "string" ? user.email.trim() : "";
|
|
249
|
+
if (!email) return {
|
|
250
|
+
ok: true,
|
|
251
|
+
sent: false,
|
|
252
|
+
skippedReason: "missing_email"
|
|
253
|
+
};
|
|
254
|
+
const subject = "Notifications digest";
|
|
255
|
+
const html = `<div><p>Here is your notifications digest:</p><ul>${notifications.map((n) => {
|
|
256
|
+
const title = typeof n.title === "string" ? n.title : "";
|
|
257
|
+
const body = typeof n.body === "string" ? n.body : "";
|
|
258
|
+
const url = getNotificationLink(n);
|
|
259
|
+
const createdAt = formatIso(n.createdAt) ?? "";
|
|
260
|
+
return url ? `<li><strong>${title}</strong><br>${body}<br><a href="${url}">${url}</a><br><small>${createdAt}</small></li>` : `<li><strong>${title}</strong><br>${body}<br><small>${createdAt}</small></li>`;
|
|
261
|
+
}).join("")}</ul></div>`;
|
|
262
|
+
let emailResult;
|
|
263
|
+
try {
|
|
264
|
+
emailResult = await sendEmail({
|
|
265
|
+
to: email,
|
|
266
|
+
subject,
|
|
267
|
+
html
|
|
268
|
+
}, { missingTransport: "skip" });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: error instanceof Error ? error.message : "Email delivery failed"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
await SettingsModel.updateOne({ userId }, {
|
|
276
|
+
$set: { lastDigestSentAt: now },
|
|
277
|
+
$setOnInsert: {
|
|
278
|
+
userId,
|
|
279
|
+
digestFrequency: "weekly"
|
|
280
|
+
}
|
|
281
|
+
}, { upsert: true });
|
|
282
|
+
return {
|
|
283
|
+
ok: true,
|
|
284
|
+
sent: emailResult.skipped !== true,
|
|
285
|
+
...emailResult.skipped ? { skippedReason: "email_skipped" } : {}
|
|
286
|
+
};
|
|
399
287
|
};
|
|
400
|
-
//#
|
|
288
|
+
//#endregion
|
|
289
|
+
export { createNotification, createNotificationsForUser, routes, sendNotificationsDigestForUser };
|
|
290
|
+
|
|
291
|
+
//# sourceMappingURL=notifications.js.map
|