@takosjp/yurucommu-core 3.0.0
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/LICENSE +16 -0
- package/README.md +82 -0
- package/migrations/0001_init.sql +495 -0
- package/migrations/0002_social_remote_actor_edges.sql +92 -0
- package/migrations/0003_activity_remote_object_edges.sql +68 -0
- package/migrations/0004_blocklist.sql +26 -0
- package/migrations/0005_story_community_scope.sql +13 -0
- package/migrations/0006_dm_community_read_status.sql +19 -0
- package/migrations/0007_moderation_reports.sql +22 -0
- package/migrations/0008_actor_fields_aka.sql +18 -0
- package/migrations/0009_object_tags.sql +13 -0
- package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
- package/migrations/0011_drop_remote_actor_fks.sql +205 -0
- package/migrations/0012_objects_content_fts.sql +39 -0
- package/migrations/0013_efficiency_indexes.sql +13 -0
- package/migrations/0014_inbox_actor_created_idx.sql +15 -0
- package/migrations/0015_community_bans.sql +16 -0
- package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
- package/migrations/0017_mobile_push_registrations.sql +22 -0
- package/migrations/README.md +122 -0
- package/package.json +75 -0
- package/packages/api/LICENSE +16 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/index.ts +4 -0
- package/packages/api/src/lib/api/account.ts +20 -0
- package/packages/api/src/lib/api/actors.ts +149 -0
- package/packages/api/src/lib/api/auth.ts +46 -0
- package/packages/api/src/lib/api/communities.ts +329 -0
- package/packages/api/src/lib/api/dm.test.ts +67 -0
- package/packages/api/src/lib/api/dm.ts +236 -0
- package/packages/api/src/lib/api/fetch.ts +111 -0
- package/packages/api/src/lib/api/follow.ts +30 -0
- package/packages/api/src/lib/api/media.ts +100 -0
- package/packages/api/src/lib/api/moderation.ts +98 -0
- package/packages/api/src/lib/api/normalize.ts +71 -0
- package/packages/api/src/lib/api/notifications.test.ts +63 -0
- package/packages/api/src/lib/api/notifications.ts +61 -0
- package/packages/api/src/lib/api/posts.test.ts +110 -0
- package/packages/api/src/lib/api/posts.ts +181 -0
- package/packages/api/src/lib/api/recommendations.ts +22 -0
- package/packages/api/src/lib/api/search.ts +88 -0
- package/packages/api/src/lib/api/stories.ts +80 -0
- package/packages/api/src/lib/api.ts +15 -0
- package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
- package/packages/api/src/lib/transport.ts +40 -0
- package/packages/api/src/social-server.ts +47 -0
- package/packages/api/src/types/index.ts +185 -0
- package/scripts/apply-takosumi-migrations.ts +621 -0
- package/src/backend/federation-helpers.ts +36 -0
- package/src/backend/index.ts +872 -0
- package/src/backend/lib/account-migration.ts +106 -0
- package/src/backend/lib/activitypub-actor-cache.ts +238 -0
- package/src/backend/lib/activitypub-helpers.ts +131 -0
- package/src/backend/lib/activitypub-validators.ts +323 -0
- package/src/backend/lib/ap-context.ts +16 -0
- package/src/backend/lib/ap-ids.ts +101 -0
- package/src/backend/lib/ap-response.ts +30 -0
- package/src/backend/lib/ap-signing.ts +87 -0
- package/src/backend/lib/ap-verify.ts +670 -0
- package/src/backend/lib/auth-lockout.ts +230 -0
- package/src/backend/lib/backend-paths.ts +34 -0
- package/src/backend/lib/base64.ts +30 -0
- package/src/backend/lib/blocklist-purge.ts +109 -0
- package/src/backend/lib/blocklist.ts +279 -0
- package/src/backend/lib/chunk.ts +33 -0
- package/src/backend/lib/client-ip.ts +169 -0
- package/src/backend/lib/community-visibility.ts +230 -0
- package/src/backend/lib/crypto.ts +424 -0
- package/src/backend/lib/delivery/circuit.ts +265 -0
- package/src/backend/lib/delivery/metrics.ts +30 -0
- package/src/backend/lib/delivery/planner.ts +190 -0
- package/src/backend/lib/delivery/queue-batching.ts +626 -0
- package/src/backend/lib/delivery/queue-delivery.ts +641 -0
- package/src/backend/lib/delivery/queue.ts +576 -0
- package/src/backend/lib/delivery/transformers.ts +56 -0
- package/src/backend/lib/delivery/types.ts +139 -0
- package/src/backend/lib/errors.ts +114 -0
- package/src/backend/lib/federation-fetch.ts +296 -0
- package/src/backend/lib/feed-cursor.ts +57 -0
- package/src/backend/lib/feed-exclude.ts +48 -0
- package/src/backend/lib/hex.ts +8 -0
- package/src/backend/lib/log-mask.ts +213 -0
- package/src/backend/lib/logger.ts +285 -0
- package/src/backend/lib/mobile-contract.ts +137 -0
- package/src/backend/lib/oauth-providers.ts +324 -0
- package/src/backend/lib/oauth-utils.ts +148 -0
- package/src/backend/lib/oidc-id-token.ts +151 -0
- package/src/backend/lib/parse-helpers.ts +31 -0
- package/src/backend/lib/post-visibility.ts +190 -0
- package/src/backend/lib/session-actor.ts +61 -0
- package/src/backend/lib/ssrf.ts +428 -0
- package/src/backend/lib/strip-image-metadata.ts +191 -0
- package/src/backend/middleware/bearer-auth.ts +70 -0
- package/src/backend/middleware/body-limit.ts +212 -0
- package/src/backend/middleware/cache.ts +429 -0
- package/src/backend/middleware/csrf.ts +130 -0
- package/src/backend/middleware/error-handler.ts +77 -0
- package/src/backend/middleware/rate-limit.ts +308 -0
- package/src/backend/public.ts +21 -0
- package/src/backend/routes/account-teardown.ts +430 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
- package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
- package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
- package/src/backend/routes/activitypub/inbox-types.ts +74 -0
- package/src/backend/routes/activitypub/inbox.ts +1191 -0
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub/query-helpers.ts +227 -0
- package/src/backend/routes/activitypub.ts +616 -0
- package/src/backend/routes/actors-helpers.ts +487 -0
- package/src/backend/routes/actors.ts +1311 -0
- package/src/backend/routes/apps.ts +313 -0
- package/src/backend/routes/auth-helpers.ts +566 -0
- package/src/backend/routes/auth.ts +615 -0
- package/src/backend/routes/communities/membership-invites.ts +208 -0
- package/src/backend/routes/communities/membership-join.ts +335 -0
- package/src/backend/routes/communities/membership-members.ts +539 -0
- package/src/backend/routes/communities/membership-requests.ts +296 -0
- package/src/backend/routes/communities/membership-shared.ts +364 -0
- package/src/backend/routes/communities/messages.ts +479 -0
- package/src/backend/routes/communities/routes.ts +624 -0
- package/src/backend/routes/communities.ts +21 -0
- package/src/backend/routes/dm/contacts.ts +525 -0
- package/src/backend/routes/dm/conversations-helpers.ts +197 -0
- package/src/backend/routes/dm/conversations.ts +25 -0
- package/src/backend/routes/dm/messages.ts +658 -0
- package/src/backend/routes/dm/query-helpers.ts +85 -0
- package/src/backend/routes/dm/read-archive.ts +228 -0
- package/src/backend/routes/dm/requests.ts +222 -0
- package/src/backend/routes/dm/typing.ts +81 -0
- package/src/backend/routes/dm.ts +15 -0
- package/src/backend/routes/follow-helpers.ts +370 -0
- package/src/backend/routes/follow.ts +588 -0
- package/src/backend/routes/media.ts +692 -0
- package/src/backend/routes/mobile.ts +159 -0
- package/src/backend/routes/moderation.ts +373 -0
- package/src/backend/routes/notifications.ts +757 -0
- package/src/backend/routes/posts/delete-cascade.ts +330 -0
- package/src/backend/routes/posts/interactions.ts +795 -0
- package/src/backend/routes/posts/post-helpers.ts +847 -0
- package/src/backend/routes/posts/queries.ts +537 -0
- package/src/backend/routes/posts/routes.ts +865 -0
- package/src/backend/routes/posts/transformers.ts +161 -0
- package/src/backend/routes/posts.ts +17 -0
- package/src/backend/routes/recommendations.ts +88 -0
- package/src/backend/routes/search.ts +730 -0
- package/src/backend/routes/stories/interactions.ts +576 -0
- package/src/backend/routes/stories/query-helpers.ts +482 -0
- package/src/backend/routes/stories/routes.ts +906 -0
- package/src/backend/routes/stories.ts +13 -0
- package/src/backend/routes/takos-tools/dm.ts +249 -0
- package/src/backend/routes/takos-tools/follows.ts +225 -0
- package/src/backend/routes/takos-tools/posts.ts +292 -0
- package/src/backend/routes/takos-tools/search.ts +228 -0
- package/src/backend/routes/takos-tools/timeline.ts +132 -0
- package/src/backend/routes/takos-tools/types.ts +10 -0
- package/src/backend/routes/takos-tools-response.ts +178 -0
- package/src/backend/routes/takos-tools.ts +153 -0
- package/src/backend/routes/timeline.ts +755 -0
- package/src/backend/runtime/bun.ts +620 -0
- package/src/backend/runtime/cloudflare.ts +202 -0
- package/src/backend/runtime/compat-bun/types.ts +44 -0
- package/src/backend/runtime/memory-kv.ts +104 -0
- package/src/backend/runtime/shared.ts +142 -0
- package/src/backend/runtime/types.ts +205 -0
- package/src/backend/server.ts +636 -0
- package/src/backend/types.ts +143 -0
- package/src/db/index.ts +97 -0
- package/src/db/schema/actors.ts +129 -0
- package/src/db/schema/communities.ts +133 -0
- package/src/db/schema/date-utils.ts +17 -0
- package/src/db/schema/index.ts +17 -0
- package/src/db/schema/messaging.ts +241 -0
- package/src/db/schema/mobile.ts +37 -0
- package/src/db/schema/posts.ts +150 -0
- package/src/db/schema/relations.ts +266 -0
- package/src/db/schema/reports.ts +33 -0
- package/src/db/schema/social.ts +106 -0
- package/src/db/schema/stories.ts +70 -0
- package/src/db/schema.ts +15 -0
- package/src/plugin/public.ts +7 -0
- package/src/runtime/site-worker.ts +10 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
|
+
import type { Env, Variables } from "../types.ts";
|
|
4
|
+
import type { Database } from "../../db/index.ts";
|
|
5
|
+
import {
|
|
6
|
+
activities,
|
|
7
|
+
actorCache,
|
|
8
|
+
actors,
|
|
9
|
+
follows,
|
|
10
|
+
inbox,
|
|
11
|
+
} from "../../db/index.ts";
|
|
12
|
+
import {
|
|
13
|
+
activityApId,
|
|
14
|
+
generateId,
|
|
15
|
+
isLocal,
|
|
16
|
+
isSafeRemoteUrl,
|
|
17
|
+
} from "../federation-helpers.ts";
|
|
18
|
+
import { enqueueDeliveryToActor } from "../lib/delivery/queue.ts";
|
|
19
|
+
import {
|
|
20
|
+
isUniqueConstraintError,
|
|
21
|
+
parseJsonObject,
|
|
22
|
+
parseNonEmptyString,
|
|
23
|
+
} from "../lib/parse-helpers.ts";
|
|
24
|
+
import { fetchAndUpsertActorCache } from "../lib/activitypub-actor-cache.ts";
|
|
25
|
+
import { actorIsBlockedBy } from "../lib/post-visibility.ts";
|
|
26
|
+
import { getInstanceFetchSigner } from "./activitypub/query-helpers.ts";
|
|
27
|
+
import { requireActor } from "./actors-helpers.ts";
|
|
28
|
+
import { logger } from "../lib/logger.ts";
|
|
29
|
+
|
|
30
|
+
const log = logger.child({ component: "follow.helpers" });
|
|
31
|
+
|
|
32
|
+
// `.batch` lives only on the concrete D1/libsql subclasses, not the Database
|
|
33
|
+
// union; reach it through a narrow structural cast (matching the other routes).
|
|
34
|
+
type Batchable = { batch: (stmts: unknown[]) => Promise<unknown> };
|
|
35
|
+
|
|
36
|
+
const REMOTE_FETCH_TIMEOUT_MS = 10000;
|
|
37
|
+
|
|
38
|
+
export type HonoContext = Context<{ Bindings: Env; Variables: Variables }>;
|
|
39
|
+
|
|
40
|
+
export type RemoteActor = {
|
|
41
|
+
id: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
preferredUsername?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
summary?: string;
|
|
46
|
+
icon?: { url?: string };
|
|
47
|
+
inbox?: string;
|
|
48
|
+
outbox?: string;
|
|
49
|
+
followers?: string;
|
|
50
|
+
following?: string;
|
|
51
|
+
endpoints?: { sharedInbox?: string };
|
|
52
|
+
publicKey?: { id?: string; publicKeyPem?: string };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type RequestContext = {
|
|
56
|
+
actor: { ap_id: string };
|
|
57
|
+
body: Record<string, unknown>;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
db: Database;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Pure helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export { isUniqueConstraintError, parseJsonObject, parseNonEmptyString };
|
|
67
|
+
|
|
68
|
+
export function parseStringArray(value: unknown): string[] | null {
|
|
69
|
+
if (!Array.isArray(value)) return null;
|
|
70
|
+
const parsed = value
|
|
71
|
+
.map((v) => parseNonEmptyString(v))
|
|
72
|
+
.filter((v): v is string => v !== null);
|
|
73
|
+
if (parsed.length !== value.length) return null;
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildApActivity(
|
|
78
|
+
type: string,
|
|
79
|
+
actorId: string,
|
|
80
|
+
object: unknown,
|
|
81
|
+
id: string,
|
|
82
|
+
): Record<string, unknown> {
|
|
83
|
+
return {
|
|
84
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
85
|
+
id,
|
|
86
|
+
type,
|
|
87
|
+
actor: actorId,
|
|
88
|
+
object,
|
|
89
|
+
published: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Request helpers
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extracts the authenticated actor and parsed JSON body from a request.
|
|
99
|
+
* Returns a Response on auth or parse failure, or the extracted context on success.
|
|
100
|
+
*/
|
|
101
|
+
export async function requireActorAndBody(
|
|
102
|
+
c: HonoContext,
|
|
103
|
+
): Promise<RequestContext | Response> {
|
|
104
|
+
const actor = requireActor(c);
|
|
105
|
+
if (actor instanceof Response) return actor;
|
|
106
|
+
const body = await parseJsonObject(c);
|
|
107
|
+
if (!body) {
|
|
108
|
+
return c.json({ error: "Invalid request body", code: "BAD_REQUEST" }, 400);
|
|
109
|
+
}
|
|
110
|
+
return { actor, body, baseUrl: c.env.APP_URL, db: c.get("db") };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isResponse(
|
|
114
|
+
value: RequestContext | Response,
|
|
115
|
+
): value is Response {
|
|
116
|
+
return value instanceof Response;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Activity helpers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates an outbound AP activity record and enqueues it for delivery.
|
|
125
|
+
*/
|
|
126
|
+
export async function createAndDeliverActivity(
|
|
127
|
+
env: Env,
|
|
128
|
+
db: Database,
|
|
129
|
+
baseUrl: string,
|
|
130
|
+
type: string,
|
|
131
|
+
actorId: string,
|
|
132
|
+
object: unknown,
|
|
133
|
+
recipientApId: string,
|
|
134
|
+
objectApId?: string | null,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const id = activityApId(baseUrl, generateId());
|
|
137
|
+
const activity = buildApActivity(type, actorId, object, id);
|
|
138
|
+
|
|
139
|
+
await db.insert(activities).values({
|
|
140
|
+
apId: id,
|
|
141
|
+
type,
|
|
142
|
+
actorApId: actorId,
|
|
143
|
+
objectApId: objectApId || undefined,
|
|
144
|
+
rawJson: JSON.stringify(activity),
|
|
145
|
+
direction: "outbound",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await enqueueDeliveryToActor(env, id, recipientApId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Delivers an Accept or Reject activity to a remote requester.
|
|
153
|
+
* No-op if the requester is local.
|
|
154
|
+
*/
|
|
155
|
+
export async function deliverResponseIfRemote(
|
|
156
|
+
env: Env,
|
|
157
|
+
db: Database,
|
|
158
|
+
baseUrl: string,
|
|
159
|
+
type: "Accept" | "Reject",
|
|
160
|
+
actorId: string,
|
|
161
|
+
requesterApId: string,
|
|
162
|
+
originalActivityApId: string | null,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (isLocal(requesterApId, baseUrl)) return;
|
|
165
|
+
await createAndDeliverActivity(
|
|
166
|
+
env,
|
|
167
|
+
db,
|
|
168
|
+
baseUrl,
|
|
169
|
+
type,
|
|
170
|
+
actorId,
|
|
171
|
+
originalActivityApId,
|
|
172
|
+
requesterApId,
|
|
173
|
+
originalActivityApId,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Database helpers
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Finds a pending follow request where `requesterApId` is trying to follow `targetApId`.
|
|
183
|
+
*/
|
|
184
|
+
export async function findPendingFollow(
|
|
185
|
+
db: Database,
|
|
186
|
+
requesterApId: string,
|
|
187
|
+
targetApId: string,
|
|
188
|
+
) {
|
|
189
|
+
return db
|
|
190
|
+
.select()
|
|
191
|
+
.from(follows)
|
|
192
|
+
.where(
|
|
193
|
+
and(
|
|
194
|
+
eq(follows.followerApId, requesterApId),
|
|
195
|
+
eq(follows.followingApId, targetApId),
|
|
196
|
+
eq(follows.status, "pending"),
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
.get();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Follow flow handlers
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export async function handleLocalFollow(
|
|
207
|
+
c: HonoContext,
|
|
208
|
+
db: Database,
|
|
209
|
+
baseUrl: string,
|
|
210
|
+
actor: { ap_id: string },
|
|
211
|
+
targetApId: string,
|
|
212
|
+
) {
|
|
213
|
+
const target = await db
|
|
214
|
+
.select({ isPrivate: actors.isPrivate })
|
|
215
|
+
.from(actors)
|
|
216
|
+
.where(eq(actors.apId, targetApId))
|
|
217
|
+
.get();
|
|
218
|
+
if (!target) return c.json({ error: "Target actor not found" }, 404);
|
|
219
|
+
|
|
220
|
+
// A blocked actor must not be able to follow / send a follow request to (and
|
|
221
|
+
// notify) the target who blocked them, nor re-establish a follow edge to a
|
|
222
|
+
// public account that blocked them. Same not-found shape so the block is not
|
|
223
|
+
// leaked; mirrors the story-like / DM block guard.
|
|
224
|
+
if (await actorIsBlockedBy(db, targetApId, actor.ap_id)) {
|
|
225
|
+
return c.json({ error: "Target actor not found" }, 404);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const status = target.isPrivate ? "pending" : "accepted";
|
|
229
|
+
const id = activityApId(baseUrl, generateId());
|
|
230
|
+
const now = new Date().toISOString();
|
|
231
|
+
const followActivity = buildApActivity("Follow", actor.ap_id, targetApId, id);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const followInsert = db.insert(follows).values({
|
|
235
|
+
followerApId: actor.ap_id,
|
|
236
|
+
followingApId: targetApId,
|
|
237
|
+
status,
|
|
238
|
+
activityApId: id,
|
|
239
|
+
acceptedAt: status === "accepted" ? now : null,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (status === "accepted") {
|
|
243
|
+
// Co-commit the edge insert + both increments in ONE batch so a crash
|
|
244
|
+
// between them can't leave the edge accepted with un-bumped counts (the
|
|
245
|
+
// retry would see the existing edge and skip the increment → permanent
|
|
246
|
+
// under-count). Increments guarded by NOT EXISTS(edge) — evaluated before
|
|
247
|
+
// the in-batch insert — so a concurrent duplicate can't double-count; the
|
|
248
|
+
// bare insert still throws on a true duplicate, rolling back the whole
|
|
249
|
+
// batch so the catch below returns the "Already following" 400.
|
|
250
|
+
const edgeAbsent = sql`NOT EXISTS (SELECT 1 FROM ${follows} WHERE ${follows.followerApId} = ${actor.ap_id} AND ${follows.followingApId} = ${targetApId})`;
|
|
251
|
+
await (db as unknown as Batchable).batch([
|
|
252
|
+
db
|
|
253
|
+
.update(actors)
|
|
254
|
+
.set({ followingCount: sql`${actors.followingCount} + 1` })
|
|
255
|
+
.where(and(eq(actors.apId, actor.ap_id), edgeAbsent)),
|
|
256
|
+
db
|
|
257
|
+
.update(actors)
|
|
258
|
+
.set({ followerCount: sql`${actors.followerCount} + 1` })
|
|
259
|
+
.where(and(eq(actors.apId, targetApId), edgeAbsent)),
|
|
260
|
+
followInsert,
|
|
261
|
+
]);
|
|
262
|
+
} else {
|
|
263
|
+
await followInsert;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await db.insert(activities).values({
|
|
267
|
+
apId: id,
|
|
268
|
+
type: "Follow",
|
|
269
|
+
actorApId: actor.ap_id,
|
|
270
|
+
objectApId: targetApId,
|
|
271
|
+
rawJson: JSON.stringify(followActivity),
|
|
272
|
+
direction: "local",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await db.insert(inbox).values({
|
|
276
|
+
actorApId: targetApId,
|
|
277
|
+
activityApId: id,
|
|
278
|
+
read: 0,
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (isUniqueConstraintError(error)) {
|
|
282
|
+
return c.json({ error: "Already following or pending" }, 400);
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return c.json({ success: true, status });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function handleRemoteFollow(
|
|
291
|
+
c: HonoContext,
|
|
292
|
+
db: Database,
|
|
293
|
+
baseUrl: string,
|
|
294
|
+
actor: { ap_id: string },
|
|
295
|
+
targetApId: string,
|
|
296
|
+
) {
|
|
297
|
+
if (!isSafeRemoteUrl(targetApId)) {
|
|
298
|
+
return c.json({ error: "Invalid target_ap_id" }, 400);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let cachedActorRow = await db
|
|
302
|
+
.select()
|
|
303
|
+
.from(actorCache)
|
|
304
|
+
.where(eq(actorCache.apId, targetApId))
|
|
305
|
+
.get();
|
|
306
|
+
|
|
307
|
+
if (!cachedActorRow) {
|
|
308
|
+
// Sign the actor GET as the instance actor so a remote running in
|
|
309
|
+
// authorized-fetch / secure mode (which 401s unsigned GETs) serves the
|
|
310
|
+
// document — otherwise following anyone on such an instance silently fails
|
|
311
|
+
// at resolution.
|
|
312
|
+
const result = await fetchAndUpsertActorCache(db, targetApId, {
|
|
313
|
+
timeout: REMOTE_FETCH_TIMEOUT_MS,
|
|
314
|
+
mode: "upsert",
|
|
315
|
+
signer: await getInstanceFetchSigner(c),
|
|
316
|
+
});
|
|
317
|
+
if (!result.ok) {
|
|
318
|
+
switch (result.reason) {
|
|
319
|
+
case "fetch_not_ok":
|
|
320
|
+
return c.json({ error: "Could not fetch remote actor" }, 400);
|
|
321
|
+
case "id_mismatch":
|
|
322
|
+
return c.json({ error: "Remote actor id mismatch" }, 400);
|
|
323
|
+
case "invalid_document":
|
|
324
|
+
case "missing_inbox":
|
|
325
|
+
case "missing_public_key":
|
|
326
|
+
return c.json({ error: "Invalid remote actor data" }, 400);
|
|
327
|
+
case "fetch_failed":
|
|
328
|
+
default:
|
|
329
|
+
return c.json({ error: "Failed to fetch remote actor" }, 400);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
cachedActorRow = result.row;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!cachedActorRow?.inbox || !isSafeRemoteUrl(cachedActorRow.inbox)) {
|
|
336
|
+
return c.json({ error: "Invalid inbox URL" }, 400);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const id = activityApId(baseUrl, generateId());
|
|
340
|
+
const followActivity = buildApActivity("Follow", actor.ap_id, targetApId, id);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
await db.insert(follows).values({
|
|
344
|
+
followerApId: actor.ap_id,
|
|
345
|
+
followingApId: targetApId,
|
|
346
|
+
status: "pending",
|
|
347
|
+
activityApId: id,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await db.insert(activities).values({
|
|
351
|
+
apId: id,
|
|
352
|
+
type: "Follow",
|
|
353
|
+
actorApId: actor.ap_id,
|
|
354
|
+
objectApId: targetApId,
|
|
355
|
+
rawJson: JSON.stringify(followActivity),
|
|
356
|
+
direction: "outbound",
|
|
357
|
+
});
|
|
358
|
+
} catch (e) {
|
|
359
|
+
log.error("Failed to create remote follow", {
|
|
360
|
+
event: "follow.remote.create_failed",
|
|
361
|
+
actor: actor.ap_id,
|
|
362
|
+
target: targetApId,
|
|
363
|
+
error: e,
|
|
364
|
+
});
|
|
365
|
+
return c.json({ error: "Failed to follow remote actor" }, 500);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await enqueueDeliveryToActor(c.env, id, targetApId);
|
|
369
|
+
return c.json({ success: true, status: "pending" });
|
|
370
|
+
}
|