@rocksky/sdk 0.3.0 → 0.4.0
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 +44 -254
- package/dist/agent.d.ts +77 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/client.d.ts +26 -113
- package/dist/client.d.ts.map +1 -1
- package/dist/dedup.d.ts +38 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/errors.d.ts +3 -23
- package/dist/errors.d.ts.map +1 -1
- package/dist/generated/types.d.ts +106 -8
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/index.d.ts +16 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6648 -1288
- package/dist/jetstream.d.ts +17 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/package.json +15 -11
- package/src/agent.ts +232 -0
- package/src/client.ts +82 -226
- package/src/dedup.ts +207 -0
- package/src/errors.ts +6 -43
- package/src/generated/types.ts +115 -8
- package/src/hash.ts +22 -0
- package/src/index.ts +16 -86
- package/src/jetstream.ts +122 -0
- package/src/http.ts +0 -195
- package/src/namespaces/_helpers.ts +0 -27
- package/src/namespaces/actor.ts +0 -93
- package/src/namespaces/album.ts +0 -34
- package/src/namespaces/apikey.ts +0 -50
- package/src/namespaces/artist.ts +0 -68
- package/src/namespaces/charts.ts +0 -38
- package/src/namespaces/dropbox.ts +0 -53
- package/src/namespaces/feed.ts +0 -112
- package/src/namespaces/googledrive.ts +0 -41
- package/src/namespaces/graph.ts +0 -62
- package/src/namespaces/like.ts +0 -46
- package/src/namespaces/mirror.ts +0 -27
- package/src/namespaces/player.ts +0 -125
- package/src/namespaces/playlist.ts +0 -95
- package/src/namespaces/scrobble.ts +0 -41
- package/src/namespaces/shout.ts +0 -99
- package/src/namespaces/song.ts +0 -60
- package/src/namespaces/spotify.ts +0 -56
- package/src/namespaces/stats.ts +0 -27
- package/src/paginate.ts +0 -90
- package/src/pipe.ts +0 -146
- package/src/realtime.ts +0 -408
- package/src/types.ts +0 -41
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RockskyIndex } from "./dedup.js";
|
|
2
|
+
/** The four public Bluesky Jetstream servers. */
|
|
3
|
+
export declare const DEFAULT_JETSTREAM_SERVERS: string[];
|
|
4
|
+
export interface JetstreamOptions {
|
|
5
|
+
/** Servers to connect to at once (defaults to {@link DEFAULT_JETSTREAM_SERVERS}). */
|
|
6
|
+
servers?: string[];
|
|
7
|
+
/** Cancels the hydration and closes all connections when aborted. */
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Hydrate `idx` from the Bluesky Jetstream firehose for `did`, connecting to
|
|
12
|
+
* every server at once, filtered to app.rocksky.* + this DID. A shared watermark
|
|
13
|
+
* de-duplicates the overlap between servers and is the reconnect cursor. Resolves
|
|
14
|
+
* when opts.signal aborts; each source reconnects with backoff.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runJetstream(idx: RockskyIndex, did: string, opts?: JetstreamOptions): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=jetstream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,UAKrC,CAAC;AAIF,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7G"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rocksky/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TypeScript SDK for
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "TypeScript SDK for Rocksky — built on atcute: AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), a local dedup index, and Jetstream real-time sync.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -31,16 +31,9 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external none && bun run build:types",
|
|
33
33
|
"build:types": "tsc -p tsconfig.build.json",
|
|
34
|
-
"test": "bun test",
|
|
35
|
-
"test:watch": "bun test --watch",
|
|
36
34
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"
|
|
38
|
-
"example
|
|
39
|
-
"example:pipe": "bun run examples/pipe.ts",
|
|
40
|
-
"example:scrobble": "bun run examples/scrobble.ts",
|
|
41
|
-
"example:pagination": "bun run examples/pagination.ts",
|
|
42
|
-
"example:paginate": "bun run examples/paginate.ts",
|
|
43
|
-
"example:realtime": "bun run examples/realtime.ts"
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"example": "bun run examples/native.ts"
|
|
44
37
|
},
|
|
45
38
|
"keywords": [
|
|
46
39
|
"rocksky",
|
|
@@ -59,7 +52,18 @@
|
|
|
59
52
|
"api"
|
|
60
53
|
],
|
|
61
54
|
"devDependencies": {
|
|
55
|
+
"@atcute/atproto": "^4.0.3",
|
|
62
56
|
"@types/bun": "latest",
|
|
63
57
|
"typescript": "^5.6.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@atcute/car": "^6.0.2",
|
|
61
|
+
"@atcute/cbor": "^2.3.6",
|
|
62
|
+
"@atcute/cid": "^2.4.2",
|
|
63
|
+
"@atcute/client": "^5.1.1",
|
|
64
|
+
"@atcute/identity": "^2.0.2",
|
|
65
|
+
"@atcute/identity-resolver": "^2.0.1",
|
|
66
|
+
"@atcute/password-session": "^1.0.2",
|
|
67
|
+
"classic-level": "^3.0.0"
|
|
64
68
|
}
|
|
65
69
|
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Client } from "@atcute/client";
|
|
2
|
+
import {
|
|
3
|
+
CompositeDidDocumentResolver,
|
|
4
|
+
CompositeHandleResolver,
|
|
5
|
+
DohJsonHandleResolver,
|
|
6
|
+
LocalActorResolver,
|
|
7
|
+
PlcDidDocumentResolver,
|
|
8
|
+
WebDidDocumentResolver,
|
|
9
|
+
WellKnownHandleResolver,
|
|
10
|
+
} from "@atcute/identity-resolver";
|
|
11
|
+
import { PasswordSession } from "@atcute/password-session";
|
|
12
|
+
|
|
13
|
+
import { RockskyError } from "./errors.js";
|
|
14
|
+
import type {
|
|
15
|
+
AlbumRecord,
|
|
16
|
+
ArtistRecord,
|
|
17
|
+
ActorTrackView,
|
|
18
|
+
ScrobbleRecord,
|
|
19
|
+
SongRecord,
|
|
20
|
+
} from "./generated/types.js";
|
|
21
|
+
import type { IndexStats, RockskyIndex } from "./dedup.js";
|
|
22
|
+
import { runJetstream, type JetstreamOptions } from "./jetstream.js";
|
|
23
|
+
|
|
24
|
+
// Collection NSIDs written by the agent.
|
|
25
|
+
const C_SCROBBLE = "app.rocksky.scrobble";
|
|
26
|
+
const C_SONG = "app.rocksky.song";
|
|
27
|
+
const C_ALBUM = "app.rocksky.album";
|
|
28
|
+
const C_ARTIST = "app.rocksky.artist";
|
|
29
|
+
const C_LIKE = "app.rocksky.like";
|
|
30
|
+
const C_FOLLOW = "app.rocksky.graph.follow";
|
|
31
|
+
const C_SHOUT = "app.rocksky.shout";
|
|
32
|
+
const C_STATUS = "app.rocksky.actor.status";
|
|
33
|
+
|
|
34
|
+
function actorResolver(): LocalActorResolver {
|
|
35
|
+
return new LocalActorResolver({
|
|
36
|
+
handleResolver: new CompositeHandleResolver({
|
|
37
|
+
methods: {
|
|
38
|
+
dns: new DohJsonHandleResolver({ dohUrl: "https://mozilla.cloudflare-dns.com/dns-query" }),
|
|
39
|
+
http: new WellKnownHandleResolver(),
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
didDocumentResolver: new CompositeDidDocumentResolver({
|
|
43
|
+
methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver() },
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nowISO(): string {
|
|
49
|
+
return new Date().toISOString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Write inputs: `createdAt` is optional (the SDK defaults it to now).
|
|
53
|
+
/** Input for {@link Agent.scrobble} (createdAt defaults to now). */
|
|
54
|
+
export type ScrobbleInput = Omit<ScrobbleRecord, "createdAt"> & { createdAt?: string };
|
|
55
|
+
/** Input for {@link Agent.createSong}. */
|
|
56
|
+
export type SongInput = Omit<SongRecord, "createdAt"> & { createdAt?: string };
|
|
57
|
+
/** Input for {@link Agent.createAlbum} (`artist` is the album artist). */
|
|
58
|
+
export type AlbumInput = Omit<AlbumRecord, "createdAt"> & { createdAt?: string };
|
|
59
|
+
/** Input for {@link Agent.createArtist}. */
|
|
60
|
+
export type ArtistInput = Omit<ArtistRecord, "createdAt"> & { createdAt?: string };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Authenticated Rocksky client: logs in with an app password and writes
|
|
64
|
+
* app.rocksky.* records to the user's PDS (via atcute). Attach a
|
|
65
|
+
* {@link RockskyIndex} with {@link Agent.useIndex} for duplicate prevention.
|
|
66
|
+
*/
|
|
67
|
+
export class Agent {
|
|
68
|
+
private idx?: RockskyIndex;
|
|
69
|
+
|
|
70
|
+
private constructor(
|
|
71
|
+
private rpc: Client,
|
|
72
|
+
readonly did: string,
|
|
73
|
+
readonly session: PasswordSession,
|
|
74
|
+
private pds: string,
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the account's PDS, authenticate with an app password, and return an
|
|
79
|
+
* Agent. `identifier` is a handle or DID.
|
|
80
|
+
*/
|
|
81
|
+
static async login(identifier: string, password: string): Promise<Agent> {
|
|
82
|
+
const actor = await actorResolver().resolve(identifier as never);
|
|
83
|
+
const session = await PasswordSession.login({ service: actor.pds, identifier, password });
|
|
84
|
+
return new Agent(new Client({ handler: session }), session.did, session, actor.pds);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Attach a local dedup index — write verbs then skip records that already exist. */
|
|
88
|
+
useIndex(idx: RockskyIndex): void {
|
|
89
|
+
this.idx = idx;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Download the caller's full repository and (re)build the dedup index. Requires
|
|
94
|
+
* an attached index ({@link Agent.useIndex}). Full backfill; keep it current
|
|
95
|
+
* with {@link Agent.hydrateFromJetstream}.
|
|
96
|
+
*/
|
|
97
|
+
async syncRepo(): Promise<IndexStats> {
|
|
98
|
+
if (!this.idx) throw new Error("no dedup index attached (call useIndex)");
|
|
99
|
+
const url = `${this.pds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(this.did)}`;
|
|
100
|
+
const res = await fetch(url);
|
|
101
|
+
if (!res.ok) throw new Error(`getRepo: ${res.status}`);
|
|
102
|
+
const car = new Uint8Array(await res.arrayBuffer());
|
|
103
|
+
return this.idx.indexCar(this.did, car);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Keep the dedup index live from the Bluesky Jetstream firehose (all four
|
|
108
|
+
* servers at once, filtered to this DID + app.rocksky.*). Resolves when the
|
|
109
|
+
* options' AbortSignal fires. Requires an attached index.
|
|
110
|
+
*/
|
|
111
|
+
hydrateFromJetstream(opts: JetstreamOptions = {}): Promise<void> {
|
|
112
|
+
if (!this.idx) throw new Error("no dedup index attached (call useIndex)");
|
|
113
|
+
return runJetstream(this.idx, this.did, opts);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async create(collection: string, record: Record<string, unknown>): Promise<string> {
|
|
117
|
+
const res = await this.rpc.post("com.atproto.repo.createRecord" as never, {
|
|
118
|
+
input: { repo: this.did, collection, record: { ...record, $type: collection } },
|
|
119
|
+
} as never);
|
|
120
|
+
if (!res.ok) throw new RockskyError(res.data);
|
|
121
|
+
return (res.data as { uri: string }).uri;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async putRecord(collection: string, rkey: string, record: Record<string, unknown>): Promise<string> {
|
|
125
|
+
const res = await this.rpc.post("com.atproto.repo.putRecord" as never, {
|
|
126
|
+
input: { repo: this.did, collection, rkey, record: { ...record, $type: collection } },
|
|
127
|
+
} as never);
|
|
128
|
+
if (!res.ok) throw new RockskyError(res.data);
|
|
129
|
+
return (res.data as { uri: string }).uri;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Delete a record by collection + rkey. */
|
|
133
|
+
async delete(collection: string, rkey: string): Promise<void> {
|
|
134
|
+
const res = await this.rpc.post("com.atproto.repo.deleteRecord" as never, {
|
|
135
|
+
input: { repo: this.did, collection, rkey },
|
|
136
|
+
} as never);
|
|
137
|
+
if (!res.ok) throw new RockskyError(res.data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Scrobble a play (app.rocksky.scrobble). createdAt defaults to now. */
|
|
141
|
+
async scrobble(rec: ScrobbleInput): Promise<string> {
|
|
142
|
+
const record = { ...rec, createdAt: rec.createdAt || nowISO() };
|
|
143
|
+
if (this.idx) {
|
|
144
|
+
const secs = Math.floor(Date.parse(record.createdAt) / 1000);
|
|
145
|
+
const existing = await this.idx.scrobbleUri(this.did, record.title!, record.artist!, record.album!, secs);
|
|
146
|
+
if (existing) return existing;
|
|
147
|
+
}
|
|
148
|
+
const uri = await this.create(C_SCROBBLE, record as Record<string, unknown>);
|
|
149
|
+
if (this.idx) {
|
|
150
|
+
const secs = Math.floor(Date.parse(record.createdAt) / 1000);
|
|
151
|
+
await this.idx.recordScrobble(this.did, record.title!, record.artist!, record.album!, secs, uri);
|
|
152
|
+
}
|
|
153
|
+
return uri;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Create a canonical track record (app.rocksky.song). */
|
|
157
|
+
async createSong(rec: SongInput): Promise<string> {
|
|
158
|
+
const record = { ...rec, createdAt: rec.createdAt || nowISO() };
|
|
159
|
+
if (this.idx) {
|
|
160
|
+
const existing = await this.idx.songUri(this.did, record.title!, record.artist!, record.album!);
|
|
161
|
+
if (existing) return existing;
|
|
162
|
+
}
|
|
163
|
+
const uri = await this.create(C_SONG, record as Record<string, unknown>);
|
|
164
|
+
if (this.idx) await this.idx.recordSong(this.did, record.title!, record.artist!, record.album!, uri);
|
|
165
|
+
return uri;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Create an album record (app.rocksky.album). `artist` is the album artist. */
|
|
169
|
+
async createAlbum(rec: AlbumInput): Promise<string> {
|
|
170
|
+
const record = { ...rec, createdAt: rec.createdAt || nowISO() };
|
|
171
|
+
if (this.idx) {
|
|
172
|
+
const existing = await this.idx.albumUri(this.did, record.title!, record.artist!);
|
|
173
|
+
if (existing) return existing;
|
|
174
|
+
}
|
|
175
|
+
const uri = await this.create(C_ALBUM, record as Record<string, unknown>);
|
|
176
|
+
if (this.idx) await this.idx.recordAlbum(this.did, record.title!, record.artist!, uri);
|
|
177
|
+
return uri;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Create an artist record (app.rocksky.artist). */
|
|
181
|
+
async createArtist(rec: ArtistInput): Promise<string> {
|
|
182
|
+
const record = { ...rec, createdAt: rec.createdAt || nowISO() };
|
|
183
|
+
if (this.idx) {
|
|
184
|
+
const existing = await this.idx.artistUri(this.did, record.name!);
|
|
185
|
+
if (existing) return existing;
|
|
186
|
+
}
|
|
187
|
+
const uri = await this.create(C_ARTIST, record as Record<string, unknown>);
|
|
188
|
+
if (this.idx) await this.idx.recordArtist(this.did, record.name!, uri);
|
|
189
|
+
return uri;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Like a record by strong reference (uri + cid). Returns the like URI. */
|
|
193
|
+
like(uri: string, cid: string): Promise<string> {
|
|
194
|
+
return this.create(C_LIKE, { subject: { uri, cid }, createdAt: nowISO() });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Follow an account by DID. Returns the follow URI. */
|
|
198
|
+
follow(did: string): Promise<string> {
|
|
199
|
+
return this.create(C_FOLLOW, { subject: did, createdAt: nowISO() });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Post a shout on a subject. Returns the shout URI. */
|
|
203
|
+
shout(subjectUri: string, subjectCid: string, message: string): Promise<string> {
|
|
204
|
+
return this.create(C_SHOUT, { subject: { uri: subjectUri, cid: subjectCid }, message, createdAt: nowISO() });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Reply to a shout, with a parent strong-ref. */
|
|
208
|
+
replyShout(
|
|
209
|
+
subjectUri: string,
|
|
210
|
+
subjectCid: string,
|
|
211
|
+
parentUri: string,
|
|
212
|
+
parentCid: string,
|
|
213
|
+
message: string,
|
|
214
|
+
): Promise<string> {
|
|
215
|
+
return this.create(C_SHOUT, {
|
|
216
|
+
subject: { uri: subjectUri, cid: subjectCid },
|
|
217
|
+
parent: { uri: parentUri, cid: parentCid },
|
|
218
|
+
message,
|
|
219
|
+
createdAt: nowISO(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Upsert the actor's now-playing status singleton (rkey "self"). */
|
|
224
|
+
setNowPlaying(track: ActorTrackView): Promise<string> {
|
|
225
|
+
return this.putRecord(C_STATUS, "self", { track, startedAt: nowISO() });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Delete the actor's now-playing status singleton. */
|
|
229
|
+
clearNowPlaying(): Promise<void> {
|
|
230
|
+
return this.delete(C_STATUS, "self");
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,252 +1,108 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { makeCall } from "./namespaces/_helpers.js";
|
|
3
|
-
import {
|
|
4
|
-
type PaginateArgs,
|
|
5
|
-
paginate as paginateFn,
|
|
6
|
-
} from "./paginate.js";
|
|
7
|
-
import {
|
|
8
|
-
RealtimeClient,
|
|
9
|
-
type RealtimeOptions,
|
|
10
|
-
createRealtimeClient,
|
|
11
|
-
} from "./realtime.js";
|
|
12
|
-
import { ActorNamespace } from "./namespaces/actor.js";
|
|
13
|
-
import { AlbumNamespace } from "./namespaces/album.js";
|
|
14
|
-
import { ApikeyNamespace } from "./namespaces/apikey.js";
|
|
15
|
-
import { ArtistNamespace } from "./namespaces/artist.js";
|
|
16
|
-
import { ChartsNamespace } from "./namespaces/charts.js";
|
|
17
|
-
import { DropboxNamespace } from "./namespaces/dropbox.js";
|
|
18
|
-
import { FeedNamespace } from "./namespaces/feed.js";
|
|
19
|
-
import { GoogleDriveNamespace } from "./namespaces/googledrive.js";
|
|
20
|
-
import { GraphNamespace } from "./namespaces/graph.js";
|
|
21
|
-
import { LikeNamespace } from "./namespaces/like.js";
|
|
22
|
-
import { MirrorNamespace } from "./namespaces/mirror.js";
|
|
23
|
-
import { PlayerNamespace } from "./namespaces/player.js";
|
|
24
|
-
import { PlaylistNamespace } from "./namespaces/playlist.js";
|
|
25
|
-
import { ScrobbleNamespace } from "./namespaces/scrobble.js";
|
|
26
|
-
import { ShoutNamespace } from "./namespaces/shout.js";
|
|
27
|
-
import { SongNamespace } from "./namespaces/song.js";
|
|
28
|
-
import { SpotifyNamespace } from "./namespaces/spotify.js";
|
|
29
|
-
import { StatsNamespace } from "./namespaces/stats.js";
|
|
30
|
-
import type { Endpoints } from "./generated/types.js";
|
|
31
|
-
import type {
|
|
32
|
-
AuthProvider,
|
|
33
|
-
ClientOptions,
|
|
34
|
-
FetchLike,
|
|
35
|
-
RequestOptions,
|
|
36
|
-
} from "./types.js";
|
|
37
|
-
|
|
38
|
-
type XrpcOpts = {
|
|
39
|
-
params?: Record<string, unknown>;
|
|
40
|
-
body?: unknown;
|
|
41
|
-
requireAuth?: boolean;
|
|
42
|
-
} & RequestOptions;
|
|
1
|
+
import { Client, simpleFetchHandler } from "@atcute/client";
|
|
43
2
|
|
|
3
|
+
import { RockskyError } from "./errors.js";
|
|
4
|
+
import type {
|
|
5
|
+
ActorProfileViewDetailed,
|
|
6
|
+
AlbumViewBasic,
|
|
7
|
+
ArtistViewBasic,
|
|
8
|
+
FeedSearchResultsView,
|
|
9
|
+
GetActorAlbumsOutput,
|
|
10
|
+
GetActorArtistsOutput,
|
|
11
|
+
GetActorScrobblesOutput,
|
|
12
|
+
GetActorSongsOutput,
|
|
13
|
+
GetTopArtistsOutput,
|
|
14
|
+
GetTopTracksOutput,
|
|
15
|
+
ScrobbleViewBasic,
|
|
16
|
+
SongViewBasic,
|
|
17
|
+
StatsGlobalStatsView,
|
|
18
|
+
} from "./generated/types.js";
|
|
19
|
+
|
|
20
|
+
/** The default public Rocksky AppView base URL. */
|
|
21
|
+
export const DEFAULT_APPVIEW = "https://api.rocksky.app";
|
|
22
|
+
|
|
23
|
+
/** Unauthenticated read client over the public Rocksky AppView XRPC. */
|
|
44
24
|
export class RockskyClient {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
readonly actor: ActorNamespace;
|
|
48
|
-
readonly album: AlbumNamespace;
|
|
49
|
-
readonly apikey: ApikeyNamespace;
|
|
50
|
-
readonly artist: ArtistNamespace;
|
|
51
|
-
readonly charts: ChartsNamespace;
|
|
52
|
-
readonly dropbox: DropboxNamespace;
|
|
53
|
-
readonly feed: FeedNamespace;
|
|
54
|
-
readonly googledrive: GoogleDriveNamespace;
|
|
55
|
-
readonly graph: GraphNamespace;
|
|
56
|
-
readonly like: LikeNamespace;
|
|
57
|
-
readonly mirror: MirrorNamespace;
|
|
58
|
-
readonly player: PlayerNamespace;
|
|
59
|
-
readonly playlist: PlaylistNamespace;
|
|
60
|
-
readonly scrobble: ScrobbleNamespace;
|
|
61
|
-
readonly shout: ShoutNamespace;
|
|
62
|
-
readonly song: SongNamespace;
|
|
63
|
-
readonly spotify: SpotifyNamespace;
|
|
64
|
-
readonly stats: StatsNamespace;
|
|
25
|
+
private rpc: Client;
|
|
65
26
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.actor = new ActorNamespace(call);
|
|
71
|
-
this.album = new AlbumNamespace(call);
|
|
72
|
-
this.apikey = new ApikeyNamespace(call);
|
|
73
|
-
this.artist = new ArtistNamespace(call);
|
|
74
|
-
this.charts = new ChartsNamespace(call);
|
|
75
|
-
this.dropbox = new DropboxNamespace(call);
|
|
76
|
-
this.feed = new FeedNamespace(call);
|
|
77
|
-
this.googledrive = new GoogleDriveNamespace(call);
|
|
78
|
-
this.graph = new GraphNamespace(call);
|
|
79
|
-
this.like = new LikeNamespace(call);
|
|
80
|
-
this.mirror = new MirrorNamespace(call);
|
|
81
|
-
this.player = new PlayerNamespace(call);
|
|
82
|
-
this.playlist = new PlaylistNamespace(call);
|
|
83
|
-
this.scrobble = new ScrobbleNamespace(call);
|
|
84
|
-
this.shout = new ShoutNamespace(call);
|
|
85
|
-
this.song = new SongNamespace(call);
|
|
86
|
-
this.spotify = new SpotifyNamespace(call);
|
|
87
|
-
this.stats = new StatsNamespace(call);
|
|
27
|
+
/** Build a read client against an AppView base URL (defaults to {@link DEFAULT_APPVIEW}). */
|
|
28
|
+
constructor(appview: string = DEFAULT_APPVIEW) {
|
|
29
|
+
this.rpc = new Client({ handler: simpleFetchHandler({ service: appview }) });
|
|
88
30
|
}
|
|
89
31
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
32
|
+
private async query<T>(nsid: string, params: Record<string, unknown>): Promise<T> {
|
|
33
|
+
const clean: Record<string, unknown> = {};
|
|
34
|
+
for (const [k, v] of Object.entries(params)) {
|
|
35
|
+
if (v !== undefined && v !== "") clean[k] = v;
|
|
36
|
+
}
|
|
37
|
+
const res = await this.rpc.get(nsid as never, { params: clean } as never);
|
|
38
|
+
if (!res.ok) throw new RockskyError(res.data);
|
|
39
|
+
return res.data as T;
|
|
96
40
|
}
|
|
97
41
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
...this.optionsSnapshot(),
|
|
102
|
-
baseUrl,
|
|
103
|
-
});
|
|
42
|
+
/** An actor's detailed profile. `actor` is a handle or DID. */
|
|
43
|
+
profile(actor: string): Promise<ActorProfileViewDetailed> {
|
|
44
|
+
return this.query("app.rocksky.actor.getProfile", { did: actor });
|
|
104
45
|
}
|
|
105
46
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*
|
|
113
|
-
* Defaults `baseUrl` to this client's base URL. Override anything by
|
|
114
|
-
* passing the option, or use `RealtimeClient.builder()` for full control.
|
|
115
|
-
*/
|
|
116
|
-
realtime(
|
|
117
|
-
options: Omit<RealtimeOptions, "baseUrl"> &
|
|
118
|
-
Partial<Pick<RealtimeOptions, "baseUrl">>,
|
|
119
|
-
): RealtimeClient {
|
|
120
|
-
return createRealtimeClient({
|
|
121
|
-
baseUrl: this.config.baseUrl,
|
|
122
|
-
...options,
|
|
47
|
+
/** An actor's scrobbles, newest first. */
|
|
48
|
+
async scrobbles(actor: string, limit = 50, offset = 0): Promise<ScrobbleViewBasic[]> {
|
|
49
|
+
const out = await this.query<GetActorScrobblesOutput>("app.rocksky.actor.getActorScrobbles", {
|
|
50
|
+
did: actor,
|
|
51
|
+
limit,
|
|
52
|
+
offset,
|
|
123
53
|
});
|
|
54
|
+
return out.scrobbles ?? [];
|
|
124
55
|
}
|
|
125
56
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*/
|
|
135
|
-
paginate<T>(args: PaginateArgs<T>) {
|
|
136
|
-
return paginateFn(args);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Direct escape hatch — call any XRPC endpoint by NSID.
|
|
141
|
-
*
|
|
142
|
-
* Known NSIDs (string literals) are typed via the generated `Endpoints`
|
|
143
|
-
* map; arbitrary strings fall back to `unknown` (override with `<T>`).
|
|
144
|
-
*/
|
|
145
|
-
xrpc<K extends keyof Endpoints>(
|
|
146
|
-
nsid: K,
|
|
147
|
-
method?: "GET" | "POST",
|
|
148
|
-
opts?: XrpcOpts,
|
|
149
|
-
): Promise<Endpoints[K]>;
|
|
150
|
-
xrpc<T = unknown>(
|
|
151
|
-
nsid: string,
|
|
152
|
-
method?: "GET" | "POST",
|
|
153
|
-
opts?: XrpcOpts,
|
|
154
|
-
): Promise<T>;
|
|
155
|
-
xrpc(
|
|
156
|
-
nsid: string,
|
|
157
|
-
method: "GET" | "POST" = "GET",
|
|
158
|
-
opts: XrpcOpts = {},
|
|
159
|
-
): Promise<unknown> {
|
|
160
|
-
return xrpcCall(this.config, nsid, method, opts);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
static builder(): RockskyClientBuilder {
|
|
164
|
-
return new RockskyClientBuilder();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private optionsSnapshot(): ClientOptions {
|
|
168
|
-
return {
|
|
169
|
-
baseUrl: this.config.baseUrl,
|
|
170
|
-
auth: this.config.auth,
|
|
171
|
-
fetch: this.config.fetch,
|
|
172
|
-
headers: this.config.headers,
|
|
173
|
-
timeoutMs: this.config.timeoutMs,
|
|
174
|
-
retries: this.config.retries,
|
|
175
|
-
retryDelayMs: this.config.retryDelayMs,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Fluent builder.
|
|
182
|
-
*
|
|
183
|
-
* const client = RockskyClient.builder()
|
|
184
|
-
* .baseUrl("https://api.rocksky.app")
|
|
185
|
-
* .auth(() => loadToken())
|
|
186
|
-
* .timeout(10_000)
|
|
187
|
-
* .retries(3)
|
|
188
|
-
* .userAgent("my-app/1.0")
|
|
189
|
-
* .build();
|
|
190
|
-
*/
|
|
191
|
-
export class RockskyClientBuilder {
|
|
192
|
-
private readonly opts: ClientOptions = {};
|
|
193
|
-
|
|
194
|
-
baseUrl(url: string): this {
|
|
195
|
-
this.opts.baseUrl = url;
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
auth(auth: AuthProvider): this {
|
|
200
|
-
this.opts.auth = auth;
|
|
201
|
-
return this;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
bearer(token: string): this {
|
|
205
|
-
this.opts.auth = token;
|
|
206
|
-
return this;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
fetch(impl: FetchLike): this {
|
|
210
|
-
this.opts.fetch = impl;
|
|
211
|
-
return this;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
header(key: string, value: string): this {
|
|
215
|
-
this.opts.headers = { ...this.opts.headers, [key]: value };
|
|
216
|
-
return this;
|
|
57
|
+
/** An actor's most-played songs. */
|
|
58
|
+
async songs(actor: string, limit = 50, offset = 0): Promise<SongViewBasic[]> {
|
|
59
|
+
const out = await this.query<GetActorSongsOutput>("app.rocksky.actor.getActorSongs", {
|
|
60
|
+
did: actor,
|
|
61
|
+
limit,
|
|
62
|
+
offset,
|
|
63
|
+
});
|
|
64
|
+
return out.songs ?? [];
|
|
217
65
|
}
|
|
218
66
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
67
|
+
/** An actor's most-played albums. */
|
|
68
|
+
async albums(actor: string, limit = 50, offset = 0): Promise<AlbumViewBasic[]> {
|
|
69
|
+
const out = await this.query<GetActorAlbumsOutput>("app.rocksky.actor.getActorAlbums", {
|
|
70
|
+
did: actor,
|
|
71
|
+
limit,
|
|
72
|
+
offset,
|
|
73
|
+
});
|
|
74
|
+
return out.albums ?? [];
|
|
222
75
|
}
|
|
223
76
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
77
|
+
/** An actor's most-played artists. */
|
|
78
|
+
async artists(actor: string, limit = 50, offset = 0): Promise<ArtistViewBasic[]> {
|
|
79
|
+
const out = await this.query<GetActorArtistsOutput>("app.rocksky.actor.getActorArtists", {
|
|
80
|
+
did: actor,
|
|
81
|
+
limit,
|
|
82
|
+
offset,
|
|
83
|
+
});
|
|
84
|
+
return out.artists ?? [];
|
|
227
85
|
}
|
|
228
86
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
87
|
+
/** The platform-wide top tracks chart. */
|
|
88
|
+
async topTracks(limit = 50, offset = 0): Promise<SongViewBasic[]> {
|
|
89
|
+
const out = await this.query<GetTopTracksOutput>("app.rocksky.charts.getTopTracks", { limit, offset });
|
|
90
|
+
return out.tracks ?? [];
|
|
232
91
|
}
|
|
233
92
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
93
|
+
/** The platform-wide top artists chart. */
|
|
94
|
+
async topArtists(limit = 50, offset = 0): Promise<ArtistViewBasic[]> {
|
|
95
|
+
const out = await this.query<GetTopArtistsOutput>("app.rocksky.charts.getTopArtists", { limit, offset });
|
|
96
|
+
return out.artists ?? [];
|
|
237
97
|
}
|
|
238
98
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return this;
|
|
99
|
+
/** Full-text search across songs, albums, artists, playlists, actors. */
|
|
100
|
+
search(query: string): Promise<FeedSearchResultsView> {
|
|
101
|
+
return this.query("app.rocksky.feed.search", { query });
|
|
242
102
|
}
|
|
243
103
|
|
|
244
|
-
|
|
245
|
-
|
|
104
|
+
/** Platform-wide totals. */
|
|
105
|
+
globalStats(): Promise<StatsGlobalStatsView> {
|
|
106
|
+
return this.query("app.rocksky.stats.getGlobalStats", {});
|
|
246
107
|
}
|
|
247
108
|
}
|
|
248
|
-
|
|
249
|
-
/** Convenience factory — equivalent to `new RockskyClient(options)`. */
|
|
250
|
-
export function createClient(options: ClientOptions = {}): RockskyClient {
|
|
251
|
-
return new RockskyClient(options);
|
|
252
|
-
}
|