@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,181 @@
|
|
|
1
|
+
import type { Post } from "../../types/index.ts";
|
|
2
|
+
import { normalizePost } from "./normalize.ts";
|
|
3
|
+
import { apiDelete, apiFetch, apiPatch, apiPost, assertOk } from "./fetch.ts";
|
|
4
|
+
|
|
5
|
+
type PostListResponse = {
|
|
6
|
+
posts?: Post[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type CreatePostResponse = {
|
|
10
|
+
post: Post;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TimelinePage = {
|
|
14
|
+
posts: Post[];
|
|
15
|
+
// Opaque composite cursor for the next page (`published\u0000apId`), echoed
|
|
16
|
+
// verbatim back to the server as `before`. Pass this — NOT a post's ap_id —
|
|
17
|
+
// to paginate: a bare ap_id decodes as a published-only cursor whose
|
|
18
|
+
// string comparison against an ISO timestamp matches every row, so the feed
|
|
19
|
+
// re-serves page 1 forever and never advances.
|
|
20
|
+
nextCursor: string | null;
|
|
21
|
+
hasMore: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function fetchTimeline(options?: {
|
|
25
|
+
limit?: number;
|
|
26
|
+
before?: string;
|
|
27
|
+
community?: string;
|
|
28
|
+
}): Promise<TimelinePage> {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
31
|
+
if (options?.before) params.set("before", options.before);
|
|
32
|
+
if (options?.community) params.set("community", options.community);
|
|
33
|
+
const query = params.toString() ? `?${params}` : "";
|
|
34
|
+
const res = await apiFetch(`/api/timeline${query}`);
|
|
35
|
+
// Throw on a non-OK response (the backend returns a JSON `{ error }` body, so
|
|
36
|
+
// without this res.json() would resolve, posts would be undefined, and the
|
|
37
|
+
// caller would render an empty "welcome" feed instead of an error+retry UI).
|
|
38
|
+
await assertOk(res, "Failed to load timeline");
|
|
39
|
+
const data = (await res.json()) as {
|
|
40
|
+
posts?: Post[];
|
|
41
|
+
next_cursor?: string | null;
|
|
42
|
+
has_more?: boolean;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
posts: (data.posts || []).map(normalizePost),
|
|
46
|
+
nextCursor: data.next_cursor ?? null,
|
|
47
|
+
hasMore: data.has_more ?? false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function fetchPost(apId: string): Promise<Post> {
|
|
52
|
+
const res = await apiFetch(`/api/posts/${encodeURIComponent(apId)}`);
|
|
53
|
+
await assertOk(res, "Post not found");
|
|
54
|
+
const data = (await res.json()) as { post: Post };
|
|
55
|
+
return normalizePost(data.post);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RepliesPage {
|
|
59
|
+
replies: Post[];
|
|
60
|
+
nextCursor: string | null;
|
|
61
|
+
hasMore: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function fetchReplies(
|
|
65
|
+
postApId: string,
|
|
66
|
+
options?: { before?: string },
|
|
67
|
+
): Promise<RepliesPage> {
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
if (options?.before) params.set("before", options.before);
|
|
70
|
+
const query = params.toString() ? `?${params}` : "";
|
|
71
|
+
const res = await apiFetch(
|
|
72
|
+
`/api/posts/${encodeURIComponent(postApId)}/replies${query}`,
|
|
73
|
+
);
|
|
74
|
+
const data = (await res.json()) as {
|
|
75
|
+
replies?: Post[];
|
|
76
|
+
next_cursor?: string | null;
|
|
77
|
+
has_more?: boolean;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
replies: (data.replies || []).map(normalizePost),
|
|
81
|
+
nextCursor: data.next_cursor ?? null,
|
|
82
|
+
hasMore: data.has_more ?? false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createPost(data: {
|
|
87
|
+
content: string;
|
|
88
|
+
summary?: string;
|
|
89
|
+
visibility?: string;
|
|
90
|
+
in_reply_to?: string;
|
|
91
|
+
community_ap_id?: string;
|
|
92
|
+
attachments?: {
|
|
93
|
+
url?: string;
|
|
94
|
+
r2_key: string;
|
|
95
|
+
content_type: string;
|
|
96
|
+
name?: string;
|
|
97
|
+
}[];
|
|
98
|
+
}): Promise<Post> {
|
|
99
|
+
const res = await apiPost("/api/posts", data);
|
|
100
|
+
await assertOk(res, "Failed to create post");
|
|
101
|
+
const result = (await res.json()) as CreatePostResponse;
|
|
102
|
+
return normalizePost(result.post);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Edit your own post's text and/or content warning. The server returns only the
|
|
106
|
+
// changed fields (it stays the authority on what actually persisted); the caller
|
|
107
|
+
// merges them into the post it already holds. `summary: null` clears the CW.
|
|
108
|
+
export async function editPost(
|
|
109
|
+
apId: string,
|
|
110
|
+
data: { content?: string; summary?: string | null },
|
|
111
|
+
): Promise<{ content: string; summary: string | null }> {
|
|
112
|
+
const res = await apiPatch(`/api/posts/${encodeURIComponent(apId)}`, data);
|
|
113
|
+
await assertOk(res, "Failed to edit post");
|
|
114
|
+
const result = (await res.json()) as {
|
|
115
|
+
post: { content: string; summary: string | null };
|
|
116
|
+
};
|
|
117
|
+
return { content: result.post.content, summary: result.post.summary };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function deletePost(apId: string): Promise<void> {
|
|
121
|
+
const res = await apiDelete(`/api/posts/${encodeURIComponent(apId)}`);
|
|
122
|
+
await assertOk(res, "Failed to delete post");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function likePost(apId: string): Promise<void> {
|
|
126
|
+
const res = await apiPost(`/api/posts/${encodeURIComponent(apId)}/like`);
|
|
127
|
+
await assertOk(res, "Failed to like");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function unlikePost(apId: string): Promise<void> {
|
|
131
|
+
const res = await apiDelete(`/api/posts/${encodeURIComponent(apId)}/like`);
|
|
132
|
+
await assertOk(res, "Failed to unlike");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function repostPost(apId: string): Promise<void> {
|
|
136
|
+
const res = await apiPost(`/api/posts/${encodeURIComponent(apId)}/repost`);
|
|
137
|
+
await assertOk(res, "Failed to repost");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function unrepostPost(apId: string): Promise<void> {
|
|
141
|
+
const res = await apiDelete(`/api/posts/${encodeURIComponent(apId)}/repost`);
|
|
142
|
+
await assertOk(res, "Failed to unrepost");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function bookmarkPost(apId: string): Promise<void> {
|
|
146
|
+
const res = await apiPost(`/api/posts/${encodeURIComponent(apId)}/bookmark`);
|
|
147
|
+
await assertOk(res, "Failed to bookmark");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function unbookmarkPost(apId: string): Promise<void> {
|
|
151
|
+
const res = await apiDelete(
|
|
152
|
+
`/api/posts/${encodeURIComponent(apId)}/bookmark`,
|
|
153
|
+
);
|
|
154
|
+
await assertOk(res, "Failed to unbookmark");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface BookmarksPage {
|
|
158
|
+
posts: Post[];
|
|
159
|
+
nextCursor: string | null;
|
|
160
|
+
hasMore: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function fetchBookmarks(options?: {
|
|
164
|
+
limit?: number;
|
|
165
|
+
before?: string;
|
|
166
|
+
}): Promise<BookmarksPage> {
|
|
167
|
+
const params = new URLSearchParams();
|
|
168
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
169
|
+
if (options?.before) params.set("before", options.before);
|
|
170
|
+
const query = params.toString() ? `?${params}` : "";
|
|
171
|
+
const res = await apiFetch(`/api/bookmarks${query}`);
|
|
172
|
+
const data = (await res.json()) as PostListResponse & {
|
|
173
|
+
next_cursor?: string | null;
|
|
174
|
+
has_more?: boolean;
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
posts: (data.posts ?? []).map(normalizePost),
|
|
178
|
+
nextCursor: data.next_cursor ?? null,
|
|
179
|
+
hasMore: data.has_more ?? false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Actor } from "../../types/index.ts";
|
|
2
|
+
import { normalizeActor } from "./normalize.ts";
|
|
3
|
+
import { apiFetch } from "./fetch.ts";
|
|
4
|
+
|
|
5
|
+
export interface RecommendedUser {
|
|
6
|
+
ap_id: string;
|
|
7
|
+
preferred_username: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
icon_url: string | null;
|
|
10
|
+
username: string;
|
|
11
|
+
mutual_count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fetchRecommendedUsers(): Promise<RecommendedUser[]> {
|
|
15
|
+
const res = await apiFetch("/api/recommendations/users");
|
|
16
|
+
if (!res.ok) return [];
|
|
17
|
+
const data = (await res.json()) as { users?: RecommendedUser[] };
|
|
18
|
+
return (data.users || []).map((u) => ({
|
|
19
|
+
...normalizeActor(u),
|
|
20
|
+
mutual_count: u.mutual_count,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Actor, Post } from "../../types/index.ts";
|
|
2
|
+
import { normalizeActor, normalizePost } from "./normalize.ts";
|
|
3
|
+
import { apiFetch } from "./fetch.ts";
|
|
4
|
+
|
|
5
|
+
export interface SearchPageOpts {
|
|
6
|
+
sort?: string;
|
|
7
|
+
offset?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Paged<T> {
|
|
12
|
+
items: T[];
|
|
13
|
+
hasMore: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pageParams(query: string, opts?: SearchPageOpts): string {
|
|
17
|
+
const params = new URLSearchParams({ q: query });
|
|
18
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
19
|
+
if (opts?.offset) params.set("offset", String(opts.offset));
|
|
20
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
21
|
+
return params.toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function searchActors(
|
|
25
|
+
query: string,
|
|
26
|
+
opts?: SearchPageOpts,
|
|
27
|
+
): Promise<Paged<Actor>> {
|
|
28
|
+
const res = await apiFetch(`/api/search/actors?${pageParams(query, opts)}`);
|
|
29
|
+
const data = (await res.json()) as { actors?: Actor[]; has_more?: boolean };
|
|
30
|
+
return {
|
|
31
|
+
items: (data.actors || []).map(normalizeActor),
|
|
32
|
+
hasMore: data.has_more ?? false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function searchRemote(query: string): Promise<Actor[]> {
|
|
37
|
+
const res = await apiFetch(
|
|
38
|
+
`/api/search/remote?q=${encodeURIComponent(query)}`,
|
|
39
|
+
);
|
|
40
|
+
const data = (await res.json()) as { actors?: Actor[] };
|
|
41
|
+
return (data.actors || []).map(normalizeActor);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function searchPosts(
|
|
45
|
+
query: string,
|
|
46
|
+
opts?: SearchPageOpts,
|
|
47
|
+
): Promise<Paged<Post>> {
|
|
48
|
+
const res = await apiFetch(`/api/search/posts?${pageParams(query, opts)}`);
|
|
49
|
+
const data = (await res.json()) as { posts?: Post[]; has_more?: boolean };
|
|
50
|
+
return {
|
|
51
|
+
items: (data.posts || []).map(normalizePost),
|
|
52
|
+
hasMore: data.has_more ?? false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Exact whole-hashtag search. Unlike searchPosts (a substring/FTS content
|
|
57
|
+
// match, where "#deploy" also hits "#deployed"), this hits the dedicated
|
|
58
|
+
// /search/hashtag/:tag route which matches the tag as a complete token — the
|
|
59
|
+
// behaviour a user expects when they click a #hashtag. Pass the tag WITHOUT the
|
|
60
|
+
// leading '#'.
|
|
61
|
+
export async function searchHashtag(
|
|
62
|
+
tag: string,
|
|
63
|
+
opts?: SearchPageOpts,
|
|
64
|
+
): Promise<Paged<Post>> {
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
67
|
+
if (opts?.offset) params.set("offset", String(opts.offset));
|
|
68
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
69
|
+
const qs = params.toString();
|
|
70
|
+
const res = await apiFetch(
|
|
71
|
+
`/api/search/hashtag/${encodeURIComponent(tag)}${qs ? `?${qs}` : ""}`,
|
|
72
|
+
);
|
|
73
|
+
const data = (await res.json()) as { posts?: Post[]; has_more?: boolean };
|
|
74
|
+
return {
|
|
75
|
+
items: (data.posts || []).map(normalizePost),
|
|
76
|
+
hasMore: data.has_more ?? false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fetchTrendingHashtags(
|
|
81
|
+
limit = 10,
|
|
82
|
+
): Promise<{ tag: string; count: number }[]> {
|
|
83
|
+
const res = await apiFetch(`/api/search/hashtags/trending?limit=${limit}`);
|
|
84
|
+
const data = (await res.json()) as {
|
|
85
|
+
trending?: { tag: string; count: number }[];
|
|
86
|
+
};
|
|
87
|
+
return data.trending || [];
|
|
88
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ActorStories, Story, StoryOverlay } from "../../types/index.ts";
|
|
2
|
+
import { normalizeActorStories, normalizeStory } from "./normalize.ts";
|
|
3
|
+
import { apiDelete, apiFetch, apiPost, assertOk } from "./fetch.ts";
|
|
4
|
+
|
|
5
|
+
export async function fetchStories(
|
|
6
|
+
community?: string,
|
|
7
|
+
): Promise<ActorStories[]> {
|
|
8
|
+
const qs = community ? `?community=${encodeURIComponent(community)}` : "";
|
|
9
|
+
const res = await apiFetch(`/api/stories${qs}`);
|
|
10
|
+
await assertOk(res, "Failed to fetch stories");
|
|
11
|
+
const data = (await res.json()) as { actor_stories?: ActorStories[] };
|
|
12
|
+
return (data.actor_stories || []).map(normalizeActorStories);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function createStory(story: {
|
|
16
|
+
attachment: { url?: string; r2_key: string; content_type: string };
|
|
17
|
+
displayDuration: string;
|
|
18
|
+
caption?: string;
|
|
19
|
+
overlays?: StoryOverlay[];
|
|
20
|
+
community_ap_id?: string;
|
|
21
|
+
}): Promise<Story> {
|
|
22
|
+
const res = await apiPost("/api/stories", story);
|
|
23
|
+
await assertOk(res, "Failed to create story");
|
|
24
|
+
const data = (await res.json()) as { story: Story };
|
|
25
|
+
return normalizeStory(data.story);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function deleteStory(apId: string): Promise<void> {
|
|
29
|
+
const res = await apiPost("/api/stories/delete", { ap_id: apId });
|
|
30
|
+
await assertOk(res, "Failed to delete story");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function markStoryViewed(apId: string): Promise<void> {
|
|
34
|
+
const res = await apiPost("/api/stories/view", { ap_id: apId });
|
|
35
|
+
await assertOk(res, "Failed to mark story as viewed");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function voteOnStory(
|
|
39
|
+
apId: string,
|
|
40
|
+
optionIndex: number,
|
|
41
|
+
): Promise<{
|
|
42
|
+
votes: Record<number, number>;
|
|
43
|
+
total: number;
|
|
44
|
+
user_vote: number;
|
|
45
|
+
}> {
|
|
46
|
+
const res = await apiPost("/api/stories/vote", {
|
|
47
|
+
ap_id: apId,
|
|
48
|
+
option_index: optionIndex,
|
|
49
|
+
});
|
|
50
|
+
await assertOk(res, "Failed to vote on story");
|
|
51
|
+
return (await res.json()) as {
|
|
52
|
+
votes: Record<number, number>;
|
|
53
|
+
total: number;
|
|
54
|
+
user_vote: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function likeStory(
|
|
59
|
+
apId: string,
|
|
60
|
+
): Promise<{ liked: boolean; like_count: number }> {
|
|
61
|
+
const res = await apiPost(`/api/stories/${encodeURIComponent(apId)}/like`);
|
|
62
|
+
await assertOk(res, "Failed to like story");
|
|
63
|
+
return (await res.json()) as { liked: boolean; like_count: number };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function unlikeStory(
|
|
67
|
+
apId: string,
|
|
68
|
+
): Promise<{ liked: boolean; like_count: number }> {
|
|
69
|
+
const res = await apiDelete(`/api/stories/${encodeURIComponent(apId)}/like`);
|
|
70
|
+
await assertOk(res, "Failed to unlike story");
|
|
71
|
+
return (await res.json()) as { liked: boolean; like_count: number };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function shareStory(
|
|
75
|
+
apId: string,
|
|
76
|
+
): Promise<{ shared: boolean; share_count: number }> {
|
|
77
|
+
const res = await apiPost(`/api/stories/${encodeURIComponent(apId)}/share`);
|
|
78
|
+
await assertOk(res, "Failed to share story");
|
|
79
|
+
return (await res.json()) as { shared: boolean; share_count: number };
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./api/fetch.ts";
|
|
2
|
+
export * from "./api/account.ts";
|
|
3
|
+
export * from "./api/auth.ts";
|
|
4
|
+
export * from "./api/actors.ts";
|
|
5
|
+
export * from "./api/follow.ts";
|
|
6
|
+
export * from "./api/posts.ts";
|
|
7
|
+
export * from "./api/communities.ts";
|
|
8
|
+
export * from "./api/dm.ts";
|
|
9
|
+
export * from "./api/notifications.ts";
|
|
10
|
+
export * from "./api/search.ts";
|
|
11
|
+
export * from "./api/media.ts";
|
|
12
|
+
export * from "./api/stories.ts";
|
|
13
|
+
export * from "./api/recommendations.ts";
|
|
14
|
+
export * from "./api/moderation.ts";
|
|
15
|
+
export * from "./api/normalize.ts";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
2
|
+
export const UPLOAD_REQUEST_TIMEOUT_MS = 60_000;
|
|
3
|
+
|
|
4
|
+
export interface FetchWithTimeoutInit extends RequestInit {
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function fetchWithTimeout(
|
|
9
|
+
input: RequestInfo | URL,
|
|
10
|
+
init: FetchWithTimeoutInit = {},
|
|
11
|
+
): Promise<Response> {
|
|
12
|
+
const { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, signal, ...rest } = init;
|
|
13
|
+
|
|
14
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
15
|
+
return fetch(input, { ...rest, signal });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const abortFromSignal = () => controller.abort(signal?.reason);
|
|
20
|
+
|
|
21
|
+
if (signal?.aborted) {
|
|
22
|
+
controller.abort(signal.reason);
|
|
23
|
+
} else if (signal) {
|
|
24
|
+
signal.addEventListener("abort", abortFromSignal, { once: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const timeoutId = globalThis.setTimeout(() => {
|
|
28
|
+
controller.abort();
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return await fetch(input, {
|
|
33
|
+
...rest,
|
|
34
|
+
signal: controller.signal,
|
|
35
|
+
});
|
|
36
|
+
} finally {
|
|
37
|
+
globalThis.clearTimeout(timeoutId);
|
|
38
|
+
if (signal) {
|
|
39
|
+
signal.removeEventListener("abort", abortFromSignal);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ApiTransport {
|
|
2
|
+
resolveUrl(path: string): string;
|
|
3
|
+
getAuthHeaders(path: string): Record<string, string>;
|
|
4
|
+
readonly credentials: RequestCredentials;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class DefaultSelfHostedTransport implements ApiTransport {
|
|
8
|
+
readonly credentials: RequestCredentials = "include";
|
|
9
|
+
|
|
10
|
+
resolveUrl(path: string): string {
|
|
11
|
+
return path;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getAuthHeaders(_path: string): Record<string, string> {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ApiTransportResolver = () => ApiTransport;
|
|
20
|
+
|
|
21
|
+
let activeTransportResolver: ApiTransportResolver = () =>
|
|
22
|
+
new DefaultSelfHostedTransport();
|
|
23
|
+
|
|
24
|
+
export function setYurucommuApiTransportResolver(
|
|
25
|
+
resolver: ApiTransportResolver,
|
|
26
|
+
): void {
|
|
27
|
+
activeTransportResolver = resolver;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setYurucommuApiTransport(transport: ApiTransport): void {
|
|
31
|
+
activeTransportResolver = () => transport;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearYurucommuApiTransport(): void {
|
|
35
|
+
activeTransportResolver = () => new DefaultSelfHostedTransport();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getYurucommuApiTransport(): ApiTransport {
|
|
39
|
+
return activeTransportResolver();
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { apiFetch, assertOk } from "./lib/api/fetch.ts";
|
|
2
|
+
import type { Actor } from "./types/index.ts";
|
|
3
|
+
|
|
4
|
+
export interface SocialServerDiscovery {
|
|
5
|
+
readonly product: "yurucommu";
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly server: {
|
|
8
|
+
readonly id: "yurucommu-server";
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly canonicalOrigin: string;
|
|
11
|
+
readonly activitypubOrigin: string;
|
|
12
|
+
};
|
|
13
|
+
readonly clients: readonly {
|
|
14
|
+
readonly id: "yurucommu" | "yurume";
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly defaultEntry: "feed" | "messages";
|
|
17
|
+
}[];
|
|
18
|
+
readonly issuer: string;
|
|
19
|
+
readonly apiBaseUrl: string;
|
|
20
|
+
readonly activitypubOrigin: string;
|
|
21
|
+
readonly mediaOrigin: string;
|
|
22
|
+
readonly socialServerCapabilitiesUrl: string;
|
|
23
|
+
readonly capabilities: readonly string[];
|
|
24
|
+
readonly endpoints: {
|
|
25
|
+
readonly api: string;
|
|
26
|
+
readonly authProviders: string;
|
|
27
|
+
readonly currentUser: string;
|
|
28
|
+
readonly timeline: string;
|
|
29
|
+
readonly conversations: string;
|
|
30
|
+
readonly notifications: string;
|
|
31
|
+
readonly mobilePushRegistrations: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function fetchCurrentActor(): Promise<Actor | null> {
|
|
36
|
+
const res = await apiFetch("/api/auth/me");
|
|
37
|
+
if (res.status === 401 || res.status === 403) return null;
|
|
38
|
+
await assertOk(res, "Failed to load current user");
|
|
39
|
+
const data = (await res.json()) as { actor?: Actor | null };
|
|
40
|
+
return data.actor ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function fetchSocialServerDiscovery(): Promise<SocialServerDiscovery> {
|
|
44
|
+
const res = await apiFetch("/.well-known/social-server");
|
|
45
|
+
await assertOk(res, "Failed to load social server discovery");
|
|
46
|
+
return (await res.json()) as SocialServerDiscovery;
|
|
47
|
+
}
|