@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "../../../../db/index.ts";
|
|
2
|
-
import { and, eq,
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
3
|
import { activities, actors, follows } from "../../../../db/index.ts";
|
|
4
4
|
import {
|
|
5
5
|
activityApId,
|
|
@@ -7,8 +7,14 @@ import {
|
|
|
7
7
|
isLocal,
|
|
8
8
|
} from "../../../federation-helpers.ts";
|
|
9
9
|
import { enqueueDeliveryToActor } from "../../../lib/delivery/queue.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { isSameActivityPubActor } from "../../../lib/activitypub-actor-identity.ts";
|
|
11
|
+
import { actorSuppressesInteractionFrom } from "../../../lib/post-visibility.ts";
|
|
11
12
|
import { logger } from "../../../lib/logger.ts";
|
|
13
|
+
import { resolveInboundActivityReference } from "../inbound-activity-reference.ts";
|
|
14
|
+
import {
|
|
15
|
+
acceptRetainedFollowDirection,
|
|
16
|
+
severFollowDirection,
|
|
17
|
+
} from "../../../lib/follow-edge-mutations.ts";
|
|
12
18
|
import {
|
|
13
19
|
type Activity,
|
|
14
20
|
type ActivityContext,
|
|
@@ -18,15 +24,49 @@ import {
|
|
|
18
24
|
} from "../inbox-types.ts";
|
|
19
25
|
import {
|
|
20
26
|
findFollowByActivityId,
|
|
27
|
+
findFollowByFollowerIdentity,
|
|
21
28
|
runBatch,
|
|
22
29
|
undoInteraction,
|
|
23
30
|
upsertActivityAndNotify,
|
|
24
31
|
} from "./inbox-shared-helpers.ts";
|
|
25
32
|
|
|
26
33
|
type ActorRow = typeof actors.$inferSelect;
|
|
34
|
+
type FollowReference = {
|
|
35
|
+
followerApId: string;
|
|
36
|
+
followingApId: string;
|
|
37
|
+
status: string;
|
|
38
|
+
};
|
|
27
39
|
|
|
28
40
|
const log = logger.child({ component: "activitypub.inbox.follow" });
|
|
29
41
|
|
|
42
|
+
function authorizesFollowUndo(
|
|
43
|
+
follow: FollowReference,
|
|
44
|
+
actor: string,
|
|
45
|
+
recipientApId: string,
|
|
46
|
+
activityId: string | null,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (!isSameActivityPubActor(follow.followerApId, actor)) {
|
|
49
|
+
log.warn("Undo(Follow) actor mismatch", {
|
|
50
|
+
event: "ap.undo.follow.actor_mismatch",
|
|
51
|
+
actor,
|
|
52
|
+
followOwner: follow.followerApId,
|
|
53
|
+
activityId,
|
|
54
|
+
});
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (follow.followingApId !== recipientApId) {
|
|
58
|
+
log.warn("Undo(Follow) recipient mismatch", {
|
|
59
|
+
event: "ap.undo.follow.recipient_mismatch",
|
|
60
|
+
actor,
|
|
61
|
+
recipient: recipientApId,
|
|
62
|
+
followedActor: follow.followingApId,
|
|
63
|
+
activityId,
|
|
64
|
+
});
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
// ---------------------------------------------------------------------------
|
|
31
71
|
// Follow handler
|
|
32
72
|
// ---------------------------------------------------------------------------
|
|
@@ -41,15 +81,15 @@ export async function handleFollow(
|
|
|
41
81
|
) {
|
|
42
82
|
const db = c.get("db");
|
|
43
83
|
|
|
44
|
-
// A Follow from an actor the recipient has personally
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
84
|
+
// A Follow from an actor the recipient has personally blocked OR muted must be
|
|
85
|
+
// dropped at write time. Without this, a suppressed remote could re-follow
|
|
86
|
+
// (auto-accepting on a public recipient → followerCount++ + a follow
|
|
87
|
+
// notification + an Accept, so the remote actor resumes receiving fan-out),
|
|
88
|
+
// or surface in the owner's
|
|
49
89
|
// follow-request list. `actor` is the HTTP-signature-verified signer. The inbox
|
|
50
90
|
// already ACKs, so dropping here causes no retry storm and does not leak the
|
|
51
|
-
//
|
|
52
|
-
if (await
|
|
91
|
+
// recipient's moderation choice to the sender.
|
|
92
|
+
if (await actorSuppressesInteractionFrom(db, recipient.apId, actor)) {
|
|
53
93
|
return;
|
|
54
94
|
}
|
|
55
95
|
|
|
@@ -59,20 +99,17 @@ export async function handleFollow(
|
|
|
59
99
|
const status = recipient.isPrivate ? "pending" : "accepted";
|
|
60
100
|
const now = new Date().toISOString();
|
|
61
101
|
|
|
62
|
-
// Was the edge already present BEFORE this dispatch?
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
),
|
|
74
|
-
)
|
|
75
|
-
.get();
|
|
102
|
+
// Was the edge already present BEFORE this dispatch? Exact keys are the
|
|
103
|
+
// steady-state path; the helper also recognizes a pre-invariant cosmetic
|
|
104
|
+
// spelling so a retry cannot create a second logical edge. The original
|
|
105
|
+
// insert + counter transition committed atomically, so a retained edge means
|
|
106
|
+
// there is nothing left to reconcile or notify.
|
|
107
|
+
const existingEdge = await findFollowByFollowerIdentity(
|
|
108
|
+
db,
|
|
109
|
+
actor,
|
|
110
|
+
recipient.apId,
|
|
111
|
+
);
|
|
112
|
+
if (existingEdge) return;
|
|
76
113
|
|
|
77
114
|
// #COUNTER-SYM (crash-retry convergence): the followers-edge insert and the
|
|
78
115
|
// recipient.followerCount +1 MUST commit together. Previously the edge was
|
|
@@ -122,11 +159,6 @@ export async function handleFollow(
|
|
|
122
159
|
.onConflictDoNothing();
|
|
123
160
|
}
|
|
124
161
|
|
|
125
|
-
// If the edge already existed, this is a duplicate (re)delivery: the counter is
|
|
126
|
-
// already correct (the batch's no-op insert kept the NOT-EXISTS guard false),
|
|
127
|
-
// so do not re-notify the owner or re-send an Accept.
|
|
128
|
-
if (existingEdge) return;
|
|
129
|
-
|
|
130
162
|
// Store activity and add to inbox (AP Native notification)
|
|
131
163
|
await upsertActivityAndNotify(
|
|
132
164
|
db,
|
|
@@ -181,58 +213,30 @@ export async function handleAccept(
|
|
|
181
213
|
if (!followId) return;
|
|
182
214
|
|
|
183
215
|
const follow = await findFollowByActivityId(db, followId);
|
|
184
|
-
if (
|
|
216
|
+
if (
|
|
217
|
+
!follow ||
|
|
218
|
+
(follow.status !== "pending" && follow.status !== "accepted")
|
|
219
|
+
) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
185
222
|
|
|
186
223
|
// Only the followed party may Accept the follow. The signing actor is bound to
|
|
187
224
|
// its domain upstream, so without this a different-domain actor that learned
|
|
188
225
|
// the follow activity id could flip someone else's pending follow to accepted.
|
|
189
|
-
if (!
|
|
226
|
+
if (!isSameActivityPubActor(follow.followingApId, actor)) return;
|
|
190
227
|
|
|
191
228
|
const now = new Date().toISOString();
|
|
192
229
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// correlated `EXISTS(... status='pending')` subquery, so they fire only when
|
|
204
|
-
// THIS batch is the one performing the transition (the still-pending edge is
|
|
205
|
-
// observed in pre-flip state). A concurrent duplicate Accept, or a retry
|
|
206
|
-
// after the flip already committed, sees a non-pending edge → both guards
|
|
207
|
-
// are false and the flip's `status='pending'` predicate is a no-op, so the
|
|
208
|
-
// counts can neither double-bump nor (on retry) under-count.
|
|
209
|
-
const pendingEdgeExists = sql`EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${follow.followerApId} AND ${follows.followingApId} = ${follow.followingApId} AND ${follows.status} = 'pending')`;
|
|
210
|
-
await runBatch(db, [
|
|
211
|
-
db
|
|
212
|
-
.update(actors)
|
|
213
|
-
.set({ followingCount: sql`${actors.followingCount} + 1` })
|
|
214
|
-
.where(and(eq(actors.apId, follow.followerApId), pendingEdgeExists)),
|
|
215
|
-
db
|
|
216
|
-
.update(actors)
|
|
217
|
-
.set({ followerCount: sql`${actors.followerCount} + 1` })
|
|
218
|
-
.where(and(eq(actors.apId, follow.followingApId), pendingEdgeExists)),
|
|
219
|
-
db
|
|
220
|
-
.update(follows)
|
|
221
|
-
.set({ status: "accepted", acceptedAt: now })
|
|
222
|
-
.where(
|
|
223
|
-
and(
|
|
224
|
-
eq(follows.followerApId, follow.followerApId),
|
|
225
|
-
eq(follows.followingApId, follow.followingApId),
|
|
226
|
-
eq(follows.status, "pending"),
|
|
227
|
-
),
|
|
228
|
-
),
|
|
229
|
-
]);
|
|
230
|
-
} catch (e) {
|
|
231
|
-
log.error("Error in handleAccept", {
|
|
232
|
-
event: "ap.accept.handler_error",
|
|
233
|
-
error: e,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
230
|
+
// Converge legacy cosmetic duplicates to one logical accepted relation. The
|
|
231
|
+
// shared mutation derives the counter delta from the accepted rows that
|
|
232
|
+
// actually exist, so pending->accepted adds one, a retry adds zero, and two
|
|
233
|
+
// accepted spellings repair back to one in the same atomic batch.
|
|
234
|
+
await acceptRetainedFollowDirection(
|
|
235
|
+
db,
|
|
236
|
+
follow.followerApId,
|
|
237
|
+
follow.followingApId,
|
|
238
|
+
now,
|
|
239
|
+
);
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
// ---------------------------------------------------------------------------
|
|
@@ -252,39 +256,15 @@ export async function handleReject(
|
|
|
252
256
|
if (!follow) return;
|
|
253
257
|
|
|
254
258
|
// Only the followed party may Reject the follow (see handleAccept).
|
|
255
|
-
if (!
|
|
259
|
+
if (!isSameActivityPubActor(follow.followingApId, actor)) return;
|
|
256
260
|
|
|
257
261
|
// A remote followee can Reject an ALREADY-ACCEPTED follow to terminate it
|
|
258
|
-
// (Mastodon does this on lock + remove-follower).
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
// duplicate are no-ops via the EXISTS(... status='accepted') + gt(>0) guards.
|
|
265
|
-
// (followingApId — the rejecting followee — is the remote signer with no local
|
|
266
|
-
// actors row, so only the local follower's followingCount is reconciled.)
|
|
267
|
-
const acceptedEdgeExists = sql`EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${follow.followerApId} AND ${follows.followingApId} = ${follow.followingApId} AND ${follows.status} = 'accepted')`;
|
|
268
|
-
await runBatch(db, [
|
|
269
|
-
db
|
|
270
|
-
.update(actors)
|
|
271
|
-
.set({ followingCount: sql`${actors.followingCount} - 1` })
|
|
272
|
-
.where(
|
|
273
|
-
and(
|
|
274
|
-
eq(actors.apId, follow.followerApId),
|
|
275
|
-
gt(actors.followingCount, 0),
|
|
276
|
-
acceptedEdgeExists,
|
|
277
|
-
),
|
|
278
|
-
),
|
|
279
|
-
db
|
|
280
|
-
.delete(follows)
|
|
281
|
-
.where(
|
|
282
|
-
and(
|
|
283
|
-
eq(follows.followerApId, follow.followerApId),
|
|
284
|
-
eq(follows.followingApId, follow.followingApId),
|
|
285
|
-
),
|
|
286
|
-
),
|
|
287
|
-
]);
|
|
262
|
+
// (Mastodon does this on lock + remove-follower). Remove every cosmetically
|
|
263
|
+
// equivalent retained edge: deleting only the first activity-id match left a
|
|
264
|
+
// second row authorized to receive posts. The shared mutation subtracts the
|
|
265
|
+
// number of accepted rows and deletes them atomically; pending and duplicate
|
|
266
|
+
// Reject deliveries remain counter-safe no-ops.
|
|
267
|
+
await severFollowDirection(db, follow.followerApId, follow.followingApId);
|
|
288
268
|
}
|
|
289
269
|
|
|
290
270
|
// ---------------------------------------------------------------------------
|
|
@@ -306,7 +286,7 @@ export async function handleUndo(
|
|
|
306
286
|
activity: Activity,
|
|
307
287
|
recipient: ActorRow | null,
|
|
308
288
|
actor: string,
|
|
309
|
-
|
|
289
|
+
baseUrl: string,
|
|
310
290
|
) {
|
|
311
291
|
const db = c.get("db");
|
|
312
292
|
const activityObject = getActivityObject(activity);
|
|
@@ -320,6 +300,7 @@ export async function handleUndo(
|
|
|
320
300
|
objectId,
|
|
321
301
|
actor,
|
|
322
302
|
recipient,
|
|
303
|
+
baseUrl,
|
|
323
304
|
);
|
|
324
305
|
if (resolved) return;
|
|
325
306
|
}
|
|
@@ -333,7 +314,7 @@ export async function handleUndo(
|
|
|
333
314
|
});
|
|
334
315
|
return;
|
|
335
316
|
}
|
|
336
|
-
await undoFollow(db, objectId, actor, recipient);
|
|
317
|
+
await undoFollow(db, objectId, actor, recipient, baseUrl);
|
|
337
318
|
} else if (typeIncludes(objectType, "Like")) {
|
|
338
319
|
await undoLike(db, objectId, activityObject, actor);
|
|
339
320
|
} else if (typeIncludes(objectType, "Announce")) {
|
|
@@ -355,7 +336,16 @@ async function resolveUndoByActivityId(
|
|
|
355
336
|
objectId: string,
|
|
356
337
|
actor: string,
|
|
357
338
|
recipient: ActorRow | null,
|
|
339
|
+
baseUrl: string,
|
|
358
340
|
): Promise<boolean> {
|
|
341
|
+
const resolvedActivityId = await resolveInboundActivityReference(
|
|
342
|
+
db,
|
|
343
|
+
objectId,
|
|
344
|
+
actor,
|
|
345
|
+
baseUrl,
|
|
346
|
+
);
|
|
347
|
+
if (!resolvedActivityId) return false;
|
|
348
|
+
|
|
359
349
|
const originalActivity = await db
|
|
360
350
|
.select({
|
|
361
351
|
apId: activities.apId,
|
|
@@ -364,19 +354,14 @@ async function resolveUndoByActivityId(
|
|
|
364
354
|
actorApId: activities.actorApId,
|
|
365
355
|
})
|
|
366
356
|
.from(activities)
|
|
367
|
-
.where(
|
|
368
|
-
or(
|
|
369
|
-
eq(activities.apId, objectId),
|
|
370
|
-
and(
|
|
371
|
-
eq(activities.direction, "inbound"),
|
|
372
|
-
sql`json_extract(${activities.rawJson}, '$.id') = ${objectId}`,
|
|
373
|
-
),
|
|
374
|
-
),
|
|
375
|
-
)
|
|
357
|
+
.where(eq(activities.apId, resolvedActivityId))
|
|
376
358
|
.get();
|
|
377
359
|
if (!originalActivity) return false;
|
|
378
360
|
|
|
379
|
-
if (
|
|
361
|
+
if (
|
|
362
|
+
originalActivity.actorApId &&
|
|
363
|
+
!isSameActivityPubActor(originalActivity.actorApId, actor)
|
|
364
|
+
) {
|
|
380
365
|
log.warn("Undo actor mismatch", {
|
|
381
366
|
event: "ap.undo.actor_mismatch",
|
|
382
367
|
actor,
|
|
@@ -399,12 +384,10 @@ async function resolveUndoByActivityId(
|
|
|
399
384
|
}
|
|
400
385
|
const follow = await findFollowByActivityId(db, originalActivity.apId);
|
|
401
386
|
if (follow) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
recipient.apId,
|
|
407
|
-
);
|
|
387
|
+
if (!authorizesFollowUndo(follow, actor, recipient.apId, objectId)) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
await undoFollowEdge(db, follow.followerApId, follow.followingApId);
|
|
408
391
|
}
|
|
409
392
|
return true;
|
|
410
393
|
}
|
|
@@ -457,29 +440,8 @@ async function undoFollowEdge(
|
|
|
457
440
|
db: Database,
|
|
458
441
|
followerApId: string,
|
|
459
442
|
followingApId: string,
|
|
460
|
-
recipientApId: string,
|
|
461
443
|
): Promise<void> {
|
|
462
|
-
|
|
463
|
-
await runBatch(db, [
|
|
464
|
-
db
|
|
465
|
-
.update(actors)
|
|
466
|
-
.set({ followerCount: sql`${actors.followerCount} - 1` })
|
|
467
|
-
.where(
|
|
468
|
-
and(
|
|
469
|
-
eq(actors.apId, recipientApId),
|
|
470
|
-
gt(actors.followerCount, 0),
|
|
471
|
-
acceptedEdgeExists,
|
|
472
|
-
),
|
|
473
|
-
),
|
|
474
|
-
db
|
|
475
|
-
.delete(follows)
|
|
476
|
-
.where(
|
|
477
|
-
and(
|
|
478
|
-
eq(follows.followerApId, followerApId),
|
|
479
|
-
eq(follows.followingApId, followingApId),
|
|
480
|
-
),
|
|
481
|
-
),
|
|
482
|
-
]);
|
|
444
|
+
await severFollowDirection(db, followerApId, followingApId);
|
|
483
445
|
}
|
|
484
446
|
|
|
485
447
|
async function undoFollow(
|
|
@@ -487,8 +449,14 @@ async function undoFollow(
|
|
|
487
449
|
objectId: string | null,
|
|
488
450
|
actor: string,
|
|
489
451
|
recipient: ActorRow,
|
|
452
|
+
baseUrl: string,
|
|
490
453
|
): Promise<void> {
|
|
491
|
-
const follow = objectId
|
|
454
|
+
const follow = objectId
|
|
455
|
+
? await findFollowByActivityId(db, objectId, {
|
|
456
|
+
actorApId: actor,
|
|
457
|
+
localBaseUrl: baseUrl,
|
|
458
|
+
})
|
|
459
|
+
: null;
|
|
492
460
|
|
|
493
461
|
// Bind the undo to the VERIFIED signer: an Undo(Follow) may only remove the
|
|
494
462
|
// SIGNER's own follow edge. The activity id is public (it appears in the
|
|
@@ -496,13 +464,10 @@ async function undoFollow(
|
|
|
496
464
|
// cross-actor forgery — an attacker severing a victim's follow + decrementing
|
|
497
465
|
// their followerCount. The bare-string Undo path already enforces this via
|
|
498
466
|
// resolveUndoByActivityId; the typed-object path must too.
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
followOwner: follow.followerApId,
|
|
504
|
-
activityId: objectId,
|
|
505
|
-
});
|
|
467
|
+
if (
|
|
468
|
+
follow &&
|
|
469
|
+
!authorizesFollowUndo(follow, actor, recipient.apId, objectId)
|
|
470
|
+
) {
|
|
506
471
|
return;
|
|
507
472
|
}
|
|
508
473
|
|
|
@@ -514,7 +479,7 @@ async function undoFollow(
|
|
|
514
479
|
// of permanently over-counting.
|
|
515
480
|
const followerApId = follow ? follow.followerApId : actor;
|
|
516
481
|
const followingApId = follow ? follow.followingApId : recipient.apId;
|
|
517
|
-
await undoFollowEdge(db, followerApId, followingApId
|
|
482
|
+
await undoFollowEdge(db, followerApId, followingApId);
|
|
518
483
|
}
|
|
519
484
|
|
|
520
485
|
/**
|