@recursiv/sdk 0.7.65 → 0.7.67
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 +26 -1
- package/dist/index.d.ts +1 -1
- package/dist/resources/curator.d.ts +182 -2
- package/dist/resources/curator.js +11 -2
- package/dist/resources/feeds.d.ts +6 -0
- package/dist/resources/profiles.d.ts +4 -1
- package/dist/resources/profiles.js +4 -1
- package/dist/resources/protocols.d.ts +14 -8
- package/dist/types.d.ts +20 -0
- package/llm.md +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -670,6 +670,25 @@ and says whether the member is personalized yet (`meta.personalized`). With a
|
|
|
670
670
|
`category` (the member's first), the session also reads open-network posts from it;
|
|
671
671
|
without, it is Minds only. It never carries the radar.
|
|
672
672
|
|
|
673
|
+
### For You: the world by the member's interests
|
|
674
|
+
|
|
675
|
+
`sdk.curator.forYouWorld({ part?, session?, snapshot?, compass? })` reads the For You overview
|
|
676
|
+
for the signed-in member, with their own key of the app. `stories` are world stories (Today,
|
|
677
|
+
the radar and the viral read) matched to the member's interests with breadth: at least four
|
|
678
|
+
themes and two breaking or general stories in the first eight, at most two per theme, and a
|
|
679
|
+
story shown three times without an open rests for a week. `items` are the member's For You
|
|
680
|
+
picks interleaved with the most engaging posts of their top interests (half or more on their
|
|
681
|
+
interests, no interest over 40%, videos weighted up), and `blocks` lay the page out like All:
|
|
682
|
+
`stories`, `lead_video`, `clips`, `posts`, `people`, `groups`, `topics`, `viral` and
|
|
683
|
+
`categories`, never the same type twice in a row, each naming the refs its views should send.
|
|
684
|
+
Every story and item carries `reasons` (`interest:<category>`, `followed:@<handle>`, `breaking`,
|
|
685
|
+
`general`, `for_you`, `engaging`, `breadth`). `part: 'stories'` and `part: 'items'` read the
|
|
686
|
+
two halves on their own, so an app can draw each as it lands; a part=items answer keeps the
|
|
687
|
+
stories rail as a `deferred` block. `meta.interests` are category ids for chips (never put them
|
|
688
|
+
in a URL), `meta.partial` names a source that missed its deadline, and `meta.foryou_cursor`
|
|
689
|
+
continues the same ranking with `curator.forYou({ cursor })`. 404 while the app's operator has
|
|
690
|
+
it off; a 503 `for_you_world_unavailable` carries Retry-After.
|
|
691
|
+
|
|
673
692
|
### The top of a feed
|
|
674
693
|
|
|
675
694
|
`sdk.feeds.top({ ref, limit?, session?, compass? })` answers the top of a feed for the
|
|
@@ -713,7 +732,13 @@ The catalog lists `compass_filter` on every network when the filter is served to
|
|
|
713
732
|
Hide politics (`compass: 'hide'`) also applies on its own, with or without compass judgments on the server, to
|
|
714
733
|
`sdk.discovery.videos`, `protocols.getPublicTrends` and the radar (`getPublicRadar`, keyless `getSharedRadar`; 0.7.62):
|
|
715
734
|
it leaves out what is categorised politics (a story not themed yet, or `mixed`, too) and says what it did in
|
|
716
|
-
`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.
|
|
717
742
|
|
|
718
743
|
`sdk.discovery.recommendations({ kind, category?, limit?, session?, exclude? })` is
|
|
719
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, } 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';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { DiscoveryLayout, PaginatedResponse, PaginationMeta, Post, SingleResponse } from '../types.js';
|
|
2
|
+
import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationMeta, Post, SingleResponse } from '../types.js';
|
|
3
|
+
import type { FeedTopNetworkCard, FeedTopTarget } from './feeds.js';
|
|
3
4
|
/** Which surface is asking: Home's For You (default) or Discover's For you. */
|
|
4
5
|
export type ForYouSurface = 'home' | 'discover';
|
|
5
6
|
export type ForYouParams = {
|
|
@@ -22,6 +23,158 @@ export interface ForYouMeta extends PaginationMeta {
|
|
|
22
23
|
/** Whether the ranker can personalize for this member yet (3 follows or 3 upvoted authors). */
|
|
23
24
|
personalized?: boolean;
|
|
24
25
|
}
|
|
26
|
+
/** Which half to read: the stories rail, the items, or both (the default). The app can read the two at once and draw each as it lands. */
|
|
27
|
+
export type ForYouWorldPart = 'stories' | 'items' | 'all';
|
|
28
|
+
export interface ForYouWorldParams {
|
|
29
|
+
part?: ForYouWorldPart;
|
|
30
|
+
/** Opaque per app session ([A-Za-z0-9_-], up to 64): the same token keeps one session's order stable across reads. */
|
|
31
|
+
session?: string;
|
|
32
|
+
/** A For You snapshot nonce the app holds (`meta.snapshot` of a previous answer): reused while the server holds it. */
|
|
33
|
+
snapshot?: string;
|
|
34
|
+
/** A Politics choice, as `posts.list` takes it. */
|
|
35
|
+
compass?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Where a story opens in the app: a radar story by its cluster id, or a World Today story the radar has none for by its Today id. */
|
|
38
|
+
export type ForYouWorldStoryTarget = {
|
|
39
|
+
kind: 'story';
|
|
40
|
+
id: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'today_story';
|
|
43
|
+
id: string;
|
|
44
|
+
};
|
|
45
|
+
/** One world story: a radar story (with Today's headline when Today seeded it), a Today story, or a viral post. */
|
|
46
|
+
export interface ForYouWorldStory {
|
|
47
|
+
/** What an impression of it names: 'cluster:<id>', 'today:<id>' or the post's ref. Send it with `block: 'top'` on a visible view. */
|
|
48
|
+
ref: string;
|
|
49
|
+
id: string;
|
|
50
|
+
source: 'today' | 'radar' | 'viral';
|
|
51
|
+
target: FeedTopTarget | ForYouWorldStoryTarget;
|
|
52
|
+
label: string;
|
|
53
|
+
kind: 'breaking' | 'viral' | 'story';
|
|
54
|
+
breaking: boolean;
|
|
55
|
+
/** A front-page or general-news story: one of those shown whatever the member's interests. */
|
|
56
|
+
general: boolean;
|
|
57
|
+
theme: string | null;
|
|
58
|
+
categories: string[];
|
|
59
|
+
/** How widely it is told: posts per source kind, and the outlets that ran it (Today), when known. */
|
|
60
|
+
counts: {
|
|
61
|
+
sources: Record<string, number>;
|
|
62
|
+
outlets: number | null;
|
|
63
|
+
};
|
|
64
|
+
media: {
|
|
65
|
+
images: number;
|
|
66
|
+
videos: number;
|
|
67
|
+
};
|
|
68
|
+
freshness: {
|
|
69
|
+
newestAt: string | null;
|
|
70
|
+
};
|
|
71
|
+
position: number;
|
|
72
|
+
/** Why it is here: `interest:<category>`, `breaking`, `general`, `viral`, `breadth` or `trending`. */
|
|
73
|
+
reasons: string[];
|
|
74
|
+
explain: {
|
|
75
|
+
score: number;
|
|
76
|
+
interest: number;
|
|
77
|
+
rank: number;
|
|
78
|
+
impression: 'fresh' | 'demoted';
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** One item: a Minds post (`ref` is its id) or an open-network post (`ref` is '<protocol>/<nativeId>'). */
|
|
82
|
+
export interface ForYouWorldItem {
|
|
83
|
+
ref: string;
|
|
84
|
+
kind: 'post' | 'network_post';
|
|
85
|
+
authorId: string;
|
|
86
|
+
target: FeedTopTarget;
|
|
87
|
+
/** Reading order through the blocks. */
|
|
88
|
+
position: number;
|
|
89
|
+
/** 'top' for the first 10: send `block: 'top'` and the position with a visible view of one. */
|
|
90
|
+
block: 'top' | 'below';
|
|
91
|
+
media: 'video' | 'image' | 'none';
|
|
92
|
+
topic: string | null;
|
|
93
|
+
/** Why it is here: `interest:<category>`, `followed:@<handle>`, `for_you`, `engaging` or `breadth`. */
|
|
94
|
+
reasons: string[];
|
|
95
|
+
explain: {
|
|
96
|
+
score: number;
|
|
97
|
+
source: 'foryou' | 'category' | 'all';
|
|
98
|
+
label: 'safe' | 'open';
|
|
99
|
+
impression: 'fresh' | 'demoted';
|
|
100
|
+
engagement: {
|
|
101
|
+
source: string;
|
|
102
|
+
rate: number;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** A viral post in the viral block. `card` is an open-network post's card (no URL on its network); null for a Minds post, read by id. */
|
|
107
|
+
export interface ForYouWorldViral {
|
|
108
|
+
ref: string;
|
|
109
|
+
kind: 'post' | 'network_post';
|
|
110
|
+
protocol: string;
|
|
111
|
+
target: FeedTopTarget;
|
|
112
|
+
label: string;
|
|
113
|
+
category: string | null;
|
|
114
|
+
media: 'video' | 'image' | null;
|
|
115
|
+
rank: number;
|
|
116
|
+
velocity: {
|
|
117
|
+
perHour1h: number;
|
|
118
|
+
z: number;
|
|
119
|
+
};
|
|
120
|
+
card: FeedTopNetworkCard | null;
|
|
121
|
+
}
|
|
122
|
+
export interface ForYouWorldCategoryShortcut {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
reason: 'interest' | 'breadth';
|
|
126
|
+
target: {
|
|
127
|
+
kind: 'category';
|
|
128
|
+
id: string;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** The page's components, laid out like All: no type follows itself. */
|
|
132
|
+
export type ForYouWorldBlockType = 'stories' | 'lead_video' | 'clips' | 'posts' | 'people' | 'groups' | 'topics' | 'categories' | 'viral';
|
|
133
|
+
export interface ForYouWorldBlock {
|
|
134
|
+
id: string;
|
|
135
|
+
type: ForYouWorldBlockType;
|
|
136
|
+
/** What it shows, in order: item refs (lead_video, clips, posts), story refs, entity keys, category ids or viral refs. */
|
|
137
|
+
refs: string[];
|
|
138
|
+
/** The stories rail of a part=items answer: fill it from your own part=stories read. */
|
|
139
|
+
deferred?: true;
|
|
140
|
+
/** people, groups and topics: the recommender's suggestions (three showings without a tap rest one). */
|
|
141
|
+
entities?: Array<Record<string, unknown> & {
|
|
142
|
+
kind: string;
|
|
143
|
+
key: string;
|
|
144
|
+
}>;
|
|
145
|
+
categories?: ForYouWorldCategoryShortcut[];
|
|
146
|
+
viral?: ForYouWorldViral[];
|
|
147
|
+
}
|
|
148
|
+
export interface ForYouWorldMeta {
|
|
149
|
+
part: ForYouWorldPart;
|
|
150
|
+
flag: {
|
|
151
|
+
world: 'on';
|
|
152
|
+
impressions: 'on' | 'off' | 'unavailable';
|
|
153
|
+
};
|
|
154
|
+
/** Fewer than 3 follows and no signals yet: a breadth-only overview. */
|
|
155
|
+
cold_start: boolean;
|
|
156
|
+
personalized: boolean | null;
|
|
157
|
+
/** The member's top interests as browse category ids, for chips. Never put them in a URL. */
|
|
158
|
+
interests: string[];
|
|
159
|
+
/**
|
|
160
|
+
* Sources left out because they missed their deadline or failed. `consent` is the Bluesky consent read: when it is
|
|
161
|
+
* named, Bluesky posts from the category pools and the viral read were left out of this answer.
|
|
162
|
+
*/
|
|
163
|
+
partial: string[];
|
|
164
|
+
safety: 'safe' | 'open' | null;
|
|
165
|
+
/** The For You snapshot the items came from; `foryou_cursor` continues it on `curator.forYou({ cursor })`. */
|
|
166
|
+
snapshot: string | null;
|
|
167
|
+
foryou_cursor: string | null;
|
|
168
|
+
short: boolean;
|
|
169
|
+
compass_filter?: CompassFilterMeta;
|
|
170
|
+
}
|
|
171
|
+
/** GET /curator/foryou/world: `stories` unless part=items, `items` and `blocks` unless part=stories. */
|
|
172
|
+
export interface ForYouWorldResponse {
|
|
173
|
+
stories?: ForYouWorldStory[];
|
|
174
|
+
items?: ForYouWorldItem[];
|
|
175
|
+
blocks?: ForYouWorldBlock[];
|
|
176
|
+
meta: ForYouWorldMeta;
|
|
177
|
+
}
|
|
25
178
|
/** RSS / Atom source. Default — `type` may be omitted for back-compat. */
|
|
26
179
|
export interface CuratorRssSource {
|
|
27
180
|
type?: 'rss';
|
|
@@ -191,6 +344,12 @@ export interface CuratorTodayStory {
|
|
|
191
344
|
headline: string;
|
|
192
345
|
dek: string | null;
|
|
193
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;
|
|
194
353
|
/** Outlet links for a world story; null for a Minds story. */
|
|
195
354
|
sources: CuratorTodaySource[] | null;
|
|
196
355
|
/** Distinct outlets behind a world story; null for a Minds story. */
|
|
@@ -227,10 +386,22 @@ export interface CuratorTodayEdition {
|
|
|
227
386
|
export interface CuratorTodayResponse {
|
|
228
387
|
/** null when the network has no published edition yet. */
|
|
229
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
|
+
*/
|
|
230
393
|
meta: {
|
|
231
394
|
generating: boolean;
|
|
395
|
+
compass_filter?: CompassFilterMeta;
|
|
232
396
|
};
|
|
233
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
|
+
}
|
|
234
405
|
export declare class CuratorResource {
|
|
235
406
|
private client;
|
|
236
407
|
constructor(client: HttpClient);
|
|
@@ -239,7 +410,7 @@ export declare class CuratorResource {
|
|
|
239
410
|
* (set by the open web, ranked by outlet count) come first; Minds
|
|
240
411
|
* conversation stories follow. Requires `posts:read`.
|
|
241
412
|
*/
|
|
242
|
-
today(): Promise<CuratorTodayResponse>;
|
|
413
|
+
today(params?: CuratorTodayParams): Promise<CuratorTodayResponse>;
|
|
243
414
|
/**
|
|
244
415
|
* Fetch the supplied sources in parallel, annotate each top item with
|
|
245
416
|
* the supplied prompt template, and insert audience-scoped posts into
|
|
@@ -278,4 +449,13 @@ export declare class CuratorResource {
|
|
|
278
449
|
forYou(params?: ForYouParams): Promise<PaginatedResponse<Post> & {
|
|
279
450
|
meta: ForYouMeta;
|
|
280
451
|
}>;
|
|
452
|
+
/**
|
|
453
|
+
* For You as an overview of the world by the member's interests, read with their own key of the app: world stories
|
|
454
|
+
* (interest-matched, with breadth), then a page of typed blocks laid out like All (a lead video, clip strips, posts,
|
|
455
|
+
* people and groups to follow, trending topics, viral posts, category shortcuts). `part` reads one half on its own.
|
|
456
|
+
* 404 until the app's operator turns it on; a 503 `for_you_world_unavailable` carries Retry-After. Always private.
|
|
457
|
+
*/
|
|
458
|
+
forYouWorld(params?: ForYouWorldParams, options?: {
|
|
459
|
+
retry?: boolean;
|
|
460
|
+
}): Promise<ForYouWorldResponse>;
|
|
281
461
|
}
|
|
@@ -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
|
|
@@ -57,4 +57,13 @@ export class CuratorResource {
|
|
|
57
57
|
const { retry, ...query } = params;
|
|
58
58
|
return retry === false ? this.client.get('/curator/foryou', query, { retry: false }) : this.client.get('/curator/foryou', query);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* For You as an overview of the world by the member's interests, read with their own key of the app: world stories
|
|
62
|
+
* (interest-matched, with breadth), then a page of typed blocks laid out like All (a lead video, clip strips, posts,
|
|
63
|
+
* people and groups to follow, trending topics, viral posts, category shortcuts). `part` reads one half on its own.
|
|
64
|
+
* 404 until the app's operator turns it on; a 503 `for_you_world_unavailable` carries Retry-After. Always private.
|
|
65
|
+
*/
|
|
66
|
+
forYouWorld(params = {}, options) {
|
|
67
|
+
return this.client.get('/curator/foryou/world', { part: params.part, session: params.session, snapshot: params.snapshot, compass: params.compass }, options);
|
|
68
|
+
}
|
|
60
69
|
}
|
|
@@ -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;
|
|
@@ -3,7 +3,10 @@ import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, Profi
|
|
|
3
3
|
export declare class ProfilesResource {
|
|
4
4
|
private client;
|
|
5
5
|
constructor(client: HttpClient);
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* List users in the network, newest first. Automated accounts are left out (a personal agent stays, `is_ai`), and
|
|
8
|
+
* so is an account wearing an official account's name; a page can come back shorter than `limit`.
|
|
9
|
+
*/
|
|
7
10
|
list(params?: {
|
|
8
11
|
limit?: number;
|
|
9
12
|
offset?: number;
|
|
@@ -3,7 +3,10 @@ export class ProfilesResource {
|
|
|
3
3
|
constructor(client) {
|
|
4
4
|
this.client = client;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* List users in the network, newest first. Automated accounts are left out (a personal agent stays, `is_ai`), and
|
|
8
|
+
* so is an account wearing an official account's name; a page can come back shorter than `limit`.
|
|
9
|
+
*/
|
|
7
10
|
list(params) {
|
|
8
11
|
return this.client.get('/profiles', params);
|
|
9
12
|
}
|
|
@@ -1043,11 +1043,10 @@ export interface SocialPost {
|
|
|
1043
1043
|
};
|
|
1044
1044
|
/**
|
|
1045
1045
|
* Present, and only then, when the ActivityPub author opted out of indexing (`noindex`,
|
|
1046
|
-
* `indexable: false`, or #nobot, #noindex or #noai in their bio). The
|
|
1047
|
-
*
|
|
1048
|
-
* author-feed and conversation reads (a profile or a post someone opened)
|
|
1049
|
-
*
|
|
1050
|
-
* to signed-out visitors.
|
|
1046
|
+
* `indexable: false`, or #nobot, #noindex or #noai in their bio). The trends, trend-feed and radar
|
|
1047
|
+
* reads leave such posts out, keyless and for a member alike, and no post served from the source index
|
|
1048
|
+
* is one. The author-feed and conversation reads (a profile or a post someone opened), keyless or a
|
|
1049
|
+
* member's, still return them with this mark, so an app should never re-share or promote one.
|
|
1051
1050
|
*/
|
|
1052
1051
|
authorOptOut?: true;
|
|
1053
1052
|
/** A bridge (Bridgy Fed, Mostr, a NIP-48 proxy tag) made this post from one on another network (SocialBridge). Present only on a copy read live. */
|
|
@@ -1207,6 +1206,12 @@ export interface SocialRadarCluster {
|
|
|
1207
1206
|
};
|
|
1208
1207
|
/** A browse-category id or `mixed`, from cached judgments only. */
|
|
1209
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;
|
|
1210
1215
|
sources: Partial<Record<SocialRadarSourceKind, number>>;
|
|
1211
1216
|
corroboration: number;
|
|
1212
1217
|
laneMix: {
|
|
@@ -1267,8 +1272,9 @@ export interface SocialRadarParams {
|
|
|
1267
1272
|
category?: string;
|
|
1268
1273
|
limit?: number;
|
|
1269
1274
|
/**
|
|
1270
|
-
* A Politics choice. Hide politics (`hide`) leaves out the stories
|
|
1271
|
-
* 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.
|
|
1272
1278
|
* A side or Mix is answered unfiltered, marked `unavailable`; the keyless `getSharedRadar` takes only `all` or `hide`.
|
|
1273
1279
|
*/
|
|
1274
1280
|
compass?: string;
|
|
@@ -1282,7 +1288,7 @@ export interface SocialRadarPosts {
|
|
|
1282
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). */
|
|
1283
1289
|
export interface SocialRadarStoryArchive {
|
|
1284
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`). */
|
|
1285
|
-
cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
|
|
1291
|
+
cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'political' | 'sources' | 'laneMix'> & {
|
|
1286
1292
|
members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
|
|
1287
1293
|
title?: string;
|
|
1288
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,18 @@ 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
|
+
### r.curator.forYouWorld — For You as the world by the member's interests
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
const head = r.curator.forYouWorld({ part: 'stories', session }) // member's own app key; read the two halves at once
|
|
648
|
+
const page = await r.curator.forYouWorld({ part: 'items', session, snapshot }) // items + blocks; snapshot = a previous meta.snapshot
|
|
649
|
+
// stories: { ref, id, source: today|radar|viral, target, label, kind, breaking, general, theme, counts, reasons, ... }
|
|
650
|
+
// items: { ref, kind, target, position, block: 'top' | 'below', media, topic, reasons, explain }
|
|
651
|
+
// blocks: [{ id, type: stories|lead_video|clips|posts|people|groups|topics|viral|categories, refs, deferred?, entities?, categories?, viral? }]
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
404 while the app's operator has it off; always private (`Cache-Control: private, no-store`). A story's or item's `target` is in the app (a radar story by cluster id, a Today story as `today_story`, a post id, `/social/post/<protocol>/<id>`), never a URL out. Send `block: 'top'` with a visible view of a story in the rail or one of the first 10 items: three such views without an open rest it for a week. `meta.partial` names a source that missed its deadline; `meta.interests` are category ids for chips, never for a URL; `meta.foryou_cursor` continues on `r.curator.forYou({ cursor })`.
|
|
655
|
+
|
|
642
656
|
### r.feeds — The top of a feed (member and visitor)
|
|
643
657
|
|
|
644
658
|
```typescript
|
|
@@ -721,6 +735,8 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
721
735
|
|
|
722
736
|
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.
|
|
723
737
|
|
|
738
|
+
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.
|
|
739
|
+
|
|
724
740
|
### r.tags
|
|
725
741
|
|
|
726
742
|
```typescript
|
|
@@ -771,6 +787,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
771
787
|
- 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`.
|
|
772
788
|
- 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.
|
|
773
789
|
- 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.
|
|
790
|
+
- 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.
|
|
774
791
|
- 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.
|
|
775
792
|
- `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.
|
|
776
793
|
- 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`.
|