@recursiv/sdk 0.7.52 → 0.7.54

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.
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, SearchMentionsParams, MentionCandidate, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
2
+ import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ProfileRelationship } from '../types.js';
3
3
  export declare class ProfilesResource {
4
4
  private client;
5
5
  constructor(client: HttpClient);
@@ -15,14 +15,25 @@ export declare class ProfilesResource {
15
15
  /** Get a user by username */
16
16
  getByUsername(username: string): Promise<SingleResponse<ProfileDetail>>;
17
17
  /** Search users by name or username */
18
- search(params: SearchProfilesParams): Promise<PaginatedResponse<Profile>>;
18
+ /**
19
+ * Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
20
+ * official account): empties last and marked `empty`, the official ones `verified`. Every page is
21
+ * a slice of one ranked pool of 200; past it `has_more` is false.
22
+ */
23
+ search(params: SearchProfilesParams): Promise<PaginatedResponse<ProfileSearchResult>>;
19
24
  /**
20
25
  * Accounts to @mention: people and agents on the caller's network, the
21
26
  * caller's follows first. Unlike `search`, this includes agents the app may
22
27
  * discover (an agent answers when mentioned); another member's personal AI
23
28
  * is never offered.
29
+ *
30
+ * A search that runs over its 2 s budget answers 503 with Retry-After. A
31
+ * type-ahead has its own deadline, so pass `{ retry: false }` to get that
32
+ * answer at once instead of waiting through retries.
24
33
  */
25
- mentions(params: SearchMentionsParams): Promise<PaginatedResponse<MentionCandidate>>;
34
+ mentions(params: SearchMentionsParams, options?: {
35
+ retry?: boolean;
36
+ }): Promise<PaginatedResponse<MentionCandidate>>;
26
37
  /** Update own profile */
27
38
  update(input: UpdateProfileInput): Promise<SingleResponse<Profile>>;
28
39
  /** Follow a user */
@@ -30,18 +41,16 @@ export declare class ProfilesResource {
30
41
  /** Unfollow a user */
31
42
  unfollow(userId: string): Promise<SuccessResponse>;
32
43
  /**
33
- * List a user's followers. Each row carries the caller's relationship with
34
- * it (`is_following`, `follows_you`), so a Follow button needs no second read.
44
+ * List a user's followers, newest follow first once the server's database
45
+ * has the follow-list order indexes (until then, in the order it reads
46
+ * them). Each row carries the caller's relationship with it (`is_following`,
47
+ * `follows_you`), so a Follow button needs no second read. `search` narrows
48
+ * the whole list to accounts whose name or handle contains it (at most 200
49
+ * characters); page the matches with `limit` and `offset`.
35
50
  */
36
- followers(userId: string, params?: {
37
- limit?: number;
38
- offset?: number;
39
- }): Promise<PaginatedResponse<FollowListProfile>>;
40
- /** List who a user follows, each row with the caller's relationship with it. */
41
- following(userId: string, params?: {
42
- limit?: number;
43
- offset?: number;
44
- }): Promise<PaginatedResponse<FollowListProfile>>;
51
+ followers(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
52
+ /** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
53
+ following(userId: string, params?: FollowListParams): Promise<PaginatedResponse<FollowListProfile>>;
45
54
  /**
46
55
  * The caller's whole follow set as ids, up to 5000 per page (default 1000).
47
56
  * Answers what `isFollowing` answers, for every account at once. Pass
@@ -51,8 +60,12 @@ export declare class ProfilesResource {
51
60
  cursor?: string;
52
61
  limit?: number;
53
62
  }): Promise<PaginatedResponse<string>>;
54
- /** The follow relationship with another user in both directions (mutual when both are true). */
55
- isFollowing(userId: string): Promise<SingleResponse<ViewerRelationship>>;
63
+ /**
64
+ * The caller's relationship with another user: follows in both directions
65
+ * (mutual when both are true), and whether the caller blocks (`is_blocking`)
66
+ * or mutes (`is_muting`) them, left out when the server cannot say.
67
+ */
68
+ isFollowing(userId: string): Promise<SingleResponse<ProfileRelationship>>;
56
69
  /**
57
70
  * Block a user. A block is a two-way wall: it severs follows in both
58
71
  * directions and hides each side's posts from the other.
@@ -20,6 +20,11 @@ export class ProfilesResource {
20
20
  return this.client.get(`/profiles/by-username/${username}`);
21
21
  }
22
22
  /** Search users by name or username */
23
+ /**
24
+ * Search accounts by name or handle, ranked by match x ln(1 + followers + min(posts, 100)) (doubled for an
25
+ * official account): empties last and marked `empty`, the official ones `verified`. Every page is
26
+ * a slice of one ranked pool of 200; past it `has_more` is false.
27
+ */
23
28
  search(params) {
24
29
  return this.client.get('/profiles/search', params);
25
30
  }
@@ -28,9 +33,13 @@ export class ProfilesResource {
28
33
  * caller's follows first. Unlike `search`, this includes agents the app may
29
34
  * discover (an agent answers when mentioned); another member's personal AI
30
35
  * is never offered.
36
+ *
37
+ * A search that runs over its 2 s budget answers 503 with Retry-After. A
38
+ * type-ahead has its own deadline, so pass `{ retry: false }` to get that
39
+ * answer at once instead of waiting through retries.
31
40
  */
32
- mentions(params) {
33
- return this.client.get('/profiles/mentions', params);
41
+ mentions(params, options) {
42
+ return this.client.get('/profiles/mentions', params, options);
34
43
  }
35
44
  /** Update own profile */
36
45
  update(input) {
@@ -45,13 +54,17 @@ export class ProfilesResource {
45
54
  return this.client.delete(`/profiles/${userId}/follow`);
46
55
  }
47
56
  /**
48
- * List a user's followers. Each row carries the caller's relationship with
49
- * it (`is_following`, `follows_you`), so a Follow button needs no second read.
57
+ * List a user's followers, newest follow first once the server's database
58
+ * has the follow-list order indexes (until then, in the order it reads
59
+ * them). Each row carries the caller's relationship with it (`is_following`,
60
+ * `follows_you`), so a Follow button needs no second read. `search` narrows
61
+ * the whole list to accounts whose name or handle contains it (at most 200
62
+ * characters); page the matches with `limit` and `offset`.
50
63
  */
51
64
  followers(userId, params) {
52
65
  return this.client.get(`/profiles/${userId}/followers`, params);
53
66
  }
54
- /** List who a user follows, each row with the caller's relationship with it. */
67
+ /** List who a user follows, in the order `followers` uses, each row with the caller's relationship with it; `search` as on `followers`. */
55
68
  following(userId, params) {
56
69
  return this.client.get(`/profiles/${userId}/following`, params);
57
70
  }
@@ -63,7 +76,11 @@ export class ProfilesResource {
63
76
  followingIds(params) {
64
77
  return this.client.get('/profiles/me/following/ids', params);
65
78
  }
66
- /** The follow relationship with another user in both directions (mutual when both are true). */
79
+ /**
80
+ * The caller's relationship with another user: follows in both directions
81
+ * (mutual when both are true), and whether the caller blocks (`is_blocking`)
82
+ * or mutes (`is_muting`) them, left out when the server cannot say.
83
+ */
67
84
  isFollowing(userId) {
68
85
  return this.client.get(`/profiles/${userId}/is-following`);
69
86
  }
@@ -305,6 +305,8 @@ export interface SocialSourceControlDisplay {
305
305
  avatarUrl: string | null;
306
306
  /** A hidden post's text, at most 140 characters; null for accounts, deleted posts, and moderated posts or authors. */
307
307
  excerpt: string | null;
308
+ /** A hidden ActivityPub post's public reader id, for the conversation read; absent for accounts, other networks and posts observed without one. */
309
+ readRef?: string;
308
310
  observedAt: string;
309
311
  }
310
312
  export interface SocialSourceControl {
@@ -641,6 +643,8 @@ export interface NativeReplyBatch {
641
643
  authorHandle: string;
642
644
  text: string;
643
645
  textTruncated?: boolean;
646
+ /** An ActivityPub parent's public reader id, when the reply was prepared with one: pass it to the conversation read. */
647
+ readRef?: string;
644
648
  };
645
649
  text: string;
646
650
  wireText: string;
@@ -742,7 +746,7 @@ export interface NativeDelegatedReplyHistoryItem {
742
746
  createdAt: string;
743
747
  text: string;
744
748
  textTruncated: boolean;
745
- source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl'>;
749
+ source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl' | 'readRef'>;
746
750
  deliveries: Array<Pick<NativeReplyBatch['deliveries'][number], 'id' | 'protocol' | 'kind' | 'state' | 'attemptedAt' | 'account' | 'recoverable' | 'replacedBy'> & {
747
751
  removalReceipt: NativeReplyRemovalReceipt | null;
748
752
  }>;
@@ -751,10 +755,12 @@ export interface NativeNostrReplyResult {
751
755
  id: string;
752
756
  createdAt: string;
753
757
  text: string;
758
+ /** `readRef`: a linked ActivityPub source's public reader id, for the conversation read (replies saved before it existed have none). */
754
759
  source: {
755
760
  protocol: SocialSourceRef['protocol'];
756
761
  nativeId: string;
757
762
  sourceUrl: string;
763
+ readRef?: string;
758
764
  };
759
765
  identityId: string;
760
766
  pubkey: string;
@@ -922,6 +928,8 @@ export interface SocialProfile {
922
928
  followersCount: number | null;
923
929
  followingCount: number | null;
924
930
  postsCount: number | null;
931
+ /** Nostr only: the NIP-05 name its domain has confirmed for this key ("lyn@primal.net"); `handle` stays the key. */
932
+ nip05?: string;
925
933
  subscription: SocialSubscription | null;
926
934
  }
927
935
  export interface SocialSubscription {
@@ -962,6 +970,12 @@ export interface SocialPost {
962
970
  replyCount: number | null;
963
971
  likeCount: number | null;
964
972
  repostCount: number | null;
973
+ /**
974
+ * When the counts were read from the source network. Present only on a post a category feed served
975
+ * from the open-network index, whose copy can be older than the page (the server refreshes counts
976
+ * older than 15 minutes when the provider answers in time). Absent: the counts are from this read.
977
+ */
978
+ countsObservedAt?: string;
965
979
  /** Pixel dimensions when the source declares them (Bluesky aspect ratio, Mastodon attachment meta), so a card can take the clip's shape before it loads. */
966
980
  media: Array<{
967
981
  type: 'image' | 'video';
@@ -1061,6 +1075,20 @@ export interface SocialTrendFeed extends SocialTrendProvenance {
1061
1075
  trend: SocialTrend;
1062
1076
  items: SocialPost[];
1063
1077
  cursor: string | null;
1078
+ /**
1079
+ * Present when the page came from the app's open-network source index instead of the provider:
1080
+ * with a category, a topic the provider no longer lists (`gone`) or whose feed held nothing in the
1081
+ * category (`empty`) is served the network's indexed posts in the category. While the provider has
1082
+ * more of the topic, `cursor` is the provider's own continuation (the topic's deeper pages may hold
1083
+ * the category); once it has none, or the topic is gone, it is an `idx:tf:` cursor that continues in
1084
+ * the index (`cursor`), the same stream for every topic of the network. The `trend` of a gone topic
1085
+ * carries its id as its title. Pass the cursor back unchanged like any other. When the index cannot
1086
+ * answer, the page is what the provider answered (the empty page with its cursor, or the gone topic's
1087
+ * 404); only an `idx:tf:` page is 503 `unavailable`, which ends that continuation, not the network.
1088
+ */
1089
+ sourceIndex?: SocialTrendSourceIndex & {
1090
+ reason: 'gone' | 'empty' | 'cursor';
1091
+ };
1064
1092
  }
1065
1093
  /** A member of a radar cluster: a headline or excerpt and a link only, never a body. */
1066
1094
  export type SocialRadarSourceKind = 'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds';
@@ -1165,17 +1193,46 @@ export interface SocialRadarPosts {
1165
1193
  builtAt: string | null;
1166
1194
  items: SocialPost[];
1167
1195
  }
1196
+ /** 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). */
1197
+ export interface SocialRadarStoryArchive {
1198
+ /** The story as the radar last published it. Articles keep their headline and link; a post is a member by reference only (no `title`). */
1199
+ cluster: Pick<SocialRadarCluster, 'id' | 'label' | 'kind' | 'seed' | 'theme' | 'sources' | 'laneMix'> & {
1200
+ members: Array<Pick<SocialRadarMember, 'kind' | 'key' | 'url' | 'publishedAt' | 'outlet'> & {
1201
+ title?: string;
1202
+ }>;
1203
+ };
1204
+ /** Its open-network posts the app's source index still holds and its source controls and safety floor admit today, in member order. */
1205
+ items: SocialPost[];
1206
+ /** A World Today story's dek: an outlet's own sentence, or the editor model's. */
1207
+ summary: {
1208
+ text: string;
1209
+ origin: 'outlet' | 'editor';
1210
+ } | null;
1211
+ /** The first and the last radar build that published it. */
1212
+ firstSeenAt: string;
1213
+ lastSeenAt: string;
1214
+ /** The index holds none of its open-network posts any more (retention or deletion), so none are listed. */
1215
+ postsExpired: boolean;
1216
+ }
1168
1217
  /**
1169
1218
  * One radar story by id, with its social member posts.
1170
- * `live`: on the current radar (`cluster` and `items` present). `aged_out`: on one of the last six
1171
- * radars but not this one. `unknown`: not seen in that window (never existed, or aged out long ago).
1219
+ * `live`: on the current radar (`cluster` and `items` present). `aged_out`: not on this radar, but
1220
+ * held by the story archive (`archive`, and the live story it continues as in `successor`) or seen on
1221
+ * one of the last six radars (`archive` null). `unknown`: in neither (never existed, or left longer ago
1222
+ * than the archive keeps stories). A server without the story archive leaves `archive` and `successor` out.
1172
1223
  */
1173
1224
  export interface SocialRadarStory {
1174
1225
  id: string;
1175
1226
  builtAt: string | null;
1176
1227
  status: 'live' | 'aged_out' | 'unknown';
1177
1228
  cluster: SocialRadarCluster | null;
1229
+ /** The live story's posts; empty otherwise (an archived story's are in `archive.items`). */
1178
1230
  items: SocialPost[];
1231
+ archive?: SocialRadarStoryArchive | null;
1232
+ successor?: {
1233
+ id: string;
1234
+ label: string;
1235
+ } | null;
1179
1236
  }
1180
1237
  /** Read options for callers that hold their own deadline, such as a discovery page. */
1181
1238
  export interface SocialReadOptions {
@@ -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
+ }