@recursiv/sdk 0.7.52 → 0.7.53
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/README.md +20 -3
- package/dist/client.d.ts +6 -2
- package/dist/client.js +7 -8
- package/dist/index.d.ts +11 -1
- package/dist/index.js +11 -1
- package/dist/resources/bookmarks.d.ts +172 -0
- package/dist/resources/bookmarks.js +44 -0
- package/dist/resources/communities.d.ts +11 -1
- package/dist/resources/communities.js +14 -0
- package/dist/resources/discovery.d.ts +90 -2
- package/dist/resources/discovery.js +83 -20
- package/dist/resources/link-preview.d.ts +11 -4
- package/dist/resources/link-preview.js +4 -3
- package/dist/resources/notifications.d.ts +26 -0
- package/dist/resources/posts.d.ts +21 -3
- package/dist/resources/posts.js +21 -1
- package/dist/resources/profiles.d.ts +23 -14
- package/dist/resources/profiles.js +18 -5
- package/dist/resources/protocols.d.ts +44 -3
- package/dist/resources/search.d.ts +159 -0
- package/dist/resources/search.js +21 -0
- package/dist/types.d.ts +130 -2
- package/llm.md +32 -3
- package/package.json +1 -1
|
@@ -8,6 +8,20 @@ const RADAR_MAX_MEMBERS = 12;
|
|
|
8
8
|
const RADAR_MAX_MINDS_READS = 4;
|
|
9
9
|
/** A discovery page holds its own deadline: a retried 5xx from one provider only makes the whole page wait. */
|
|
10
10
|
const NO_RETRY = { retry: false };
|
|
11
|
+
/**
|
|
12
|
+
* The provider no longer lists the topic (`trend_unavailable`): that topic's end, never a network
|
|
13
|
+
* failure. Matched on the server's code, which every SDK error carries: any other 404 on the route
|
|
14
|
+
* (`unsupported_protocol`, a missing route) is the network failing, and still fails the source.
|
|
15
|
+
*/
|
|
16
|
+
const isGone = (error) => typeof error === 'object' && error !== null && error.code === 'trend_unavailable';
|
|
17
|
+
/**
|
|
18
|
+
* Topic pages one read of a topics network walks past when each is gone or carries nothing in the
|
|
19
|
+
* category, so a page reaches a topic that carries posts. Live (2026-09-26, Bluesky, news) 21 of 25
|
|
20
|
+
* topics answered no posts in the category; one topic a read left the network out of most pages.
|
|
21
|
+
*/
|
|
22
|
+
const TOPIC_WALK = 5;
|
|
23
|
+
/** The walk starts no topic read after this long: well inside the page deadline, so it commits its progress rather than timing the network out. */
|
|
24
|
+
const TOPIC_WALK_MS = 8000;
|
|
11
25
|
const RECOMMENDATION_KINDS = ['groups', 'people', 'tags'];
|
|
12
26
|
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512;
|
|
13
27
|
function recommendationItem(kind, candidate) {
|
|
@@ -116,7 +130,7 @@ function windowLayout(layout, index) {
|
|
|
116
130
|
}
|
|
117
131
|
/** Per-viewer feed session. Create a new session when user, app, category or
|
|
118
132
|
* source changes. All reads retain existing server authorization/moderation.
|
|
119
|
-
* At most four
|
|
133
|
+
* At most four sources per page (a topics network's read walks up to TOPIC_WALK topic pages); providers fail independently. */
|
|
120
134
|
export class DiscoveryFeedSession {
|
|
121
135
|
posts;
|
|
122
136
|
protocols;
|
|
@@ -181,23 +195,9 @@ export class DiscoveryFeedSession {
|
|
|
181
195
|
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(post => post.protocol !== stream.protocol || post.author.protocol !== stream.protocol))
|
|
182
196
|
throw new Error('Invalid discovery source response');
|
|
183
197
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
stream.done = true;
|
|
188
|
-
return { items: [], layout: ordinary, partial: false };
|
|
189
|
-
}
|
|
190
|
-
const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY);
|
|
191
|
-
validatePosts(data);
|
|
192
|
-
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
193
|
-
throw new Error('Invalid discovery topic');
|
|
194
|
-
const next = data.cursor;
|
|
195
|
-
stream.topics.shift();
|
|
196
|
-
if (next && next !== topic.cursor && !topic.used.includes(next))
|
|
197
|
-
stream.topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
|
|
198
|
-
stream.done = !stream.topics.length;
|
|
199
|
-
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
200
|
-
}
|
|
198
|
+
const started = Date.now();
|
|
199
|
+
if (stream.topics)
|
|
200
|
+
return this.walkTopics(stream, params, validatePosts, false, started);
|
|
201
201
|
const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true }, NO_RETRY);
|
|
202
202
|
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
|
|
203
203
|
throw new Error('Invalid discovery source response');
|
|
@@ -206,7 +206,14 @@ export class DiscoveryFeedSession {
|
|
|
206
206
|
throw new Error('Invalid discovery preview');
|
|
207
207
|
stream.topics = [...new Set(data.items.map(topic => topic.nativeId))].map(topic => ({ topic, used: [] }));
|
|
208
208
|
stream.done = !stream.topics.length;
|
|
209
|
-
|
|
209
|
+
const partial = data.categoryFilter?.status === 'partial';
|
|
210
|
+
// Outside a category the catalog's previews are this page. In a category a preview is not
|
|
211
|
+
// judged to be in it, so the catalog carries no card: the read walks into its topics now,
|
|
212
|
+
// not a round later, so the network's first page in a category is not empty by design.
|
|
213
|
+
const previews = data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []);
|
|
214
|
+
if (previews.length || stream.done)
|
|
215
|
+
return { items: previews, layout: ordinary, partial };
|
|
216
|
+
return this.walkTopics(stream, params, validatePosts, partial, started);
|
|
210
217
|
}
|
|
211
218
|
validatePosts(data);
|
|
212
219
|
const next = data.cursor ?? undefined;
|
|
@@ -216,6 +223,42 @@ export class DiscoveryFeedSession {
|
|
|
216
223
|
stream.cursor = next;
|
|
217
224
|
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
218
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* One read of a topics network: the next topic page that carries posts. A topic the provider no
|
|
228
|
+
* longer lists (Bluesky rotates its trends between the catalog read and the walk through it) is
|
|
229
|
+
* that topic's end, not the network's failure; a topic page with nothing in the category is
|
|
230
|
+
* walked past too, and its continuation kept, so the page reaches a topic that carries posts
|
|
231
|
+
* instead of spending a round on each empty one. Bounded by TOPIC_WALK reads and TOPIC_WALK_MS;
|
|
232
|
+
* a walk that stops short has moved the stream on, and the next read continues from there.
|
|
233
|
+
*/
|
|
234
|
+
async walkTopics(stream, params, validatePosts, partial, started) {
|
|
235
|
+
const topics = stream.topics;
|
|
236
|
+
for (let reads = 0; reads < TOPIC_WALK && Date.now() - started < TOPIC_WALK_MS; reads++) {
|
|
237
|
+
const topic = topics[0];
|
|
238
|
+
if (!topic)
|
|
239
|
+
break;
|
|
240
|
+
const response = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY)
|
|
241
|
+
.catch((error) => { if (isGone(error))
|
|
242
|
+
return null; throw error; });
|
|
243
|
+
topics.shift();
|
|
244
|
+
if (!response)
|
|
245
|
+
continue;
|
|
246
|
+
const { data } = response;
|
|
247
|
+
validatePosts(data);
|
|
248
|
+
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
249
|
+
throw new Error('Invalid discovery topic');
|
|
250
|
+
const next = data.cursor;
|
|
251
|
+
if (next && next !== topic.cursor && !topic.used.includes(next))
|
|
252
|
+
topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
|
|
253
|
+
partial ||= data.categoryFilter?.status === 'partial';
|
|
254
|
+
if (data.items.length) {
|
|
255
|
+
stream.done = !topics.length;
|
|
256
|
+
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
stream.done = !topics.length;
|
|
260
|
+
return { items: [], layout: ordinary, partial };
|
|
261
|
+
}
|
|
219
262
|
async readPage() {
|
|
220
263
|
// The radar is one small snapshot read: start it with the page, not after the slowest provider,
|
|
221
264
|
// and start the lead story's member reads the moment the snapshot names it.
|
|
@@ -466,10 +509,30 @@ export class DiscoveryResource {
|
|
|
466
509
|
posts;
|
|
467
510
|
protocols;
|
|
468
511
|
curator;
|
|
469
|
-
|
|
512
|
+
http;
|
|
513
|
+
constructor(posts, protocols, curator, http) {
|
|
470
514
|
this.posts = posts;
|
|
471
515
|
this.protocols = protocols;
|
|
472
516
|
this.curator = curator;
|
|
517
|
+
this.http = http;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Suggestions for the signed-in viewer from the app's one recommender: active or warm members, groups
|
|
521
|
+
* with several posters, tags used by several people, open-network accounts or stories, overall or in one
|
|
522
|
+
* browse category. What the viewer follows, joined, blocked or muted is left out, and so is anything
|
|
523
|
+
* resting for them. Call `recordImpressions` when suggestions are actually on screen and when one is tapped.
|
|
524
|
+
*/
|
|
525
|
+
recommendations(params, options) {
|
|
526
|
+
if (!this.http)
|
|
527
|
+
throw new Error('recommendations needs the HTTP client');
|
|
528
|
+
const { exclude, ...rest } = params;
|
|
529
|
+
return this.http.get('/discover/recommendations', { ...rest, exclude: exclude?.length ? exclude.join(',') : undefined }, options);
|
|
530
|
+
}
|
|
531
|
+
/** Records that suggestions were shown (at most once per half hour counts) or engaged with. */
|
|
532
|
+
recordImpressions(input) {
|
|
533
|
+
if (!this.http)
|
|
534
|
+
throw new Error('recordImpressions needs the HTTP client');
|
|
535
|
+
return this.http.post('/discover/impressions', input);
|
|
473
536
|
}
|
|
474
537
|
/**
|
|
475
538
|
* A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
|
|
@@ -9,7 +9,13 @@ export interface LinkPreview {
|
|
|
9
9
|
siteName: string | null;
|
|
10
10
|
}
|
|
11
11
|
export interface LinkPreviewResponse {
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The preview, or null when the link cannot be previewed: the page refused a
|
|
14
|
+
* server (a private repository, a paywall or bot wall), is not HTML, or could
|
|
15
|
+
* not be reached. Show the plain link. Servers before this change answered
|
|
16
|
+
* 422 for these links instead, so treat that rejection the same way.
|
|
17
|
+
*/
|
|
18
|
+
data: LinkPreview | null;
|
|
13
19
|
cached?: boolean;
|
|
14
20
|
}
|
|
15
21
|
export declare class LinkPreviewResource {
|
|
@@ -17,9 +23,10 @@ export declare class LinkPreviewResource {
|
|
|
17
23
|
constructor(client: HttpClient);
|
|
18
24
|
/**
|
|
19
25
|
* Fetch Open Graph / Twitter card metadata for a public URL. Cached
|
|
20
|
-
* server-side so repeated lookups are cheap
|
|
21
|
-
*
|
|
22
|
-
* link previews.
|
|
26
|
+
* server-side so repeated lookups are cheap, including links that have no
|
|
27
|
+
* preview (`data: null`). No auth required — the endpoint is intentionally
|
|
28
|
+
* public so unauthenticated UIs can render link previews. A URL that is not
|
|
29
|
+
* http(s), or points at a private or internal address, still rejects (422).
|
|
23
30
|
*/
|
|
24
31
|
get(url: string): Promise<LinkPreviewResponse>;
|
|
25
32
|
}
|
|
@@ -5,9 +5,10 @@ export class LinkPreviewResource {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Fetch Open Graph / Twitter card metadata for a public URL. Cached
|
|
8
|
-
* server-side so repeated lookups are cheap
|
|
9
|
-
*
|
|
10
|
-
* link previews.
|
|
8
|
+
* server-side so repeated lookups are cheap, including links that have no
|
|
9
|
+
* preview (`data: null`). No auth required — the endpoint is intentionally
|
|
10
|
+
* public so unauthenticated UIs can render link previews. A URL that is not
|
|
11
|
+
* http(s), or points at a private or internal address, still rejects (422).
|
|
11
12
|
*/
|
|
12
13
|
get(url) {
|
|
13
14
|
return this.client.get('/link-preview', { url });
|
|
@@ -7,6 +7,12 @@ export interface NotificationActor {
|
|
|
7
7
|
username: string | null;
|
|
8
8
|
image: string | null;
|
|
9
9
|
is_ai: boolean | null;
|
|
10
|
+
/**
|
|
11
|
+
* Follow rows only: whether the viewer already follows this person, so
|
|
12
|
+
* Follow back can render without reading the follow set. Absent on other
|
|
13
|
+
* rows and from servers older than this field.
|
|
14
|
+
*/
|
|
15
|
+
viewer_follows?: boolean;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* The post a notification references, as it reads NOW, with the viewer's own
|
|
@@ -15,7 +21,18 @@ export interface NotificationActor {
|
|
|
15
21
|
*/
|
|
16
22
|
export interface NotificationPostPreview {
|
|
17
23
|
id: string;
|
|
24
|
+
/**
|
|
25
|
+
* The post's words as plain text (no Markdown or HTML marks; a link reads as
|
|
26
|
+
* its label and a bare URL as its site), up to 140 characters. A longer post
|
|
27
|
+
* is cut at the last whole word and ends in "…". Show it as it is.
|
|
28
|
+
*/
|
|
18
29
|
excerpt: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* True when `excerpt` is a cut of a longer post. Use this rather than the
|
|
32
|
+
* excerpt's length to decide whether there is more to read. Absent from
|
|
33
|
+
* servers older than this field.
|
|
34
|
+
*/
|
|
35
|
+
excerpt_truncated?: boolean;
|
|
19
36
|
media_url: string | null;
|
|
20
37
|
media_type: string | null;
|
|
21
38
|
reactions_count?: number;
|
|
@@ -24,7 +41,10 @@ export interface NotificationPostPreview {
|
|
|
24
41
|
downvote_count?: number;
|
|
25
42
|
/** The viewer's own reaction on this post ('upvote', 'downvote', ...), if any. */
|
|
26
43
|
user_reaction?: string | null;
|
|
44
|
+
/** Stored replies a reader can open. */
|
|
27
45
|
reply_count?: number;
|
|
46
|
+
/** A legacy import's comment count; none of those comments are stored as replies. */
|
|
47
|
+
reply_count_external?: number;
|
|
28
48
|
unique_commenters?: number;
|
|
29
49
|
reposts_count?: number;
|
|
30
50
|
/** Id of the viewer's own repost of this post; delete it to undo the repost. */
|
|
@@ -43,6 +63,12 @@ export interface Notification {
|
|
|
43
63
|
read_at: string | null;
|
|
44
64
|
/** For `post_reaction` rows: which reaction ('upvote', 'downvote', ...). Null otherwise or on older rows. */
|
|
45
65
|
reaction_type?: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* For `agent_needs_input` rows: whether you have written in the agent's
|
|
68
|
+
* thread since it asked, which is what answers an ask. The title then reads
|
|
69
|
+
* "You answered <agent>". Null on other rows; absent from older servers.
|
|
70
|
+
*/
|
|
71
|
+
answered?: boolean | null;
|
|
46
72
|
actor?: NotificationActor | null;
|
|
47
73
|
post_preview?: NotificationPostPreview | null;
|
|
48
74
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, ReactionType } from '../types.js';
|
|
2
|
+
import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, SearchPostsResponse, ReactionType } from '../types.js';
|
|
3
|
+
import type { BookmarkedPost, BookmarksPage, SavedBookmark, UnsavedBookmark } from './bookmarks.js';
|
|
3
4
|
/** How GET /posts/categories ordered its answer: the member's own order, the app's popularity order, or the fixed order. */
|
|
4
5
|
export type CategoryOrder = 'personal' | 'popular' | 'static';
|
|
5
6
|
/** Why an answer is not `personal`. */
|
|
@@ -33,6 +34,19 @@ export declare class PostsResource {
|
|
|
33
34
|
}>;
|
|
34
35
|
/** List posts the authenticated caller has upvoted. */
|
|
35
36
|
liked(params?: Pick<ListPostsParams, 'limit' | 'offset' | 'cursor'>): Promise<PaginatedResponse<Post>>;
|
|
37
|
+
/**
|
|
38
|
+
* The caller's saved posts in this app, newest saved first, each with `bookmarked_at`. Page on
|
|
39
|
+
* `meta.next_cursor` (a page can be short while it skips posts the member can no longer read).
|
|
40
|
+
* Same as `bookmarks.posts`; 503 `bookmarks_unavailable` where synced bookmarks are not available.
|
|
41
|
+
*/
|
|
42
|
+
bookmarked(params?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
cursor?: string;
|
|
45
|
+
}): Promise<BookmarksPage<BookmarkedPost>>;
|
|
46
|
+
/** Save a post to the caller's synced bookmarks (idempotent). A bare repost is saved as its original: see `post_id`. */
|
|
47
|
+
bookmark(postId: string): Promise<SingleResponse<SavedBookmark>>;
|
|
48
|
+
/** Remove a post from the caller's synced bookmarks (idempotent). Pass the `post_id` its save returned. */
|
|
49
|
+
unbookmark(postId: string): Promise<SingleResponse<UnsavedBookmark>>;
|
|
36
50
|
/** Posts similar to this one (embedding neighbours), in the app's scope. */
|
|
37
51
|
similar(id: string, params?: {
|
|
38
52
|
limit?: number;
|
|
@@ -48,8 +62,12 @@ export declare class PostsResource {
|
|
|
48
62
|
update(id: string, input: UpdatePostInput): Promise<SingleResponse<CreatedPost>>;
|
|
49
63
|
/** Delete a post */
|
|
50
64
|
delete(id: string): Promise<DeleteResponse>;
|
|
51
|
-
/**
|
|
52
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Search posts by text query. Relevance (the default) ranks relevance x recency x engagement through
|
|
67
|
+
* the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as
|
|
68
|
+
* `cursor`; the first relevance page carries `latest`, the last page `fall_over` (SearchPostsResponse).
|
|
69
|
+
*/
|
|
70
|
+
search(params: SearchPostsParams): Promise<SearchPostsResponse>;
|
|
53
71
|
/** Add or update a reaction on a post */
|
|
54
72
|
react(postId: string, type: ReactionType): Promise<SingleResponse<{
|
|
55
73
|
success: true;
|
package/dist/resources/posts.js
CHANGED
|
@@ -20,6 +20,22 @@ export class PostsResource {
|
|
|
20
20
|
liked(params) {
|
|
21
21
|
return this.list({ ...params, liked: true });
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The caller's saved posts in this app, newest saved first, each with `bookmarked_at`. Page on
|
|
25
|
+
* `meta.next_cursor` (a page can be short while it skips posts the member can no longer read).
|
|
26
|
+
* Same as `bookmarks.posts`; 503 `bookmarks_unavailable` where synced bookmarks are not available.
|
|
27
|
+
*/
|
|
28
|
+
bookmarked(params) {
|
|
29
|
+
return this.client.get('/posts', { bookmarked: true, ...params });
|
|
30
|
+
}
|
|
31
|
+
/** Save a post to the caller's synced bookmarks (idempotent). A bare repost is saved as its original: see `post_id`. */
|
|
32
|
+
bookmark(postId) {
|
|
33
|
+
return this.client.put('/bookmarks', { kind: 'post', post_id: postId });
|
|
34
|
+
}
|
|
35
|
+
/** Remove a post from the caller's synced bookmarks (idempotent). Pass the `post_id` its save returned. */
|
|
36
|
+
unbookmark(postId) {
|
|
37
|
+
return this.client.delete(`/bookmarks/${encodeURIComponent(`post:${postId}`)}`);
|
|
38
|
+
}
|
|
23
39
|
/** Posts similar to this one (embedding neighbours), in the app's scope. */
|
|
24
40
|
similar(id, params) {
|
|
25
41
|
return this.client.get(`/posts/${encodeURIComponent(id)}/similar`, params);
|
|
@@ -44,7 +60,11 @@ export class PostsResource {
|
|
|
44
60
|
delete(id) {
|
|
45
61
|
return this.client.delete(`/posts/${id}`);
|
|
46
62
|
}
|
|
47
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Search posts by text query. Relevance (the default) ranks relevance x recency x engagement through
|
|
65
|
+
* the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as
|
|
66
|
+
* `cursor`; the first relevance page carries `latest`, the last page `fall_over` (SearchPostsResponse).
|
|
67
|
+
*/
|
|
48
68
|
search(params) {
|
|
49
69
|
return this.client.get('/posts/search', params);
|
|
50
70
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, SearchMentionsParams, MentionCandidate, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
|
|
2
|
+
import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
|
|
3
3
|
export declare class ProfilesResource {
|
|
4
4
|
private client;
|
|
5
5
|
constructor(client: HttpClient);
|
|
@@ -15,14 +15,25 @@ export declare class ProfilesResource {
|
|
|
15
15
|
/** Get a user by username */
|
|
16
16
|
getByUsername(username: string): Promise<SingleResponse<ProfileDetail>>;
|
|
17
17
|
/** Search users by name or username */
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
|
|
20
|
+
* official account): empties last and marked `empty`, the official ones `verified`. Every page is
|
|
21
|
+
* a slice of one ranked pool of 200; past it `has_more` is false.
|
|
22
|
+
*/
|
|
23
|
+
search(params: SearchProfilesParams): Promise<PaginatedResponse<ProfileSearchResult>>;
|
|
19
24
|
/**
|
|
20
25
|
* Accounts to @mention: people and agents on the caller's network, the
|
|
21
26
|
* caller's follows first. Unlike `search`, this includes agents the app may
|
|
22
27
|
* discover (an agent answers when mentioned); another member's personal AI
|
|
23
28
|
* is never offered.
|
|
29
|
+
*
|
|
30
|
+
* A search that runs over its 2 s budget answers 503 with Retry-After. A
|
|
31
|
+
* type-ahead has its own deadline, so pass `{ retry: false }` to get that
|
|
32
|
+
* answer at once instead of waiting through retries.
|
|
24
33
|
*/
|
|
25
|
-
mentions(params: SearchMentionsParams
|
|
34
|
+
mentions(params: SearchMentionsParams, options?: {
|
|
35
|
+
retry?: boolean;
|
|
36
|
+
}): Promise<PaginatedResponse<MentionCandidate>>;
|
|
26
37
|
/** Update own profile */
|
|
27
38
|
update(input: UpdateProfileInput): Promise<SingleResponse<Profile>>;
|
|
28
39
|
/** Follow a user */
|
|
@@ -30,18 +41,16 @@ export declare class ProfilesResource {
|
|
|
30
41
|
/** Unfollow a user */
|
|
31
42
|
unfollow(userId: string): Promise<SuccessResponse>;
|
|
32
43
|
/**
|
|
33
|
-
* List a user's followers
|
|
34
|
-
*
|
|
44
|
+
* List a user's followers, newest follow first once the server's database
|
|
45
|
+
* has the follow-list order indexes (until then, in the order it reads
|
|
46
|
+
* them). Each row carries the caller's relationship with it (`is_following`,
|
|
47
|
+
* `follows_you`), so a Follow button needs no second read. `search` narrows
|
|
48
|
+
* the whole list to accounts whose name or handle contains it (at most 200
|
|
49
|
+
* characters); page the matches with `limit` and `offset`.
|
|
35
50
|
*/
|
|
36
|
-
followers(userId: string, params?:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}): Promise<PaginatedResponse<FollowListProfile>>;
|
|
40
|
-
/** List who a user follows, each row with the caller's relationship with it. */
|
|
41
|
-
following(userId: string, params?: {
|
|
42
|
-
limit?: number;
|
|
43
|
-
offset?: number;
|
|
44
|
-
}): Promise<PaginatedResponse<FollowListProfile>>;
|
|
51
|
+
followers(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
|
|
52
|
+
/** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
|
|
53
|
+
following(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
|
|
45
54
|
/**
|
|
46
55
|
* The caller's whole follow set as ids, up to 5000 per page (default 1000).
|
|
47
56
|
* Answers what `isFollowing` answers, for every account at once. Pass
|
|
@@ -20,6 +20,11 @@ export class ProfilesResource {
|
|
|
20
20
|
return this.client.get(`/profiles/by-username/${username}`);
|
|
21
21
|
}
|
|
22
22
|
/** Search users by name or username */
|
|
23
|
+
/**
|
|
24
|
+
* Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
|
|
25
|
+
* official account): empties last and marked `empty`, the official ones `verified`. Every page is
|
|
26
|
+
* a slice of one ranked pool of 200; past it `has_more` is false.
|
|
27
|
+
*/
|
|
23
28
|
search(params) {
|
|
24
29
|
return this.client.get('/profiles/search', params);
|
|
25
30
|
}
|
|
@@ -28,9 +33,13 @@ export class ProfilesResource {
|
|
|
28
33
|
* caller's follows first. Unlike `search`, this includes agents the app may
|
|
29
34
|
* discover (an agent answers when mentioned); another member's personal AI
|
|
30
35
|
* is never offered.
|
|
36
|
+
*
|
|
37
|
+
* A search that runs over its 2 s budget answers 503 with Retry-After. A
|
|
38
|
+
* type-ahead has its own deadline, so pass `{ retry: false }` to get that
|
|
39
|
+
* answer at once instead of waiting through retries.
|
|
31
40
|
*/
|
|
32
|
-
mentions(params) {
|
|
33
|
-
return this.client.get('/profiles/mentions', params);
|
|
41
|
+
mentions(params, options) {
|
|
42
|
+
return this.client.get('/profiles/mentions', params, options);
|
|
34
43
|
}
|
|
35
44
|
/** Update own profile */
|
|
36
45
|
update(input) {
|
|
@@ -45,13 +54,17 @@ export class ProfilesResource {
|
|
|
45
54
|
return this.client.delete(`/profiles/${userId}/follow`);
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
|
-
* List a user's followers
|
|
49
|
-
*
|
|
57
|
+
* List a user's followers, newest follow first once the server's database
|
|
58
|
+
* has the follow-list order indexes (until then, in the order it reads
|
|
59
|
+
* them). Each row carries the caller's relationship with it (`is_following`,
|
|
60
|
+
* `follows_you`), so a Follow button needs no second read. `search` narrows
|
|
61
|
+
* the whole list to accounts whose name or handle contains it (at most 200
|
|
62
|
+
* characters); page the matches with `limit` and `offset`.
|
|
50
63
|
*/
|
|
51
64
|
followers(userId, params) {
|
|
52
65
|
return this.client.get(`/profiles/${userId}/followers`, params);
|
|
53
66
|
}
|
|
54
|
-
/** List who a user follows, each row with the caller's relationship with it
|
|
67
|
+
/** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
|
|
55
68
|
following(userId, params) {
|
|
56
69
|
return this.client.get(`/profiles/${userId}/following`, params);
|
|
57
70
|
}
|
|
@@ -305,6 +305,8 @@ export interface SocialSourceControlDisplay {
|
|
|
305
305
|
avatarUrl: string | null;
|
|
306
306
|
/** A hidden post's text, at most 140 characters; null for accounts, deleted posts, and moderated posts or authors. */
|
|
307
307
|
excerpt: string | null;
|
|
308
|
+
/** A hidden ActivityPub post's public reader id, for the conversation read; absent for accounts, other networks and posts observed without one. */
|
|
309
|
+
readRef?: string;
|
|
308
310
|
observedAt: string;
|
|
309
311
|
}
|
|
310
312
|
export interface SocialSourceControl {
|
|
@@ -641,6 +643,8 @@ export interface NativeReplyBatch {
|
|
|
641
643
|
authorHandle: string;
|
|
642
644
|
text: string;
|
|
643
645
|
textTruncated?: boolean;
|
|
646
|
+
/** An ActivityPub parent's public reader id, when the reply was prepared with one: pass it to the conversation read. */
|
|
647
|
+
readRef?: string;
|
|
644
648
|
};
|
|
645
649
|
text: string;
|
|
646
650
|
wireText: string;
|
|
@@ -742,7 +746,7 @@ export interface NativeDelegatedReplyHistoryItem {
|
|
|
742
746
|
createdAt: string;
|
|
743
747
|
text: string;
|
|
744
748
|
textTruncated: boolean;
|
|
745
|
-
source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl'>;
|
|
749
|
+
source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl' | 'readRef'>;
|
|
746
750
|
deliveries: Array<Pick<NativeReplyBatch['deliveries'][number], 'id' | 'protocol' | 'kind' | 'state' | 'attemptedAt' | 'account' | 'recoverable' | 'replacedBy'> & {
|
|
747
751
|
removalReceipt: NativeReplyRemovalReceipt | null;
|
|
748
752
|
}>;
|
|
@@ -751,10 +755,12 @@ export interface NativeNostrReplyResult {
|
|
|
751
755
|
id: string;
|
|
752
756
|
createdAt: string;
|
|
753
757
|
text: string;
|
|
758
|
+
/** `readRef`: a linked ActivityPub source's public reader id, for the conversation read (replies saved before it existed have none). */
|
|
754
759
|
source: {
|
|
755
760
|
protocol: SocialSourceRef['protocol'];
|
|
756
761
|
nativeId: string;
|
|
757
762
|
sourceUrl: string;
|
|
763
|
+
readRef?: string;
|
|
758
764
|
};
|
|
759
765
|
identityId: string;
|
|
760
766
|
pubkey: string;
|
|
@@ -962,6 +968,12 @@ export interface SocialPost {
|
|
|
962
968
|
replyCount: number | null;
|
|
963
969
|
likeCount: number | null;
|
|
964
970
|
repostCount: number | null;
|
|
971
|
+
/**
|
|
972
|
+
* When the counts were read from the source network. Present only on a post a category feed served
|
|
973
|
+
* from the open-network index, whose copy can be older than the page (the server refreshes counts
|
|
974
|
+
* older than 15 minutes when the provider answers in time). Absent: the counts are from this read.
|
|
975
|
+
*/
|
|
976
|
+
countsObservedAt?: string;
|
|
965
977
|
/** Pixel dimensions when the source declares them (Bluesky aspect ratio, Mastodon attachment meta), so a card can take the clip's shape before it loads. */
|
|
966
978
|
media: Array<{
|
|
967
979
|
type: 'image' | 'video';
|
|
@@ -1165,17 +1177,46 @@ export interface SocialRadarPosts {
|
|
|
1165
1177
|
builtAt: string | null;
|
|
1166
1178
|
items: SocialPost[];
|
|
1167
1179
|
}
|
|
1180
|
+
/** A story that has left the radar, as the radar's story archive keeps it (up to 30 days after it was last on the radar). */
|
|
1181
|
+
export interface SocialRadarStoryArchive {
|
|
1182
|
+
/** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
|
|
1183
|
+
cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
|
|
1184
|
+
members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
|
|
1185
|
+
title?: string;
|
|
1186
|
+
}>;
|
|
1187
|
+
};
|
|
1188
|
+
/** Its open-network posts the app's source index still holds and its source controls and safety floor admit today, in member order. */
|
|
1189
|
+
items: SocialPost[];
|
|
1190
|
+
/** A World Today story's dek: an outlet's own sentence, or the editor model's. */
|
|
1191
|
+
summary: {
|
|
1192
|
+
text: string;
|
|
1193
|
+
origin: 'outlet' | 'editor';
|
|
1194
|
+
} | null;
|
|
1195
|
+
/** The first and the last radar build that published it. */
|
|
1196
|
+
firstSeenAt: string;
|
|
1197
|
+
lastSeenAt: string;
|
|
1198
|
+
/** The index holds none of its open-network posts any more (retention or deletion), so none are listed. */
|
|
1199
|
+
postsExpired: boolean;
|
|
1200
|
+
}
|
|
1168
1201
|
/**
|
|
1169
1202
|
* One radar story by id, with its social member posts.
|
|
1170
|
-
* `live`: on the current radar (`cluster` and `items` present). `aged_out`: on
|
|
1171
|
-
*
|
|
1203
|
+
* `live`: on the current radar (`cluster` and `items` present). `aged_out`: not on this radar, but
|
|
1204
|
+
* held by the story archive (`archive`, and the live story it continues as in `successor`) or seen on
|
|
1205
|
+
* one of the last six radars (`archive` null). `unknown`: in neither (never existed, or left longer ago
|
|
1206
|
+
* than the archive keeps stories). A server without the story archive leaves `archive` and `successor` out.
|
|
1172
1207
|
*/
|
|
1173
1208
|
export interface SocialRadarStory {
|
|
1174
1209
|
id: string;
|
|
1175
1210
|
builtAt: string | null;
|
|
1176
1211
|
status: 'live' | 'aged_out' | 'unknown';
|
|
1177
1212
|
cluster: SocialRadarCluster | null;
|
|
1213
|
+
/** The live story's posts; empty otherwise (an archived story's are in `archive.items`). */
|
|
1178
1214
|
items: SocialPost[];
|
|
1215
|
+
archive?: SocialRadarStoryArchive | null;
|
|
1216
|
+
successor?: {
|
|
1217
|
+
id: string;
|
|
1218
|
+
label: string;
|
|
1219
|
+
} | null;
|
|
1179
1220
|
}
|
|
1180
1221
|
/** Read options for callers that hold their own deadline, such as a discovery page. */
|
|
1181
1222
|
export interface SocialReadOptions {
|