@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,161 @@
|
|
|
1
|
+
import { formatUsername, safeJsonParse } from "../../federation-helpers.ts";
|
|
2
|
+
|
|
3
|
+
export const MAX_POST_CONTENT_LENGTH = 5000;
|
|
4
|
+
export const MAX_POST_SUMMARY_LENGTH = 500;
|
|
5
|
+
// Bound the attachments payload. PostAttachment is an open-ended record, so cap
|
|
6
|
+
// both the COUNT and the serialized SIZE — the size cap bounds row/federated-doc
|
|
7
|
+
// bloat regardless of internal shape (key count / field length). 16 KiB is ample
|
|
8
|
+
// for MAX_ATTACHMENTS media descriptors with alt text + blurhash.
|
|
9
|
+
export const MAX_ATTACHMENTS = 8;
|
|
10
|
+
export const MAX_ATTACHMENTS_JSON_LENGTH = 16 * 1024;
|
|
11
|
+
|
|
12
|
+
/** Truncate a string to `max` characters (no-op when already within bounds). */
|
|
13
|
+
export function truncate(s: string, max: number): string {
|
|
14
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Remote inbound Note fields are attacker-controlled and were stored verbatim
|
|
18
|
+
// (capped only by the 512 KiB inbox body limit), vs the ~5 KB local ceiling.
|
|
19
|
+
// Truncate to the same local bounds at ingest. These never reject (a 5xx would
|
|
20
|
+
// just make the peer retry); they store a bounded version.
|
|
21
|
+
export function boundInboundContent(content: unknown): string {
|
|
22
|
+
return typeof content === "string"
|
|
23
|
+
? truncate(content, MAX_POST_CONTENT_LENGTH)
|
|
24
|
+
: "";
|
|
25
|
+
}
|
|
26
|
+
export function boundInboundSummary(summary: unknown): string | null {
|
|
27
|
+
return typeof summary === "string" && summary.length > 0
|
|
28
|
+
? truncate(summary, MAX_POST_SUMMARY_LENGTH)
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
/** Drop an oversized inbound attachments blob to "[]" rather than store it. */
|
|
32
|
+
export function boundAttachmentsJson(json: string): string {
|
|
33
|
+
return json.length > MAX_ATTACHMENTS_JSON_LENGTH ? "[]" : json;
|
|
34
|
+
}
|
|
35
|
+
// Capped at 90 (not 100): a page's object ids are re-queried via
|
|
36
|
+
// `inArray(col, objectApIds)` for like/bookmark enrichment, and Cloudflare D1
|
|
37
|
+
// allows at most 100 bound parameters per query. 90 leaves headroom for the
|
|
38
|
+
// other bound params in those enrichment statements. (Tests run on libsql,
|
|
39
|
+
// whose ~32k ceiling hides this — see lib/blocklist.ts for the same constraint.)
|
|
40
|
+
export const MAX_POSTS_PAGE_LIMIT = 90;
|
|
41
|
+
|
|
42
|
+
export type PostRow = {
|
|
43
|
+
ap_id: string;
|
|
44
|
+
type: string;
|
|
45
|
+
attributed_to: string;
|
|
46
|
+
author_username: string | null;
|
|
47
|
+
author_name: string | null;
|
|
48
|
+
author_icon_url: string | null;
|
|
49
|
+
content: string;
|
|
50
|
+
summary: string | null;
|
|
51
|
+
attachments_json: string | null;
|
|
52
|
+
in_reply_to: string | null;
|
|
53
|
+
visibility: string;
|
|
54
|
+
community_ap_id: string | null;
|
|
55
|
+
like_count: number;
|
|
56
|
+
reply_count: number;
|
|
57
|
+
announce_count: number;
|
|
58
|
+
published: string;
|
|
59
|
+
updated?: string | null;
|
|
60
|
+
liked?: number | boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type FormattedPost = {
|
|
64
|
+
ap_id: string;
|
|
65
|
+
type: string;
|
|
66
|
+
author: {
|
|
67
|
+
ap_id: string;
|
|
68
|
+
username: string;
|
|
69
|
+
preferred_username: string | null;
|
|
70
|
+
name: string | null;
|
|
71
|
+
icon_url: string | null;
|
|
72
|
+
};
|
|
73
|
+
content: string;
|
|
74
|
+
summary: string | null;
|
|
75
|
+
attachments: unknown[];
|
|
76
|
+
in_reply_to: string | null;
|
|
77
|
+
visibility: string;
|
|
78
|
+
community_ap_id: string | null;
|
|
79
|
+
like_count: number;
|
|
80
|
+
reply_count: number;
|
|
81
|
+
announce_count: number;
|
|
82
|
+
published: string;
|
|
83
|
+
edited_at: string | null;
|
|
84
|
+
liked: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const MENTION_REGEX = /@([a-zA-Z0-9_]+(?:@[a-zA-Z0-9.-]+)?)/g;
|
|
88
|
+
|
|
89
|
+
export function extractMentions(content: string): string[] {
|
|
90
|
+
const matches = Array.from(content.matchAll(MENTION_REGEX), (m) => m[1]);
|
|
91
|
+
return [...new Set(matches)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Unicode-aware hashtag body (matches the web client tokenizer), so Japanese
|
|
95
|
+
// tags like #海の日 federate as AS2 Hashtag tags too.
|
|
96
|
+
const HASHTAG_REGEX = /#([\p{L}\p{N}_]+)/gu;
|
|
97
|
+
|
|
98
|
+
export function extractHashtags(content: string): string[] {
|
|
99
|
+
const matches = Array.from(content.matchAll(HASHTAG_REGEX), (m) => m[1]);
|
|
100
|
+
// De-duplicate case-insensitively, keeping first-seen casing.
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
const out: string[] = [];
|
|
103
|
+
for (const tag of matches) {
|
|
104
|
+
const key = tag.toLowerCase();
|
|
105
|
+
if (seen.has(key)) continue;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
out.push(tag);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatPost(
|
|
113
|
+
p: PostRow,
|
|
114
|
+
currentActorApId?: string,
|
|
115
|
+
): FormattedPost {
|
|
116
|
+
return {
|
|
117
|
+
ap_id: p.ap_id,
|
|
118
|
+
type: p.type,
|
|
119
|
+
author: {
|
|
120
|
+
ap_id: p.attributed_to,
|
|
121
|
+
username: formatUsername(p.attributed_to),
|
|
122
|
+
preferred_username: p.author_username,
|
|
123
|
+
name: p.author_name,
|
|
124
|
+
icon_url: p.author_icon_url,
|
|
125
|
+
},
|
|
126
|
+
content: p.content,
|
|
127
|
+
summary: p.summary,
|
|
128
|
+
attachments: safeJsonParse(p.attachments_json, []),
|
|
129
|
+
in_reply_to: p.in_reply_to,
|
|
130
|
+
visibility: p.visibility,
|
|
131
|
+
community_ap_id: p.community_ap_id,
|
|
132
|
+
like_count: p.like_count,
|
|
133
|
+
reply_count: p.reply_count,
|
|
134
|
+
announce_count: p.announce_count,
|
|
135
|
+
published: p.published,
|
|
136
|
+
// Surfaced only when the post was actually edited (see timeline.formatPost).
|
|
137
|
+
edited_at: p.updated && p.updated !== p.published ? p.updated : null,
|
|
138
|
+
liked: currentActorApId ? !!p.liked : false,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const VALID_VISIBILITIES = new Set([
|
|
143
|
+
"public",
|
|
144
|
+
"unlisted",
|
|
145
|
+
"followers",
|
|
146
|
+
"direct",
|
|
147
|
+
] as const);
|
|
148
|
+
|
|
149
|
+
export function normalizeVisibility(
|
|
150
|
+
value?: string,
|
|
151
|
+
): "public" | "unlisted" | "followers" | "direct" {
|
|
152
|
+
if (value === "private" || value === "followers_only") return "followers";
|
|
153
|
+
if (
|
|
154
|
+
VALID_VISIBILITIES.has(
|
|
155
|
+
value as "public" | "unlisted" | "followers" | "direct",
|
|
156
|
+
)
|
|
157
|
+
) {
|
|
158
|
+
return value as "public" | "unlisted" | "followers" | "direct";
|
|
159
|
+
}
|
|
160
|
+
return "public";
|
|
161
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Posts, Likes, and Bookmarks routes for Yurucommu backend
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import type { Env, Variables } from "../types.ts";
|
|
4
|
+
import baseRoutes from "./posts/routes.ts";
|
|
5
|
+
import interactionRoutes from "./posts/interactions.ts";
|
|
6
|
+
|
|
7
|
+
const posts = new Hono<{ Bindings: Env; Variables: Variables }>();
|
|
8
|
+
|
|
9
|
+
// Interaction routes FIRST: its only GET is the static `/bookmarks`, which would
|
|
10
|
+
// otherwise be shadowed by baseRoutes' `GET /:id` (matching id="bookmarks" → a
|
|
11
|
+
// 404 "post not found"). interactionRoutes never shadows a base route — its
|
|
12
|
+
// other routes are POST/DELETE `/:id/{like,repost,bookmark}` (two-segment), so a
|
|
13
|
+
// non-matching request falls through to baseRoutes' `/:id` / `/` / `/:id/replies`.
|
|
14
|
+
posts.route("/", interactionRoutes);
|
|
15
|
+
posts.route("/", baseRoutes);
|
|
16
|
+
|
|
17
|
+
export default posts;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import type { Env, Variables } from "../types.ts";
|
|
4
|
+
import { formatUsername } from "../federation-helpers.ts";
|
|
5
|
+
import { CacheTags, CacheTTL, withCache } from "../middleware/cache.ts";
|
|
6
|
+
import { batchLoadActorInfo } from "./communities/membership-shared.ts";
|
|
7
|
+
|
|
8
|
+
const recommendations = new Hono<{ Bindings: Env; Variables: Variables }>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/recommendations/users
|
|
12
|
+
*
|
|
13
|
+
* Returns recommended users based on friends-of-friends algorithm.
|
|
14
|
+
* Finds users followed by people the current user follows,
|
|
15
|
+
* ranked by number of mutual connections.
|
|
16
|
+
*/
|
|
17
|
+
recommendations.get(
|
|
18
|
+
"/users",
|
|
19
|
+
withCache({
|
|
20
|
+
ttl: CacheTTL.ACTOR_PROFILE,
|
|
21
|
+
varyByActor: true,
|
|
22
|
+
cacheTag: CacheTags.ACTOR,
|
|
23
|
+
}),
|
|
24
|
+
async (c) => {
|
|
25
|
+
const actor = c.get("actor");
|
|
26
|
+
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
27
|
+
|
|
28
|
+
const db = c.get("db");
|
|
29
|
+
const myApId = actor.ap_id;
|
|
30
|
+
|
|
31
|
+
const candidates = await db.all<{ ap_id: string; mutual_count: number }>(
|
|
32
|
+
sql`
|
|
33
|
+
SELECT f2.following_ap_id AS ap_id, COUNT(DISTINCT f2.follower_ap_id) AS mutual_count
|
|
34
|
+
FROM follows f1
|
|
35
|
+
JOIN follows f2 ON f1.following_ap_id = f2.follower_ap_id AND f2.status = 'accepted'
|
|
36
|
+
WHERE f1.follower_ap_id = ${myApId}
|
|
37
|
+
AND f1.status = 'accepted'
|
|
38
|
+
AND f2.following_ap_id != ${myApId}
|
|
39
|
+
AND f2.following_ap_id NOT IN (
|
|
40
|
+
SELECT following_ap_id FROM follows
|
|
41
|
+
WHERE follower_ap_id = ${myApId} AND status IN ('accepted', 'pending')
|
|
42
|
+
)
|
|
43
|
+
AND f2.following_ap_id NOT IN (
|
|
44
|
+
SELECT blocked_ap_id FROM blocks WHERE blocker_ap_id = ${myApId}
|
|
45
|
+
)
|
|
46
|
+
AND f2.following_ap_id NOT IN (
|
|
47
|
+
SELECT muted_ap_id FROM mutes WHERE muter_ap_id = ${myApId}
|
|
48
|
+
)
|
|
49
|
+
AND f2.following_ap_id NOT IN (
|
|
50
|
+
-- Hide deleted AND private/locked (is_private = 1) accounts. A locked
|
|
51
|
+
-- account opted out of discovery (discoverable:false), and every other
|
|
52
|
+
-- actor-discovery surface (search.actors, takos-tools searchUsers /
|
|
53
|
+
-- getUserProfile) excludes is_private = 1 — this friends-of-friends
|
|
54
|
+
-- panel must match or it leaks the locked account's handle/name/icon.
|
|
55
|
+
-- is_private lives only on the LOCAL actors table; remote candidates
|
|
56
|
+
-- (actorCache) are unaffected, same scope as the other surfaces.
|
|
57
|
+
SELECT ap_id FROM actors WHERE deleted_at IS NOT NULL OR is_private = 1
|
|
58
|
+
)
|
|
59
|
+
GROUP BY f2.following_ap_id
|
|
60
|
+
ORDER BY mutual_count DESC
|
|
61
|
+
LIMIT 5
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (candidates.length === 0) return c.json({ users: [] });
|
|
66
|
+
|
|
67
|
+
const actorMap = await batchLoadActorInfo(
|
|
68
|
+
db,
|
|
69
|
+
candidates.map((r) => r.ap_id),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const users = candidates.map((row) => {
|
|
73
|
+
const info = actorMap.get(row.ap_id);
|
|
74
|
+
return {
|
|
75
|
+
ap_id: row.ap_id,
|
|
76
|
+
preferred_username: info?.preferredUsername ?? null,
|
|
77
|
+
name: info?.name ?? null,
|
|
78
|
+
icon_url: info?.iconUrl ?? null,
|
|
79
|
+
username: formatUsername(row.ap_id),
|
|
80
|
+
mutual_count: Number(row.mutual_count),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return c.json({ users });
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export default recommendations;
|