@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
|
@@ -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,19 +45,11 @@ 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.
|
|
@@ -217,60 +209,70 @@ async function createAndFanoutActivity(
|
|
|
217
209
|
actorApIdStr: string,
|
|
218
210
|
objectApIdStr: string,
|
|
219
211
|
activity: Record<string, unknown>,
|
|
220
|
-
communityApId
|
|
212
|
+
communityApId: string | null | undefined,
|
|
213
|
+
localStatements: readonly [D1Statement, ...D1Statement[]],
|
|
221
214
|
): Promise<void> {
|
|
222
215
|
const id = activity.id as string;
|
|
223
|
-
await
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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> {
|
|
231
251
|
// A community-scoped story has reach == community: fan its activity out to the
|
|
232
252
|
// community's members/followers (the same audience posts use), NOT the
|
|
233
253
|
// author's personal follower graph. A personal story keeps author-follower
|
|
234
254
|
// reach.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
});
|
|
239
273
|
}
|
|
240
274
|
}
|
|
241
275
|
|
|
242
|
-
/**
|
|
243
|
-
* Delete all related data for a story, then the story object itself.
|
|
244
|
-
*
|
|
245
|
-
* Delegates to `deleteObjectCascade` (the SAME teardown the expiry path
|
|
246
|
-
* `cleanupExpiredStories` runs) so it also reaps the story's mandatory R2 blob
|
|
247
|
-
* and its `media_uploads` row — child-row-only deletion here would orphan the
|
|
248
|
-
* image in R2 forever (there is no orphan-key sweep). The cascade covers
|
|
249
|
-
* storyViews/Votes/Shares + likes + announces + bookmarks + objectRecipients +
|
|
250
|
-
* media; it reads `attachments_json` off the still-present object row, so the
|
|
251
|
-
* object row is dropped afterwards.
|
|
252
|
-
*/
|
|
253
|
-
/**
|
|
254
|
-
* Returns true when THIS call actually removed the objects row (false if it was
|
|
255
|
-
* already gone). Callers gate the author's postCount decrement on this so a
|
|
256
|
-
* concurrent duplicate delete — or a race with the opportunistic expiry sweep —
|
|
257
|
-
* decrements at most once for the single +1 the story counted at create time.
|
|
258
|
-
*/
|
|
259
|
-
async function deleteStoryAndRelatedData(
|
|
260
|
-
db: Database,
|
|
261
|
-
apId: string,
|
|
262
|
-
media?: IObjectStorage,
|
|
263
|
-
): Promise<boolean> {
|
|
264
|
-
const mediaKeys = await deleteObjectCascade(db, apId, media);
|
|
265
|
-
const deleted = await db
|
|
266
|
-
.delete(objects)
|
|
267
|
-
.where(eq(objects.apId, apId))
|
|
268
|
-
.returning({ apId: objects.apId });
|
|
269
|
-
// Irreversible R2 purge LAST — after the objects row is gone.
|
|
270
|
-
await purgeMediaBlobs(media, mediaKeys);
|
|
271
|
-
return deleted.length > 0;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
276
|
/**
|
|
275
277
|
* Resolve the scope of a stories read request.
|
|
276
278
|
*
|
|
@@ -367,7 +369,7 @@ stories.get("/", async (c) => {
|
|
|
367
369
|
),
|
|
368
370
|
);
|
|
369
371
|
|
|
370
|
-
const excludeAuthors =
|
|
372
|
+
const excludeAuthors = excludeModeratedActors(actor.ap_id);
|
|
371
373
|
if (excludeAuthors) {
|
|
372
374
|
storiesWhere = and(storiesWhere, excludeAuthors);
|
|
373
375
|
}
|
|
@@ -527,14 +529,11 @@ stories.get("/:actorId", async (c) => {
|
|
|
527
529
|
}
|
|
528
530
|
|
|
529
531
|
// Check blocked/muted (if authenticated)
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
);
|
|
535
|
-
if (blockedIds.includes(targetApId) || mutedIds.includes(targetApId)) {
|
|
536
|
-
return c.json({ stories: [] });
|
|
537
|
-
}
|
|
532
|
+
if (
|
|
533
|
+
actor &&
|
|
534
|
+
(await personalActorIsSuppressedBy(db, actor.ap_id, targetApId))
|
|
535
|
+
) {
|
|
536
|
+
return c.json({ stories: [] });
|
|
538
537
|
}
|
|
539
538
|
|
|
540
539
|
// Personal stories are follower-scoped: the home feed (`GET /`) only surfaces
|
|
@@ -576,6 +575,7 @@ stories.get("/:actorId", async (c) => {
|
|
|
576
575
|
eq(objects.attributedTo, targetApId),
|
|
577
576
|
gt(objects.endTime, now),
|
|
578
577
|
scopeCondition,
|
|
578
|
+
excludeModeratedActors(actor?.ap_id ?? ""),
|
|
579
579
|
),
|
|
580
580
|
)
|
|
581
581
|
.orderBy(desc(objects.published))
|
|
@@ -721,12 +721,6 @@ stories.post("/", async (c) => {
|
|
|
721
721
|
};
|
|
722
722
|
const attachmentsJson = JSON.stringify(storyData);
|
|
723
723
|
|
|
724
|
-
// Insert the story object and bump the author's denormalized postCount in a
|
|
725
|
-
// single atomic batch. D1 has no interactive transactions, so doing these as
|
|
726
|
-
// two separate writes could drift the counter relative to the stored stories
|
|
727
|
-
// on a mid-request failure. The `Database` union type does not surface `batch`
|
|
728
|
-
// (it is only on the concrete D1/libsql subclasses), so reach it through a
|
|
729
|
-
// narrow structural cast.
|
|
730
724
|
const storyInsert = db.insert(objects).values({
|
|
731
725
|
apId,
|
|
732
726
|
type: "Story",
|
|
@@ -737,14 +731,12 @@ stories.post("/", async (c) => {
|
|
|
737
731
|
endTime,
|
|
738
732
|
published: now,
|
|
739
733
|
isLocal: 1,
|
|
740
|
-
});
|
|
734
|
+
}) as D1Statement;
|
|
741
735
|
|
|
742
736
|
const postCountBump = db
|
|
743
737
|
.update(actors)
|
|
744
738
|
.set({ postCount: sql`${actors.postCount} + 1` })
|
|
745
|
-
.where(eq(actors.apId, actor.ap_id));
|
|
746
|
-
|
|
747
|
-
await (db as unknown as Batchable).batch([storyInsert, postCountBump]);
|
|
739
|
+
.where(eq(actors.apId, actor.ap_id)) as D1Statement;
|
|
748
740
|
|
|
749
741
|
const responseData = transformStoryData(attachmentsJson);
|
|
750
742
|
const authorInfo = buildAuthor(actor.ap_id, {
|
|
@@ -803,6 +795,7 @@ stories.post("/", async (c) => {
|
|
|
803
795
|
object: storyObject,
|
|
804
796
|
},
|
|
805
797
|
communityApIdValue,
|
|
798
|
+
[storyInsert, postCountBump],
|
|
806
799
|
);
|
|
807
800
|
|
|
808
801
|
return c.json({ story }, 201);
|
|
@@ -829,46 +822,70 @@ stories.post("/delete", async (c) => {
|
|
|
829
822
|
return c.json({ error: "Forbidden" }, 403);
|
|
830
823
|
}
|
|
831
824
|
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
// community), mirroring its Create; a personal story keeps author-follower
|
|
836
|
-
// 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.
|
|
837
828
|
const baseUrl = c.env.APP_URL;
|
|
838
829
|
const deleteTo = story.communityApId
|
|
839
830
|
? [`${story.communityApId}/followers`]
|
|
840
831
|
: ["https://www.w3.org/ns/activitystreams#Public"];
|
|
841
|
-
|
|
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(
|
|
842
841
|
db,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
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
|
+
},
|
|
855
853
|
);
|
|
856
854
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
// (gt > 0 still guards underflow). Mirrors the EXISTS-guarded post-delete path.
|
|
866
|
-
if (removed) {
|
|
867
|
-
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
|
|
868
863
|
.update(actors)
|
|
869
864
|
.set({ postCount: sql`${actors.postCount} - 1` })
|
|
870
|
-
.where(
|
|
871
|
-
|
|
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
|
+
);
|
|
872
889
|
|
|
873
890
|
return c.json({ success: true });
|
|
874
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,
|