@takosjp/yurucommu-core 3.4.1 → 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 +35 -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/public.ts +16 -0
- package/src/backend/retention.ts +115 -0
- 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 +333 -169
- 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 +130 -115
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// v2: 1 Story = 1 Media (Instagram style)
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { and, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm";
|
|
5
|
-
import type { Database } from "../../../db/index.ts";
|
|
5
|
+
import type { D1Statement, Database } from "../../../db/index.ts";
|
|
6
6
|
import {
|
|
7
7
|
activities,
|
|
8
8
|
actors,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
follows,
|
|
11
11
|
likes,
|
|
12
12
|
objects,
|
|
13
|
+
runBatch,
|
|
13
14
|
storyViews,
|
|
14
15
|
} from "../../../db/index.ts";
|
|
15
16
|
import {
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
objectApId,
|
|
26
27
|
} from "../../federation-helpers.ts";
|
|
27
28
|
import { storyToActivityPub } from "../../lib/activitypub-helpers.ts";
|
|
28
|
-
import {
|
|
29
|
+
import { excludeModeratedActors } from "../../lib/feed-exclude.ts";
|
|
29
30
|
import { maybeReapDrainedTombstones } from "../actors.ts";
|
|
30
31
|
import { checkCommunityPostPermission } from "../posts/post-helpers.ts";
|
|
31
32
|
import { rateLimit, RateLimitConfigs } from "../../middleware/rate-limit.ts";
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
cleanupExpiredStories,
|
|
35
36
|
fetchActorCache,
|
|
36
37
|
fetchBatchVotes,
|
|
37
|
-
fetchBlockedAndMutedIds,
|
|
38
38
|
type StoryAuthor,
|
|
39
39
|
sumVotes,
|
|
40
40
|
transformStoryData,
|
|
@@ -45,30 +45,20 @@ import {
|
|
|
45
45
|
enqueueFanoutToFollowers,
|
|
46
46
|
} from "../../lib/delivery/queue.ts";
|
|
47
47
|
import { logger } from "../../lib/logger.ts";
|
|
48
|
+
import { personalActorIsSuppressedBy } from "../../lib/personal-actor-moderation.ts";
|
|
49
|
+
import { prepareDeliveryFanoutJob } from "../../lib/delivery/fanout-outbox.ts";
|
|
48
50
|
|
|
49
51
|
const log = logger.child({ component: "stories.routes" });
|
|
50
52
|
|
|
51
|
-
/**
|
|
52
|
-
* Narrow view over the concrete D1/libsql drizzle client's atomic batch API.
|
|
53
|
-
* The shared `Database` union type does not surface `batch` (it lives on the
|
|
54
|
-
* concrete subclasses), so we reach it through a structural cast at the call
|
|
55
|
-
* site that needs an atomic multi-statement write.
|
|
56
|
-
*/
|
|
57
|
-
type Batchable = {
|
|
58
|
-
batch(statements: readonly unknown[]): Promise<unknown>;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
53
|
const stories = new Hono<{ Bindings: Env; Variables: Variables }>();
|
|
62
54
|
|
|
63
55
|
// Best-effort, opportunistic retention of expired stories.
|
|
64
56
|
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// below ensures at most one sweep runs at a time per isolate, so a burst of
|
|
71
|
-
// feed requests cannot kick off several concurrent full-table delete sweeps.
|
|
57
|
+
// The public Worker also exposes a scheduled retention handler. Keep this
|
|
58
|
+
// probabilistic read-path pass as a self-hosting fallback for runtimes that do
|
|
59
|
+
// not configure a cron trigger. Expired stories are already excluded from every
|
|
60
|
+
// read query, so a missed sweep affects storage only. The guard below ensures
|
|
61
|
+
// at most one fallback pass runs at a time per isolate.
|
|
72
62
|
let expiredStoryCleanupInFlight = false;
|
|
73
63
|
|
|
74
64
|
function maybeCleanupExpiredStories(
|
|
@@ -219,60 +209,70 @@ async function createAndFanoutActivity(
|
|
|
219
209
|
actorApIdStr: string,
|
|
220
210
|
objectApIdStr: string,
|
|
221
211
|
activity: Record<string, unknown>,
|
|
222
|
-
communityApId
|
|
212
|
+
communityApId: string | null | undefined,
|
|
213
|
+
localStatements: readonly [D1Statement, ...D1Statement[]],
|
|
223
214
|
): Promise<void> {
|
|
224
215
|
const id = activity.id as string;
|
|
225
|
-
await
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
216
|
+
const preparedFanout = await prepareDeliveryFanoutJob(
|
|
217
|
+
db,
|
|
218
|
+
communityApId
|
|
219
|
+
? {
|
|
220
|
+
kind: "community",
|
|
221
|
+
activityId: id,
|
|
222
|
+
targetApId: communityApId,
|
|
223
|
+
}
|
|
224
|
+
: {
|
|
225
|
+
kind: "followers",
|
|
226
|
+
activityId: id,
|
|
227
|
+
targetApId: actorApIdStr,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
await runBatch(db, [
|
|
231
|
+
...localStatements,
|
|
232
|
+
db.insert(activities).values({
|
|
233
|
+
apId: id,
|
|
234
|
+
type: activity.type as string,
|
|
235
|
+
actorApId: actorApIdStr,
|
|
236
|
+
objectApId: objectApIdStr,
|
|
237
|
+
rawJson: JSON.stringify(activity),
|
|
238
|
+
direction: "outbound",
|
|
239
|
+
}) as D1Statement,
|
|
240
|
+
preparedFanout.statement,
|
|
241
|
+
]);
|
|
242
|
+
await publishDurableStoryFanoutWakeup(env, id, actorApIdStr, communityApId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function publishDurableStoryFanoutWakeup(
|
|
246
|
+
env: Env,
|
|
247
|
+
activityId: string,
|
|
248
|
+
actorApId: string,
|
|
249
|
+
communityApId?: string | null,
|
|
250
|
+
): Promise<void> {
|
|
233
251
|
// A community-scoped story has reach == community: fan its activity out to the
|
|
234
252
|
// community's members/followers (the same audience posts use), NOT the
|
|
235
253
|
// author's personal follower graph. A personal story keeps author-follower
|
|
236
254
|
// reach.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
255
|
+
try {
|
|
256
|
+
if (communityApId) {
|
|
257
|
+
await enqueueFanoutToCommunity(env, activityId, communityApId);
|
|
258
|
+
} else {
|
|
259
|
+
await enqueueFanoutToFollowers(env, activityId, actorApId);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Activity and semantic fanout intent were committed atomically above. The
|
|
263
|
+
// Queue RPC is only a wakeup; keep the route moving so Create returns the
|
|
264
|
+
// committed story and Delete completes its local cascade while scheduled
|
|
265
|
+
// recovery republishes the pending row.
|
|
266
|
+
log.error("Failed to publish durable story fanout wakeup", {
|
|
267
|
+
event: "stories.fanout_wakeup_failed",
|
|
268
|
+
activityId,
|
|
269
|
+
actor: actorApId,
|
|
270
|
+
communityApId: communityApId ?? null,
|
|
271
|
+
error,
|
|
272
|
+
});
|
|
241
273
|
}
|
|
242
274
|
}
|
|
243
275
|
|
|
244
|
-
/**
|
|
245
|
-
* Delete all related data for a story, then the story object itself.
|
|
246
|
-
*
|
|
247
|
-
* Delegates to `deleteObjectCascade` (the SAME teardown the expiry path
|
|
248
|
-
* `cleanupExpiredStories` runs) so it also reaps the story's mandatory R2 blob
|
|
249
|
-
* and its `media_uploads` row — child-row-only deletion here would orphan the
|
|
250
|
-
* image in R2 forever (there is no orphan-key sweep). The cascade covers
|
|
251
|
-
* storyViews/Votes/Shares + likes + announces + bookmarks + objectRecipients +
|
|
252
|
-
* media; it reads `attachments_json` off the still-present object row, so the
|
|
253
|
-
* object row is dropped afterwards.
|
|
254
|
-
*/
|
|
255
|
-
/**
|
|
256
|
-
* Returns true when THIS call actually removed the objects row (false if it was
|
|
257
|
-
* already gone). Callers gate the author's postCount decrement on this so a
|
|
258
|
-
* concurrent duplicate delete — or a race with the opportunistic expiry sweep —
|
|
259
|
-
* decrements at most once for the single +1 the story counted at create time.
|
|
260
|
-
*/
|
|
261
|
-
async function deleteStoryAndRelatedData(
|
|
262
|
-
db: Database,
|
|
263
|
-
apId: string,
|
|
264
|
-
media?: IObjectStorage,
|
|
265
|
-
): Promise<boolean> {
|
|
266
|
-
const mediaKeys = await deleteObjectCascade(db, apId, media);
|
|
267
|
-
const deleted = await db
|
|
268
|
-
.delete(objects)
|
|
269
|
-
.where(eq(objects.apId, apId))
|
|
270
|
-
.returning({ apId: objects.apId });
|
|
271
|
-
// Irreversible R2 purge LAST — after the objects row is gone.
|
|
272
|
-
await purgeMediaBlobs(media, mediaKeys);
|
|
273
|
-
return deleted.length > 0;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
276
|
/**
|
|
277
277
|
* Resolve the scope of a stories read request.
|
|
278
278
|
*
|
|
@@ -369,7 +369,7 @@ stories.get("/", async (c) => {
|
|
|
369
369
|
),
|
|
370
370
|
);
|
|
371
371
|
|
|
372
|
-
const excludeAuthors =
|
|
372
|
+
const excludeAuthors = excludeModeratedActors(actor.ap_id);
|
|
373
373
|
if (excludeAuthors) {
|
|
374
374
|
storiesWhere = and(storiesWhere, excludeAuthors);
|
|
375
375
|
}
|
|
@@ -529,14 +529,11 @@ stories.get("/:actorId", async (c) => {
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
// Check blocked/muted (if authenticated)
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
);
|
|
537
|
-
if (blockedIds.includes(targetApId) || mutedIds.includes(targetApId)) {
|
|
538
|
-
return c.json({ stories: [] });
|
|
539
|
-
}
|
|
532
|
+
if (
|
|
533
|
+
actor &&
|
|
534
|
+
(await personalActorIsSuppressedBy(db, actor.ap_id, targetApId))
|
|
535
|
+
) {
|
|
536
|
+
return c.json({ stories: [] });
|
|
540
537
|
}
|
|
541
538
|
|
|
542
539
|
// Personal stories are follower-scoped: the home feed (`GET /`) only surfaces
|
|
@@ -578,6 +575,7 @@ stories.get("/:actorId", async (c) => {
|
|
|
578
575
|
eq(objects.attributedTo, targetApId),
|
|
579
576
|
gt(objects.endTime, now),
|
|
580
577
|
scopeCondition,
|
|
578
|
+
excludeModeratedActors(actor?.ap_id ?? ""),
|
|
581
579
|
),
|
|
582
580
|
)
|
|
583
581
|
.orderBy(desc(objects.published))
|
|
@@ -723,12 +721,6 @@ stories.post("/", async (c) => {
|
|
|
723
721
|
};
|
|
724
722
|
const attachmentsJson = JSON.stringify(storyData);
|
|
725
723
|
|
|
726
|
-
// Insert the story object and bump the author's denormalized postCount in a
|
|
727
|
-
// single atomic batch. D1 has no interactive transactions, so doing these as
|
|
728
|
-
// two separate writes could drift the counter relative to the stored stories
|
|
729
|
-
// on a mid-request failure. The `Database` union type does not surface `batch`
|
|
730
|
-
// (it is only on the concrete D1/libsql subclasses), so reach it through a
|
|
731
|
-
// narrow structural cast.
|
|
732
724
|
const storyInsert = db.insert(objects).values({
|
|
733
725
|
apId,
|
|
734
726
|
type: "Story",
|
|
@@ -739,14 +731,12 @@ stories.post("/", async (c) => {
|
|
|
739
731
|
endTime,
|
|
740
732
|
published: now,
|
|
741
733
|
isLocal: 1,
|
|
742
|
-
});
|
|
734
|
+
}) as D1Statement;
|
|
743
735
|
|
|
744
736
|
const postCountBump = db
|
|
745
737
|
.update(actors)
|
|
746
738
|
.set({ postCount: sql`${actors.postCount} + 1` })
|
|
747
|
-
.where(eq(actors.apId, actor.ap_id));
|
|
748
|
-
|
|
749
|
-
await (db as unknown as Batchable).batch([storyInsert, postCountBump]);
|
|
739
|
+
.where(eq(actors.apId, actor.ap_id)) as D1Statement;
|
|
750
740
|
|
|
751
741
|
const responseData = transformStoryData(attachmentsJson);
|
|
752
742
|
const authorInfo = buildAuthor(actor.ap_id, {
|
|
@@ -805,6 +795,7 @@ stories.post("/", async (c) => {
|
|
|
805
795
|
object: storyObject,
|
|
806
796
|
},
|
|
807
797
|
communityApIdValue,
|
|
798
|
+
[storyInsert, postCountBump],
|
|
808
799
|
);
|
|
809
800
|
|
|
810
801
|
return c.json({ story }, 201);
|
|
@@ -831,46 +822,70 @@ stories.post("/delete", async (c) => {
|
|
|
831
822
|
return c.json({ error: "Forbidden" }, 403);
|
|
832
823
|
}
|
|
833
824
|
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
// community), mirroring its Create; a personal story keeps author-follower
|
|
838
|
-
// reach.
|
|
825
|
+
// A community-scoped story tombstone is addressed to and fanned out to the
|
|
826
|
+
// community (reach == community), mirroring its Create; a personal story
|
|
827
|
+
// keeps author-follower reach.
|
|
839
828
|
const baseUrl = c.env.APP_URL;
|
|
840
829
|
const deleteTo = story.communityApId
|
|
841
830
|
? [`${story.communityApId}/followers`]
|
|
842
831
|
: ["https://www.w3.org/ns/activitystreams#Public"];
|
|
843
|
-
|
|
832
|
+
const deleteActivity = {
|
|
833
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
834
|
+
id: activityApId(baseUrl, generateId()),
|
|
835
|
+
type: "Delete",
|
|
836
|
+
actor: actor.ap_id,
|
|
837
|
+
to: deleteTo,
|
|
838
|
+
object: apId,
|
|
839
|
+
};
|
|
840
|
+
const preparedFanout = await prepareDeliveryFanoutJob(
|
|
844
841
|
db,
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
story.communityApId,
|
|
842
|
+
story.communityApId
|
|
843
|
+
? {
|
|
844
|
+
kind: "community",
|
|
845
|
+
activityId: deleteActivity.id,
|
|
846
|
+
targetApId: story.communityApId,
|
|
847
|
+
}
|
|
848
|
+
: {
|
|
849
|
+
kind: "followers",
|
|
850
|
+
activityId: deleteActivity.id,
|
|
851
|
+
targetApId: actor.ap_id,
|
|
852
|
+
},
|
|
857
853
|
);
|
|
858
854
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// (gt > 0 still guards underflow). Mirrors the EXISTS-guarded post-delete path.
|
|
868
|
-
if (removed) {
|
|
869
|
-
await db
|
|
855
|
+
// Reap every projection of the OLD Create/Update activities while the story
|
|
856
|
+
// row still exists. The outbound Delete is deliberately inserted only in the
|
|
857
|
+
// final batch below; inserting it first made this cascade erase its own
|
|
858
|
+
// pending fanout before a Queue consumer could resolve the Activity.
|
|
859
|
+
const mediaKeys = await deleteObjectCascade(db, apId, c.env.MEDIA);
|
|
860
|
+
const storyExists = sql`EXISTS (SELECT 1 FROM ${objects} WHERE ${objects.apId} = ${apId})`;
|
|
861
|
+
await runBatch(db, [
|
|
862
|
+
db
|
|
870
863
|
.update(actors)
|
|
871
864
|
.set({ postCount: sql`${actors.postCount} - 1` })
|
|
872
|
-
.where(
|
|
873
|
-
|
|
865
|
+
.where(
|
|
866
|
+
and(eq(actors.apId, actor.ap_id), gt(actors.postCount, 0), storyExists),
|
|
867
|
+
) as D1Statement,
|
|
868
|
+
db.delete(objects).where(eq(objects.apId, apId)) as D1Statement,
|
|
869
|
+
db.insert(activities).values({
|
|
870
|
+
apId: deleteActivity.id,
|
|
871
|
+
type: deleteActivity.type,
|
|
872
|
+
actorApId: actor.ap_id,
|
|
873
|
+
objectApId: apId,
|
|
874
|
+
rawJson: JSON.stringify(deleteActivity),
|
|
875
|
+
direction: "outbound",
|
|
876
|
+
}) as D1Statement,
|
|
877
|
+
preparedFanout.statement,
|
|
878
|
+
]);
|
|
879
|
+
|
|
880
|
+
// Irreversible R2 purge only after the story row is gone and its durable
|
|
881
|
+
// federation intent exists.
|
|
882
|
+
await purgeMediaBlobs(c.env.MEDIA, mediaKeys);
|
|
883
|
+
await publishDurableStoryFanoutWakeup(
|
|
884
|
+
c.env,
|
|
885
|
+
deleteActivity.id,
|
|
886
|
+
actor.ap_id,
|
|
887
|
+
story.communityApId,
|
|
888
|
+
);
|
|
874
889
|
|
|
875
890
|
return c.json({ success: true });
|
|
876
891
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* yurucommu_get_dm_messages
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { and, desc, eq,
|
|
8
|
+
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
|
9
9
|
import {
|
|
10
10
|
activities,
|
|
11
11
|
actors,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
activityApId,
|
|
19
19
|
generateId,
|
|
20
20
|
objectApId,
|
|
21
|
-
safeJsonParse,
|
|
22
21
|
} from "../../federation-helpers.ts";
|
|
23
22
|
import {
|
|
24
23
|
MAX_DM_CONTENT_LENGTH,
|
|
@@ -30,11 +29,16 @@ import {
|
|
|
30
29
|
errRequired,
|
|
31
30
|
ok,
|
|
32
31
|
requireString,
|
|
33
|
-
resolveDmPartner,
|
|
34
32
|
toolLimit,
|
|
35
33
|
type ToolResponse,
|
|
36
34
|
} from "../takos-tools-response.ts";
|
|
35
|
+
import {
|
|
36
|
+
dmWhereForActor,
|
|
37
|
+
getOtherParticipant,
|
|
38
|
+
recipientObjectIds,
|
|
39
|
+
} from "../dm/conversations-helpers.ts";
|
|
37
40
|
import type { Input, ToolContext } from "./types.ts";
|
|
41
|
+
import { isActorBlockedStrict } from "../../lib/blocklist.ts";
|
|
38
42
|
|
|
39
43
|
// `.batch` lives only on the concrete D1/libsql subclasses; reach it through a
|
|
40
44
|
// narrow structural cast so the Note + recipient + activity + inbox commit
|
|
@@ -165,13 +169,7 @@ export async function handleGetDmThreads(
|
|
|
165
169
|
content: objects.content,
|
|
166
170
|
})
|
|
167
171
|
.from(objects)
|
|
168
|
-
.where(
|
|
169
|
-
and(
|
|
170
|
-
eq(objects.visibility, "direct"),
|
|
171
|
-
eq(objects.type, "Note"),
|
|
172
|
-
isNotNull(objects.conversation),
|
|
173
|
-
),
|
|
174
|
-
)
|
|
172
|
+
.where(dmWhereForActor(db, actor.ap_id))
|
|
175
173
|
.orderBy(desc(objects.published))
|
|
176
174
|
.limit(2000);
|
|
177
175
|
|
|
@@ -182,7 +180,7 @@ export async function handleGetDmThreads(
|
|
|
182
180
|
> = {};
|
|
183
181
|
|
|
184
182
|
for (const dm of dms) {
|
|
185
|
-
const partner =
|
|
183
|
+
const partner = getOtherParticipant(dm, actor.ap_id);
|
|
186
184
|
if (partner && !threads[partner]) {
|
|
187
185
|
threads[partner] = {
|
|
188
186
|
partner,
|
|
@@ -206,6 +204,9 @@ export async function handleGetDmMessages(
|
|
|
206
204
|
const threadId = requireString(input, "thread_id");
|
|
207
205
|
const limit = toolLimit(input.limit, 50, 100);
|
|
208
206
|
if (!threadId) return c.json(errRequired("Thread ID"), 400);
|
|
207
|
+
if (await isActorBlockedStrict(db, threadId)) {
|
|
208
|
+
return c.json(errNotFound("Thread"), 404);
|
|
209
|
+
}
|
|
209
210
|
|
|
210
211
|
const baseUrl = c.env.APP_URL;
|
|
211
212
|
// Read from the STORED conversation id (incl. a legacy-scheme thread) so the
|
|
@@ -225,19 +226,18 @@ export async function handleGetDmMessages(
|
|
|
225
226
|
eq(objects.visibility, "direct"),
|
|
226
227
|
eq(objects.type, "Note"),
|
|
227
228
|
eq(objects.conversation, conversationId),
|
|
229
|
+
or(
|
|
230
|
+
eq(objects.attributedTo, actor.ap_id),
|
|
231
|
+
inArray(objects.apId, recipientObjectIds(db, actor.ap_id)),
|
|
232
|
+
),
|
|
228
233
|
),
|
|
229
234
|
)
|
|
230
235
|
.orderBy(desc(objects.published))
|
|
231
236
|
.limit(limit);
|
|
232
237
|
|
|
233
|
-
const filtered = messages.filter((m) => {
|
|
234
|
-
if (m.attributedTo === actor.ap_id) return true;
|
|
235
|
-
return safeJsonParse<string[]>(m.toJson, []).includes(actor.ap_id);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
238
|
return c.json(
|
|
239
239
|
ok({
|
|
240
|
-
messages:
|
|
240
|
+
messages: messages.map((m) => ({
|
|
241
241
|
ap_id: m.apId,
|
|
242
242
|
content: m.content,
|
|
243
243
|
from: m.attributedTo,
|