@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
|
@@ -5,17 +5,34 @@
|
|
|
5
5
|
* yurucommu_like_post, yurucommu_bookmark_post
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { and,
|
|
8
|
+
import { and, eq, gt, or } from "drizzle-orm";
|
|
9
9
|
import { sql } from "drizzle-orm";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
actors,
|
|
12
|
+
bookmarks,
|
|
13
|
+
objects,
|
|
14
|
+
runBatch,
|
|
15
|
+
type D1Statement,
|
|
16
|
+
} from "../../../db/index.ts";
|
|
17
|
+
import { objectApId } from "../../federation-helpers.ts";
|
|
11
18
|
import {
|
|
12
19
|
actorIsBlockedBy,
|
|
13
20
|
canViewerReadObjectFull,
|
|
14
21
|
} from "../../lib/post-visibility.ts";
|
|
22
|
+
import {
|
|
23
|
+
prepareObjectDeleteCascade,
|
|
24
|
+
purgeMediaBlobs,
|
|
25
|
+
} from "../posts/delete-cascade.ts";
|
|
15
26
|
import {
|
|
16
27
|
MAX_POST_CONTENT_LENGTH,
|
|
17
28
|
normalizeVisibility,
|
|
18
29
|
} from "../posts/transformers.ts";
|
|
30
|
+
import {
|
|
31
|
+
prepareCreatedPostFederation,
|
|
32
|
+
prepareDeletedPostFederation,
|
|
33
|
+
} from "../posts/federation.ts";
|
|
34
|
+
import { setPostLike } from "../posts/like-mutation.ts";
|
|
35
|
+
import { preparePostInsertStatements } from "../posts/post-helpers.ts";
|
|
19
36
|
import {
|
|
20
37
|
errAuth,
|
|
21
38
|
errNotFound,
|
|
@@ -28,9 +45,20 @@ import {
|
|
|
28
45
|
} from "../takos-tools-response.ts";
|
|
29
46
|
import type { Input, ToolContext } from "./types.ts";
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Tool results expose both a full `ap_id` and a compact `post_id`, so every
|
|
50
|
+
* tool mutation must accept either form. Keep the historical `/ap/notes/:id`
|
|
51
|
+
* candidate too: older versions minted tool posts under that non-canonical
|
|
52
|
+
* path, and their returned compact IDs must remain usable for cleanup.
|
|
53
|
+
*/
|
|
54
|
+
function toolPostWhereByIdOrApId(baseUrl: string, postId: string) {
|
|
55
|
+
const normalizedBase = baseUrl.replace(/\/+$/u, "");
|
|
56
|
+
return or(
|
|
57
|
+
eq(objects.apId, postId),
|
|
58
|
+
eq(objects.apId, objectApId(normalizedBase, postId)),
|
|
59
|
+
eq(objects.apId, `${normalizedBase}/ap/notes/${postId}`),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
34
62
|
|
|
35
63
|
export async function handleCreatePost(
|
|
36
64
|
c: ToolContext,
|
|
@@ -44,7 +72,11 @@ export async function handleCreatePost(
|
|
|
44
72
|
// Constrain visibility to the canonical enum (unknown → "public"), matching
|
|
45
73
|
// the web post route; a raw value would be invisible to every feed filter.
|
|
46
74
|
const visibility = normalizeVisibility(String(input.visibility || "public"));
|
|
47
|
-
const
|
|
75
|
+
const requestedInReplyTo = input.in_reply_to
|
|
76
|
+
? String(input.in_reply_to)
|
|
77
|
+
: null;
|
|
78
|
+
let inReplyTo: string | null = null;
|
|
79
|
+
let parentAuthor: string | null = null;
|
|
48
80
|
|
|
49
81
|
if (!content) return c.json(errRequired("Content"), 400);
|
|
50
82
|
// Enforce the same content cap as the canonical post route so this MCP path
|
|
@@ -62,9 +94,10 @@ export async function handleCreatePost(
|
|
|
62
94
|
// private-community), bumping that restricted parent's reply_count and exposing
|
|
63
95
|
// its existence via the stored in_reply_to (a privacy oracle), or reply to a
|
|
64
96
|
// parent whose author blocked the actor (block bypass + notification).
|
|
65
|
-
if (
|
|
97
|
+
if (requestedInReplyTo) {
|
|
66
98
|
const parent = await db
|
|
67
99
|
.select({
|
|
100
|
+
apId: objects.apId,
|
|
68
101
|
attributedTo: objects.attributedTo,
|
|
69
102
|
visibility: objects.visibility,
|
|
70
103
|
toJson: objects.toJson,
|
|
@@ -75,7 +108,7 @@ export async function handleCreatePost(
|
|
|
75
108
|
endTime: objects.endTime,
|
|
76
109
|
})
|
|
77
110
|
.from(objects)
|
|
78
|
-
.where(
|
|
111
|
+
.where(toolPostWhereByIdOrApId(c.env.APP_URL, requestedInReplyTo))
|
|
79
112
|
.get();
|
|
80
113
|
if (
|
|
81
114
|
!parent ||
|
|
@@ -87,54 +120,58 @@ export async function handleCreatePost(
|
|
|
87
120
|
404,
|
|
88
121
|
);
|
|
89
122
|
}
|
|
123
|
+
// Persist the resolved canonical/full AP identifier, never the compact
|
|
124
|
+
// caller input, so reply traversal and counter recomputation share one key.
|
|
125
|
+
inReplyTo = parent.apId;
|
|
126
|
+
parentAuthor = parent.attributedTo;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
const postId = crypto.randomUUID();
|
|
93
130
|
const now = new Date().toISOString();
|
|
94
|
-
const apId =
|
|
131
|
+
const apId = objectApId(c.env.APP_URL.replace(/\/+$/u, ""), postId);
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.update(objects)
|
|
129
|
-
.set({
|
|
130
|
-
replyCount: sql`(SELECT COUNT(*) FROM ${objects} WHERE ${objects.inReplyTo} = ${inReplyTo})`,
|
|
131
|
-
})
|
|
132
|
-
.where(eq(objects.apId, inReplyTo)),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
await (db as unknown as Batchable).batch(stmts);
|
|
133
|
+
const preparedFederation = await prepareCreatedPostFederation({
|
|
134
|
+
db,
|
|
135
|
+
env: c.env,
|
|
136
|
+
actorApId: actor.ap_id,
|
|
137
|
+
objectApId: apId,
|
|
138
|
+
content,
|
|
139
|
+
summary: null,
|
|
140
|
+
inReplyTo,
|
|
141
|
+
parentAuthor,
|
|
142
|
+
visibility,
|
|
143
|
+
community: null,
|
|
144
|
+
published: now,
|
|
145
|
+
});
|
|
146
|
+
const localStatements = preparePostInsertStatements(db, {
|
|
147
|
+
apId,
|
|
148
|
+
actorApId: actor.ap_id,
|
|
149
|
+
content,
|
|
150
|
+
summary: null,
|
|
151
|
+
attachments: undefined,
|
|
152
|
+
inReplyTo,
|
|
153
|
+
visibility,
|
|
154
|
+
communityId: null,
|
|
155
|
+
to: preparedFederation.to,
|
|
156
|
+
cc: preparedFederation.cc,
|
|
157
|
+
audience: preparedFederation.audience,
|
|
158
|
+
tags: preparedFederation.tags,
|
|
159
|
+
parentAuthor,
|
|
160
|
+
baseUrl: c.env.APP_URL,
|
|
161
|
+
now,
|
|
162
|
+
});
|
|
163
|
+
await runBatch(db, [...localStatements, ...preparedFederation.statements]);
|
|
164
|
+
const { mentionFailures } = await preparedFederation.complete();
|
|
136
165
|
|
|
137
|
-
return c.json(
|
|
166
|
+
return c.json(
|
|
167
|
+
ok({
|
|
168
|
+
post_id: postId,
|
|
169
|
+
ap_id: apId,
|
|
170
|
+
...(mentionFailures.length > 0
|
|
171
|
+
? { mention_processing: { failed_count: mentionFailures.length } }
|
|
172
|
+
: {}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
138
175
|
}
|
|
139
176
|
|
|
140
177
|
export async function handleDeletePost(
|
|
@@ -149,9 +186,21 @@ export async function handleDeletePost(
|
|
|
149
186
|
if (!postId) return c.json(errRequired("Post ID"), 400);
|
|
150
187
|
|
|
151
188
|
const post = await db
|
|
152
|
-
.select({
|
|
189
|
+
.select({
|
|
190
|
+
apId: objects.apId,
|
|
191
|
+
inReplyTo: objects.inReplyTo,
|
|
192
|
+
visibility: objects.visibility,
|
|
193
|
+
communityApId: objects.communityApId,
|
|
194
|
+
toJson: objects.toJson,
|
|
195
|
+
ccJson: objects.ccJson,
|
|
196
|
+
})
|
|
153
197
|
.from(objects)
|
|
154
|
-
.where(
|
|
198
|
+
.where(
|
|
199
|
+
and(
|
|
200
|
+
toolPostWhereByIdOrApId(c.env.APP_URL, postId),
|
|
201
|
+
eq(objects.attributedTo, actor.ap_id),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
155
204
|
.get();
|
|
156
205
|
if (!post) {
|
|
157
206
|
return c.json(
|
|
@@ -163,25 +212,61 @@ export async function handleDeletePost(
|
|
|
163
212
|
);
|
|
164
213
|
}
|
|
165
214
|
|
|
215
|
+
const [cascade, preparedFederation] = await Promise.all([
|
|
216
|
+
prepareObjectDeleteCascade(db, post.apId, c.env.MEDIA),
|
|
217
|
+
prepareDeletedPostFederation({
|
|
218
|
+
db,
|
|
219
|
+
env: c.env,
|
|
220
|
+
actorApId: actor.ap_id,
|
|
221
|
+
post,
|
|
222
|
+
}),
|
|
223
|
+
]);
|
|
224
|
+
|
|
166
225
|
// #COUNTER-SYM: gate the decrement on the object STILL existing (correlated
|
|
167
226
|
// EXISTS) and run it BEFORE the delete, so two concurrent/retried deletes of
|
|
168
227
|
// the same post can't double-decrement postCount — the second batch's EXISTS
|
|
169
228
|
// is false (the first already deleted the row) → its -1 matches 0 rows. gt(>0)
|
|
170
229
|
// guards underflow. Mirrors the canonical web delete path (posts/routes.ts).
|
|
171
|
-
const objectExists = sql`EXISTS (SELECT 1 FROM ${objects} WHERE ${objects.apId} = ${
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
230
|
+
const objectExists = sql`EXISTS (SELECT 1 FROM ${objects} WHERE ${objects.apId} = ${post.apId})`;
|
|
231
|
+
const ops: D1Statement[] = [...cascade.statements];
|
|
232
|
+
// Direct messages never increment postCount, so their deletion must not
|
|
233
|
+
// decrement it. This keeps the tool path symmetric with canonical post/DM
|
|
234
|
+
// creation and deletion.
|
|
235
|
+
if (post.visibility !== "direct") {
|
|
236
|
+
ops.push(
|
|
237
|
+
db
|
|
238
|
+
.update(actors)
|
|
239
|
+
.set({ postCount: sql`${actors.postCount} - 1` })
|
|
240
|
+
.where(
|
|
241
|
+
and(
|
|
242
|
+
eq(actors.apId, actor.ap_id),
|
|
243
|
+
gt(actors.postCount, 0),
|
|
244
|
+
objectExists,
|
|
245
|
+
),
|
|
246
|
+
) as D1Statement,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
ops.push(...preparedFederation.statements);
|
|
250
|
+
ops.push(
|
|
251
|
+
db.delete(objects).where(eq(objects.apId, post.apId)) as D1Statement,
|
|
252
|
+
);
|
|
253
|
+
if (post.inReplyTo) {
|
|
254
|
+
const parentId = post.inReplyTo;
|
|
255
|
+
ops.push(
|
|
256
|
+
db
|
|
257
|
+
.update(objects)
|
|
258
|
+
.set({
|
|
259
|
+
replyCount: sql`(SELECT COUNT(*) FROM ${objects} WHERE ${objects.inReplyTo} = ${parentId})`,
|
|
260
|
+
})
|
|
261
|
+
.where(eq(objects.apId, parentId)) as D1Statement,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
await runBatch(db, ops as [D1Statement, ...D1Statement[]]);
|
|
265
|
+
|
|
266
|
+
// The irreversible external delete is last: failure leaks a blob instead of
|
|
267
|
+
// leaving a live post whose media has already been destroyed.
|
|
268
|
+
await purgeMediaBlobs(c.env.MEDIA, cascade.mediaKeys);
|
|
269
|
+
await preparedFederation.complete();
|
|
185
270
|
|
|
186
271
|
return c.json(ok({ deleted: true }));
|
|
187
272
|
}
|
|
@@ -216,54 +301,34 @@ export async function handleLikePost(
|
|
|
216
301
|
endTime: objects.endTime,
|
|
217
302
|
})
|
|
218
303
|
.from(objects)
|
|
219
|
-
.where(
|
|
304
|
+
.where(toolPostWhereByIdOrApId(c.env.APP_URL, postId))
|
|
220
305
|
.get();
|
|
221
306
|
// Block-gate too (the read-gate passes for any public post, so it alone does
|
|
222
307
|
// not stop a blocked actor): an actor the author blocked must not bump the
|
|
223
308
|
// author's like_count, mirroring the canonical like route (interactions.ts).
|
|
309
|
+
if (!post) {
|
|
310
|
+
return c.json(errNotFound("Post"), 404);
|
|
311
|
+
}
|
|
312
|
+
// Creating an interaction requires current read/moderation entitlement.
|
|
313
|
+
// Removing a retained interaction stays available by id after access is
|
|
314
|
+
// revoked, matching the canonical web unlike cleanup path.
|
|
224
315
|
if (
|
|
225
|
-
|
|
226
|
-
!(await canViewerReadObjectFull(db, post, actor.ap_id)) ||
|
|
227
|
-
|
|
316
|
+
likeActive &&
|
|
317
|
+
(!(await canViewerReadObjectFull(db, post, actor.ap_id)) ||
|
|
318
|
+
(await actorIsBlockedBy(db, post.attributedTo, actor.ap_id)))
|
|
228
319
|
) {
|
|
229
320
|
return c.json(errNotFound("Post"), 404);
|
|
230
321
|
}
|
|
231
322
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
.values({ actorApId: actor.ap_id, objectApId: post.apId })
|
|
240
|
-
.onConflictDoNothing()
|
|
241
|
-
: db
|
|
242
|
-
.delete(likes)
|
|
243
|
-
.where(
|
|
244
|
-
and(
|
|
245
|
-
eq(likes.actorApId, actor.ap_id),
|
|
246
|
-
eq(likes.objectApId, post.apId),
|
|
247
|
-
),
|
|
248
|
-
);
|
|
249
|
-
await (db as unknown as Batchable).batch([
|
|
250
|
-
toggleStmt,
|
|
251
|
-
db
|
|
252
|
-
.update(objects)
|
|
253
|
-
.set({
|
|
254
|
-
likeCount: sql`(SELECT COUNT(*) FROM ${likes} WHERE ${likes.objectApId} = ${post.apId})`,
|
|
255
|
-
})
|
|
256
|
-
.where(eq(objects.apId, post.apId)),
|
|
257
|
-
]);
|
|
323
|
+
const result = await setPostLike({
|
|
324
|
+
db,
|
|
325
|
+
env: c.env,
|
|
326
|
+
actorApId: actor.ap_id,
|
|
327
|
+
post,
|
|
328
|
+
active: likeActive,
|
|
329
|
+
});
|
|
258
330
|
|
|
259
|
-
|
|
260
|
-
.select({ count: count() })
|
|
261
|
-
.from(likes)
|
|
262
|
-
.where(eq(likes.objectApId, post.apId))
|
|
263
|
-
.get();
|
|
264
|
-
const likeCount = likeCountResult?.count ?? 0;
|
|
265
|
-
|
|
266
|
-
return c.json(ok({ liked: likeActive, like_count: likeCount }));
|
|
331
|
+
return c.json(ok({ liked: result.active, like_count: result.likeCount }));
|
|
267
332
|
}
|
|
268
333
|
|
|
269
334
|
export async function handleBookmarkPost(
|
|
@@ -280,12 +345,30 @@ export async function handleBookmarkPost(
|
|
|
280
345
|
if (!postId) return c.json(errRequired("Post ID"), 400);
|
|
281
346
|
|
|
282
347
|
const post = await db
|
|
283
|
-
.select({
|
|
348
|
+
.select({
|
|
349
|
+
apId: objects.apId,
|
|
350
|
+
visibility: objects.visibility,
|
|
351
|
+
attributedTo: objects.attributedTo,
|
|
352
|
+
toJson: objects.toJson,
|
|
353
|
+
ccJson: objects.ccJson,
|
|
354
|
+
audienceJson: objects.audienceJson,
|
|
355
|
+
communityApId: objects.communityApId,
|
|
356
|
+
type: objects.type,
|
|
357
|
+
endTime: objects.endTime,
|
|
358
|
+
})
|
|
284
359
|
.from(objects)
|
|
285
|
-
.where(
|
|
360
|
+
.where(toolPostWhereByIdOrApId(c.env.APP_URL, postId))
|
|
286
361
|
.get();
|
|
287
362
|
if (!post) return c.json(errNotFound("Post"), 404);
|
|
288
363
|
|
|
364
|
+
if (
|
|
365
|
+
bookmark &&
|
|
366
|
+
(!(await canViewerReadObjectFull(db, post, actor.ap_id)) ||
|
|
367
|
+
(await actorIsBlockedBy(db, post.attributedTo, actor.ap_id)))
|
|
368
|
+
) {
|
|
369
|
+
return c.json(errNotFound("Post"), 404);
|
|
370
|
+
}
|
|
371
|
+
|
|
289
372
|
await togglePostRelation(db, bookmarks, actor.ap_id, post.apId, bookmark);
|
|
290
373
|
|
|
291
374
|
return c.json(ok({ bookmarked: bookmark }));
|
|
@@ -10,7 +10,8 @@ import { sql } from "drizzle-orm";
|
|
|
10
10
|
import { actors, objects } from "../../../db/index.ts";
|
|
11
11
|
|
|
12
12
|
import { NO_AUDIENCE_PREDICATE } from "../../lib/community-visibility.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { excludeModeratedActors } from "../../lib/feed-exclude.ts";
|
|
14
|
+
import { operatorActorNotBlockedSql } from "../../lib/blocklist.ts";
|
|
14
15
|
import { extractHashtags } from "../posts/transformers.ts";
|
|
15
16
|
import { postContentSearchPredicate } from "../search.ts";
|
|
16
17
|
import {
|
|
@@ -79,6 +80,7 @@ async function searchUsers(c: ToolContext, input: Input) {
|
|
|
79
80
|
// otherwise a scrubbed `deleted-<id>` tombstone can surface here.
|
|
80
81
|
isNull(actors.deletedAt),
|
|
81
82
|
eq(actors.isPrivate, 0),
|
|
83
|
+
operatorActorNotBlockedSql(sql`${actors.apId}`),
|
|
82
84
|
or(
|
|
83
85
|
sql`instr(lower(${actors.preferredUsername}), lower(${query})) > 0`,
|
|
84
86
|
sql`instr(lower(${actors.name}), lower(${query})) > 0`,
|
|
@@ -124,8 +126,8 @@ async function searchPosts(
|
|
|
124
126
|
NO_AUDIENCE_PREDICATE,
|
|
125
127
|
isNull(objects.deletedAt),
|
|
126
128
|
// Honor the caller's block/mute filter, matching the web search +
|
|
127
|
-
// feed surfaces
|
|
128
|
-
|
|
129
|
+
// feed surfaces, plus the global operator defederation boundary.
|
|
130
|
+
excludeModeratedActors(actor?.ap_id ?? ""),
|
|
129
131
|
),
|
|
130
132
|
)
|
|
131
133
|
.orderBy(desc(objects.published), desc(objects.apId))
|
|
@@ -158,6 +160,7 @@ async function getTrending(c: ToolContext, input: Input) {
|
|
|
158
160
|
eq(objects.visibility, "public"),
|
|
159
161
|
NO_AUDIENCE_PREDICATE,
|
|
160
162
|
isNull(objects.deletedAt),
|
|
163
|
+
operatorActorNotBlockedSql(sql`${objects.attributedTo}`),
|
|
161
164
|
sql`${objects.published} > ${sinceDate}`,
|
|
162
165
|
),
|
|
163
166
|
)
|
|
@@ -206,7 +209,13 @@ async function getUserProfile(
|
|
|
206
209
|
isPrivate: actors.isPrivate,
|
|
207
210
|
})
|
|
208
211
|
.from(actors)
|
|
209
|
-
.where(
|
|
212
|
+
.where(
|
|
213
|
+
and(
|
|
214
|
+
eq(actors.preferredUsername, username),
|
|
215
|
+
isNull(actors.deletedAt),
|
|
216
|
+
operatorActorNotBlockedSql(sql`${actors.apId}`),
|
|
217
|
+
),
|
|
218
|
+
)
|
|
210
219
|
.get();
|
|
211
220
|
|
|
212
221
|
if (!actorRecord) return c.json(errNotFound("User"), 404);
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
|
|
8
|
-
import { actors, inbox, objects } from "../../../db/index.ts";
|
|
8
|
+
import { activities, actors, inbox, objects } from "../../../db/index.ts";
|
|
9
9
|
import { NO_AUDIENCE_PREDICATE } from "../../lib/community-visibility.ts";
|
|
10
10
|
import { encodeFeedCursor, feedCursorWhere } from "../../lib/feed-cursor.ts";
|
|
11
|
+
import { excludeModeratedActors } from "../../lib/feed-exclude.ts";
|
|
12
|
+
import { notificationEligibilityWhere } from "../../lib/notification-eligibility.ts";
|
|
11
13
|
import {
|
|
12
14
|
ACTOR_SUMMARY_COLUMNS,
|
|
13
15
|
errAuth,
|
|
@@ -20,7 +22,7 @@ import type { Input, ToolContext } from "./types.ts";
|
|
|
20
22
|
export async function handleGetTimeline(
|
|
21
23
|
c: ToolContext,
|
|
22
24
|
input: Input,
|
|
23
|
-
|
|
25
|
+
actor: { ap_id: string } | null,
|
|
24
26
|
) {
|
|
25
27
|
const db = c.get("db");
|
|
26
28
|
const limit = toolLimit(input.limit, 20, 50);
|
|
@@ -30,6 +32,7 @@ export async function handleGetTimeline(
|
|
|
30
32
|
eq(objects.visibility, "public"),
|
|
31
33
|
NO_AUDIENCE_PREDICATE,
|
|
32
34
|
isNull(objects.deletedAt),
|
|
35
|
+
excludeModeratedActors(actor?.ap_id ?? ""),
|
|
33
36
|
];
|
|
34
37
|
// Composite (published, apId) cursor so agent pagination doesn't skip posts
|
|
35
38
|
// sharing a published millisecond (see lib/feed-cursor.ts).
|
|
@@ -95,35 +98,40 @@ export async function handleGetNotifications(
|
|
|
95
98
|
const limit = toolLimit(input.limit, 20, 50);
|
|
96
99
|
const unreadOnly = Boolean(input.unread_only);
|
|
97
100
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
// Apply the same eligibility owner as the web list/badge/realtime/push paths
|
|
102
|
+
// in SQL before the limit. A post-query filter both drifted from moderation
|
|
103
|
+
// policy and let blocked rows consume the agent's page budget.
|
|
104
|
+
const inboxEntries = await db
|
|
105
|
+
.select({
|
|
106
|
+
activityApId: inbox.activityApId,
|
|
107
|
+
activityType: activities.type,
|
|
108
|
+
activityActorApId: activities.actorApId,
|
|
109
|
+
activityObjectApId: activities.objectApId,
|
|
110
|
+
read: inbox.read,
|
|
111
|
+
createdAt: inbox.createdAt,
|
|
112
|
+
})
|
|
113
|
+
.from(inbox)
|
|
114
|
+
.innerJoin(activities, eq(inbox.activityApId, activities.apId))
|
|
115
|
+
.leftJoin(objects, eq(activities.objectApId, objects.apId))
|
|
116
|
+
.where(
|
|
117
|
+
and(
|
|
118
|
+
eq(inbox.actorApId, actor.ap_id),
|
|
119
|
+
...(unreadOnly ? [eq(inbox.read, 0)] : []),
|
|
120
|
+
...notificationEligibilityWhere(db, actor.ap_id, {
|
|
121
|
+
direct: "exclude",
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
.orderBy(desc(inbox.createdAt))
|
|
126
|
+
.limit(limit);
|
|
119
127
|
|
|
120
128
|
return c.json(
|
|
121
129
|
ok({
|
|
122
|
-
notifications:
|
|
130
|
+
notifications: inboxEntries.map((entry) => ({
|
|
123
131
|
id: entry.activityApId,
|
|
124
|
-
type: entry.
|
|
125
|
-
from_actor: entry.
|
|
126
|
-
object: entry.
|
|
132
|
+
type: entry.activityType.toLowerCase(),
|
|
133
|
+
from_actor: entry.activityActorApId,
|
|
134
|
+
object: entry.activityObjectApId,
|
|
127
135
|
read: !!entry.read,
|
|
128
136
|
created_at: entry.createdAt,
|
|
129
137
|
})),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* input validation, and common data operations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { and, eq, inArray } from "drizzle-orm";
|
|
8
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
9
9
|
import type { Database } from "../../db/index.ts";
|
|
10
10
|
import { actors, bookmarks, follows, likes } from "../../db/index.ts";
|
|
11
11
|
import {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseLimit,
|
|
14
14
|
safeJsonParse,
|
|
15
15
|
} from "../federation-helpers.ts";
|
|
16
|
+
import { operatorActorNotBlockedSql } from "../lib/blocklist.ts";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Types
|
|
@@ -77,7 +78,7 @@ export function ok(data: unknown): ToolResponse {
|
|
|
77
78
|
export function formatActorSummary(a: ActorSummary): Record<string, unknown> {
|
|
78
79
|
return {
|
|
79
80
|
ap_id: a.apId,
|
|
80
|
-
username: formatUsername(a.apId),
|
|
81
|
+
username: formatUsername(a.apId, a.preferredUsername),
|
|
81
82
|
preferred_username: a.preferredUsername,
|
|
82
83
|
name: a.name,
|
|
83
84
|
icon_url: a.iconUrl,
|
|
@@ -162,6 +163,9 @@ export async function fetchFollowList(
|
|
|
162
163
|
targetApId,
|
|
163
164
|
),
|
|
164
165
|
eq(follows.status, "accepted"),
|
|
166
|
+
operatorActorNotBlockedSql(
|
|
167
|
+
sql`${isFollowers ? follows.followerApId : follows.followingApId}`,
|
|
168
|
+
),
|
|
165
169
|
),
|
|
166
170
|
)
|
|
167
171
|
.limit(limit);
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
safeJsonParse,
|
|
25
25
|
} from "../federation-helpers.ts";
|
|
26
26
|
import { CacheTags, CacheTTL, withCache } from "../middleware/cache.ts";
|
|
27
|
-
import {
|
|
27
|
+
import { excludeModeratedActors } from "../lib/feed-exclude.ts";
|
|
28
28
|
import { chunkForInClause } from "../lib/chunk.ts";
|
|
29
29
|
|
|
30
30
|
// Match every other feed's page cap. Clamping to 90 (not 100) leaves headroom
|
|
@@ -302,7 +302,7 @@ function formatPost(
|
|
|
302
302
|
type: p.type,
|
|
303
303
|
author: {
|
|
304
304
|
ap_id: p.attributedTo,
|
|
305
|
-
username: formatUsername(p.attributedTo),
|
|
305
|
+
username: formatUsername(p.attributedTo, author.preferredUsername),
|
|
306
306
|
preferred_username: author.preferredUsername,
|
|
307
307
|
name: author.name,
|
|
308
308
|
icon_url: author.iconUrl,
|
|
@@ -475,7 +475,7 @@ async function handleCommunityTimeline(
|
|
|
475
475
|
return c.json({ error: "Not a community member" }, 403);
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
const excludeAuthors =
|
|
478
|
+
const excludeAuthors = excludeModeratedActors(viewerApId);
|
|
479
479
|
|
|
480
480
|
// This is the home "narrow to a community" filter: it shows the slice of the
|
|
481
481
|
// viewer's reach that belongs to this community — (1) posts deliberately
|
|
@@ -565,7 +565,7 @@ timeline.get(
|
|
|
565
565
|
const before = c.req.query("before");
|
|
566
566
|
const viewerApId = actor?.ap_id || "";
|
|
567
567
|
|
|
568
|
-
const excludeAuthors =
|
|
568
|
+
const excludeAuthors = excludeModeratedActors(viewerApId);
|
|
569
569
|
|
|
570
570
|
const base = [
|
|
571
571
|
eq(objects.type, "Note"),
|
|
@@ -702,7 +702,7 @@ timeline.get("/following", async (c) => {
|
|
|
702
702
|
const before = c.req.query("before");
|
|
703
703
|
const viewerApId = actor.ap_id;
|
|
704
704
|
|
|
705
|
-
const excludeAuthors =
|
|
705
|
+
const excludeAuthors = excludeModeratedActors(viewerApId);
|
|
706
706
|
|
|
707
707
|
// Own posts: all visibilities except direct
|
|
708
708
|
// Followed users' posts: public, unlisted, or followers visibility
|