@takosjp/yurucommu-core 3.4.3 → 3.4.4
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/migrations/0023_delivery_resolution_outbox.sql +33 -0
- package/migrations/0024_delivery_fanout_outbox.sql +35 -0
- package/migrations/0025_delivery_endpoint_terminal_retention.sql +6 -0
- package/migrations/0026_remote_actor_fetch_failures.sql +29 -0
- package/migrations/0027_remote_actor_tombstones.sql +12 -0
- package/migrations/0028_remote_actor_delivery_fence.sql +30 -0
- package/migrations/0029_delivery_endpoint_recipients.sql +47 -0
- package/package.json +2 -1
- package/packages/api/package.json +1 -1
- package/packages/api/src/lib/api/communities.ts +2 -0
- package/packages/api/src/lib/api/fetch.ts +69 -13
- package/packages/api/src/lib/api/normalize.ts +32 -10
- package/packages/api/src/lib/api/notifications.ts +4 -1
- package/packages/api/src/lib/rtc-client.ts +127 -29
- package/src/backend/federation-helpers.ts +1 -0
- package/src/backend/index.ts +16 -3
- package/src/backend/lib/account-migration.ts +340 -0
- package/src/backend/lib/activity-delete-cascade.ts +193 -0
- package/src/backend/lib/activitypub-actor-cache.ts +615 -48
- package/src/backend/lib/activitypub-actor-identity-sql.ts +179 -0
- package/src/backend/lib/activitypub-actor-identity.ts +39 -0
- package/src/backend/lib/activitypub-validators.ts +62 -4
- package/src/backend/lib/ap-ids.ts +36 -6
- package/src/backend/lib/ap-verify.ts +7 -17
- package/src/backend/lib/blocklist-purge.ts +676 -42
- package/src/backend/lib/blocklist.ts +278 -39
- package/src/backend/lib/community-visibility.ts +47 -33
- package/src/backend/lib/delivery/fanout-outbox.ts +336 -0
- package/src/backend/lib/delivery/planner.ts +16 -12
- package/src/backend/lib/delivery/queue-batching.ts +389 -145
- package/src/backend/lib/delivery/queue-delivery.ts +47 -25
- package/src/backend/lib/delivery/queue.ts +479 -73
- package/src/backend/lib/delivery/resolution-outbox.ts +456 -0
- package/src/backend/lib/delivery/types.ts +31 -7
- package/src/backend/lib/feed-exclude.ts +40 -28
- package/src/backend/lib/follow-edge-mutations.ts +217 -0
- package/src/backend/lib/notification-eligibility.ts +15 -9
- package/src/backend/lib/notification-push.ts +2 -2
- package/src/backend/lib/oidc-id-token.ts +66 -0
- package/src/backend/lib/personal-actor-moderation.ts +239 -0
- package/src/backend/lib/post-visibility.ts +79 -16
- package/src/backend/lib/remote-activity-id.ts +61 -0
- package/src/backend/lib/unread-counts.ts +3 -0
- package/src/backend/retention.ts +38 -1
- package/src/backend/routes/account-teardown.ts +422 -156
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
- package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
- package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
- package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
- package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
- package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
- package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
- package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
- package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
- package/src/backend/routes/activitypub/inbox-types.ts +14 -2
- package/src/backend/routes/activitypub/inbox.ts +81 -105
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub.ts +6 -5
- package/src/backend/routes/actors-helpers.ts +34 -8
- package/src/backend/routes/actors.ts +328 -164
- package/src/backend/routes/auth-helpers.ts +57 -9
- package/src/backend/routes/auth.ts +10 -2
- package/src/backend/routes/communities/membership-invites.ts +4 -1
- package/src/backend/routes/communities/membership-members.ts +201 -73
- package/src/backend/routes/communities/membership-requests.ts +141 -76
- package/src/backend/routes/communities/membership-shared.ts +228 -21
- package/src/backend/routes/communities/messages.ts +9 -2
- package/src/backend/routes/communities/routes.ts +48 -13
- package/src/backend/routes/dm/contacts.ts +29 -41
- package/src/backend/routes/dm/conversations-helpers.ts +9 -1
- package/src/backend/routes/dm/messages.ts +36 -42
- package/src/backend/routes/dm/read-archive.ts +4 -2
- package/src/backend/routes/dm/requests.ts +62 -54
- package/src/backend/routes/follow-helpers.ts +200 -60
- package/src/backend/routes/follow.ts +146 -140
- package/src/backend/routes/media.ts +20 -94
- package/src/backend/routes/moderation.ts +72 -4
- package/src/backend/routes/notes.ts +3 -4
- package/src/backend/routes/notifications.ts +209 -108
- package/src/backend/routes/posts/delete-cascade.ts +253 -89
- package/src/backend/routes/posts/federation.ts +373 -0
- package/src/backend/routes/posts/interactions.ts +160 -184
- package/src/backend/routes/posts/like-mutation.ts +240 -0
- package/src/backend/routes/posts/post-helpers.ts +112 -162
- package/src/backend/routes/posts/queries.ts +150 -54
- package/src/backend/routes/posts/routes.ts +169 -329
- package/src/backend/routes/posts/transformers.ts +61 -6
- package/src/backend/routes/recommendations.ts +37 -44
- package/src/backend/routes/rtc/index.ts +26 -6
- package/src/backend/routes/search.ts +39 -55
- package/src/backend/routes/stories/interactions.ts +22 -6
- package/src/backend/routes/stories/query-helpers.ts +28 -41
- package/src/backend/routes/stories/routes.ts +125 -108
- package/src/backend/routes/takos-tools/dm.ts +17 -17
- package/src/backend/routes/takos-tools/posts.ts +189 -106
- package/src/backend/routes/takos-tools/search.ts +13 -4
- package/src/backend/routes/takos-tools/timeline.ts +35 -27
- package/src/backend/routes/takos-tools-response.ts +6 -2
- package/src/backend/routes/timeline.ts +5 -5
- package/src/backend/runtime/call-signaling-do.ts +35 -4
- package/src/backend/runtime/one-time-ticket.ts +116 -0
- package/src/backend/runtime/realtime-stream-do.ts +5 -61
- package/src/backend/runtime/signaling-hub.ts +36 -3
- package/src/backend/server.ts +95 -80
- package/src/db/d1-write.ts +67 -43
- package/src/db/schema/federation.ts +42 -1
- package/src/db/schema/messaging.ts +110 -2
- package/src/db/schema.ts +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { isSafeRemoteUrl } from "../../../federation-helpers.ts";
|
|
2
|
+
import { MAX_ATTACHMENTS_JSON_LENGTH } from "../../../lib/attachments.ts";
|
|
3
|
+
import type { Activity, ActivityObject } from "../inbox-types.ts";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DISPLAY_DURATION = "PT5S";
|
|
6
|
+
const MAX_DISPLAY_DURATION_SECONDS = 60;
|
|
7
|
+
const MAX_STORY_LIFETIME_MS = 25 * 60 * 60 * 1000;
|
|
8
|
+
const MAX_STORY_CAPTION_LENGTH = 500;
|
|
9
|
+
const MAX_STORY_OVERLAYS = 20;
|
|
10
|
+
const MAX_STORY_URL_LENGTH = 2048;
|
|
11
|
+
const MAX_STORY_TEXT_LENGTH = 500;
|
|
12
|
+
const MAX_MEDIA_TYPE_LENGTH = 255;
|
|
13
|
+
const MAX_MEDIA_DIMENSION = 32_768;
|
|
14
|
+
const MAX_STORY_ADDRESSES = 64;
|
|
15
|
+
|
|
16
|
+
type StoryAttachmentProjection = {
|
|
17
|
+
r2_key: string;
|
|
18
|
+
content_type: string;
|
|
19
|
+
url: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type StoryOverlayPosition = {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type StoryOverlayOption = {
|
|
32
|
+
type: "Note";
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type StoryOverlayProjection = {
|
|
37
|
+
type: "Question" | "Note" | "Link";
|
|
38
|
+
position: StoryOverlayPosition;
|
|
39
|
+
name?: string;
|
|
40
|
+
href?: string;
|
|
41
|
+
oneOf?: StoryOverlayOption[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type InboundStoryProjection = {
|
|
45
|
+
attachment: StoryAttachmentProjection;
|
|
46
|
+
displayDuration: string;
|
|
47
|
+
caption?: string;
|
|
48
|
+
overlays?: StoryOverlayProjection[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type InboundStoryProjectionResult = {
|
|
52
|
+
projection: InboundStoryProjection;
|
|
53
|
+
json: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
57
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function boundedText(value: unknown, max: number): string | undefined {
|
|
61
|
+
if (typeof value !== "string") return undefined;
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
return trimmed.length > 0 ? trimmed.slice(0, max) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizedDimension(value: unknown, fallback: number): number {
|
|
67
|
+
if (
|
|
68
|
+
typeof value !== "number" ||
|
|
69
|
+
!Number.isFinite(value) ||
|
|
70
|
+
value <= 0 ||
|
|
71
|
+
value > MAX_MEDIA_DIMENSION
|
|
72
|
+
) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
return Math.max(1, Math.round(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizedMediaType(value: unknown): string {
|
|
79
|
+
if (typeof value !== "string") return "image/jpeg";
|
|
80
|
+
const mediaType = value.trim().toLowerCase();
|
|
81
|
+
if (
|
|
82
|
+
mediaType.length === 0 ||
|
|
83
|
+
mediaType.length > MAX_MEDIA_TYPE_LENGTH ||
|
|
84
|
+
!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)
|
|
85
|
+
) {
|
|
86
|
+
return "image/jpeg";
|
|
87
|
+
}
|
|
88
|
+
return mediaType;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizedRemoteAttachment(
|
|
92
|
+
raw: unknown,
|
|
93
|
+
): StoryAttachmentProjection | null {
|
|
94
|
+
const candidate = Array.isArray(raw) ? raw[0] : raw;
|
|
95
|
+
if (!isRecord(candidate)) return null;
|
|
96
|
+
|
|
97
|
+
const url = typeof candidate.url === "string" ? candidate.url.trim() : "";
|
|
98
|
+
if (
|
|
99
|
+
url.length === 0 ||
|
|
100
|
+
url.length > MAX_STORY_URL_LENGTH ||
|
|
101
|
+
!isSafeRemoteUrl(url)
|
|
102
|
+
) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
r2_key: "",
|
|
108
|
+
content_type: normalizedMediaType(
|
|
109
|
+
candidate.mediaType ?? candidate.content_type,
|
|
110
|
+
),
|
|
111
|
+
url,
|
|
112
|
+
width: normalizedDimension(candidate.width, 1080),
|
|
113
|
+
height: normalizedDimension(candidate.height, 1920),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizedPosition(raw: unknown): StoryOverlayPosition | null {
|
|
118
|
+
if (!isRecord(raw)) return null;
|
|
119
|
+
const fields = ["x", "y", "width", "height"] as const;
|
|
120
|
+
for (const field of fields) {
|
|
121
|
+
const value = raw[field];
|
|
122
|
+
if (
|
|
123
|
+
typeof value !== "number" ||
|
|
124
|
+
!Number.isFinite(value) ||
|
|
125
|
+
value < 0 ||
|
|
126
|
+
value > 1
|
|
127
|
+
) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
x: raw.x as number,
|
|
133
|
+
y: raw.y as number,
|
|
134
|
+
width: raw.width as number,
|
|
135
|
+
height: raw.height as number,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizedQuestionOptions(raw: unknown): StoryOverlayOption[] | null {
|
|
140
|
+
if (!Array.isArray(raw) || raw.length < 2 || raw.length > 4) return null;
|
|
141
|
+
const options: StoryOverlayOption[] = [];
|
|
142
|
+
for (const candidate of raw) {
|
|
143
|
+
if (!isRecord(candidate)) return null;
|
|
144
|
+
const name = boundedText(candidate.name, MAX_STORY_TEXT_LENGTH);
|
|
145
|
+
if (!name) return null;
|
|
146
|
+
options.push({ type: "Note", name });
|
|
147
|
+
}
|
|
148
|
+
return options;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizedOverlays(raw: unknown): StoryOverlayProjection[] | null {
|
|
152
|
+
if (raw === null) return [];
|
|
153
|
+
if (!Array.isArray(raw)) return null;
|
|
154
|
+
|
|
155
|
+
const overlays: StoryOverlayProjection[] = [];
|
|
156
|
+
let hasQuestion = false;
|
|
157
|
+
for (const candidate of raw.slice(0, MAX_STORY_OVERLAYS)) {
|
|
158
|
+
if (!isRecord(candidate)) continue;
|
|
159
|
+
const position = normalizedPosition(candidate.position);
|
|
160
|
+
if (!position) continue;
|
|
161
|
+
|
|
162
|
+
if (candidate.type === "Question") {
|
|
163
|
+
if (hasQuestion) continue;
|
|
164
|
+
const oneOf = normalizedQuestionOptions(candidate.oneOf);
|
|
165
|
+
if (!oneOf) continue;
|
|
166
|
+
hasQuestion = true;
|
|
167
|
+
const name = boundedText(candidate.name, MAX_STORY_TEXT_LENGTH);
|
|
168
|
+
overlays.push({
|
|
169
|
+
type: "Question",
|
|
170
|
+
position,
|
|
171
|
+
...(name ? { name } : {}),
|
|
172
|
+
oneOf,
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (candidate.type === "Link") {
|
|
178
|
+
const href =
|
|
179
|
+
typeof candidate.href === "string" ? candidate.href.trim() : "";
|
|
180
|
+
if (
|
|
181
|
+
href.length === 0 ||
|
|
182
|
+
href.length > MAX_STORY_URL_LENGTH ||
|
|
183
|
+
!isSafeRemoteUrl(href)
|
|
184
|
+
) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const name = boundedText(candidate.name, MAX_STORY_TEXT_LENGTH);
|
|
188
|
+
overlays.push({
|
|
189
|
+
type: "Link",
|
|
190
|
+
position,
|
|
191
|
+
...(name ? { name } : {}),
|
|
192
|
+
href,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (candidate.type === "Note") {
|
|
198
|
+
const name = boundedText(candidate.name, MAX_STORY_TEXT_LENGTH);
|
|
199
|
+
overlays.push({
|
|
200
|
+
type: "Note",
|
|
201
|
+
position,
|
|
202
|
+
...(name ? { name } : {}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return overlays;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizedDisplayDuration(raw: unknown): string | null {
|
|
210
|
+
if (typeof raw !== "string") return null;
|
|
211
|
+
const match = raw
|
|
212
|
+
.trim()
|
|
213
|
+
.match(
|
|
214
|
+
/^PT(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?$/,
|
|
215
|
+
);
|
|
216
|
+
if (!match || (!match[1] && !match[2] && !match[3])) return null;
|
|
217
|
+
const seconds =
|
|
218
|
+
Number(match[1] ?? 0) * 3600 +
|
|
219
|
+
Number(match[2] ?? 0) * 60 +
|
|
220
|
+
Number(match[3] ?? 0);
|
|
221
|
+
if (
|
|
222
|
+
!Number.isFinite(seconds) ||
|
|
223
|
+
seconds < 0.001 ||
|
|
224
|
+
seconds > MAX_DISPLAY_DURATION_SECONDS
|
|
225
|
+
) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return `PT${Number(seconds.toFixed(3))}S`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseStoredStoryProjection(
|
|
232
|
+
json: string,
|
|
233
|
+
): InboundStoryProjection | null {
|
|
234
|
+
let raw: unknown;
|
|
235
|
+
try {
|
|
236
|
+
raw = JSON.parse(json);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (!isRecord(raw)) return null;
|
|
241
|
+
const attachment = normalizedRemoteAttachment(raw.attachment);
|
|
242
|
+
const displayDuration = normalizedDisplayDuration(raw.displayDuration);
|
|
243
|
+
if (!attachment || !displayDuration) return null;
|
|
244
|
+
|
|
245
|
+
const overlays =
|
|
246
|
+
raw.overlays === undefined ? undefined : normalizedOverlays(raw.overlays);
|
|
247
|
+
if (overlays === null) return null;
|
|
248
|
+
const caption = boundedText(raw.caption, MAX_STORY_CAPTION_LENGTH);
|
|
249
|
+
return {
|
|
250
|
+
attachment,
|
|
251
|
+
displayDuration,
|
|
252
|
+
...(caption ? { caption } : {}),
|
|
253
|
+
...(overlays && overlays.length > 0 ? { overlays } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function serializedProjection(
|
|
258
|
+
projection: InboundStoryProjection,
|
|
259
|
+
dropOverlaysWhenOversized: boolean,
|
|
260
|
+
): InboundStoryProjectionResult | null {
|
|
261
|
+
let normalized = projection;
|
|
262
|
+
let json = JSON.stringify(normalized);
|
|
263
|
+
if (
|
|
264
|
+
json.length > MAX_ATTACHMENTS_JSON_LENGTH &&
|
|
265
|
+
dropOverlaysWhenOversized &&
|
|
266
|
+
normalized.overlays
|
|
267
|
+
) {
|
|
268
|
+
const { overlays: _overlays, ...withoutOverlays } = normalized;
|
|
269
|
+
normalized = withoutOverlays;
|
|
270
|
+
json = JSON.stringify(normalized);
|
|
271
|
+
}
|
|
272
|
+
return json.length <= MAX_ATTACHMENTS_JSON_LENGTH
|
|
273
|
+
? { projection: normalized, json }
|
|
274
|
+
: null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Build the canonical stored projection for a newly received Story. */
|
|
278
|
+
export function buildInboundStoryCreateProjection(
|
|
279
|
+
object: ActivityObject,
|
|
280
|
+
): InboundStoryProjectionResult | null {
|
|
281
|
+
const attachment = normalizedRemoteAttachment(object.attachment);
|
|
282
|
+
if (!attachment) return null;
|
|
283
|
+
const displayDuration =
|
|
284
|
+
object.displayDuration === undefined
|
|
285
|
+
? DEFAULT_DISPLAY_DURATION
|
|
286
|
+
: normalizedDisplayDuration(object.displayDuration);
|
|
287
|
+
if (!displayDuration) return null;
|
|
288
|
+
const overlays =
|
|
289
|
+
object.overlays === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: normalizedOverlays(object.overlays);
|
|
292
|
+
if (overlays === null) return null;
|
|
293
|
+
const caption = boundedText(object.content, MAX_STORY_CAPTION_LENGTH);
|
|
294
|
+
|
|
295
|
+
return serializedProjection(
|
|
296
|
+
{
|
|
297
|
+
attachment,
|
|
298
|
+
displayDuration,
|
|
299
|
+
...(caption ? { caption } : {}),
|
|
300
|
+
...(overlays && overlays.length > 0 ? { overlays } : {}),
|
|
301
|
+
},
|
|
302
|
+
true,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Apply a presence-sensitive inbound Story patch to its stored projection. */
|
|
307
|
+
export function buildInboundStoryUpdateProjection(
|
|
308
|
+
object: ActivityObject,
|
|
309
|
+
existingJson: string,
|
|
310
|
+
): InboundStoryProjectionResult | null {
|
|
311
|
+
const existing = parseStoredStoryProjection(existingJson);
|
|
312
|
+
if (!existing) return null;
|
|
313
|
+
|
|
314
|
+
const attachment =
|
|
315
|
+
object.attachment === undefined
|
|
316
|
+
? existing.attachment
|
|
317
|
+
: normalizedRemoteAttachment(object.attachment);
|
|
318
|
+
if (!attachment) return null;
|
|
319
|
+
const displayDuration =
|
|
320
|
+
object.displayDuration === undefined
|
|
321
|
+
? existing.displayDuration
|
|
322
|
+
: normalizedDisplayDuration(object.displayDuration);
|
|
323
|
+
if (!displayDuration) return null;
|
|
324
|
+
const overlays =
|
|
325
|
+
object.overlays === undefined
|
|
326
|
+
? existing.overlays
|
|
327
|
+
: normalizedOverlays(object.overlays);
|
|
328
|
+
if (overlays === null) return null;
|
|
329
|
+
const caption =
|
|
330
|
+
object.content === undefined
|
|
331
|
+
? existing.caption
|
|
332
|
+
: boundedText(object.content, MAX_STORY_CAPTION_LENGTH);
|
|
333
|
+
|
|
334
|
+
return serializedProjection(
|
|
335
|
+
{
|
|
336
|
+
attachment,
|
|
337
|
+
displayDuration,
|
|
338
|
+
...(caption ? { caption } : {}),
|
|
339
|
+
...(overlays && overlays.length > 0 ? { overlays } : {}),
|
|
340
|
+
},
|
|
341
|
+
false,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function hasStoryProjectionUpdate(object: ActivityObject): boolean {
|
|
346
|
+
return (
|
|
347
|
+
object.attachment !== undefined ||
|
|
348
|
+
object.displayDuration !== undefined ||
|
|
349
|
+
object.content !== undefined ||
|
|
350
|
+
object.overlays !== undefined
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function appendAddresses(target: Set<string>, value: unknown): boolean {
|
|
355
|
+
const candidates =
|
|
356
|
+
typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
|
|
357
|
+
for (const candidate of candidates) {
|
|
358
|
+
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
359
|
+
if (candidate.length > MAX_STORY_URL_LENGTH) return true;
|
|
360
|
+
if (target.has(candidate)) continue;
|
|
361
|
+
if (target.size >= MAX_STORY_ADDRESSES) return true;
|
|
362
|
+
target.add(candidate);
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const STORY_ADDRESS_FIELDS = ["to", "cc", "bto", "bcc", "audience"] as const;
|
|
368
|
+
|
|
369
|
+
/** Collect both envelope and embedded-object reach, including hidden fields. */
|
|
370
|
+
export function storyAddressedCollections(
|
|
371
|
+
activity: Activity,
|
|
372
|
+
object: ActivityObject,
|
|
373
|
+
): { addresses: string[]; overflow: boolean } {
|
|
374
|
+
const addresses = new Set<string>();
|
|
375
|
+
for (const source of [activity, object]) {
|
|
376
|
+
for (const field of STORY_ADDRESS_FIELDS) {
|
|
377
|
+
if (appendAddresses(addresses, source[field])) {
|
|
378
|
+
return { addresses: [...addresses], overflow: true };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { addresses: [...addresses], overflow: false };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function declaresStoryAddressing(
|
|
386
|
+
activity: Activity,
|
|
387
|
+
object: ActivityObject,
|
|
388
|
+
): boolean {
|
|
389
|
+
return [activity, object].some((source) =>
|
|
390
|
+
STORY_ADDRESS_FIELDS.some((field) => source[field] !== undefined),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Normalize and clamp a Create(Story) expiry; already-expired input is dropped. */
|
|
395
|
+
export function normalizeInboundStoryCreateEndTime(
|
|
396
|
+
publishedAt: string,
|
|
397
|
+
requestedEndTime: string | undefined,
|
|
398
|
+
nowIso: string,
|
|
399
|
+
): string | null {
|
|
400
|
+
const publishedMs = Date.parse(publishedAt);
|
|
401
|
+
const nowMs = Date.parse(nowIso);
|
|
402
|
+
if (!Number.isFinite(publishedMs) || !Number.isFinite(nowMs)) return null;
|
|
403
|
+
const maxEndMs = publishedMs + MAX_STORY_LIFETIME_MS;
|
|
404
|
+
const requestedMs = requestedEndTime ? Date.parse(requestedEndTime) : NaN;
|
|
405
|
+
const endMs = Number.isFinite(requestedMs)
|
|
406
|
+
? Math.min(requestedMs, maxEndMs)
|
|
407
|
+
: maxEndMs;
|
|
408
|
+
return endMs > nowMs ? new Date(endMs).toISOString() : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Preserve/shorten an existing Story expiry; never extend or revive it. */
|
|
412
|
+
export function normalizeInboundStoryUpdateEndTime(
|
|
413
|
+
publishedAt: string,
|
|
414
|
+
existingEndTime: string | null,
|
|
415
|
+
requestedEndTime: string | undefined,
|
|
416
|
+
nowIso: string,
|
|
417
|
+
): string | null {
|
|
418
|
+
const publishedMs = Date.parse(publishedAt);
|
|
419
|
+
const existingMs = existingEndTime ? Date.parse(existingEndTime) : NaN;
|
|
420
|
+
const nowMs = Date.parse(nowIso);
|
|
421
|
+
if (
|
|
422
|
+
!Number.isFinite(publishedMs) ||
|
|
423
|
+
!Number.isFinite(existingMs) ||
|
|
424
|
+
!Number.isFinite(nowMs) ||
|
|
425
|
+
existingMs <= nowMs
|
|
426
|
+
) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const requestedMs =
|
|
430
|
+
requestedEndTime === undefined ? existingMs : Date.parse(requestedEndTime);
|
|
431
|
+
if (!Number.isFinite(requestedMs)) return null;
|
|
432
|
+
const endMs = Math.min(
|
|
433
|
+
existingMs,
|
|
434
|
+
requestedMs,
|
|
435
|
+
publishedMs + MAX_STORY_LIFETIME_MS,
|
|
436
|
+
);
|
|
437
|
+
return new Date(endMs).toISOString();
|
|
438
|
+
}
|