@recursiv/sdk 0.7.37 → 0.7.40
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 +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/resources/discovery-learning.d.ts +27 -0
- package/dist/resources/discovery-learning.js +58 -0
- package/dist/resources/discovery.d.ts +69 -0
- package/dist/resources/discovery.js +156 -0
- package/dist/resources/posts.d.ts +5 -0
- package/dist/resources/posts.js +4 -0
- package/dist/resources/protocols.d.ts +19 -1
- package/dist/resources/protocols.js +6 -2
- package/dist/types.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -595,3 +595,17 @@ Standalone public text publication uses `protocols.prepareNativePost({ clientMut
|
|
|
595
595
|
For Bluesky accounts advertising `quote`, prepare the same publication with `quote: { protocol: 'bluesky', nativeId: sourceAtUri }`. The server resolves the exact source CID using each connected account, checks quote permission and Minds safety, and rechecks before writing. Review each delivery’s `quote` alongside the account and comment before submitting. Changed or unavailable sources cannot publish; uncertain deliveries reconcile the original record key. ActivityPub/Nostr quotes are not provided by this method. Public post reads include at most one filtered `quote` with `state: 'available'` and `post`, or `state: 'unavailable'` without source content. Available quotes may include `embeddedReferences`: exact standalone source-reference lines in the unchanged outer `text`. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
|
|
596
596
|
|
|
597
597
|
Topic browsing can request `getPublicTrends({ protocol: "bluesky", includePreviews: true })`. Optional `SocialTrend.preview` is a real image-bearing post from that topic, never a generated illustration. The server samples at most eight posts for each of ten topics, with three concurrent reads and a shared 2.5-second deadline; missing images leave usable text topics. App moderation and user source controls filter previews on every request. Bluesky CDN image attachments, quote-with-media images, and unlabeled link-preview thumbnails are supported; omitted content retains the source link.
|
|
598
|
+
|
|
599
|
+
### Continuous public discovery
|
|
600
|
+
|
|
601
|
+
Create one `sdk.discovery.createFeed({ networks, category, source, learning })`
|
|
602
|
+
per signed-in viewer and filter. Pass the app-advertised network catalog, then call
|
|
603
|
+
`next()` as the reader approaches the end. Each page contains safe json-render
|
|
604
|
+
module specs and native or protocol post objects. Preserve their existing source
|
|
605
|
+
authority and interactive controls. `retryFailed()` explicitly retries unavailable
|
|
606
|
+
sources; create a new session to refresh exhausted source windows.
|
|
607
|
+
|
|
608
|
+
Optional `DiscoveryLearning` consumes real visible impressions and explicit
|
|
609
|
+
engagement/hide outcomes. Persist its count-only snapshot per viewer; never share
|
|
610
|
+
a session or learning state across accounts. It starts conservatively and does
|
|
611
|
+
not imply measured retention improvement.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './resources/discovery-learning.js';
|
|
2
|
+
import { DiscoveryResource } from './resources/discovery.js';
|
|
3
|
+
export * from './resources/discovery.js';
|
|
1
4
|
import { AuthResource } from './resources/auth.js';
|
|
2
5
|
import { PostsResource } from './resources/posts.js';
|
|
3
6
|
import { UsersResource } from './resources/users.js';
|
|
@@ -113,6 +116,7 @@ export declare class Recursiv {
|
|
|
113
116
|
readonly webhooks: WebhooksResource;
|
|
114
117
|
readonly jobs: JobsResource;
|
|
115
118
|
readonly curator: CuratorResource;
|
|
119
|
+
readonly discovery: DiscoveryResource;
|
|
116
120
|
readonly video: VideoResource;
|
|
117
121
|
readonly appSubscriptions: AppSubscriptionsResource;
|
|
118
122
|
readonly reports: ReportsResource;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './resources/discovery-learning.js';
|
|
2
|
+
import { DiscoveryResource } from './resources/discovery.js';
|
|
3
|
+
export * from './resources/discovery.js';
|
|
1
4
|
import { HttpClient } from './client.js';
|
|
2
5
|
import { AuthResource } from './resources/auth.js';
|
|
3
6
|
import { PostsResource } from './resources/posts.js';
|
|
@@ -114,6 +117,7 @@ export class Recursiv {
|
|
|
114
117
|
webhooks;
|
|
115
118
|
jobs;
|
|
116
119
|
curator;
|
|
120
|
+
discovery;
|
|
117
121
|
video;
|
|
118
122
|
appSubscriptions;
|
|
119
123
|
reports;
|
|
@@ -175,6 +179,7 @@ export class Recursiv {
|
|
|
175
179
|
this.webhooks = new WebhooksResource(client);
|
|
176
180
|
this.jobs = new JobsResource(client);
|
|
177
181
|
this.curator = new CuratorResource(client);
|
|
182
|
+
this.discovery = new DiscoveryResource(this.posts, this.protocols);
|
|
178
183
|
this.video = new VideoResource(client);
|
|
179
184
|
this.appSubscriptions = new AppSubscriptionsResource(client);
|
|
180
185
|
this.reports = new ReportsResource(client);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DiscoveryLayout } from '../types.js';
|
|
2
|
+
export type DiscoveryPresentation = DiscoveryLayout['presentation'];
|
|
3
|
+
type Counts = {
|
|
4
|
+
views: number;
|
|
5
|
+
engagements: number;
|
|
6
|
+
hides: number;
|
|
7
|
+
};
|
|
8
|
+
export interface DiscoveryLearningSnapshot {
|
|
9
|
+
version: 1;
|
|
10
|
+
startedAt: number;
|
|
11
|
+
layouts: Record<DiscoveryPresentation, Counts>;
|
|
12
|
+
}
|
|
13
|
+
/** Local, per-viewer experimental presentation preferences. Stores counts only,
|
|
14
|
+
* never source text, author IDs or inferred traits. It cannot promote content,
|
|
15
|
+
* bypass safety, add unsupported layouts or establish a causal lift claim. */
|
|
16
|
+
export declare class DiscoveryLearning {
|
|
17
|
+
private now;
|
|
18
|
+
private value;
|
|
19
|
+
private observed;
|
|
20
|
+
constructor(input?: unknown, now?: () => number);
|
|
21
|
+
observe(id: string, presentation: DiscoveryPresentation, event: 'view' | 'engage' | 'hide'): void;
|
|
22
|
+
/** Jev proposes a suitable layout. Sufficient recent feedback may restore full
|
|
23
|
+
* post cards; every tenth eligible batch explores the safe proposal again. */
|
|
24
|
+
choose(proposal: DiscoveryPresentation, page: number): DiscoveryPresentation;
|
|
25
|
+
snapshot(): DiscoveryLearningSnapshot;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const names = ['posts', 'visual', 'conversation', 'video'];
|
|
2
|
+
const fresh = (now) => ({ version: 1, startedAt: now, layouts: {
|
|
3
|
+
video: { views: 0, engagements: 0, hides: 0 }, posts: { views: 0, engagements: 0, hides: 0 }, visual: { views: 0, engagements: 0, hides: 0 }, conversation: { views: 0, engagements: 0, hides: 0 },
|
|
4
|
+
} });
|
|
5
|
+
/** Local, per-viewer experimental presentation preferences. Stores counts only,
|
|
6
|
+
* never source text, author IDs or inferred traits. It cannot promote content,
|
|
7
|
+
* bypass safety, add unsupported layouts or establish a causal lift claim. */
|
|
8
|
+
export class DiscoveryLearning {
|
|
9
|
+
now;
|
|
10
|
+
value;
|
|
11
|
+
observed = new Set();
|
|
12
|
+
constructor(input, now = Date.now) {
|
|
13
|
+
this.now = now;
|
|
14
|
+
const value = input;
|
|
15
|
+
const time = now();
|
|
16
|
+
this.value = value?.version === 1 && Number.isFinite(value.startedAt) && value.startedAt <= time
|
|
17
|
+
&& time - value.startedAt < 14 * 86400_000 && names.every(name => {
|
|
18
|
+
const c = value.layouts?.[name];
|
|
19
|
+
return c && [c.views, c.engagements, c.hides].every(n => Number.isSafeInteger(n) && n >= 0 && n <= 10000)
|
|
20
|
+
&& c.engagements <= c.views && c.hides <= c.views;
|
|
21
|
+
}) ? JSON.parse(JSON.stringify(value)) : fresh(time);
|
|
22
|
+
}
|
|
23
|
+
observe(id, presentation, event) {
|
|
24
|
+
if (!names.includes(presentation) || !id || id.length > 8192)
|
|
25
|
+
return;
|
|
26
|
+
const key = `${id}:${event}`;
|
|
27
|
+
if (this.observed.has(key))
|
|
28
|
+
return;
|
|
29
|
+
// Engagements/hides require a real visible impression in this session.
|
|
30
|
+
if (event !== 'view' && !this.observed.has(`${id}:view`))
|
|
31
|
+
return;
|
|
32
|
+
this.observed.add(key);
|
|
33
|
+
while (this.observed.size > 4096)
|
|
34
|
+
this.observed.delete(this.observed.values().next().value);
|
|
35
|
+
const counters = this.value.layouts[presentation];
|
|
36
|
+
counters[event === 'view' ? 'views' : event === 'engage' ? 'engagements' : 'hides']++;
|
|
37
|
+
if (counters.views >= 10000)
|
|
38
|
+
for (const counts of Object.values(this.value.layouts)) {
|
|
39
|
+
counts.views = Math.floor(counts.views / 2);
|
|
40
|
+
counts.engagements = Math.floor(counts.engagements / 2);
|
|
41
|
+
counts.hides = Math.floor(counts.hides / 2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Jev proposes a suitable layout. Sufficient recent feedback may restore full
|
|
45
|
+
* post cards; every tenth eligible batch explores the safe proposal again. */
|
|
46
|
+
choose(proposal, page) {
|
|
47
|
+
if (proposal === 'posts' || page % 10 === 0)
|
|
48
|
+
return proposal;
|
|
49
|
+
const selected = this.value.layouts[proposal];
|
|
50
|
+
const baseline = this.value.layouts.posts;
|
|
51
|
+
if (selected.views < 50 || baseline.views < 50)
|
|
52
|
+
return proposal;
|
|
53
|
+
const value = (c) => (c.engagements + 2 - 3 * c.hides) / (c.views + 10);
|
|
54
|
+
// Require a material difference, rather than switching on a single click.
|
|
55
|
+
return value(selected) + 0.1 < value(baseline) ? 'posts' : proposal;
|
|
56
|
+
}
|
|
57
|
+
snapshot() { return JSON.parse(JSON.stringify(this.value)); }
|
|
58
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DiscoveryLearning } from './discovery-learning.js';
|
|
2
|
+
import type { DiscoveryLayout, Post } from '../types.js';
|
|
3
|
+
import type { PostsResource } from './posts.js';
|
|
4
|
+
import type { ProtocolsResource, SocialNetwork, SocialPost } from './protocols.js';
|
|
5
|
+
export type DiscoveryItem = {
|
|
6
|
+
key: string;
|
|
7
|
+
kind: 'minds';
|
|
8
|
+
post: Post;
|
|
9
|
+
} | {
|
|
10
|
+
key: string;
|
|
11
|
+
kind: 'social';
|
|
12
|
+
post: SocialPost;
|
|
13
|
+
};
|
|
14
|
+
/** json-render Spec: values are references to authenticated SDK results, never
|
|
15
|
+
* arbitrary executable components, links, generated text or provider markup. */
|
|
16
|
+
export interface DiscoveryModule {
|
|
17
|
+
id: string;
|
|
18
|
+
presentation: DiscoveryLayout['presentation'];
|
|
19
|
+
source: DiscoveryLayout['source'];
|
|
20
|
+
items: DiscoveryItem[];
|
|
21
|
+
spec: {
|
|
22
|
+
root: 'module';
|
|
23
|
+
elements: {
|
|
24
|
+
module: {
|
|
25
|
+
type: 'DiscoveryPosts' | 'DiscoveryGallery' | 'DiscoveryConversation' | 'DiscoveryVideo';
|
|
26
|
+
props: {
|
|
27
|
+
itemKeys: string[];
|
|
28
|
+
};
|
|
29
|
+
children: [];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface DiscoveryFeedPage {
|
|
35
|
+
modules: DiscoveryModule[];
|
|
36
|
+
hasMore: boolean;
|
|
37
|
+
failedSources: string[];
|
|
38
|
+
partialCategories: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Per-viewer feed session. Create a new session when user, app, category or
|
|
41
|
+
* source changes. All reads retain existing server authorization/moderation.
|
|
42
|
+
* At most four requests per page; providers fail independently. */
|
|
43
|
+
export declare class DiscoveryFeedSession {
|
|
44
|
+
private posts;
|
|
45
|
+
private protocols;
|
|
46
|
+
private category?;
|
|
47
|
+
private learning?;
|
|
48
|
+
private streams;
|
|
49
|
+
private pending;
|
|
50
|
+
private seen;
|
|
51
|
+
private page;
|
|
52
|
+
private nextSource;
|
|
53
|
+
constructor(posts: Pick<PostsResource, 'list'>, protocols: Pick<ProtocolsResource, 'getPublicTrends' | 'getPublicTrendFeed'>, networks: SocialNetwork[], category?: string | undefined, source?: string, learning?: DiscoveryLearning | undefined);
|
|
54
|
+
retryFailed(): void;
|
|
55
|
+
next(): Promise<DiscoveryFeedPage>;
|
|
56
|
+
private read;
|
|
57
|
+
private readPage;
|
|
58
|
+
}
|
|
59
|
+
export declare class DiscoveryResource {
|
|
60
|
+
private posts;
|
|
61
|
+
private protocols;
|
|
62
|
+
constructor(posts: PostsResource, protocols: ProtocolsResource);
|
|
63
|
+
createFeed(options: {
|
|
64
|
+
networks: SocialNetwork[];
|
|
65
|
+
category?: string;
|
|
66
|
+
source?: string;
|
|
67
|
+
learning?: DiscoveryLearning;
|
|
68
|
+
}): DiscoveryFeedSession;
|
|
69
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const ordinary = { version: 'discovery-layout-v1', presentation: 'posts', source: 'fallback' };
|
|
2
|
+
const key = (protocol, id) => JSON.stringify([protocol, id]);
|
|
3
|
+
/** Per-viewer feed session. Create a new session when user, app, category or
|
|
4
|
+
* source changes. All reads retain existing server authorization/moderation.
|
|
5
|
+
* At most four requests per page; providers fail independently. */
|
|
6
|
+
export class DiscoveryFeedSession {
|
|
7
|
+
posts;
|
|
8
|
+
protocols;
|
|
9
|
+
category;
|
|
10
|
+
learning;
|
|
11
|
+
streams;
|
|
12
|
+
pending = null;
|
|
13
|
+
seen = new Set();
|
|
14
|
+
page = 0;
|
|
15
|
+
nextSource = 0;
|
|
16
|
+
constructor(posts, protocols, networks, category, source, learning) {
|
|
17
|
+
this.posts = posts;
|
|
18
|
+
this.protocols = protocols;
|
|
19
|
+
this.category = category;
|
|
20
|
+
this.learning = learning;
|
|
21
|
+
const sources = [...new Set(networks.filter(n => n.capabilities.includes('public_trends')).map(n => n.protocol))].slice(0, 16);
|
|
22
|
+
this.streams = ['minds', ...sources].filter(protocol => !source || source === 'all' || source === protocol)
|
|
23
|
+
.map(protocol => ({ protocol, offset: 0, done: false, failed: false, used: [] }));
|
|
24
|
+
}
|
|
25
|
+
retryFailed() { for (const stream of this.streams)
|
|
26
|
+
stream.failed = false; }
|
|
27
|
+
next() {
|
|
28
|
+
if (this.pending)
|
|
29
|
+
return this.pending;
|
|
30
|
+
this.pending = this.readPage().finally(() => { this.pending = null; });
|
|
31
|
+
return this.pending;
|
|
32
|
+
}
|
|
33
|
+
async read(stream) {
|
|
34
|
+
const params = { limit: 20, ...(this.category ? { category: this.category } : {}), discovery: true };
|
|
35
|
+
if (stream.protocol === 'minds') {
|
|
36
|
+
const response = await this.posts.list({ ...params, sort: 'new', ...(stream.cursor ? { cursor: stream.cursor } : { offset: stream.offset }) });
|
|
37
|
+
stream.offset += response.meta.scanned ?? response.meta.limit;
|
|
38
|
+
const next = response.meta.next_cursor ?? undefined;
|
|
39
|
+
stream.done = !response.meta.has_more || !!next && (next === stream.cursor || stream.used.includes(next));
|
|
40
|
+
if (stream.cursor)
|
|
41
|
+
stream.used = [...stream.used, stream.cursor].slice(-32);
|
|
42
|
+
stream.cursor = next;
|
|
43
|
+
return { items: response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post })),
|
|
44
|
+
layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial' };
|
|
45
|
+
}
|
|
46
|
+
const validatePosts = (data) => {
|
|
47
|
+
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(post => post.protocol !== stream.protocol || post.author.protocol !== stream.protocol))
|
|
48
|
+
throw new Error('Invalid discovery source response');
|
|
49
|
+
};
|
|
50
|
+
if (stream.topics) {
|
|
51
|
+
const topic = stream.topics[0];
|
|
52
|
+
if (!topic) {
|
|
53
|
+
stream.done = true;
|
|
54
|
+
return { items: [], layout: ordinary, partial: false };
|
|
55
|
+
}
|
|
56
|
+
const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor });
|
|
57
|
+
validatePosts(data);
|
|
58
|
+
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
59
|
+
throw new Error('Invalid discovery topic');
|
|
60
|
+
const next = data.cursor;
|
|
61
|
+
stream.topics.shift();
|
|
62
|
+
if (next && next !== topic.cursor && !topic.used.includes(next))
|
|
63
|
+
stream.topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
|
|
64
|
+
stream.done = !stream.topics.length;
|
|
65
|
+
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
66
|
+
}
|
|
67
|
+
const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true });
|
|
68
|
+
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
|
|
69
|
+
throw new Error('Invalid discovery source response');
|
|
70
|
+
if (data.kind === 'topics') {
|
|
71
|
+
if (data.items.some(topic => topic.preview && (topic.preview.protocol !== stream.protocol || topic.preview.author.protocol !== stream.protocol)))
|
|
72
|
+
throw new Error('Invalid discovery preview');
|
|
73
|
+
stream.topics = [...new Set(data.items.map(topic => topic.nativeId))].map(topic => ({ topic, used: [] }));
|
|
74
|
+
stream.done = !stream.topics.length;
|
|
75
|
+
return { items: data.items.flatMap(topic => !this.category && topic.preview ? [{ key: key(topic.preview.protocol, topic.preview.nativeId), kind: 'social', post: topic.preview }] : []), layout: ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
76
|
+
}
|
|
77
|
+
validatePosts(data);
|
|
78
|
+
const next = data.cursor ?? undefined;
|
|
79
|
+
stream.done = !next || next === stream.cursor || stream.used.includes(next);
|
|
80
|
+
if (stream.cursor)
|
|
81
|
+
stream.used = [...stream.used, stream.cursor].slice(-32);
|
|
82
|
+
stream.cursor = next;
|
|
83
|
+
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
84
|
+
}
|
|
85
|
+
async readPage() {
|
|
86
|
+
const active = this.streams.filter(stream => !stream.done && !stream.failed);
|
|
87
|
+
const native = active.find(stream => stream.protocol === 'minds');
|
|
88
|
+
const remote = active.filter(stream => stream.protocol !== 'minds');
|
|
89
|
+
const selected = [...(native ? [native] : []), ...Array.from({ length: Math.min(native ? 3 : 4, remote.length) }, (_, index) => remote[(this.nextSource + index) % remote.length])];
|
|
90
|
+
this.nextSource += 3;
|
|
91
|
+
const reads = await Promise.all(selected.map(async (stream) => {
|
|
92
|
+
// Commit continuation state only if this page finishes within its UI
|
|
93
|
+
// deadline. A late provider cannot move the cursor past unseen content.
|
|
94
|
+
const candidate = { ...stream, used: [...stream.used], topics: stream.topics?.map(topic => ({ ...topic, used: [...topic.used] })) };
|
|
95
|
+
let cancel = () => { };
|
|
96
|
+
try {
|
|
97
|
+
const read = await Promise.race([this.read(candidate), new Promise((_resolve, reject) => {
|
|
98
|
+
const timer = setTimeout(() => reject(new Error('Discovery source timed out')), 15000);
|
|
99
|
+
cancel = () => clearTimeout(timer);
|
|
100
|
+
})]);
|
|
101
|
+
Object.assign(stream, candidate);
|
|
102
|
+
return read;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
stream.failed = true;
|
|
106
|
+
return { items: [], layout: ordinary, partial: false };
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
cancel();
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
// Round-robin before grouping: the largest network cannot occupy every slot.
|
|
113
|
+
const mixed = [];
|
|
114
|
+
for (let index = 0; index < Math.max(0, ...reads.map(read => read.items.length)); index++)
|
|
115
|
+
for (const read of reads) {
|
|
116
|
+
const item = read.items[index];
|
|
117
|
+
if (!item || this.seen.has(item.key))
|
|
118
|
+
continue;
|
|
119
|
+
this.seen.add(item.key);
|
|
120
|
+
mixed.push({ item, layout: read.layout });
|
|
121
|
+
}
|
|
122
|
+
// Deduplication memory is bounded independently of the UI's virtual window.
|
|
123
|
+
while (this.seen.size > 4096)
|
|
124
|
+
this.seen.delete(this.seen.values().next().value);
|
|
125
|
+
const modules = [];
|
|
126
|
+
const page = this.page++;
|
|
127
|
+
for (let index = 0; index < mixed.length;) {
|
|
128
|
+
const first = mixed[index];
|
|
129
|
+
const proposal = first.layout.source === 'jev' ? first.layout.presentation : 'posts';
|
|
130
|
+
const preferred = this.learning?.choose(proposal, page) ?? proposal;
|
|
131
|
+
const group = mixed.slice(index, index + 4);
|
|
132
|
+
const video = preferred === 'video' && first.item.post.media?.some(media => media.type === 'video');
|
|
133
|
+
const visual = preferred === 'visual' && group.filter(row => row.item.post.media?.some(media => media.type === 'image')).length >= 2;
|
|
134
|
+
const discussion = preferred === 'conversation' && group.filter(row => row.item.kind === 'social' ? (row.item.post.replyCount ?? 0) > 0 : (row.item.post.reply_count ?? 0) > 0).length >= 2;
|
|
135
|
+
const presentation = video ? 'video' : visual ? 'visual' : discussion ? 'conversation' : 'posts';
|
|
136
|
+
const selected = presentation === 'posts' || video ? [first] : group;
|
|
137
|
+
const items = selected.map(row => row.item);
|
|
138
|
+
modules.push({ id: `discovery-${page}-${index}-${items[0].key}`, presentation, source: presentation === 'posts' ? 'fallback' : 'jev', items,
|
|
139
|
+
spec: { root: 'module', elements: { module: { type: video ? 'DiscoveryVideo' : visual ? 'DiscoveryGallery' : discussion ? 'DiscoveryConversation' : 'DiscoveryPosts', props: { itemKeys: items.map(item => item.key) }, children: [] } } } });
|
|
140
|
+
index += selected.length;
|
|
141
|
+
}
|
|
142
|
+
return { modules, hasMore: this.streams.some(stream => !stream.done && !stream.failed),
|
|
143
|
+
failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export class DiscoveryResource {
|
|
147
|
+
posts;
|
|
148
|
+
protocols;
|
|
149
|
+
constructor(posts, protocols) {
|
|
150
|
+
this.posts = posts;
|
|
151
|
+
this.protocols = protocols;
|
|
152
|
+
}
|
|
153
|
+
createFeed(options) {
|
|
154
|
+
return new DiscoveryFeedSession(this.posts, this.protocols, options.networks, options.category, options.source, options.learning);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -5,6 +5,11 @@ export declare class PostsResource {
|
|
|
5
5
|
constructor(client: HttpClient);
|
|
6
6
|
/** List posts with optional filters */
|
|
7
7
|
list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
|
|
8
|
+
/** Broad categories currently enabled for public posts in this app. */
|
|
9
|
+
categories(): Promise<SingleResponse<Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}>>>;
|
|
8
13
|
/** List posts the authenticated caller has upvoted. */
|
|
9
14
|
liked(params?: Pick<ListPostsParams, 'limit' | 'offset' | 'cursor'>): Promise<PaginatedResponse<Post>>;
|
|
10
15
|
/** Get a single post with replies */
|
package/dist/resources/posts.js
CHANGED
|
@@ -7,6 +7,10 @@ export class PostsResource {
|
|
|
7
7
|
list(params) {
|
|
8
8
|
return this.client.get('/posts', params);
|
|
9
9
|
}
|
|
10
|
+
/** Broad categories currently enabled for public posts in this app. */
|
|
11
|
+
categories() {
|
|
12
|
+
return this.client.get('/posts/categories');
|
|
13
|
+
}
|
|
10
14
|
/** List posts the authenticated caller has upvoted. */
|
|
11
15
|
liked(params) {
|
|
12
16
|
return this.list({ ...params, liked: true });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DiscoveryLayout } from '../types.js';
|
|
1
2
|
import type { HttpClient } from '../client.js';
|
|
2
3
|
import type { PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
|
|
3
4
|
/** Canonical protocol operation names returned by the adapter registry. */
|
|
@@ -784,6 +785,11 @@ export interface SocialNetwork {
|
|
|
784
785
|
capabilities: Array<'profile_search' | 'profile_read' | 'original_author_posts' | 'minds_subscription' | 'following_feed' | 'conversation_read' | 'public_trends' | 'trend_feed' | 'popular_accounts' | 'post_search'>;
|
|
785
786
|
profileSearchPagination: boolean;
|
|
786
787
|
postSearchAccountRequired?: boolean;
|
|
788
|
+
/** Categories available from the current app configuration. */
|
|
789
|
+
categories?: Array<{
|
|
790
|
+
id: string;
|
|
791
|
+
name: string;
|
|
792
|
+
}>;
|
|
787
793
|
conversationCoverage?: string;
|
|
788
794
|
}
|
|
789
795
|
/** Public source identity, independent of any local Minds user or handle. */
|
|
@@ -839,9 +845,10 @@ export interface SocialPost {
|
|
|
839
845
|
likeCount: number | null;
|
|
840
846
|
repostCount: number | null;
|
|
841
847
|
media: Array<{
|
|
842
|
-
type: 'image';
|
|
848
|
+
type: 'image' | 'video';
|
|
843
849
|
url: string;
|
|
844
850
|
alt: string;
|
|
851
|
+
poster?: string;
|
|
845
852
|
}>;
|
|
846
853
|
hasAdditionalContent: boolean;
|
|
847
854
|
/** At most one independently filtered quoted post. */
|
|
@@ -879,6 +886,7 @@ export interface SocialAuthorFeed {
|
|
|
879
886
|
coverage: 'original_posts';
|
|
880
887
|
}
|
|
881
888
|
export interface SocialTrendProvenance {
|
|
889
|
+
discovery?: DiscoveryLayout;
|
|
882
890
|
provider: {
|
|
883
891
|
name: string;
|
|
884
892
|
url: string;
|
|
@@ -887,6 +895,12 @@ export interface SocialTrendProvenance {
|
|
|
887
895
|
coverage: 'provider_ranked_window';
|
|
888
896
|
/** Exact ranking interval is not supplied by these providers. */
|
|
889
897
|
rankingWindow: null;
|
|
898
|
+
categoryFilter?: {
|
|
899
|
+
selected: string;
|
|
900
|
+
status: 'complete' | 'partial';
|
|
901
|
+
classified: number;
|
|
902
|
+
unclassified: number;
|
|
903
|
+
};
|
|
890
904
|
}
|
|
891
905
|
export interface SocialTrend {
|
|
892
906
|
protocol: string;
|
|
@@ -1012,6 +1026,8 @@ export declare class ProtocolsResource {
|
|
|
1012
1026
|
limit?: number;
|
|
1013
1027
|
cursor?: string;
|
|
1014
1028
|
includePreviews?: boolean;
|
|
1029
|
+
category?: string;
|
|
1030
|
+
discovery?: boolean;
|
|
1015
1031
|
}): Promise<SingleResponse<SocialTrends>>;
|
|
1016
1032
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
1017
1033
|
getPublicTrendFeed(params: {
|
|
@@ -1019,6 +1035,8 @@ export declare class ProtocolsResource {
|
|
|
1019
1035
|
topic: string;
|
|
1020
1036
|
limit?: number;
|
|
1021
1037
|
cursor?: string;
|
|
1038
|
+
category?: string;
|
|
1039
|
+
discovery?: boolean;
|
|
1022
1040
|
}): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1023
1041
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
1024
1042
|
getPublicPopularProfiles(params: {
|
|
@@ -110,11 +110,15 @@ export class ProtocolsResource {
|
|
|
110
110
|
/** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
|
|
111
111
|
getPublicTrends(params) {
|
|
112
112
|
return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
|
|
113
|
-
...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {})
|
|
113
|
+
...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
|
|
114
|
+
...(params.category !== undefined ? { category: params.category } : {}),
|
|
115
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
114
116
|
}
|
|
115
117
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
116
118
|
getPublicTrendFeed(params) {
|
|
117
|
-
return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor
|
|
119
|
+
return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
|
|
120
|
+
...(params.category !== undefined ? { category: params.category } : {}),
|
|
121
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
118
122
|
}
|
|
119
123
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
120
124
|
getPublicPopularProfiles(params) {
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,20 @@ export interface PaginationParams {
|
|
|
5
5
|
cursor?: string;
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
}
|
|
8
|
+
export interface DiscoveryLayout {
|
|
9
|
+
version: 'discovery-layout-v1';
|
|
10
|
+
presentation: 'posts' | 'visual' | 'conversation' | 'video';
|
|
11
|
+
source: 'jev' | 'fallback';
|
|
12
|
+
}
|
|
8
13
|
export interface PaginationMeta {
|
|
14
|
+
discovery?: DiscoveryLayout;
|
|
15
|
+
scanned?: number;
|
|
16
|
+
category_filter?: {
|
|
17
|
+
selected: string;
|
|
18
|
+
status: 'complete' | 'partial';
|
|
19
|
+
classified: number;
|
|
20
|
+
unclassified: number;
|
|
21
|
+
};
|
|
9
22
|
limit: number;
|
|
10
23
|
offset: number;
|
|
11
24
|
has_more: boolean;
|
|
@@ -93,6 +106,7 @@ export interface MediaItem {
|
|
|
93
106
|
type: string;
|
|
94
107
|
}
|
|
95
108
|
export interface Post {
|
|
109
|
+
reply_count?: number;
|
|
96
110
|
id: string;
|
|
97
111
|
/** Original Minds numeric GUID when this row came from the legacy import. */
|
|
98
112
|
legacy_guid?: string | null;
|
|
@@ -171,6 +185,10 @@ export interface UpdatePostInput {
|
|
|
171
185
|
}
|
|
172
186
|
export type ReactionType = 'like' | 'heart' | 'fire' | 'laugh' | 'sad' | 'angry' | 'upvote' | 'downvote';
|
|
173
187
|
export interface ListPostsParams extends PaginationParams {
|
|
188
|
+
/** Public-only feed with a bounded Jev presentation hint. Limit <=25. */
|
|
189
|
+
discovery?: boolean;
|
|
190
|
+
/** Public-only broad category. Requires an enabled app experiment; limit <=25. */
|
|
191
|
+
category?: string;
|
|
174
192
|
community_id?: string;
|
|
175
193
|
author_id?: string;
|
|
176
194
|
organization_id?: string;
|