@recursiv/sdk 0.7.66 → 0.7.68
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 +8 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/resources/bookmarks.d.ts +138 -4
- package/dist/resources/bookmarks.js +72 -3
- package/dist/resources/curator.d.ts +19 -1
- package/dist/resources/curator.js +2 -2
- package/dist/resources/feeds.d.ts +6 -0
- package/dist/resources/network.d.ts +6 -0
- package/dist/resources/protocols.d.ts +10 -3
- package/dist/types.d.ts +20 -0
- package/llm.md +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
94
94
|
| Resource | Key methods |
|
|
95
95
|
|----------|------------|
|
|
96
96
|
| `r.posts` | `create`, `update`, `list`, `liked`, `bookmarked`, `bookmark`, `unbookmark`, `search`, `replies`, `react` |
|
|
97
|
-
| `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices |
|
|
97
|
+
| `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices; `collections`: `list`, `create`, `update`, `remove`, `add`, `removeItem`, `items`, `posts` — private named collections of saves (0.7.68, where `network.getConfig()` advertises `capabilities.bookmark_collections`) |
|
|
98
98
|
| `r.feeds` | `top` (what the member is most likely to engage with, rotated by what they saw, with open-network lanes and the lead story), `stories` (the story tail), `publicTop` (a visitor's top, keyless and edge-cacheable) |
|
|
99
99
|
| `r.communities` | `create`, `join`, `search`, `members` |
|
|
100
100
|
| `r.chat` | `send`, `dm`, `createGroup` (`encrypted: true` for an end-to-end encrypted group, 0.7.59), `messages` (cursor paging), `acceptRequest`, `declineRequest`, `leave`, `deleteConversation`; a private group's admin: `addMembers`, `removeMember`, `updateGroup` (0.7.59) |
|
|
@@ -732,7 +732,13 @@ The catalog lists `compass_filter` on every network when the filter is served to
|
|
|
732
732
|
Hide politics (`compass: 'hide'`) also applies on its own, with or without compass judgments on the server, to
|
|
733
733
|
`sdk.discovery.videos`, `protocols.getPublicTrends` and the radar (`getPublicRadar`, keyless `getSharedRadar`; 0.7.62):
|
|
734
734
|
it leaves out what is categorised politics (a story not themed yet, or `mixed`, too) and says what it did in
|
|
735
|
-
`meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`.
|
|
735
|
+
`meta.compass_filter`. These reads answer a side or Mix unfiltered, marked `unavailable`. Every radar story and every
|
|
736
|
+
World Today story (`curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its
|
|
737
|
+
theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice,
|
|
738
|
+
a legislature, or a party or politician. Hide politics leaves those out whatever their theme, on the radar and on
|
|
739
|
+
`curator.today({ compass: 'hide' })`, where the next story then leads. The stories of `feeds.stories()` and the lead of
|
|
740
|
+
`feeds.top()` carry `political` too (0.7.67), and those reads and `curator.forYouWorld()` leave political stories out
|
|
741
|
+
under Hide politics.
|
|
736
742
|
|
|
737
743
|
`sdk.discovery.recommendations({ kind, category?, limit?, session?, exclude? })` is
|
|
738
744
|
the app's one recommender for suggestion rails: `kind` is `member`, `group`, `tag`,
|
package/dist/index.d.ts
CHANGED
|
@@ -195,7 +195,7 @@ export { VideoResource } from './resources/video.js';
|
|
|
195
195
|
export type { VideoUploadCreds, CreateVideoUploadResult, VideoStatusInfo, VideoPlaybackToken, VideoEntitlement, } from './resources/video.js';
|
|
196
196
|
export { AppSubscriptionsResource } from './resources/app-subscriptions.js';
|
|
197
197
|
export type { AppSubscriptionBoostCredit, AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, AppSubscriptionTierChangePreview, AppSubscriptionTierChangeResult, } from './resources/app-subscriptions.js';
|
|
198
|
-
export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
|
|
198
|
+
export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayParams, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
|
|
199
199
|
export { ReportsResource } from './resources/reports.js';
|
|
200
200
|
export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
|
|
201
201
|
export { ModerationResource } from './resources/moderation.js';
|
|
@@ -203,10 +203,10 @@ export type { ModerationTargetType, ModerationActionType, ModerationAppealStatus
|
|
|
203
203
|
export { LinkPreviewResource } from './resources/link-preview.js';
|
|
204
204
|
export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
|
|
205
205
|
export type { SearchAnswer, SearchGroup, SearchOmitReason, SearchParams, SearchPerson, SearchRelated, SearchResponse, SearchSection, SearchStory, SearchTopic } from './resources/search.js';
|
|
206
|
-
export { BOOKMARK_MERGE_MAX_ITEMS, BookmarksResource } from './resources/bookmarks.js';
|
|
206
|
+
export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
|
|
207
207
|
export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
|
|
208
208
|
export type { FeedStoriesParams, FeedStoriesResponse, FeedStory, FeedTopExplain, FeedTopItem, FeedTopMeta, FeedTopNetwork, FeedTopNetworkCard, FeedTopNetworkEntry, FeedTopParams, FeedTopResponse, FeedTopTarget, PublicFeedTopMeta, PublicFeedTopParams, PublicFeedTopResponse, } from './resources/feeds.js';
|
|
209
|
-
export type { BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, SavedBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
|
|
209
|
+
export type { BookmarkCollection, BookmarkCollectionInput, BookmarkCollectionKind, BookmarkCollectionPatch, BookmarkCollectionsList, BookmarkCollectionTarget, BookmarkCollectionVisibility, BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKeysPage, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, FiledBookmark, RemovedBookmarkCollection, SavedBookmark, UnfiledBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
|
|
210
210
|
export { AccountsResource } from './resources/accounts.js';
|
|
211
211
|
export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
|
|
212
212
|
export type { LinkPreview, LinkPreviewResponse, } from './resources/link-preview.js';
|
package/dist/index.js
CHANGED
|
@@ -257,7 +257,7 @@ export { ReportsResource } from './resources/reports.js';
|
|
|
257
257
|
export { ModerationResource } from './resources/moderation.js';
|
|
258
258
|
export { LinkPreviewResource } from './resources/link-preview.js';
|
|
259
259
|
export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
|
|
260
|
-
export { BOOKMARK_MERGE_MAX_ITEMS, BookmarksResource } from './resources/bookmarks.js';
|
|
260
|
+
export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
|
|
261
261
|
export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
|
|
262
262
|
export { AccountsResource } from './resources/accounts.js';
|
|
263
263
|
export { OrganizationSettingsResource } from './resources/organization-settings.js';
|
|
@@ -122,6 +122,83 @@ export interface BookmarksPage<T> extends PaginatedResponse<T> {
|
|
|
122
122
|
next_cursor: string | null;
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
/** A page of saved keys. `collections` (0.7.68) is there only where the server serves collections. */
|
|
126
|
+
export interface BookmarkKeysPage extends BookmarksPage<BookmarkKeyRow> {
|
|
127
|
+
/**
|
|
128
|
+
* For this page's keys that are filed somewhere: the ids of the collections each sits in, oldest
|
|
129
|
+
* collection first. A key filed nowhere is absent. The whole field is absent while collections are
|
|
130
|
+
* off on the server, so its presence also says collections can be used.
|
|
131
|
+
*/
|
|
132
|
+
collections?: Record<BookmarkKey, string[]>;
|
|
133
|
+
}
|
|
134
|
+
/** What a collection holds. Before the cutover only `posts` collections are made (any saved thing: posts, open-network posts, articles). */
|
|
135
|
+
export type BookmarkCollectionKind = 'posts' | 'accounts' | 'tags';
|
|
136
|
+
/** Who may see a collection. Before the cutover every collection is `private`. */
|
|
137
|
+
export type BookmarkCollectionVisibility = 'private' | 'unlisted' | 'public';
|
|
138
|
+
/** A member's named collection of saves in this app (0.7.68). Collections are labels on saves. */
|
|
139
|
+
export interface BookmarkCollection {
|
|
140
|
+
id: string;
|
|
141
|
+
kind: BookmarkCollectionKind;
|
|
142
|
+
/** Unique per member and app: lowercase letters and digits joined by hyphens, at most 64 characters. */
|
|
143
|
+
slug: string;
|
|
144
|
+
/** At most 120 characters. */
|
|
145
|
+
title: string;
|
|
146
|
+
/** At most 500 characters. */
|
|
147
|
+
description: string | null;
|
|
148
|
+
visibility: BookmarkCollectionVisibility;
|
|
149
|
+
item_count: number;
|
|
150
|
+
created_at: string;
|
|
151
|
+
updated_at: string;
|
|
152
|
+
}
|
|
153
|
+
export interface BookmarkCollectionsList {
|
|
154
|
+
/** Oldest first. */
|
|
155
|
+
data: BookmarkCollection[];
|
|
156
|
+
/** The most collections one member keeps in this app, and the most things one collection holds. */
|
|
157
|
+
meta: {
|
|
158
|
+
max_collections: number;
|
|
159
|
+
max_items: number;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** A new collection. Without `slug` one is made from the title (made unique with -2, -3, ...). */
|
|
163
|
+
export interface BookmarkCollectionInput {
|
|
164
|
+
title: string;
|
|
165
|
+
description?: string | null;
|
|
166
|
+
slug?: string;
|
|
167
|
+
}
|
|
168
|
+
/** Any of a collection's title, description (null clears it) and slug. */
|
|
169
|
+
export interface BookmarkCollectionPatch {
|
|
170
|
+
title?: string;
|
|
171
|
+
description?: string | null;
|
|
172
|
+
slug?: string;
|
|
173
|
+
}
|
|
174
|
+
export interface RemovedBookmarkCollection {
|
|
175
|
+
id: string;
|
|
176
|
+
removed: boolean;
|
|
177
|
+
}
|
|
178
|
+
export interface FiledBookmark {
|
|
179
|
+
collection_id: string;
|
|
180
|
+
/** The saved thing's key. A bare repost is filed as its original, as saving one is: use this key and `post_id`. */
|
|
181
|
+
key: BookmarkKey;
|
|
182
|
+
filed: true;
|
|
183
|
+
/** False when it was already in the collection: nothing changed. */
|
|
184
|
+
created: boolean;
|
|
185
|
+
/** True when filing it saved it too. */
|
|
186
|
+
saved_now: boolean;
|
|
187
|
+
added_at: string;
|
|
188
|
+
item_count: number;
|
|
189
|
+
post_id?: string;
|
|
190
|
+
}
|
|
191
|
+
export interface UnfiledBookmark {
|
|
192
|
+
collection_id: string;
|
|
193
|
+
key: BookmarkKey;
|
|
194
|
+
filed: false;
|
|
195
|
+
/** False when it was not in the collection. It stays saved either way. */
|
|
196
|
+
removed: boolean;
|
|
197
|
+
}
|
|
198
|
+
/** What to file: a saved key, a Minds post, or an item (saved first if it is not saved yet). */
|
|
199
|
+
export type BookmarkCollectionTarget = BookmarkKey | {
|
|
200
|
+
postId: string;
|
|
201
|
+
} | BookmarkInput;
|
|
125
202
|
/** Most items one `merge` call takes. */
|
|
126
203
|
export declare const BOOKMARK_MERGE_MAX_ITEMS = 200;
|
|
127
204
|
/**
|
|
@@ -131,6 +208,56 @@ export declare const BOOKMARK_MERGE_MAX_ITEMS = 200;
|
|
|
131
208
|
export interface BookmarkRequestOptions {
|
|
132
209
|
retry?: boolean;
|
|
133
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Bookmark collections (0.7.68): the member's private, named labels on their saves in this app.
|
|
213
|
+
* Filing something saves it first; one saved thing can sit in several collections; unsaving it takes it
|
|
214
|
+
* out of every one; deleting a collection leaves everything it held saved.
|
|
215
|
+
*
|
|
216
|
+
* Off on a server until it advertises `capabilities.bookmark_collections` (`network.getConfig()`): until
|
|
217
|
+
* then, and until its tables are applied, every call rejects with a 503 whose code is `lists_unavailable`
|
|
218
|
+
* (saving still works). A collection that is not the member's is 404 `collection_not_found`. Caps: 409
|
|
219
|
+
* `collection_limit` (collections per member) and `collection_item_limit` (things per collection).
|
|
220
|
+
*/
|
|
221
|
+
export declare class BookmarkCollectionsResource {
|
|
222
|
+
private client;
|
|
223
|
+
constructor(client: HttpClient);
|
|
224
|
+
/** The member's collections in this app, oldest first, with the caps. */
|
|
225
|
+
list(options?: BookmarkRequestOptions): Promise<BookmarkCollectionsList>;
|
|
226
|
+
/**
|
|
227
|
+
* Make a private collection. 409 `slug_taken` for a slug the member already uses. Retried on a 5xx like any
|
|
228
|
+
* POST: pass an `idempotencyKey` so a retry cannot make a second collection.
|
|
229
|
+
*/
|
|
230
|
+
create(input: BookmarkCollectionInput, options?: BookmarkRequestOptions & {
|
|
231
|
+
idempotencyKey?: string;
|
|
232
|
+
}): Promise<SingleResponse<BookmarkCollection>>;
|
|
233
|
+
/** Rename, re-describe or re-slug a collection. */
|
|
234
|
+
update(collectionId: string, patch: BookmarkCollectionPatch): Promise<SingleResponse<BookmarkCollection>>;
|
|
235
|
+
/** Delete a collection. Idempotent; what it held stays saved. */
|
|
236
|
+
remove(collectionId: string, options?: BookmarkRequestOptions): Promise<SingleResponse<RemovedBookmarkCollection>>;
|
|
237
|
+
/**
|
|
238
|
+
* File something in a collection; idempotent. A key or `{ postId }` files what is saved, and a Minds post is
|
|
239
|
+
* saved first when it is not (404 `post_not_found` if the member cannot read it); a key alone that is not saved
|
|
240
|
+
* is 409 `not_saved`. An item (a Minds post, open-network post or article) goes in one request that saves it if
|
|
241
|
+
* it is not saved and files it together: when the collection is missing (404), full (409 `collection_item_limit`)
|
|
242
|
+
* or unavailable (503), or the member's saves are at their cap (409 `bookmark_limit`), nothing is saved.
|
|
243
|
+
*/
|
|
244
|
+
add(collectionId: string, target: BookmarkCollectionTarget, options?: BookmarkRequestOptions): Promise<SingleResponse<FiledBookmark>>;
|
|
245
|
+
/** Take something out of a collection. Idempotent; it stays saved, and in any other collection. */
|
|
246
|
+
removeItem(collectionId: string, target: BookmarkKey | {
|
|
247
|
+
postId: string;
|
|
248
|
+
}, options?: BookmarkRequestOptions): Promise<SingleResponse<UnfiledBookmark>>;
|
|
249
|
+
/** The open-network posts and articles filed in a collection, newest saved first. Page on `meta.next_cursor`. */
|
|
250
|
+
items(collectionId: string, params?: {
|
|
251
|
+
kind?: 'social' | 'article';
|
|
252
|
+
limit?: number;
|
|
253
|
+
cursor?: string;
|
|
254
|
+
}, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkItem>>;
|
|
255
|
+
/** The Minds posts filed in a collection, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
|
|
256
|
+
posts(collectionId: string, params?: {
|
|
257
|
+
limit?: number;
|
|
258
|
+
cursor?: string;
|
|
259
|
+
}, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkedPost>>;
|
|
260
|
+
}
|
|
134
261
|
/**
|
|
135
262
|
* Synced bookmarks: the signed-in member's saved Minds posts, open-network posts and articles in this app,
|
|
136
263
|
* across every device they sign in on. Private to the member; needs the member's own app key.
|
|
@@ -140,6 +267,8 @@ export interface BookmarkRequestOptions {
|
|
|
140
267
|
*/
|
|
141
268
|
export declare class BookmarksResource {
|
|
142
269
|
private client;
|
|
270
|
+
/** Named collections of the member's saves (0.7.68). */
|
|
271
|
+
readonly collections: BookmarkCollectionsResource;
|
|
143
272
|
constructor(client: HttpClient);
|
|
144
273
|
/** Save one item. Idempotent: saving what is saved changes nothing. 409 `bookmark_limit` past the member's limit. */
|
|
145
274
|
save(item: BookmarkInput, options?: BookmarkRequestOptions): Promise<SingleResponse<SavedBookmark>>;
|
|
@@ -147,22 +276,27 @@ export declare class BookmarksResource {
|
|
|
147
276
|
unsave(target: BookmarkKey | {
|
|
148
277
|
postId: string;
|
|
149
278
|
}, options?: BookmarkRequestOptions): Promise<SingleResponse<UnsavedBookmark>>;
|
|
150
|
-
/** Saved Minds posts, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
|
|
279
|
+
/** Saved Minds posts, newest saved first, hydrated like the feed; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
|
|
151
280
|
posts(params?: {
|
|
152
281
|
limit?: number;
|
|
153
282
|
cursor?: string;
|
|
283
|
+
collection?: string;
|
|
154
284
|
}, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkedPost>>;
|
|
155
|
-
/** Saved open-network posts and articles, newest saved first. Page on `meta.next_cursor`. */
|
|
285
|
+
/** Saved open-network posts and articles, newest saved first; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
|
|
156
286
|
list(params?: {
|
|
157
287
|
kind?: 'social' | 'article';
|
|
158
288
|
limit?: number;
|
|
159
289
|
cursor?: string;
|
|
290
|
+
collection?: string;
|
|
160
291
|
}, options?: BookmarkRequestOptions): Promise<BookmarksPage<BookmarkItem>>;
|
|
161
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache.
|
|
294
|
+
* Where collections are served, `collections` says which collections this page's keys are filed in.
|
|
295
|
+
*/
|
|
162
296
|
keys(params?: {
|
|
163
297
|
limit?: number;
|
|
164
298
|
cursor?: string;
|
|
165
|
-
}, options?: BookmarkRequestOptions): Promise<
|
|
299
|
+
}, options?: BookmarkRequestOptions): Promise<BookmarkKeysPage>;
|
|
166
300
|
/**
|
|
167
301
|
* Upload a device's saves, e.g. when its member signs in: at most 200 items a call. Idempotent (a second
|
|
168
302
|
* merge of the same items is a no-op); invalid items and posts the member cannot read are `skipped`, and
|
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
/** Most items one `merge` call takes. */
|
|
2
2
|
export const BOOKMARK_MERGE_MAX_ITEMS = 200;
|
|
3
|
+
const itemPath = (collectionId, key) => `/bookmarks/collections/${encodeURIComponent(collectionId)}/items/${encodeURIComponent(key)}`;
|
|
4
|
+
/**
|
|
5
|
+
* Bookmark collections (0.7.68): the member's private, named labels on their saves in this app.
|
|
6
|
+
* Filing something saves it first; one saved thing can sit in several collections; unsaving it takes it
|
|
7
|
+
* out of every one; deleting a collection leaves everything it held saved.
|
|
8
|
+
*
|
|
9
|
+
* Off on a server until it advertises `capabilities.bookmark_collections` (`network.getConfig()`): until
|
|
10
|
+
* then, and until its tables are applied, every call rejects with a 503 whose code is `lists_unavailable`
|
|
11
|
+
* (saving still works). A collection that is not the member's is 404 `collection_not_found`. Caps: 409
|
|
12
|
+
* `collection_limit` (collections per member) and `collection_item_limit` (things per collection).
|
|
13
|
+
*/
|
|
14
|
+
export class BookmarkCollectionsResource {
|
|
15
|
+
client;
|
|
16
|
+
constructor(client) {
|
|
17
|
+
this.client = client;
|
|
18
|
+
}
|
|
19
|
+
/** The member's collections in this app, oldest first, with the caps. */
|
|
20
|
+
list(options) {
|
|
21
|
+
return this.client.get('/bookmarks/collections', undefined, options);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Make a private collection. 409 `slug_taken` for a slug the member already uses. Retried on a 5xx like any
|
|
25
|
+
* POST: pass an `idempotencyKey` so a retry cannot make a second collection.
|
|
26
|
+
*/
|
|
27
|
+
create(input, options) {
|
|
28
|
+
return this.client.post('/bookmarks/collections', input, options);
|
|
29
|
+
}
|
|
30
|
+
/** Rename, re-describe or re-slug a collection. */
|
|
31
|
+
update(collectionId, patch) {
|
|
32
|
+
return this.client.patch(`/bookmarks/collections/${encodeURIComponent(collectionId)}`, patch);
|
|
33
|
+
}
|
|
34
|
+
/** Delete a collection. Idempotent; what it held stays saved. */
|
|
35
|
+
remove(collectionId, options) {
|
|
36
|
+
return this.client.delete(`/bookmarks/collections/${encodeURIComponent(collectionId)}`, options);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* File something in a collection; idempotent. A key or `{ postId }` files what is saved, and a Minds post is
|
|
40
|
+
* saved first when it is not (404 `post_not_found` if the member cannot read it); a key alone that is not saved
|
|
41
|
+
* is 409 `not_saved`. An item (a Minds post, open-network post or article) goes in one request that saves it if
|
|
42
|
+
* it is not saved and files it together: when the collection is missing (404), full (409 `collection_item_limit`)
|
|
43
|
+
* or unavailable (503), or the member's saves are at their cap (409 `bookmark_limit`), nothing is saved.
|
|
44
|
+
*/
|
|
45
|
+
add(collectionId, target, options) {
|
|
46
|
+
if (typeof target === 'string')
|
|
47
|
+
return this.client.put(itemPath(collectionId, target), undefined, options);
|
|
48
|
+
if ('postId' in target)
|
|
49
|
+
return this.client.put(itemPath(collectionId, `post:${target.postId}`), undefined, options);
|
|
50
|
+
return this.client.post(`/bookmarks/collections/${encodeURIComponent(collectionId)}/items`, target, options);
|
|
51
|
+
}
|
|
52
|
+
/** Take something out of a collection. Idempotent; it stays saved, and in any other collection. */
|
|
53
|
+
removeItem(collectionId, target, options) {
|
|
54
|
+
const key = typeof target === 'string' ? target : `post:${target.postId}`;
|
|
55
|
+
return this.client.delete(itemPath(collectionId, key), options);
|
|
56
|
+
}
|
|
57
|
+
/** The open-network posts and articles filed in a collection, newest saved first. Page on `meta.next_cursor`. */
|
|
58
|
+
items(collectionId, params, options) {
|
|
59
|
+
return this.client.get('/bookmarks', { ...params, collection: collectionId }, options);
|
|
60
|
+
}
|
|
61
|
+
/** The Minds posts filed in a collection, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
|
|
62
|
+
posts(collectionId, params, options) {
|
|
63
|
+
return this.client.get('/posts', { bookmarked: true, ...params, collection: collectionId }, options);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
3
66
|
/**
|
|
4
67
|
* Synced bookmarks: the signed-in member's saved Minds posts, open-network posts and articles in this app,
|
|
5
68
|
* across every device they sign in on. Private to the member; needs the member's own app key.
|
|
@@ -9,8 +72,11 @@ export const BOOKMARK_MERGE_MAX_ITEMS = 200;
|
|
|
9
72
|
*/
|
|
10
73
|
export class BookmarksResource {
|
|
11
74
|
client;
|
|
75
|
+
/** Named collections of the member's saves (0.7.68). */
|
|
76
|
+
collections;
|
|
12
77
|
constructor(client) {
|
|
13
78
|
this.client = client;
|
|
79
|
+
this.collections = new BookmarkCollectionsResource(client);
|
|
14
80
|
}
|
|
15
81
|
/** Save one item. Idempotent: saving what is saved changes nothing. 409 `bookmark_limit` past the member's limit. */
|
|
16
82
|
save(item, options) {
|
|
@@ -21,15 +87,18 @@ export class BookmarksResource {
|
|
|
21
87
|
const key = typeof target === 'string' ? target : `post:${target.postId}`;
|
|
22
88
|
return this.client.delete(`/bookmarks/${encodeURIComponent(key)}`, options);
|
|
23
89
|
}
|
|
24
|
-
/** Saved Minds posts, newest saved first, hydrated like the feed. Page on `meta.next_cursor`. */
|
|
90
|
+
/** Saved Minds posts, newest saved first, hydrated like the feed; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
|
|
25
91
|
posts(params, options) {
|
|
26
92
|
return this.client.get('/posts', { bookmarked: true, ...params }, options);
|
|
27
93
|
}
|
|
28
|
-
/** Saved open-network posts and articles, newest saved first. Page on `meta.next_cursor`. */
|
|
94
|
+
/** Saved open-network posts and articles, newest saved first; `collection` narrows to one collection (0.7.68). Page on `meta.next_cursor`. */
|
|
29
95
|
list(params, options) {
|
|
30
96
|
return this.client.get('/bookmarks', params, options);
|
|
31
97
|
}
|
|
32
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Every saved key (hidden posts included), newest saved first, up to 500 a page. For a saved-state cache.
|
|
100
|
+
* Where collections are served, `collections` says which collections this page's keys are filed in.
|
|
101
|
+
*/
|
|
33
102
|
keys(params, options) {
|
|
34
103
|
return this.client.get('/bookmarks/keys', params, options);
|
|
35
104
|
}
|
|
@@ -344,6 +344,12 @@ export interface CuratorTodayStory {
|
|
|
344
344
|
headline: string;
|
|
345
345
|
dek: string | null;
|
|
346
346
|
topic: string | null;
|
|
347
|
+
/**
|
|
348
|
+
* Whether the story is political: its headline, dek or topic names an election, a war or troop deployment, a court or
|
|
349
|
+
* justice, a legislature, or a party or politician, or the radar story it seeded is political. Hide politics
|
|
350
|
+
* (`today({ compass: 'hide' })`) leaves these out. Absent from a server before it was served.
|
|
351
|
+
*/
|
|
352
|
+
political?: boolean;
|
|
347
353
|
/** Outlet links for a world story; null for a Minds story. */
|
|
348
354
|
sources: CuratorTodaySource[] | null;
|
|
349
355
|
/** Distinct outlets behind a world story; null for a Minds story. */
|
|
@@ -380,10 +386,22 @@ export interface CuratorTodayEdition {
|
|
|
380
386
|
export interface CuratorTodayResponse {
|
|
381
387
|
/** null when the network has no published edition yet. */
|
|
382
388
|
data: CuratorTodayEdition | null;
|
|
389
|
+
/**
|
|
390
|
+
* `compass_filter`: present when the read named a filtering `compass`: what Hide politics did to the stories (counts
|
|
391
|
+
* only), or status 'unavailable' when it was not applied (not served to this reader yet, or a side or Mix).
|
|
392
|
+
*/
|
|
383
393
|
meta: {
|
|
384
394
|
generating: boolean;
|
|
395
|
+
compass_filter?: CompassFilterMeta;
|
|
385
396
|
};
|
|
386
397
|
}
|
|
398
|
+
export interface CuratorTodayParams {
|
|
399
|
+
/**
|
|
400
|
+
* A Politics choice. Hide politics (`hide`) leaves out the political stories (CuratorTodayStory.political), and the
|
|
401
|
+
* next story leads. Any other choice is answered unfiltered, marked `unavailable`.
|
|
402
|
+
*/
|
|
403
|
+
compass?: string;
|
|
404
|
+
}
|
|
387
405
|
export declare class CuratorResource {
|
|
388
406
|
private client;
|
|
389
407
|
constructor(client: HttpClient);
|
|
@@ -392,7 +410,7 @@ export declare class CuratorResource {
|
|
|
392
410
|
* (set by the open web, ranked by outlet count) come first; Minds
|
|
393
411
|
* conversation stories follow. Requires `posts:read`.
|
|
394
412
|
*/
|
|
395
|
-
today(): Promise<CuratorTodayResponse>;
|
|
413
|
+
today(params?: CuratorTodayParams): Promise<CuratorTodayResponse>;
|
|
396
414
|
/**
|
|
397
415
|
* Fetch the supplied sources in parallel, annotate each top item with
|
|
398
416
|
* the supplied prompt template, and insert audience-scoped posts into
|
|
@@ -8,8 +8,8 @@ export class CuratorResource {
|
|
|
8
8
|
* (set by the open web, ranked by outlet count) come first; Minds
|
|
9
9
|
* conversation stories follow. Requires `posts:read`.
|
|
10
10
|
*/
|
|
11
|
-
today() {
|
|
12
|
-
return this.client.get('/curator/today');
|
|
11
|
+
today(params = {}) {
|
|
12
|
+
return params.compass ? this.client.get('/curator/today', { compass: params.compass }) : this.client.get('/curator/today');
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Fetch the supplied sources in parallel, annotate each top item with
|
|
@@ -97,6 +97,12 @@ export interface FeedStory {
|
|
|
97
97
|
kind: 'breaking' | 'viral' | 'story';
|
|
98
98
|
breaking: boolean;
|
|
99
99
|
theme: string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the story is political whatever its theme: its headline names an election, a war or troop deployment, a
|
|
102
|
+
* court or justice, a legislature, or a party or politician, its theme is politics, or a quarter of its judged posts
|
|
103
|
+
* are politics. Hide politics (`compass: 'hide'`) leaves such a story out. Absent from a server older than 0.7.67.
|
|
104
|
+
*/
|
|
105
|
+
political?: boolean;
|
|
100
106
|
sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
|
|
101
107
|
media: {
|
|
102
108
|
images: number;
|
|
@@ -7,8 +7,14 @@ interface NetworkConfig {
|
|
|
7
7
|
primary_color: string | null;
|
|
8
8
|
theme: string | null;
|
|
9
9
|
features: Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* `bookmark_collections` (0.7.68; absent on older servers): this server serves bookmark collections
|
|
12
|
+
* (`bookmarks.collections`). Show collections only when it is true, and still treat a 503
|
|
13
|
+
* `lists_unavailable` as off.
|
|
14
|
+
*/
|
|
10
15
|
capabilities: {
|
|
11
16
|
ws_token: boolean;
|
|
17
|
+
bookmark_collections?: boolean;
|
|
12
18
|
};
|
|
13
19
|
auth_methods: string;
|
|
14
20
|
default_template_name: string | null;
|
|
@@ -1206,6 +1206,12 @@ export interface SocialRadarCluster {
|
|
|
1206
1206
|
};
|
|
1207
1207
|
/** A browse-category id or `mixed`, from cached judgments only. */
|
|
1208
1208
|
theme?: string;
|
|
1209
|
+
/**
|
|
1210
|
+
* Whether the story is political, whatever its theme: a quarter or more of its judged posts are politics, its theme is
|
|
1211
|
+
* politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature,
|
|
1212
|
+
* or a party or politician. Hide politics (`compass: 'hide'`) leaves these out. Absent from a server before it was served.
|
|
1213
|
+
*/
|
|
1214
|
+
political?: boolean;
|
|
1209
1215
|
sources: Partial<Record<SocialRadarSourceKind, number>>;
|
|
1210
1216
|
corroboration: number;
|
|
1211
1217
|
laneMix: {
|
|
@@ -1266,8 +1272,9 @@ export interface SocialRadarParams {
|
|
|
1266
1272
|
category?: string;
|
|
1267
1273
|
limit?: number;
|
|
1268
1274
|
/**
|
|
1269
|
-
* A Politics choice. Hide politics (`hide`) leaves out the stories
|
|
1270
|
-
* or themed `mixed` (a story has no compass placement, so it fails closed);
|
|
1275
|
+
* A Politics choice. Hide politics (`hide`) leaves out the political stories (SocialRadarCluster.political) whatever
|
|
1276
|
+
* their theme, and the ones not themed yet or themed `mixed` (a story has no compass placement, so it fails closed);
|
|
1277
|
+
* the stories after them fill the list.
|
|
1271
1278
|
* A side or Mix is answered unfiltered, marked `unavailable`; the keyless `getSharedRadar` takes only `all` or `hide`.
|
|
1272
1279
|
*/
|
|
1273
1280
|
compass?: string;
|
|
@@ -1281,7 +1288,7 @@ export interface SocialRadarPosts {
|
|
|
1281
1288
|
/** 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). */
|
|
1282
1289
|
export interface SocialRadarStoryArchive {
|
|
1283
1290
|
/** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
|
|
1284
|
-
cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
|
|
1291
|
+
cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix'> & {
|
|
1285
1292
|
members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
|
|
1286
1293
|
title?: string;
|
|
1287
1294
|
}>;
|
package/dist/types.d.ts
CHANGED
|
@@ -698,6 +698,20 @@ export interface Conversation {
|
|
|
698
698
|
/** Whether `chat.leave()` is allowed for the caller. */
|
|
699
699
|
can_leave?: boolean;
|
|
700
700
|
created_at: string;
|
|
701
|
+
/**
|
|
702
|
+
* When the conversation last had activity, the time `conversations()` sorts
|
|
703
|
+
* by (0.7.67): its newest message, or, in an end-to-end encrypted
|
|
704
|
+
* (`mls_v2`) conversation, the relay's receive time of its newest encrypted
|
|
705
|
+
* message when that is later; its creation when it has no messages.
|
|
706
|
+
* In an `mls_v2` conversation, membership and key changes and quiet events
|
|
707
|
+
* (a reaction, an edit, a delete) do not move it. In any other
|
|
708
|
+
* conversation it is read from the messages there now, so deleting the
|
|
709
|
+
* newest message moves it back, to the previous message or to the
|
|
710
|
+
* creation: do not keep the later of a cached and a served value. A time
|
|
711
|
+
* only, never content, so a device that cannot read an encrypted chat can
|
|
712
|
+
* still show when it last moved. Omitted by older APIs.
|
|
713
|
+
*/
|
|
714
|
+
last_activity_at?: string;
|
|
701
715
|
}
|
|
702
716
|
export interface ConversationRequestResult {
|
|
703
717
|
id: string;
|
|
@@ -733,6 +747,12 @@ export interface ConversationDetail {
|
|
|
733
747
|
/** Whether `chat.leave()` is allowed for the caller. */
|
|
734
748
|
can_leave?: boolean;
|
|
735
749
|
created_at: string;
|
|
750
|
+
/**
|
|
751
|
+
* As `Conversation.last_activity_at` (0.7.67). Served by `conversation()`;
|
|
752
|
+
* omitted by `createGroup()`, to a member who declined the request, and by
|
|
753
|
+
* older APIs.
|
|
754
|
+
*/
|
|
755
|
+
last_activity_at?: string;
|
|
736
756
|
}
|
|
737
757
|
export type ChatMemberRole = 'admin' | 'member';
|
|
738
758
|
/**
|
package/llm.md
CHANGED
|
@@ -504,6 +504,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
504
504
|
|
|
505
505
|
Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
|
|
506
506
|
|
|
507
|
+
Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
|
|
508
|
+
|
|
507
509
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
508
510
|
|
|
509
511
|
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
|
|
@@ -639,6 +641,23 @@ await r.bookmarks.unsave(key) // or uns
|
|
|
639
641
|
|
|
640
642
|
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).
|
|
641
643
|
|
|
644
|
+
Collections (0.7.68) are private, named labels on saves: filing saves first, one thing can sit in several collections, unsaving takes it out of every one, and deleting a collection leaves what it held saved. Use them only when `(await r.network.getConfig()).data.capabilities.bookmark_collections` is true; still treat 503 `lists_unavailable` as off.
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
const { data: lists, meta: caps } = await r.bookmarks.collections.list() // oldest first; caps.max_collections (25), caps.max_items (1000)
|
|
648
|
+
const { data: list } = await r.bookmarks.collections.create({ title: 'Read later' }, { idempotencyKey }) // private; slug from the title
|
|
649
|
+
await r.bookmarks.collections.add(list.id, { postId: post.id }) // saves it first if needed; idempotent
|
|
650
|
+
await r.bookmarks.collections.add(list.id, socialOrArticleItem) // one request: saved and filed together, or neither
|
|
651
|
+
await r.bookmarks.collections.removeItem(list.id, key) // it stays saved
|
|
652
|
+
const { data: posts } = await r.bookmarks.collections.posts(list.id) // = r.bookmarks.posts({ collection: list.id })
|
|
653
|
+
const { data: items } = await r.bookmarks.collections.items(list.id) // open-network posts and articles in it
|
|
654
|
+
const { collections: filedIn } = await r.bookmarks.keys() // { [key]: [collection ids] }; absent while collections are off
|
|
655
|
+
await r.bookmarks.collections.update(list.id, { title: 'Later' })
|
|
656
|
+
await r.bookmarks.collections.remove(list.id)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
404 `collection_not_found` for a collection that is not the member's; 409 `collection_limit`, `collection_item_limit`, `slug_taken`, `bookmark_limit` (filing that would save past the saves cap), and `not_saved` (a key that is not saved, sent without its item); 400 `not_supported_yet` for a kind other than posts or a visibility other than private.
|
|
660
|
+
|
|
642
661
|
### r.curator.forYouWorld — For You as the world by the member's interests
|
|
643
662
|
|
|
644
663
|
```typescript
|
|
@@ -733,6 +752,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
733
752
|
|
|
734
753
|
Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a group or a one-on-one chat, `request_status`: `accepted` from the start for the person who wrote, anyone who follows them and agents, else `pending` until that person accepts or replies (a decline reads `pending` to anyone else), so `accepted` alone does not mean they accepted anything. Only the admin (the creator, then the longest-standing person left) may `addMembers(id, { member_ids })`, `removeMember(id, userId)` and `updateGroup(id, { name })` (else 403 `not_admin`). `createGroup({ name, member_ids, encrypted: true, device_id? })` makes an end-to-end encrypted group of people in the pilot, at most 8 counting pending invitees, or throws 409 `conversation_not_eligible` with `details.reason` and creates nothing. `r.realtime.onChatMembers` / `onChatConversation` tell you when people or the name change.
|
|
735
754
|
|
|
755
|
+
Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
|
|
756
|
+
|
|
736
757
|
### r.tags
|
|
737
758
|
|
|
738
759
|
```typescript
|
|
@@ -783,6 +804,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
783
804
|
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`; `feed` with `ref` is declared in 0.7.60 but not sent yet, so skip kinds you cannot open). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`. Watch's Minds videos: `const { data, meta } = await r.discovery.videos({ limit: 12 })` returns public video posts in hot order from a list shared per app (rebuilt about once a minute), each re-checked per call for the viewer; page on `meta.next_cursor`.
|
|
784
805
|
- Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide` only). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
|
|
785
806
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass? })` (`compass: 'hide'` leaves out politics, 0.7.62); render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
|
|
807
|
+
- Political stories (0.7.67): every radar story (`getPublicRadar`, `getSharedRadar`, a story read) and every World Today story (`r.curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature, or a party or politician. `compass: 'hide'` leaves those out whatever their theme; `r.curator.today({ compass: 'hide' })` does the same to the front page, the next story leads, and `meta.compass_filter` counts what was left out. A story from `r.feeds.stories()` or a `r.feeds.top()` lead carries `political` too, and both reads, and `r.curator.forYouWorld()`, leave political stories out under `compass: 'hide'` whatever their theme. An older server omits `political`: decide by `theme` as before.
|
|
786
808
|
- Post language: a `SocialPost` may carry `language`, the lower-case primary subtag the post declares (`'de'`, `'en'`; Bluesky `langs[0]`, Mastodon `language`, a Nostr NIP-32 `ISO-639-1` label). It is absent when the post declares none (most Nostr notes) and on posts from the source index; it is never guessed and no post is dropped for it. Order by it (the viewer's language first, undeclared next), never filter on it.
|
|
787
809
|
- `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
|
|
788
810
|
- Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
|