@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { HttpClient } from '../client.js';
|
|
2
|
+
import type { Post } from '../types.js';
|
|
3
|
+
import type { SocialPost } from './protocols.js';
|
|
4
|
+
/** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
|
|
5
|
+
export declare const SEARCH_SECTIONS: readonly ["top_posts", "latest_posts", "open_network", "people", "groups", "topics", "stories", "related", "trending"];
|
|
6
|
+
export type SearchSection = (typeof SEARCH_SECTIONS)[number];
|
|
7
|
+
/** Why a section is absent: out of time, the server was busy, not available for this app, not allowed for this key, or it failed. */
|
|
8
|
+
export type SearchOmitReason = 'timeout' | 'saturated' | 'unavailable' | 'scope' | 'error';
|
|
9
|
+
export interface SearchParams {
|
|
10
|
+
q: string;
|
|
11
|
+
/** Items per section, 1-20 (default 6). */
|
|
12
|
+
limit?: number;
|
|
13
|
+
/** Only these sections (default: all). */
|
|
14
|
+
sections?: readonly SearchSection[];
|
|
15
|
+
/** For organization-bound keys, as on `posts.search`. */
|
|
16
|
+
organization_id?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SearchPerson {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string | null;
|
|
21
|
+
username: string | null;
|
|
22
|
+
image: string | null;
|
|
23
|
+
is_ai: boolean;
|
|
24
|
+
/** The query is this account's handle. */
|
|
25
|
+
exact: boolean;
|
|
26
|
+
/** The account's public life, what the ranking read: match x ln(1 + followers + min(posts, 100)). */
|
|
27
|
+
followers_count: number;
|
|
28
|
+
posts_count: number;
|
|
29
|
+
/** Nobody follows it and it never posted: listed after every account with a public life, so a client can fold these under "more handles". */
|
|
30
|
+
empty: boolean;
|
|
31
|
+
/** An account the operator vouches for: draw the verified badge. */
|
|
32
|
+
verified: boolean;
|
|
33
|
+
/** Membership badges, as every embedded author carries them. */
|
|
34
|
+
plus: boolean;
|
|
35
|
+
pro: boolean;
|
|
36
|
+
founder: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface SearchGroup {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
image: string | null;
|
|
43
|
+
member_count: number;
|
|
44
|
+
post_count: number;
|
|
45
|
+
is_member: boolean;
|
|
46
|
+
}
|
|
47
|
+
export type SearchTopic = {
|
|
48
|
+
kind: 'tag';
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'category';
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
export interface SearchStory {
|
|
58
|
+
/** Radar cluster id: open it with `protocols` radar story reads. */
|
|
59
|
+
id: string;
|
|
60
|
+
label: string;
|
|
61
|
+
kind: 'breaking' | 'viral' | 'story';
|
|
62
|
+
theme: string | null;
|
|
63
|
+
newest_at: string;
|
|
64
|
+
sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
|
|
65
|
+
lead: {
|
|
66
|
+
type: 'image' | 'video';
|
|
67
|
+
url: string;
|
|
68
|
+
poster?: string;
|
|
69
|
+
width?: number;
|
|
70
|
+
height?: number;
|
|
71
|
+
} | null;
|
|
72
|
+
/** Where the words were found: the story's headline, or only one of its members'. */
|
|
73
|
+
matched: 'label' | 'member';
|
|
74
|
+
}
|
|
75
|
+
export interface SearchRelated {
|
|
76
|
+
categories: Array<{
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
count: number;
|
|
80
|
+
}>;
|
|
81
|
+
tags: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
slug: string;
|
|
85
|
+
count: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/** One blended search answer. Only answered sections are present; an answered section may be empty. */
|
|
89
|
+
export interface SearchAnswer {
|
|
90
|
+
query: string;
|
|
91
|
+
sections: {
|
|
92
|
+
/** Relevance x recency x engagement, through the promotion safety floor. `has_more`: page further with `posts.search`. */
|
|
93
|
+
top_posts?: {
|
|
94
|
+
items: Post[];
|
|
95
|
+
has_more: boolean;
|
|
96
|
+
};
|
|
97
|
+
/** Newest first, never repeating a top post. `partial`: these are the newest of what was read in time. */
|
|
98
|
+
latest_posts?: {
|
|
99
|
+
items: Post[];
|
|
100
|
+
partial: boolean;
|
|
101
|
+
};
|
|
102
|
+
open_network?: {
|
|
103
|
+
items: SocialPost[];
|
|
104
|
+
withheld: number;
|
|
105
|
+
};
|
|
106
|
+
people?: {
|
|
107
|
+
items: SearchPerson[];
|
|
108
|
+
};
|
|
109
|
+
groups?: {
|
|
110
|
+
items: SearchGroup[];
|
|
111
|
+
};
|
|
112
|
+
topics?: {
|
|
113
|
+
items: SearchTopic[];
|
|
114
|
+
};
|
|
115
|
+
stories?: {
|
|
116
|
+
items: SearchStory[];
|
|
117
|
+
};
|
|
118
|
+
/** Categories and topics around the matches. */
|
|
119
|
+
related?: SearchRelated;
|
|
120
|
+
/** Present when the answer is thin or old: what is moving in the nearest category. */
|
|
121
|
+
trending?: {
|
|
122
|
+
category: {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
} | null;
|
|
126
|
+
items: Post[];
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/** Answered, non-empty sections in the order to render them. */
|
|
130
|
+
order: SearchSection[];
|
|
131
|
+
/** Sections the server left out, and why; the rest of the answer stands. */
|
|
132
|
+
omitted: Array<{
|
|
133
|
+
section: SearchSection;
|
|
134
|
+
reason: SearchOmitReason;
|
|
135
|
+
}>;
|
|
136
|
+
/** Fewer than five matches across Minds and the open networks. */
|
|
137
|
+
few_results: boolean;
|
|
138
|
+
}
|
|
139
|
+
export interface SearchResponse {
|
|
140
|
+
data: SearchAnswer;
|
|
141
|
+
meta: {
|
|
142
|
+
limit: number;
|
|
143
|
+
elapsed_ms: number | null;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export declare class SearchResource {
|
|
147
|
+
private client;
|
|
148
|
+
constructor(client: HttpClient);
|
|
149
|
+
/**
|
|
150
|
+
* Blended search (GET /search): the best and newest posts, open-network posts, people, groups, topics,
|
|
151
|
+
* news stories, and, so a search never dead-ends, related categories and trending posts. Each section
|
|
152
|
+
* has its own time budget; one that runs out is listed in `omitted` instead of failing the answer.
|
|
153
|
+
* Needs `posts:read`. People need `users:read`, groups `communities:read`; open network and stories need
|
|
154
|
+
* `users:read` and the caller's own app key bound to its network, trending that app key. A section the
|
|
155
|
+
* key cannot have is listed in `omitted` with reason `scope` or `unavailable`.
|
|
156
|
+
* Servers before this endpoint answer 404: fall back to `posts.search`.
|
|
157
|
+
*/
|
|
158
|
+
query(params: SearchParams): Promise<SearchResponse>;
|
|
159
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
|
|
2
|
+
export const SEARCH_SECTIONS = ['top_posts', 'latest_posts', 'open_network', 'people', 'groups', 'topics', 'stories', 'related', 'trending'];
|
|
3
|
+
export class SearchResource {
|
|
4
|
+
client;
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Blended search (GET /search): the best and newest posts, open-network posts, people, groups, topics,
|
|
10
|
+
* news stories, and, so a search never dead-ends, related categories and trending posts. Each section
|
|
11
|
+
* has its own time budget; one that runs out is listed in `omitted` instead of failing the answer.
|
|
12
|
+
* Needs `posts:read`. People need `users:read`, groups `communities:read`; open network and stories need
|
|
13
|
+
* `users:read` and the caller's own app key bound to its network, trending that app key. A section the
|
|
14
|
+
* key cannot have is listed in `omitted` with reason `scope` or `unavailable`.
|
|
15
|
+
* Servers before this endpoint answer 404: fall back to `posts.search`.
|
|
16
|
+
*/
|
|
17
|
+
query(params) {
|
|
18
|
+
const { sections, ...rest } = params;
|
|
19
|
+
return this.client.get('/search', { ...rest, ...(sections && sections.length > 0 ? { sections: sections.join(',') } : {}) });
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -123,9 +123,31 @@ export interface MediaItem {
|
|
|
123
123
|
id: string;
|
|
124
124
|
url: string;
|
|
125
125
|
type: string;
|
|
126
|
+
/**
|
|
127
|
+
* Set on a legacy Minds video whose file is known to be gone (the server probes it): leave it out
|
|
128
|
+
* of Watch and clip queues and show the poster instead. Absent when the file plays or is unjudged.
|
|
129
|
+
*/
|
|
130
|
+
unplayable?: boolean;
|
|
131
|
+
}
|
|
132
|
+
/** The group a post is in, as every post payload carries it. */
|
|
133
|
+
export interface PostCommunity {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
slug: string;
|
|
137
|
+
/**
|
|
138
|
+
* A post in a group that is not public is for the group's members, and is
|
|
139
|
+
* never boosted: offer Boost only when this is 'public'.
|
|
140
|
+
*/
|
|
141
|
+
privacy: 'public' | 'private' | 'hidden';
|
|
126
142
|
}
|
|
127
143
|
export interface Post {
|
|
144
|
+
/** Stored replies a reader can open. */
|
|
128
145
|
reply_count?: number;
|
|
146
|
+
/**
|
|
147
|
+
* The comment count a legacy import brought with the post (minds.com). None of those comments are
|
|
148
|
+
* stored as replies, so it is not in `reply_count`.
|
|
149
|
+
*/
|
|
150
|
+
reply_count_external?: number;
|
|
129
151
|
id: string;
|
|
130
152
|
/** Original Minds numeric GUID when this row came from the legacy import. */
|
|
131
153
|
legacy_guid?: string | null;
|
|
@@ -133,6 +155,8 @@ export interface Post {
|
|
|
133
155
|
content_format: 'plain' | 'markdown';
|
|
134
156
|
author: UserSummary;
|
|
135
157
|
community_id: string | null;
|
|
158
|
+
/** The group the post is in (null when it is in none). */
|
|
159
|
+
community?: PostCommunity | null;
|
|
136
160
|
organization_id: string | null;
|
|
137
161
|
reply_to_id?: string | null;
|
|
138
162
|
/** Present when this post is an X-style repost — the original post is nested. */
|
|
@@ -154,10 +178,25 @@ export interface RepostedPost {
|
|
|
154
178
|
content_format: 'plain' | 'markdown';
|
|
155
179
|
author: UserSummary | null;
|
|
156
180
|
community_id: string | null;
|
|
181
|
+
/** The group the original is in (null when it is in none): it decides what the repost card offers. */
|
|
182
|
+
community?: PostCommunity | null;
|
|
157
183
|
organization_id: string | null;
|
|
158
184
|
tags: Tag[];
|
|
159
185
|
reactions_count: number;
|
|
160
186
|
score: number;
|
|
187
|
+
/** The original's engagement, as the REST payload sends it for the original itself. */
|
|
188
|
+
upvote_count?: number;
|
|
189
|
+
downvote_count?: number;
|
|
190
|
+
/** The viewer's own reaction on the original ('upvote', 'downvote', ...), if any. */
|
|
191
|
+
user_reaction?: string | null;
|
|
192
|
+
/** Stored replies a reader can open. */
|
|
193
|
+
reply_count?: number;
|
|
194
|
+
/** A legacy import's comment count; none of those comments are stored as replies, so it is not in `reply_count`. */
|
|
195
|
+
reply_count_external?: number;
|
|
196
|
+
unique_commenters?: number;
|
|
197
|
+
reposts_count?: number;
|
|
198
|
+
/** Id of the viewer's own repost of the original; delete it to undo the repost. */
|
|
199
|
+
reposted_by_viewer_id?: string | null;
|
|
161
200
|
media: MediaItem[];
|
|
162
201
|
created_at: string;
|
|
163
202
|
}
|
|
@@ -178,6 +217,8 @@ export interface PostReply {
|
|
|
178
217
|
user_reaction?: string | null;
|
|
179
218
|
/** This reply's own reply count, so its sub-thread can be opened. */
|
|
180
219
|
reply_count?: number;
|
|
220
|
+
/** A legacy import's comment count for this reply; not stored as replies, so not in `reply_count`. */
|
|
221
|
+
reply_count_external?: number;
|
|
181
222
|
created_at: string;
|
|
182
223
|
}
|
|
183
224
|
export interface ListPostRepliesParams {
|
|
@@ -243,6 +284,11 @@ export interface ListPostsParams extends PaginationParams {
|
|
|
243
284
|
audience_user_id?: 'me' | string;
|
|
244
285
|
/** Return only posts the authenticated caller has upvoted. */
|
|
245
286
|
liked?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Return only the caller's saved posts, newest saved first, each with `bookmarked_at`. Takes only `limit`
|
|
289
|
+
* (at most 50) and `cursor`; see `posts.bookmarked` / `bookmarks.posts`.
|
|
290
|
+
*/
|
|
291
|
+
bookmarked?: boolean;
|
|
246
292
|
}
|
|
247
293
|
export interface SearchPostsParams extends PaginationParams {
|
|
248
294
|
q: string;
|
|
@@ -251,6 +297,42 @@ export interface SearchPostsParams extends PaginationParams {
|
|
|
251
297
|
sort?: 'relevance' | 'new';
|
|
252
298
|
since?: string;
|
|
253
299
|
until?: string;
|
|
300
|
+
/**
|
|
301
|
+
* The `meta.next_cursor` of the previous page. Later relevance pages are slices of the ranking the
|
|
302
|
+
* first page kept (ten minutes), so nothing is skipped or repeated while it lives; after that the page
|
|
303
|
+
* continues past the last post's score boundary. Newest-first pages continue by (created_at, id). A
|
|
304
|
+
* cursor is bound to its search and sort: another's is refused (400 `invalid_cursor`). Without one,
|
|
305
|
+
* `offset` counts.
|
|
306
|
+
*/
|
|
307
|
+
cursor?: string;
|
|
308
|
+
}
|
|
309
|
+
/** A way on from the end of a search: newest first, the open networks, or a topic the matches share. */
|
|
310
|
+
export type SearchFallOverLane = {
|
|
311
|
+
kind: 'new';
|
|
312
|
+
} | {
|
|
313
|
+
kind: 'open_network';
|
|
314
|
+
} | {
|
|
315
|
+
kind: 'tag';
|
|
316
|
+
id: string;
|
|
317
|
+
name: string;
|
|
318
|
+
slug: string;
|
|
319
|
+
};
|
|
320
|
+
/** One page of `posts.search`. */
|
|
321
|
+
export interface SearchPostsResponse extends PaginatedResponse<Post> {
|
|
322
|
+
/**
|
|
323
|
+
* On the first relevance page: up to three of the newest matches that are not on the page, newest
|
|
324
|
+
* first, one per author, for a "Latest" strip above the best matches. Absent when there are none, and on later pages.
|
|
325
|
+
*/
|
|
326
|
+
latest?: Post[];
|
|
327
|
+
/**
|
|
328
|
+
* On the last page (`meta.has_more` false): what exists past the end of this search, so a client
|
|
329
|
+
* can offer a way on instead of a wall. `new` when newest-first goes deeper than the ranking did;
|
|
330
|
+
* `open_network` when the caller's app can search the open networks; `tag` for the topic the
|
|
331
|
+
* page's posts share most. May be empty.
|
|
332
|
+
*/
|
|
333
|
+
fall_over?: {
|
|
334
|
+
lanes: SearchFallOverLane[];
|
|
335
|
+
};
|
|
254
336
|
}
|
|
255
337
|
export type QueryParams = Record<string, unknown>;
|
|
256
338
|
export interface CommunityCreator {
|
|
@@ -273,6 +355,8 @@ export interface Community {
|
|
|
273
355
|
post_count?: number;
|
|
274
356
|
/** Whether the caller is an accepted member. Returned by `list` and `search`. */
|
|
275
357
|
is_member?: boolean;
|
|
358
|
+
/** The caller's role in the group, or null outside it. Returned by `list` and the detail read; a group's creator reads as its owner. */
|
|
359
|
+
viewer_role?: 'owner' | 'admin' | 'moderator' | 'member' | null;
|
|
276
360
|
created_at: string;
|
|
277
361
|
}
|
|
278
362
|
/** Filters accepted by the communities directory. */
|
|
@@ -281,14 +365,36 @@ export interface ListCommunitiesParams extends PaginationParams {
|
|
|
281
365
|
member?: boolean;
|
|
282
366
|
/** Restrict personal keys to one organization. Project-bound keys are already scoped. */
|
|
283
367
|
organization_id?: string;
|
|
284
|
-
/**
|
|
285
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Server-side ordering. With `q`, name matches come first and this breaks ties. `joined` (with
|
|
370
|
+
* `member: true`) lists the caller's groups by when they joined, newest first.
|
|
371
|
+
*/
|
|
372
|
+
sort?: 'members' | 'active' | 'newest' | 'joined';
|
|
286
373
|
/**
|
|
287
374
|
* Case-insensitive text search over name and description, up to 200
|
|
288
375
|
* characters. With `member: true` it searches only the caller's own groups.
|
|
289
376
|
*/
|
|
290
377
|
q?: string;
|
|
291
378
|
}
|
|
379
|
+
/** Which join requests to answer at once: named people (at most 100), or the oldest 100 waiting. */
|
|
380
|
+
export type AnswerJoinRequestsInput = {
|
|
381
|
+
user_ids: string[];
|
|
382
|
+
} | {
|
|
383
|
+
all: true;
|
|
384
|
+
};
|
|
385
|
+
/** What a many-at-once answer did: how many it answered, who it skipped, and how many still wait. */
|
|
386
|
+
export interface ApprovedJoinRequests {
|
|
387
|
+
approved: number;
|
|
388
|
+
/** Named people with no pending request here: already in, already answered, banned, or never asked. */
|
|
389
|
+
skipped: string[];
|
|
390
|
+
/** Pending requests left in the group (bans not counted). */
|
|
391
|
+
remaining: number;
|
|
392
|
+
}
|
|
393
|
+
export interface DeclinedJoinRequests {
|
|
394
|
+
declined: number;
|
|
395
|
+
skipped: string[];
|
|
396
|
+
remaining: number;
|
|
397
|
+
}
|
|
292
398
|
/** The caller's own membership row in a group, if any. */
|
|
293
399
|
export type CommunityMembershipStatus = 'accepted' | 'pending' | 'invited' | 'banned';
|
|
294
400
|
export interface CommunityDetail extends Community {
|
|
@@ -862,6 +968,14 @@ export interface Profile {
|
|
|
862
968
|
/** A row of a followers/following list: the profile plus the caller's relationship with it. */
|
|
863
969
|
export interface FollowListProfile extends Profile, ViewerRelationship {
|
|
864
970
|
}
|
|
971
|
+
/** Paging and search for a follower or following list (profiles.followers / following). */
|
|
972
|
+
export interface FollowListParams {
|
|
973
|
+
limit?: number;
|
|
974
|
+
offset?: number;
|
|
975
|
+
/** Name or handle, matched anywhere in either (at most 200 characters); a blank term lists everyone. */
|
|
976
|
+
search?: string;
|
|
977
|
+
[key: string]: unknown;
|
|
978
|
+
}
|
|
865
979
|
/** An account the caller blocked. */
|
|
866
980
|
export interface BlockedProfile extends Profile {
|
|
867
981
|
blocked_at: string;
|
|
@@ -901,10 +1015,24 @@ export interface UpdateProfileInput {
|
|
|
901
1015
|
username?: string;
|
|
902
1016
|
bio?: string | null;
|
|
903
1017
|
image?: string | null;
|
|
1018
|
+
/** The profile banner as an absolute image URL, or null to remove it (PUT /profiles/me has accepted it since #1867). */
|
|
1019
|
+
banner?: string | null;
|
|
904
1020
|
}
|
|
905
1021
|
export interface SearchProfilesParams extends PaginationParams {
|
|
906
1022
|
q: string;
|
|
907
1023
|
}
|
|
1024
|
+
/** A row of `profiles.search`: the profile plus what the ranking read (match x ln(1 + followers + min(posts, 100))). */
|
|
1025
|
+
export interface ProfileSearchResult extends Profile {
|
|
1026
|
+
followers_count: number;
|
|
1027
|
+
posts_count: number;
|
|
1028
|
+
/** Nobody follows it and it never posted: listed after every account with a public life, for a client to fold under "more handles". */
|
|
1029
|
+
empty: boolean;
|
|
1030
|
+
/** An account the operator vouches for: draw the verified badge. */
|
|
1031
|
+
verified: boolean;
|
|
1032
|
+
plus: boolean;
|
|
1033
|
+
pro: boolean;
|
|
1034
|
+
founder: boolean;
|
|
1035
|
+
}
|
|
908
1036
|
export interface SearchMentionsParams {
|
|
909
1037
|
/** What the member typed after `@` (a leading `@` is ignored). */
|
|
910
1038
|
q: string;
|
package/llm.md
CHANGED
|
@@ -479,7 +479,7 @@ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_fo
|
|
|
479
479
|
await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
-
Methods: `list`, `me`, `get`, `getByUsername`, `search`, `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing`, `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
|
|
482
|
+
Methods: `list`, `me`, `get`, `getByUsername`, `search` (ranked by match x ln(1 + followers + min(posts, 100)) over one pool of 200, the same for every page; each row (`ProfileSearchResult`) carries `followers_count`, `posts_count`, `empty` (fold empties under "more handles") and `verified` (the accounts the operator names)), `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing`, `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
@@ -580,7 +580,36 @@ await r.posts.react(post.id, { type: 'fire' })
|
|
|
580
580
|
const { data: results } = await r.posts.search({ query: 'shipped', community_id: 'comm_xxx' })
|
|
581
581
|
```
|
|
582
582
|
|
|
583
|
-
Methods: `list`, `get`, `create`, `update`, `delete`, `search
|
|
583
|
+
Methods: `list`, `get`, `create`, `update`, `delete`, `search`, `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`, `liked`, `bookmarked`, `bookmark`, `unbookmark`
|
|
584
|
+
|
|
585
|
+
`search` ranks relevance x recency x engagement (recency falls as a power of age: this month outranks years ago) through the promotion safety floor; `sort: 'new'` lists newest first. Page with `meta.next_cursor` as `cursor`: later relevance pages are slices of the ranking the first page kept for ten minutes (nothing skipped or repeated while it lives; after that, the page continues past the last post's score boundary), newest-first pages continue by (created_at, id). A cursor is bound to its search and sort: another search's, or the other sort's, is 400 `invalid_cursor`. The first relevance page carries `latest` (up to three of the newest matches not on the page, one per author, for a Latest strip); the last page carries `fall_over.lanes` (`new`, `open_network`, `tag`): draw a way on, not a wall.
|
|
586
|
+
|
|
587
|
+
### r.bookmarks — Synced saves (per member, per app)
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
await r.bookmarks.save({ kind: 'post', post_id: post.id }) // idempotent; a bare repost saves its original (see data.post_id)
|
|
591
|
+
await r.bookmarks.save({ kind: 'social', protocol: 'bluesky', native_id: 'at://did:plc:.../app.bsky.feed.post/...',
|
|
592
|
+
author: { display_name: 'Alice', handle: 'alice.bsky.social' }, snippet: 'hello' })
|
|
593
|
+
await r.bookmarks.save({ kind: 'article', url: 'https://news.example/a', title: 'Headline', outlet: 'Example' })
|
|
594
|
+
const { data: saved, meta } = await r.bookmarks.posts({ limit: 20 }) // = r.posts.bookmarked(); page on meta.next_cursor
|
|
595
|
+
const { data: items } = await r.bookmarks.list({ kind: 'social' }) // open-network posts and articles, as saved snapshots
|
|
596
|
+
const { data: keys } = await r.bookmarks.keys(undefined, { retry: false }) // every key, for a saved-state cache
|
|
597
|
+
const { data: merged } = await r.bookmarks.merge(deviceItems) // ≤200 per call, each with its own saved_at; idempotent
|
|
598
|
+
await r.bookmarks.unsave(key) // or unsave({ postId }); r.posts.bookmark/unbookmark too
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Newest saved first. Pages can be short while they skip posts the member can no longer read: page on `meta.next_cursor`, not length. Needs the member's own app key (else 403 `project_binding_required`). 503 `bookmarks_unavailable` means the server has no synced bookmarks yet: keep saves on the device (pass `{ retry: false }` to the probe so the 503 is not retried).
|
|
602
|
+
|
|
603
|
+
### r.search
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// One query, answered as sections. Each has its own time budget; a slow one is listed in `omitted`.
|
|
607
|
+
const { data: answer } = await r.search.query({ q: 'apple', limit: 6 })
|
|
608
|
+
for (const section of answer.order) render(section, answer.sections[section])
|
|
609
|
+
// top_posts, latest_posts, open_network, people, groups, topics, stories, related, trending
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`trending` joins when the answer is thin or old, so a search never dead-ends. `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`). Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
584
613
|
|
|
585
614
|
### r.communities
|
|
586
615
|
|
|
@@ -649,7 +678,7 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
649
678
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
650
679
|
- `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
|
|
651
680
|
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
|
|
652
|
-
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
681
|
+
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts; an `agent_needs_input` row carries `answered`, true once you have written in the agent's thread since it asked, when its title reads "You answered <agent>"), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
653
682
|
- `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
|
|
654
683
|
- `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
|
|
655
684
|
- `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
|