@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
|
Binary file
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Env, Variables } from "../../types.ts";
|
|
3
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
|
+
import { communities, instanceActor } from "../../../db/index.ts";
|
|
5
|
+
import type { Database } from "../../../db/index.ts";
|
|
6
|
+
import { generateKeyPair } from "../../federation-helpers.ts";
|
|
7
|
+
import type { RemoteFetchSigner } from "../../lib/activitypub-actor-cache.ts";
|
|
8
|
+
import { logger } from "../../lib/logger.ts";
|
|
9
|
+
|
|
10
|
+
const log = logger.child({ component: "activitypub.query_helpers" });
|
|
11
|
+
|
|
12
|
+
export const INSTANCE_ACTOR_USERNAME = "community";
|
|
13
|
+
|
|
14
|
+
/** Columns of a community needed to serve its ActivityPub Group actor doc. */
|
|
15
|
+
export interface FederatedCommunityActor {
|
|
16
|
+
apId: string;
|
|
17
|
+
preferredUsername: string;
|
|
18
|
+
name: string;
|
|
19
|
+
summary: string | null;
|
|
20
|
+
iconUrl: string | null;
|
|
21
|
+
inbox: string;
|
|
22
|
+
outbox: string;
|
|
23
|
+
followersUrl: string;
|
|
24
|
+
publicKeyPem: string;
|
|
25
|
+
joinPolicy: string;
|
|
26
|
+
memberCount: number;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load a community that is servable over federation as a Group actor: it must
|
|
32
|
+
* exist, not be deleted, and be PUBLIC. A private community's existence /
|
|
33
|
+
* name / summary is members-only (mirroring the discovery-list gate), so it is
|
|
34
|
+
* NOT exposed as a federated actor — callers 404 a null result.
|
|
35
|
+
*/
|
|
36
|
+
export async function loadFederatedCommunity(
|
|
37
|
+
db: Database,
|
|
38
|
+
apId: string,
|
|
39
|
+
): Promise<FederatedCommunityActor | null> {
|
|
40
|
+
const row = await db
|
|
41
|
+
.select({
|
|
42
|
+
apId: communities.apId,
|
|
43
|
+
preferredUsername: communities.preferredUsername,
|
|
44
|
+
name: communities.name,
|
|
45
|
+
summary: communities.summary,
|
|
46
|
+
iconUrl: communities.iconUrl,
|
|
47
|
+
inbox: communities.inbox,
|
|
48
|
+
outbox: communities.outbox,
|
|
49
|
+
followersUrl: communities.followersUrl,
|
|
50
|
+
publicKeyPem: communities.publicKeyPem,
|
|
51
|
+
joinPolicy: communities.joinPolicy,
|
|
52
|
+
memberCount: communities.memberCount,
|
|
53
|
+
createdAt: communities.createdAt,
|
|
54
|
+
})
|
|
55
|
+
.from(communities)
|
|
56
|
+
.where(
|
|
57
|
+
and(
|
|
58
|
+
eq(communities.apId, apId),
|
|
59
|
+
isNull(communities.deletedAt),
|
|
60
|
+
eq(communities.visibility, "public"),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
.get();
|
|
64
|
+
return row ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type InstanceActorResult = {
|
|
68
|
+
apId: string;
|
|
69
|
+
preferredUsername: string;
|
|
70
|
+
name: string | null;
|
|
71
|
+
summary: string | null;
|
|
72
|
+
publicKeyPem: string;
|
|
73
|
+
privateKeyPem: string;
|
|
74
|
+
joinPolicy: string;
|
|
75
|
+
postingPolicy: string;
|
|
76
|
+
visibility: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Process-local in-flight lock for `getInstanceActor`. Two concurrent
|
|
81
|
+
* cold-start callers used to race: each would observe a missing row,
|
|
82
|
+
* each would generate its own RSA keypair, and the loser of the insert
|
|
83
|
+
* would simply drop its key. That is correct (because `onConflictDoNothing`
|
|
84
|
+
* keeps the winner's row) but wasteful — RSA generation is ~250ms.
|
|
85
|
+
*
|
|
86
|
+
* Cloudflare D1 does not support `SELECT FOR UPDATE`, so the synthetic
|
|
87
|
+
* advisory lock here is a single in-process Promise keyed by AP-ID. Across
|
|
88
|
+
* processes / D1 shards the race is still resolved by the database's
|
|
89
|
+
* conflict-do-nothing semantics, but the local lock keeps the hot path
|
|
90
|
+
* cheap when the same worker isolate receives a burst of concurrent
|
|
91
|
+
* requests.
|
|
92
|
+
*/
|
|
93
|
+
const inFlightInstanceActor = new Map<string, Promise<InstanceActorResult>>();
|
|
94
|
+
|
|
95
|
+
export async function getInstanceActor(
|
|
96
|
+
c: Context<{ Bindings: Env; Variables: Variables }>,
|
|
97
|
+
): Promise<InstanceActorResult> {
|
|
98
|
+
const db = c.get("db");
|
|
99
|
+
const baseUrl = c.env.APP_URL;
|
|
100
|
+
const apId = `${baseUrl}/ap/actor`;
|
|
101
|
+
|
|
102
|
+
// Hot path: row exists, no lock acquisition needed.
|
|
103
|
+
const existing = await db.query.instanceActor.findFirst({
|
|
104
|
+
where: eq(instanceActor.apId, apId),
|
|
105
|
+
});
|
|
106
|
+
if (existing) {
|
|
107
|
+
return {
|
|
108
|
+
apId: existing.apId,
|
|
109
|
+
preferredUsername: existing.preferredUsername,
|
|
110
|
+
name: existing.name,
|
|
111
|
+
summary: existing.summary,
|
|
112
|
+
publicKeyPem: existing.publicKeyPem,
|
|
113
|
+
privateKeyPem: existing.privateKeyPem,
|
|
114
|
+
joinPolicy: existing.joinPolicy,
|
|
115
|
+
postingPolicy: existing.postingPolicy,
|
|
116
|
+
visibility: existing.visibility,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Cold path: serialise lazy-create through the synthetic advisory lock.
|
|
121
|
+
const inflight = inFlightInstanceActor.get(apId);
|
|
122
|
+
if (inflight) return await inflight;
|
|
123
|
+
|
|
124
|
+
const promise = (async (): Promise<InstanceActorResult> => {
|
|
125
|
+
try {
|
|
126
|
+
// Re-check under lock. Some other in-isolate caller may have created
|
|
127
|
+
// the row between our initial findFirst and lock acquisition.
|
|
128
|
+
const racy = await db.query.instanceActor.findFirst({
|
|
129
|
+
where: eq(instanceActor.apId, apId),
|
|
130
|
+
});
|
|
131
|
+
if (racy) {
|
|
132
|
+
return {
|
|
133
|
+
apId: racy.apId,
|
|
134
|
+
preferredUsername: racy.preferredUsername,
|
|
135
|
+
name: racy.name,
|
|
136
|
+
summary: racy.summary,
|
|
137
|
+
publicKeyPem: racy.publicKeyPem,
|
|
138
|
+
privateKeyPem: racy.privateKeyPem,
|
|
139
|
+
joinPolicy: racy.joinPolicy,
|
|
140
|
+
postingPolicy: racy.postingPolicy,
|
|
141
|
+
visibility: racy.visibility,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// NO interactive transaction: the production runtime is Cloudflare D1,
|
|
146
|
+
// whose drizzle driver does not support `db.transaction()` (it throws),
|
|
147
|
+
// which previously made this cold path 500 on every call — the instance
|
|
148
|
+
// actor row could never be lazily created, breaking `/ap/actor` and any
|
|
149
|
+
// path that signs as the instance actor. The insert is idempotent on the
|
|
150
|
+
// natural primary key (`ap_id`) via `onConflictDoNothing`, so concurrent
|
|
151
|
+
// creators race safely: the loser's insert is a no-op and BOTH re-read
|
|
152
|
+
// the winner's row (and thus the winner's keypair) via the trailing
|
|
153
|
+
// findFirst — no transaction is needed for correctness here.
|
|
154
|
+
const { publicKeyPem, privateKeyPem } = await generateKeyPair();
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
await db
|
|
157
|
+
.insert(instanceActor)
|
|
158
|
+
.values({
|
|
159
|
+
apId,
|
|
160
|
+
preferredUsername: INSTANCE_ACTOR_USERNAME,
|
|
161
|
+
name: "Yurucommu",
|
|
162
|
+
summary: "Yurucommu Community",
|
|
163
|
+
publicKeyPem,
|
|
164
|
+
privateKeyPem,
|
|
165
|
+
joinPolicy: "open",
|
|
166
|
+
postingPolicy: "members",
|
|
167
|
+
visibility: "public",
|
|
168
|
+
createdAt: now,
|
|
169
|
+
updatedAt: now,
|
|
170
|
+
})
|
|
171
|
+
.onConflictDoNothing({ target: instanceActor.apId });
|
|
172
|
+
|
|
173
|
+
const created = await db.query.instanceActor.findFirst({
|
|
174
|
+
where: eq(instanceActor.apId, apId),
|
|
175
|
+
});
|
|
176
|
+
if (!created) {
|
|
177
|
+
throw new Error("Failed to load instance actor after lazy-create");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
apId: created.apId,
|
|
182
|
+
preferredUsername: created.preferredUsername,
|
|
183
|
+
name: created.name,
|
|
184
|
+
summary: created.summary,
|
|
185
|
+
publicKeyPem: created.publicKeyPem,
|
|
186
|
+
privateKeyPem: created.privateKeyPem,
|
|
187
|
+
joinPolicy: created.joinPolicy,
|
|
188
|
+
postingPolicy: created.postingPolicy,
|
|
189
|
+
visibility: created.visibility,
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
log.error("getInstanceActor failed", {
|
|
193
|
+
event: "ap.instance_actor.lazy_create_failed",
|
|
194
|
+
apId,
|
|
195
|
+
error: err,
|
|
196
|
+
});
|
|
197
|
+
throw err;
|
|
198
|
+
} finally {
|
|
199
|
+
inFlightInstanceActor.delete(apId);
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
|
|
203
|
+
inFlightInstanceActor.set(apId, promise);
|
|
204
|
+
return await promise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the authorized-fetch signing identity from the instance actor. Used to
|
|
209
|
+
* HTTP-sign outbound actor/object GETs so secure-mode remotes serve the
|
|
210
|
+
* document. The `keyId` matches the `#main-key` fragment the served instance
|
|
211
|
+
* actor doc (`GET /ap/actor`) exposes, so the remote can fetch + verify it.
|
|
212
|
+
*/
|
|
213
|
+
export async function getInstanceFetchSigner(
|
|
214
|
+
c: Context<{ Bindings: Env; Variables: Variables }>,
|
|
215
|
+
): Promise<RemoteFetchSigner> {
|
|
216
|
+
const instance = await getInstanceActor(c);
|
|
217
|
+
return {
|
|
218
|
+
keyId: `${instance.apId}#main-key`,
|
|
219
|
+
privateKeyPem: instance.privateKeyPem,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** @internal Test-only helper for inspecting the synthetic lock. */
|
|
224
|
+
export const __instanceActorInternals = {
|
|
225
|
+
inflightSize: () => inFlightInstanceActor.size,
|
|
226
|
+
clear: () => inFlightInstanceActor.clear(),
|
|
227
|
+
};
|